ALVA-RULE-200: ett ärende stängs aldrig utan ett varför
Kravet är enkelt att formulera och lätt att bygga fel. En obligatorisk
fritextruta blir "klart" på tredje ärendet, och då har vi bara gjort
dokumentationen långsammare utan att göra den bättre.
Därför fyra frågor med olika adressat i stället för ett fält:
MOTIVERING Varför följer slutsatsen av evidensen? Detta är raden som
saknas i varje verkstadsprotokoll. Underlaget säger vad
som mättes, slutsatsen vad som är fel — ingenting säger
varför det ena medför det andra, och det är precis det
steget en försäkringsbedömare granskar.
UTESLUTET Vad övervägdes och varför föll det bort?
ÅTGÄRDSVAL Varför denna åtgärd och inte en annan?
KVARSTÅENDE Vad är fortfarande osäkert? Får vara "inget" — men aktivt.
Kvalitetsgranskningen är riktad mot hur någon med bråttom faktiskt
skriver: en katalog över icke-svar (klart, åtgärdat, trasig, se ovan,
vet ej), minsta längd, och kravet att texten bär ett orsakssamband eller
refererar konkret evidens. Den som skriver "12,4 V vid stift 14"
hänvisar till en mätning utan att säga ordet — regeln får inte tvinga
fram ett språkbruk som inte är teknikerns.
Den regel som gör underlaget användbart för ett försäkringsbolag härleds
ur loggen: en hypotes som dokumenterats och inte blivit slutsatsen MÅSTE
bemötas. Utan den är en felsökning en gissning som råkade stämma.
Den ärliga vägen finns: orsaken kunde inte fastställas är ett giltigt
utfall, ofta mer användbart än en påhittad orsak — men varför den inte
kunde det är fortfarande ett varför.
Slutsatsen är kunddelbar. Den besvarar "varför kostade det här vad det
kostade" och är den enda rad en bedömare behöver. Att bygga funktionen
och sedan hålla den intern vore att bygga den förgäves.
I gränssnittet granskas fälten medan man skriver, inte efter Spara, och
obemötta hypoteser listas — teknikern ska aldrig behöva gissa vad som
fattas. Det är skillnaden mellan ett krav som respekteras och ett som
kringgås.
---
Typkontrollen avslöjade under arbetet en riktig bugg i mitt eget
grindarbete: schemat och grinden skrevs mot antagna fältnamn i stället
för mot domänmodellen. atgard_utford heter beskrivning och utford, inte
text. Följden var värre än ett typfel — grinden såg utförd åtgärd som
utebliven och krävde därför aldrig kundbesked eller kvalitetskontroll.
Det syntes inte, eftersom testfixturerna hade samma antagande.
Rättat i schema, grind och fixturer, och låst av två nya test: varje
fält i schemat måste finnas i domänmodellen, och varje händelse
demoärendet producerar måste passera serverns validering. Ett schema som
avvisar riktig trafik är värre än inget schema.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
// ALVA-RULE-200 · Slutsatspanelen.
|
||||
//
|
||||
// Här möter teknikern kravet, och här avgörs om regeln blir älskad eller
|
||||
// hatad. Tre val styr det:
|
||||
//
|
||||
// Fyra fält, inte ett. En stor ruta ger "klart". Fyra namngivna
|
||||
// frågor med olika adressat ger fyra svar,
|
||||
// eftersom var och en är konkret nog att
|
||||
// besvara utan att först fundera på vad som
|
||||
// efterfrågas.
|
||||
//
|
||||
// Granskning medan man Bristerna visas under fältet direkt, inte som
|
||||
// skriver. ett felmeddelande efter Spara. Att bli nekad
|
||||
// efter att ha skrivit klart är det som gör
|
||||
// obligatoriska fält förhatliga.
|
||||
//
|
||||
// Hypoteserna listas. Systemet visar vilka misstankar som ligger i
|
||||
// loggen och behöver bemötas. Teknikern ska
|
||||
// aldrig behöva gissa vad som fattas.
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import type { Arende, Handelse } from "./domain";
|
||||
import { granskaSlutsats, obemottaHypoteser } from "../../../services/gemensam/motivering.mjs";
|
||||
import { FARG, Etikett, Knapp, Statusmärke } from "@/alva/komponenter";
|
||||
|
||||
interface Brist {
|
||||
falt: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const FALT = [
|
||||
{
|
||||
id: "motivering" as const,
|
||||
etikett: "Motivering",
|
||||
fraga: "Varför följer slutsatsen av underlaget?",
|
||||
hjalp: "Knyt mätvärden, foton och kontroller till slutsatsen. Vad i evidensen gör att just detta följer?",
|
||||
rader: 4,
|
||||
},
|
||||
{
|
||||
id: "uteslutet" as const,
|
||||
etikett: "Uteslutna alternativ",
|
||||
fraga: "Vad övervägdes, och varför föll det bort?",
|
||||
hjalp: "En felsökning utan uteslutna alternativ är en gissning som råkade stämma.",
|
||||
rader: 3,
|
||||
},
|
||||
{
|
||||
id: "atgardsval" as const,
|
||||
etikett: "Val av åtgärd",
|
||||
fraga: "Varför denna åtgärd och inte en annan?",
|
||||
hjalp: "Byte eller justering? Hela enheten eller en del? Skälet läses av den som granskar kostnaden.",
|
||||
rader: 2,
|
||||
},
|
||||
{
|
||||
id: "kvarstaende" as const,
|
||||
etikett: "Kvarstående osäkerhet",
|
||||
fraga: "Vad är fortfarande osäkert?",
|
||||
hjalp: 'Får vara "inget" — men det måste sägas aktivt.',
|
||||
rader: 2,
|
||||
},
|
||||
];
|
||||
|
||||
export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse) => void }) {
|
||||
const handelser = useMemo(() => arende.handelser.map((p) => p.handelse), [arende]);
|
||||
const befintlig = useMemo(
|
||||
() => [...handelser].reverse().find((h) => h.typ === "slutsats"),
|
||||
[handelser],
|
||||
) as Extract<Handelse, { typ: "slutsats" }> | undefined;
|
||||
|
||||
const [fastställd, setFastställd] = useState(befintlig?.orsakFastställd !== false);
|
||||
const [utkast, setUtkast] = useState({
|
||||
motivering: befintlig?.motivering ?? "",
|
||||
uteslutet: befintlig?.uteslutet ?? "",
|
||||
atgardsval: befintlig?.atgardsval ?? "",
|
||||
kvarstaende: befintlig?.kvarstaende ?? "",
|
||||
});
|
||||
|
||||
const förslag = { typ: "slutsats" as const, ...utkast, orsakFastställd: fastställd };
|
||||
const brister: Brist[] = useMemo(
|
||||
() => granskaSlutsats(förslag, handelser),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[utkast, fastställd, handelser],
|
||||
);
|
||||
const obemötta: string[] = useMemo(() => obemottaHypoteser(handelser, förslag), [handelser, förslag]);
|
||||
|
||||
const brist = (falt: string) => brister.find((b) => b.falt === falt && !b.text.includes("bemöts inte"));
|
||||
const klar = brister.length === 0;
|
||||
|
||||
const arbeteUtfört = handelser.some((h) => h.typ === "atgard_utford" && h.utford);
|
||||
|
||||
return (
|
||||
<section className="mb-4 border bg-white" style={{ borderColor: FARG.lightSteel }}>
|
||||
<header
|
||||
className="flex flex-wrap items-baseline justify-between gap-2 border-b px-4 py-2"
|
||||
style={{ borderColor: FARG.lightSteel, background: FARG.background }}
|
||||
>
|
||||
<h3 className="text-[16px] font-semibold uppercase tracking-[0.06em]" style={{ color: FARG.graphite }}>
|
||||
Closing statement
|
||||
</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
<Statusmärke status={klar ? "complete" : "incomplete"} />
|
||||
<span className="font-mono text-[11px]" style={{ color: FARG.steel }}>
|
||||
ALVA-RULE-200
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="px-4 py-2">
|
||||
<p className="mb-4 max-w-[62ch] text-[13px] leading-[20px]" style={{ color: FARG.steel }}>
|
||||
Underlaget visar vad som mättes. Slutsatsen visar vad som är fel. Det här är raden som säger varför det
|
||||
ena följer av det andra — och den enda en garantihandläggare eller försäkringsbedömare behöver granska.
|
||||
</p>
|
||||
|
||||
{/* Den ärliga vägen. Att orsaken inte kunnat fastställas är ett
|
||||
giltigt utfall, och ofta mer användbart än en påhittad orsak.
|
||||
Men varför den inte kunde det är fortfarande ett varför. */}
|
||||
<div className="mb-4 flex flex-wrap gap-2">
|
||||
{[
|
||||
{ värde: true, text: "Orsak fastställd" },
|
||||
{ värde: false, text: "Orsak kunde inte fastställas" },
|
||||
].map((val) => (
|
||||
<button
|
||||
key={String(val.värde)}
|
||||
type="button"
|
||||
onClick={() => setFastställd(val.värde)}
|
||||
aria-pressed={fastställd === val.värde}
|
||||
className="border px-4 py-2 text-[12px] font-semibold uppercase tracking-[0.08em]"
|
||||
style={{
|
||||
background: fastställd === val.värde ? FARG.graphite : FARG.white,
|
||||
color: fastställd === val.värde ? FARG.white : FARG.graphite,
|
||||
borderColor: fastställd === val.värde ? FARG.graphite : FARG.steel,
|
||||
}}
|
||||
>
|
||||
{val.text}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{obemötta.length > 0 && (
|
||||
<div className="mb-4 border-2 px-4 py-2" style={{ borderColor: FARG.graphite }}>
|
||||
<Etikett ton="graphite">Hypoteser i loggen som behöver bemötas</Etikett>
|
||||
<ul className="mt-2 text-[13px] leading-[20px]" style={{ color: FARG.graphite }}>
|
||||
{obemötta.map((text) => (
|
||||
<li key={text} className="flex gap-2">
|
||||
<span aria-hidden="true">□</span>
|
||||
{text}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="mt-2 text-[12px] leading-[16px]" style={{ color: FARG.steel }}>
|
||||
Ange under Uteslutna alternativ varför de föll bort — eller varför de kvarstår.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{FALT.filter((f) => f.id !== "atgardsval" || arbeteUtfört).map((f) => {
|
||||
const fel = brist(f.id);
|
||||
const etikett = f.id === "motivering" && !fastställd ? "Skäl till att orsaken inte fastställts" : f.etikett;
|
||||
return (
|
||||
<div key={f.id} className="border-t py-2" style={{ borderColor: FARG.lightSteel }}>
|
||||
<label
|
||||
htmlFor={`slutsats-${f.id}`}
|
||||
className="block text-[11px] font-semibold uppercase tracking-[0.12em]"
|
||||
style={{ color: FARG.steel }}
|
||||
>
|
||||
{etikett}
|
||||
</label>
|
||||
<p className="mt-1 text-[13px]" style={{ color: FARG.graphite }}>
|
||||
{f.fraga}
|
||||
</p>
|
||||
<textarea
|
||||
id={`slutsats-${f.id}`}
|
||||
rows={f.rader}
|
||||
value={utkast[f.id]}
|
||||
onChange={(e) => setUtkast({ ...utkast, [f.id]: e.target.value })}
|
||||
aria-describedby={`slutsats-${f.id}-hjalp`}
|
||||
aria-invalid={Boolean(fel)}
|
||||
className="mt-2 w-full border px-4 py-2 text-[14px] leading-[20px]"
|
||||
style={{
|
||||
borderColor: fel ? FARG.graphite : FARG.lightSteel,
|
||||
borderWidth: fel ? 2 : 1,
|
||||
background: FARG.white,
|
||||
color: FARG.graphite,
|
||||
}}
|
||||
/>
|
||||
<p id={`slutsats-${f.id}-hjalp`} className="mt-1 text-[12px] leading-[16px]" style={{ color: FARG.steel }}>
|
||||
{fel ? fel.text : f.hjalp}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="mt-4 flex flex-wrap items-center gap-4">
|
||||
<Knapp disabled={!klar} onClick={() => skicka(förslag)}>
|
||||
{befintlig ? "Uppdatera slutsats" : "Registrera slutsats"}
|
||||
</Knapp>
|
||||
{!klar && (
|
||||
<span className="text-[12px]" style={{ color: FARG.steel }}>
|
||||
{brister.length} {brister.length === 1 ? "brist" : "brister"} kvar.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
// signerad behållare för overifierade påståenden, och kvalitetsgrinden
|
||||
// ett råd. Går något av testerna sönder har den garantin försvunnit —
|
||||
// och det syns inte i gränssnittet förrän någon bestrider ett ärende.
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { granskaHändelse, tillPost } from "../../../../services/gemensam/handelser.mjs";
|
||||
import { SPARRFRAGOR as SPARR_SERVER, grinda, grindaArendetyp } from "../../../../services/gemensam/grind.mjs";
|
||||
@@ -23,9 +24,18 @@ function komplettLogg(extra: Record<string, unknown>[] = []) {
|
||||
{ typ: "matarstallning", lage: "utgaende", varde: "14 205" },
|
||||
{ typ: "reproducering", status: "ja", beskrivning: "Reproducerat vid 88 km/h." },
|
||||
{ typ: "felorsak", avvikelse: "Obalans", orsaker: ["Normalt slitage"], underlag: ["Mätvärde"], sakerhet: "hog" },
|
||||
{ typ: "atgard_utford", text: "Balanserade hjulen." },
|
||||
{ typ: "kundbeslut", utfall: "godkant", kanal: "Telefon" },
|
||||
{ typ: "kvalitetskontroll", utfall: "borta", beskrivning: "Provkört, symptomet borta." },
|
||||
{ typ: "atgard_utford", beskrivning: "Balanserade hjulen.", utford: true },
|
||||
{ typ: "kundbeslut", beslut: "godkant", kanal: "Telefon" },
|
||||
{ typ: "kvalitetskontroll", resultat: "symptomet_borta", beskrivning: "Provkört, symptomet borta." },
|
||||
{
|
||||
typ: "slutsats",
|
||||
motivering:
|
||||
"Uppmätt obalans 38 g på höger framhjul mot toleransen 5 g, vilket förklarar varför vibrationen " +
|
||||
"uppträder hastighetsberoende kring 88 km/h och försvann efter balansering.",
|
||||
uteslutet: "Kast och bussningar kontrollerades utan anmärkning och uteslöts därför.",
|
||||
kvarstaende: "Inget. Symptomet reproducerades före och uteblev efter åtgärd.",
|
||||
atgardsval: "Balansering valdes framför däckbyte eftersom däcket är oskadat och slitaget inom gräns.",
|
||||
},
|
||||
{ typ: "matvarde", beskrivning: "Lufttryck", varde: "2,4" },
|
||||
{ typ: "foto", beskrivning: "Objektet" },
|
||||
{ typ: "foto", beskrivning: "Typskylt" },
|
||||
@@ -117,7 +127,7 @@ describe("C-2 · kvalitetsgrinden spärrar på servern", () => {
|
||||
|
||||
it("arbete utfört trots avböjt förslag är ett hårt fel, inte en varning", () => {
|
||||
const logg = komplettLogg().map((h) =>
|
||||
h.typ === "kundbeslut" ? { ...h, utfall: "avbojt" } : h,
|
||||
h.typ === "kundbeslut" ? { ...h, beslut: "avbojt" } : h,
|
||||
);
|
||||
expect(grinda(logg, GENERISK).map((h) => h.id)).toContain("avbojt_men_utfort");
|
||||
});
|
||||
@@ -194,3 +204,44 @@ describe("ärendetypens regelpaket", () => {
|
||||
expect(grindaArendetyp(logg, trasigt)[0].rubrik).toMatch(/Okänt krav/);
|
||||
});
|
||||
});
|
||||
|
||||
// Skydd mot den drift som faktiskt inträffade: schemat på servern skrevs
|
||||
// mot antagna fältnamn i stället för mot domänmodellen. Följden var värre
|
||||
// än ett typfel — grinden såg utförd åtgärd som utebliven, och krävde
|
||||
// därför aldrig kundbesked eller kvalitetskontroll. Det syntes inte,
|
||||
// eftersom testfixturerna hade samma antagande.
|
||||
describe("schemat följer domänmodellen", () => {
|
||||
it("varje obligatoriskt fält i schemat finns i domain.ts", async () => {
|
||||
const { HÄNDELSESCHEMA } = await import("../../../../services/gemensam/handelser.mjs");
|
||||
const domain = readFileSync("src/felsokning/domain.ts", "utf8");
|
||||
const avvikelser: string[] = [];
|
||||
|
||||
for (const [typ, schema] of Object.entries(HÄNDELSESCHEMA as Record<string, object>)) {
|
||||
// Klipp ut den del av unionen som beskriver typen.
|
||||
const start = domain.indexOf(`typ: "${typ}"`);
|
||||
if (start < 0) {
|
||||
avvikelser.push(`${typ}: saknas i domain.ts`);
|
||||
continue;
|
||||
}
|
||||
const block = domain.slice(start, start + 700);
|
||||
for (const falt of Object.keys(schema)) {
|
||||
// Bilagefälten ärvs via intersektion med Bilaga och står inte i blocket.
|
||||
if (["bilagaId", "bilagaHash", "dataUrl"].includes(falt)) continue;
|
||||
if (!new RegExp(`\\b${falt}\\??:`).test(block)) avvikelser.push(`${typ}.${falt}`);
|
||||
}
|
||||
}
|
||||
expect(avvikelser).toEqual([]);
|
||||
});
|
||||
|
||||
it("demoärendets händelser passerar serverns validering", async () => {
|
||||
// Slutkontrollen: allt klienten faktiskt producerar måste gå igenom.
|
||||
// Ett schema som avvisar riktig trafik är värre än inget schema.
|
||||
const { granskaHändelse } = await import("../../../../services/gemensam/handelser.mjs");
|
||||
const { byggDemoArende } = await import("../demo");
|
||||
const avvisade = byggDemoArende(1)
|
||||
.handelser.map((p) => [p.handelse.typ, granskaHändelse(p.handelse)] as const)
|
||||
.filter(([, fel]) => fel !== null)
|
||||
.map(([typ, fel]) => `${typ}: ${fel}`);
|
||||
expect([...new Set(avvisade)]).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
// @vitest-environment node
|
||||
// ALVA-RULE-200 · Ett ärende stängs aldrig utan ett varför.
|
||||
//
|
||||
// Regeln är värdelös om den går att uppfylla med "klart". Testerna nedan
|
||||
// är därför mest en katalog över hur någon med bråttom faktiskt skriver,
|
||||
// och ett krav på att systemet inte accepterar det.
|
||||
//
|
||||
// Den viktigaste regeln är den sista: en hypotes som dokumenterats och
|
||||
// inte blivit slutsatsen måste bemötas. Utan den är en felsökning en
|
||||
// gissning som råkade stämma — och det är precis den skillnaden en
|
||||
// försäkringsbedömare försöker avgöra.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
MINSTA_MOTIVERING,
|
||||
granskaFalt,
|
||||
granskaSlutsats,
|
||||
obemottaHypoteser,
|
||||
} from "../../../../services/gemensam/motivering.mjs";
|
||||
|
||||
const GILTIG = {
|
||||
typ: "slutsats",
|
||||
motivering:
|
||||
"Uppmätt spänningsfall 2,1 V över reläets slutkontakt vid belastning, mot 0,2 V som är gränsen. " +
|
||||
"Det förklarar varför bränslepumpen inte får full matning och varför felet bara uppträder varm.",
|
||||
uteslutet:
|
||||
"Bränslepumpen själv uteslöts eftersom den drar rätt ström vid direktmatning. Kablaget mättes utan anmärkning.",
|
||||
kvarstaende: "Inget. Symptomet reproducerades och försvann efter bytet.",
|
||||
atgardsval:
|
||||
"Reläet byttes i stället för att rengöras eftersom kontaktytorna är eroderade och rengöring inte återställer dem.",
|
||||
};
|
||||
|
||||
const ARBETE = [{ typ: "atgard_utford", beskrivning: "Bytte bränslepumpsrelä.", utford: true }];
|
||||
|
||||
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/);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"klart",
|
||||
"OK",
|
||||
"åtgärdat",
|
||||
"fixat",
|
||||
"se ovan",
|
||||
"trasig",
|
||||
"defekt",
|
||||
"sliten",
|
||||
"behöver bytas",
|
||||
"enligt kund",
|
||||
"vet ej",
|
||||
"-",
|
||||
"n/a",
|
||||
])("avvisar icke-svaret %s", (text) => {
|
||||
expect(granskaFalt("Motivering", text)).toMatch(/är inget skäl/);
|
||||
});
|
||||
|
||||
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/);
|
||||
});
|
||||
|
||||
it("avvisar en omformulering av slutsatsen utan orsakssamband", () => {
|
||||
// Vad, men inte varför. Detta är den vanligaste bristen och den
|
||||
// svåraste att fånga — texten ser fullgod ut tills man frågar vad
|
||||
// den egentligen påstår.
|
||||
const fel = granskaFalt(
|
||||
"Motivering",
|
||||
"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/);
|
||||
});
|
||||
|
||||
it("godtar ett resonemang med orsakssamband", () => {
|
||||
expect(granskaFalt("Motivering", GILTIG.motivering, { minsta: MINSTA_MOTIVERING, kravOrsak: true })).toBeNull();
|
||||
});
|
||||
|
||||
it("godtar konkreta mätvärden som evidensreferens även utan orsaksord", () => {
|
||||
// Den som skriver "12,4 V vid stift 14" hänvisar till en mätning
|
||||
// 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.", {
|
||||
minsta: MINSTA_MOTIVERING,
|
||||
kravOrsak: true,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
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/);
|
||||
});
|
||||
|
||||
it("en fullständig slutsats passerar", () => {
|
||||
expect(granskaSlutsats(GILTIG, ARBETE)).toEqual([]);
|
||||
});
|
||||
|
||||
it("kräver val av åtgärd när arbete utförts — men inte annars", () => {
|
||||
const utanVal = { ...GILTIG, atgardsval: undefined };
|
||||
expect(granskaSlutsats(utanVal, ARBETE).map((b) => b.falt)).toContain("atgardsval");
|
||||
// Utan utfört arbete finns inget åtgärdsval att motivera.
|
||||
expect(granskaSlutsats(utanVal, [{ typ: "atgard_utford", beskrivning: "Ingen åtgärd.", utford: false }])).toEqual([]);
|
||||
});
|
||||
|
||||
it("kvarstående osäkerhet får vara inget — men måste sägas aktivt", () => {
|
||||
expect(granskaSlutsats({ ...GILTIG, kvarstaende: "Inget." }, ARBETE)).toEqual([]);
|
||||
expect(granskaSlutsats({ ...GILTIG, kvarstaende: "" }, ARBETE).map((b) => b.falt)).toContain("kvarstaende");
|
||||
});
|
||||
|
||||
it("den ärliga vägen: orsaken kunde inte fastställas, men varför måste stå där", () => {
|
||||
const oklart = {
|
||||
typ: "slutsats",
|
||||
orsakFastställd: false,
|
||||
motivering:
|
||||
"Felet gick inte att reproducera under de förhållanden som rådde, eftersom det enligt kunden " +
|
||||
"bara uppträder vid längre körning i kyla. Ingen avvikelse kunde mätas fram vid undersökningen.",
|
||||
uteslutet: "Kablaget och kontaktdonen kontrollerades utan anmärkning vid genomgång.",
|
||||
kvarstaende: "Orsaken är inte fastställd. Fordonet bör återkomma vid symptom.",
|
||||
};
|
||||
expect(granskaSlutsats(oklart, [])).toEqual([]);
|
||||
|
||||
// Men "vet ej" duger inte som skäl till att man inte vet.
|
||||
expect(granskaSlutsats({ ...oklart, motivering: "vet ej" }, []).map((b) => b.falt)).toContain("motivering");
|
||||
});
|
||||
});
|
||||
|
||||
describe("dokumenterade hypoteser måste bemötas", () => {
|
||||
const HYPOTES = { typ: "hypotes", text: "Misstänkt glapp i hjullagret höger fram", niva: "medel" };
|
||||
|
||||
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/);
|
||||
});
|
||||
|
||||
it("hypotesen är bemött när uteslutandet nämner den", () => {
|
||||
const bemött = {
|
||||
...GILTIG,
|
||||
uteslutet:
|
||||
"Hjullagret kontrollerades utan glapp och uteslöts därför. Bränslepumpen drar rätt ström vid direktmatning.",
|
||||
};
|
||||
expect(obemottaHypoteser([...ARBETE, HYPOTES], bemött)).toEqual([]);
|
||||
expect(granskaSlutsats(bemött, [...ARBETE, HYPOTES])).toEqual([]);
|
||||
});
|
||||
|
||||
it("hypotesen är bemött när den blev den fastställda orsaken", () => {
|
||||
const logg = [
|
||||
...ARBETE,
|
||||
HYPOTES,
|
||||
{ typ: "felorsak", avvikelse: "Glapp i hjullagret höger fram, 1,2 mm axiellt.", orsaker: [], underlag: [] },
|
||||
];
|
||||
expect(obemottaHypoteser(logg, GILTIG)).toEqual([]);
|
||||
});
|
||||
|
||||
it("flera hypoteser rapporteras var för sig", () => {
|
||||
const logg = [
|
||||
...ARBETE,
|
||||
HYPOTES,
|
||||
{ typ: "hypotes", text: "Möjlig obalans i framhjulen", niva: "lag" },
|
||||
];
|
||||
const brister = granskaSlutsats(GILTIG, logg).filter((b) => b.text.includes("bemöts inte"));
|
||||
expect(brister).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("ett ärende utan hypoteser kräver inget bemötande", () => {
|
||||
expect(obemottaHypoteser(ARBETE, GILTIG)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("grinden spärrar avslutet utan slutsats", () => {
|
||||
it("saknad slutsats ger ett hinder med id slutsats_slutsats", async () => {
|
||||
const { grinda } = await import("../../../../services/gemensam/grind.mjs");
|
||||
const { ALLA_METODIKER } = await import("../../../../services/gemensam/metodiker.mjs");
|
||||
const hinder = grinda([], ALLA_METODIKER.at(-1)).map((h) => h.id);
|
||||
expect(hinder).toContain("slutsats_slutsats");
|
||||
});
|
||||
});
|
||||
@@ -189,6 +189,30 @@ export type Handelse =
|
||||
modell: string;
|
||||
}
|
||||
// Avslutet signeras av teknikern — slutsatsen får en ansvarig avsändare.
|
||||
/**
|
||||
* ALVA-RULE-200 · Teknikerns varför.
|
||||
*
|
||||
* Fyra frågor med olika adressat, som i dag blandas ihop till en rad i
|
||||
* en arbetsorder — vilket är varför verkstadsprotokoll är svåra att
|
||||
* använda i efterhand. Fälten valideras i
|
||||
* services/gemensam/motivering.mjs och spärrar avslutet.
|
||||
*/
|
||||
| {
|
||||
typ: "slutsats";
|
||||
/** Varför följer slutsatsen av evidensen? */
|
||||
motivering: string;
|
||||
/** Vad övervägdes och varför föll det bort? */
|
||||
uteslutet: string;
|
||||
/** Vad är fortfarande osäkert? Får vara "inget" — men aktivt sagt. */
|
||||
kvarstaende: string;
|
||||
/** Varför denna åtgärd och inte en annan? Krävs när arbete utförts. */
|
||||
atgardsval?: string;
|
||||
/**
|
||||
* Falskt när orsaken inte kunnat fastställas. Det är ett giltigt
|
||||
* utfall — men varför den inte kunde det är fortfarande ett varför.
|
||||
*/
|
||||
orsakFastställd?: boolean;
|
||||
}
|
||||
| { typ: "arende_avslutat"; signatur?: string };
|
||||
|
||||
export interface LoggPost {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Metodik, NastaSteg } from "@/felsokning/metodik";
|
||||
import { nastaSteg } from "@/felsokning/metodik";
|
||||
import { fasFor, klaraFaser } from "../../../../services/gemensam/faser.mjs";
|
||||
import { Fasrad } from "@/alva/komponenter";
|
||||
import { Slutsatspanel } from "@/felsokning/Slutsats";
|
||||
import { arendebeteckning, fasDefinition } from "@/alva/system";
|
||||
import {
|
||||
arAvslutat,
|
||||
@@ -1302,6 +1303,11 @@ function GuideFlik({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ALVA-RULE-200. Panelen visas när metodiken är genomförd — det
|
||||
är då varför-frågan har ett underlag att vila på. Att fråga
|
||||
tidigare vore att be om en gissning. */}
|
||||
{steg.klart && <Slutsatspanel arende={arende} skicka={skicka} />}
|
||||
|
||||
<Panel rubrik={`Metodik: ${metodik.namn} · Steg: ${steg.steg.rubrik}`}>
|
||||
{steg.klart ? (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user