fix(okr): frontmatter-verdi krysser ikke linjeskift (patch-lane #5)

`get()`-regexens verdi-gren brukte `\s*`, som inkluderer linjeskift. Enhver
key med tom rest-av-linje slukte dermed den neste ikke-tomme linja. Utslaget
var bredere enn list-keys: en tom `title:` returnerte neste keys hele linje
("type: OKR"), og et tomt `description:` dempet okf-checks anbefalt-felt-
advarsel med data som tilhorte en annen key.

Verdi-grenen strammet til `[ \t]*`. Innrykk-ankeret (`^\s*`) er urort -- det
er load-bearing for inject-okr-context.mjs:69s nestede organisasjon:-lesing.
Kontrakt-kommentaren i :13-16 lovet allerede null for list-keys; den er naa
sann i stedet for aa bli rettet ned.

Sju RED-verifiserte tester: (L1) list-key gir null - (L2) tom skalar sluker
ikke neste rot-key - (L3) tom key foran list-blokk - (L4) innrykk-toleransen
bevart, forelder gir null - (L5) tab-separert verdi - (L6) tomt anbefalt felt
demper ikke okf-check-advarselen (rootLevelGet arvet defekten) - (L7)
broedtekst-fallbacken plukker ikke list-verdi fra frontmatter.

(L7) gjor S57s latente begrunnelse maalbar: uttrekket i okf-check.mjs:125 ble
beholdt mot nettopp denne defekten, og naar fm-laget endelig taper for
list-keys er `body = raw` foerste gang en roednende mutasjon.

Mutasjons-verifisert (M1 revert, M3 anker-fjerning, M4 uttrekk-fjerning). M2
viste at (L5) ikke vokter tegnklassen -- .trim() gjor `[ \t]*` og `[ ]*`
ekvivalente -- saa testen er omskrevet til aa paastaa det den faktisk viser.

Suite 321 -> 328.
This commit is contained in:
Kjell Tore Guttormsen 2026-08-09 14:43:15 +02:00
commit 9a68441161
4 changed files with 109 additions and 15 deletions

View file

@ -11,6 +11,17 @@
// beholder en intern '#' ("A #B" -> A #B). Retter comment-leak-bugen der
// `okr_frikoblet_fra_loenn: true # ...` lakk kommentaren inn i verdien.
//
// Verdi-delen matches med `[ \t]*`, ALDRI `\s*`: `\s` inkluderer linjeskift, saa
// en verdi-`\s*` lot enhver key med tom rest-av-linje sluke den neste ikke-tomme
// linja. Utslaget var ikke begrenset til list-keys -- en tom `title:` returnerte
// neste keys hele linje ("type: OKR"), og et tomt `description:` dempet
// okf-checks anbefalt-felt-advarsel med data som tilhorte en annen key.
// De to `\s`-ene har ULIK jobb: innrykk-anker (bevart) vs. verdi-avgrensning
// (strammet). Tester: (L1)-(L4) + (L6). Bytt aldri verdi-grenen tilbake til `\s*`.
// `\t` i klassen er intensjons-dokumentasjon, ikke atferd: .trim() under gjor
// `[ \t]*` og `[ ]*` ekvivalente (mutasjons-verifisert). Det som BAERER fiksen er
// at linjeskift er utenfor klassen.
//
// Tolererer fler-linje OKF-list-verdier (f.eks. `tags:`) uten krasj: get() paa
// en list-key returnerer null (rest-av-linja er tom); list-elementer paa
// foelgende linjer konsumeres aldri (ingen konsument leser tre-filenes `tags`).
@ -26,7 +37,7 @@ export function parseFrontmatter(content) {
const get = (key) => {
if (raw === null) return null;
const m = raw.match(new RegExp(`^\\s*${key}:\\s*(.*)$`, 'm'));
const m = raw.match(new RegExp(`^\\s*${key}:[ \\t]*(.*)$`, 'm'));
if (!m) return null;
let v = m[1].trim();
if (v === '') return null;

View file

@ -113,15 +113,13 @@ function rootMarkers(root) {
// hver for seg (mutasjon M1 roedner (20a)/(20d) NETTOPP fordi fallbacken ikke
// ser frontmatter-linjene).
//
// AERLIG OM DEKNINGEN: uttrekket er ikke produksjonsobserverbart i dag. Naar
// noekkelen finnes i frontmatter i en form parseFrontmatter i det hele tatt
// returnerer, vinner fm-laget foer fallbacken kjoeres -- saa `body = raw`
// roedner ingen test (maalt, ikke antatt). Det beholdes likevel: fjernes det,
// ligger en latent defekt og venter paa at lib/frontmatter.mjs's list-key-
// kontrakt (:13-15) blir sann. I dag returnerer get() paa en list-key foerste
// LIST-ELEMENT, ikke null, fordi `\s*` i :29-regexen spiser linjeskiftet.
// Fikses det (patch-lane), begynner fallbacken aa kjoere for list-keys -- og
// uten dette uttrekket ville den plukket «- a» ut av frontmatter.
// DEKNING: uttrekket ER produksjonsobserverbart siden lib/frontmatter.mjs's
// list-key-kontrakt ble sann (verdi-grenen strammet fra `\s*` til `[ \t]*`).
// get() paa en list-key gir naa null, saa fm-laget taper og fallbacken kjoerer
// for list-keys -- foerste gang den grenen naas i praksis. Uten uttrekket ville
// `^okf_version:` matchet INNE i frontmatteren og plukket «- 9.9». `body = raw`
// roedner naa (L7) (mutasjons-verifisert). S57 beholdt dette mot nettopp den
// latente defekten; den er ikke lenger latent, og vakten er ekte.
const body = fmRaw === null ? raw : raw.slice(raw.indexOf('\n---', 3) + 4);
const pick = (key) => {
const fromFm = fmGet(key);

View file

@ -123,11 +123,54 @@ test('writeFrontmatter: array-verdi (tags) -> OKF multi-linje liste (Step 3)', (
const { get } = parseFrontmatter(block);
assert.equal(get('type'), 'OKR', 'skalar FOR list-blokk resolver');
assert.equal(get('kilde'), 'innboks', 'skalar ETTER innrykket list-blokk resolver fortsatt');
// Lese-siden uendret (Step 3 scope = kun skrive-siden): get() paa en list-key
// krasjer ikke (eksisterende tolerance-kontrakt, frontmatter.mjs:14-16). Den
// returnerer foerste list-element fordi parser-\s* spiser newline -- IKKE
// null; konsumentene leser aldri tag-VERDIER, kun at nabo-skalarer resolver.
assert.doesNotThrow(() => get('tags'), 'get paa list-key krasjer ikke');
// get() paa en list-key returnerer null -- tolerance-kontrakten i
// frontmatter.mjs:14-16 er sann etter at verdi-grenen ble strammet fra \s*
// til [ \t]* (se (L1)). Fram til da returnerte den foerste list-ELEMENT.
assert.equal(get('tags'), null, 'get paa list-key gir null');
});
// --- Linjeskift-aksen (L1-L5) ------------------------------------------------
// `:\s*(.*)$` lot \s* krysse linjeskift, saa ENHVER key med tom rest-av-linje
// slukte den neste ikke-tomme linja. Utslaget er ikke begrenset til list-keys:
// en tom `title:` returnerte neste keys HELE linje ("type: OKR"). Fikset ved aa
// stramme KUN den andre \s* til [ \t]*; den foerste (`^\s*`, innrykk-toleranse)
// er load-bearing for inject-okr-context.mjs:69 og staar urort -- (L4) vokter det.
test('(L1) list-key: get() returnerer null (kontrakten :14-16 blir sann)', () => {
const { get } = parseFrontmatter(
'---\ntype: OKR\ntags:\n - alpha\n - beta\ntitle: Ekte tittel\n---\n',
);
assert.equal(get('tags'), null, 'list-key skal gi null, ikke foerste list-element');
assert.equal(get('title'), 'Ekte tittel', 'skalar etter list-blokk resolver fortsatt');
});
test('(L2) tom skalar-key sluker ikke neste rot-key', () => {
const { get } = parseFrontmatter('---\ntitle:\ntype: OKR\n---\n');
assert.equal(get('title'), null, 'tom title skal gi null, ikke "type: OKR"');
assert.equal(get('type'), 'OKR', 'den slukte keyen resolver selv');
});
test('(L3) tom key rett foran innrykket list-blokk gir null', () => {
const { get } = parseFrontmatter('---\ntype: OKR\ndescription:\ntags:\n - x\n---\n');
assert.equal(get('description'), null, 'tom description skal gi null, ikke "tags:"');
});
test('(L4) innrykk-toleransen BEVART: nestet oppslag resolver, forelder gir null', () => {
const { get } = parseFrontmatter(
'---\norganisasjon:\n navn: "NestetOrg"\n type: "offentlig"\n---\n',
);
assert.equal(get('navn'), 'NestetOrg', 'load-bearing inject:69 -- foerste \\s* er urort');
assert.equal(get('organisasjon'), null, 'tom forelder-key skal ikke gi "navn: ..."');
});
test('(L5) tab etter kolon: verdien resolver rent', () => {
// MERK: denne er IKKE en vakt for `\t` i tegnklassen. Mutasjonstest viste at
// `[ ]*` passerer like godt -- .trim() paa :39 normaliserer tabene uansett,
// saa `[ \t]*` og `[ ]*` er atferdsmessig ekvivalente for all input. `\t` staar
// igjen som intensjons-dokumentasjon (horisontal whitespace), ikke som atferd.
// Testen vokter kontrakten "tab-separert verdi resolver", som holder uansett.
const { get } = parseFrontmatter('---\nnavn:\t\tOrg\n---\n');
assert.equal(get('navn'), 'Org');
});
test('writeFrontmatter: skalar uendret av array-gren (additiv)', () => {

View file

@ -1194,6 +1194,48 @@ test('(N4) okf-check: et NESTET anbefalt felt demper ikke advarselen', () => {
}
});
test('(L6) okf-check: et TOMT anbefalt felt demper ikke advarselen', () => {
// Linjeskift-aksen naar rootLevelGet: innrykk-filteret fjerner list-elementer,
// men et tomt `description:` fulgt av neste ROT-key slukte den linja og gjorde
// feltet ikke-tomt -- advarselen ble dempet av data som tilhorte `title`.
const dir = tmpRoot();
try {
buildCleanIngest(dir);
writeFileSync(
join(dir, 'dokumenter', 'tom-anbefalt.md'),
'---\ntype: Notat\ndescription:\ntitle: Ekte tittel\n---\n# Tom anbefalt\n',
);
const r = checkBundle(dir, { files: [join(dir, 'dokumenter', 'tom-anbefalt.md')] });
assert.ok(
r.warnings.some((w) => /tom-anbefalt\.md: mangler anbefalt felt .description./.test(w)),
`tomt description -> advarsel forventet, fikk:\n${r.warnings.join('\n')}`,
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(L7) okf-check: broedtekst-fallbacken plukker ikke list-verdi FRA frontmatter', () => {
// S57 beholdt broedtekst-uttrekket (okf-check.mjs:125) mot en LATENT defekt den
// ikke kunne maale: saa lenge get() paa en list-key ga foerste list-element,
// vant fm-laget alltid og fallbacken kjorte aldri for list-keys. Etter (L1) gir
// fm-laget null -> fallbacken kjorer -> uttrekket er foerste gang observerbart.
// Uten det ville `^okf_version:` matchet inne i frontmatteren og plukket «- 9.9».
const dir = tmpRoot();
try {
buildCleanIngest(dir);
writeFileSync(
join(dir, 'index.md'),
['---', 'type: Index', 'okf_version:', ' - 9.9', '---', '# Rot', '', 'okf_layout: kb-layout-2026-06', ''].join('\n'),
);
const r = checkBundle(dir, { files: [] });
assert.equal(r.okfVersion, null, 'list-verdi i frontmatter skal ikke lekke via fallbacken');
assert.equal(r.okfLayout, 'kb-layout-2026-06', 'ekte broedtekst-markoer resolver fortsatt');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('(N5) okf-check: en NESTET okf_version i rot-index leses ikke som rot-markoer', () => {
const dir = tmpRoot();
try {