ms-ai-architect/scripts/kb-update/neutralize-cosmo-labels.mjs
Kjell Tore Guttormsen 1c9af82daa refactor(ms-ai-architect): R13b — nøytraliser de mekaniske persona-forekomstene utenfor headinger, etter at ordrens egen korrigerte bøttegrense ble målt usann
Ordren ba meg behandle sitt eget premiss som et premiss: «PM-ens 87/45 er korrigert
til 85/47 ved maaling ... Behandle ogsaa 85/47 som et premiss og re-maal det foer du
bygger.» Re-målt over de 389 ref-filene holder 85/47 heller ikke — og feilen ligger i
KLASSIFIKATOREN, ikke i tellingen. Summen 132 står; fordelingen er 62/70.

TRE FUNN, alle pinnet i test:

1. FJORTEN bold-etiketter er DIALOG-ATTRIBUSJONER, ikke etiketter. Dialog-bøtta
   matchet bare kursivformen `*Cosmo:*`. Fetformen står foran sitert tale —
   `**Naar kunden sier:** "..."` / `**Cosmo svarer:** "..."` — så å slette den
   etterlater replikken uten taler. Redaksjonelt, ikke mekanisk.
2. TI tabellceller er PROVENIENSPÅSTANDER i kildekvalitets-kolonnen. Celleposisjon
   deler de 26 tabellforekomstene i tre klasser, ikke én: 12 radetiketter (kolonne 0),
   4 kolonneoverskrifter, 10 proveniensverdier (`Moenstre er Cosmo-design`,
   `Raadgivende innhold basert paa Cosmo-persona`). Å skrive om en av dem er å avgi en
   ny påstand om hvor innholdet kommer fra, og den siste har ikke noe slettemål i det
   hele tatt. Seks av ordrens ni sammensetninger ligger helt inne i denne klassen —
   ordrens egen advarsel traff, målingen lokaliserte den.
3. Lekkasje ANDRE veien, +1 mekanisk: `- **For arkitekten (Cosmo):** ...` i
   reasoning-models-o1-o3-optimization.md:549 er nøyaktig målklassen, men et
   linjestart-forankret nett ser den ikke bak `- ` og bokførte den som prosa.

Operatøren ratifiserte 2026-09-15 alt. A: kjør de 62, hold dialog og proveniens for
R14. Begge tilbakeholdte klasser henger på #R14-persona-ramme som aldri er besvart;
de 62 gjør ikke det, fordi R13 ALT har kjørt `For Cosmo` -> `For arkitekten` over 401
headinger — dette gjør bare etiketter og tabellrader konsistente med en beslutning
som allerede er utført.

UTFØRT: 62 forekomster i 50 filer (ordren sa 47) — 46 etiketter i 16 ratifiserte
varianter, 16 celler i 4. Diffen er 62 fjernet = 62 lagt til, ren in-place-erstatning;
hver endret linje klassifisert, ANNET = 0. Ingen måltekst innfører et ord kilden ikke
hadde, utover R13s sanksjonerte `arkitekten` (samme invariant-test, gjenbrukt).

GATEN, DEKOMPONERT I TRE KLAUSULER, hver validert BEGGE veier før den ble konsumert:
  R1 REFERENT      mekaniske sites 62 -> 0, produkt 451 uendret.
                   Kjent-pos: `**For Cosmo:**` OG `| Cosmos raad |` (genitiven er den
                   et bold-only nett mister) feller begge. Kjent-neg:
                   `**Cosmos DB-anbefaling:**` — ser ut som persona, er produkt —
                   og `| Azure Cosmos DB |` passerer urørt.
  R2 REKKEVIDDE    0 umålte varianter, 0 utenfor rekkevidde. Kjent-pos: en ukjent
                   etikettform rapporteres som umålt OG transformen KASTER, den
                   hopper ikke stille over. Kjent-neg: dialog + proveniens bokføres
                   utenfor scope og er byte-identiske.
  R3 HVA SOM STÅR  16 redaksjonelle etiketter + 10 celler uendret, heading/TOC
                   fortsatt 0 (R13 ikke regradert), prosa 132 -> 70. Kjent-pos: et
                   fjernet tegn i `Cosmos DB` feller produkt-differansen. Kjent-neg:
                   alle kun-produkt-filer byte-identiske gjennom transformen.

Gaten ble kjørt FØR transformen og felte R1 (exit 1) — et nullresultat som aldri er
tvunget til det andre svaret er ingen måling. R13b har egen baseline-fil;
cosmo-gate-baseline.json er et referansepunkt-artefakt (personaHeadings 401) og er
IKKE re-emittert. Ny .gitignore-negasjon (6 entries), aldri `git add -f`.

Målt underveis: 0 av de 62 målinjene ligger i en kodeblokk, så fence-agnostisk
transform er trygg her — samme konklusjon R13 nådde for headinger. Nøyaktig 1 linje
bærer begge klasser; dens `Cosmo-persona` står igjen, som den skal.

Suite 1134/1134 (1120 + 14 nye), validate-plugin 250/0/0, transform idempotent,
R13-driveren fortsatt no-op. RX-OPS1 adversarial-scan kjørt MANUELT på de 56 stagede
filene (OK, exit 0) — `core.hooksPath` skygger repoets pre-commit, fiksen eies av
`.claude`.

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

126 lines
6 KiB
JavaScript

#!/usr/bin/env node
// neutralize-cosmo-labels.mjs — R13b: apply the ratified neutralisation to the MECHANICAL
// persona sites that sit OUTSIDE headings — bold labels and table cells — in one per-file
// operation, behind the same plan-then-write discipline as R13's heading driver.
//
// SCOPE (operator-ratified 2026-09-15, alternative A; measured, not inherited). IN: the 62
// mechanical occurrences — 46 bold labels in 16 ratified variants, 16 table cells in 4.
// OUT, and deliberately so: the 70 editorial ones, which R14 owns. The dispatching order
// carried "MEKANISK 85 / REDAKSJONELT 47" and asked for that premise to be re-measured before
// anything was built; re-measured, it is wrong in the CLASSIFIER, not the count — 14 bold
// labels are dialogue speaker attributions standing in front of quoted speech, 10 table cells
// are provenance claims in the source-quality column, and one list-item label hidden behind a
// `- ` marker leaks the other way INTO scope. Full derivation: lib/cosmo-persona.mjs.
//
// Also out, unchanged from R13: the 4 SKILL.md, the 23 commands, CLAUDE.md / README / NOTICE,
// and docs/ — all R14 — and R13's own headings and TOC, which are green and are not regraded.
//
// INVARIANTS asserted per file BEFORE any write (a throw aborts the whole run and writes
// nothing):
// - zero MECHANICAL label/cell sites remain
// - the editorial counts are untouched — if one moved, the scope has been breached
// - product occurrences unchanged (the 451 Azure Cosmos DB mentions)
// - persona on headings and in TOC entries still zero (R13 not regraded)
// - line count unchanged; no NEW dead fragment link
// - only lines the transform claims to have touched differ from the original
// Idempotent: a re-run matches no key and is a no-op, so an interrupted run is re-runnable.
//
// Usage: node scripts/kb-update/neutralize-cosmo-labels.mjs [--dry]
import { readFileSync, realpathSync, globSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { atomicWriteSync } from './lib/atomic-write.mjs';
import { classifyCosmo, classifyPersonaSites, findDeadAnchors, neutralizeLabels } from './lib/cosmo-persona.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PLUGIN_ROOT = join(__dirname, '..', '..');
/** Throws on the first violation, so the run aborts before a single byte is written. */
function assertInvariant(rel, before, after, changes) {
const sb = classifyPersonaSites(before);
const sa = classifyPersonaSites(after);
const left = sa.label.mechanical + sa.table.mechanical;
if (left !== 0) throw new Error(`${rel}: ${left} mekanisk(e) persona-site(r) staar igjen`);
if (sa.unmapped.length) throw new Error(`${rel}: ukjent persona-site: ${sa.unmapped.join(', ')}`);
if (sa.label.editorial !== sb.label.editorial || sa.table.editorial !== sb.table.editorial) {
throw new Error(`${rel}: REDAKSJONELT scope roert — etiketter `
+ `${sb.label.editorial} -> ${sa.label.editorial}, celler `
+ `${sb.table.editorial} -> ${sa.table.editorial}`);
}
const b = classifyCosmo(before);
const a = classifyCosmo(after);
if (a.product !== b.product) throw new Error(`${rel}: produkt endret ${b.product} -> ${a.product}`);
if (a.persona.heading !== 0 || a.persona.toc !== 0) {
throw new Error(`${rel}: R13 regradert — heading ${a.persona.heading}, toc ${a.persona.toc}`);
}
const lb = before.split('\n');
const la = after.split('\n');
if (lb.length !== la.length) throw new Error(`${rel}: linjeantall endret ${lb.length} -> ${la.length}`);
const deadBefore = new Set(findDeadAnchors(before).map((d) => d.anchor));
const newDead = findDeadAnchors(after).map((d) => d.anchor).filter((x) => !deadBefore.has(x));
if (newDead.length) throw new Error(`${rel}: nye doede ankre: ${newDead.join(', ')}`);
const touched = new Set(changes.map((c) => c.line));
for (let i = 0; i < lb.length; i++) {
if (lb[i] === la[i]) continue;
if (!touched.has(i + 1)) {
throw new Error(`${rel}:${i + 1}: linje endret uten aa staa i endringslista\n`
+ ` foer: ${lb[i]}\n etter: ${la[i]}`);
}
}
}
function main() {
const dry = process.argv.includes('--dry');
const files = globSync('skills/*/references/**/*.md', { cwd: PLUGIN_ROOT }).sort();
const planned = [];
let labelChanges = 0;
let cellChanges = 0;
let heldBack = 0;
for (const rel of files) {
const before = readFileSync(join(PLUGIN_ROOT, rel), 'utf8');
heldBack += (() => { const s = classifyPersonaSites(before); return s.label.editorial + s.table.editorial; })();
const { content: after, changes } = neutralizeLabels(before, { relPath: rel });
if (after === before) continue;
assertInvariant(rel, before, after, changes);
labelChanges += changes.filter((c) => c.kind === 'label').length;
cellChanges += changes.filter((c) => c.kind === 'cell').length;
planned.push({ rel, out: after, changes });
}
console.log(`ref-filer: ${files.length} | aa endre: ${planned.length} | urørt: ${files.length - planned.length}`);
console.log(`etikett-endringer: ${labelChanges} | tabellcelle-endringer: ${cellChanges} `
+ `| SUM ${labelChanges + cellChanges}`);
console.log(`redaksjonelle sites holdt tilbake til R14: ${heldBack}`);
if (dry) {
const byTarget = new Map();
for (const p of planned) {
for (const c of p.changes) {
const k = `${c.kind.padEnd(5)} ${c.from} -> ${c.to}`;
byTarget.set(k, (byTarget.get(k) || 0) + 1);
}
}
console.log('\n(dry run — ingen skriving)\n--- mapping som vil brukes ---');
for (const [k, v] of [...byTarget.entries()].sort((x, y) => y[1] - x[1])) {
console.log(`${String(v).padStart(4)} ${k}`);
}
return;
}
for (const p of planned) atomicWriteSync(join(PLUGIN_ROOT, p.rel), p.out);
console.log(`\nSkrev ${planned.length} filer.`);
}
const isMain = (() => {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
})();
if (isMain) main();