fix(okr): fase D akse-B — index-rekkefølge fra kaller, ikke fra disk

Vår alfabetiske `.sort()` i okf-index.mjs lå på akse B (ingest-spec.md:178-181),
som krever manifestets ekstraksjonsrekkefølge fra kalleren og eksplisitt forbyr
«filesystem enumeration order». Ruling-dokumentet (portfolio-optimiser-commons
docs/plan/2026-07-25-ordering-axes-ruling.md §4 @ a67a243) navngir alfabetisk
sortering som deterministisk FEIL på denne aksen: B ber ikke om *en*
deterministisk rekkefølge, den ber om *kallerens*.

Kontrakt (operatør-valgt), implementert i orderEntries():
1. navn som alt står i indeksen  -> beholder linjerekkefølgen (§6 «preserved
   byte for byte»)
2. nye navn med kaller-rang      -> kallerens ekstraksjonsrekkefølge
3. nye navn uten kaller-rang     -> alfabetisk

(3) er en dokumentert genesis-fallback, ikke en B-etterlevelse: CLI-en tar kun
en katalog og har ingen kaller-liste å tre gjennom, og alternativet — rå
readdirSync-rekkefølge — er nettopp det B forbyr. Fallbacken gjelder bare ved
genesis; så snart en indeks finnes vinner (1), så et alfabetisk valg overstyrer
aldri en rekkefølge en kaller har etablert.

§5 i rulingen: den som arver B arver §6-idempotensen med den. `order` er derfor
KUN rangering — medlemskapet leses fortsatt fra disk (akse A, :175-177), så en
sti i lista kan aldri opprette eller gjenopplive en fil, og et gate-discardet
dokument kan ikke snike seg inn i indeksen. parseExistingIndex bærer nå
linkOrder eksplisitt framfor å hvile på JS-objekters innsettingsrekkefølge.

innboks-ingest.mjs trer sin faktiske ekstraksjonsrekkefølge gjennom; uten det
ville fiksen vært et ubrukt API.

Tester: 182 -> 187. Alle fem nye verifisert røde mot mutant (orderEntries ->
`[...names].sort()`), de 39 øvrige grønne under samme mutasjon. Ende-til-ende-
testen diskriminerer ved at dokumentrekkefølgen (Zulu før Alfa) er den motsatte
av den alfabetiske.

Ingen versjonsbump; release avventer operatør-go.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EaQkAtvpNCLxWkjdEJ11vx
This commit is contained in:
Kjell Tore Guttormsen 2026-07-31 15:50:56 +02:00
commit 3f4e89fe9b
4 changed files with 207 additions and 18 deletions

View file

@ -244,3 +244,32 @@ test('ingest: binaer-original (.docx/.pdf) passerer strict -- pekerfil uten lenk
assert.deepEqual(manifest(bundleRoot), m1, 'binaer-ingest idempotent');
});
});
// AKSE B ende-til-ende (ingest-spec.md:178-181 + §6). Primitivet er dekket i
// okf-check.test.mjs; DENNE testen beviser at ingest faktisk TRER sin
// ekstraksjonsrekkefoelge gjennom til indeksen -- uten den ville fiksen vaert
// et ubrukt API. Diskriminatoren er at dokumentrekkefoelgen (Zulu foer Alfa)
// er den MOTSATTE av den alfabetiske, saa en regresjon til .sort() er synlig.
test('akse B ende-til-ende: index foelger ekstraksjonsrekkefoelge, ikke alfabetisk', async () => {
await withBundle(async ({ bundleRoot, inbox }) => {
writeFileSync(
join(inbox, 'notat.txt'),
'# Notat 2026\n\nIntro.\n\n## Zulu tema\n\nZulu-tekst.\n\n## Alfa tema\n\nAlfa-tekst.\n',
);
await ingestInbox(inbox, bundleRoot);
const links = readFileSync(join(bundleRoot, 'dokumenter', 'index.md'), 'utf8')
.split('\n')
.map((l) => l.match(/^\*\s*\[[^\]]*\]\(([^)]+)\)/))
.filter(Boolean)
.map((m) => m[1]);
assert.deepEqual(
links,
['notat-2026.md', 'zulu-tema.md', 'alfa-tema.md', 'notat.kilde.md'],
'index-lenker skal staa i dokumentets ekstraksjonsrekkefoelge',
);
const alphabetical = [...links].sort();
assert.notDeepEqual(links, alphabetical, 'testen er kun gyldig hvis den skiller seg fra alfabetisk');
});
});

View file

@ -310,6 +310,111 @@ test('okf-index: tittel/beskrivelse med ]/) + lenke saneres -> 2x byte-identisk
}
});
// --- Akse B: generering/lenkerekkefoelge (ingest-spec.md:178-181 + §6) ---
//
// B sier at rekkefoelgen paa genererte lenker er kallerens ekstraksjons-
// rekkefoelge, "never filesystem enumeration order". Ruling-dokumentet
// (portfolio-optimiser-commons docs/plan/2026-07-25-ordering-axes-ruling.md §4)
// navngir alfabetisk sortering eksplisitt som deterministisk FEIL paa denne
// aksen: B ber ikke om *en* deterministisk rekkefoelge, den ber om *kallerens*.
// §5 i samme dokument: den som arver B, arver §6-idempotensen med den.
// Hjelper: lenkene i en index.md, i linjerekkefoelge.
function entryLinks(path) {
return readFileSync(path, 'utf8')
.split('\n')
.map((l) => l.match(/^\*\s*\[[^\]]*\]\(([^)]+)\)/))
.filter(Boolean)
.map((m) => m[1]);
}
test('akse B: eksisterende index-rekkefoelge bevares (ikke re-sortert alfabetisk)', () => {
const dir = tmpRoot();
try {
mkdirSync(dir, { recursive: true });
for (const n of ['a', 'b', 'c']) {
writeFileSync(join(dir, `${n}.md`), `---\ntype: OKR\ntitle: ${n.toUpperCase()}\n---\n# ${n}\n`);
}
// Eksisterende indeks staar i IKKE-alfabetisk rekkefoelge: c, a, b.
writeFileSync(
join(dir, 'index.md'),
'# Bundle\n\nokf_version: 0.1\nokf_layout: kb-layout-2026-06\n\n'
+ '* [C](c.md) - tredje\n* [A](a.md) - foerste\n* [B](b.md) - andre\n',
);
generateIndexes(dir);
assert.deepEqual(
entryLinks(join(dir, 'index.md')),
['c.md', 'a.md', 'b.md'],
'entries som allerede staar i indeksen skal beholde sin linjerekkefoelge',
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('akse B: nye entries appendes i KALLERENS rekkefoelge, ikke alfabetisk', () => {
const dir = tmpRoot();
try {
mkdirSync(dir, { recursive: true });
for (const n of ['a', 'm', 'z']) {
writeFileSync(join(dir, `${n}.md`), `---\ntype: OKR\ntitle: ${n.toUpperCase()}\n---\n# ${n}\n`);
}
writeFileSync(
join(dir, 'index.md'),
'# Bundle\n\nokf_version: 0.1\nokf_layout: kb-layout-2026-06\n\n* [A](a.md) - finnes\n',
);
// Kaller ekstraherte z FOER m. Alfabetisk ville gitt m, z.
generateIndexes(dir, { order: ['z.md', 'm.md'] });
assert.deepEqual(
entryLinks(join(dir, 'index.md')),
['a.md', 'z.md', 'm.md'],
'nye entries skal appendes i kallerens ekstraksjonsrekkefoelge',
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('akse B + §6: lenking er idempotent by target (ingen duplikat paa andre kjoering)', () => {
const dir = tmpRoot();
try {
mkdirSync(dir, { recursive: true });
for (const n of ['a', 'z']) {
writeFileSync(join(dir, `${n}.md`), `---\ntype: OKR\ntitle: ${n.toUpperCase()}\n---\n# ${n}\n`);
}
generateIndexes(dir, { order: ['z.md', 'a.md'] });
const run1 = readFileSync(join(dir, 'index.md'), 'utf8');
generateIndexes(dir, { order: ['z.md', 'a.md'] });
const run2 = readFileSync(join(dir, 'index.md'), 'utf8');
assert.equal(run1, run2, 'andre kjoering skal vaere byte-identisk');
const links = entryLinks(join(dir, 'index.md'));
assert.deepEqual(links, ['z.md', 'a.md'], 'kallerens rekkefoelge skal gjelde ogsaa ved genesis');
assert.equal(new Set(links).size, links.length, 'ingen lenke skal forekomme to ganger');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('akse B: fjernet fil mister sin lenke, gjenvaerende beholder rekkefoelge', () => {
const dir = tmpRoot();
try {
mkdirSync(dir, { recursive: true });
for (const n of ['a', 'b', 'c']) {
writeFileSync(join(dir, `${n}.md`), `---\ntype: OKR\ntitle: ${n.toUpperCase()}\n---\n# ${n}\n`);
}
generateIndexes(dir, { order: ['c.md', 'a.md', 'b.md'] });
rmSync(join(dir, 'a.md'));
generateIndexes(dir);
assert.deepEqual(
entryLinks(join(dir, 'index.md')),
['c.md', 'b.md'],
'lenke til fjernet fil skal bort; resten beholder innbyrdes rekkefoelge',
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('okf-index: innboks/ + dot-katalog -> ingen egen index.md OG ikke listet i rot-index', () => {
const dir = tmpRoot();
try {