Files
alva/felsokning/services/gemensam/sammanfattning.mjs
T
Claude 3ec25dea9c ALVA: analysvy, sammanfattning och integrationsgränssnitt
---- Statistiken är ett produktbeslut ----------------------------------

Vilka siffror som visas avgör vad organisationen optimerar, så tre mått
är uteslutna med avsikt och låsta av test: ärenden per tekniker,
genomsnittlig ledtid och aktivitet. Alla tre belönar den som hoppar över
kontroller, och ett svårt fel SKA ta längre tid.

Medtagna, var och en handlingsbar:

  VERIFIERINGSGRAD   Andel avslut med fastställd orsak. Det enda mått
                     ett försäkringsbolag egentligen bryr sig om. Att
                     den inte är 100 % är friskt.
  REPRODUKTIONSGRAD  Låg siffra förutsäger återkommande fordon — man kan
                     inte åtgärda det man inte sett.
  OMARBETNING        Samma fordon tillbaka med samma orsakskategori inom
                     ett fönster. Det dyraste felet i en verkstad och det
                     enda ingen mäter, eftersom det kräver att två
                     ärenden kopplas ihop. Här är kopplingen gratis.
  UNDANTAGSFREKVENS  Vilka kontroller som hoppas över, per steg och fas.
                     Ett steg högt i listan är antingen felskrivet eller
                     kräver utrustning som saknas — bägge åtgärdbara.

Ett mått utan underlag visas som NOT APPLICABLE, aldrig som noll.
Skillnaden avgör om någon fattar beslut på en siffra som inte finns.

---- Sammanfattningen är härledd, inte genererad -----------------------

Frestelsen är att låta modellen skriva den. Det vore fel av tre skäl:
den blir en del av ett beslutsunderlag och måste därför gå att lita på
utan att granskas mot loggen varje gång; samma ärende måste ge samma
text om två år; och verkstadsgolvet har dålig täckning.

Den är alltså en projektion som briefen och rapporten. Den innehåller
inget som inte står i loggen och säger uttryckligen när något saknas i
stället för att utelämna det. Ett test kräver att den aldrig skriver
"felet konstaterat" när orsaken inte fastställts.

---- Integration utan påhittade endpoints ------------------------------

Jag känner inte Beonodes, ServiceCams eller CABAS faktiska gränssnitt.
En uppfunnen endpoint som ser färdig ut är sämre än en tom: den ser ut
att fungera tills någon försöker.

Därför ett profildrivet gränssnitt. En profil beskriver vad en KATEGORI
av system förväntar sig, och märks validated först efter att den körts
mot leverantören. Tills dess står den som draft, och det syns i
gränssnittet — en integrationslista där allt ser färdigt ut är den
snabbaste vägen till ett misslyckat införande, eftersom verkstaden
planerar efter den.

Kategorier: diagnosprotokoll, DMS, videooffert, fordonsdata,
skadekalkyl (CABAS är nordisk standard), garanti och försäkring.

Skadekalkyl går åt båda håll. Det ALVA tillför en kalkyl är inte fler
poster utan beviskedjan bakom dem: vad som kontrollerades, vad som
uteslöts och varför. Det är den enda del av en kalkyl som i dag inte går
att granska i efterhand.

Inkommande protokoll blir evidens, inte bilagor, med härkomsten bevarad
i varje post — ett värde som kommit utifrån får aldrig se ut som något
teknikern själv mätt. Saknas instrumentets identitet nedgraderas värdet
till E1 enligt samma regel som gäller manuella mätningar.

Utgående leveranser signeras med HMAC över tidsstämpel och kropp;
tidsstämpeln ligger inne i signaturen så en fångad leverans inte går att
spela upp i morgon. Verifieringsfunktionen exporteras så mottagaren kan
använda exakt samma kod — de flesta integrationsfel uppstår i glappet
mellan två implementationer av samma signatur.

Prenumerationer går genom samma SSRF-gräns som leverantörsuppslagen, och
leverans sker efter att loggen skrivits: en mottagare ska aldrig kunna
se en händelse som inte finns i loggen.

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

151 lines
6.0 KiB
JavaScript

// ALVA-PROC-0030 · Case summary.
//
// Några få meningar som ger vem som helst en bild av ärendet: kunden i
// telefon, försäkringshandläggaren som öppnar en akt, teknikern som tar
// över efter helgen.
//
// ---- Varför den är härledd och inte genererad -------------------------
//
// Frestelsen är att låta modellen skriva den. Det vore fel här, och
// skälet är inte försiktighet utan funktion:
//
// Den ska gå att lita på. En sammanfattning som en bedömare läser
// blir en del av beslutsunderlaget. Är den
// genererad måste den granskas mot loggen
// varje gång — och då är den inte längre en
// genväg.
//
// Den ska vara stabil. Samma ärende måste ge samma text i dag och
// om två år. En sammanfattning som ändrar sig
// mellan visningar är oanvändbar i en tvist.
//
// Den ska fungera offline. Verkstadsgolvet har dålig täckning.
//
// Sammanfattningen är därför en projektion, precis som briefen och
// rapporten. Den innehåller inget som inte står i loggen, och den säger
// uttryckligen när något saknas i stället för att utelämna det.
//
// Modellen har fortfarande en roll — men i andra änden: den kan föreslå
// en språkligt jämnare formulering som teknikern GODKÄNNER, och då blir
// godkännandet en händelse i loggen. Det som visas här är alltid det
// härledda.
const handelserI = (arende) => (arende.handelser ?? []).map((p) => p.handelse ?? p);
const av = (handelser, typ) => handelser.filter((h) => h?.typ === typ);
const sista = (handelser, typ) => av(handelser, typ).at(-1);
const REPRODUKTION = {
ja: "Symptomet reproducerades.",
delvis: "Symptomet kunde delvis reproduceras.",
nej: "Symptomet kunde inte reproduceras under de förhållanden som rådde vid undersökningen.",
};
const KVALITET = {
symptomet_borta: "Vid kvalitetskontrollen var symptomet borta.",
kvarstar: "Vid kvalitetskontrollen kvarstod symptomet.",
delvis: "Vid kvalitetskontrollen kvarstod symptomet delvis.",
ej_verifierbar: "Kvalitetskontrollen kunde inte verifiera resultatet.",
};
/** Klipper en mening vid en rimlig gräns utan att kapa mitt i ett ord. */
function korta(text, max = 220) {
const = String(text ?? "").trim().replace(/\s+/g, " ");
if (.length <= max) return ;
const brytpunkt = .lastIndexOf(" ", max);
return `${.slice(0, brytpunkt > max * 0.6 ? brytpunkt : max)}`;
}
/**
* Bygger sammanfattningen.
*
* Ordningen är den en läsare faktiskt behöver: vad gällde det, vad sade
* kunden, kunde vi se det, vad blev orsaken, vad gjorde vi, höll det,
* vad är kvar. Varje mening utelämnas när underlaget saknas — utom de
* som handlar om att något saknas, för de är själva poängen.
*
* @returns { meningar[], text, fullstandig, saknas[] }
*/
export function sammanfatta(arende) {
const h = handelserI(arende);
const meningar = [];
const saknas = [];
// 1 · Objektet.
const objekt = sista(h, "objekt_identifierat")?.objekt;
if (objekt) {
const delar = [objekt.beskrivning, objekt.identifierare].filter(Boolean).join(", ");
meningar.push(`Ärendet gäller ${delar || "ett identifierat objekt"}.`);
} else {
saknas.push("objektidentifiering");
meningar.push("Objektet är inte identifierat i loggen.");
}
// 2 · Kundens ord, ordagrant men kortat.
const fel = sista(h, "felbeskrivning")?.text;
if (fel) meningar.push(`Kunden beskrev: ”${korta(fel, 160)}`);
else saknas.push("felbeskrivning");
// 3 · Kunde vi se det? Detta är den mening en bedömare läser först.
const repro = sista(h, "reproducering");
if (repro) meningar.push(REPRODUKTION[repro.status] ?? "Reproduktionen är dokumenterad.");
else saknas.push("symptomverifiering");
// 4 · Orsaken, med teknikerns eget varför när det finns.
const orsak = sista(h, "felorsak");
const slutsats = sista(h, "slutsats");
if (slutsats && slutsats.orsakFastställd === false) {
meningar.push(`Orsaken kunde inte fastställas: ${korta(slutsats.motivering, 200)}`);
} else if (orsak) {
const kategori = (orsak.orsaker ?? []).join(", ");
meningar.push(`Konstaterad avvikelse: ${korta(orsak.avvikelse, 180)}${kategori ? ` Bedömd orsak: ${kategori}.` : ""}`);
if (slutsats?.motivering) meningar.push(`Motivering: ${korta(slutsats.motivering, 220)}`);
} else {
saknas.push("felorsaksanalys");
}
// 5 · Vad som gjordes.
const atgard = sista(h, "atgard_utford");
if (atgard) {
meningar.push(
atgard.utford
? `Utförd åtgärd: ${korta(atgard.beskrivning, 160)}`
: `Ingen åtgärd utfördes: ${korta(atgard.motivering || atgard.beskrivning, 160)}`,
);
} else saknas.push("åtgärd");
// 6 · Höll det?
const kvalitet = sista(h, "kvalitetskontroll");
if (kvalitet) meningar.push(KVALITET[kvalitet.resultat] ?? "Kvalitetskontrollen är dokumenterad.");
// 7 · Vad som är kvar. Utelämnas aldrig när den finns — det är den
// enda meningen som talar om vad läsaren själv behöver göra.
if (slutsats?.kvarstaende) meningar.push(`Kvarstående: ${korta(slutsats.kvarstaende, 160)}`);
return {
version: "ALVA-PROC-0030",
meningar,
text: meningar.join(" "),
/** Sant när inget obligatoriskt underlag saknas. */
fullstandig: saknas.length === 0,
saknas,
};
}
/**
* Kortformen: en enda mening för en lista eller ett API-svar där
* utrymmet är en rad.
*/
export function enrading(arende) {
const h = handelserI(arende);
const objekt = sista(h, "objekt_identifierat")?.objekt;
const slutsats = sista(h, "slutsats");
const orsak = sista(h, "felorsak");
const avslutat = h.some((x) => x?.typ === "arende_avslutat");
const vad = objekt?.identifierare ?? "Oidentifierat objekt";
if (!avslutat) return `${vad} — pågående.`;
if (slutsats?.orsakFastställd === false) return `${vad} — avslutat utan fastställd orsak.`;
if (orsak) return `${vad}${korta(orsak.avvikelse, 90)}`;
return `${vad} — avslutat.`;
}