diff --git a/felsokning/app/src/alva/komponenter.tsx b/felsokning/app/src/alva/komponenter.tsx new file mode 100644 index 0000000..968b270 --- /dev/null +++ b/felsokning/app/src/alva/komponenter.tsx @@ -0,0 +1,326 @@ +// ALVA:s komponentbibliotek. +// +// Ett bibliotek, två användningar: den publika webbplatsen och det +// inloggade gränssnittet. Besökaren ska aldrig märka att den lämnat +// marknadswebben när den loggar in, eftersom den inte har gjort det — +// det är samma system, samma typografi, samma rutnät, samma ord. +// +// Regler som gäller varje komponent här: +// +// Rutnät 8 px, undantagslöst. Ett rutnät med undantag är inget. +// Färg Nästan monokromt. ALVA Blue bara för aktivt steg, +// verifierad status och markerad komponent — aldrig som +// dekoration. +// Rörelse Ingen. En animation som inte bär information är brus i +// ett utrymme där teknikern redan har för mycket brus. +// Ikoner ✓ ○ □ → och inget annat. Inga emojier, inga +// illustrationer. +// Rubriker VERSALER. Sektionsrubriker är etiketter, inte meningar. + +import type { ReactNode } from "react"; +import { type Fas, FASER, fasDefinition } from "./system"; +import { STATUS, type Status } from "./sprak"; + +// ---- Färg ------------------------------------------------------------- + +export const FARG = { + graphite: "#1B1E22", + steel: "#4D5662", + lightSteel: "#D7DCE2", + background: "#F6F7F8", + blue: "#005CA9", + white: "#FFFFFF", +} as const; + +// ---- Typografi -------------------------------------------------------- + +/** + * Sektionsetikett. Versaler, spärrad, liten. Detta är hur ett tekniskt + * dokument märker upp ett fält — inte hur en rubrik i en artikel ser ut. + */ +export function Etikett({ children, ton = "steel" }: { children: ReactNode; ton?: "steel" | "blue" | "graphite" }) { + const färg = ton === "blue" ? FARG.blue : ton === "graphite" ? FARG.graphite : FARG.steel; + return ( +
+ {children} +
+ ); +} + +export function Rubrik({ children, niva = 2 }: { children: ReactNode; niva?: 1 | 2 | 3 }) { + const storlek = niva === 1 ? "text-[32px] leading-[40px]" : niva === 2 ? "text-[22px] leading-[32px]" : "text-[16px] leading-[24px]"; + const Tagg = (niva === 1 ? "h1" : niva === 2 ? "h2" : "h3") as "h1"; + return ( + + {children} + + ); +} + +// ---- Ikoner ----------------------------------------------------------- +// +// Fyra tecken. En status som inte går att uttrycka med dem är en status +// som inte är genomtänkt. + +export type Ikon = "klar" | "pagaende" | "vantar" | "nasta"; + +const IKONTECKEN: Record = { + klar: "✓", + pagaende: "○", + vantar: "□", + nasta: "→", +}; + +export function Symbol({ ikon, ton }: { ikon: Ikon; ton?: string }) { + return ( + + ); +} + +// ---- Status ----------------------------------------------------------- + +const STATUSFARG: Record = { + pending: FARG.steel, + in_progress: FARG.blue, + complete: FARG.graphite, + passed: FARG.blue, + failed: FARG.graphite, + blocked: FARG.graphite, + incomplete: FARG.steel, + not_applicable: FARG.steel, +}; + +/** + * Status anges alltid med ord, aldrig enbart med färg. En tekniker med + * nedsatt färgseende ska kunna skilja PASSED från FAILED — och den som + * läser en utskrift i svartvitt likaså. + */ +export function Statusmärke({ status }: { status: Status }) { + const ram = status === "failed" || status === "blocked"; + return ( + + {STATUS[status]} + + ); +} + +// ---- Datablock -------------------------------------------------------- + +/** + * Ett fält med etikett och värde. Grunden i varje ALVA-skärm: systemet + * redovisar tillstånd i namngivna fält i stället för i löpande text. + */ +export function Falt({ etikett, children }: { etikett: string; children: ReactNode }) { + return ( +
+ {etikett} +
+ {children} +
+
+ ); +} + +export function Block({ children, rubrik, beteckning }: { children: ReactNode; rubrik?: string; beteckning?: string }) { + return ( +
+ {(rubrik || beteckning) && ( +
+ {rubrik && {rubrik}} + {beteckning && {beteckning}} +
+ )} +
{children}
+
+ ); +} + +// ---- Fasindikator ----------------------------------------------------- + +/** + * ALVA-modellen som ett tillstånd, inte som en illustration. Den visar + * var i metoden arbetet befinner sig — vilket är den enda frågan en + * tekniker som återupptar ett ärende faktiskt har. + */ +export function Fasrad({ aktiv, klara = [] }: { aktiv: Fas; klara?: Fas[] }) { + return ( +
    + {FASER.map((f) => { + const ärAktiv = f.id === aktiv; + const ärKlar = klara.includes(f.id); + return ( +
  1. +
    + {f.bokstav} + +
    +
    {f.namn}
    +
    + {f.syfte} +
    +
  2. + ); + })} +
+ ); +} + +// ---- Skärmstruktur ---------------------------------------------------- + +/** + * Varje ALVA-skärm har samma struktur, och det är avsiktligt monotont: + * + * SYSTEM → CURRENT STEP → OBJECTIVE → REQUIRED INPUT → RESULT → NEXT ACTION + * + * En tekniker som lärt sig läsa en skärm har lärt sig läsa alla. Det är + * samma princip som gör att en instrumentpanel i ett flygplan går att + * flytta mellan flygplanstyper. + */ +export interface Procedursteg { + fas: Fas; + steg: string; + syfte: string; + underlag?: string; + forvantat?: string; + resultat?: ReactNode; + status: Status; + nasta?: string; +} + +export function Procedurvy({ + beteckning: bet, + steg, + klaraFaser = [], + children, +}: { + beteckning: string; + steg: Procedursteg; + klaraFaser?: Fas[]; + children?: ReactNode; +}) { + const fas = fasDefinition(steg.fas); + return ( +
+
+
+ System + {bet} +
+ +
+ +
+ + + + {fas.namn} — {fas.syfte} + + {steg.steg} + {steg.syfte} + {steg.underlag && {steg.underlag}} + {steg.forvantat && {steg.forvantat}} + + + + {steg.resultat && {steg.resultat}} + {steg.nasta && ( + + + + {steg.nasta} + + + )} + + + {children} +
+
+ ); +} + +// ---- Interaktion ------------------------------------------------------ + +export function Knapp({ + children, + onClick, + variant = "primar", + disabled, + type = "button", +}: { + children: ReactNode; + onClick?: () => void; + variant?: "primar" | "sekundar"; + disabled?: boolean; + type?: "button" | "submit"; +}) { + const primär = variant === "primar"; + return ( + + ); +} + +export function Tabell({ kolumner, rader }: { kolumner: string[]; rader: ReactNode[][] }) { + return ( + + + + {kolumner.map((k) => ( + + ))} + + + + {rader.map((rad, i) => ( + + {rad.map((cell, j) => ( + + ))} + + ))} + +
+ {k} +
+ {cell} +
+ ); +} diff --git a/felsokning/app/src/alva/sprak.ts b/felsokning/app/src/alva/sprak.ts new file mode 100644 index 0000000..d7ca3f6 --- /dev/null +++ b/felsokning/app/src/alva/sprak.ts @@ -0,0 +1,109 @@ +// ALVA:s statusspråk. +// +// Tonen är varken mänsklig, trevlig eller otrevlig. Den är professionell, +// och det betyder något specifikt: systemet rapporterar tillstånd, det +// tilltalar inte användaren. +// +// Inte "Nu ska vi felsöka bilen." utan "Diagnostic procedure initiated." +// Inte "Testet lyckades." utan "Verification passed." +// Inte "Det verkar som..." utan "Evidence indicates..." +// Inte "Jag tror..." utan "Confidence level: 92%" +// +// Det sista paret är det viktigaste. "Jag tror" är en person som gissar. +// "Confidence level" är ett mätvärde som går att ifrågasätta, jämföra och +// dokumentera. Skillnaden är hela produktens existensberättigande. +// +// Katalogen är uttömmande med avsikt. Ett statusmeddelande som skrivs +// på plats i en komponent blir förr eller senare "Bra jobbat!" — därför +// finns bara det som står här, och ett test låser listan. + +export type Status = + | "pending" + | "in_progress" + | "complete" + | "passed" + | "failed" + | "blocked" + | "incomplete" + | "not_applicable"; + +/** + * Statusord. Versaler i gränssnittet; formen här är kanonisk och + * översätts inte — se språkgränsen i system.ts. + */ +export const STATUS: Record = { + pending: "PENDING", + in_progress: "IN PROGRESS", + complete: "COMPLETE", + passed: "PASSED", + failed: "FAILED", + blocked: "BLOCKED", + incomplete: "INCOMPLETE", + not_applicable: "NOT APPLICABLE", +}; + +/** + * Systemmeddelanden. Varje rad är ett konstaterande i presens eller + * perfekt — aldrig en uppmaning med utropstecken, aldrig ett omdöme om + * användarens arbete. + */ +export const MEDDELANDE = { + procedur_startad: "Diagnostic procedure initiated.", + procedur_avslutad: "Diagnostic procedure closed.", + analys_klar: "Analysis complete.", + lokalisering_klar: "Localization complete.", + orsak_faststalld: "Root cause identified.", + verifiering_godkand: "Verification passed.", + verifiering_underkand: "Verification failed.", + atgard_registrerad: "Corrective action recorded.", + + fortsatt_verifiering: "Proceed to verification.", + fortsatt_atgard: "Proceed to corrective action.", + + underlag_saknas: "Inspection incomplete. Additional evidence required.", + evidens_saknas: "Evidence not available.", + matdon_ej_sparbart: "Measurement not traceable. Instrument identity required.", + sparr_aktiv: "Procedure halted. Safety precondition not met.", + grind_ej_passerad: "Closure blocked. Mandatory records incomplete.", + + kalla_ansluten: "Knowledge source connected.", + kalla_ej_ansluten: "Knowledge source unavailable.", + ingen_kalla: "No authorized knowledge source configured.", +} as const; + +/** + * Bedömning uttrycks som ett tal, aldrig som en åsikt. + * `Evidence indicates ...` följt av `Confidence level: 92%`. + */ +export const bedomning = (påstående: string, konfidens: number): string => + `Evidence indicates ${påstående}. Confidence level: ${Math.round(konfidens * 100)}%.`; + +/** Ord som aldrig får förekomma i produktens egen text. */ +export const FORBJUDNA_ORD = [ + "amazing", + "smart", + "magic", + "magisk", + "revolutionary", + "revolutionerande", + "fantastisk", + "grymt", + "välkommen", + "welcome", + "bra jobbat", + "well done", + "oops", + "hoppsan", +]; + +/** Ord som beskriver produkten. Om ett annat behövs saknas något. */ +export const GODKANDA_ORD = [ + "verified", + "repeatable", + "traceable", + "documented", + "validated", + "standardized", + "operational", + "engineering", +]; diff --git a/felsokning/app/src/alva/system.ts b/felsokning/app/src/alva/system.ts new file mode 100644 index 0000000..2b3f76d --- /dev/null +++ b/felsokning/app/src/alva/system.ts @@ -0,0 +1,170 @@ +// ALVA — Analysis · Localization · Verification · Action +// +// ALVA är inte en assistent. ALVA är en metod. +// +// Skillnaden är inte kosmetisk och den är kodad här. Man säger inte +// "fråga ALVA" utan "följ ALVA", på samma sätt som man säger ISO 9001 +// eller Six Sigma. Systemet hjälper inte till — det tillämpar en +// definierad metod för att stegvis eliminera osäkerhet. +// +// ---- Språkgränsen ----------------------------------------------------- +// +// En avgränsning som styr hela produkten och som är värd att slå fast: +// +// ALVA:s STRUKTUR är engelsk och oföränderlig. Fasnamn, statusord, +// dokumenttyper och identifierare skrivs likadant i varje land och +// varje översättning — precis som DIN 2014 heter DIN 2014 på svenska, +// och ett ISO-krav inte översätts till "internationell standard 9001". +// +// ALVA:s INNEHÅLL är på arbetsspråket. Frågan som ställs till +// teknikern, kontrollpunktens text och teknikerns egna anteckningar är +// svenska i en svensk verkstad och tyska i en tysk. +// +// Alternativet — att översätta VERIFICATION till "Verifiering" i den +// svenska versionen — hade gjort ALVA till ett ord för samma sak i varje +// land i stället för samma sak i varje land. Ramen ska vara invariant. +// Det är hela poängen med en standard. + +export const ALVA = { + namn: "ALVA", + utlast: "Analysis · Localization · Verification · Action", + position: "Guided Diagnostic Platform", + /** Ingen slogan. Detta är den enda meningen som beskriver produkten. */ + definition: + "Standardized diagnostic procedures for repeatable and verifiable troubleshooting.", +} as const; + +// ---- Metoden ---------------------------------------------------------- + +export type Fas = "analysis" | "localization" | "verification" | "action"; + +export interface FasDefinition { + id: Fas; + bokstav: "A" | "L" | "V" | "A"; + namn: string; + /** Vad fasen gör. En mening, imperativ, ingen utsmyckning. */ + syfte: string; + /** Vad fasen uttryckligen INTE gör — det som skiljer den från nästa. */ + avgransning: string; +} + +export const FASER: readonly FasDefinition[] = [ + { + id: "analysis", + bokstav: "A", + namn: "Analysis", + syfte: "Collect evidence.", + avgransning: "Facts only. Hypotheses are not collected in this phase.", + }, + { + id: "localization", + bokstav: "L", + namn: "Localization", + syfte: "Isolate the fault.", + avgransning: "Narrow the area. The cause is not yet established.", + }, + { + id: "verification", + bokstav: "V", + namn: "Verification", + syfte: "Confirm root cause.", + avgransning: "The cause is verified — not the symptom.", + }, + { + id: "action", + bokstav: "A", + namn: "Action", + syfte: "Execute corrective action.", + avgransning: "Result verified and documented. Otherwise the phase is incomplete.", + }, +] as const; + +export const fasDefinition = (fas: Fas): FasDefinition => + FASER.find((f) => f.id === fas) ?? FASER[0]; + +// ---- Nomenklatur ------------------------------------------------------ +// +// Allt har ett ID. Det är inte administration för sin egen skull: ett +// system där varje ärende, regel, procedur och dokument går att hänvisa +// till i klartext är ett system man kan tala om i telefon, skriva i ett +// protokoll och slå upp två år senare. + +export type Objektklass = + | "CASE" // ett diagnostiskt ärende + | "PROC" // en procedur (metodik) + | "RULE" // en regel i regelmotorn + | "DOC" // ett dokument i kunskapslagret + | "COMP" // en komponent + | "VAL" // en valideringskörning + | "ORG" // en organisation + | "USER" // en användare + | "SRC" // en kunskapskälla + | "REP"; // en rapport + +const SIFFROR: Record = { + CASE: 5, + PROC: 4, + RULE: 3, + DOC: 3, + COMP: 3, + VAL: 2, + ORG: 4, + USER: 4, + SRC: 3, + REP: 4, +}; + +/** ALVA-CASE-91821 · ALVA-PROC-0042 · ALVA-RULE-120 */ +export function beteckning(klass: Objektklass, nummer: number): string { + return `ALVA-${klass}-${String(nummer).padStart(SIFFROR[klass], "0")}`; +} + +const BETECKNING = /^ALVA-([A-Z]{3,4})-(\d+)$/; + +export function tolkaBeteckning(text: string): { klass: Objektklass; nummer: number } | null { + const träff = text.trim().toUpperCase().match(BETECKNING); + if (!träff || !(träff[1] in SIFFROR)) return null; + return { klass: träff[1] as Objektklass, nummer: Number(träff[2]) }; +} + +/** + * Ett ärende-id ur loggen är en ogenomskinlig sträng. Beteckningen är det + * som skrivs på papper och sägs högt, så den härleds deterministiskt ur + * ärendenumret — samma ärende får alltid samma beteckning. + */ +export const arendebeteckning = (nummer: number) => beteckning("CASE", nummer); + +// ---- Dokumentklasser -------------------------------------------------- +// +// Namnen är inte påhittade för att låta industriella. De motsvarar +// dokumenttyper som faktiskt har olika funktion och olika livslängd, och +// den som arbetat i en fordonsorganisation känner igen dem. + +export const DOKUMENTKLASSER = [ + { id: "PROC", namn: "ALVA Procedure", syfte: "Executable diagnostic procedure." }, + { id: "SPEC", namn: "ALVA Specification", syfte: "Normative requirement on the platform." }, + { id: "SI", namn: "ALVA Service Instruction", syfte: "Instruction for a defined operation." }, + { id: "DR", namn: "ALVA Diagnostic Report", syfte: "Outcome of one diagnostic case." }, + { id: "VR", namn: "ALVA Validation Report", syfte: "Evidence that a procedure performs as specified." }, + { id: "IP", namn: "ALVA Inspection Protocol", syfte: "Recorded inspection against a checklist." }, + { id: "TB", namn: "ALVA Technical Bulletin", syfte: "Notice of a known condition and its handling." }, + { id: "RN", namn: "ALVA Release Notes", syfte: "Changes in a platform version." }, + { id: "ES", namn: "ALVA Engineering Standard", syfte: "Binding internal engineering rule." }, + { id: "RH", namn: "ALVA Revision History", syfte: "Change record for a controlled document." }, +] as const; + +// ---- Versionshantering ------------------------------------------------ +// +// ALVA 1.0, ALVA 1.1, ALVA 2.0. Inte Summer Edition, inte AI Edition, +// inte Pro Max. En version är ett tillstånd av systemet, inte en +// marknadsföringshändelse. + +export const PLATTFORMSVERSION = "ALVA 1.0"; + +// ---- Rutnät ----------------------------------------------------------- +// +// 8 px. Varje mått i gränssnittet är en multipel. Undantagslöst — ett +// rutnät med undantag är inget rutnät. + +export const RUTNÄT = 8; +export const steg = (antal: number) => `${antal * RUTNÄT}px`; diff --git a/felsokning/app/src/felsokning/__tests__/alva.test.ts b/felsokning/app/src/felsokning/__tests__/alva.test.ts new file mode 100644 index 0000000..09aa6d8 --- /dev/null +++ b/felsokning/app/src/felsokning/__tests__/alva.test.ts @@ -0,0 +1,135 @@ +// @vitest-environment node +// ALVA som metod, inte som etikett. +// +// Testerna låser tre saker: att varje procedursteg hör till exakt en fas, +// att statusspråket inte glider tillbaka mot vänlighet, och att +// beteckningarna är stabila. Går något av dem sönder har ALVA blivit ett +// namn på en app igen. +import { describe, expect, it } from "vitest"; +import { FASORDNING, STEGFAS, fasFor, faserI, klaraFaser } from "../../../../services/gemensam/faser.mjs"; +import { ALLA_METODIKER } from "../../../../services/gemensam/metodiker.mjs"; +import { FASER, arendebeteckning, beteckning, tolkaBeteckning } from "@/alva/system"; +import { FORBJUDNA_ORD, MEDDELANDE, STATUS, bedomning } from "@/alva/sprak"; + +describe("ALVA-modellen är tillämpad på varje procedur", () => { + it("varje steg i varje metodik är klassificerat — ingen tyst fallback", () => { + const oklassade: string[] = []; + for (const m of ALLA_METODIKER) { + for (const steg of m.steg) { + if (!(steg.id in STEGFAS)) oklassade.push(`${m.id}/${steg.id}`); + } + } + expect(oklassade).toEqual([]); + }); + + it("faserna kommer i ALVA-ordning inom varje metodik", () => { + for (const m of ALLA_METODIKER) { + const ordning = m.steg.map((s: { id: string }) => FASORDNING.indexOf(fasFor(s.id))); + // En procedur får hoppa över en fas, men aldrig gå bakåt: att + // verifiera före lokalisering är att gissa. + for (let i = 1; i < ordning.length; i += 1) { + expect(ordning[i], `${m.id} steg ${i}`).toBeGreaterThanOrEqual(ordning[i - 1]); + } + } + }); + + it("varje metodik börjar i Analysis", () => { + for (const m of ALLA_METODIKER) { + expect(fasFor(m.steg[0].id), m.id).toBe("analysis"); + } + }); + + it("säkerhetssteget hör till Analysis — det är förutsättningen, inte en åtgärd", () => { + expect(fasFor("sakerhet")).toBe("analysis"); + const hogvolt = ALLA_METODIKER.find((m: { id: string }) => m.id === "hogvolt")!; + expect(faserI(hogvolt)[0]).toBe("analysis"); + }); + + it("en metodik behöver inte innehålla alla fyra faser", () => { + // Läckagemetodiken slutar med lokalisering. Läckan är hittad; + // åtgärden är ett annat arbete. Att fylla ut modellen med ett + // konstruerat Action-steg hade varit att tillämpa den slarvigt. + const lackage = ALLA_METODIKER.find((m: { id: string }) => m.id === "lackage")!; + expect(faserI(lackage)).not.toContain("action"); + expect(faserI(lackage).length).toBeGreaterThan(1); + }); + + it("härleder klara faser ur loggen", () => { + const m = ALLA_METODIKER.find((x: { id: string }) => x.id === "generisk")!; + const symptom = m.steg.find((s: { id: string }) => s.id === "symptom")!; + const logg = (symptom.fragor ?? []).map((f: { id: string }) => ({ + typ: "fraga_besvarad", + stegId: "symptom", + frageId: f.id, + svar: "x", + })); + expect(klaraFaser(m, logg)).toEqual(["analysis"]); + expect(klaraFaser(m, [])).toEqual([]); + }); +}); + +describe("statusspråket", () => { + it("meddelanden är konstateranden, inte tilltal", () => { + for (const [nyckel, text] of Object.entries(MEDDELANDE)) { + expect(text, nyckel).not.toMatch(/!/); + expect(text, nyckel).not.toMatch(/\b(du|din|ditt|dina|vi|vår|our|your)\b/i); + // Varje rad slutar med punkt: det är ett protokoll, inte en etikett. + expect(text, nyckel).toMatch(/\.$/); + } + }); + + it("inget förbjudet ord finns i katalogen", () => { + const allt = [...Object.values(MEDDELANDE), ...Object.values(STATUS)].join(" ").toLowerCase(); + for (const ord of FORBJUDNA_ORD) expect(allt, ord).not.toContain(ord); + }); + + it("bedömning uttrycks som ett tal, aldrig som en åsikt", () => { + const rad = bedomning("continuity is interrupted between pin 14 and ground", 0.92); + expect(rad).toBe( + "Evidence indicates continuity is interrupted between pin 14 and ground. Confidence level: 92%.", + ); + expect(rad).not.toMatch(/\b(I think|jag tror|verkar som)\b/i); + }); + + it("statusord är versaler och oöversatta — ramen är invariant", () => { + for (const [nyckel, ord] of Object.entries(STATUS)) { + expect(ord, nyckel).toBe(ord.toUpperCase()); + expect(ord, nyckel).toMatch(/^[A-Z ]+$/); + } + }); +}); + +describe("nomenklaturen", () => { + it("bildar beteckningar med fast bredd per objektklass", () => { + expect(beteckning("CASE", 91821)).toBe("ALVA-CASE-91821"); + expect(beteckning("PROC", 42)).toBe("ALVA-PROC-0042"); + expect(beteckning("RULE", 120)).toBe("ALVA-RULE-120"); + expect(beteckning("VAL", 19)).toBe("ALVA-VAL-19"); + }); + + it("är återläsbar — det som skrivs på papper går att slå upp", () => { + expect(tolkaBeteckning("ALVA-CASE-91821")).toEqual({ klass: "CASE", nummer: 91821 }); + expect(tolkaBeteckning(" alva-proc-0042 ")).toEqual({ klass: "PROC", nummer: 42 }); + expect(tolkaBeteckning("ALVA-HITTEPA-1")).toBeNull(); + expect(tolkaBeteckning("CASE-1")).toBeNull(); + }); + + it("samma ärende ger alltid samma beteckning", () => { + expect(arendebeteckning(7)).toBe(arendebeteckning(7)); + expect(arendebeteckning(7)).toBe("ALVA-CASE-00007"); + }); +}); + +describe("metoden är dokumenterad, inte antydd", () => { + it("fyra faser med bokstäverna A L V A", () => { + expect(FASER.map((f) => f.bokstav).join("")).toBe("ALVA"); + expect(FASER.map((f) => f.id)).toEqual(FASORDNING); + }); + + it("varje fas säger både vad den gör och vad den inte gör", () => { + for (const f of FASER) { + expect(f.syfte, f.id).toMatch(/\.$/); + expect(f.avgransning.length, f.id).toBeGreaterThan(10); + } + }); +}); diff --git a/felsokning/services/gemensam/faser.mjs b/felsokning/services/gemensam/faser.mjs new file mode 100644 index 0000000..c64a3e3 --- /dev/null +++ b/felsokning/services/gemensam/faser.mjs @@ -0,0 +1,106 @@ +// ALVA-modellen tillämpad på metodikbiblioteket. +// +// ALVA är en metod, inte ett gränssnitt. Om metoden bara fanns i +// marknadsföringen vore den en etikett på en wizard. Här är den den +// struktur som varje procedur faktiskt följer: +// +// A Analysis Samla fakta. Inte hypoteser. +// L Localization Avgränsa. Identifiera exakt område. +// V Verification Verifiera orsaken. Inte symptomet. +// A Action Utför korrigering. Verifiera resultat. Dokumentera. +// +// Varje steg i varje metodik hör till exakt en fas. Klassificeringen är +// data och inte en gissning i vyn, av två skäl: den ska gå att granska, +// och en procedur vars faser inte går ihop ska falla i test i stället +// för att se rätt ut på skärmen. +// +// ---- Varför just den här indelningen --------------------------------- +// +// Gränsen mellan L och V är den enda som är svår, och den är också den +// som bär hela metoden. Att mäta spänning vid en komponent är +// lokalisering — den avgränsar var felet finns. Att mäta spänningsfall +// över samma komponent under last är verifiering — den fastställer +// varför. Samma instrument, samma komponent, olika fas. +// +// Regeln vi tillämpar: ett steg är Verification när det kan avfärda en +// kandidatorsak. Kan det bara flytta uppmärksamheten är det Localization. + +/** Steg-id → fas. Uttömmande; ett okänt steg-id faller i test. */ +export const STEGFAS = { + // ---- Analysis: vad hände, när, var. Inga slutsatser. ---------------- + sakerhet: "analysis", // Förutsättningen för att få samla fakta alls. + symptom: "analysis", + forutsattningar: "analysis", + grund: "analysis", + + // ---- Localization: avgränsa området --------------------------------- + visuell: "localization", + felkoder: "localization", + avlasning: "localization", + koder: "localization", + buss: "localization", + inspelning: "localization", + lokalisering: "localization", + batteri: "localization", + glapp: "localization", + metod: "localization", + + // ---- Verification: fastställ orsaken -------------------------------- + matningar: "verification", + kontroller: "verification", + grundkontroller: "verification", + rela: "verification", + start: "verification", + laddning: "verification", + krypstrom: "verification", + mekanik: "verification", + tandning_bransle: "verification", + packning: "verification", + system: "verification", + styrning: "verification", + orsak: "verification", + installning: "verification", + + // ---- Action: korrigera och verifiera resultatet --------------------- + provkorning: "action", + funktionstest: "action", + kalibrering: "action", +}; + +export const FASORDNING = ["analysis", "localization", "verification", "action"]; + +/** Fasen för ett steg. Okänt steg → analysis, och testet faller. */ +export const fasFor = (stegId) => STEGFAS[stegId] ?? "analysis"; + +/** + * Faserna en metodik faktiskt innehåller, i ALVA-ordning. + * + * Alla metodiker innehåller inte alla fyra. En läckagemetodik som slutar + * med lokalisering är inte ofullständig — läckan är hittad, åtgärden är + * ett annat arbete. Att tvinga in ett Action-steg där hade varit att + * fylla ut modellen i stället för att tillämpa den. + */ +export function faserI(metodik) { + const funna = new Set((metodik?.steg ?? []).map((s) => fasFor(s.id))); + return FASORDNING.filter((f) => funna.has(f)); +} + +/** + * Klara faser givet loggen: en fas är klar när metodikens samtliga steg i + * den fasen är dokumenterade. + */ +export function klaraFaser(metodik, handelser) { + const besvarade = new Set(); + const utforda = new Set(); + for (const h of handelser) { + if (h?.typ === "fraga_besvarad") besvarade.add(`${h.stegId}/${h.frageId}`); + if (h?.typ === "kontroll_utford") utforda.add(`${h.stegId}/${h.kontrollId}`); + } + const klar = (steg) => + (steg.fragor ?? []).every((f) => besvarade.has(`${steg.id}/${f.id}`)) && + (steg.kontroller ?? []).every((k) => utforda.has(`${steg.id}/${k.id}`)); + + return faserI(metodik).filter((fas) => + (metodik.steg ?? []).filter((s) => fasFor(s.id) === fas).every(klar), + ); +}