diff --git a/felsokning/app/src/felsokning/__tests__/slutsats.test.ts b/felsokning/app/src/felsokning/__tests__/slutsats.test.ts index 090aaf7..d10317e 100644 --- a/felsokning/app/src/felsokning/__tests__/slutsats.test.ts +++ b/felsokning/app/src/felsokning/__tests__/slutsats.test.ts @@ -17,6 +17,12 @@ import { granskaSlutsats, obemottaHypoteser, } from "../../../../services/gemensam/motivering.mjs"; +import { ICKESVAR_PER_SPRAK } from "../../../../services/gemensam/sprak/ord.mjs"; + +// Fältet anges numera med sin katalognyckel, inte med en svensk etikett. +// Testerna prövar därför mönster som gäller på engelska — standardspråket +// — och språkberoendet prövas för sig längst ned. +const MOT = "slutsats.falt.motivering"; const GILTIG = { typ: "slutsats", @@ -34,8 +40,8 @@ const ARBETE = [{ typ: "atgard_utford", beskrivning: "Bytte bränslepumpsrelä." describe("motiveringen måste vara ett skäl, inte en upprepning", () => { it("avvisar tomt fält", () => { - expect(granskaFalt("Motivering", "")).toMatch(/saknas/); - expect(granskaFalt("Motivering", " ")).toMatch(/saknas/); + expect(granskaFalt(MOT, "")).toMatch(/is missing/); + expect(granskaFalt(MOT, " ")).toMatch(/is missing/); }); it.each([ @@ -53,11 +59,11 @@ describe("motiveringen måste vara ett skäl, inte en upprepning", () => { "-", "n/a", ])("avvisar icke-svaret %s", (text) => { - expect(granskaFalt("Motivering", text)).toMatch(/är inget skäl/); + expect(granskaFalt(MOT, text)).toMatch(/is not a reason/); }); it("avvisar text som är för kort för att gå att granska", () => { - expect(granskaFalt("Motivering", "Reläet var dåligt", { minsta: MINSTA_MOTIVERING })).toMatch(/för kort/); + expect(granskaFalt(MOT, "Reläet var dåligt", { minsta: MINSTA_MOTIVERING })).toMatch(/too short/); }); it("avvisar en omformulering av slutsatsen utan orsakssamband", () => { @@ -65,15 +71,15 @@ describe("motiveringen måste vara ett skäl, inte en upprepning", () => { // svåraste att fånga — texten ser fullgod ut tills man frågar vad // den egentligen påstår. const fel = granskaFalt( - "Motivering", + MOT, "Reläet till bränslepumpen fungerade inte som det ska och byttes därmed ut mot ett nytt relä.", { minsta: MINSTA_MOTIVERING, kravOrsak: true }, ); - expect(fel).toMatch(/anger vad, men inte varför/); + expect(fel).toMatch(/states what, but not why/); }); it("godtar ett resonemang med orsakssamband", () => { - expect(granskaFalt("Motivering", GILTIG.motivering, { minsta: MINSTA_MOTIVERING, kravOrsak: true })).toBeNull(); + expect(granskaFalt(MOT, GILTIG.motivering, { minsta: MINSTA_MOTIVERING, kravOrsak: true })).toBeNull(); }); it("godtar konkreta mätvärden som evidensreferens även utan orsaksord", () => { @@ -81,7 +87,7 @@ describe("motiveringen måste vara ett skäl, inte en upprepning", () => { // utan att säga ordet mätning. Regeln får inte tvinga fram ett // språkbruk som inte är teknikerns. expect( - granskaFalt("Motivering", "Stift 14 låg på 12,4 V med tändning på, stift 7 på 0,03 ohm mot jord.", { + granskaFalt(MOT, "Stift 14 låg på 12,4 V med tändning på, stift 7 på 0,03 ohm mot jord.", { minsta: MINSTA_MOTIVERING, kravOrsak: true, }), @@ -93,7 +99,8 @@ describe("hela slutsatsen inför avslut", () => { it("saknad slutsats spärrar avslutet", () => { const brister = granskaSlutsats(undefined, []); expect(brister).toHaveLength(1); - expect(brister[0].text).toMatch(/kan inte avslutas utan en slutsats/); + expect(brister[0].nyckel).toBe("slutsats.utan_slutsats"); + expect(brister[0].text).toMatch(/cannot be closed without a closing statement/); }); it("en fullständig slutsats passerar", () => { @@ -134,7 +141,7 @@ describe("dokumenterade hypoteser måste bemötas", () => { it("en obemött hypotes spärrar avslutet", () => { const brister = granskaSlutsats(GILTIG, [...ARBETE, HYPOTES]); - expect(brister.map((b) => b.text).join(" ")).toMatch(/finns i loggen men bemöts inte/); + expect(brister.map((b) => b.text).join(" ")).toMatch(/is in the log but is not addressed/); }); it("hypotesen är bemött när uteslutandet nämner den", () => { @@ -162,7 +169,7 @@ describe("dokumenterade hypoteser måste bemötas", () => { HYPOTES, { typ: "hypotes", text: "Möjlig obalans i framhjulen", niva: "lag" }, ]; - const brister = granskaSlutsats(GILTIG, logg).filter((b) => b.text.includes("bemöts inte")); + const brister = granskaSlutsats(GILTIG, logg).filter((b) => b.nyckel === "slutsats.hypotes_obemott"); expect(brister).toHaveLength(2); }); @@ -318,3 +325,80 @@ describe("kundbeskedet går att registrera även när åtgärden skrevs först", expect(kod).toContain("{forslag.length > 0 && !beslut && ("); }); }); + +// ---- Regeln gällde bara på svenska (ALVA-SPEC-060) ---------------------- +// +// Icke-svarslistan var enbart svensk. En tysk tekniker som skrev +// "erledigt" i motiveringsfältet passerade filtret — inte därför att +// texten dög, utan därför att filtret bara kände igen "klart". Grinden +// släppte igenom avslutet och rapporten såg fullständig ut. +// +// Det var alltså inte en översättningsbrist utan ett hål i den regel som +// är hela produktens värde, och det gällde varje marknad utom en. +describe("icke-svar fångas oavsett språk", () => { + const SPRAKPROV: [string, string][] = [ + ["de", "erledigt"], + ["en", "done"], + ["fr", "réparé"], + ["es", "hecho"], + ["it", "fatto"], + ["pl", "zrobione"], + ["nl", "klaar"], + ["pt", "feito"], + ["ro", "gata"], + ["sv", "klart"], + ]; + + it.each(SPRAKPROV)("%s: %s är inget skäl", (_kod, text) => { + expect(granskaFalt(MOT, text)).toMatch(/is not a reason/); + }); + + it("varje språk har en egen lista, inte en kopia av svenskan", () => { + // Motvikt mot provet ovan: det hade också passerat om alla listor + // vore samma svenska lista och proven råkade stå i den. + const per = ICKESVAR_PER_SPRAK as Record; + for (const [kod, lista] of Object.entries(per)) { + expect(lista.length, kod).toBeGreaterThan(15); + if (kod === "sv") continue; + const egna = lista.filter((o) => !per.sv.includes(o)); + expect(egna.length, kod).toBeGreaterThan(10); + } + }); + + it("ett giltigt kort svar är fortfarande giltigt", () => { + // "Inget kvarstår" får inte fångas som icke-svar. Att tvinga fram + // utfyllnad är sämre än ett kort sant svar — se sprak/ord.mjs. + expect(granskaFalt("slutsats.falt.kvarstaende", "Inget", { minsta: 5 })).toBeNull(); + expect(granskaFalt("slutsats.falt.kvarstaende", "Nothing", { minsta: 5 })).toBeNull(); + expect(granskaFalt("slutsats.falt.kvarstaende", "Keine", { minsta: 5 })).toBeNull(); + }); + + it("orsakssamband känns igen på andra språk än svenska", () => { + // Utan det här nekas en tysk eller polsk tekniker avslut på ett + // korrekt skrivet underlag — vilket är värre än att släppa igenom + // ett dåligt, eftersom det gör spärren till något man kringgår. + const prov = [ + "Der Relaiskontakt war eingebrannt, was den Startaussetzer erklärt und im Foto zu sehen ist.", + "The relay contact was pitted, which explains the intermittent no-start; the photo shows the arcing.", + "Styk przekaźnika był wypalony, dlatego rozrusznik nie zawsze działał, co widać na zdjęciu.", + ]; + for (const text of prov) { + expect(granskaFalt(MOT, text, { minsta: MINSTA_MOTIVERING, kravOrsak: true }), text).toBeNull(); + } + }); +}); + +describe("slutsatsen talar organisationens språk", () => { + it("samma brist, olika språk, samma nyckel", () => { + const en = granskaSlutsats(undefined, [])[0]; + const de = granskaSlutsats(undefined, [], "de")[0]; + expect(de.nyckel).toBe(en.nyckel); + expect(de.text).not.toBe(en.text); + expect(de.text).toMatch(/Abschlussfeststellung/); + }); + + it("fältnamnet översätts med resten av meningen", () => { + expect(granskaFalt(MOT, "", { sprak: "de" })).toBe("Begründung fehlt."); + expect(granskaFalt(MOT, "", { sprak: "sv" })).toBe("Motivering saknas."); + }); +}); diff --git a/felsokning/services/gemensam/grind.mjs b/felsokning/services/gemensam/grind.mjs index 8209d63..01d867c 100644 --- a/felsokning/services/gemensam/grind.mjs +++ b/felsokning/services/gemensam/grind.mjs @@ -177,8 +177,8 @@ export function grinda(handelser, metodik, sprak = STANDARD) { // varför det ena medför det andra. Det är precis det steget en // garantihandläggare eller försäkringsbedömare behöver granska. const slutsats = av(handelser, "slutsats").at(-1); - for (const brist of granskaSlutsats(slutsats, handelser)) { - hinder.push({ id: `slutsats_${brist.falt}`, rubrik: brist.text }); + for (const brist of granskaSlutsats(slutsats, handelser, sprak)) { + hinder.push({ id: `slutsats_${brist.falt}`, nyckel: brist.nyckel, rubrik: brist.text }); } // Säkerhetsspärrar. Ett nekande svar på en spärrfråga får aldrig diff --git a/felsokning/services/gemensam/motivering.mjs b/felsokning/services/gemensam/motivering.mjs index 0420658..0bba7a0 100644 --- a/felsokning/services/gemensam/motivering.mjs +++ b/felsokning/services/gemensam/motivering.mjs @@ -44,69 +44,8 @@ // kunde det. Det är också ett varför, och ofta ett mer användbart än en // påhittad orsak. -/** Fraser som ser ut som svar men inte är det. */ -const ICKESVAR = [ - "klart", - "ok", - "okej", - "åtgärdat", - "atgardat", - "fixat", - "se ovan", - "se logg", - "enligt ovan", - "som vanligt", - "trasig", - "defekt", - "sliten", - "utsliten", - "behöver bytas", - "behover bytas", - "byttes", - "bytt", - "enligt kund", - "kunden sa", - "vet ej", - "ingen aning", - "diverse", - "allmänt slitage", - "-", - "n/a", - "na", -]; - -// Ett resonemang innehåller ett därför. Utan orsakssamband är texten en -// omformulering av slutsatsen, inte ett skäl för den. -const ORSAKSORD = [ - "eftersom", - "därför", - "darfor", - "beror", - "orsakas", - "orsakade", - "medför", - "medfor", - "leder till", - "ledde till", - "vilket", - "på grund av", - "pga", - "resulterar", - "resulterade", - "innebär", - "innebar", - "följaktligen", - "foljaktligen", - "till följd av", - "förklarar", - "forklarar", - "stämmer överens", - "stammer overens", - "bekräftas av", - "bekraftas av", - "visar att", - "utesluter", -]; +import { EVIDENSORD, ICKESVAR, MINSTA_ORSAKSORD, ORSAKSORD } from "./sprak/ord.mjs"; +import { STANDARD, t } from "./sprak/index.mjs"; export const MINSTA_MOTIVERING = 40; export const MINSTA_KORT = 15; @@ -119,9 +58,14 @@ function ärIckesvar(text) { return ICKESVAR.includes(n); } +// Ett resonemang innehåller ett därför. Utan orsakssamband är texten en +// omformulering av slutsatsen, inte ett skäl för den. +// +// Orden jämförs som delsträngar och kommer från alla språk samtidigt — +// se sprak/ord.mjs för varför unionen är rätt riktning att fela åt. function harOrsakssamband(text) { const n = normalisera(text); - return ORSAKSORD.some((ord) => n.includes(ord)); + return ORSAKSORD.some((ord) => ord.length >= MINSTA_ORSAKSORD && n.includes(ord)); } /** @@ -132,26 +76,24 @@ function harOrsakssamband(text) { */ function refererarEvidens(text) { const n = normalisera(text); - const ord = ["mät", "mat", "foto", "bild", "video", "felkod", "avläs", "avlas", "prov", "test", "dokument", "historik", "kontroll"]; const värde = /\d+([.,]\d+)?\s*(v|volt|a|ampere|ω|ohm|bar|kpa|mpa|nm|°c|c|mm|km|mil|%|g|hz|rpm|varv)\b/; - return ord.some((o) => n.includes(o)) || värde.test(n); + return EVIDENSORD.some((o) => n.includes(o)) || värde.test(n); } /** * Granskar ett fält i slutsatsen. * @returns null när fältet duger, annars en förklaring till teknikern. */ -export function granskaFalt(namn, text, { minsta = MINSTA_KORT, kravOrsak = false } = {}) { +export function granskaFalt(faltNyckel, text, { minsta = MINSTA_KORT, kravOrsak = false, sprak = STANDARD } = {}) { const rå = String(text ?? "").trim(); - if (!rå) return `${namn} saknas.`; - if (ärIckesvar(rå)) { - return `${namn}: "${rå}" är inget skäl. Beskriv vad som faktiskt gäller och varför.`; - } + const falt = t(sprak, faltNyckel); + if (!rå) return t(sprak, "slutsats.saknas", { falt }); + if (ärIckesvar(rå)) return t(sprak, "slutsats.ickesvar.falt", { falt, text: rå }); if (rå.length < minsta) { - return `${namn} är för kort (${rå.length} av minst ${minsta} tecken) för att gå att granska i efterhand.`; + return t(sprak, "slutsats.for_kort", { falt, langd: rå.length, minsta }); } if (kravOrsak && !harOrsakssamband(rå) && !refererarEvidens(rå)) { - return `${namn} anger vad, men inte varför. Knyt slutsatsen till underlaget — vad i evidensen gör att det här följer?`; + return t(sprak, "slutsats.utan_varfor", { falt }); } return null; } @@ -193,48 +135,42 @@ export function obemottaHypoteser(handelser, slutsats) { * @param handelser hela loggen, för de regler som härleds ur den * @returns lista med brister; tom lista betyder godkänd */ -export function granskaSlutsats(slutsats, handelser = []) { +export function granskaSlutsats(slutsats, handelser = [], sprak = STANDARD) { const brister = []; const lägg = (fält, text) => text && brister.push({ falt: fält, text }); + const g = (nyckel, text, val) => granskaFalt(nyckel, text, { ...val, sprak }); if (!slutsats) { - return [ - { - falt: "slutsats", - text: "Ärendet kan inte avslutas utan en slutsats. Motivera varför slutsatsen följer av underlaget.", - }, - ]; + return [{ falt: "slutsats", nyckel: "slutsats.utan_slutsats", text: t(sprak, "slutsats.utan_slutsats") }]; } const fastställd = slutsats.orsakFastställd !== false; if (fastställd) { - lägg("motivering", granskaFalt("Motivering", slutsats.motivering, { minsta: MINSTA_MOTIVERING, kravOrsak: true })); + lägg("motivering", g("slutsats.falt.motivering", slutsats.motivering, { minsta: MINSTA_MOTIVERING, kravOrsak: true })); } else { // Den ärliga vägen. Att orsaken inte kunnat fastställas är ett // giltigt utfall — men varför den inte kunnat det är fortfarande ett // varför, och det måste stå där. lägg( "motivering", - granskaFalt("Skäl till att orsaken inte fastställts", slutsats.motivering, { - minsta: MINSTA_MOTIVERING, - kravOrsak: true, - }), + g("slutsats.falt.motivering_ej", slutsats.motivering, { minsta: MINSTA_MOTIVERING, kravOrsak: true }), ); } - lägg("uteslutet", granskaFalt("Uteslutna alternativ", slutsats.uteslutet, { minsta: MINSTA_KORT })); - lägg("kvarstaende", granskaFalt("Kvarstående osäkerhet", slutsats.kvarstaende, { minsta: 5 })); + lägg("uteslutet", g("slutsats.falt.uteslutet", slutsats.uteslutet, { minsta: MINSTA_KORT })); + lägg("kvarstaende", g("slutsats.falt.kvarstaende", slutsats.kvarstaende, { minsta: 5 })); const utfördArbete = handelser.some((h) => h?.typ === "atgard_utford" && h.utford === true); if (utfördArbete) { - lägg("atgardsval", granskaFalt("Val av åtgärd", slutsats.atgardsval, { minsta: MINSTA_KORT, kravOrsak: true })); + lägg("atgardsval", g("slutsats.falt.atgardsval", slutsats.atgardsval, { minsta: MINSTA_KORT, kravOrsak: true })); } for (const text of obemottaHypoteser(handelser, slutsats)) { brister.push({ falt: "uteslutet", - text: `Hypotesen "${text}" finns i loggen men bemöts inte. Ange varför den uteslöts, eller varför den kvarstår.`, + nyckel: "slutsats.hypotes_obemott", + text: t(sprak, "slutsats.hypotes_obemott", { text }), }); } diff --git a/felsokning/services/gemensam/sprak/de.mjs b/felsokning/services/gemensam/sprak/de.mjs index c0098c8..0cd1fee 100644 --- a/felsokning/services/gemensam/sprak/de.mjs +++ b/felsokning/services/gemensam/sprak/de.mjs @@ -62,6 +62,18 @@ export const DE = { "slutsats.osakert": "Was unsicher bleibt", "slutsats.ickesvar": "Das ist keine Feststellung. Angeben, was festgestellt wurde und welche Evidenz dies trägt.", + "slutsats.falt.motivering": "Begründung", + "slutsats.falt.motivering_ej": "Grund, warum die Ursache nicht festgestellt wurde", + "slutsats.falt.uteslutet": "Verworfene Alternativen", + "slutsats.falt.kvarstaende": "Verbleibende Unsicherheit", + "slutsats.falt.atgardsval": "Wahl der Maßnahme", + "slutsats.saknas": "{falt} fehlt.", + "slutsats.ickesvar.falt": "{falt}: „{text}“ ist keine Begründung. Angeben, was tatsächlich gilt, und warum.", + "slutsats.for_kort": "{falt} ist zu kurz ({langd} von mindestens {minsta} Zeichen), um im Nachhinein prüfbar zu sein.", + "slutsats.utan_varfor": "{falt} nennt das Was, nicht das Warum. Die Feststellung an die Evidenz binden — was darin lässt dies folgen?", + "slutsats.utan_slutsats": "Der Vorgang kann ohne Abschlussfeststellung nicht abgeschlossen werden. Angeben, warum die Feststellung aus der Evidenz folgt.", + "slutsats.hypotes_obemott": "Die Hypothese „{text}“ steht im Protokoll, wird aber nicht behandelt. Angeben, warum sie verworfen wurde oder warum sie offen bleibt.", + // ---- Vorgangsablauf ----------------------------------------------------- "arende.nytt": "Neuer Vorgang", diff --git a/felsokning/services/gemensam/sprak/en.mjs b/felsokning/services/gemensam/sprak/en.mjs index 5e62e5e..9427423 100644 --- a/felsokning/services/gemensam/sprak/en.mjs +++ b/felsokning/services/gemensam/sprak/en.mjs @@ -88,6 +88,18 @@ export const EN = { "slutsats.avfardat": "Which hypotheses were dismissed, and why", "slutsats.osakert": "What remains uncertain", "slutsats.ickesvar": "That is not an answer. State what was established and which evidence carries it.", + "slutsats.falt.motivering": "Rationale", + "slutsats.falt.motivering_ej": "Reason the cause could not be established", + "slutsats.falt.uteslutet": "Dismissed alternatives", + "slutsats.falt.kvarstaende": "Remaining uncertainty", + "slutsats.falt.atgardsval": "Choice of action", + "slutsats.saknas": "{falt} is missing.", + "slutsats.ickesvar.falt": "{falt}: “{text}” is not a reason. State what actually applies, and why.", + "slutsats.for_kort": "{falt} is too short ({langd} of at least {minsta} characters) to be reviewable afterwards.", + "slutsats.utan_varfor": "{falt} states what, but not why. Tie the conclusion to the evidence — what in it makes this follow?", + "slutsats.utan_slutsats": "The case cannot be closed without a closing statement. State why the conclusion follows from the evidence.", + "slutsats.hypotes_obemott": "The hypothesis “{text}” is in the log but is not addressed. State why it was dismissed, or why it remains open.", + // ---- Case flow ----------------------------------------------------------- "arende.nytt": "New case", diff --git a/felsokning/services/gemensam/sprak/es.mjs b/felsokning/services/gemensam/sprak/es.mjs index 7645cfb..1a124e3 100644 --- a/felsokning/services/gemensam/sprak/es.mjs +++ b/felsokning/services/gemensam/sprak/es.mjs @@ -60,6 +60,18 @@ export const ES = { "slutsats.avfardat": "Qué hipótesis se han descartado, y por qué", "slutsats.osakert": "Qué sigue siendo incierto", "slutsats.ickesvar": "Eso no es una conclusión. Indicar qué se ha establecido y qué evidencia lo sustenta.", + "slutsats.falt.motivering": "Justificación", + "slutsats.falt.motivering_ej": "Motivo por el que no se pudo establecer la causa", + "slutsats.falt.uteslutet": "Alternativas descartadas", + "slutsats.falt.kvarstaende": "Incertidumbre restante", + "slutsats.falt.atgardsval": "Elección de la acción", + "slutsats.saknas": "Falta {falt}.", + "slutsats.ickesvar.falt": "{falt}: «{text}» no es un motivo. Indicar qué se aplica realmente, y por qué.", + "slutsats.for_kort": "{falt} es demasiado corto ({langd} de al menos {minsta} caracteres) para poder revisarse después.", + "slutsats.utan_varfor": "{falt} indica el qué, pero no el porqué. Vincular la conclusión con la evidencia — ¿qué hay en ella que la sustente?", + "slutsats.utan_slutsats": "El expediente no se puede cerrar sin una conclusión. Indicar por qué la conclusión se desprende de la evidencia.", + "slutsats.hypotes_obemott": "La hipótesis «{text}» figura en el registro pero no se aborda. Indicar por qué se descartó, o por qué sigue abierta.", + // ---- Flujo del expediente ----------------------------------------------- "arende.nytt": "Nuevo expediente", diff --git a/felsokning/services/gemensam/sprak/fr.mjs b/felsokning/services/gemensam/sprak/fr.mjs index eb1ce93..ee5a87b 100644 --- a/felsokning/services/gemensam/sprak/fr.mjs +++ b/felsokning/services/gemensam/sprak/fr.mjs @@ -61,6 +61,18 @@ export const FR = { "slutsats.avfardat": "Quelles hypothèses ont été écartées, et pourquoi", "slutsats.osakert": "Ce qui reste incertain", "slutsats.ickesvar": "Ce n'est pas une conclusion. Indiquer ce qui est établi et quelles preuves l'étayent.", + "slutsats.falt.motivering": "Justification", + "slutsats.falt.motivering_ej": "Motif pour lequel la cause n'a pas pu être établie", + "slutsats.falt.uteslutet": "Alternatives écartées", + "slutsats.falt.kvarstaende": "Incertitude restante", + "slutsats.falt.atgardsval": "Choix de l'action", + "slutsats.saknas": "{falt} est absent.", + "slutsats.ickesvar.falt": "{falt} : « {text} » n'est pas un motif. Indiquer ce qui s'applique réellement, et pourquoi.", + "slutsats.for_kort": "{falt} est trop court ({langd} sur au moins {minsta} caractères) pour être vérifiable après coup.", + "slutsats.utan_varfor": "{falt} indique le quoi, mais pas le pourquoi. Relier la conclusion aux preuves — qu'est-ce qui, dans celles-ci, la rend fondée ?", + "slutsats.utan_slutsats": "Le dossier ne peut pas être clôturé sans conclusion. Indiquer pourquoi la conclusion découle des preuves.", + "slutsats.hypotes_obemott": "L'hypothèse « {text} » figure dans le journal mais n'est pas traitée. Indiquer pourquoi elle a été écartée, ou pourquoi elle reste ouverte.", + // ---- Flux du dossier ---------------------------------------------------- "arende.nytt": "Nouveau dossier", diff --git a/felsokning/services/gemensam/sprak/it.mjs b/felsokning/services/gemensam/sprak/it.mjs index db0ce3c..64041d8 100644 --- a/felsokning/services/gemensam/sprak/it.mjs +++ b/felsokning/services/gemensam/sprak/it.mjs @@ -61,6 +61,18 @@ export const IT = { "slutsats.osakert": "Che cosa resta incerto", "slutsats.ickesvar": "Questa non è una conclusione. Indicare che cosa è stato accertato e quale evidenza lo sostiene.", + "slutsats.falt.motivering": "Motivazione", + "slutsats.falt.motivering_ej": "Motivo per cui la causa non è stata accertata", + "slutsats.falt.uteslutet": "Alternative escluse", + "slutsats.falt.kvarstaende": "Incertezza residua", + "slutsats.falt.atgardsval": "Scelta dell'intervento", + "slutsats.saknas": "{falt} manca.", + "slutsats.ickesvar.falt": "{falt}: «{text}» non è una motivazione. Indicare che cosa vale realmente, e perché.", + "slutsats.for_kort": "{falt} è troppo breve ({langd} su almeno {minsta} caratteri) per poter essere verificato in seguito.", + "slutsats.utan_varfor": "{falt} indica il che cosa, non il perché. Collegare la conclusione all'evidenza — che cosa in essa la sostiene?", + "slutsats.utan_slutsats": "La pratica non può essere chiusa senza una conclusione. Indicare perché la conclusione discende dall'evidenza.", + "slutsats.hypotes_obemott": "L'ipotesi «{text}» è presente nel registro ma non viene trattata. Indicare perché è stata esclusa, o perché resta aperta.", + // ---- Flusso della pratica ----------------------------------------------- "arende.nytt": "Nuova pratica", diff --git a/felsokning/services/gemensam/sprak/nl.mjs b/felsokning/services/gemensam/sprak/nl.mjs index 4cceb15..df7fd52 100644 --- a/felsokning/services/gemensam/sprak/nl.mjs +++ b/felsokning/services/gemensam/sprak/nl.mjs @@ -60,6 +60,18 @@ export const NL = { "slutsats.avfardat": "Welke hypothesen zijn verworpen, en waarom", "slutsats.osakert": "Wat onzeker blijft", "slutsats.ickesvar": "Dat is geen conclusie. Aangeven wat is vastgesteld en welk bewijs dat draagt.", + "slutsats.falt.motivering": "Onderbouwing", + "slutsats.falt.motivering_ej": "Reden waarom de oorzaak niet kon worden vastgesteld", + "slutsats.falt.uteslutet": "Verworpen alternatieven", + "slutsats.falt.kvarstaende": "Resterende onzekerheid", + "slutsats.falt.atgardsval": "Keuze van de maatregel", + "slutsats.saknas": "{falt} ontbreekt.", + "slutsats.ickesvar.falt": "{falt}: „{text}” is geen onderbouwing. Aangeven wat er werkelijk geldt, en waarom.", + "slutsats.for_kort": "{falt} is te kort ({langd} van minstens {minsta} tekens) om achteraf toetsbaar te zijn.", + "slutsats.utan_varfor": "{falt} geeft het wat aan, niet het waarom. Verbind de conclusie met het bewijs — wat daarin maakt dat dit volgt?", + "slutsats.utan_slutsats": "De zaak kan niet worden afgesloten zonder eindconclusie. Aangeven waarom de conclusie uit het bewijs volgt.", + "slutsats.hypotes_obemott": "De hypothese „{text}” staat in het logboek maar wordt niet behandeld. Aangeven waarom zij is verworpen, of waarom zij open blijft.", + // ---- Zaakverloop -------------------------------------------------------- "arende.nytt": "Nieuwe zaak", diff --git a/felsokning/services/gemensam/sprak/ord.mjs b/felsokning/services/gemensam/sprak/ord.mjs new file mode 100644 index 0000000..29090ef --- /dev/null +++ b/felsokning/services/gemensam/sprak/ord.mjs @@ -0,0 +1,156 @@ +// ALVA-SPEC-060 · Ordlistorna bakom ALVA-RULE-200. +// +// ---- Varför den här filen finns ----------------------------------------- +// +// motivering.mjs granskar teknikerns slutsats med två ordlistor: fraser +// som ser ut som svar men inte är det ("klart", "trasig"), och ord som +// visar att en text faktiskt innehåller ett resonemang ("eftersom", +// "beror på"). Båda listorna var enbart svenska. +// +// Det var inte en översättningsbrist utan ett hål i regeln. En tysk +// tekniker som skrev "erledigt" i motiveringsfältet passerade filtret +// för icke-svar, därför att filtret bara kände igen "klart". Grinden +// släppte igenom avslutet och rapporten såg fullständig ut. Regeln som +// är hela produktens värde gällde i praktiken bara på svenska. +// +// ---- Union, inte uppslag per språk -------------------------------------- +// +// Listorna slås ihop över alla språk i stället för att väljas efter +// användarens språkinställning. Två skäl: +// +// Ett icke-svar är ett icke-svar oavsett språk. Jämförelsen görs mot +// HELA det normaliserade fältet, inte mot delsträngar, så "ok" fångas +// likadant vare sig verkstaden står i Kraków eller Köln — och en +// tekniker som skriver på ett annat språk än organisationens +// inställning ska inte kunna passera på den vägen. +// +// Orsaksorden går åt andra hållet: unionen gör kontrollen mer +// tillåtande, aldrig strängare. Ett felaktigt träffat orsaksord +// släpper igenom en text, det blockerar aldrig en riktig. Det är rätt +// riktning för ett fel att luta åt när alternativet är att neka en +// tekniker avslut på ett korrekt skrivet underlag. +// +// ---- Vad som medvetet INTE står i listorna ------------------------------- +// +// "inget", "keine", "none". Att ingenting kvarstår osäkert är ett giltigt +// och ofta korrekt svar (se motivering.mjs). Att göra det till ett +// icke-svar hade tvingat fram utfyllnad, och utfyllnad är sämre än ett +// kort sant svar. +// +// Orsaksord kortare än fyra tecken. De jämförs som delsträngar och skulle +// träffa inuti orelaterade ord på andra språk. +// +// ---- Granskningsstatus --------------------------------------------------- +// +// Svenska och engelska är genomgångna. Övriga språk är en första +// uppsättning och ska läsas av någon som arbetar i verkstad på språket +// innan de räknas som granskade — det är samma gräns som `granskat` i +// SPRAK, och den gäller här därför att listorna är metodik, inte +// gränssnitt. + +/** Fraser som ser ut som svar men inte är det. Jämförs mot hela fältet. */ +export const ICKESVAR_PER_SPRAK = { + sv: ["klart", "ok", "okej", "åtgärdat", "atgardat", "fixat", "se ovan", "se logg", "enligt ovan", "som vanligt", + "trasig", "defekt", "sliten", "utsliten", "behöver bytas", "behover bytas", "byttes", "bytt", + "enligt kund", "kunden sa", "vet ej", "ingen aning", "diverse", "allmänt slitage", "-", "n/a", "na"], + en: ["done", "ok", "okay", "fixed", "repaired", "see above", "see log", "as above", "as usual", + "broken", "defective", "worn", "worn out", "needs replacing", "replaced", + "per customer", "customer said", "don't know", "dont know", "no idea", "various", "general wear"], + de: ["erledigt", "behoben", "repariert", "siehe oben", "siehe protokoll", "wie oben", "wie üblich", "wie ublich", + "kaputt", "verschlissen", "abgenutzt", "muss getauscht werden", "getauscht", "gewechselt", + "laut kunde", "kunde sagte", "weiß nicht", "weiss nicht", "keine ahnung", "diverses", + "allgemeiner verschleiß", "allgemeiner verschleiss", "k.a."], + fr: ["fait", "terminé", "termine", "réparé", "repare", "voir ci-dessus", "voir journal", "comme ci-dessus", + "comme d'habitude", "cassé", "casse", "défectueux", "defectueux", "usé", "usagé", "usage", + "à remplacer", "a remplacer", "remplacé", "remplace", "selon le client", "le client a dit", + "je ne sais pas", "aucune idée", "aucune idee", "divers", "usure générale", "usure generale"], + es: ["hecho", "listo", "reparado", "ver arriba", "ver registro", "como arriba", "como siempre", + "roto", "defectuoso", "desgastado", "hay que cambiarlo", "cambiado", "sustituido", + "según el cliente", "segun el cliente", "el cliente dijo", "no sé", "no se", "ni idea", + "varios", "desgaste general"], + it: ["fatto", "risolto", "riparato", "vedi sopra", "vedi registro", "come sopra", "come al solito", + "rotto", "difettoso", "usurato", "da sostituire", "sostituito", "cambiato", + "secondo il cliente", "il cliente ha detto", "non so", "nessuna idea", "vari", "usura generale", "n/d"], + pl: ["zrobione", "gotowe", "naprawione", "patrz wyżej", "patrz wyzej", "patrz dziennik", "jak wyżej", "jak wyzej", + "jak zwykle", "zepsute", "uszkodzone", "zużyte", "zuzyte", "do wymiany", "wymienione", + "według klienta", "wedlug klienta", "klient powiedział", "klient powiedzial", + "nie wiem", "nie mam pojęcia", "nie mam pojecia", "różne", "rozne", "ogólne zużycie", "ogolne zuzycie"], + nl: ["klaar", "oké", "oke", "gedaan", "gerepareerd", "zie boven", "zie logboek", "zoals boven", + "zoals gewoonlijk", "kapot", "versleten", "moet vervangen worden", "vervangen", + "volgens klant", "klant zei", "weet niet", "geen idee", "diversen", "algemene slijtage", "n.v.t.", "nvt"], + pt: ["feito", "pronto", "reparado", "ver acima", "ver registo", "como acima", "como habitual", + "partido", "avariado", "defeituoso", "desgastado", "a substituir", "substituído", "substituido", + "segundo o cliente", "o cliente disse", "não sei", "nao sei", "nenhuma ideia", "diversos", "desgaste geral"], + ro: ["gata", "rezolvat", "reparat", "vezi mai sus", "vezi jurnal", "ca mai sus", "ca de obicei", + "stricat", "uzat", "de înlocuit", "de inlocuit", "înlocuit", "inlocuit", + "conform clientului", "clientul a spus", "nu știu", "nu stiu", "habar n-am", "diverse", + "uzură generală", "uzura generala"], +}; + +/** + * Ord som visar att texten innehåller ett orsakssamband. + * + * Jämförs som delsträngar, därför minst fyra tecken — se filhuvudet. + */ +export const ORSAKSORD_PER_SPRAK = { + sv: ["eftersom", "därför", "darfor", "beror", "orsakas", "orsakade", "medför", "medfor", "leder till", + "ledde till", "vilket", "på grund av", "pga", "resulterar", "resulterade", "innebär", "innebar", + "följaktligen", "foljaktligen", "till följd av", "förklarar", "forklarar", "stämmer överens", + "stammer overens", "bekräftas av", "bekraftas av", "visar att", "utesluter"], + en: ["because", "since", "due to", "caused", "causes", "results in", "resulted in", "leads to", "led to", + "therefore", "consequently", "explains", "confirmed by", "shows that", "rules out", + "consistent with", "indicates"], + de: ["weil", "aufgrund", "verursacht", "führt zu", "fuhrt zu", "führte zu", "fuhrte zu", "folglich", + "daher", "deshalb", "erklärt", "erklart", "bestätigt durch", "bestatigt durch", "zeigt, dass", + "zeigt dass", "schließt aus", "schliesst aus", "stimmt überein", "stimmt uberein", "deutet auf"], + fr: ["parce que", "puisque", "en raison de", "à cause de", "a cause de", "provoqué", "provoque", + "entraîne", "entraine", "entraîné", "conduit à", "conduit a", "par conséquent", "par consequent", + "explique", "confirmé par", "confirme par", "montre que", "exclut", "correspond à", "correspond a", + "indique"], + es: ["porque", "dado que", "debido a", "causado", "provoca", "provocó", "provoco", "conduce a", + "por lo tanto", "por consiguiente", "explica", "confirmado por", "muestra que", "descarta", + "coincide con", "indica"], + it: ["perché", "perche", "poiché", "poiche", "dato che", "a causa di", "causato", "provoca", "porta a", + "quindi", "pertanto", "di conseguenza", "spiega", "confermato da", "mostra che", "esclude", + "coincide con", "indica"], + pl: ["ponieważ", "poniewaz", "dlatego", "z powodu", "spowodowane", "powoduje", "prowadzi do", + "w związku", "w zwiazku", "zatem", "wyjaśnia", "wyjasnia", "potwierdzone przez", + "pokazuje, że", "pokazuje ze", "wyklucza", "zgodne z", "wskazuje"], + nl: ["omdat", "aangezien", "doordat", "vanwege", "veroorzaakt", "leidt tot", "leidde tot", "daarom", + "bijgevolg", "verklaart", "bevestigd door", "toont aan", "sluit uit", "komt overeen", "duidt op"], + pt: ["porque", "dado que", "devido a", "causado", "provoca", "leva a", "portanto", "por conseguinte", + "explica", "confirmado por", "mostra que", "exclui", "coincide com", "indica"], + ro: ["deoarece", "pentru că", "pentru ca", "din cauza", "cauzat", "provoacă", "provoaca", "duce la", + "prin urmare", "așadar", "asadar", "explică", "explica", "confirmat de", "arată că", "arata ca", + "exclude", "corespunde cu", "indică", "indica"], +}; + +/** + * Ord som visar att texten hänvisar till dokumenterad evidens. + * + * Samma delsträngsjämförelse och samma unionsresonemang som ovan. Här + * är det extra tydligt varför unionen är rätt: engelskans "photo" + * innehåller inte "foto", så en engelsk tekniker som skrev "photo shows + * corrosion at pin 14" hänvisade till evidens som filtret inte såg. + */ +export const EVIDENSORD_PER_SPRAK = { + sv: ["mät", "mat", "foto", "bild", "video", "felkod", "avläs", "avlas", "prov", "test", "dokument", "historik", "kontroll"], + en: ["measur", "photo", "image", "picture", "video", "fault code", "dtc", "reading", "test", "document", "history", "check", "scan"], + de: ["mess", "foto", "bild", "video", "fehlercode", "ablesung", "prüf", "pruf", "dokument", "historie", "kontrolle"], + fr: ["mesur", "photo", "image", "vidéo", "video", "code défaut", "code defaut", "relevé", "releve", "essai", "document", "historique", "contrôle", "controle"], + es: ["medici", "medida", "foto", "imagen", "vídeo", "video", "código de avería", "codigo de averia", "lectura", "prueba", "documento", "historial", "control"], + it: ["misur", "foto", "immagine", "video", "codice errore", "lettura", "prova", "documento", "storico", "controllo"], + pl: ["pomiar", "zdjęcie", "zdjecie", "obraz", "wideo", "kod błędu", "kod bledu", "odczyt", "test", "dokument", "historia", "kontrola"], + nl: ["meting", "gemeten", "foto", "afbeelding", "video", "foutcode", "uitlezing", "test", "document", "historie", "controle"], + pt: ["medi", "foto", "imagem", "vídeo", "video", "código de avaria", "codigo de avaria", "leitura", "ensaio", "documento", "histórico", "historico", "controlo"], + ro: ["măsur", "masur", "foto", "imagine", "video", "cod de eroare", "citire", "test", "document", "istoric", "control"], +}; + +const union = (per) => [...new Set(Object.values(per).flat())]; + +export const ICKESVAR = union(ICKESVAR_PER_SPRAK); +export const ORSAKSORD = union(ORSAKSORD_PER_SPRAK); +export const EVIDENSORD = union(EVIDENSORD_PER_SPRAK); + +/** Kortaste orsaksord som får jämföras som delsträng. Se filhuvudet. */ +export const MINSTA_ORSAKSORD = 4; diff --git a/felsokning/services/gemensam/sprak/pl.mjs b/felsokning/services/gemensam/sprak/pl.mjs index a4d4be0..6be7b1f 100644 --- a/felsokning/services/gemensam/sprak/pl.mjs +++ b/felsokning/services/gemensam/sprak/pl.mjs @@ -62,6 +62,18 @@ export const PL = { "slutsats.avfardat": "Które hipotezy odrzucono i dlaczego", "slutsats.osakert": "Co pozostaje niepewne", "slutsats.ickesvar": "To nie jest wniosek. Podać, co zostało ustalone i jakie dowody to potwierdzają.", + "slutsats.falt.motivering": "Uzasadnienie", + "slutsats.falt.motivering_ej": "Powód, dla którego nie ustalono przyczyny", + "slutsats.falt.uteslutet": "Odrzucone alternatywy", + "slutsats.falt.kvarstaende": "Pozostała niepewność", + "slutsats.falt.atgardsval": "Wybór działania", + "slutsats.saknas": "Brakuje: {falt}.", + "slutsats.ickesvar.falt": "{falt}: „{text}” nie jest uzasadnieniem. Podać, co faktycznie obowiązuje i dlaczego.", + "slutsats.for_kort": "{falt} jest za krótkie ({langd} z co najmniej {minsta} znaków), aby dało się je później zweryfikować.", + "slutsats.utan_varfor": "{falt} podaje co, ale nie dlaczego. Powiązać wniosek z dowodami — co w nich sprawia, że to wynika?", + "slutsats.utan_slutsats": "Sprawy nie można zamknąć bez wniosku końcowego. Podać, dlaczego wniosek wynika z dowodów.", + "slutsats.hypotes_obemott": "Hipoteza „{text}” znajduje się w dzienniku, ale nie została omówiona. Podać, dlaczego ją odrzucono lub dlaczego pozostaje otwarta.", + // ---- Przebieg sprawy ---------------------------------------------------- "arende.nytt": "Nowa sprawa", diff --git a/felsokning/services/gemensam/sprak/pt.mjs b/felsokning/services/gemensam/sprak/pt.mjs index 635779f..aab6820 100644 --- a/felsokning/services/gemensam/sprak/pt.mjs +++ b/felsokning/services/gemensam/sprak/pt.mjs @@ -60,6 +60,18 @@ export const PT = { "slutsats.avfardat": "Que hipóteses foram afastadas, e porquê", "slutsats.osakert": "O que permanece incerto", "slutsats.ickesvar": "Isso não é uma conclusão. Indicar o que foi estabelecido e que evidência o sustenta.", + "slutsats.falt.motivering": "Fundamentação", + "slutsats.falt.motivering_ej": "Motivo pelo qual a causa não pôde ser estabelecida", + "slutsats.falt.uteslutet": "Alternativas afastadas", + "slutsats.falt.kvarstaende": "Incerteza remanescente", + "slutsats.falt.atgardsval": "Escolha da ação", + "slutsats.saknas": "Falta {falt}.", + "slutsats.ickesvar.falt": "{falt}: «{text}» não é uma fundamentação. Indicar o que se aplica realmente, e porquê.", + "slutsats.for_kort": "{falt} é demasiado curto ({langd} de pelo menos {minsta} caracteres) para poder ser verificado posteriormente.", + "slutsats.utan_varfor": "{falt} indica o quê, mas não o porquê. Ligar a conclusão à evidência — o que nela faz com que isto decorra?", + "slutsats.utan_slutsats": "O processo não pode ser encerrado sem uma conclusão. Indicar por que motivo a conclusão decorre da evidência.", + "slutsats.hypotes_obemott": "A hipótese «{text}» consta do registo mas não é tratada. Indicar por que motivo foi afastada, ou por que motivo permanece em aberto.", + // ---- Fluxo do processo -------------------------------------------------- "arende.nytt": "Novo processo", diff --git a/felsokning/services/gemensam/sprak/ro.mjs b/felsokning/services/gemensam/sprak/ro.mjs index 3b3eaed..37e145a 100644 --- a/felsokning/services/gemensam/sprak/ro.mjs +++ b/felsokning/services/gemensam/sprak/ro.mjs @@ -66,6 +66,18 @@ export const RO = { "slutsats.avfardat": "Ce ipoteze au fost înlăturate și de ce", "slutsats.osakert": "Ce rămâne incert", "slutsats.ickesvar": "Aceasta nu este o concluzie. Indică ce a fost stabilit și ce dovezi susțin acest lucru.", + "slutsats.falt.motivering": "Justificare", + "slutsats.falt.motivering_ej": "Motivul pentru care cauza nu a putut fi stabilită", + "slutsats.falt.uteslutet": "Alternative înlăturate", + "slutsats.falt.kvarstaende": "Incertitudine rămasă", + "slutsats.falt.atgardsval": "Alegerea acțiunii", + "slutsats.saknas": "Lipsește: {falt}.", + "slutsats.ickesvar.falt": "{falt}: „{text}” nu este o justificare. Indică ce se aplică în realitate și de ce.", + "slutsats.for_kort": "{falt} este prea scurt ({langd} din cel puțin {minsta} caractere) pentru a putea fi verificat ulterior.", + "slutsats.utan_varfor": "{falt} spune ce, dar nu de ce. Leagă concluzia de dovezi — ce anume din ele face ca aceasta să decurgă?", + "slutsats.utan_slutsats": "Cazul nu poate fi închis fără o concluzie finală. Indică de ce concluzia decurge din dovezi.", + "slutsats.hypotes_obemott": "Ipoteza „{text}” se află în jurnal, dar nu este tratată. Indică de ce a fost înlăturată sau de ce rămâne deschisă.", + // ---- Parcursul cazului -------------------------------------------------- "arende.nytt": "Caz nou", diff --git a/felsokning/services/gemensam/sprak/sv.mjs b/felsokning/services/gemensam/sprak/sv.mjs index 95e7cb4..85478b1 100644 --- a/felsokning/services/gemensam/sprak/sv.mjs +++ b/felsokning/services/gemensam/sprak/sv.mjs @@ -59,6 +59,18 @@ export const SV = { "slutsats.avfardat": "Vilka hypoteser som avfärdats, och varför", "slutsats.osakert": "Vad som fortfarande är osäkert", "slutsats.ickesvar": "Det är inte ett svar. Ange vad som är konstaterat och vilken evidens som bär det.", + "slutsats.falt.motivering": "Motivering", + "slutsats.falt.motivering_ej": "Skäl till att orsaken inte fastställts", + "slutsats.falt.uteslutet": "Uteslutna alternativ", + "slutsats.falt.kvarstaende": "Kvarstående osäkerhet", + "slutsats.falt.atgardsval": "Val av åtgärd", + "slutsats.saknas": "{falt} saknas.", + "slutsats.ickesvar.falt": "{falt}: ”{text}” är inget skäl. Beskriv vad som faktiskt gäller och varför.", + "slutsats.for_kort": "{falt} är för kort ({langd} av minst {minsta} tecken) för att gå att granska i efterhand.", + "slutsats.utan_varfor": "{falt} anger vad, men inte varför. Knyt slutsatsen till underlaget — vad i evidensen gör att det här följer?", + "slutsats.utan_slutsats": "Ärendet kan inte avslutas utan en slutsats. Motivera varför slutsatsen följer av underlaget.", + "slutsats.hypotes_obemott": "Hypotesen ”{text}” finns i loggen men bemöts inte. Ange varför den uteslöts, eller varför den kvarstår.", + // ---- Ärendeflödet ------------------------------------------------------- "arende.nytt": "Nytt ärende",