// 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 ` 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 ", never "Cosmos ". // 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 ` — 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 || ''}:${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 || ''}:${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 || ''}:${i + 1}: "${whole}"`, ); } if (anchor !== slugifyHeading(text)) { throw new Error( `anker matcher ikke slugify(lenketekst) i ${relPath || ''}:${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 }; }