Files
Claude 322d228c80 ALVA-RULE-200 gällde bara på svenska — nu på tio språk
Slutsatsgranskningen avvisar icke-svar med en ordlista: "klart", "ok",
"trasig". Listan 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 hålet fanns på varje marknad utom en.

Samma sak åt andra hållet: orsaksorden som visar att en text innehåller
ett resonemang var svenska, liksom evidensorden. Engelskans "photo"
innehåller inte "foto", så "the photo shows the arcing at pin 14"
räknades inte som en hänvisning till underlag. Följden hade varit värre
än ett släppt avslut — en korrekt skriven motivering hade nekats, och en
spärr som nekar rätt svar är en spärr man lär sig kringgå.

Listorna ligger nu i sprak/ord.mjs, per språk, och slås ihop till en
union i stället för att väljas efter inställning. Ett icke-svar är ett
icke-svar oavsett språk — jämförelsen görs mot hela fältet, så en
tekniker som skriver på ett annat språk än organisationens ska inte
kunna passera på den vägen. Orsaks- och evidensorden går åt motsatt
håll: unionen gör kontrollen mer tillåtande, aldrig strängare, och det
är rätt riktning att fela åt.

Medvetet utanför listorna: "inget", "keine", "none". Att ingenting
kvarstår osäkert är ett giltigt och ofta korrekt svar; att göra det till
ett icke-svar hade tvingat fram utfyllnad, och utfyllnad är sämre än ett
kort sant svar. Ett test låser det.

Bristernas texter kommer nu ur katalogen och bär sin nyckel, precis som
grindens hinder. Fältnamnet översätts med resten av meningen —
"Begründung fehlt.", inte "Motivering fehlt.".

Mutationsprövat: med listan återställd till enbart svenska föll nio
tester, ett per språk. Svenska och engelska är genomgångna; övriga
listor är en första uppsättning och står som ogranskade i filhuvudet,
av samma skäl som `granskat` i SPRAK — de är metodik, inte gränssnitt.

597 tester, 186 integrationskontroller, typkontroll, lint och genomgång
gröna.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
2026-08-06 16:44:57 +00:00

179 lines
7.4 KiB
JavaScript

// ALVA-RULE-200 · Closing statement.
//
// Ett ärende stängs aldrig utan att teknikern har lämnat ett varför.
//
// Det låter enkelt och är det inte. En obligatorisk fritextruta blir
// "klart" på tredje ärendet, och då har vi bara gjort dokumentationen
// långsammare utan att göra den bättre. Ett varför som ska hålla i en
// garantitvist måste vara strukturerat, kopplat till evidensen, och
// kvalitetsgranskat mot det som faktiskt skrivs när någon har bråttom.
//
// ---- Vad ett varför faktiskt består av -------------------------------
//
// Fyra frågor med olika adressat. De blandas i dag ihop till en enda
// rad i en arbetsorder, och det är därför verkstadsprotokoll är svåra
// att använda i efterhand:
//
// MOTIVERING Varför följer slutsatsen av evidensen?
// Läses av: nästa tekniker, försäkringsbedömaren.
// Detta är den rad som saknas i varje system jag sett.
// Underlaget säger VAD som mättes. Slutsatsen säger VAD
// som är fel. Ingenting säger varför det ena medför det
// andra — och det är precis det steget en bedömare
// behöver granska.
//
// UTESLUTET Vad övervägdes och varför föll det bort?
// Läses av: försäkringsbolaget, garantihandläggaren.
// En felsökning utan uteslutna alternativ är en gissning
// som råkade stämma. Härleds delvis ur loggen: en
// hypotes som dokumenterats och inte blev slutsatsen
// MÅSTE bemötas.
//
// ÅTGÄRDSVAL Varför denna åtgärd och inte en annan?
// Läses av: kunden, kostnadsgranskaren.
// Byte eller justering? Varför hela enheten?
//
// KVARSTÅENDE Vad är fortfarande osäkert?
// Läses av: nästa tekniker, flottansvarig.
// Får vara "inget" — men det måste sägas aktivt.
//
// ---- Den ärliga vägen -------------------------------------------------
//
// Ibland vet teknikern inte varför. Systemet måste ha en väg för det som
// inte är en lögn: orsaken kunde inte fastställas, och varför den inte
// kunde det. Det är också ett varför, och ofta ett mer användbart än en
// påhittad orsak.
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;
const normalisera = (text) => String(text ?? "").trim().toLowerCase().replace(/\s+/g, " ");
/** Är texten ett icke-svar? Jämför mot hela fältet, inte delsträngar. */
function ärIckesvar(text) {
const n = normalisera(text).replace(/[.!]+$/, "");
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) => ord.length >= MINSTA_ORSAKSORD && n.includes(ord));
}
/**
* Refererar texten till dokumenterad evidens? Godtar både uttryckliga
* ord för underlag och konkreta mätvärden med enhet — den som skriver
* "12,4 V vid stift 14" hänvisar till en mätning även utan att säga
* ordet "mätning".
*/
function refererarEvidens(text) {
const n = normalisera(text);
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 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(faltNyckel, text, { minsta = MINSTA_KORT, kravOrsak = false, sprak = STANDARD } = {}) {
const = String(text ?? "").trim();
const falt = t(sprak, faltNyckel);
if (!) return t(sprak, "slutsats.saknas", { falt });
if (ärIckesvar()) return t(sprak, "slutsats.ickesvar.falt", { falt, text: });
if (.length < minsta) {
return t(sprak, "slutsats.for_kort", { falt, langd: .length, minsta });
}
if (kravOrsak && !harOrsakssamband() && !refererarEvidens()) {
return t(sprak, "slutsats.utan_varfor", { falt });
}
return null;
}
/**
* Hypoteser som dokumenterats men inte blivit slutsatsen.
*
* Härleds ur loggen: den som skrivit ned en misstanke och sedan landat i
* något annat måste säga varför den föll bort. Det är den enskilda regel
* som gör underlaget användbart för en försäkringsbedömare — utan den är
* en felsökning en gissning som råkade stämma.
*/
export function obemottaHypoteser(handelser, slutsats) {
const hypoteser = handelser.filter((h) => h?.typ === "hypotes").map((h) => h.text);
if (hypoteser.length === 0) return [];
const uteslutet = normalisera(slutsats?.uteslutet);
const orsak = normalisera(
[slutsats?.motivering, ...(handelser.filter((h) => h?.typ === "felorsak").map((h) => h.avvikelse) ?? [])].join(" "),
);
// En hypotes är bemött om något meningsbärande ord ur den återkommer i
// uteslutandet eller i den fastställda orsaken. Trubbigt, men det
// fångar den som skriver "uteslutet: inget" med tre öppna hypoteser i
// loggen.
return hypoteser.filter((text) => {
const ord = normalisera(text)
.split(/[^a-zåäöéü0-9]+/)
.filter((o) => o.length >= 5);
if (ord.length === 0) return false;
return !ord.some((o) => uteslutet.includes(o) || orsak.includes(o));
});
}
/**
* Granskar hela slutsatsen inför avslut.
*
* @param slutsats händelsen av typ "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 = [], 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", nyckel: "slutsats.utan_slutsats", text: t(sprak, "slutsats.utan_slutsats") }];
}
const fastställd = slutsats.orsakFastställd !== false;
if (fastställd) {
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",
g("slutsats.falt.motivering_ej", slutsats.motivering, { minsta: MINSTA_MOTIVERING, kravOrsak: true }),
);
}
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", g("slutsats.falt.atgardsval", slutsats.atgardsval, { minsta: MINSTA_KORT, kravOrsak: true }));
}
for (const text of obemottaHypoteser(handelser, slutsats)) {
brister.push({
falt: "uteslutet",
nyckel: "slutsats.hypotes_obemott",
text: t(sprak, "slutsats.hypotes_obemott", { text }),
});
}
return brister;
}