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>
117 lines
5 KiB
JavaScript
117 lines
5 KiB
JavaScript
#!/usr/bin/env node
|
|
// neutralize-cosmo-headings.mjs — R13 Cosmo del 1: apply the ratified heading
|
|
// neutralisation across the reference corpus, rewriting each persona heading and the TOC
|
|
// entries anchored to it in ONE per-file operation, so no intermediate state leaves a
|
|
// dead fragment link.
|
|
//
|
|
// SCOPE (operator-ratified 2026-09-12, alternative A). In: the 401 persona headings in
|
|
// 374 reference files (40 measured variants) plus the 327 TOC entries that anchor to
|
|
// them. OUT: the 4 SKILL.md files, the 23 commands, CLAUDE.md / README.md / NOTICE.md,
|
|
// the docs/ files (all R14), and the 132 persona occurrences in prose, tables, dialogue
|
|
// and provenance lines — those need editorial judgement, not a scripted transform, and
|
|
// are booked as an open decision for R13b/R14.
|
|
//
|
|
// INVARIANTS asserted per file BEFORE any write (a throw aborts the whole run and
|
|
// writes nothing — the plan-then-write discipline of strip-stale-verified-pipe.mjs):
|
|
// - zero persona occurrences remain on heading lines
|
|
// - product occurrence count byte-for-byte unchanged (the 451 Azure Cosmos DB mentions)
|
|
// - line count unchanged (headings are edited in place, never added or removed)
|
|
// - no NEW dead fragment link
|
|
// - only heading lines and TOC-entry lines differ from the original
|
|
// Idempotent: a re-run finds no persona heading and is a no-op, so an interrupted run is
|
|
// recovered by re-running.
|
|
//
|
|
// Usage: node scripts/kb-update/neutralize-cosmo-headings.mjs [--dry]
|
|
import { readFileSync, realpathSync, globSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { atomicWriteSync } from './lib/atomic-write.mjs';
|
|
import { classifyCosmo, findDeadAnchors, neutralizeContent } from './lib/cosmo-persona.mjs';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const PLUGIN_ROOT = join(__dirname, '..', '..');
|
|
|
|
/**
|
|
* Assert every per-file invariant. Throws on the first violation so the run aborts
|
|
* before a single byte is written.
|
|
*/
|
|
function assertInvariant(rel, before, after, changes) {
|
|
const b = classifyCosmo(before);
|
|
const a = classifyCosmo(after);
|
|
if (a.persona.heading !== 0) {
|
|
throw new Error(`${rel}: ${a.persona.heading} persona-forekomst(er) staar igjen paa heading-linjer`);
|
|
}
|
|
if (a.product !== b.product) {
|
|
throw new Error(`${rel}: produkt-forekomster endret ${b.product} -> ${a.product}`);
|
|
}
|
|
const lb = before.split('\n').length;
|
|
const la = after.split('\n').length;
|
|
if (lb !== la) throw new Error(`${rel}: linjeantall endret ${lb} -> ${la}`);
|
|
|
|
const deadBefore = new Set(findDeadAnchors(before).map((d) => d.anchor));
|
|
const newDead = findDeadAnchors(after).map((d) => d.anchor).filter((x) => !deadBefore.has(x));
|
|
if (newDead.length) throw new Error(`${rel}: nye doede ankre: ${newDead.join(', ')}`);
|
|
|
|
// Only the lines the transform claims to have touched may differ.
|
|
const touched = new Set(changes.map((c) => c.line));
|
|
const lbArr = before.split('\n');
|
|
const laArr = after.split('\n');
|
|
for (let i = 0; i < lbArr.length; i++) {
|
|
if (lbArr[i] === laArr[i]) continue;
|
|
if (!touched.has(i + 1)) {
|
|
throw new Error(`${rel}:${i + 1}: linje endret uten aa staa i endringslista\n`
|
|
+ ` foer: ${lbArr[i]}\n etter: ${laArr[i]}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function main() {
|
|
const dry = process.argv.includes('--dry');
|
|
const files = globSync('skills/*/references/**/*.md', { cwd: PLUGIN_ROOT }).sort();
|
|
const planned = [];
|
|
const skipped = [];
|
|
let headingChanges = 0;
|
|
let tocChanges = 0;
|
|
|
|
for (const rel of files) {
|
|
const abs = join(PLUGIN_ROOT, rel);
|
|
const before = readFileSync(abs, 'utf8');
|
|
const { content: after, changes } = neutralizeContent(before, { relPath: rel });
|
|
if (after === before) { skipped.push(rel); continue; }
|
|
assertInvariant(rel, before, after, changes);
|
|
headingChanges += changes.filter((c) => c.kind === 'heading').length;
|
|
tocChanges += changes.filter((c) => c.kind === 'toc').length;
|
|
planned.push({ rel, out: after, changes });
|
|
}
|
|
|
|
console.log(`ref-filer: ${files.length} | aa endre: ${planned.length} | urørt: ${skipped.length}`);
|
|
console.log(`heading-endringer: ${headingChanges} | TOC-endringer: ${tocChanges}`);
|
|
|
|
if (dry) {
|
|
console.log('\n(dry run — ingen skriving)');
|
|
const byTarget = new Map();
|
|
for (const p of planned) {
|
|
for (const c of p.changes.filter((x) => x.kind === 'heading')) {
|
|
const k = `${c.from} -> ${c.to}`;
|
|
byTarget.set(k, (byTarget.get(k) || 0) + 1);
|
|
}
|
|
}
|
|
console.log('\n--- heading-mapping som vil brukes ---');
|
|
for (const [k, v] of [...byTarget.entries()].sort((x, y) => y[1] - x[1])) {
|
|
console.log(`${String(v).padStart(4)} ${k}`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
for (const p of planned) atomicWriteSync(join(PLUGIN_ROOT, p.rel), p.out);
|
|
console.log(`\nSkrev ${planned.length} filer.`);
|
|
}
|
|
|
|
const isMain = (() => {
|
|
try {
|
|
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
} catch {
|
|
return false;
|
|
}
|
|
})();
|
|
if (isMain) main();
|