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>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-12 22:12:28 +02:00
commit 3a73eeafdc
380 changed files with 1711 additions and 729 deletions

View file

@ -0,0 +1,324 @@
// cosmo-persona.mjs — R13 Cosmo del 1: classify a `cosmo` occurrence as PERSONA or
// PRODUCT, and neutralise the persona out of reference-corpus headings (and the TOC
// entries that anchor to them) without touching a single Azure Cosmos DB mention.
//
// WHY THIS LIB EXISTS. The roadmap's R13 acceptance gate
// (.claude/projects/2026-07-02-helhetlig-roadmap § R13) reads
// `grep -rl "Cosmo" skills/*/references | wc -l -> 0`. Measured 2026-09-12 over the
// 389 reference files, that gate is false twice over:
//
// 1. Of 1 638 case-insensitive `cosmo` occurrences, 451 are AZURE COSMOS DB — a real
// Microsoft product, legitimate content in the BCDR / engineering / cost files.
// A gate demanding zero `Cosmo` demands they be deleted. The discriminator is NOT
// the letter s: `Cosmos DB` / `CosmosClient` / `cosmos_ru` are product, while
// `Cosmos <norsk substantiv>` is the persona's GENITIVE (`### Cosmos tonalitet`).
// 2. Of the 1 187 persona occurrences, 132 live in prose, tables, dialogue and
// provenance lines that heading neutralisation cannot reach. "Zero persona" is
// therefore unreachable under the ratified heading-only scope either.
//
// THE RATIFIED GATE (operator 2026-09-12, alternative A) — three clauses, baselines
// measured over skills/*/references:
// G1 persona occurrences on markdown heading lines 401 -> 0 (374 files, 40 variants)
// G2 every fragment link resolves to a real heading unchanged (1 pre-existing dead)
// G3 product occurrences unchanged 451 -> 451
// The 132 residual persona occurrences are booked as an open decision for R13b/R14.
//
// WHY EXACT-STRING KEYING AND NOT FENCE-AWARENESS. Measured: BOTH fence rules are
// wrong on this corpus. A naive ``` toggle (what transform.mjs buildToc uses) leaves
// chain-of-thought-prompting.md unbalanced — 19 markers — and hides the real heading at
// line 403; the CommonMark "a closing fence carries no info string" rule instead
// unbalances service-level-documentation-dr.md, whose ```markdown blocks contain
// ```mermaid / ```bash examples as literal content. Measured resolution: fence-agnostic
// detection finds 401 heading-like persona lines in EXACTLY the same 40 variants that
// fence-aware detection finds 400 in — i.e. no code-block line in the corpus is
// byte-identical to a persona heading, so fence state cannot change the verdict. The
// transform therefore keys on the 40 enumerated heading texts and ignores fences. That
// is both correct here and auditable: every rewrite is a table entry a human ratified.
//
// ARCHITECTURE INVARIANT: this lib NEVER writes and never guesses. An unmapped persona
// heading throws; an in-file slug collision throws. Applying content is the driver's
// job (neutralize-cosmo-headings.mjs), behind the operator gate.
// Function words that may follow a bare product mention of "Cosmos" in prose, e.g.
// "Redis for hot, Cosmos for warm data" / "autoscale for Cosmos i DR-region". A
// genitive persona use is always "Cosmos <substantiv>", never "Cosmos <funksjonsord>".
// Enumerated rather than inferred: the net is validated by running it over all 1 638
// occurrences and hand-checking every bare-Cosmos line (25 of them).
const PRODUCT_FOLLOWERS = [
'for', 'og', 'eller', 'i', 'med', 'til', 'som', 'er', 'ellers', 'via',
'på', 'pa', 'av', 'fra', 'ved', 'kan', 'skal', 'hvis', 'men', 'the', 'to',
];
/**
* Is the `cosmo` occurrence at the START of `text` the persona (not the product)?
*
* PERSONA /cosmo(?!s)/i 1 178 measured (851 `Cosmo`,
* 327 lowercase TOC anchors)
* `Cosmos <lowercase substantiv>` 9 measured (Norwegian genitive)
* PRODUCT everything else 451 measured
*
* @param {string} text the document text from the occurrence's first character on
* @returns {boolean}
*/
export function isPersonaOccurrence(text) {
const s = String(text ?? '');
// `Cosmo`, `Cosmo's`, `Cosmo-tilnærming`, and the lowercase `#...-cosmo)` anchors.
if (/^cosmo(?!s)/i.test(s)) return true;
// Norwegian genitive: capital `Cosmos` + space + a lowercase noun. A capital follower
// (DB, PITR, L2, Kusto, Snowflake) or no space at all (`Cosmos;`, `Cosmos-DB`,
// `CosmosClient`) is product, as is a lowercase function word.
const gen = /^Cosmos[ \t]+(\S+)/.exec(s);
if (!gen) return false;
const follower = gen[1];
if (!/^[a-zæøå]/.test(follower)) return false;
const word = follower.replace(/[^\p{L}]/gu, '').toLowerCase();
return word !== '' && !PRODUCT_FOLLOWERS.includes(word);
}
/** Does this line carry at least one persona occurrence? */
function hasPersona(line) {
for (const m of String(line).matchAll(/cosmo/gi)) {
if (isPersonaOccurrence(String(line).slice(m.index))) return true;
}
return false;
}
/**
* GitHub-style heading slug. MIRRORS transform.mjs slugify (lowercase, drop all
* punctuation, spaces hyphens, Unicode-aware so æ/ø/å survive) kept local rather
* than imported so this lib stays dependency-free, and pinned to parity by
* test-cosmo-persona's slug cases against known corpus anchors.
* @param {string} heading
* @returns {string}
*/
export function slugifyHeading(heading) {
return String(heading ?? '')
.toLowerCase()
.replace(/[^\p{L}\p{N}\s_-]/gu, '')
.trim()
.replace(/\s/g, '-');
}
/**
* Count every `cosmo` occurrence, bucketing the persona ones by where they sit.
* The buckets are what make the ratified scope legible: `heading` + `toc` is what R13
* neutralises, `prose` is the 132 booked for R13b/R14, `product` is what G3 protects.
*
* @param {string} content
* @returns {{persona: {heading: number, toc: number, prose: number},
* personaTotal: number, product: number, total: number}}
*/
export function classifyCosmo(content) {
const persona = { heading: 0, toc: 0, prose: 0 };
let product = 0;
for (const line of String(content ?? '').split('\n')) {
const isHeading = /^#{1,6}\s+/.test(line);
// Spans covered by a markdown fragment link `[text](#anchor)` — both the visible
// link text and the anchor sit inside one span, which is why a TOC entry for a
// persona heading contributes two occurrences.
const linkSpans = [...line.matchAll(/\[[^\]]*\]\(#[^)]*\)/g)]
.map((m) => [m.index, m.index + m[0].length]);
for (const m of line.matchAll(/cosmo/gi)) {
if (!isPersonaOccurrence(line.slice(m.index))) { product++; continue; }
if (isHeading) persona.heading++;
else if (linkSpans.some(([a, b]) => m.index >= a && m.index < b)) persona.toc++;
else persona.prose++;
}
}
const personaTotal = persona.heading + persona.toc + persona.prose;
return { persona, personaTotal, product, total: personaTotal + product };
}
/**
* The ratified rewrite table: all 40 persona heading variants measured in
* the reference corpus on 2026-09-12, keyed by heading text (the line without its `#`
* prefix) neutral text. Operator-ratified 2026-09-12: `For Cosmo`,
* `For Cosmo Skyberg` and `For arkitekten (Cosmo)` all converge on `For arkitekten`,
* giving the corpus ONE heading vocabulary where it had three.
*
* No target introduces a word its source did not carry, except the two sanctioned
* substitutions for the persona token `arkitekten` (the role Cosmo played) and `å`
* (where the name was the clause's subject). Pinned by test-cosmo-persona.
*/
export const HEADING_MAP = {
// --- the four bulk variants (363 of 401 occurrences) ---
'For arkitekten (Cosmo)': 'For arkitekten',
'For Cosmo': 'For arkitekten',
'For Cosmo Skyberg': 'For arkitekten',
'For Cosmo: Beslutningsveiledning': 'Beslutningsveiledning',
// --- the tail: 36 variants, 1-2 occurrences each ---
'Oppsummering for Cosmo': 'Oppsummering',
'10. For Cosmo: Modellvalgveiledning': '10. Modellvalgveiledning',
'Key insights for Cosmo': 'Key insights',
'Architecture decision prompts for Cosmo': 'Architecture decision prompts',
'Røde flagg (når skal Cosmo advare?)': 'Røde flagg (når skal arkitekten advare?)',
'Fallgruver (Cosmo Har Sett Før)': 'Fallgruver',
"Desicion Matrix (Cosmo's Cheat Sheet)": 'Desicion Matrix',
"Shortcut Design Patterns (Cosmo's Checklist)": 'Shortcut Design Patterns',
'For Cosmo — Beslutningsveiledning': 'Beslutningsveiledning',
'Praktiske anbefalinger for arkitekten (Cosmo)': 'Praktiske anbefalinger for arkitekten',
'For arkitekten (Cosmo) — spørsmål, fallgruver og anbefalinger':
'For arkitekten — spørsmål, fallgruver og anbefalinger',
'For Cosmo — når bruker denne kunnskapen?': 'For arkitekten — når bruker denne kunnskapen?',
'For Cosmo: Quick Reference Card': 'Quick Reference Card',
"Cosmo's Talking Points": 'Talking Points',
'Når skal Cosmo foreslå Impact Assessment?': 'Når skal arkitekten foreslå Impact Assessment?',
'Cosmos veiledningsstrategi': 'Veiledningsstrategi',
'Cosmos spørsmål for å utdype': 'Spørsmål for å utdype',
'Red flags Cosmo skal varsle om': 'Red flags arkitekten skal varsle om',
'Cosmos tonalitet': 'Tonalitet',
'Cosmos sjekkliste før avslutning': 'Sjekkliste før avslutning',
"Cosmo's quick decision tree": 'Quick decision tree',
"Cosmo's Stakeholder Communication Checklist": 'Stakeholder Communication Checklist',
'Cosmo-tilnærming': 'Tilnærming',
'Decision trees for Cosmo': 'Decision trees',
'Red flags for Cosmo å se etter': 'Red flags å se etter',
'For Cosmo Skyberg: Application Insights for LLM Monitoring':
'Application Insights for LLM Monitoring',
'For Cosmo: Anvendelse i Arkitekturrådgivning': 'Anvendelse i Arkitekturrådgivning',
'For Arkitekten (Cosmo)': 'For Arkitekten',
"Cosmo's Quick Decision Matrix": 'Quick Decision Matrix',
'Cosmos anbefalinger': 'Anbefalinger',
'For Cosmo: Practical Implementation': 'Practical Implementation',
'For Cosmo: Veiledning i Arkitekturdialog': 'Veiledning i Arkitekturdialog',
'10. Anbefalinger for Cosmo Skyberg': '10. Anbefalinger',
'For Cosmo: Anvendelse i Microsoft AI-arkitektur': 'Anvendelse i Microsoft AI-arkitektur',
'Cosmo-oppsummering': 'Oppsummering',
'Spørsmål Cosmo bør stille kunden': 'Spørsmål å stille kunden',
};
/**
* Per-file overrides where the shared table would collide inside one document. Measured
* by sweeping the ratified mapping over all 389 files and collecting every slug that
* would end up shared: exactly TWO, both adjudicated by reading the file.
*
* 1. feedback-loops-continuous-improvement.md carries both `## For arkitekten (Cosmo)`
* (L674) and `## For Cosmo` (L745), which the ratified mapping would fold onto one
* slug. The second section's own body names the alternative the brief already
* sanctions "Nøkkelpunkter å fremheve i konsultasjon"
* (docs/cosmo-removal-brief-2026-06.md:24) so the target is read off the content
* rather than invented.
* 2. application-insights-llm-monitoring.md's `## For Cosmo Skyberg: Application
* Insights for LLM Monitoring` (L717) would collide with the document's own H1 title
* (L1), because the heading's suffix merely restated that title. Dropping the suffix
* loses nothing and lands on the ratified `For arkitekten` vocabulary.
*/
export const FILE_HEADING_OVERRIDES = {
'skills/ms-ai-engineering/references/mlops-genaiops/feedback-loops-continuous-improvement.md': {
'For Cosmo': 'Nøkkelpunkter for rådgivning',
},
'skills/ms-ai-governance/references/monitoring-observability/application-insights-llm-monitoring.md': {
'For Cosmo Skyberg: Application Insights for LLM Monitoring': 'For arkitekten',
},
};
/**
* Every fragment link whose anchor matches no heading slug in the same document.
* Heading collection is fence-agnostic, matching the transform see the header note.
*
* @param {string} content
* @returns {Array<{text: string, anchor: string}>}
*/
export function findDeadAnchors(content) {
const src = String(content ?? '');
const slugs = new Set();
for (const line of src.split('\n')) {
const m = /^#{1,6}\s+(.+?)\s*$/.exec(line);
if (m) slugs.add(slugifyHeading(m[1].trim()));
}
const dead = [];
for (const m of src.matchAll(/\[([^\]]*)\]\(#([^)]*)\)/g)) {
if (!slugs.has(m[2])) dead.push({ text: m[1], anchor: m[2] });
}
return dead;
}
/**
* Neutralise the persona out of this document's headings, and rewrite the TOC entries
* that anchor to them in the SAME operation so no step of the run ever leaves a dead
* fragment link. Pure: returns new content, writes nothing.
*
* Surgical by design. The TOC is NOT regenerated (buildToc would reorder entries,
* re-derive every anchor and drop `###` entries); only the persona entries are
* rewritten, in place, leaving all 2 709 non-persona fragment links byte-identical.
* Measured precondition that makes this exact: all 327 persona TOC links carry a link
* text that is one of the 40 heading texts AND an anchor equal to slugify of it
* (327/327, zero deviations).
*
* Throws never writes a guess when:
* - a heading carries persona but is not in HEADING_MAP (an unmeasured variant)
* - a rewrite would make two headings in this file share one slug
* - a persona fragment link's anchor is not slugify of its own link text
*
* Idempotent: neutral content has no persona headings left to match.
*
* @param {string} content
* @param {{relPath?: string}} [opts] relPath selects FILE_HEADING_OVERRIDES
* @returns {{content: string, changes: Array<{kind: string, line: number, from: string, to: string}>}}
*/
export function neutralizeContent(content, { relPath = '' } = {}) {
const src = String(content ?? '');
const overrides = FILE_HEADING_OVERRIDES[relPath] || {};
const target = (text) => (Object.prototype.hasOwnProperty.call(overrides, text)
? overrides[text]
: HEADING_MAP[text]);
const lines = src.split('\n');
const changes = [];
// --- pass 1: headings ---
const rewritten = lines.map((line, i) => {
const m = /^(#{1,6})(\s+)(.+?)(\s*)$/.exec(line);
if (!m || !hasPersona(line)) return line;
const from = m[3];
const to = target(from);
if (to === undefined) {
throw new Error(`ukjent persona-heading i ${relPath || '<ukjent fil>'}:${i + 1}: "${from}"`);
}
changes.push({ kind: 'heading', line: i + 1, from, to });
return `${m[1]}${m[2]}${to}${m[4]}`;
});
// --- collision check, BEFORE any TOC work: would two headings share one slug? ---
const slugCount = new Map();
for (const line of rewritten) {
const m = /^#{1,6}\s+(.+?)\s*$/.exec(line);
if (!m) continue;
const s = slugifyHeading(m[1].trim());
slugCount.set(s, (slugCount.get(s) || 0) + 1);
}
for (const c of changes) {
const s = slugifyHeading(c.to);
if (slugCount.get(s) > 1) {
throw new Error(
`slug-kollisjon i ${relPath || '<ukjent fil>'}:${c.line}: "${c.from}" -> "${c.to}" `
+ `gir slug "#${s}" som alt finnes i fila. Krever en FILE_HEADING_OVERRIDES-entry.`,
);
}
}
// --- pass 2: the TOC entries anchoring to the headings we just renamed ---
const out = rewritten.map((line, i) => line.replace(
/\[([^\]]*)\]\(#([^)]*)\)/g,
(whole, text, anchor) => {
if (!hasPersona(whole)) return whole;
const to = target(text);
if (to === undefined) {
throw new Error(
`persona-fragmentlenke som ikke peker paa en kjent heading, `
+ `${relPath || '<ukjent fil>'}:${i + 1}: "${whole}"`,
);
}
if (anchor !== slugifyHeading(text)) {
throw new Error(
`anker matcher ikke slugify(lenketekst) i ${relPath || '<ukjent fil>'}:${i + 1}: `
+ `"${whole}" — forventet "#${slugifyHeading(text)}"`,
);
}
changes.push({ kind: 'toc', line: i + 1, from: text, to });
return `[${to}](#${slugifyHeading(to)})`;
},
));
return { content: out.join('\n'), changes };
}