ms-ai-architect/tests/kb-update/test-cosmo-persona.test.mjs
Kjell Tore Guttormsen 3a73eeafdc refactor(ms-ai-architect): R13 del 1 — nøytraliser Cosmo-personaen i ref-korpusets headinger, etter å ha rettet en gate som var målt usann to ganger
Ordre 20260912T193441Z-7358817909. Steg 1 var ikke transformen, men å rette
roadmapens R13-gate og få den ratifisert. Gaten `grep -rl "Cosmo"
skills/*/references -> 0` var usann på to uavhengige måter:

1. Ordren fanget den første: 451 av forekomstene er Azure Cosmos DB, ekte
   produktinnhold. Diskriminatoren er ikke bokstaven «s» — `Cosmos <norsk
   substantiv>` er genitiv av personaen (`### Cosmos tonalitet`), mens
   `Cosmos DB`/`CosmosClient`/`cosmos_ru` er produkt.
2. Denne økten fant den andre: 132 persona-forekomster ligger i prosa,
   tabeller, dialog-replikker og proveniens-linjer. Heading-nøytralisering
   kan ikke nå dem, så «0 persona» er uoppnåelig også under den ratifiserte
   formen. Operatøren ratifiserte alternativ A: gaten speiler formen, og de
   132 bokføres til R13b/R14.

Tre korreksjoner av premisser som sto i ordren og STATE:
  «ca 320 produkt»   -> 451 (case-sensitivt nett manglet 327 lowercase
                        TOC-ankre + 99 identifikatorer; sann nevner 1 638)
  «169 headinger»    -> 401. 169 var `^## For Cosmo`-prefikset (168) og var
                        internt inkonsistent med sin egen topp-variant (204)
  «417 matcher ingen
   populasjon»       -> 417 er cosmo-headinger utenfor kodefences; briefens
                        nevner var reell hele tiden

Fence-bevissthet er målt skadelig, ikke nødvendig: begge toggle-regler er
gale på dette korpuset (naiv toggle skjuler en ekte heading i
chain-of-thought-prompting.md, CommonMark-regelen ubalanserer
service-level-documentation-dr.md). Fence-agnostisk deteksjon finner 401
heading-linjer i nøyaktig de samme 40 variantene som fence-bevisst finner
400 i — ingen kodeblokk-linje er byte-identisk til en persona-heading. Derfor
nøkles transformen på 40 enumererte heading-tekster og ignorerer fences. En
ukjent variant kaster; en slug-kollisjon kaster. Ingenting auto-fikses.

TOC-en regenereres ikke, den rettes kirurgisk: alle 327 persona-lenker hadde
lenketekst lik én av de 40 heading-tekstene og anker lik slugify av den
(327/327, 0 avvik), så heading og TOC-entry skrives i samme operasjon og
ingen mellomtilstand etterlater en død lenke.

Ratifisert målform: `For Cosmo`, `For Cosmo Skyberg` og `For arkitekten
(Cosmo)` konvergerer på `For arkitekten`. To filer kolliderte og er adjudisert
ved å lese dem, ikke ved regel.

Verifisering (alle 7 kriterier fra ordren):
  G1 persona på heading-linjer   401 -> 0
  G2 døde fragmentlenker         1 -> 1 (pre-eksisterende, unntatt)
  G3 produkt-forekomster         451 -> 451; `Cosmos DB|Azure Cosmos` 308 = 308
  de 3 kun-produkt-filene        byte-identiske
  nettet validert begge veier    injisert persona feller G1; genitiv feller G1;
                                 produkt-heading og de 3 filene passerer
  hele diffen                    802 heading-linjer + 654 TOC-linjer, ANNET = 0
  linjeantall                    728 lagt til = 728 slettet
  suite                          1120/1120 (1097 + 23 nye)
  validate-plugin                250 PASS / 0 FAIL
  stikkprøve                     10 filer, alle 5 skills, inkl. de 3 mest
                                 produkt-tunge (26/20/19) — kun heading+TOC

Utenfor scope, urørt: de 4 SKILL.md, de 23 commands, CLAUDE.md, README.md,
NOTICE.md, docs/ (alt R14).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 22:12:28 +02:00

327 lines
14 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);
});