Revisionen: C-1, C-2, M-2, M-3, m-1 och m-2 åtgärdade
De fyra fynd som underminerade bevisvärdet sitter i samma kodväg och åtgärdas därför tillsammans. C-1 · Härkomsten sätts av servern. anvandare kommer ur den verifierade token och tidpunkt ur serverns klocka. Klientens tidsstämpel kastas inte — vid offline-arbete är den det enda som finns — utan bevaras som registrerad_tidpunkt bredvid mottagningstiden, så glappet blir synligt i stället för osynligt. C-2 · Kvalitetsgrinden flyttad till services/gemensam/grind.mjs och utvärderas nu på servern vid arende_avslutat. Ett avslut som inte passerar får 409 med de faktiska hindren. Metodikdatan flyttades till services/gemensam/metodiker.mjs och klientens metodiker.ts är en typad återexport, så det finns fortfarande exakt en sanning. M-2 · Högvoltsspärren är ett hinder. Ett nekande svar på behörighets- eller spänningsfrihetsfrågan stoppar metodiken i stället för att räknas som besvarad, och spärrar avslutet på servern. Ett obesvarat säkerhetskrav spärrar också — tystnad är inte ett ja. Reglerna ligger i data så nästa farliga metodik bara behöver en rad. M-3 · Händelser valideras mot schema före skrivning. Loggen är append-only, så en felaktig post kan aldrig rättas — kontrollen måste ske innan, efteråt är det för sent för alltid. Okänd typ avvisas. m-1 · Delningskoden använder förkastningsurval i stället för modulo. m-2 · Id-kollisioner räknas i stället för att passera tyst. 21 nya tester låser varje regel. Ett skräddarsytt test kräver dessutom att varje typ i händelseschemat är klassificerad i delningslistan — en ny typ kan alltså varken läcka eller tappas bort. 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,188 @@
|
||||
// Kvalitetsgrinden på serversidan.
|
||||
//
|
||||
// Revisionen (docs/QUALITY-AUDIT.md, C-2) fann att grinden bara fanns i
|
||||
// webbläsaren. Ett ärende kunde därmed avslutas utan evidens genom ett
|
||||
// anrop till API:t, och rapporten presenterade det som komplett — trots
|
||||
// att dokumentationen påstod att systemet aldrig kan skriva en slutsats
|
||||
// ECM inte godkänt.
|
||||
//
|
||||
// Modulen utvärderar de obligatoriska raderna direkt mot händelseloggen.
|
||||
// Klientens ecm.ts har fler rader (råd, rekommendationer, härledd
|
||||
// evidensnivå per påstående) och fortsätter vara vägledningen i
|
||||
// gränssnittet. Det som blockerar ett avslut bor här.
|
||||
//
|
||||
// Avsiktligt: reglerna är rena funktioner av (händelser, metodik). Ingen
|
||||
// databasåtkomst, ingen tid, inget slumpmässigt — samma logg ger alltid
|
||||
// samma utfall, vilket är vad som gör en spärr granskbar.
|
||||
|
||||
const UNDANTAG_MOTIVERAT = (h) => typeof h.undantag === "string" && h.undantag.trim().length > 0;
|
||||
|
||||
// Frågor som måste besvaras jakande för att arbetet ska få avslutas.
|
||||
// Speglar SPARRFRAGOR i app/src/felsokning/metodik.ts; ett test jämför
|
||||
// listorna så de inte kan glida isär.
|
||||
export const SPARRFRAGOR = {
|
||||
"hogvolt/sakerhet/behorighet": "Behörighet för högvoltsarbete bekräftad",
|
||||
"hogvolt/sakerhet/avstangt": "Fordonet spänningslöst enligt tillverkarens rutin",
|
||||
};
|
||||
|
||||
/** Händelser av en viss typ. */
|
||||
const av = (handelser, typ) => handelser.filter((h) => h.typ === typ);
|
||||
|
||||
/**
|
||||
* Utvärderar avslutsgrinden.
|
||||
*
|
||||
* @returns lista med hinder; tom lista betyder att ärendet får avslutas.
|
||||
*/
|
||||
export function grinda(handelser, metodik) {
|
||||
const hinder = [];
|
||||
const krav = (id, rubrik, ok, detalj) => {
|
||||
if (!ok) hinder.push(detalj ? { id, rubrik, detalj } : { id, rubrik });
|
||||
};
|
||||
|
||||
krav("objekt", "Fordons-/objektidentifiering verifierad", handelser.some((h) => h.typ === "objekt_identifierat"));
|
||||
|
||||
// Historiken får besvaras nekande, men då krävs en motivering — annars
|
||||
// vore "Nej" ett sätt att hoppa över kontrollen utan att det syns.
|
||||
const historik = av(handelser, "historik_kontrollerad").at(-1);
|
||||
krav(
|
||||
"historik",
|
||||
"Fordonshistorik kontrollerad eller motiverad",
|
||||
Boolean(historik) && (historik.kontrollerad || (historik.kommentar ?? "").trim().length > 0),
|
||||
historik ? "Nekad historikkontroll kräver en motivering." : "Ingen historikkontroll dokumenterad.",
|
||||
);
|
||||
|
||||
for (const [lage, rubrik] of [
|
||||
["ingaende", "Ingående mätarställning dokumenterad"],
|
||||
["utgaende", "Utgående mätarställning dokumenterad"],
|
||||
]) {
|
||||
krav(`matarstallning_${lage}`, rubrik, av(handelser, "matarstallning").some((h) => h.lage === lage));
|
||||
}
|
||||
|
||||
krav(
|
||||
"reproducering",
|
||||
"Symptomverifiering: reproducerat eller dokumenterat ej reproducerbart",
|
||||
handelser.some((h) => h.typ === "reproducering"),
|
||||
);
|
||||
|
||||
krav("felorsak", "Felorsaksanalys dokumenterad", handelser.some((h) => h.typ === "felorsak"));
|
||||
|
||||
// Åtgärdskedjan. En utebliven åtgärd är ett giltigt utfall — men bara
|
||||
// när skälet står i loggen.
|
||||
const atgarder = av(handelser, "atgard_utford");
|
||||
const utfordArbete = atgarder.some((h) => (h.text ?? "").trim().length > 0);
|
||||
krav(
|
||||
"atgard",
|
||||
"Åtgärd dokumenterad eller motiverad",
|
||||
atgarder.length > 0,
|
||||
"Varken utförd åtgärd eller skäl till att den uteblev finns dokumenterat.",
|
||||
);
|
||||
|
||||
if (utfordArbete) {
|
||||
const beslut = av(handelser, "kundbeslut");
|
||||
krav("kundbeslut", "Kundens besked på åtgärdsförslaget registrerat", beslut.length > 0);
|
||||
|
||||
// Hårt fel, inte en varning: arbete utfört trots att kunden avböjt.
|
||||
krav(
|
||||
"avbojt_men_utfort",
|
||||
"Utfört arbete trots avböjt åtgärdsförslag",
|
||||
!beslut.some((h) => h.utfall === "avbojt"),
|
||||
"Kunden avböjde förslaget men arbete har dokumenterats som utfört.",
|
||||
);
|
||||
|
||||
krav(
|
||||
"kvalitetskontroll",
|
||||
"Kvalitetskontroll genomförd — symptomet verifierat",
|
||||
handelser.some((h) => h.typ === "kvalitetskontroll"),
|
||||
);
|
||||
}
|
||||
|
||||
// Metodikens kontroller: varje kontroll ska bära evidens eller ett
|
||||
// dokumenterat undantag. Det är den rad som gör metodiken bindande.
|
||||
const utforda = new Map();
|
||||
for (const h of av(handelser, "kontroll_utford")) utforda.set(`${h.stegId}/${h.kontrollId}`, h);
|
||||
|
||||
const saknade = [];
|
||||
const utanFoto = [];
|
||||
const foton = av(handelser, "foto").length;
|
||||
for (const steg of metodik?.steg ?? []) {
|
||||
for (const kontroll of steg.kontroller ?? []) {
|
||||
const h = utforda.get(`${steg.id}/${kontroll.id}`);
|
||||
if (!h) {
|
||||
saknade.push(`${steg.rubrik} · ${kontroll.text}`);
|
||||
continue;
|
||||
}
|
||||
if (UNDANTAG_MOTIVERAT(h)) continue;
|
||||
if (!(h.resultat ?? "").trim()) saknade.push(`${steg.rubrik} · ${kontroll.text} (utan resultat)`);
|
||||
else if (kontroll.krav === "foto") utanFoto.push(kontroll.text);
|
||||
}
|
||||
}
|
||||
krav(
|
||||
"metodik_kontroller",
|
||||
"Metodikens kontroller: evidens eller dokumenterat undantag",
|
||||
saknade.length === 0,
|
||||
saknade.slice(0, 5).join(" · "),
|
||||
);
|
||||
krav("foton", "Foton finns för fotokrävande kontroller", utanFoto.length === 0 || foton >= utanFoto.length);
|
||||
|
||||
// Säkerhetsspärrar. Ett nekande svar på en spärrfråga får aldrig
|
||||
// passera som "besvarad" — se SPARRFRAGOR i klientens metodik.ts.
|
||||
// Klientens spärr är vägledning; den här är hindret (QUALITET M-2).
|
||||
for (const [nyckel, text] of Object.entries(SPARRFRAGOR)) {
|
||||
const [metodikId, stegId, frageId] = nyckel.split("/");
|
||||
if (metodik?.id !== metodikId) continue;
|
||||
const svaret = av(handelser, "fraga_besvarad").findLast(
|
||||
(h) => h.stegId === stegId && h.frageId === frageId,
|
||||
);
|
||||
krav(`sparr_${frageId}`, text, svaret?.svar === "Ja", "Säkerhetskravet är inte uppfyllt.");
|
||||
}
|
||||
|
||||
// Evidensnivån måste överstiga E0 — annars avslutas ett ärende utan
|
||||
// att någonting alls har dokumenterats.
|
||||
krav(
|
||||
"evidens",
|
||||
"Evidensnivå över E0",
|
||||
handelser.some((h) => ["foto", "video", "matvarde", "observation", "kontroll_utford"].includes(h.typ)),
|
||||
"Ingen evidens av något slag finns i loggen.",
|
||||
);
|
||||
|
||||
return hinder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra krav som följer av ärendetypen. Reglerna kommer från regelpaketet
|
||||
* (ECM Knowledge Library) och är alltså data, inte kod.
|
||||
*/
|
||||
export function grindaArendetyp(handelser, regelpaket) {
|
||||
const typ = av(handelser, "arendetyp_satt").at(-1)?.arendetyp;
|
||||
const regler = regelpaket?.arendetyper?.[typ]?.krav ?? [];
|
||||
const hinder = [];
|
||||
|
||||
const uppfyllt = {
|
||||
miltal: () => av(handelser, "matarstallning").length > 0,
|
||||
historik: () => av(handelser, "historik_kontrollerad").some((h) => h.kontrollerad),
|
||||
foto: () => av(handelser, "foto").length > 0,
|
||||
claim: () => harReferens(handelser, ["ao_claim", "claim"]),
|
||||
skadenummer: () => harReferens(handelser, ["ao_skadenummer", "skadenummer"]),
|
||||
};
|
||||
|
||||
for (const regel of regler) {
|
||||
const kontroll = uppfyllt[regel.typ ?? regel];
|
||||
// En okänd kravtyp får aldrig tolkas som uppfylld. Ett regelpaket som
|
||||
// inte går att utvärdera ska stoppa avslutet, inte släppa igenom det.
|
||||
if (!kontroll) {
|
||||
hinder.push({ id: `arendetyp_${regel.typ ?? regel}`, rubrik: `Okänt krav i regelpaketet: ${regel.typ ?? regel}` });
|
||||
} else if (!kontroll()) {
|
||||
hinder.push({ id: `arendetyp_${regel.typ ?? regel}`, rubrik: regel.rubrik ?? `Krav för ärendetypen: ${regel.typ ?? regel}` });
|
||||
}
|
||||
}
|
||||
return hinder;
|
||||
}
|
||||
|
||||
function harReferens(handelser, nycklar) {
|
||||
for (const h of av(handelser, "arbetsorder_skannad")) {
|
||||
for (const f of h.falt ?? []) {
|
||||
if (nycklar.includes(f.id) && String(f.varde ?? "").trim()) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user