#!/usr/bin/env node // check-cosmo-gate.mjs — the RATIFIED R13 persona gate (operator 2026-09-12, alt. A). // // Replaces the roadmap's `grep -rl "Cosmo" skills/*/references | wc -l -> 0`, which was // measured false twice over (it would delete 451 Azure Cosmos DB occurrences, and it is // unreachable under a heading-only scope because 132 persona occurrences live in prose, // tables and dialogue). Full derivation: lib/cosmo-persona.mjs header. // // THREE CLAUSES // G1 persona occurrences on markdown heading lines in the reference corpus = 0 // G2 every fragment link resolves to a real heading slug, except the anchors on the // committed exemption list (pre-existing, not caused by R13) // G3 product occurrences unchanged from the committed baseline, and each product-only // file still carries zero persona // // USAGE // node scripts/kb-update/check-cosmo-gate.mjs # check (exit 1 on fail) // node scripts/kb-update/check-cosmo-gate.mjs --emit-baseline # write the baseline JSON // node scripts/kb-update/check-cosmo-gate.mjs --validate-net # prove the net both ways // // --validate-net is not decoration. A gate whose null result has never been forced to // the other answer is not a measurement (memory decision-gate-must-be-validated-both- // ways): it injects a known-positive persona heading into a copy of a real corpus file // and asserts G1 FAILS, then asserts the known-negative product-only files PASS. import { readFileSync, writeFileSync, existsSync } from 'node:fs'; import { join, dirname, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; import { globSync } from 'node:fs'; import { classifyCosmo, findDeadAnchors, classifyPersonaSites, neutralizeLabels, } from './lib/cosmo-persona.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const PLUGIN_ROOT = join(__dirname, '..', '..'); const BASELINE_PATH = join(__dirname, 'data', 'cosmo-gate-baseline.json'); // R13b lives in its OWN baseline file. The R13 one records pre-transform reference points // (personaHeadings 401) that regenerating would destroy — never build over a reference-point // artefact, so R13b adds a file rather than re-emitting that one. const LABELS_BASELINE_PATH = join(__dirname, 'data', 'cosmo-labels-baseline.json'); /** Every reference file in the corpus, as repo-relative paths, sorted. */ function refFiles() { return globSync('skills/*/references/**/*.md', { cwd: PLUGIN_ROOT }).sort(); } /** Measure the whole corpus: persona buckets, product total, per-file detail. */ function measure() { const files = refFiles(); const totals = { heading: 0, toc: 0, prose: 0, product: 0 }; const headingFiles = new Set(); const perFile = {}; const deadAnchors = []; for (const rel of files) { const src = readFileSync(join(PLUGIN_ROOT, rel), 'utf8'); const c = classifyCosmo(src); totals.heading += c.persona.heading; totals.toc += c.persona.toc; totals.prose += c.persona.prose; totals.product += c.product; if (c.persona.heading > 0) headingFiles.add(rel); perFile[rel] = { persona: c.personaTotal, product: c.product }; for (const d of findDeadAnchors(src)) deadAnchors.push(`${rel}#${d.anchor}`); } return { files, totals, headingFiles: [...headingFiles].sort(), perFile, deadAnchors }; } /** Files carrying Azure Cosmos DB but no persona at all — must stay untouched. */ function productOnlyFiles(m) { return m.files.filter((f) => m.perFile[f].product > 0 && m.perFile[f].persona === 0); } function emitBaseline() { const m = measure(); const pof = productOnlyFiles(m); const baseline = { _comment: 'Ratified R13 persona-gate baseline. Measured before the heading transform; ' + 'G1 drives personaHeadings to 0, G2 and G3 must not move. Regenerate ONLY with a ' + 'ratified scope change — a silently moved baseline is a gate that proves nothing.', measured: new Date().toISOString().slice(0, 10), refFiles: m.files.length, personaHeadings: m.totals.heading, personaHeadingFiles: m.headingFiles.length, personaTocOccurrences: m.totals.toc, personaProseOccurrences: m.totals.prose, productOccurrences: m.totals.product, productOnlyFiles: pof, productOnlyFileCounts: Object.fromEntries(pof.map((f) => [f, m.perFile[f].product])), deadAnchorsExempt: m.deadAnchors, }; writeFileSync(BASELINE_PATH, `${JSON.stringify(baseline, null, 2)}\n`, 'utf8'); console.log(`baseline skrevet: ${relative(PLUGIN_ROOT, BASELINE_PATH)}`); console.log(JSON.stringify(baseline, null, 2)); } function check() { if (!existsSync(BASELINE_PATH)) { console.error('FEIL: ingen baseline. Kjoer --emit-baseline foer transformen.'); process.exit(2); } const b = JSON.parse(readFileSync(BASELINE_PATH, 'utf8')); const m = measure(); const fails = []; // --- G1: no persona left on any heading line --- if (m.totals.heading !== 0) { fails.push(`G1: ${m.totals.heading} persona-forekomster staar fortsatt paa heading-linjer ` + `i ${m.headingFiles.length} filer (baseline var ${b.personaHeadings})`); } // --- G2: no dead fragment links beyond the committed exemptions --- const exempt = new Set(b.deadAnchorsExempt); const newDead = m.deadAnchors.filter((d) => !exempt.has(d)); if (newDead.length) { fails.push(`G2: ${newDead.length} NYE doede fragmentlenker: ${newDead.slice(0, 10).join(', ')}`); } const healed = b.deadAnchorsExempt.filter((d) => !m.deadAnchors.includes(d)); // --- G3: product untouched --- if (m.totals.product !== b.productOccurrences) { fails.push(`G3: produkt-forekomster ${m.totals.product} != baseline ${b.productOccurrences} ` + `(differanse ${m.totals.product - b.productOccurrences})`); } for (const [f, n] of Object.entries(b.productOnlyFileCounts)) { const now = m.perFile[f]; if (!now) { fails.push(`G3: kun-produkt-fila ${f} finnes ikke lenger`); continue; } if (now.product !== n) fails.push(`G3: ${f} produkt ${now.product} != baseline ${n}`); if (now.persona !== 0) fails.push(`G3: ${f} har faatt ${now.persona} persona-forekomst(er)`); } const labelLines = checkLabels(fails); console.log('=== R13 persona-gate (ratifisert 2026-09-12, alt. A) ==='); console.log(`ref-filer : ${m.files.length} (baseline ${b.refFiles})`); console.log(`G1 persona paa heading-linjer : ${m.totals.heading} (baseline ${b.personaHeadings})`); console.log(` persona i TOC-linjer : ${m.totals.toc} (baseline ${b.personaTocOccurrences})`); console.log(` persona i proesa/tabell : ${m.totals.prose} (baseline ${b.personaProseOccurrences}) ` + '— UTENFOR R13-scope, bokfoert til R13b/R14'); console.log(`G2 doede fragmentlenker : ${m.deadAnchors.length} ` + `(${exempt.size} unntatt, ${newDead.length} nye, ${healed.length} forsvunnet)`); console.log(`G3 produkt-forekomster : ${m.totals.product} (baseline ${b.productOccurrences})`); console.log(` kun-produkt-filer urørt : ${Object.keys(b.productOnlyFileCounts).length}`); console.log('--- R13b etikett/celle-gate (ratifisert 2026-09-15, alt. A) ---'); for (const l of labelLines) console.log(l); if (fails.length) { console.log('\nGATE FAILED'); for (const f of fails) console.log(` x ${f}`); process.exit(1); } console.log('\nGATE PASSED'); } /** Force the gate to the other answer — a null result nobody could falsify is not one. */ function validateNet() { const m = measure(); let bad = 0; // Known-positive: inject a persona heading into a real corpus file's content and // assert the classifier sees it. If this cannot fail the gate, the gate is blind. const victim = m.files[0]; const injected = `${readFileSync(join(PLUGIN_ROOT, victim), 'utf8')}\n## For Cosmo\n`; const ic = classifyCosmo(injected); const base = classifyCosmo(readFileSync(join(PLUGIN_ROOT, victim), 'utf8')); if (ic.persona.heading !== base.persona.heading + 1) { console.log(`x KJENT-POSITIV: injisert "## For Cosmo" ble IKKE sett ` + `(${base.persona.heading} -> ${ic.persona.heading})`); bad++; } else { console.log(`ok KJENT-POSITIV: injisert "## For Cosmo" felte G1 ` + `(${base.persona.heading} -> ${ic.persona.heading}) i ${victim}`); } // Known-positive #2: the Norwegian genitive must also be caught, since it is the // case the letter-s heuristic gets wrong in BOTH directions. const gen = classifyCosmo('### Cosmos tonalitet\n'); if (gen.persona.heading !== 1) { console.log('x KJENT-POSITIV: genitiv "### Cosmos tonalitet" ikke sett'); bad++; } else console.log('ok KJENT-POSITIV: genitiv "### Cosmos tonalitet" felte G1'); // Known-negative: a product heading must NOT trip the gate. const prod = classifyCosmo('### Azure Cosmos DB for GraphRAG\n'); if (prod.personaTotal !== 0 || prod.product !== 1) { console.log(`x KJENT-NEGATIV: produkt-heading feilklassifisert ` + `(persona ${prod.personaTotal}, produkt ${prod.product})`); bad++; } else console.log('ok KJENT-NEGATIV: "### Azure Cosmos DB for GraphRAG" gir 0 persona, 1 produkt'); // Known-negative #2: the real product-only files must be clean of persona. for (const f of productOnlyFiles(m)) { const c = classifyCosmo(readFileSync(join(PLUGIN_ROOT, f), 'utf8')); if (c.personaTotal !== 0) { console.log(`x KJENT-NEGATIV: ${f} klassifisert med persona`); bad++; } else console.log(`ok KJENT-NEGATIV: ${f} — 0 persona, ${c.product} produkt`); } console.log('--- R13b klausulene, begge veier ---'); bad += validateLabelsNet(); console.log(bad === 0 ? '\nNETTET VALIDERT BEGGE VEIER' : `\nNETT-VALIDERING FEILET (${bad})`); process.exit(bad === 0 ? 0 : 1); } // ================================ R13b: the three decomposed clauses ======================== // // R1 REFERENT — has the search string more than one referent? Zero persona in // MECHANICAL bold labels and table cells, while product stays untouched. // R2 REKKEVIDDE — does the operation reach every occurrence it claims? An in-class site // in neither ratified table must be REPORTED, never silently skipped. // R3 HVA SOM STAAR — the 70 editorial occurrences unchanged in count AND content, product // 451 unchanged, R13's headings and TOC still zero (not regraded). // // Each clause carries BOTH a known-positive and a known-negative in --validate-net. A gate // never forced to the other answer is not a measurement. /** Per-corpus measurement of the R13b sites. */ function measureLabels() { const files = refFiles(); const t = { mechLabel: 0, mechCell: 0, edLabel: 0, edCell: 0, prose: 0, product: 0, unmapped: [], mechFiles: [], unreachable: [], }; for (const rel of files) { const src = readFileSync(join(PLUGIN_ROOT, rel), 'utf8'); const s = classifyPersonaSites(src); t.mechLabel += s.label.mechanical; t.mechCell += s.table.mechanical; t.edLabel += s.label.editorial; t.edCell += s.table.editorial; for (const u of s.unmapped) t.unmapped.push(`${rel}: ${u}`); const c = classifyCosmo(src); t.prose += c.persona.prose; t.product += c.product; // R2: a site the tables know but the transform does not actually rewrite is out of reach. if (s.label.mechanical + s.table.mechanical > 0) { t.mechFiles.push(rel); let after; try { after = neutralizeLabels(src, { relPath: rel }).content; } catch (e) { t.unreachable.push(`${rel}: ${e.message}`); continue; } const a = classifyPersonaSites(after); const left = a.label.mechanical + a.table.mechanical; if (left) t.unreachable.push(`${rel}: ${left} site(r) utenfor transformens rekkevidde`); } } return t; } function emitLabelsBaseline() { const t = measureLabels(); const baseline = { _comment: 'Ratified R13b label/cell baseline (operator 2026-09-15, alt. A). Measured BEFORE ' + 'the label transform. R1 drives mechanical -> 0; R3 pins editorial and product as ' + 'immovable. The 85/47 split the dispatching order carried was re-measured to 62/70 — ' + 'the classifier was wrong, not the count. Regenerate ONLY on a ratified scope change.', measured: new Date().toISOString().slice(0, 10), refFiles: refFiles().length, mechanicalLabels: t.mechLabel, mechanicalCells: t.mechCell, mechanicalTotal: t.mechLabel + t.mechCell, mechanicalFiles: t.mechFiles.length, editorialLabels: t.edLabel, editorialCells: t.edCell, editorialTotal: 70, personaProseBefore: t.prose, personaProseAfter: t.prose - (t.mechLabel + t.mechCell), productOccurrences: t.product, mixedLines: [ 'skills/ms-ai-advisor/references/prompt-engineering/reasoning-models-o1-o3-optimization.md:549', ], }; writeFileSync(LABELS_BASELINE_PATH, `${JSON.stringify(baseline, null, 2)}\n`, 'utf8'); console.log(`R13b-baseline skrevet: ${relative(PLUGIN_ROOT, LABELS_BASELINE_PATH)}`); console.log(JSON.stringify(baseline, null, 2)); } /** Evaluate R1-R3, pushing onto the shared fails list; returns the printable lines. */ function checkLabels(fails) { if (!existsSync(LABELS_BASELINE_PATH)) return ['(R13b: ingen baseline — klausulene hoppes over)']; const b = JSON.parse(readFileSync(LABELS_BASELINE_PATH, 'utf8')); const t = measureLabels(); const mech = t.mechLabel + t.mechCell; // --- R1 REFERENT --- if (mech !== 0) { fails.push(`R1: ${mech} mekanisk(e) persona-site(r) staar igjen ` + `(${t.mechLabel} etiketter, ${t.mechCell} celler; baseline ${b.mechanicalTotal})`); } if (t.product !== b.productOccurrences) { fails.push(`R1/R3: produkt ${t.product} != baseline ${b.productOccurrences}`); } // --- R2 REKKEVIDDE --- if (t.unmapped.length) { fails.push(`R2: ${t.unmapped.length} persona-site(r) i INGEN ratifisert tabell — ` + `umaalt variant: ${t.unmapped.slice(0, 5).join(' | ')}`); } if (t.unreachable.length) { fails.push(`R2: ${t.unreachable.length} site(r) utenfor transformens rekkevidde: ` + `${t.unreachable.slice(0, 5).join(' | ')}`); } // --- R3 HVA SOM SKAL STAA --- if (t.edLabel !== b.editorialLabels || t.edCell !== b.editorialCells) { fails.push(`R3: REDAKSJONELT scope roert — etiketter ${t.edLabel} ` + `(baseline ${b.editorialLabels}), celler ${t.edCell} (baseline ${b.editorialCells})`); } if (t.prose !== b.personaProseAfter && t.prose !== b.personaProseBefore) { fails.push(`R3: persona i proesa/tabell ${t.prose} er hverken foer-verdien ` + `${b.personaProseBefore} eller etter-verdien ${b.personaProseAfter}`); } const done = mech === 0; return [ `R1 mekaniske sites (etikett+celle) : ${mech} (baseline ${b.mechanicalTotal} ` + `= ${b.mechanicalLabels}+${b.mechanicalCells} i ${b.mechanicalFiles} filer)`, `R2 umaalte varianter / utenfor rekkevidde : ${t.unmapped.length} / ${t.unreachable.length}`, `R3 redaksjonelt holdt for R14 : ${t.edLabel} etiketter + ${t.edCell} celler ` + `(baseline ${b.editorialLabels} + ${b.editorialCells})`, ` persona i proesa/tabell : ${t.prose} (${b.personaProseBefore} foer R13b, ` + `${b.personaProseAfter} etter)${done ? ' — R13b UTFOERT' : ' — R13b IKKE kjoert'}`, ]; } /** Force each R13b clause to the other answer. */ function validateLabelsNet() { let bad = 0; const ok = (msg) => console.log(`ok ${msg}`); const no = (msg) => { console.log(`x ${msg}`); bad++; }; // R1 known-positive — BOTH forms, because the genitive table variant is precisely what a // bold-only net misses. const victim = refFiles()[0]; const base = readFileSync(join(PLUGIN_ROOT, victim), 'utf8'); const inj = classifyPersonaSites(`${base}\n**For Cosmo:** x\n\n| A | Cosmos råd |\n`); const pre = classifyPersonaSites(base); if (inj.label.mechanical !== pre.label.mechanical + 1) no('R1 KJENT-POSITIV: injisert "**For Cosmo:**" ble IKKE sett'); else ok('R1 KJENT-POSITIV: injisert "**For Cosmo:**" felte R1'); if (inj.table.mechanical !== pre.table.mechanical + 1) no('R1 KJENT-POSITIV: injisert "| Cosmos råd |" (genitiv) ble IKKE sett'); else ok('R1 KJENT-POSITIV: injisert "| Cosmos råd |" (genitiv) felte R1'); // R1 known-negative — the second case is the dangerous one: it LOOKS like a persona label. const prod = classifyPersonaSites('**Cosmos DB-anbefaling:** bruk autoscale\n| Azure Cosmos DB | 8000 NOK |\n'); if (prod.label.mechanical + prod.table.mechanical !== 0 || prod.unmapped.length) { no(`R1 KJENT-NEGATIV: produkt feilklassifisert (${JSON.stringify(prod)})`); } else ok('R1 KJENT-NEGATIV: "**Cosmos DB-anbefaling:**" og "| Azure Cosmos DB |" passerer urort'); // R2 known-positive — an in-class site the tables do not know must be REPORTED. const hole = classifyPersonaSites('**Cosmo-hjørnet:** noe tekst\n'); if (hole.unmapped.length !== 1) no('R2 KJENT-POSITIV: ukjent etikettform ble IKKE rapportert'); else ok('R2 KJENT-POSITIV: ukjent etikettform "**Cosmo-hjørnet:**" rapportert som umaalt'); let threw = false; try { neutralizeLabels('**Cosmo-hjørnet:** noe tekst\n'); } catch { threw = true; } if (!threw) no('R2 KJENT-POSITIV: transformen hoppet STILLE over en ukjent form'); else ok('R2 KJENT-POSITIV: transformen KASTER paa ukjent form, hopper ikke stille over'); // R2 known-negative — the editorial classes must be reported OUT of scope and left alone. const ed = '**Cosmo svarer:**\n| A | B | Cosmo-syntese av verified sources |\n'; const edC = classifyPersonaSites(ed); if (edC.label.editorial !== 1 || edC.table.editorial !== 1) no('R2 KJENT-NEGATIV: redaksjonell form ikke bokfoert som utenfor scope'); else if (neutralizeLabels(ed).content !== ed) no('R2 KJENT-NEGATIV: redaksjonell form ble ENDRET'); else ok('R2 KJENT-NEGATIV: dialog + proveniens bokfoert utenfor scope OG byte-identiske'); // R3 known-positive — break one product occurrence and the product clause must fell. const broken = classifyCosmo('| Azure Cosmo DB | 8000 |\n'); const intact = classifyCosmo('| Azure Cosmos DB | 8000 |\n'); if (!(intact.product === 1 && broken.product === 0)) no(`R3 KJENT-POSITIV: fjernet tegn i "Cosmos DB" felte IKKE produkt-differansen (${intact.product} -> ${broken.product})`); else ok('R3 KJENT-POSITIV: fjernet tegn i "Cosmos DB" felte produkt-differansen (1 -> 0)'); // R3 known-negative — the product-only files must be byte-identical through the transform. for (const f of productOnlyFiles(measure())) { const src = readFileSync(join(PLUGIN_ROOT, f), 'utf8'); if (neutralizeLabels(src, { relPath: f }).content !== src) no(`R3 KJENT-NEGATIV: kun-produkt-fila ${f} ble endret`); else ok(`R3 KJENT-NEGATIV: ${f} byte-identisk gjennom transformen`); } return bad; } const arg = process.argv[2]; if (arg === '--emit-baseline') emitBaseline(); else if (arg === '--emit-labels-baseline') emitLabelsBaseline(); else if (arg === '--validate-net') validateNet(); else check();