1ba5aaaefa
Fyra av granskningens fynd åtgärdade, i bevisvärdesordning. HASHKEDJAN (ALVA-SPEC-070). Triggrar skyddar loggen mot applikationen, inte mot den som äger databasen — det var granskningens allvarligaste invändning mot ett system vars hela värde är bevisvärde. Varje händelse bär nu en hash av sitt innehåll och föregående händelses hash, beräknad av servern vid insättningen. All skrivning går genom en enda kedjande funktion; en händelse vid sidan av kedjan är ett hål i beviset, så den bekväma vägen förbi finns inte. Digest tas över den LAGRADE händelsen, efter kryptering: verifieringen ska kunna räkna om den ur databasen för all framtid, och krypto-shredding förstör nycklar, inte rader, så kedjan överlever en radering. Radlås per ärende hindrar att två samtidiga batchar forkar kedjan — en falsk larmande verifiering avfärdas snart som trasig, och då är den värdelös. Integrationstestet provar hotmodellen ordagrant: triggern släpps, en rad ändras med full databasbehörighet, triggern återskapas. Verifieringen pekar ut raden — inte bara att något är fel, utan vilken. FÖRSEGLINGEN. Avslut skriver kedjans rot och en HMAC med en nyckel som aldrig finns i databasen, i samma transaktion som avslutshändelsen. Den som räknar om hela kedjan efter sin ändring stoppas av att förseglingen inte går att räkna om utan nyckeln. Engångs: triggern vägrar ändra en satt försegling. Svaret säger vad det bevisar och inte — innehållet är oförändrat sedan mottagandet, ingenting om tiden före, ingenting om sanningshalten. Den texten följer med in i varje rapport som citerar svaret, för det är precis den skillnad en motpartsjurist annars hittar. SIGNATUREN. Fältet hette signatur men var teknikerns egen text — det inbjöd en jurist att tro något som inte gällde. Det skrivs nu ur verifierad token som övriga härkomstfält och intygar exakt vad det kan intyga: vem som var inloggad när avslutet togs emot. SÄKERHETSNIVÅN (ALVA-SPEC-071). Var teknikerns fria val — ett självskattat värde som ser ut som en mätning. Nu ett tak härlett ur underlaget: hög kräver reproducerat symptom OCH spårbart mätvärde ur mätdonsregistret; enbart observationer bär inte ens medel. Teknikern kan sänka men aldrig höja — asymmetrin är poängen, ärlig osäkerhet är information. Grinden spärrar påståenden över taket på alla tio språken, och gränssnittet visar taket medan arbetet pågår i stället för att spara beskedet till avslutsknappen. "Delvis reproducerat" bär inte hög: delvis är ett annat ord för att felet inte är förstått. Taket bet direkt i två av våra egna testfixturer som påstod hög utan spårbart mätdon — vilket är regeln som fungerar, inte testet som är fel. Genomgången avslöjade följdkravet: vid medel/låg kräver panelen att teknikern anger vilka ytterligare kontroller som skulle stärka bedömningen, och det fältet fylls nu i som en tekniker skulle. Kvar ur granskningens lista, medvetet: extern förankring (RFC 3161), klienthashat foto vid upptagning, gränsvärden som data, OIDC/SAML. 766 tester, 200 integrationskontroller mot riktig Postgres — inklusive sabotage som databasägare — genomgång 4/4, portalspärr, typkontroll, lint och artefaktmätning gröna. Utgåva 3.3, API-specen uppdaterad, åtgärderna bokförda i panelrapportens bilaga A. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
277 lines
12 KiB
JavaScript
277 lines
12 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";
|
|
import { inomTak, sakerhetstak } from "./sakerhet.mjs";
|
|
import { STANDARD, arJakande, arendetypNu, t } from "./sprak/index.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": "grind.hogvolt.behorighet",
|
|
"hogvolt/sakerhet/avstangt": "grind.hogvolt.spanningslos",
|
|
};
|
|
|
|
/** 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, sprak = STANDARD) {
|
|
const hinder = [];
|
|
|
|
// Hindret bär både nyckeln och den färdiga texten. Nyckeln därför att
|
|
// en klient som byter språk ska kunna rendera om utan att fråga
|
|
// servern; texten därför att ett hinder som passerar genom en logg
|
|
// eller en PDF måste vara läsbart utan katalogen.
|
|
const krav = (id, nyckel, ok, detaljNyckel, variabler) => {
|
|
if (ok) return;
|
|
const post = { id, nyckel, rubrik: t(sprak, nyckel, variabler) };
|
|
if (detaljNyckel) {
|
|
post.detaljNyckel = detaljNyckel;
|
|
post.detalj = t(sprak, detaljNyckel, variabler);
|
|
}
|
|
hinder.push(post);
|
|
};
|
|
|
|
// Fritext som kommer ur data, inte ur katalogen — namnen på metodikens
|
|
// kontroller. De står på metodikens språk och kan inte översättas här.
|
|
const kravMedData = (id, nyckel, ok, detalj) => {
|
|
if (ok) return;
|
|
const post = { id, nyckel, rubrik: t(sprak, nyckel) };
|
|
if (detalj) post.detalj = detalj;
|
|
hinder.push(post);
|
|
};
|
|
|
|
krav("objekt", "grind.objekt", 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",
|
|
"grind.historik",
|
|
Boolean(historik) && (historik.kontrollerad || (historik.kommentar ?? "").trim().length > 0),
|
|
historik ? "grind.historik.nekad" : "grind.historik.saknas",
|
|
);
|
|
|
|
// Mätarställningen ska vara FOTOGRAFERAD, inte bara inskriven.
|
|
//
|
|
// Skillnaden är hela dess bevisvärde. En inskriven siffra är teknikerns
|
|
// påstående om vad som stod på mätaren; ett foto visar vad som stod
|
|
// där. Mätarställningen är samtidigt det som avgör garanti- och
|
|
// försäkringsfrågor i efterhand, och den enda uppgift i ärendet som
|
|
// någon kan ha ett intresse av att skriva fel.
|
|
//
|
|
// Klienten krävde redan fotot — men bara klienten, och en regel som
|
|
// bara finns i gränssnittet är en vana, inte en spärr. Det är samma
|
|
// förhållande som QUALITET C-2 gällde, fast åt andra hållet: här var
|
|
// det servern som var svagare, vilket är värre, eftersom servern är
|
|
// den som är auktoritativ.
|
|
//
|
|
// Undantag finns och måste finnas — en trasig display, en timräknare
|
|
// som sitter oåtkomligt, en spegling som inte går att fota bort. Men
|
|
// undantaget kräver en motivering, av samma skäl som ett nekat
|
|
// historiksvar gör det.
|
|
for (const [lage, nyckel] of [
|
|
["ingaende", "grind.matarstallning.ingaende"],
|
|
["utgaende", "grind.matarstallning.utgaende"],
|
|
]) {
|
|
const poster = av(handelser, "matarstallning").filter((h) => h.lage === lage);
|
|
const dokumenterad = poster.length > 0;
|
|
const fotograferad = poster.some((h) => h.bilagaId || h.dataUrl);
|
|
const motiverat = poster.some((h) => (h.undantag ?? "").trim().length > 0);
|
|
krav(
|
|
`matarstallning_${lage}`,
|
|
nyckel,
|
|
dokumenterad && (fotograferad || motiverat),
|
|
dokumenterad ? "grind.matarstallning.ej_foto" : "grind.matarstallning.saknas",
|
|
);
|
|
}
|
|
|
|
krav("reproducering", "grind.reproducering", handelser.some((h) => h.typ === "reproducering"));
|
|
|
|
krav("felorsak", "grind.felorsak", handelser.some((h) => h.typ === "felorsak"));
|
|
|
|
// Säkerhetsnivån är ett tak, inte ett val (ALVA-SPEC-071). En felorsak
|
|
// som påstår högre säkerhet än underlaget bär spärrar avslutet — inte
|
|
// för att slutsatsen är fel, utan för att PÅSTÅENDET om den är det.
|
|
// Sänkning prövas aldrig: ärlig osäkerhet är information.
|
|
const felorsaken = av(handelser, "felorsak").at(-1);
|
|
if (felorsaken?.sakerhet) {
|
|
const tak = sakerhetstak(handelser);
|
|
krav(
|
|
"sakerhetstak",
|
|
"grind.sakerhet",
|
|
inomTak(felorsaken.sakerhet, tak),
|
|
"grind.sakerhet.detalj",
|
|
{ niva: felorsaken.sakerhet, tak },
|
|
);
|
|
}
|
|
|
|
// Å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", "grind.atgard", atgarder.length > 0, "grind.atgard.saknas");
|
|
|
|
if (utfordArbete) {
|
|
const beslut = av(handelser, "kundbeslut");
|
|
krav("kundbeslut", "grind.kundbeslut", beslut.length > 0);
|
|
|
|
// Hårt fel, inte en varning: arbete utfört trots att kunden avböjt.
|
|
krav(
|
|
"avbojt_men_utfort",
|
|
"grind.kundbeslut.avbojt",
|
|
!beslut.some((h) => h.beslut === "avbojt"),
|
|
"grind.kundbeslut.avbojt.detalj",
|
|
);
|
|
|
|
krav("kvalitetskontroll", "grind.kvalitetskontroll", 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)`);
|
|
}
|
|
}
|
|
kravMedData("metodik_kontroller", "grind.kontroller", saknade.length === 0, saknade.slice(0, 5).join(" · "));
|
|
krav("foton", "grind.foton", foton >= fotokravande.length, fotokravande.length ? "grind.foton.detalj" : undefined, {
|
|
kontroller: fotokravande.length,
|
|
foton,
|
|
});
|
|
|
|
// 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, sprak)) {
|
|
hinder.push({ id: `slutsats_${brist.falt}`, nyckel: brist.nyckel, 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 [fraga, nyckel] of Object.entries(SPARRFRAGOR)) {
|
|
const [metodikId, stegId, frageId] = fraga.split("/");
|
|
if (metodik?.id !== metodikId) continue;
|
|
const svaret = av(handelser, "fraga_besvarad").findLast(
|
|
(h) => h.stegId === stegId && h.frageId === frageId,
|
|
);
|
|
krav(`sparr_${frageId}`, nyckel, arJakande(svaret?.svar), "grind.sparr.ej_uppfyllt");
|
|
}
|
|
|
|
// Evidensnivån måste överstiga E0 — annars avslutas ett ärende utan
|
|
// att någonting alls har dokumenterats.
|
|
krav(
|
|
"evidens",
|
|
"grind.evidens",
|
|
handelser.some((h) => ["foto", "video", "matvarde", "observation", "kontroll_utford"].includes(h.typ)),
|
|
"grind.evidens.saknas",
|
|
);
|
|
|
|
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, sprak = STANDARD) {
|
|
// Ärendetypen slås upp i sin nuvarande form. Ett ärende som öppnades
|
|
// när typerna hette "Garanti" ska få garantins extra krav även efter
|
|
// att de bytt språk — se ARENDETYP_ARV.
|
|
const typ = arendetypNu(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.
|
|
const krav = regel.typ ?? regel;
|
|
if (!kontroll) {
|
|
hinder.push({
|
|
id: `arendetyp_${krav}`,
|
|
nyckel: "grind.arendetyp.okant",
|
|
rubrik: t(sprak, "grind.arendetyp.okant", { krav }),
|
|
});
|
|
} else if (!kontroll()) {
|
|
// Regelpaketets egen rubrik står på regelpaketets språk och går
|
|
// inte att översätta här — den är data från kunden, inte vår text.
|
|
hinder.push({
|
|
id: `arendetyp_${krav}`,
|
|
nyckel: regel.rubrik ? undefined : "grind.arendetyp.krav",
|
|
rubrik: regel.rubrik ?? t(sprak, "grind.arendetyp.krav", { krav }),
|
|
});
|
|
}
|
|
}
|
|
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;
|
|
}
|