Ordren ba meg behandle sitt eget premiss som et premiss: «PM-ens 87/45 er korrigert
til 85/47 ved maaling ... Behandle ogsaa 85/47 som et premiss og re-maal det foer du
bygger.» Re-målt over de 389 ref-filene holder 85/47 heller ikke — og feilen ligger i
KLASSIFIKATOREN, ikke i tellingen. Summen 132 står; fordelingen er 62/70.
TRE FUNN, alle pinnet i test:
1. FJORTEN bold-etiketter er DIALOG-ATTRIBUSJONER, ikke etiketter. Dialog-bøtta
matchet bare kursivformen `*Cosmo:*`. Fetformen står foran sitert tale —
`**Naar kunden sier:** "..."` / `**Cosmo svarer:** "..."` — så å slette den
etterlater replikken uten taler. Redaksjonelt, ikke mekanisk.
2. TI tabellceller er PROVENIENSPÅSTANDER i kildekvalitets-kolonnen. Celleposisjon
deler de 26 tabellforekomstene i tre klasser, ikke én: 12 radetiketter (kolonne 0),
4 kolonneoverskrifter, 10 proveniensverdier (`Moenstre er Cosmo-design`,
`Raadgivende innhold basert paa Cosmo-persona`). Å skrive om en av dem er å avgi en
ny påstand om hvor innholdet kommer fra, og den siste har ikke noe slettemål i det
hele tatt. Seks av ordrens ni sammensetninger ligger helt inne i denne klassen —
ordrens egen advarsel traff, målingen lokaliserte den.
3. Lekkasje ANDRE veien, +1 mekanisk: `- **For arkitekten (Cosmo):** ...` i
reasoning-models-o1-o3-optimization.md:549 er nøyaktig målklassen, men et
linjestart-forankret nett ser den ikke bak `- ` og bokførte den som prosa.
Operatøren ratifiserte 2026-09-15 alt. A: kjør de 62, hold dialog og proveniens for
R14. Begge tilbakeholdte klasser henger på #R14-persona-ramme som aldri er besvart;
de 62 gjør ikke det, fordi R13 ALT har kjørt `For Cosmo` -> `For arkitekten` over 401
headinger — dette gjør bare etiketter og tabellrader konsistente med en beslutning
som allerede er utført.
UTFØRT: 62 forekomster i 50 filer (ordren sa 47) — 46 etiketter i 16 ratifiserte
varianter, 16 celler i 4. Diffen er 62 fjernet = 62 lagt til, ren in-place-erstatning;
hver endret linje klassifisert, ANNET = 0. Ingen måltekst innfører et ord kilden ikke
hadde, utover R13s sanksjonerte `arkitekten` (samme invariant-test, gjenbrukt).
GATEN, DEKOMPONERT I TRE KLAUSULER, hver validert BEGGE veier før den ble konsumert:
R1 REFERENT mekaniske sites 62 -> 0, produkt 451 uendret.
Kjent-pos: `**For Cosmo:**` OG `| Cosmos raad |` (genitiven er den
et bold-only nett mister) feller begge. Kjent-neg:
`**Cosmos DB-anbefaling:**` — ser ut som persona, er produkt —
og `| Azure Cosmos DB |` passerer urørt.
R2 REKKEVIDDE 0 umålte varianter, 0 utenfor rekkevidde. Kjent-pos: en ukjent
etikettform rapporteres som umålt OG transformen KASTER, den
hopper ikke stille over. Kjent-neg: dialog + proveniens bokføres
utenfor scope og er byte-identiske.
R3 HVA SOM STÅR 16 redaksjonelle etiketter + 10 celler uendret, heading/TOC
fortsatt 0 (R13 ikke regradert), prosa 132 -> 70. Kjent-pos: et
fjernet tegn i `Cosmos DB` feller produkt-differansen. Kjent-neg:
alle kun-produkt-filer byte-identiske gjennom transformen.
Gaten ble kjørt FØR transformen og felte R1 (exit 1) — et nullresultat som aldri er
tvunget til det andre svaret er ingen måling. R13b har egen baseline-fil;
cosmo-gate-baseline.json er et referansepunkt-artefakt (personaHeadings 401) og er
IKKE re-emittert. Ny .gitignore-negasjon (6 entries), aldri `git add -f`.
Målt underveis: 0 av de 62 målinjene ligger i en kodeblokk, så fence-agnostisk
transform er trygg her — samme konklusjon R13 nådde for headinger. Nøyaktig 1 linje
bærer begge klasser; dens `Cosmo-persona` står igjen, som den skal.
Suite 1134/1134 (1120 + 14 nye), validate-plugin 250/0/0, transform idempotent,
R13-driveren fortsatt no-op. RX-OPS1 adversarial-scan kjørt MANUELT på de 56 stagede
filene (OK, exit 0) — `core.hooksPath` skygger repoets pre-commit, fiksen eies av
`.claude`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
521 lines
26 KiB
JavaScript
521 lines
26 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 };
|
||
}
|