feat(ms-ai-architect): insertToc + backfill-toc applier (Fase 1b-verktøy) — store-fil-TOC foldet inn i Cosmo-utfasing
1b-premisset (ikke-advisor storfiler er Cosmo-frie, trygt å TOC-e nå) er motbevist mot ground truth: 11/12 har en `## …Cosmo…`-overskrift → en TOC bygget fra `##`-overskriftene ville emittert ny Cosmo-anker (bryter «aldri ny Cosmo-innhold»), og å nøytralisere overskriften er selve persona-fjerningen (ikke en sidehandling — scope-guard). Store-fil-TOC (de 20 filene >800 linjer, advisor + ikke-advisor) folder derfor inn i Cosmo-utfasingen. Denne committen leverer kun det gjenbrukbare verktøyet; ingen ref-filer rørt. - transform.insertToc: retrofitter `## Innhold` i en eksisterende storfil rett før første ikke-fence `## `-seksjon (samme slot som composeKbFile), robust mot de 3 legacy-header-variantene (med/uten `---`), idempotent, fence-aware. - scripts/kb-update/backfill-toc.mjs: dry-run/--write applier (ren glue rundt insertToc; transform.mjs forblir write-fri — arkitektur-invariant). - docs/cosmo-removal-brief: «Sammenfall»-seksjon dokumenterer fold-inn + bruk. - TDD: +6 transform-tester. Suite 509/509 (kb-update 355 + kb-eval 154).
This commit is contained in:
parent
2240f1efdd
commit
bfc79710c8
4 changed files with 179 additions and 0 deletions
|
|
@ -23,6 +23,7 @@ import {
|
|||
resolveTargetPath,
|
||||
buildToc,
|
||||
composeKbFile,
|
||||
insertToc,
|
||||
} from '../../scripts/kb-update/lib/transform.mjs';
|
||||
import { parseSourceHeader } from '../../scripts/kb-update/lib/kb-headers.mjs';
|
||||
import { classifyChange } from '../../scripts/kb-update/lib/verify-out.mjs';
|
||||
|
|
@ -217,6 +218,89 @@ test('composeKbFile is idempotent: a body that already carries a TOC is not doub
|
|||
assert.equal(validateKbFile(twice).valid, true);
|
||||
});
|
||||
|
||||
// --- insertToc: backfill a TOC into an EXISTING large file (Fase 1b) -----------
|
||||
// composeKbFile builds a NEW file from meta+body; insertToc retrofits the ~20 largest
|
||||
// hand-written files on disk, whose legacy headers vary (some carry a `---` rule, some
|
||||
// don't), without touching their bodies. It places `## Innhold` immediately before the
|
||||
// first non-fenced `## ` section — the same relative slot composeKbFile uses.
|
||||
|
||||
// Large legacy file WITHOUT a `---` header rule (the secure-model-deployment shape).
|
||||
const LEGACY_NO_RULE =
|
||||
'# Secure Model Deployment\n\n' +
|
||||
'**Kategori:** AI Security Engineering\n' +
|
||||
'**Dato:** 2026-02-05\n\n' +
|
||||
'## Introduksjon\n\n' + 'Brødtekst.\n'.repeat(110) +
|
||||
'\n## Container Scanning\n\nMer tekst.\n' +
|
||||
'\n## Avslutning\n\nSlutt.\n';
|
||||
|
||||
// Large legacy file WITH a `---` header rule (the ros-ai-threat-library shape).
|
||||
const LEGACY_WITH_RULE =
|
||||
'# AI-trusselbibliotek\n\n' +
|
||||
'**Sist oppdatert:** 2026-06-18\n' +
|
||||
'**Kategori:** Norwegian Public Sector AI Governance\n\n' +
|
||||
'---\n\n' +
|
||||
'## Oversikt\n\n' + 'Brødtekst.\n'.repeat(110) +
|
||||
'\n## Metode\n\nMer tekst.\n';
|
||||
|
||||
test('insertToc adds a ## Innhold before the first ## section, body untouched (no `---` header)', () => {
|
||||
const out = insertToc(LEGACY_NO_RULE);
|
||||
assert.equal((out.match(/^##\s+Innhold/gm) || []).length, 1);
|
||||
// TOC sits before the first content section …
|
||||
assert.ok(out.indexOf('## Innhold') < out.indexOf('## Introduksjon'));
|
||||
// … and lists every ## section in order
|
||||
assert.match(out, /- \[Introduksjon\]\(#introduksjon\)/);
|
||||
assert.match(out, /- \[Container Scanning\]\(#container-scanning\)/);
|
||||
assert.match(out, /- \[Avslutning\]\(#avslutning\)/);
|
||||
// body from the first section onward is byte-identical (zero churn)
|
||||
const tail = (s) => s.slice(s.indexOf('## Introduksjon'));
|
||||
assert.equal(tail(out), tail(LEGACY_NO_RULE));
|
||||
});
|
||||
|
||||
test('insertToc places the TOC after a `---` header rule, before the first section', () => {
|
||||
const out = insertToc(LEGACY_WITH_RULE);
|
||||
assert.ok(out.indexOf('---') < out.indexOf('## Innhold'));
|
||||
assert.ok(out.indexOf('## Innhold') < out.indexOf('## Oversikt'));
|
||||
const tail = (s) => s.slice(s.indexOf('## Oversikt'));
|
||||
assert.equal(tail(out), tail(LEGACY_WITH_RULE)); // body preserved
|
||||
});
|
||||
|
||||
test('insertToc is a no-op on a small file (TOC is for large files only)', () => {
|
||||
const small = '# T\n\n## A\n\ntekst\n\n## B\n\nmer\n';
|
||||
assert.equal(insertToc(small), small);
|
||||
});
|
||||
|
||||
test('insertToc is idempotent: a file that already carries a TOC is unchanged', () => {
|
||||
const once = insertToc(LEGACY_NO_RULE);
|
||||
const twice = insertToc(once);
|
||||
assert.equal(twice, once);
|
||||
assert.equal((once.match(/^##\s+Innhold/gm) || []).length, 1);
|
||||
});
|
||||
|
||||
test('insertToc turns an N4-failing large file into an N4-passing one (real-check oracle)', () => {
|
||||
const before = checkN4([{ path: 'skills/x/references/y/z.md', content: LEGACY_NO_RULE }]);
|
||||
assert.equal(before.largeFiles, 1);
|
||||
assert.equal(before.withToc, 0);
|
||||
assert.equal(before.pass, false);
|
||||
const after = checkN4([{ path: 'skills/x/references/y/z.md', content: insertToc(LEGACY_NO_RULE) }]);
|
||||
assert.equal(after.withToc, 1);
|
||||
assert.equal(after.pass, true);
|
||||
});
|
||||
|
||||
test('insertToc is fence-aware: a ## inside a code fence is neither anchor nor listed', () => {
|
||||
const fenced =
|
||||
'# T\n\n**Kategori:** X\n\n' +
|
||||
'```md\n## Not A Real Section\n```\n\n' +
|
||||
'## Faktisk Seksjon\n\n' + 'Brødtekst.\n'.repeat(110) +
|
||||
'\n## Andre Seksjon\n\nx.\n';
|
||||
const out = insertToc(fenced);
|
||||
assert.doesNotMatch(out, /\[Not A Real Section\]\(#/); // fenced heading not listed
|
||||
assert.match(out, /- \[Faktisk Seksjon\]\(#faktisk-seksjon\)/);
|
||||
assert.match(out, /- \[Andre Seksjon\]\(#andre-seksjon\)/);
|
||||
// the fenced block stays above the TOC; TOC sits before the first real section
|
||||
assert.ok(out.indexOf('## Not A Real Section') < out.indexOf('## Innhold'));
|
||||
assert.ok(out.indexOf('## Innhold') < out.indexOf('## Faktisk Seksjon'));
|
||||
});
|
||||
|
||||
// --- validateKbFile: large files must carry a TOC (the write-path regression gate)
|
||||
|
||||
test('validateKbFile flags a large file missing a TOC', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue