31727a4ea0
C-5. granskaHändelse itererade schemats nycklar och aldrig händelsens, så okända fält accepterades och sparades ordagrant. Två garantier vilade på motsatsen: krypto-shreddingen skyddar en fast lista av fältnamn, så personuppgifter på en vanlig observation krypterades aldrig och överlevde raderingen — och delningsfiltret är typnivå, så samma fält gick ut i kundens delningslänk. Schemat är nu stängt, med varje valfritt fält deklarerat. Det gäller även `kalla`: protokollinläsningen fungerade bara därför att schemat var öppet. Avslaget är hårt, inte en tyst strykning. Verifierat mot riktig trafik — samtliga händelser som klienten faktiskt producerar passerar. M-7. Klienten upprepar inte längre grindens regel utan anropar grinda() och visar dess egna hinder. Villkoret hade glidit isär två gånger utan att något test märkte det. Det avslöjade omedelbart ett verkligt fel i grinden: den krävde textresultat även på kontroller vars krav är foto — trots att gränssnittet märker fältet "Observation (valfritt)". Servern hade alltså nekat avslut på nästan varje riktigt ärende, och det syntes inte så länge klienten hade ett eget och mildare villkor. Evidens graderas nu efter kontrollens eget krav. M-8. Protokollinläsningen svarar med utfall per händelse i stället för en siffra, 207 vid delvis lyckad inläsning, innehållshärledda id:n i stället för klockan, och en transaktion runt hela importen. M-9. En genererad webhookhemlighet lämnas ut en gång vid skapandet. m-8. Profilens vägslagning begränsas till egna egenskaper. m-10. Ett mätvärde som kommer ur en kontroll redovisas en gång. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
218 lines
8.7 KiB
JavaScript
218 lines
8.7 KiB
JavaScript
// 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.
|
|
|
|
import { granskaSlutsats } from "./motivering.mjs";
|
|
|
|
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.utford === true);
|
|
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.beslut === "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 fotokravande = [];
|
|
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;
|
|
|
|
// En fotokontroll verifieras av fotot. Regeln krävde tidigare ett
|
|
// textresultat även här — trots att gränssnittet märker fältet
|
|
// "Observation (valfritt)". Grinden nekade därmed avslut på nästan
|
|
// varje riktigt ärende, vilket inte syntes så länge klienten hade
|
|
// ett eget och mildare villkor (QUALITY-AUDIT-2 · M-7).
|
|
if (kontroll.krav === "foto") {
|
|
fotokravande.push(kontroll.text);
|
|
continue;
|
|
}
|
|
if (!(h.resultat ?? "").trim()) saknade.push(`${steg.rubrik} · ${kontroll.text} (utan resultat)`);
|
|
}
|
|
}
|
|
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",
|
|
foton >= fotokravande.length,
|
|
fotokravande.length ? `${fotokravande.length} kontroller kräver foto, ${foton} foton i loggen.` : undefined,
|
|
);
|
|
|
|
// ALVA-RULE-200 · Slutsatsen. Ett ärende stängs aldrig utan att
|
|
// teknikern lämnat ett varför — motiveringen som knyter slutsatsen
|
|
// till underlaget, vad som uteslöts, och vad som kvarstår osäkert.
|
|
//
|
|
// Detta är den rad som saknas i varje verkstadsprotokoll: underlaget
|
|
// säger vad som mättes, slutsatsen vad som är fel, men ingenting
|
|
// 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 });
|
|
}
|
|
|
|
// 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;
|
|
}
|