Regelverk, ärendetyper och säkerhetssvar på engelska — med arv där data byter språk

ecm.ts bar regelverkets rådtexter: undantagsorsaker, orsakskategorier,
underlagskällor, evidensgrader och de meningar som säger vad som fattas.
Cirka hundra strängar, nu på engelska tillsammans med serverns
ecm-regler.json — klienten och regelpaketet måste säga samma sak, annars
är det inte samma regel.

Tre av dem var inte text utan DATA, och där gjordes bytet med arv i
stället för rakt av.

ÄRENDETYPEN ligger i loggen och slår upp regelpaketets extra krav. Ett
gammalt garantiärende vars typ heter "Garanti" hade inte matchat
"Warranty", fått noll extra krav, och släppts igenom grinden med FÄRRE
krav än när det öppnades — utan att någonting i gränssnittet visade det.
ARENDETYP_ARV normaliserar typen vid uppslaget, aldrig i loggen.

GALLRINGSTIDEN slås upp på samma nyckel, och det var värre. Ett
garantiärende hade fallit ur tabellen och tagit standardtiden 36 månader
i stället för 120 — alltså raderat garantiunderlaget sju år i förtid,
tyst och oåterkalleligt. Ett uppslag som missar ska ge det försiktigaste
utfallet; här gav det det farligaste. Två tester låser det.

SÄKERHETSSPÄRREN avgörs av en textsträng. När knappen bytte från "Ja"
till "Yes" gick klient och server isär: klienten skrev "Yes", servern
jämförde mot "Ja". Utfallet blev fail-closed — högvoltsärenden gick inte
att avsluta alls — vilket är rätt riktning för ett fel att falla åt, men
det var en slump och inte en konstruktion. `arJakande` känner igen ett ja
på alla språken. Att en säkerhetsspärr avgörs av fritext är i sig felet;
rätt lösning är ett booleskt fält, och att arvet står synligt i koden är
en påminnelse om det.

Pre-diagnostikens kvitteringar känns igen på sin text och har fått samma
behandling: gamla ärenden hade annars visat en ogjord kontroll för arbete
som faktiskt utförts, och teknikern hade kvitterat en gång till — två
kvitteringar för samma sak i en append-only logg.

Genomgången drev gränssnittet på svenska knappnamn och gick sönder på
rätt sätt: den hittade "+ Dokumentera felorsak", "Registrera besked" och
kundkanalerna "Telefon"/"E-post", som alla missats av de tidigare
svepen. Fyra av fyra ärenden avslutas igen.

609 tester, 186 integrationskontroller mot riktig Postgres, genomgång,
portalspärr, typkontroll och lint gröna. Mutationsprövat: utan
ARENDETYP_ARV faller två tester, utan `arJakande` ett.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-06 18:05:19 +00:00
parent 3ea14575f1
commit 8aae3a2a97
20 changed files with 589 additions and 414 deletions
+6 -3
View File
@@ -16,7 +16,7 @@
// samma utfall, vilket är vad som gör en spärr granskbar.
import { granskaSlutsats } from "./motivering.mjs";
import { STANDARD, t } from "./sprak/index.mjs";
import { STANDARD, arJakande, arendetypNu, t } from "./sprak/index.mjs";
const UNDANTAG_MOTIVERAT = (h) => typeof h.undantag === "string" && h.undantag.trim().length > 0;
@@ -190,7 +190,7 @@ export function grinda(handelser, metodik, sprak = STANDARD) {
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");
krav(`sparr_${frageId}`, nyckel, arJakande(svaret?.svar), "grind.sparr.ej_uppfyllt");
}
// Evidensnivån måste överstiga E0 — annars avslutas ett ärende utan
@@ -210,7 +210,10 @@ export function grinda(handelser, metodik, sprak = STANDARD) {
* (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;
// Ä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 = [];
@@ -24,6 +24,7 @@
// sämre i praktiken — då raderas beviset tillsammans med personuppgiften.
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
import { arendetypNu } from "./sprak/index.mjs";
/**
* Fält som räknas som identifierande och därför krypteras.
@@ -134,16 +135,27 @@ export function öppnaHändelse(handelse, nycklar) {
* den som inte gör ett aktivt val får det försiktigaste.
*/
export const GALLRING_MANADER = {
Garanti: 120, // Följer den längsta rimliga garantitiden på en drivlina.
Warranty: 120, // Följer den längsta rimliga garantitiden på en drivlina.
Goodwill: 120,
Försäkring: 120, // Preskription för försäkringskrav.
Reklamation: 36, // Konsumentköplagens reklamationsrätt.
Begagnatgaranti: 60,
Insurance: 120, // Preskription för försäkringskrav.
Complaint: 36, // Konsumentköplagens reklamationsrätt.
"Used-vehicle warranty": 60,
standard: 36,
};
/**
* Gallringsdatum för ett avslutat ärende.
*
* Ärendetypen normaliseras genom ARENDETYP_ARV innan den slås upp. Det
* är inte en artighet mot gamla data utan en spärr: när typerna bytte
* språk hade ett garantiärende vars logg säger "Garanti" fallit ur
* tabellen och tagit standardvärdet 36 månader i stället för 120 —
* alltså raderat garantiunderlaget sju år för tidigt, tyst, och
* oåterkalleligt. Ett uppslag som missar ska ge det FÖRSIKTIGASTE
* utfallet; här gav det det farligaste.
*/
export function gallringsdatum(arendetyp, avslutat, manader = GALLRING_MANADER) {
const antal = manader[arendetyp] ?? manader.standard;
const antal = manader[arendetypNu(arendetyp)] ?? manader.standard;
const d = new Date(avslutat);
d.setMonth(d.getMonth() + antal);
return d;
@@ -187,3 +187,51 @@ export function metodikvarning(sprak) {
if (!s || s.granskat) return null;
return t(sprak, "metodik.ogranskad", { sprak: s.egetNamn });
}
/**
* Ärendetyper som skrevs på svenska innan engelska blev källspråket.
*
* Ärendetypen är DATA: den ligger i händelseloggen och slår upp
* regelpaketets extra krav. Ett byte av språk får därför inte göras tyst
* — ett gammalt garantiärende vars typ heter "Garanti" hade annars inte
* matchat "Warranty", fått noll extra krav, och släppts igenom grinden
* med FÄRRE krav än när det öppnades. Ingenting i gränssnittet hade
* visat det.
*
* Tabellen är därför en del av regeln, inte en bekvämlighet, och den tas
* bort först när ingen logg innehåller de gamla värdena.
*/
export const ARENDETYP_ARV = {
"Privat kund": "Private customer",
Företagskund: "Business customer",
Garanti: "Warranty",
Försäkring: "Insurance",
Reklamation: "Complaint",
Begagnatgaranti: "Used-vehicle warranty",
"Intern kvalitetskontroll": "Internal quality check",
"Teknisk utredning": "Technical investigation",
};
/** Ärendetypen i sin nuvarande form, oavsett vilket språk den skrevs i. */
export const arendetypNu = (typ) => ARENDETYP_ARV[typ] ?? typ;
/**
* Jakande svar på en säkerhetsspärr.
*
* Svaret på en ja/nej-fråga ligger i loggen som text, och spärrfrågorna
* släpper bara igenom ett jakande svar. När knappen bytte från "Ja" till
* "Yes" gick de två isär: klienten skrev "Yes", servern jämförde mot
* "Ja". Utfallet blev fail-closed — högvoltsärenden gick inte att
* avsluta alls — vilket är rätt riktning för ett fel att falla åt, men
* det var en slump och inte en konstruktion.
*
* Det verkliga felet är att en säkerhetsspärr avgörs av en textsträng.
* Rätt lösning är ett booleskt fält i händelsen; tills det finns står
* båda formerna här, och de gäller alla språk samtidigt av samma skäl
* som ordlistorna i ord.mjs gör det: den som svarar ska inte kunna
* missas för att organisationens språkinställning var en annan.
*/
export const JAKANDE = new Set(["Yes", "Ja", "Oui", "Sí", "Si", "Sì", "Tak", "Da", "Sim", "Ja."]);
/** Är svaret jakande, oavsett vilket språk knappen stod på? */
export const arJakande = (svar) => JAKANDE.has(String(svar ?? "").trim());
+84 -39
View File
@@ -2,58 +2,103 @@
"version": "2.0",
"beskrivning": "ECM Knowledge Library — serverdistribuerat regelpaket. Uppdateras utan appändring: redigera filen (eller montera en ConfigMap över den i klustret) och starta om tjänsten. Klienten hämtar paketet vid inloggning och faller tillbaka till sitt inbyggda standardpaket offline.",
"arendetypRegler": {
"Garanti": [
{ "id": "garanti_miltal", "rubrik": "Miltal dokumenterat", "krav": "miltal", "detaljVidBrist": "Garantiärenden kräver dokumenterad mätarställning." },
{ "id": "garanti_historik", "rubrik": "Servicehistorik kontrollerad", "krav": "historik", "detaljVidBrist": "Garantiärenden kräver kontrollerad servicehistorik." },
{ "id": "garanti_claim", "rubrik": "Claim-/garantinummer registrerat", "krav": "claim", "detaljVidBrist": "Ange claim-/garantinummer (läses ur arbetsordern)." }
"Warranty": [
{
"id": "garanti_miltal",
"rubrik": "Mileage documented",
"krav": "miltal",
"detaljVidBrist": "Warranty cases require a documented odometer reading."
},
{
"id": "garanti_historik",
"rubrik": "Service history checked",
"krav": "historik",
"detaljVidBrist": "Warranty cases require a checked service history."
},
{
"id": "garanti_claim",
"rubrik": "Claim or warranty number recorded",
"krav": "claim",
"detaljVidBrist": "State the claim or warranty number (read from the work order)."
}
],
"Goodwill": [
{ "id": "goodwill_miltal", "rubrik": "Miltal dokumenterat", "krav": "miltal", "detaljVidBrist": "Goodwillärenden kräver dokumenterad mätarställning." },
{ "id": "goodwill_historik", "rubrik": "Servicehistorik kontrollerad", "krav": "historik", "detaljVidBrist": "Goodwillärenden kräver kontrollerad servicehistorik." }
{
"id": "goodwill_miltal",
"rubrik": "Mileage documented",
"krav": "miltal",
"detaljVidBrist": "Goodwill cases require a documented odometer reading."
},
{
"id": "goodwill_historik",
"rubrik": "Service history checked",
"krav": "historik",
"detaljVidBrist": "Goodwill cases require a checked service history."
}
],
"Försäkring": [
{ "id": "forsakring_skadenummer", "rubrik": "Skadenummer registrerat", "krav": "skadenummer", "detaljVidBrist": "Försäkringsärenden kräver skadenummer (läses ur arbetsordern)." },
{ "id": "forsakring_bildbevis", "rubrik": "Bildbevis finns", "krav": "foto", "detaljVidBrist": "Försäkringsärenden kräver bilddokumentation." }
"Insurance": [
{
"id": "forsakring_skadenummer",
"rubrik": "Claim number recorded",
"krav": "skadenummer",
"detaljVidBrist": "Insurance cases require a claim number (read from the work order)."
},
{
"id": "forsakring_bildbevis",
"rubrik": "Photographic evidence present",
"krav": "foto",
"detaljVidBrist": "Insurance cases require photographic documentation."
}
],
"Reklamation": [
{ "id": "reklamation_historik", "rubrik": "Historik och tidigare försök kontrollerade", "krav": "historik", "detaljVidBrist": "Reklamationer kräver kontrollerad historik (tidigare reparationer/försök)." }
"Complaint": [
{
"id": "reklamation_historik",
"rubrik": "History and previous attempts checked",
"krav": "historik",
"detaljVidBrist": "Complaint cases require a checked history (previous repairs and attempts)."
}
],
"Begagnatgaranti": [
{ "id": "begagnat_miltal", "rubrik": "Miltal dokumenterat", "krav": "miltal", "detaljVidBrist": "Begagnatgaranti kräver dokumenterad mätarställning." }
"Used-vehicle warranty": [
{
"id": "begagnat_miltal",
"rubrik": "Mileage documented",
"krav": "miltal",
"detaljVidBrist": "Used-vehicle warranty cases require a documented odometer reading."
}
]
},
"undantagsorsaker": [
"Komponenten är oåtkomlig",
"Fordonet kan inte lyftas säkert",
"Kunden avböjde demontering",
"Dålig sikt/åtkomst",
"Utrustning saknas"
"The component is inaccessible",
"The vehicle cannot be lifted safely",
"The customer declined disassembly",
"Poor visibility or access",
"Equipment not available"
],
"orsakskategorier": [
"Normalt slitage",
"Ålder",
"Körsträcka",
"Materialutmattning",
"Tillverkningsfel",
"Bristande underhåll",
"Felaktig tidigare reparation",
"Yttre påverkan",
"Korrosion",
"Överhettning",
"Förorening",
"Felaktig användning",
"Modifiering",
"Olycka eller skada",
"Okänd orsak"
"Normal wear",
"Age",
"Distance travelled",
"Material fatigue",
"Manufacturing defect",
"Inadequate maintenance",
"Faulty previous repair",
"External influence",
"Corrosion",
"Overheating",
"Contamination",
"Improper use",
"Modification",
"Accident or damage",
"Unknown cause"
],
"underlagskallor": [
"Foto",
"Video",
"Mätresultat",
"Diagnosutläsning",
"Tidigare historik",
"Servicehistorik",
"Teknisk dokumentation",
"Direkt observation"
"Measurement result",
"Diagnostic readout",
"Previous history",
"Service history",
"Technical documentation",
"Direct observation"
]
}
@@ -215,7 +215,7 @@ kontroll "statistiken är organisationsknuten data" "$(echo "$STAT" | falt '.ors
# 10b. ECM Knowledge Library: regelpaketet serveras till inloggade klienter
REGLER=$(curl -s "$BAS/api/ecm/regler" -H "Authorization: Bearer $TOKEN_J")
kontroll "regelpaketet serveras" "$(echo "$REGLER" | falt .version)" "2.0"
kontroll "regelpaketet innehåller garantiregler" "$(echo "$REGLER" | falt '.arendetypRegler.Garanti.length')" "3"
kontroll "regelpaketet innehåller garantiregler" "$(echo "$REGLER" | falt '.arendetypRegler.Warranty.length')" "3"
# 10c. Publikt kundgodkännande — den enda skrivande publika vägen
# Ärendets ursprungliga delningskod saknar registrerad nivå och får