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

324 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 };
}