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:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
];
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user