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>
199 lines
11 KiB
JavaScript
199 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 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',
|
|
);
|
|
});
|