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>
596 lines
29 KiB
JavaScript
596 lines
29 KiB
JavaScript
// 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 };
|
||
}
|
||
|
||
// ===========================================================================================
|
||
// R13b — the MECHANICAL persona sites OUTSIDE headings: bold labels and table cells.
|
||
//
|
||
// THE SPLIT IS A MEASUREMENT, NOT THE ORDER'S PREMISE. The dispatching order carried
|
||
// "MEKANISK 85 / REDAKSJONELT 47" — itself a correction of an earlier "87/45" — and asked for
|
||
// it to be re-measured before anything was built. Re-measured over the 389 reference files, it
|
||
// is wrong again, and wrong in the CLASSIFIER rather than the count:
|
||
//
|
||
// 1. FOURTEEN bold labels are DIALOGUE SPEAKER ATTRIBUTIONS. The dialogue bucket only ever
|
||
// matched the italic `*Cosmo:*`; the bold form introduces quoted speech —
|
||
// `**Naar kunden sier:** "..."` / `**Cosmo svarer:** "..."` — so deleting it leaves the
|
||
// quote with no speaker. That is editorial work, not a scripted rewrite.
|
||
// 2. TEN table cells are PROVENANCE CLAIMS in the source-quality column. Cell position splits
|
||
// the 26 table occurrences into three classes, not one: 12 row labels (column 0),
|
||
// 4 column headers, 10 provenance values such as `Moenstre er Cosmo-design` and
|
||
// `Raadgivende innhold basert paa Cosmo-persona`. Rewriting one asserts something new
|
||
// about where the content came from, and the last of them has no deletion target at all.
|
||
// Six of the order's nine compound forms live entirely inside this class.
|
||
// 3. Leaking the other way, +1 mechanical: `- **For arkitekten (Cosmo):** ...` in
|
||
// reasoning-models-o1-o3-optimization.md:549 is exactly the target class, but a
|
||
// line-start-anchored net cannot see it behind the `- ` marker and books it as prose.
|
||
//
|
||
// MEASURED: MEKANISK 62 (46 labels + 16 cells) / REDAKSJONELT 70; sum 132 unchanged.
|
||
// Operator-ratified 2026-09-15, alternative A: execute the 62, hold dialogue and provenance
|
||
// for R14. Both held-back classes depend on the never-answered #R14-persona-ramme (neutral
|
||
// frame / new named persona / none); the 62 do not, because R13 has ALREADY executed
|
||
// `For Cosmo` -> `For arkitekten` across 401 headings, so this only brings labels and table
|
||
// rows into line with a decision that has already run.
|
||
//
|
||
// KEYED ON EXACT TEXT, like HEADING_MAP, and for the same reason: every rewrite is then a
|
||
// table entry a human ratified, and an unmeasured variant THROWS instead of being guessed at.
|
||
// Exact keying is also what keeps the two table classes apart without any row-role heuristic —
|
||
// the header cell is `Cosmo-anbefaling`, the provenance cell is
|
||
// `Cosmo-anbefaling basert paa verified data`, and they simply are different keys.
|
||
|
||
/**
|
||
* The ratified bold-label rewrites: 16 variants, 46 occurrences. Keys are the text INSIDE the
|
||
* `**...**` span, colon included where the source has it inside the bold.
|
||
* No target introduces a word its source did not carry, except R13's sanctioned `arkitekten`.
|
||
*/
|
||
export const LABEL_MAP = {
|
||
// --- the bulk: the three forms that converge on R13's ratified heading vocabulary ---
|
||
'For Cosmo:': 'For arkitekten:',
|
||
'For Cosmo Skyberg:': 'For arkitekten:',
|
||
'For arkitekten (Cosmo):': 'For arkitekten:',
|
||
// --- compound nouns: the persona is the first element, so the head stands alone ---
|
||
'Cosmo-anbefaling:': 'Anbefaling:',
|
||
"Cosmo's anbefaling:": 'Anbefaling:',
|
||
'Cosmo Skybergs anbefaling:': 'Anbefaling:',
|
||
'Cosmo-svar:': 'Svar:',
|
||
'Cosmo-spørsmål:': 'Spørsmål:',
|
||
'Cosmo-checkpoint:': 'Checkpoint:',
|
||
"Cosmo's one-liner:": 'One-liner:',
|
||
"Cosmo's rule of thumb:": 'Rule of thumb:',
|
||
// --- parenthetical or prepositional: the name drops out and the label survives intact ---
|
||
'Owner (Cosmo):': 'Owner:',
|
||
'Note til Cosmo:': 'Note:',
|
||
'Oppsummering for Cosmo': 'Oppsummering',
|
||
'Oppsummering for Cosmo:': 'Oppsummering:',
|
||
// --- addressed TO the architect, so the role takes the name's place ---
|
||
'Til Cosmo:': 'Til arkitekten:',
|
||
};
|
||
|
||
/**
|
||
* Bold labels that carry persona but are OUT of R13b's scope: speaker attributions in front of
|
||
* quoted speech. Enumerated rather than pattern-matched, so that a variant in NEITHER table is
|
||
* a hole in the net and throws, instead of being silently skipped. 6 variants, 14 occurrences.
|
||
*/
|
||
export const EDITORIAL_LABELS = new Set([
|
||
'Cosmo:', // 5 — `**Cosmo:** "Jeg forstaar at dere er klare for produksjon..."`
|
||
'Cosmo svarer:', // 4 — paired with `**Naar kunden sier:**`
|
||
'Cosmo forklarer:', // 2 — same dialogue frame
|
||
'Cosmo spør:', // 1 — same dialogue frame
|
||
'Mitt svar (Cosmo):', // 1 — first person, inside a scripted exchange
|
||
'Cosmo Skyberg tipset:', // 1 — past-tense attribution of advice to a person
|
||
// The two the ORDER already booked as editorial, and the reason its predecessor undercounted
|
||
// provenance: both are whole sentences that merely happen to be bold, and the first also
|
||
// points at `/architect`, an R14 delivery surface. Listed here so the net has no hole.
|
||
'For spørsmål om NLU-konfigurasjon, kontakt Cosmo Skyberg via `/architect`.',
|
||
'Sist oppdatert av Cosmo Skyberg, Microsoft AI Solution Architect',
|
||
]);
|
||
|
||
/** The ratified table-cell rewrites: 4 variants, 16 occurrences (12 row labels, 4 headers). */
|
||
export const TABLE_CELL_MAP = {
|
||
'For arkitekten (Cosmo)': 'For arkitekten',
|
||
'**For arkitekten (Cosmo)**': '**For arkitekten**',
|
||
'Cosmo-anbefaling': 'Anbefaling',
|
||
'Cosmos råd': 'Råd',
|
||
};
|
||
|
||
/** Provenance cells held back for R14's editorial pass. 10 occurrences, measured. */
|
||
export const EDITORIAL_CELLS = new Set([
|
||
'Cosmo-syntese av verified sources',
|
||
'Cosmo-anbefaling basert på verified data',
|
||
'Cosmo-anbefalinger basert på best practices',
|
||
"Cosmo's domain expertise + maturity model",
|
||
"Cosmo's domain knowledge + norsk lovverk",
|
||
"Cosmo's expertise + best practices fra docs",
|
||
'Rådgivende innhold basert på Cosmo-persona',
|
||
'Mønstre er Cosmo-design, PowerShell-kode er Verified',
|
||
'Tabeller og anbefalinger basert på Cosmo-erfaring + Microsoft docs',
|
||
'GDPR/AI Act-mapping er Cosmo-tolkninger (ikke Microsoft-spesifikk)',
|
||
]);
|
||
|
||
/** A bold label at the start of a line, optionally behind a list marker. */
|
||
const LABEL_RE = /^(\s*(?:[-*+]\s+)?)\*\*([^*\n]+)\*\*/;
|
||
/** A markdown table row. */
|
||
const TABLE_RE = /^\s*\|/;
|
||
|
||
/**
|
||
* Where do this document's non-heading persona occurrences sit, and is each one in a ratified
|
||
* table? Counts only the SITES R13b acts on — bold labels and table cells; prose, dialogue
|
||
* outside bold labels and provenance lines are R14's and are not counted here.
|
||
*
|
||
* `unmapped` is the fail-safe: a persona label or cell in neither the mechanical nor the
|
||
* editorial table is an unmeasured variant, and the driver refuses to run rather than guess.
|
||
*
|
||
* @param {string} content
|
||
* @returns {{label: {mechanical: number, editorial: number},
|
||
* table: {mechanical: number, editorial: number}, unmapped: string[]}}
|
||
*/
|
||
export function classifyPersonaSites(content) {
|
||
const label = { mechanical: 0, editorial: 0 };
|
||
const table = { mechanical: 0, editorial: 0 };
|
||
const unmapped = [];
|
||
for (const line of String(content ?? '').split('\n')) {
|
||
if (/^#{1,6}\s+/.test(line)) continue;
|
||
if (TABLE_RE.test(line)) {
|
||
const segs = line.split('|');
|
||
for (let i = 1; i < segs.length - 1; i++) {
|
||
const cell = segs[i].trim();
|
||
if (!hasPersona(cell)) continue;
|
||
if (TABLE_CELL_MAP[cell] !== undefined) table.mechanical++;
|
||
else if (EDITORIAL_CELLS.has(cell)) table.editorial++;
|
||
else unmapped.push(`cell: ${JSON.stringify(cell)}`);
|
||
}
|
||
continue;
|
||
}
|
||
const m = LABEL_RE.exec(line);
|
||
if (!m || !hasPersona(m[2])) continue;
|
||
if (LABEL_MAP[m[2]] !== undefined) label.mechanical++;
|
||
else if (EDITORIAL_LABELS.has(m[2])) label.editorial++;
|
||
else unmapped.push(`label: ${JSON.stringify(m[2])}`);
|
||
}
|
||
return { label, table, unmapped };
|
||
}
|
||
|
||
/**
|
||
* Neutralise the ratified bold labels and table cells. Pure: returns new content, writes
|
||
* nothing. Surgical — only the label span or the single matching cell is rewritten, so the
|
||
* rest of the line, including any persona R14 still owns, stays byte-identical. Splitting and
|
||
* rejoining a row on `|` is lossless, so cell padding survives untouched.
|
||
*
|
||
* Throws — never writes a guess — on a persona label or cell in neither ratified table.
|
||
* Idempotent: a neutral target matches no key.
|
||
*
|
||
* @param {string} content
|
||
* @param {{relPath?: string}} [opts]
|
||
* @returns {{content: string, changes: Array<{kind: string, line: number, from: string, to: string}>}}
|
||
*/
|
||
export function neutralizeLabels(content, { relPath = '' } = {}) {
|
||
const where = (i) => `${relPath || '<ukjent fil>'}:${i + 1}`;
|
||
const changes = [];
|
||
const out = String(content ?? '').split('\n').map((line, i) => {
|
||
if (/^#{1,6}\s+/.test(line)) return line;
|
||
|
||
if (TABLE_RE.test(line)) {
|
||
const segs = line.split('|');
|
||
let touched = false;
|
||
for (let c = 1; c < segs.length - 1; c++) {
|
||
const cell = segs[c].trim();
|
||
if (!hasPersona(cell)) continue;
|
||
const to = TABLE_CELL_MAP[cell];
|
||
if (to === undefined) {
|
||
if (EDITORIAL_CELLS.has(cell)) continue;
|
||
throw new Error(`ukjent persona-tabellcelle i ${where(i)}: ${JSON.stringify(cell)}`);
|
||
}
|
||
segs[c] = segs[c].replace(cell, to);
|
||
changes.push({ kind: 'cell', line: i + 1, from: cell, to });
|
||
touched = true;
|
||
}
|
||
return touched ? segs.join('|') : line;
|
||
}
|
||
|
||
const m = LABEL_RE.exec(line);
|
||
if (!m || !hasPersona(m[2])) return line;
|
||
const to = LABEL_MAP[m[2]];
|
||
if (to === undefined) {
|
||
if (EDITORIAL_LABELS.has(m[2])) return line;
|
||
throw new Error(`ukjent persona-etikett i ${where(i)}: ${JSON.stringify(m[2])}`);
|
||
}
|
||
changes.push({ kind: 'label', line: i + 1, from: m[2], to });
|
||
return `${m[1]}**${to}**${line.slice(m[0].length)}`;
|
||
});
|
||
return { content: out.join('\n'), changes };
|
||
}
|
||
|
||
// ================================ R14: the delivery surface ================================
|
||
//
|
||
// R13 emptied the reference corpus's headings, R13b its mechanical labels and cells. R14 is
|
||
// the rest of the plugin: SKILL.md files, 23 commands, the 70 editorial occurrences, and the
|
||
// six sites that PRODUCE new persona on every KB generation. What stays behind is the
|
||
// DERIVATION REGISTER below — statements that are true about the past.
|
||
//
|
||
// WHY A SEPARATE COUNTER AND NOT classifyCosmo. One line outside the corpus defeats the R13
|
||
// classifier: `docs/ref-kb-gold-reconciliation-2026-06.md:93` reads "Cosmos multi-region
|
||
// writes (active-active)", which the Norwegian-genitive rule scores as persona because
|
||
// PRODUCT_FOLLOWERS was enumerated over a corpus that contains no English product prose.
|
||
// The verdict was settled by the `/azure/cosmos-db/` URL in the same table row, so that is
|
||
// the discriminator applied here — a rule, not a filename exemption.
|
||
//
|
||
// The rule is CLOSED, not open-ended. Measured over all tracked `*.md` on 2026-09-15: only 8
|
||
// lines have the ambiguous `Cosmos <lowercase>` shape at all, and only 33 lines carry a
|
||
// cosmos-db URL; of those 33, exactly 2 classify as persona and BOTH are genuinely the
|
||
// product. It is also not a laundering channel: the URL only excuses the GENITIVE shape, so a
|
||
// bare `Cosmo` on a line that happens to cite cosmos-db still counts as persona.
|
||
//
|
||
// classifyCosmo itself is deliberately NOT changed: the G1/G3/R1/R3 baselines are measured
|
||
// with it, and moving a classifier under a committed baseline is how a gate stops proving
|
||
// anything.
|
||
|
||
/**
|
||
* Persona occurrences in DELIVERY content — classifyCosmo plus the adjudicated product rule.
|
||
* @param {string} content
|
||
* @returns {number}
|
||
*/
|
||
export function personaInDeliveryContent(content) {
|
||
let n = 0;
|
||
for (const line of String(content ?? '').split('\n')) {
|
||
const citesProduct = /cosmos-db|cosmosdb/i.test(line);
|
||
for (const m of line.matchAll(/cosmo/gi)) {
|
||
const rest = line.slice(m.index);
|
||
if (!isPersonaOccurrence(rest)) continue;
|
||
// Only the genitive shape is excused by the URL. A bare `Cosmo` is the persona
|
||
// whatever else the line cites.
|
||
if (citesProduct && /^Cosmos[ \t]/.test(rest)) continue;
|
||
n++;
|
||
}
|
||
}
|
||
return n;
|
||
}
|
||
|
||
/**
|
||
* The derivation register: files whose persona mentions are TRUE statements about the past
|
||
* and must survive R14. Two classes — history (CHANGELOG and the README version table, which
|
||
* CLAUDE.md forbids rewriting: v1.0.0 really did ship the persona, and v1.16.5 really did
|
||
* remove `For Cosmo` from two files) and derivation records (the plans, briefs and the
|
||
* measurement document that record why the decisions were taken).
|
||
*
|
||
* PINNED ON COUNTS, NEVER ON FILENAMES. A filename exemption is blind to what the file later
|
||
* comes to contain; a count moves the moment a new persona line is added, so it still fells
|
||
* the gate. The set is closed under its own documentation: the measurement document counts
|
||
* itself, which is why the register can be committed at all.
|
||
*
|
||
* Counts measured 2026-09-15 with personaInDeliveryContent — the same function G4 uses, so
|
||
* the register's population matches the field it is subtracted from.
|
||
*/
|
||
export const DERIVATION_REGISTER = {
|
||
'CHANGELOG.md': 6,
|
||
'README.md': 3,
|
||
'docs/cosmo-removal-brief-2026-06.md': 32,
|
||
'docs/devils-advocate-audit-2026-06-18.md': 1,
|
||
'docs/kb-mechanism-redesign-brief.md': 1,
|
||
'docs/kb-refresh-backlog-2026-06.md': 3,
|
||
'docs/r14-persona-gate-measurement-2026-09.md': 32,
|
||
'docs/ref-kb-correctness-program-2026-06.md': 4,
|
||
'docs/ref-kb-direction-note-2026-06.md': 6,
|
||
'docs/ref-kb-workflow-plan-2026-06.md': 25,
|
||
'docs/skill-validation-routine-brief-2026-07.md': 2,
|
||
'docs/spor1-authority-selection.md': 3,
|
||
};
|