feat(ms-ai-architect): RX-KB1b — deterministisk footer-dato-avvik-deteksjon + label-whitelist i corpus-audit (flag-to-human, 3 flagg) [skip-docs]
This commit is contained in:
parent
6125f933be
commit
722a131eb7
2 changed files with 119 additions and 0 deletions
|
|
@ -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).
|
// pipe.mjs removes the current 87; this detects any reappearance within the non-advisor scope).
|
||||||
const RE_PLAIN_VERIFIED = /(?:^|[^*])Verified:/m;
|
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. */
|
/** Audit a single file's content. Pure. */
|
||||||
function auditContent(content) {
|
function auditContent(content) {
|
||||||
const head = content.slice(0, HEADER_REGION_BYTES);
|
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 (/\*\*Category:\*\*/i.test(head)) dialect = 'category';
|
||||||
else if (/\*\*Kategori:\*\*/i.test(head)) dialect = 'kategori';
|
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 {
|
return {
|
||||||
title: RE_TITLE.test(content),
|
title: RE_TITLE.test(content),
|
||||||
status: RE_STATUS.test(content),
|
status: RE_STATUS.test(content),
|
||||||
|
|
@ -83,6 +129,7 @@ function auditContent(content) {
|
||||||
dialect,
|
dialect,
|
||||||
verified,
|
verified,
|
||||||
plainVerified: RE_PLAIN_VERIFIED.test(head),
|
plainVerified: RE_PLAIN_VERIFIED.test(head),
|
||||||
|
footerConflict,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,6 +152,7 @@ export function auditHeaders(paths, readFile = (p) => readFileSync(p, 'utf8')) {
|
||||||
verifiedPipe: 0,
|
verifiedPipe: 0,
|
||||||
verifiedDuplicate: 0,
|
verifiedDuplicate: 0,
|
||||||
plainVerifiedPipe: 0,
|
plainVerifiedPipe: 0,
|
||||||
|
footerConflicts: 0,
|
||||||
missingTitle: 0,
|
missingTitle: 0,
|
||||||
missingStatus: 0,
|
missingStatus: 0,
|
||||||
missingEnglishLastUpdated: 0,
|
missingEnglishLastUpdated: 0,
|
||||||
|
|
@ -131,6 +179,7 @@ export function auditHeaders(paths, readFile = (p) => readFileSync(p, 'utf8')) {
|
||||||
if (r.verified.duplicateWithinHeaderRegion) aggregate.verifiedDuplicate++;
|
if (r.verified.duplicateWithinHeaderRegion) aggregate.verifiedDuplicate++;
|
||||||
}
|
}
|
||||||
if (r.plainVerified) aggregate.plainVerifiedPipe++;
|
if (r.plainVerified) aggregate.plainVerifiedPipe++;
|
||||||
|
if (r.footerConflict) aggregate.footerConflicts++;
|
||||||
if (!r.title) aggregate.missingTitle++;
|
if (!r.title) aggregate.missingTitle++;
|
||||||
if (!r.status) aggregate.missingStatus++;
|
if (!r.status) aggregate.missingStatus++;
|
||||||
if (!r.lastUpdatedEnglish) aggregate.missingEnglishLastUpdated++;
|
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 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(` 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(` 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 English Last updated: ${a.missingEnglishLastUpdated}`);
|
||||||
console.log(` Missing Status: ${a.missingStatus}`);
|
console.log(` Missing Status: ${a.missingStatus}`);
|
||||||
console.log(` Missing title: ${a.missingTitle}`);
|
console.log(` Missing title: ${a.missingTitle}`);
|
||||||
|
|
|
||||||
|
|
@ -163,3 +163,68 @@ test('auditHeaders — an unreadable file is reported, not thrown', () => {
|
||||||
assert.equal(r.aggregate.files, 1);
|
assert.equal(r.aggregate.files, 1);
|
||||||
assert.equal(r.aggregate.readErrors, 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);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue