322d228c80
Slutsatsgranskningen avvisar icke-svar med en ordlista: "klart", "ok", "trasig". Listan var enbart svensk. En tysk tekniker som skrev "erledigt" i motiveringsfältet passerade filtret — inte därför att texten dög, utan därför att filtret bara kände igen "klart". Grinden släppte igenom avslutet och rapporten såg fullständig ut. Det var alltså inte en översättningsbrist utan ett hål i den regel som är hela produktens värde, och hålet fanns på varje marknad utom en. Samma sak åt andra hållet: orsaksorden som visar att en text innehåller ett resonemang var svenska, liksom evidensorden. Engelskans "photo" innehåller inte "foto", så "the photo shows the arcing at pin 14" räknades inte som en hänvisning till underlag. Följden hade varit värre än ett släppt avslut — en korrekt skriven motivering hade nekats, och en spärr som nekar rätt svar är en spärr man lär sig kringgå. Listorna ligger nu i sprak/ord.mjs, per språk, och slås ihop till en union i stället för att väljas efter inställning. Ett icke-svar är ett icke-svar oavsett språk — jämförelsen görs mot hela fältet, så en tekniker som skriver på ett annat språk än organisationens ska inte kunna passera på den vägen. Orsaks- och evidensorden går åt motsatt håll: unionen gör kontrollen mer tillåtande, aldrig strängare, och det är rätt riktning att fela åt. Medvetet utanför listorna: "inget", "keine", "none". Att ingenting kvarstår osäkert är ett giltigt och ofta korrekt svar; att göra det till ett icke-svar hade tvingat fram utfyllnad, och utfyllnad är sämre än ett kort sant svar. Ett test låser det. Bristernas texter kommer nu ur katalogen och bär sin nyckel, precis som grindens hinder. Fältnamnet översätts med resten av meningen — "Begründung fehlt.", inte "Motivering fehlt.". Mutationsprövat: med listan återställd till enbart svenska föll nio tester, ett per språk. Svenska och engelska är genomgångna; övriga listor är en första uppsättning och står som ogranskade i filhuvudet, av samma skäl som `granskat` i SPRAK — de är metodik, inte gränssnitt. 597 tester, 186 integrationskontroller, typkontroll, lint och genomgång gröna. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
257 lines
11 KiB
JavaScript
257 lines
11 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 { STANDARD, 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"));
|
|
|
|
// Å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, svaret?.svar === "Ja", "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) {
|
|
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.
|
|
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;
|
|
}
|