Files
alva/felsokning/app/services/gemensam/motivering.mjs
T
Bernt 04e0a1e8ec
Guidad Felsökning / test-och-bygg (push) Has been cancelled
Guidad Felsökning / tjanster (push) Has been cancelled
Guidad Felsökning / terraform (push) Has been cancelled
Guidad Felsökning / leverantorskedja (push) Has been cancelled
Guidad Felsökning / aterstallning (push) Has been cancelled
Guidad Felsökning / publicera (push) Has been cancelled
Guidad Felsökning / driftsatt (push) Has been cancelled
ALVA deployment config
2026-08-08 13:12:13 +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;
}