ms-ai-architect/scripts/kb-update/check-cosmo-gate.mjs
Kjell Tore Guttormsen 3a73eeafdc refactor(ms-ai-architect): R13 del 1 — nøytraliser Cosmo-personaen i ref-korpusets headinger, etter å ha rettet en gate som var målt usann to ganger
Ordre 20260912T193441Z-7358817909. Steg 1 var ikke transformen, men å rette
roadmapens R13-gate og få den ratifisert. Gaten `grep -rl "Cosmo"
skills/*/references -> 0` var usann på to uavhengige måter:

1. Ordren fanget den første: 451 av forekomstene er Azure Cosmos DB, ekte
   produktinnhold. Diskriminatoren er ikke bokstaven «s» — `Cosmos <norsk
   substantiv>` er genitiv av personaen (`### Cosmos tonalitet`), mens
   `Cosmos DB`/`CosmosClient`/`cosmos_ru` er produkt.
2. Denne økten fant den andre: 132 persona-forekomster ligger i prosa,
   tabeller, dialog-replikker og proveniens-linjer. Heading-nøytralisering
   kan ikke nå dem, så «0 persona» er uoppnåelig også under den ratifiserte
   formen. Operatøren ratifiserte alternativ A: gaten speiler formen, og de
   132 bokføres til R13b/R14.

Tre korreksjoner av premisser som sto i ordren og STATE:
  «ca 320 produkt»   -> 451 (case-sensitivt nett manglet 327 lowercase
                        TOC-ankre + 99 identifikatorer; sann nevner 1 638)
  «169 headinger»    -> 401. 169 var `^## For Cosmo`-prefikset (168) og var
                        internt inkonsistent med sin egen topp-variant (204)
  «417 matcher ingen
   populasjon»       -> 417 er cosmo-headinger utenfor kodefences; briefens
                        nevner var reell hele tiden

Fence-bevissthet er målt skadelig, ikke nødvendig: begge toggle-regler er
gale på dette korpuset (naiv toggle skjuler en ekte heading i
chain-of-thought-prompting.md, CommonMark-regelen ubalanserer
service-level-documentation-dr.md). Fence-agnostisk deteksjon finner 401
heading-linjer i nøyaktig de samme 40 variantene som fence-bevisst finner
400 i — ingen kodeblokk-linje er byte-identisk til en persona-heading. Derfor
nøkles transformen på 40 enumererte heading-tekster og ignorerer fences. En
ukjent variant kaster; en slug-kollisjon kaster. Ingenting auto-fikses.

TOC-en regenereres ikke, den rettes kirurgisk: alle 327 persona-lenker hadde
lenketekst lik én av de 40 heading-tekstene og anker lik slugify av den
(327/327, 0 avvik), så heading og TOC-entry skrives i samme operasjon og
ingen mellomtilstand etterlater en død lenke.

Ratifisert målform: `For Cosmo`, `For Cosmo Skyberg` og `For arkitekten
(Cosmo)` konvergerer på `For arkitekten`. To filer kolliderte og er adjudisert
ved å lese dem, ikke ved regel.

Verifisering (alle 7 kriterier fra ordren):
  G1 persona på heading-linjer   401 -> 0
  G2 døde fragmentlenker         1 -> 1 (pre-eksisterende, unntatt)
  G3 produkt-forekomster         451 -> 451; `Cosmos DB|Azure Cosmos` 308 = 308
  de 3 kun-produkt-filene        byte-identiske
  nettet validert begge veier    injisert persona feller G1; genitiv feller G1;
                                 produkt-heading og de 3 filene passerer
  hele diffen                    802 heading-linjer + 654 TOC-linjer, ANNET = 0
  linjeantall                    728 lagt til = 728 slettet
  suite                          1120/1120 (1097 + 23 nye)
  validate-plugin                250 PASS / 0 FAIL
  stikkprøve                     10 filer, alle 5 skills, inkl. de 3 mest
                                 produkt-tunge (26/20/19) — kun heading+TOC

Utenfor scope, urørt: de 4 SKILL.md, de 23 commands, CLAUDE.md, README.md,
NOTICE.md, docs/ (alt R14).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 22:12:28 +02:00

188 lines
9.1 KiB
JavaScript

#!/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 } 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');
/** 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)`);
}
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}`);
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(bad === 0 ? '\nNETTET VALIDERT BEGGE VEIER' : `\nNETT-VALIDERING FEILET (${bad})`);
process.exit(bad === 0 ? 0 : 1);
}
const arg = process.argv[2];
if (arg === '--emit-baseline') emitBaseline();
else if (arg === '--validate-net') validateNet();
else check();