ms-ai-architect/tests/kb-update/test-cosmo-persona.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

386 lines
17 KiB
JavaScript

// test-cosmo-persona.test.mjs — R13 Cosmo del 1: persona-classification + heading
// neutralisation in the reference corpus. The gate this pins is the RATIFIED one
// (operator 2026-09-12, alternative A), NOT the roadmap's `grep -rl "Cosmo" -> 0`,
// which was measured false twice over: it would delete 451 Azure Cosmos DB
// occurrences, and it is unreachable under a heading-only scope because 132 persona
// occurrences live in prose, tables and code blocks that heading neutralisation
// cannot reach.
//
// The three clauses (measured baselines 2026-09-12 over skills/*/references, 389 files):
// G1 persona occurrences on markdown heading lines 401 -> 0 (374 files, 40 variants)
// G2 every fragment link resolves to a real heading 1 dead -> 1 dead
// (agent-framework.md#pattern-3-human-in-the-loop is pre-existing and exempt)
// G3 product occurrences unchanged 451 -> 451
//
// WHY exact-string keying and NOT fence-awareness: measured, both toggle rules are
// wrong on this corpus. A naive ``` toggle leaves chain-of-thought-prompting.md
// unbalanced (19 markers) and hides the real heading at line 403; the CommonMark
// info-string rule instead unbalances service-level-documentation-dr.md (nested
// ```markdown examples). Measured resolution: fence-agnostic detection finds 401
// heading-like persona lines in EXACTLY the same 40 variants as fence-aware
// detection finds 400 — no code-block line is byte-identical to a persona heading.
// So the transform keys on the 40 enumerated heading texts and ignores fences, which
// is both correct and auditable. An unmapped persona heading THROWS: content is never
// auto-rewritten by pattern.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
isPersonaOccurrence,
classifyCosmo,
HEADING_MAP,
FILE_HEADING_OVERRIDES,
neutralizeContent,
slugifyHeading,
findDeadAnchors,
} from '../../scripts/kb-update/lib/cosmo-persona.mjs';
// ---------------------------------------------------------------- classification
test('isPersonaOccurrence — bare Cosmo is persona', () => {
assert.equal(isPersonaOccurrence('Cosmo anbefaler X'), true);
assert.equal(isPersonaOccurrence("Cosmo's Cheat Sheet"), true);
assert.equal(isPersonaOccurrence('Cosmo-tilnærming'), true);
assert.equal(isPersonaOccurrence('Cosmo Skyberg, arkitekt'), true);
});
test('isPersonaOccurrence — lowercase anchor fragment is persona', () => {
// The 327 TOC anchors: `](#for-arkitekten-cosmo)` — persona, and invisible to a
// case-sensitive net. This is why the roadmap gate could not even see them.
assert.equal(isPersonaOccurrence('cosmo)'), true);
assert.equal(isPersonaOccurrence('cosmo-skyberg)'), true);
});
test('isPersonaOccurrence — Azure Cosmos DB and SDK identifiers are product', () => {
assert.equal(isPersonaOccurrence('Cosmos DB for NoSQL'), false);
assert.equal(isPersonaOccurrence('Cosmos-DB"'), false);
assert.equal(isPersonaOccurrence('CosmosClient(connectionString)'), false);
assert.equal(isPersonaOccurrence('CosmosAIGraph'), false);
assert.equal(isPersonaOccurrence('cosmosdb show'), false);
assert.equal(isPersonaOccurrence('cosmos_primary_monthly'), false);
assert.equal(isPersonaOccurrence('Cosmos;'), false); // Microsoft.Azure.Cosmos;
assert.equal(isPersonaOccurrence('cosmos-db/continuous-backup-restore)'), false);
});
test('isPersonaOccurrence — Norwegian genitive "Cosmos <substantiv>" is persona', () => {
// The discriminator is NOT the letter s: 9 measured occurrences are the persona's
// genitive, e.g. `### Cosmos tonalitet`.
assert.equal(isPersonaOccurrence('Cosmos tonalitet'), true);
assert.equal(isPersonaOccurrence('Cosmos veiledningsstrategi'), true);
assert.equal(isPersonaOccurrence('Cosmos råd |'), true);
assert.equal(isPersonaOccurrence('Cosmos anbefalinger'), true);
assert.equal(isPersonaOccurrence('Cosmos erfaringsbaserte anbefalinger)'), true);
});
test('isPersonaOccurrence — a function word after Cosmos keeps it product', () => {
// "Redis for hot, Cosmos for warm data" — product prose, not a genitive.
assert.equal(isPersonaOccurrence('Cosmos for warm data)'), false);
assert.equal(isPersonaOccurrence('Cosmos og Kusto'), false);
assert.equal(isPersonaOccurrence('Cosmos i DR-region'), false);
});
test('classifyCosmo — buckets persona by syntactic location and counts product', () => {
const doc = [
'# Tittel',
'',
'- [For Cosmo](#for-cosmo)', // TOC line: 2 persona (text + anchor)
'',
'## For Cosmo', // heading: 1 persona
'',
'**For Cosmo:** bruk denne.', // prose: 1 persona
'',
'### Azure Cosmos DB for NoSQL', // heading: product only
'',
'Bruk `CosmosClient` mot Cosmos DB.', // prose: 2 product
'',
].join('\n');
const c = classifyCosmo(doc);
assert.equal(c.persona.heading, 1);
assert.equal(c.persona.toc, 2);
assert.equal(c.persona.prose, 1);
assert.equal(c.product, 3); // Azure Cosmos (heading) + CosmosClient + Cosmos DB (prose)
assert.equal(c.personaTotal, 4);
});
// ---------------------------------------------------------------- the mapping table
test('HEADING_MAP covers all 40 measured persona heading variants', () => {
assert.equal(Object.keys(HEADING_MAP).length, 40);
});
test('HEADING_MAP — every neutral target is free of persona', () => {
for (const [from, to] of Object.entries(HEADING_MAP)) {
assert.equal(
classifyCosmo(`## ${to}`).personaTotal, 0,
`target for "${from}" still carries persona: "${to}"`,
);
}
});
test('HEADING_MAP — no target invents a word absent from its source', () => {
// The order forbids new or changed Cosmo content beyond the neutralisation itself.
// Enforced word-by-word: every word of a neutral target must already occur in the
// source heading, EXCEPT the two sanctioned substitutions for the persona token —
// "arkitekten" (the role Cosmo played) and "å" (when a name was the sentence's
// subject, e.g. "Spørsmål Cosmo bør stille" -> "Spørsmål å stille"). Dropping words
// is allowed; introducing any other word is not.
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(HEADING_MAP)) {
const src = new Set(words(from));
for (const w of words(to)) {
assert.ok(
src.has(w) || SANCTIONED.has(w),
`target invents the word "${w}": "${from}" -> "${to}"`,
);
}
}
});
test('FILE_HEADING_OVERRIDES resolves both measured in-file slug collisions', () => {
// Swept over all 389 files: exactly two documents where the ratified mapping would
// make two headings share one slug. One collides with a sibling persona heading, the
// other with the document's own H1 title.
const collision1 = 'skills/ms-ai-engineering/references/mlops-genaiops/feedback-loops-continuous-improvement.md';
const collision2 = 'skills/ms-ai-governance/references/monitoring-observability/application-insights-llm-monitoring.md';
assert.equal(FILE_HEADING_OVERRIDES[collision1]['For Cosmo'], 'Nøkkelpunkter for rådgivning');
assert.equal(
FILE_HEADING_OVERRIDES[collision2]['For Cosmo Skyberg: Application Insights for LLM Monitoring'],
'For arkitekten',
);
assert.equal(Object.keys(FILE_HEADING_OVERRIDES).length, 2);
});
test('FILE_HEADING_OVERRIDES only ever redirects a variant HEADING_MAP already knows', () => {
// An override is a collision remedy, never a back door for an unmeasured heading:
// every key must already be in the shared table, and every target persona-free.
for (const [rel, map] of Object.entries(FILE_HEADING_OVERRIDES)) {
for (const [from, to] of Object.entries(map)) {
assert.ok(HEADING_MAP[from] !== undefined, `${rel}: "${from}" is not a known variant`);
assert.notEqual(to.trim(), '', `${rel}: empty target for "${from}"`);
assert.equal(classifyCosmo(`## ${to}`).personaTotal, 0, `${rel}: target keeps persona: "${to}"`);
}
}
});
// ---------------------------------------------------------------- slug parity
test('slugifyHeading matches transform.mjs slug semantics (æøå survive)', () => {
assert.equal(slugifyHeading('For arkitekten (Cosmo)'), 'for-arkitekten-cosmo');
assert.equal(slugifyHeading('Spørsmål å stille kunden'), 'spørsmål-å-stille-kunden');
assert.equal(slugifyHeading('For Cosmo: Beslutningsveiledning'), 'for-cosmo-beslutningsveiledning');
});
// ---------------------------------------------------------------- the transform
test('neutralizeContent rewrites the heading AND its TOC entry and anchor together', () => {
const before = [
'# Tittel',
'',
'## Innhold',
'',
'- [Introduksjon](#introduksjon)',
'- [For arkitekten (Cosmo)](#for-arkitekten-cosmo)',
'',
'## Introduksjon',
'',
'## For arkitekten (Cosmo)',
'',
'Tekst.',
'',
].join('\n');
const { content, changes } = neutralizeContent(before, { relPath: 'x.md' });
assert.match(content, /^## For arkitekten$/m);
assert.doesNotMatch(content, /Cosmo/i);
assert.match(content, /^- \[For arkitekten\]\(#for-arkitekten\)$/m);
assert.equal(changes.length, 2, 'one heading + one TOC entry');
});
test('neutralizeContent leaves product headings and product prose byte-identical', () => {
const before = [
'# Tittel',
'',
'### Azure Cosmos DB for GraphRAG',
'',
'Bruk `CosmosClient` mot Cosmos DB; se `cosmos_primary_monthly`.',
'',
'### Cosmos DB Autoscale',
'',
].join('\n');
const { content, changes } = neutralizeContent(before, { relPath: 'x.md' });
assert.equal(content, before);
assert.equal(changes.length, 0);
});
test('neutralizeContent preserves the product occurrence count exactly', () => {
const before = [
'## For Cosmo',
'',
'Azure Cosmos DB med `CosmosClient` og cosmosdb-CLI.',
'',
].join('\n');
const productBefore = classifyCosmo(before).product;
const { content } = neutralizeContent(before, { relPath: 'x.md' });
assert.equal(classifyCosmo(content).product, productBefore);
assert.equal(classifyCosmo(content).personaTotal, 0);
});
test('neutralizeContent rewrites a heading that a fence toggle would hide', () => {
// The chain-of-thought-prompting.md case: an unbalanced fence means a naive
// toggle believes line 4 is inside a code block. Exact-string keying must not care.
const before = [
'```python',
'x = 1',
'```python', // stray opener — leaves a naive toggle "inside" forever
'',
'## For arkitekten (Cosmo)',
'',
].join('\n');
const { content } = neutralizeContent(before, { relPath: 'x.md' });
assert.match(content, /^## For arkitekten$/m);
});
test('neutralizeContent is idempotent — a second run changes nothing', () => {
const before = [
'- [For Cosmo](#for-cosmo)',
'',
'## For Cosmo',
'',
].join('\n');
const once = neutralizeContent(before, { relPath: 'x.md' }).content;
const twice = neutralizeContent(once, { relPath: 'x.md' });
assert.equal(twice.content, once);
assert.equal(twice.changes.length, 0);
});
test('neutralizeContent THROWS on an in-file slug collision', () => {
// Two persona headings mapping to the same neutral text would produce a duplicate
// heading and an ambiguous anchor. Fail loud; never write blind.
const before = [
'## For arkitekten (Cosmo)',
'',
'## For Cosmo',
'',
].join('\n');
assert.throws(
() => neutralizeContent(before, { relPath: 'x.md' }),
/kollisjon/i,
);
});
test('neutralizeContent THROWS on an unmapped persona heading', () => {
// Content is never auto-rewritten by pattern: an unseen variant is an operator
// decision, not a silent guess.
const before = ['## For Cosmo: en helt ny variant ingen har målt', ''].join('\n');
assert.throws(
() => neutralizeContent(before, { relPath: 'x.md' }),
/ukjent persona-heading/i,
);
});
test('neutralizeContent does not touch persona in prose, tables or dialogue', () => {
// Explicitly OUT of the ratified R13 scope — the 132 residual occurrences booked
// for R13b/R14. A transform that reached them would exceed its mandate.
const before = [
'**For Cosmo:** bruk denne modellen.',
'',
'| For arkitekten (Cosmo) | Baseline |',
'',
'*Cosmo:* "La oss starte med risikoprofilen."',
'',
].join('\n');
const { content, changes } = neutralizeContent(before, { relPath: 'x.md' });
assert.equal(content, before);
assert.equal(changes.length, 0);
});
// ---------------------------------------------------------------- G2: dead anchors
test('findDeadAnchors reports a fragment link with no matching heading', () => {
const doc = [
'- [Finnes](#finnes)',
'- [Finnes ikke](#finnes-ikke)',
'',
'## Finnes',
'',
].join('\n');
const dead = findDeadAnchors(doc);
assert.equal(dead.length, 1);
assert.equal(dead[0].anchor, 'finnes-ikke');
});
test('findDeadAnchors returns empty when every anchor resolves', () => {
const doc = ['- [Spørsmål å stille](#spørsmål-å-stille)', '', '## Spørsmål å stille', ''].join('\n');
assert.deepEqual(findDeadAnchors(doc), []);
});
test('a neutralized document has no dead anchors', () => {
// The whole point of rewriting heading and TOC entry in one operation.
const before = [
'- [For Cosmo: Quick Reference Card](#for-cosmo-quick-reference-card)',
'',
'## For Cosmo: Quick Reference Card',
'',
].join('\n');
const { content } = neutralizeContent(before, { relPath: 'x.md' });
assert.deepEqual(findDeadAnchors(content), []);
assert.match(content, /^- \[Quick Reference Card\]\(#quick-reference-card\)$/m);
});
// ================================== R14: the delivery-surface clause ======================
//
// G4 persona occurrences in the DELIVERY SURFACE = 0.
//
// Delivery surface = tracked `*.md`, minus the reference corpus (G1/G3 already own it) and
// minus the derivation register — CHANGELOG rows, docs records and the measurement document
// itself, all TRUE statements about the past that CLAUDE.md forbids rewriting.
//
// The register is pinned on PER-FILE OCCURRENCE COUNTS, never on filenames. Measured: inject
// one new persona line into an exempt file and the count moves, so a number-pinned exemption
// FELLS it while a filename-pinned one is blind to it. That is the difference between an
// exemption and a hole.
//
// The one adjudicated product line: `docs/ref-kb-gold-reconciliation-2026-06.md:93` reads
// "Cosmos multi-region writes (active-active)", which the R13 Norwegian-genitive rule scores
// as persona because the enumerated PRODUCT_FOLLOWERS list was built over a corpus with no
// English product prose. The verdict was settled by the `/azure/cosmos-db/` URL in the same
// table row, so G4 applies THAT discriminator rather than exempting the file. Measured over
// all tracked `*.md`: 8 `Cosmos <lowercase>` lines exist in total and 33 lines carry a
// cosmos-db URL — of those, 2 classify as persona and BOTH are genuinely the product, so the
// rule is closed, not open-ended. The classifier itself is untouched: G1/G3/R1/R3 baselines
// rest on it.
import { personaInDeliveryContent, DERIVATION_REGISTER } from '../../scripts/kb-update/lib/cosmo-persona.mjs';
test('G4 known-negative — an Azure Cosmos DB product line carries no delivery persona', () => {
assert.equal(personaInDeliveryContent('| Azure Cosmos DB | 8000 NOK |\n'), 0);
assert.equal(personaInDeliveryContent('Bruk `CosmosClient` mot cosmos_ru-kvoten.\n'), 0);
// The adjudicated line: genitive-shaped, but the row's own URL settles it as product.
assert.equal(
personaInDeliveryContent(
'| Cosmos multi-region writes (active-active) | learn.microsoft.com/azure/cosmos-db/distribute-data-globally |\n',
),
0,
);
});
test('G4 known-positive — persona in the delivery surface is seen, in every form it takes', () => {
// Prose, the form 62 of the 62 measured occurrences actually took.
assert.equal(personaInDeliveryContent('Du er Cosmo Skyberg i en anskaffelsesrolle.\n'), 1);
// Norwegian genitive — the case the letter-s heuristic gets wrong in BOTH directions.
assert.equal(personaInDeliveryContent('Legg til Cosmos helhetsvurdering:\n'), 1);
// A heading, and a persona line that ALSO carries a cosmos-db URL: the URL rule must not
// become a laundering channel for a genuine persona mention.
assert.equal(personaInDeliveryContent('# Cosmo Skyberg - Microsoft AI Solution Architect\n'), 1);
assert.equal(
personaInDeliveryContent('Cosmo anbefaler learn.microsoft.com/azure/cosmos-db/ som kilde.\n'),
1,
);
});
test('G4 register is pinned on counts, so a new persona line in an exempt file still fells it', () => {
// The exemption must be a number, not a filename. A filename-pinned exemption is blind to
// what the file later comes to contain; this asserts the blindness is absent.
assert.ok(DERIVATION_REGISTER['CHANGELOG.md'] > 0, 'CHANGELOG is registered with a count');
for (const [file, count] of Object.entries(DERIVATION_REGISTER)) {
assert.equal(typeof count, 'number', `${file} is pinned on a count, not a filename`);
}
});