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>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-15 21:09:00 +02:00
commit 1c9af82daa
56 changed files with 793 additions and 63 deletions

View file

@ -26,11 +26,17 @@ 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';
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() {
@ -121,6 +127,8 @@ function check() {
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})`);
@ -131,6 +139,8 @@ function check() {
+ `(${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}`);
@ -178,11 +188,190 @@ function validateNet() {
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();

View file

@ -0,0 +1,18 @@
{
"_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": "2026-09-15",
"refFiles": 389,
"mechanicalLabels": 46,
"mechanicalCells": 16,
"mechanicalTotal": 62,
"mechanicalFiles": 50,
"editorialLabels": 16,
"editorialCells": 10,
"editorialTotal": 70,
"personaProseBefore": 132,
"personaProseAfter": 70,
"productOccurrences": 451,
"mixedLines": [
"skills/ms-ai-advisor/references/prompt-engineering/reasoning-models-o1-o3-optimization.md:549"
]
}

View file

@ -322,3 +322,200 @@ export function neutralizeContent(content, { relPath = '' } = {}) {
return { content: out.join('\n'), changes };
}
// ===========================================================================================
// R13b — the MECHANICAL persona sites OUTSIDE headings: bold labels and table cells.
//
// THE SPLIT IS A MEASUREMENT, NOT THE ORDER'S PREMISE. The dispatching order carried
// "MEKANISK 85 / REDAKSJONELT 47" — itself a correction of an earlier "87/45" — and asked for
// it to be re-measured before anything was built. Re-measured over the 389 reference files, it
// is wrong again, and wrong in the CLASSIFIER rather than the count:
//
// 1. FOURTEEN bold labels are DIALOGUE SPEAKER ATTRIBUTIONS. The dialogue bucket only ever
// matched the italic `*Cosmo:*`; the bold form introduces quoted speech —
// `**Naar kunden sier:** "..."` / `**Cosmo svarer:** "..."` — so deleting it leaves the
// quote with no speaker. That is editorial work, not a scripted rewrite.
// 2. TEN table cells are PROVENANCE CLAIMS in the source-quality column. Cell position splits
// the 26 table occurrences into three classes, not one: 12 row labels (column 0),
// 4 column headers, 10 provenance values such as `Moenstre er Cosmo-design` and
// `Raadgivende innhold basert paa Cosmo-persona`. Rewriting one asserts something new
// about where the content came from, and the last of them has no deletion target at all.
// Six of the order's nine compound forms live entirely inside this class.
// 3. Leaking the other way, +1 mechanical: `- **For arkitekten (Cosmo):** ...` in
// reasoning-models-o1-o3-optimization.md:549 is exactly the target class, but a
// line-start-anchored net cannot see it behind the `- ` marker and books it as prose.
//
// MEASURED: MEKANISK 62 (46 labels + 16 cells) / REDAKSJONELT 70; sum 132 unchanged.
// Operator-ratified 2026-09-15, alternative A: execute the 62, hold dialogue and provenance
// for R14. Both held-back classes depend on the never-answered #R14-persona-ramme (neutral
// frame / new named persona / none); the 62 do not, because R13 has ALREADY executed
// `For Cosmo` -> `For arkitekten` across 401 headings, so this only brings labels and table
// rows into line with a decision that has already run.
//
// KEYED ON EXACT TEXT, like HEADING_MAP, and for the same reason: every rewrite is then a
// table entry a human ratified, and an unmeasured variant THROWS instead of being guessed at.
// Exact keying is also what keeps the two table classes apart without any row-role heuristic —
// the header cell is `Cosmo-anbefaling`, the provenance cell is
// `Cosmo-anbefaling basert paa verified data`, and they simply are different keys.
/**
* The ratified bold-label rewrites: 16 variants, 46 occurrences. Keys are the text INSIDE the
* `**...**` span, colon included where the source has it inside the bold.
* No target introduces a word its source did not carry, except R13's sanctioned `arkitekten`.
*/
export const LABEL_MAP = {
// --- the bulk: the three forms that converge on R13's ratified heading vocabulary ---
'For Cosmo:': 'For arkitekten:',
'For Cosmo Skyberg:': 'For arkitekten:',
'For arkitekten (Cosmo):': 'For arkitekten:',
// --- compound nouns: the persona is the first element, so the head stands alone ---
'Cosmo-anbefaling:': 'Anbefaling:',
"Cosmo's anbefaling:": 'Anbefaling:',
'Cosmo Skybergs anbefaling:': 'Anbefaling:',
'Cosmo-svar:': 'Svar:',
'Cosmo-spørsmål:': 'Spørsmål:',
'Cosmo-checkpoint:': 'Checkpoint:',
"Cosmo's one-liner:": 'One-liner:',
"Cosmo's rule of thumb:": 'Rule of thumb:',
// --- parenthetical or prepositional: the name drops out and the label survives intact ---
'Owner (Cosmo):': 'Owner:',
'Note til Cosmo:': 'Note:',
'Oppsummering for Cosmo': 'Oppsummering',
'Oppsummering for Cosmo:': 'Oppsummering:',
// --- addressed TO the architect, so the role takes the name's place ---
'Til Cosmo:': 'Til arkitekten:',
};
/**
* Bold labels that carry persona but are OUT of R13b's scope: speaker attributions in front of
* quoted speech. Enumerated rather than pattern-matched, so that a variant in NEITHER table is
* a hole in the net and throws, instead of being silently skipped. 6 variants, 14 occurrences.
*/
export const EDITORIAL_LABELS = new Set([
'Cosmo:', // 5 — `**Cosmo:** "Jeg forstaar at dere er klare for produksjon..."`
'Cosmo svarer:', // 4 — paired with `**Naar kunden sier:**`
'Cosmo forklarer:', // 2 — same dialogue frame
'Cosmo spør:', // 1 — same dialogue frame
'Mitt svar (Cosmo):', // 1 — first person, inside a scripted exchange
'Cosmo Skyberg tipset:', // 1 — past-tense attribution of advice to a person
// The two the ORDER already booked as editorial, and the reason its predecessor undercounted
// provenance: both are whole sentences that merely happen to be bold, and the first also
// points at `/architect`, an R14 delivery surface. Listed here so the net has no hole.
'For spørsmål om NLU-konfigurasjon, kontakt Cosmo Skyberg via `/architect`.',
'Sist oppdatert av Cosmo Skyberg, Microsoft AI Solution Architect',
]);
/** The ratified table-cell rewrites: 4 variants, 16 occurrences (12 row labels, 4 headers). */
export const TABLE_CELL_MAP = {
'For arkitekten (Cosmo)': 'For arkitekten',
'**For arkitekten (Cosmo)**': '**For arkitekten**',
'Cosmo-anbefaling': 'Anbefaling',
'Cosmos råd': 'Råd',
};
/** Provenance cells held back for R14's editorial pass. 10 occurrences, measured. */
export const EDITORIAL_CELLS = new Set([
'Cosmo-syntese av verified sources',
'Cosmo-anbefaling basert på verified data',
'Cosmo-anbefalinger basert på best practices',
"Cosmo's domain expertise + maturity model",
"Cosmo's domain knowledge + norsk lovverk",
"Cosmo's expertise + best practices fra docs",
'Rådgivende innhold basert på Cosmo-persona',
'Mønstre er Cosmo-design, PowerShell-kode er Verified',
'Tabeller og anbefalinger basert på Cosmo-erfaring + Microsoft docs',
'GDPR/AI Act-mapping er Cosmo-tolkninger (ikke Microsoft-spesifikk)',
]);
/** A bold label at the start of a line, optionally behind a list marker. */
const LABEL_RE = /^(\s*(?:[-*+]\s+)?)\*\*([^*\n]+)\*\*/;
/** A markdown table row. */
const TABLE_RE = /^\s*\|/;
/**
* Where do this document's non-heading persona occurrences sit, and is each one in a ratified
* table? Counts only the SITES R13b acts on bold labels and table cells; prose, dialogue
* outside bold labels and provenance lines are R14's and are not counted here.
*
* `unmapped` is the fail-safe: a persona label or cell in neither the mechanical nor the
* editorial table is an unmeasured variant, and the driver refuses to run rather than guess.
*
* @param {string} content
* @returns {{label: {mechanical: number, editorial: number},
* table: {mechanical: number, editorial: number}, unmapped: string[]}}
*/
export function classifyPersonaSites(content) {
const label = { mechanical: 0, editorial: 0 };
const table = { mechanical: 0, editorial: 0 };
const unmapped = [];
for (const line of String(content ?? '').split('\n')) {
if (/^#{1,6}\s+/.test(line)) continue;
if (TABLE_RE.test(line)) {
const segs = line.split('|');
for (let i = 1; i < segs.length - 1; i++) {
const cell = segs[i].trim();
if (!hasPersona(cell)) continue;
if (TABLE_CELL_MAP[cell] !== undefined) table.mechanical++;
else if (EDITORIAL_CELLS.has(cell)) table.editorial++;
else unmapped.push(`cell: ${JSON.stringify(cell)}`);
}
continue;
}
const m = LABEL_RE.exec(line);
if (!m || !hasPersona(m[2])) continue;
if (LABEL_MAP[m[2]] !== undefined) label.mechanical++;
else if (EDITORIAL_LABELS.has(m[2])) label.editorial++;
else unmapped.push(`label: ${JSON.stringify(m[2])}`);
}
return { label, table, unmapped };
}
/**
* Neutralise the ratified bold labels and table cells. Pure: returns new content, writes
* nothing. Surgical only the label span or the single matching cell is rewritten, so the
* rest of the line, including any persona R14 still owns, stays byte-identical. Splitting and
* rejoining a row on `|` is lossless, so cell padding survives untouched.
*
* Throws never writes a guess on a persona label or cell in neither ratified table.
* Idempotent: a neutral target matches no key.
*
* @param {string} content
* @param {{relPath?: string}} [opts]
* @returns {{content: string, changes: Array<{kind: string, line: number, from: string, to: string}>}}
*/
export function neutralizeLabels(content, { relPath = '' } = {}) {
const where = (i) => `${relPath || '<ukjent fil>'}:${i + 1}`;
const changes = [];
const out = String(content ?? '').split('\n').map((line, i) => {
if (/^#{1,6}\s+/.test(line)) return line;
if (TABLE_RE.test(line)) {
const segs = line.split('|');
let touched = false;
for (let c = 1; c < segs.length - 1; c++) {
const cell = segs[c].trim();
if (!hasPersona(cell)) continue;
const to = TABLE_CELL_MAP[cell];
if (to === undefined) {
if (EDITORIAL_CELLS.has(cell)) continue;
throw new Error(`ukjent persona-tabellcelle i ${where(i)}: ${JSON.stringify(cell)}`);
}
segs[c] = segs[c].replace(cell, to);
changes.push({ kind: 'cell', line: i + 1, from: cell, to });
touched = true;
}
return touched ? segs.join('|') : line;
}
const m = LABEL_RE.exec(line);
if (!m || !hasPersona(m[2])) return line;
const to = LABEL_MAP[m[2]];
if (to === undefined) {
if (EDITORIAL_LABELS.has(m[2])) return line;
throw new Error(`ukjent persona-etikett i ${where(i)}: ${JSON.stringify(m[2])}`);
}
changes.push({ kind: 'label', line: i + 1, from: m[2], to });
return `${m[1]}**${to}**${line.slice(m[0].length)}`;
});
return { content: out.join('\n'), changes };
}

View file

@ -0,0 +1,126 @@
#!/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();