ms-ai-architect/tests/kb-update/test-cosmo-labels.test.mjs
Kjell Tore Guttormsen ced0c5f46d refactor(ms-ai-architect): R14 — persona ut av plugin-flaten, og de seks stedene som produserte den
Nøytral arkitekt-ramme (ratifisert 15.09) i SKILL.md-ene, 23 kommandoer, 70 redaksjonelle
forekomster i ref-korpuset, README/CLAUDE.md/NOTICE og A11Y-rapportens proveniens-linje.
Hver av de 70 avgjort i kontekst: dialog fikk ny taler, proveniens ny opphavspåstand.

Ordren navnga fem produsent-steder; den live sjette manglet. `PROMPT_TEMPLATE` settes i
generate-skills.sh:27 og leses ALDRI — prompten er en heredoc i samme script. Å rette bare
prompt-template.md ville latt generatoren re-minte personaen ved neste KB-kjøring.

Ny G4-klausul i check-cosmo-gate.mjs: persona i leveranseflaten = 0, derivasjonsregisteret
pinnet per fil PÅ TALL (et filnavn-unntak er blindt for hva fila senere inneholder).
Registeret utvidet med README.md 3 — versjonstabellens rader er historikk, ikke leveranse.
Den ene genitiv-formede produktlinja adjudiseres på cosmos-db-URL-en i samme rad; regelen
er målt lukket (8 tvetydige linjer totalt, 33 URL-linjer, 2 persona-klassifiserte, begge
produkt) og hvitvasker ikke bar `Cosmo`. classifyCosmo urørt — baselinene hviler på den.
R3 godtar nå R14-utført (0) men ingenting imellom: et halvveis sveip felles fortsatt.

Bevis: gate PASSED + NETTET VALIDERT BEGGE VEIER · 1137/1137 · validate-plugin 250/0/0 ·
begge --dry-drivere 0 · Layer B 45/45 OK (kjent-positiv feller, exit 1).

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

203 lines
11 KiB
JavaScript

// 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 R14 has emptied the editorial ones', () => {
// Asserts the state AFTER the transform. The pre-transform reference points — 62 mechanical
// in 50 files, 16 editorial labels + 10 cells — live in cosmo-labels-baseline.json and are
// 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.
//
// R13b held the editorial sites at 16+10 so that R14 could adjudicate each in context. R14
// (2026-09-15) did exactly that, so 0 is now the correct state, not a breach. What must NOT
// happen is a value in between: a half-swept corpus is the failure this still catches.
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, 0, 'R14 is executed: the 16 dialogue/sentence labels are neutralised');
assert.equal(t.edCell, 0, 'R14 is executed: the 10 provenance cells are neutralised');
});
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',
);
});