Files
alva/felsokning/services/gemensam/grind.mjs
T
Claude ae5acbed92 Fotograferad mätarställning, versionshistorik och tysk kravbild i foten
---- Mätarställningen ska fotograferas ----------------------------------

Klienten krävde redan fotot: värdefältet visas först efter bilden. Men
GRINDEN gjorde det inte, 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 servern svagare, vilket är värre, eftersom
servern är den auktoritativa.

Skillnaden är hela uppgiftens 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 avgör garanti- och försäkringsfrågor i
efterhand och är den enda uppgift i ärendet någon kan ha ett intresse av
att skriva fel. Evidensgraderingen följer nu med: fotograferad ger E2,
inskriven ger E1 och märks som inskriven i rapporten.

Undantag finns och måste finnas — slocknad display, oåtkomlig timräknare
— men kräver motivering, som det nekade historiksvaret.

---- Versionen räknades aldrig upp --------------------------------------

Systemet påstod "ALVA 1.0" efter tre revisioner, en fakturamodul och en
stängd portal. Versionen sa alltså ingenting om vad man fick.

Historiken är HÄRLEDD UR VAD SOM FAKTISKT LEVERERATS och varje post pekar
på sin commit. En utgåva som inte går att peka på hör inte hemma i listan
— en påhittad ändringslogg är samma obefogade säkerhet som produkten
finns för att undvika. Första siffran höjs bara när GARANTIERNA ändras,
vilket hänt två gånger: 2.0 när fakturan blev ett härlett dokument, 3.0
när TÜV-härdningen ändrade vad loggen garanterar. Nuvarande: 3.1.

Och ett fynd på vägen: ritningsstämpeln läste versionen ur den AKTUELLA
konstanten, så ett ärende stängt under 1.0 visade 3.1 — precis det
stämpelns egen beskrivning säger att den inte ska göra. Avslutshändelsen
bär numera versionen, och stämpeln läser den därifrån. Äldre ärenden
säger uttryckligen "ej registrerad" i stället för att låna dagens siffra.

---- Tysk kravbild i foten ----------------------------------------------

Listan är tysk därför att § 5 DDG är den strängaste och mest utkrävbara i
EU: klarar man den klarar man de andra marknaderna på köpet.

Två fällor som är aktuella just nu, och bägge ser fortfarande ut som
upplysningar:

  ODR-LÄNKEN SKA BORT. Förordning 524/2013 upphävdes genom förordning
  (EU) 2024/3228 och plattformen stängdes 20 juli 2025. Länken har legat
  i tiotusentals fotnoter sedan 2016 och pekar numera ingenstans. Foten
  saknar den medvetet.

  § 5 TMG HETER § 5 DDG sedan 14 maj 2024.

Modulen hittar inte på uppgifter. Ett Impressum med påhittad adress är
inte ett halvfärdigt Impressum utan ett vilseledande, och skadan större
än den tomma rutans. Osatta fält redovisas som en åtgärdslista med
rättslig grund, så driften kan spärra en driftsättning på listan i
stället för att upptäcka bristen när brevet kommer.

460 enhetstester · genomgången 4/4 · typkontroll ren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
2026-08-06 15:15:35 +00:00

247 lines
10 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.",
);
// 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, rubrik] of [
["ingaende", "Ingående mätarställning fotograferad"],
["utgaende", "Utgående mätarställning fotograferad"],
]) {
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}`,
rubrik,
dokumenterad && (fotograferad || motiverat),
!dokumenterad
? "Ingen mätarställning dokumenterad."
: "Mätarställningen är inskriven men inte fotograferad. Fotografera mätaren eller ange varför det inte går.",
);
}
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;
}