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 (
+
+ {IKONTECKEN[ikon]}
+
+ );
+}
+
+// ---- 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 (
+
+
+ );
+}
+
+// ---- 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 (
+
+
+ {f.bokstav}
+
+
+
{f.namn}
+
+ {f.syfte}
+
+
+ );
+ })}
+
+ );
+}
+
+// ---- 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 (
+
+ );
+}
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),
+ );
+}