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>
386 lines
17 KiB
JavaScript
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`);
|
|
}
|
|
});
|