From 722a131eb73bcd641ab7b973f2b5842474384120 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 17 Jul 2026 03:23:40 +0200 Subject: [PATCH] =?UTF-8?q?feat(ms-ai-architect):=20RX-KB1b=20=E2=80=94=20?= =?UTF-8?q?deterministisk=20footer-dato-avvik-deteksjon=20+=20label-whitel?= =?UTF-8?q?ist=20i=20corpus-audit=20(flag-to-human,=203=20flagg)=20[skip-d?= =?UTF-8?q?ocs]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/kb-update/audit-corpus-headers.mjs | 54 +++++++++++++++ .../test-audit-corpus-headers.test.mjs | 65 +++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/scripts/kb-update/audit-corpus-headers.mjs b/scripts/kb-update/audit-corpus-headers.mjs index f1a494a..e838a60 100644 --- a/scripts/kb-update/audit-corpus-headers.mjs +++ b/scripts/kb-update/audit-corpus-headers.mjs @@ -53,6 +53,49 @@ const RE_CLEAN_DATE = /^\d{4}-\d{2}(-\d{2})?$/; // pipe.mjs removes the current 87; this detects any reappearance within the non-advisor scope). const RE_PLAIN_VERIFIED = /(?:^|[^*])Verified:/m; +// ── RX-KB1b: footer date-conflict + label-whitelist ──────────────────────── +// A file that carries a header **Last updated:** date AND a norsk footer date under a +// last-updated-EQUIVALENT label ("Sist oppdatert") with a different value contradicts +// itself — it asserts two distinct last-updated dates. That is the only norsk footer +// label that can conflict with the header. LABEL WHITELIST: "Verifisert"/"Sist +// verifisert" record an INDEPENDENT axis (when content was last checked against source, +// not when the file was edited), so a differing date there is expected and is never a +// conflict. Detection is read-only and flag-to-human: the audit reports, the operator +// resolves per file against git/source (aldri-auto-fiks — a footer date can be right and +// the header stale, or vice versa; only a human can adjudicate which). + +// Header **Last updated:** DATE — captured value (RE_LAST_UPDATED above only tests presence). +const RE_HEADER_LAST_UPDATED_DATE = /\*\*Last updated:\*\*\s*(\d{4}-\d{2}(?:-\d{2})?)/i; + +// Any norsk footer meta-line carrying a date: "**Label:** DATE", "*Label: DATE", "Label: DATE". +// A valid month (01-12) is required, which also excludes template placeholders ([Dato], +// YYYY-MM-DD — they never match \d{4}-(0[1-9]|1[0-2])). Whole-file scan: footers live well +// outside the 500-byte header window. Group 1 = label, group 2 = date. +const RE_FOOTER_META = + /(?:^|\n)[ \t>*_-]*\*{0,2}\s*(Sist oppdatert|Sist verifisert|Verifisert)\s*:?\**\s*(\d{4}-(?:0[1-9]|1[0-2])(?:-\d{2})?)/gi; + +// Only this label is last-updated-equivalent (conflict-bearing). Everything else the +// RE_FOOTER_META captures ("sist verifisert", "verifisert") is on the whitelist. +const FOOTER_LABEL_LAST_UPDATED = 'sist oppdatert'; + +/** + * Footer date-conflict for one file's full content. Returns null when there is no header + * Last-updated date, no last-updated-equivalent footer, or all such footers agree on the + * YYYY-MM prefix. Otherwise returns the header date and the conflicting footer date(s). Pure. + */ +function detectFooterConflict(content, headerLastUpdated) { + if (!headerLastUpdated) return null; + const footerDates = []; + for (const m of content.matchAll(RE_FOOTER_META)) { + if (m[1].toLowerCase() === FOOTER_LABEL_LAST_UPDATED) footerDates.push(m[2]); + } + if (footerDates.length === 0) return null; + const headerPrefix = headerLastUpdated.slice(0, 7); + const conflicting = footerDates.filter((d) => d.slice(0, 7) !== headerPrefix); + if (conflicting.length === 0) return null; + return { headerLastUpdated, footerDates: conflicting }; +} + /** Audit a single file's content. Pure. */ function auditContent(content) { const head = content.slice(0, HEADER_REGION_BYTES); @@ -76,6 +119,9 @@ function auditContent(content) { else if (/\*\*Category:\*\*/i.test(head)) dialect = 'category'; else if (/\*\*Kategori:\*\*/i.test(head)) dialect = 'kategori'; + const headerLuMatch = head.match(RE_HEADER_LAST_UPDATED_DATE); + const footerConflict = detectFooterConflict(content, headerLuMatch ? headerLuMatch[1] : null); + return { title: RE_TITLE.test(content), status: RE_STATUS.test(content), @@ -83,6 +129,7 @@ function auditContent(content) { dialect, verified, plainVerified: RE_PLAIN_VERIFIED.test(head), + footerConflict, }; } @@ -105,6 +152,7 @@ export function auditHeaders(paths, readFile = (p) => readFileSync(p, 'utf8')) { verifiedPipe: 0, verifiedDuplicate: 0, plainVerifiedPipe: 0, + footerConflicts: 0, missingTitle: 0, missingStatus: 0, missingEnglishLastUpdated: 0, @@ -131,6 +179,7 @@ export function auditHeaders(paths, readFile = (p) => readFileSync(p, 'utf8')) { if (r.verified.duplicateWithinHeaderRegion) aggregate.verifiedDuplicate++; } if (r.plainVerified) aggregate.plainVerifiedPipe++; + if (r.footerConflict) aggregate.footerConflicts++; if (!r.title) aggregate.missingTitle++; if (!r.status) aggregate.missingStatus++; if (!r.lastUpdatedEnglish) aggregate.missingEnglishLastUpdated++; @@ -184,6 +233,11 @@ function main(argv) { console.log(` Verified header present: ${a.verifiedPresent} (date: ${a.verifiedDate}, stale non-date: ${a.verifiedNonDate})`); console.log(` Verified pipe rows: ${a.verifiedPipe}, duplicates within 500B: ${a.verifiedDuplicate}`); console.log(` Plain (non-bold) Verified tails (RX-KB1 poison class): ${a.plainVerifiedPipe}`); + console.log(` Footer "Sist oppdatert" ≠ header Last updated (RX-KB1b, flag-to-human): ${a.footerConflicts}`); + for (const [abs, r] of Object.entries(report.files)) { + if (!r.footerConflict) continue; + console.log(` FLAG ${relative(PLUGIN_ROOT, abs)} — header ${r.footerConflict.headerLastUpdated} vs footer ${r.footerConflict.footerDates.join(', ')}`); + } console.log(` Missing English Last updated: ${a.missingEnglishLastUpdated}`); console.log(` Missing Status: ${a.missingStatus}`); console.log(` Missing title: ${a.missingTitle}`); diff --git a/tests/kb-update/test-audit-corpus-headers.test.mjs b/tests/kb-update/test-audit-corpus-headers.test.mjs index 7ccd604..46b663c 100644 --- a/tests/kb-update/test-audit-corpus-headers.test.mjs +++ b/tests/kb-update/test-audit-corpus-headers.test.mjs @@ -163,3 +163,68 @@ test('auditHeaders — an unreadable file is reported, not thrown', () => { assert.equal(r.aggregate.files, 1); assert.equal(r.aggregate.readErrors, 1); }); + +// ── RX-KB1b: footer date-conflict + label-whitelist ──────────────────────── +// A file that claims one **Last updated:** date in the header and a DIFFERENT date +// under a norsk "Sist oppdatert" footer contradicts itself. Only the last-updated- +// EQUIVALENT label ("Sist oppdatert") is conflict-bearing; "Verifisert"/"Sist +// verifisert" record an independent axis (when content was checked vs source) and are +// whitelisted. Detection is read-only and flag-to-human — the audit never mutates. + +// header 2026-06-24, italic footer "Sist oppdatert: 2026-02" — the +// stakeholder-communication-ai-decisions.md shape (genuine self-contradiction). +const FOOTER_CONFLICT = + '# T\n\n**Category:** x\n**Status:** GA\n**Last updated:** 2026-06-24\n\n---\n\n' + + '## A\n\ntekst\n\n*Sist oppdatert: 2026-02 av Cosmo Skyberg (AI Architect Plugin)*\n'; + +// header and footer agree on the month — no conflict. +const FOOTER_MATCH = + '# T\n\n**Category:** x\n**Status:** GA\n**Last updated:** 2026-02-03\n\n---\n\n' + + '## A\n\ntekst\n\n**Sist oppdatert:** 2026-02-03\n'; + +// footer carries a DIFFERENT date but under the whitelisted "Sist verifisert" label — +// an independent provenance axis, NOT a conflict. +const FOOTER_VERIFISERT_INDEP = + '# T\n\n**Category:** x\n**Status:** GA\n**Last updated:** 2026-06-24\n\n---\n\n' + + '## A\n\ntekst\n\n**Sist verifisert:** 2026-02-04\n'; + +// placeholder footer ([Dato]) is template scaffolding, not a real date — never a conflict. +const FOOTER_PLACEHOLDER = + '# T\n\n**Category:** x\n**Status:** GA\n**Last updated:** 2026-06-24\n\n---\n\n' + + '## A\n\n## Versjon og godkjenning\n- **Sist oppdatert:** [Dato]\n'; + +test('auditHeaders — footer "Sist oppdatert" date ≠ header Last updated is flagged as a conflict', () => { + const r = auditHeaders(['a.md'], reader({ 'a.md': FOOTER_CONFLICT })); + const f = r.files['a.md']; + assert.ok(f.footerConflict, 'conflict object present'); + assert.equal(f.footerConflict.headerLastUpdated, '2026-06-24'); + assert.deepEqual(f.footerConflict.footerDates, ['2026-02']); + assert.equal(r.aggregate.footerConflicts, 1); +}); + +test('auditHeaders — footer "Sist oppdatert" matching the header month raises no conflict', () => { + const r = auditHeaders(['b.md'], reader({ 'b.md': FOOTER_MATCH })); + assert.equal(r.files['b.md'].footerConflict, null); + assert.equal(r.aggregate.footerConflicts, 0); +}); + +test('auditHeaders — a differing "Sist verifisert" footer is whitelisted, not a conflict', () => { + const r = auditHeaders(['c.md'], reader({ 'c.md': FOOTER_VERIFISERT_INDEP })); + assert.equal(r.files['c.md'].footerConflict, null); + assert.equal(r.aggregate.footerConflicts, 0); +}); + +test('auditHeaders — a placeholder "[Dato]" footer never counts as a conflict', () => { + const r = auditHeaders(['d.md'], reader({ 'd.md': FOOTER_PLACEHOLDER })); + assert.equal(r.files['d.md'].footerConflict, null); + assert.equal(r.aggregate.footerConflicts, 0); +}); + +test('auditHeaders — footer conflict needs BOTH a header date and a norsk footer date', () => { + // V_NONDATE has a norsk "Sist oppdatert" but no English header Last updated → no conflict. + // V_DATE has an English header date but no norsk footer → no conflict. + const r = auditHeaders(['a.md', 'b.md'], reader({ 'a.md': V_NONDATE, 'b.md': V_DATE })); + assert.equal(r.files['a.md'].footerConflict, null); + assert.equal(r.files['b.md'].footerConflict, null); + assert.equal(r.aggregate.footerConflicts, 0); +});