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

@ -0,0 +1,199 @@
// test-cosmo-labels.test.mjs — R13b: neutralise the MECHANICAL persona occurrences that
// sit OUTSIDE headings in the reference corpus — bold labels and table cells.
//
// THE SCOPE IS A MEASUREMENT, NOT THE ORDER'S PREMISE. The dispatching order carried
// "MEKANISK 85 / REDAKSJONELT 47", itself a correction of an earlier "87/45". Re-measured
// 2026-09-12 over skills/*/references (389 files), that split is wrong too — and wrong in
// the CLASSIFIER, not in the count. Three findings, all pinned by tests below:
//
// 1. FOURTEEN bold labels are DIALOGUE SPEAKER ATTRIBUTIONS, not labels. The dialogue
// bucket only ever matched the italic form `*Cosmo:*`; the bold form sits in front of
// quoted speech — **Naar kunden sier:** "..." / **Cosmo svarer:** "..." — and
// deleting it leaves the quote with no speaker. Editorial, not mechanical.
// 2. TEN table cells are PROVENANCE CLAIMS in the source-quality column ("Moenstre er
// Cosmo-design", "Raadgivende innhold basert paa Cosmo-persona"). Cell position splits
// the 26 table occurrences into three classes, not one: 12 row labels (column 0),
// 4 column headers, 10 provenance. Rewriting a provenance claim asserts something new
// about where the content came from; one of the ten 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: reasoning-models-o1-o3-optimization.md:549
// `- **For arkitekten (Cosmo):** ...` is exactly the target class, but a line-start
// anchored regex cannot see it behind the `- ` list marker, so it was booked as prose.
//
// MEASURED SPLIT: MEKANISK 62 (46 labels + 16 table cells) / REDAKSJONELT 70. Sum 132
// unchanged. Operator-ratified 2026-09-15: run the 62; dialogue and provenance go to R14's
// editorial pass, because both hang on the never-answered #R14-persona-ramme (neutral
// frame / new named persona / none) while the 62 do not — R13 already executed
// `For Cosmo` -> `For arkitekten` across 401 headings, so this only makes labels and table
// rows consistent with a decision that has ALREADY run.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, globSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
LABEL_MAP,
EDITORIAL_LABELS,
TABLE_CELL_MAP,
EDITORIAL_CELLS,
classifyPersonaSites,
neutralizeLabels,
} from '../../scripts/kb-update/lib/cosmo-persona.mjs';
const PLUGIN_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const refFiles = () => globSync('skills/*/references/**/*.md', { cwd: PLUGIN_ROOT }).sort();
// ------------------------------------------------------------------ the ratified tables
test('LABEL_MAP is the 16 ratified bold-label variants', () => {
assert.equal(Object.keys(LABEL_MAP).length, 16);
assert.equal(LABEL_MAP['For Cosmo:'], 'For arkitekten:');
assert.equal(LABEL_MAP['For Cosmo Skyberg:'], 'For arkitekten:');
assert.equal(LABEL_MAP['For arkitekten (Cosmo):'], 'For arkitekten:');
assert.equal(LABEL_MAP['Cosmo-anbefaling:'], 'Anbefaling:');
assert.equal(LABEL_MAP['Owner (Cosmo):'], 'Owner:');
assert.equal(LABEL_MAP['Til Cosmo:'], 'Til arkitekten:');
assert.equal(LABEL_MAP["Cosmo's rule of thumb:"], 'Rule of thumb:');
});
test('TABLE_CELL_MAP is the 4 ratified cell variants, bold form kept bold', () => {
assert.equal(Object.keys(TABLE_CELL_MAP).length, 4);
assert.equal(TABLE_CELL_MAP['For arkitekten (Cosmo)'], 'For arkitekten');
assert.equal(TABLE_CELL_MAP['**For arkitekten (Cosmo)**'], '**For arkitekten**');
assert.equal(TABLE_CELL_MAP['Cosmo-anbefaling'], 'Anbefaling');
assert.equal(TABLE_CELL_MAP['Cosmos raad'.replace('raad', 'råd')], 'Råd');
});
test('no target invents a word absent from its source (R13 invariant, reused)', () => {
// Identical rule to HEADING_MAP's: dropping words is allowed, inventing one is not,
// except the two substitutions the operator sanctioned for the persona token.
const SANCTIONED = new Set(['arkitekten', 'å']);
const words = (s) => s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
for (const [from, to] of Object.entries({ ...LABEL_MAP, ...TABLE_CELL_MAP })) {
const src = new Set(words(from));
for (const w of words(to)) {
assert.ok(src.has(w) || SANCTIONED.has(w), `target invents "${w}": "${from}" -> "${to}"`);
}
}
});
test('every mapped target is itself persona-free, and the two sets are disjoint', () => {
for (const [from, to] of Object.entries({ ...LABEL_MAP, ...TABLE_CELL_MAP })) {
assert.ok(!/cosmo/i.test(to), `target keeps persona: "${from}" -> "${to}"`);
}
for (const k of EDITORIAL_LABELS) assert.equal(LABEL_MAP[k], undefined, `"${k}" in both sets`);
for (const k of EDITORIAL_CELLS) assert.equal(TABLE_CELL_MAP[k], undefined, `"${k}" in both sets`);
});
test('the editorial sets are the measured dialogue and provenance classes', () => {
assert.equal(EDITORIAL_LABELS.size, 8);
assert.ok(EDITORIAL_LABELS.has('Cosmo svarer:'));
assert.ok(EDITORIAL_LABELS.has('Cosmo:'));
assert.equal(EDITORIAL_CELLS.size, 10);
assert.ok(EDITORIAL_CELLS.has('Rådgivende innhold basert på Cosmo-persona'));
});
// ------------------------------------------------------------------ KLAUSUL 2: reach
test('the corpus carries ZERO mechanical sites, and the editorial ones are all still there', () => {
// Asserts the state AFTER the transform. The pre-transform reference point — 62 mechanical
// in 50 files — lives in cosmo-labels-baseline.json and is read from there rather than
// restated here: a number asserted in two places drifts, and the baseline is the artefact
// the gate compares against. The test below pins that the two agree.
const t = { mechLabel: 0, mechCell: 0, edLabel: 0, edCell: 0, unmapped: [] };
for (const rel of refFiles()) {
const s = classifyPersonaSites(readFileSync(join(PLUGIN_ROOT, rel), 'utf8'));
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}`);
}
assert.deepEqual(t.unmapped, [], 'an unmeasured persona site exists — the net has a hole');
assert.equal(t.mechLabel + t.mechCell, 0, 'R13b is executed: no mechanical site remains');
assert.equal(t.edLabel, 16, 'dialogue (14) + the order\u2019s own 2 sentence forms, held for R14');
assert.equal(t.edCell, 10, 'provenance cells held back for R14');
});
test('the ratified tables and the gate baseline describe the SAME 62 occurrences', () => {
// The baseline is a reference-point artefact: it records what the corpus looked like BEFORE
// the transform, and must never be silently re-emitted. This pins its arithmetic against the
// tables that produced it, so a regenerated-over baseline shows up as a failing test.
const b = JSON.parse(readFileSync(join(PLUGIN_ROOT, 'scripts/kb-update/data/cosmo-labels-baseline.json'), 'utf8'));
assert.equal(b.mechanicalLabels, 46);
assert.equal(b.mechanicalCells, 16);
assert.equal(b.mechanicalTotal, 62);
assert.equal(b.mechanicalFiles, 50);
// The 70 editorial occurrences split 16 labels + 10 cells + 44 that sit at no label or
// cell site at all (plain prose, italic dialogue, provenance footers) — R14's, every one.
assert.equal(b.editorialLabels + b.editorialCells, 26);
assert.equal(b.editorialTotal - b.editorialLabels - b.editorialCells, 44);
assert.equal(b.personaProseBefore - b.mechanicalTotal, b.personaProseAfter);
assert.equal(b.personaProseBefore, 132, 'R13 left 132 persona occurrences outside headings');
assert.equal(b.personaProseAfter, 70, 'R13b leaves 70 for R14');
assert.equal(b.productOccurrences, 451, 'the Azure Cosmos DB mentions R1/R3 protect');
});
test('reach: the list-item label behind a "- " marker is INSIDE scope', () => {
// Finding 3. A line-start-anchored net books this as prose and silently misses it.
const src = '- **For arkitekten (Cosmo):** Baseline (erfaring + Cosmo-persona)\n';
const s = classifyPersonaSites(src);
assert.equal(s.label.mechanical, 1);
const { content } = neutralizeLabels(src);
assert.equal(content, '- **For arkitekten:** Baseline (erfaring + Cosmo-persona)\n');
});
test('reach: a label form the tables do not know is REPORTED, never silently skipped', () => {
// KLAUSUL 2's known-positive: construct an in-class occurrence the transform cannot
// handle and assert it surfaces as unmapped rather than passing through untouched.
const s = classifyPersonaSites('**Cosmo-hjørnet:** noe tekst\n');
assert.deepEqual(s.unmapped, ['label: "Cosmo-hjørnet:"']);
assert.throws(() => neutralizeLabels('**Cosmo-hjørnet:** noe tekst\n'), /ukjent persona-etikett/);
});
// ------------------------------------------------------------------ KLAUSUL 1: referent
test('known-negative: a product label and a product table row must NOT be touched', () => {
// The second is the dangerous one: it LOOKS like a persona label and is product.
const src = '**Cosmos DB-anbefaling:** bruk autoscale\n\n| Azure Cosmos DB | 8000 NOK |\n';
const s = classifyPersonaSites(src);
assert.equal(s.label.mechanical + s.table.mechanical, 0);
assert.deepEqual(s.unmapped, []);
assert.equal(neutralizeLabels(src).content, src, 'product content was rewritten');
});
test('known-positive: both injected forms trip the net, genitive included', () => {
const s = classifyPersonaSites('**For Cosmo:** x\n\n| Trade-off | Cosmos råd |\n');
assert.equal(s.label.mechanical, 1);
assert.equal(s.table.mechanical, 1);
});
// ------------------------------------------------------------------ KLAUSUL 3: what stays
test('neutralizeLabels leaves dialogue and provenance byte-identical', () => {
const src = [
'**Når kunden sier:** "Vi må teste sikkerheten"',
'**Cosmo svarer:**',
'1. Start med PyRIT',
'',
'| Beslutningsveiledning | **Baseline** | Cosmo-syntese av verified sources |',
'',
].join('\n');
assert.equal(neutralizeLabels(src).content, src);
});
test('neutralizeLabels is idempotent and never changes the line count', () => {
const src = '**For Cosmo:** x\n| For arkitekten (Cosmo) | Baseline | y |\n';
const once = neutralizeLabels(src).content;
assert.equal(neutralizeLabels(once).content, once);
assert.equal(once.split('\n').length, src.split('\n').length);
assert.equal(once, '**For arkitekten:** x\n| For arkitekten | Baseline | y |\n');
});
test('a table row rewrites only the mapped cell, leaving its siblings byte-identical', () => {
const src = '| For arkitekten (Cosmo) | **Baseline** | Best practices + erfaring |\n';
assert.equal(
neutralizeLabels(src).content,
'| For arkitekten | **Baseline** | Best practices + erfaring |\n',
);
});