ALVA: metoden i kod — faser, nomenklatur, statusspråk, komponenter

ALVA är inte en omdöpning utan en annan produktklass. Skillnaden mellan
en assistent och en metod är att metoden går att granska, och därför
ligger den i kod och i test, inte i marknadsföringen.

Språkgränsen är den bärande avgränsningen: ALVA:s STRUKTUR är engelsk
och oföränderlig — fasnamn, statusord, dokumenttyper, identifierare
skrivs likadant i varje land, precis som DIN 2014 heter DIN 2014 på
svenska. ALVA:s INNEHÅLL är på arbetsspråket: frågan till teknikern och
kontrollpunktens text är svenska i en svensk verkstad. Att översätta
VERIFICATION till Verifiering hade gjort ALVA till ett ord för samma sak
i varje land i stället för samma sak i varje land.

Faserna är tillämpade på metodikbiblioteket, inte påklistrade: alla 31
steg-id i de 16 metodikerna är klassificerade i data. Gränsen mellan L
och V är den enda svåra och bär hela metoden — att mäta spänning vid en
komponent avgränsar var felet finns, att mäta spänningsfall under last
fastställer varför. Regeln: ett steg är Verification när det kan avfärda
en kandidatorsak.

Ett test kräver att faserna kommer i ALVA-ordning inom varje metodik. Ett
annat slår fast att en metodik inte behöver innehålla alla fyra —
läckagemetodiken slutar med lokalisering, och att fylla ut modellen med
ett konstruerat Action-steg hade varit att tillämpa den slarvigt.

Statusspråket är en uttömmande katalog, låst av test: inga utropstecken,
inget tilltal, varje rad slutar med punkt. Bedömning uttrycks som ett tal
— Confidence level: 92% — aldrig som Jag tror. Skillnaden är produktens
existensberättigande: det ena är en person som gissar, det andra ett
mätvärde som går att ifrågasätta.

Komponentbiblioteket är ett, för både webbplats och portal. 8 px-rutnät
utan undantag, nästan monokromt, ALVA Blue bara för aktivt steg och
verifierad status. Fyra ikoner: ✓ ○ □ →. Ingen animation — rörelse som
inte bär information är brus i ett utrymme där teknikern redan har för
mycket.

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-05 13:55:15 +00:00
parent 03fbea8fac
commit 610bb9eca6
5 changed files with 846 additions and 0 deletions
+326
View File
@@ -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 (
<div className="text-[11px] font-semibold uppercase tracking-[0.12em]" style={{ color: färg }}>
{children}
</div>
);
}
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 (
<Tagg className={`${storlek} font-semibold uppercase tracking-[0.06em]`} style={{ color: FARG.graphite }}>
{children}
</Tagg>
);
}
// ---- 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<Ikon, string> = {
klar: "✓",
pagaende: "○",
vantar: "□",
nasta: "→",
};
export function Symbol({ ikon, ton }: { ikon: Ikon; ton?: string }) {
return (
<span aria-hidden="true" className="inline-block w-4 text-center font-normal" style={{ color: ton ?? FARG.steel }}>
{IKONTECKEN[ikon]}
</span>
);
}
// ---- Status -----------------------------------------------------------
const STATUSFARG: Record<Status, string> = {
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 (
<span
className="inline-block border px-2 py-[2px] text-[11px] font-semibold uppercase tracking-[0.1em]"
style={{
color: STATUSFARG[status],
borderColor: ram ? FARG.graphite : FARG.lightSteel,
background: FARG.white,
}}
>
{STATUS[status]}
</span>
);
}
// ---- 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 (
<div className="border-t py-2" style={{ borderColor: FARG.lightSteel }}>
<Etikett>{etikett}</Etikett>
<div className="mt-1 text-[14px] leading-[20px]" style={{ color: FARG.graphite }}>
{children}
</div>
</div>
);
}
export function Block({ children, rubrik, beteckning }: { children: ReactNode; rubrik?: string; beteckning?: string }) {
return (
<section className="mb-6 border bg-white" style={{ borderColor: FARG.lightSteel }}>
{(rubrik || beteckning) && (
<header
className="flex items-baseline justify-between border-b px-4 py-3"
style={{ borderColor: FARG.lightSteel, background: FARG.background }}
>
{rubrik && <Rubrik niva={3}>{rubrik}</Rubrik>}
{beteckning && <span className="font-mono text-[11px]" style={{ color: FARG.steel }}>{beteckning}</span>}
</header>
)}
<div className="px-4 py-3">{children}</div>
</section>
);
}
// ---- 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 (
<ol className="flex border" style={{ borderColor: FARG.lightSteel }}>
{FASER.map((f) => {
const ärAktiv = f.id === aktiv;
const ärKlar = klara.includes(f.id);
return (
<li
key={f.id}
aria-current={ärAktiv ? "step" : undefined}
className="flex-1 border-r px-4 py-3 last:border-r-0"
style={{
borderColor: FARG.lightSteel,
background: ärAktiv ? FARG.blue : FARG.white,
color: ärAktiv ? FARG.white : ärKlar ? FARG.graphite : FARG.steel,
}}
>
<div className="flex items-center gap-2">
<span className="text-[18px] font-semibold leading-none">{f.bokstav}</span>
<Symbol ikon={ärKlar ? "klar" : ärAktiv ? "pagaende" : "vantar"} ton={ärAktiv ? FARG.white : undefined} />
</div>
<div className="mt-1 text-[11px] font-semibold uppercase tracking-[0.1em]">{f.namn}</div>
<div className="mt-1 text-[11px] leading-[16px]" style={{ opacity: ärAktiv ? 0.9 : 0.75 }}>
{f.syfte}
</div>
</li>
);
})}
</ol>
);
}
// ---- 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 (
<div style={{ background: FARG.background }}>
<div className="mx-auto max-w-[880px] px-6 py-8">
<div className="mb-6 flex items-baseline justify-between">
<Etikett ton="graphite">System</Etikett>
<span className="font-mono text-[11px]" style={{ color: FARG.steel }}>{bet}</span>
</div>
<div className="mb-6">
<Fasrad aktiv={steg.fas} klara={klaraFaser} />
</div>
<Block rubrik="Current step">
<Falt etikett="Phase">
{fas.namn} {fas.syfte}
</Falt>
<Falt etikett="Step">{steg.steg}</Falt>
<Falt etikett="Objective">{steg.syfte}</Falt>
{steg.underlag && <Falt etikett="Input required">{steg.underlag}</Falt>}
{steg.forvantat && <Falt etikett="Expected value">{steg.forvantat}</Falt>}
<Falt etikett="Status">
<Statusmärke status={steg.status} />
</Falt>
{steg.resultat && <Falt etikett="Result">{steg.resultat}</Falt>}
{steg.nasta && (
<Falt etikett="Next action">
<span className="inline-flex items-center gap-2">
<Symbol ikon="nasta" />
{steg.nasta}
</span>
</Falt>
)}
</Block>
{children}
</div>
</div>
);
}
// ---- 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 (
<button
type={type}
onClick={onClick}
disabled={disabled}
className="border px-4 py-2 text-[13px] font-semibold uppercase tracking-[0.08em] disabled:opacity-40"
style={{
background: primär ? FARG.graphite : FARG.white,
color: primär ? FARG.white : FARG.graphite,
borderColor: primär ? FARG.graphite : FARG.steel,
}}
>
{children}
</button>
);
}
export function Tabell({ kolumner, rader }: { kolumner: string[]; rader: ReactNode[][] }) {
return (
<table className="w-full border-collapse text-[13px]">
<thead>
<tr>
{kolumner.map((k) => (
<th
key={k}
scope="col"
className="border-b px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-[0.1em]"
style={{ borderColor: FARG.lightSteel, color: FARG.steel }}
>
{k}
</th>
))}
</tr>
</thead>
<tbody>
{rader.map((rad, i) => (
<tr key={i}>
{rad.map((cell, j) => (
<td key={j} className="border-b px-3 py-2 align-top" style={{ borderColor: FARG.lightSteel, color: FARG.graphite }}>
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
+109
View File
@@ -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<Status, string> = {
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",
];
+170
View File
@@ -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<Objektklass, number> = {
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`;
@@ -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);
}
});
});
+106
View File
@@ -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),
);
}