diff --git a/scripts/kb-update/lib/transform.mjs b/scripts/kb-update/lib/transform.mjs index 14a1fdc..32066eb 100644 --- a/scripts/kb-update/lib/transform.mjs +++ b/scripts/kb-update/lib/transform.mjs @@ -359,6 +359,92 @@ export function insertHeaderFields(content, meta) { return lines.join('\n'); } +/** + * Surgically insert-or-update ONLY the `**Verified:**` and `**Verified by:**` header lines + * on an EXISTING KB file — the R7–R10 born-verified STAMP primitive. Unlike composeKbFile + * (which rebuilds the whole header and may insert a `## Innhold` → body diff), this touches + * nothing but the two stamp lines: the body is byte-identical, no TOC is ever added, and a + * large file's structure is otherwise unchanged. It is the mirror image of + * normalizeStaleVerified — that strips a stale stamp; this writes a fresh, judge-cleared one. + * + * Insert vs update: a stamp line already inside the 500-byte scan window has ONLY its value + * token replaced in place (so re-stamping the same value is a byte-identical no-op — + * idempotent, and a pipe row's sibling tokens survive); an absent stamp line is inserted + * immediately after the last line of the first contiguous bold-label meta run that still + * leaves room in the window (the same anchor + back-off discipline as insertHeaderFields), + * ordered `**Verified:**` then `**Verified by:**` to match buildKbHeader. + * + * THROWS if either stamp would land past the 500-byte window — a header so full the stamp + * would be invisible to parseVerified*Header. A silent, unparseable stamp is worse than none + * (the file would read as unverified while carrying a verified line); R7 treats this throw as + * "flagged: header-slanking kreves", never a forced stamp. + * + * @param {string} content — full existing file content + * @param {{verified: string, verified_by: string}} fields — both required + * @returns {string} content with the two stamp lines inserted/updated, body byte-identical + * @throws if verified/verified_by is blank, or a stamp lands past the 500-byte window + */ +export function insertVerifiedFields(content, fields) { + const s = String(content ?? ''); + const f = fields ?? {}; + const blank = (v) => v === undefined || v === null || String(v).trim() === ''; + if (blank(f.verified) || blank(f.verified_by)) { + throw new Error('insertVerifiedFields: both verified and verified_by are required'); + } + const vStr = String(f.verified).trim(); + const vbStr = String(f.verified_by).trim(); + + const lines = s.split('\n'); + let vIdx = -1; // existing **Verified:** line (update target) + let vbIdx = -1; // existing **Verified by:** line (update target) + let anchorIdx = -1; // last contiguous meta line whose insertion point still fits the window + let titleIdx = -1; // fallback anchor for the "no meta line" dialect + let cum = 0; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (/^##\s/.test(line) || /^---\s*$/.test(line)) break; // reached body / rule + if (titleIdx === -1 && /^#\s+\S/.test(line)) titleIdx = i; + if (cum < HEADER_REGION_BYTES) { + // Only recognise an existing stamp the parsers can actually read (inside the window). + // `**Verified:**` (colon straight after) never matches the `**Verified by:**` line. + if (vIdx === -1 && /\*\*Verified:\*\*/i.test(line)) vIdx = i; + if (vbIdx === -1 && /\*\*Verified by:\*\*/i.test(line)) vbIdx = i; + } + const nextCum = cum + line.length + 1; // byte offset just AFTER this line + if (RE_META_LINE.test(line)) { + if (nextCum > HEADER_REGION_BYTES) break; // back off — insertion point past the window + anchorIdx = i; + } else if (anchorIdx !== -1) { + break; // first non-meta line after the run — the contiguous run is over + } + cum = nextCum; + } + + // Update present stamp lines in place — replace ONLY the value token (function replacer so a + // `$` in the value is never treated as a backreference); a pipe row's siblings are preserved. + if (vIdx !== -1) lines[vIdx] = lines[vIdx].replace(/(\*\*Verified:\*\*\s*)\S+/i, (_m, p1) => p1 + vStr); + if (vbIdx !== -1) lines[vbIdx] = lines[vbIdx].replace(/(\*\*Verified by:\*\*\s*)\S+/i, (_m, p1) => p1 + vbStr); + + // Insert absent stamp lines after the meta-run anchor (title fallback for the no-meta + // dialect; -1 + 1 = 0 → very top if there is no title either), Verified then Verified by. + const toInsert = []; + if (vIdx === -1) toInsert.push(`**Verified:** ${vStr}`); + if (vbIdx === -1) toInsert.push(`**Verified by:** ${vbStr}`); + if (toInsert.length) { + lines.splice((anchorIdx !== -1 ? anchorIdx : titleIdx) + 1, 0, ...toInsert); + } + + const out = lines.join('\n'); + // Post-write invariant: both stamps must be visible to the header parsers (inside 500B). + // A stamp that landed past the window is a silent lie — refuse it (header-slanking needed). + if (parseVerifiedHeader(out) !== vStr || parseVerifiedByHeader(out) !== vbStr) { + throw new Error( + 'insertVerifiedFields: stamp would land past the 500-byte header window (header-slanking required)', + ); + } + return out; +} + /** * Surgically strip STALE `**Verified:**` value(s) from a legacy KB file — the bounded * Spor 1 carve-out for the 14 files carrying `**Verified:** MCP `. That non-date diff --git a/tests/kb-update/test-transform.test.mjs b/tests/kb-update/test-transform.test.mjs index afb160a..59e8075 100644 --- a/tests/kb-update/test-transform.test.mjs +++ b/tests/kb-update/test-transform.test.mjs @@ -25,6 +25,7 @@ import { composeKbFile, insertToc, insertHeaderFields, + insertVerifiedFields, normalizeStaleVerified, stampVerifiedMeta, } from '../../scripts/kb-update/lib/transform.mjs'; @@ -704,6 +705,80 @@ test('insertToc(insertHeaderFields(large)) → Type+Source+TOC, passes real chec assert.equal(tail(out), tail(HDR_LARGE)); }); +// --- insertVerifiedFields: the R7–R10 surgical stamp primitive --------------------- +// Insert-or-update ONLY the **Verified:** / **Verified by:** header lines in place, body +// byte-identical, NEVER a TOC (contrast composeKbFile, which rebuilds the header + may add +// ## Innhold → body diff). THROWS when the stamp would land past the 500-byte window so a +// judge-cleared file is never given a silent, unparseable stamp. + +// A migrated reference carrying Type+Source but NOT yet Verified — the R7 stamp target. +const HDR_UNVERIFIED = + '# Azure AI Foundry\n\n' + + '**Last updated:** 2026-06\n' + + '**Status:** GA\n' + + '**Category:** AI Services\n' + + '**Type:** reference\n' + + '**Source:** https://learn.microsoft.com/azure/ai-foundry/x\n\n' + + '---\n\n' + + '## Introduksjon\n\nBrødtekst.\n'; + +// Same, but large (>100 lines) — proves stamping never triggers a TOC insertion. +const HDR_LARGE_UNVERIFIED = + '# Big Ref\n\n' + + '**Last updated:** 2026-06\n' + + '**Status:** GA\n' + + '**Category:** X\n' + + '**Type:** reference\n' + + '**Source:** ' + MS_URL + '\n\n' + + '---\n\n' + + LARGE_BODY; + +// A header whose title line alone exceeds the 500-byte window: any stamp inserted after it +// lands past the window → invisible to parseVerified*Header → insertVerifiedFields must THROW. +const HDR_NO_ROOM = + '# ' + 'A'.repeat(520) + '\n\n' + + '**Type:** reference\n**Source:** ' + MS_URL + '\n\n---\n\n## Introduksjon\n\ntekst.\n'; + +test('insertVerifiedFields stamps Verified + Verified by, body byte-identical, no TOC', () => { + const out = insertVerifiedFields(HDR_UNVERIFIED, { verified: '2026-07-04', verified_by: 'judge-v3.1' }); + assert.equal(parseVerifiedHeader(out), '2026-07-04'); + assert.equal(parseVerifiedByHeader(out), 'judge-v3.1'); + assert.doesNotMatch(out, /## Innhold/); // never a TOC (contrast composeKbFile) + const tail = (s) => s.slice(s.indexOf('## Introduksjon')); + assert.equal(tail(out), tail(HDR_UNVERIFIED)); // body byte-identical +}); + +test('insertVerifiedFields updates an existing stamp in place (idempotent + re-stampable)', () => { + const once = insertVerifiedFields(HDR_UNVERIFIED, { verified: '2026-07-04', verified_by: 'judge-v3.1' }); + const twice = insertVerifiedFields(once, { verified: '2026-07-04', verified_by: 'judge-v3.1' }); + assert.equal(twice, once, 'same values → byte-identical (idempotent)'); + const updated = insertVerifiedFields(once, { verified: '2026-08-01', verified_by: 'human' }); + assert.equal(parseVerifiedHeader(updated), '2026-08-01'); + assert.equal(parseVerifiedByHeader(updated), 'human'); + assert.equal((updated.match(/\*\*Verified:\*\*/g) || []).length, 1, 'not duplicated'); + assert.equal((updated.match(/\*\*Verified by:\*\*/g) || []).length, 1, 'not duplicated'); +}); + +test('insertVerifiedFields never inserts a TOC into a large file (contrast composeKbFile)', () => { + const out = insertVerifiedFields(HDR_LARGE_UNVERIFIED, { verified: '2026-07-04', verified_by: 'judge-v3.1' }); + assert.equal(parseVerifiedHeader(out), '2026-07-04'); + assert.doesNotMatch(out, /## Innhold/); + const tail = (s) => s.slice(s.indexOf('## Introduksjon')); + assert.equal(tail(out), tail(HDR_LARGE_UNVERIFIED)); +}); + +test('insertVerifiedFields throws when the stamp would land past the 500-byte window', () => { + assert.throws( + () => insertVerifiedFields(HDR_NO_ROOM, { verified: '2026-07-04', verified_by: 'judge-v3.1' }), + /500|window|vindu/i, + ); +}); + +test('insertVerifiedFields requires both verified and verified_by', () => { + assert.throws(() => insertVerifiedFields(HDR_UNVERIFIED, { verified: '2026-07-04' }), /verified_by|required/i); + assert.throws(() => insertVerifiedFields(HDR_UNVERIFIED, { verified_by: 'judge-v3.1' }), /verified|required/i); +}); + // --- normalizeStaleVerified: surgical carve-out for the 14 `**Verified:** MCP` files --- // Acts on EVERY stale (non-date) **Verified:** that falls inside the 500-byte scan window — // exactly what parseVerifiedHeader reads as the file's verified value, whether it sits in