ms-ai-architect/scripts/kb-update/check-cosmo-gate.mjs
Kjell Tore Guttormsen ced0c5f46d refactor(ms-ai-architect): R14 — persona ut av plugin-flaten, og de seks stedene som produserte den
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>
2026-09-15 23:15:27 +02:00

487 lines
24 KiB
JavaScript

#!/usr/bin/env node
// check-cosmo-gate.mjs — the RATIFIED R13 persona gate (operator 2026-09-12, alt. A).
//
// Replaces the roadmap's `grep -rl "Cosmo" skills/*/references | wc -l -> 0`, which was
// measured false twice over (it would delete 451 Azure Cosmos DB occurrences, and it is
// unreachable under a heading-only scope because 132 persona occurrences live in prose,
// tables and dialogue). Full derivation: lib/cosmo-persona.mjs header.
//
// THREE CLAUSES
// G1 persona occurrences on markdown heading lines in the reference corpus = 0
// G2 every fragment link resolves to a real heading slug, except the anchors on the
// committed exemption list (pre-existing, not caused by R13)
// G3 product occurrences unchanged from the committed baseline, and each product-only
// file still carries zero persona
//
// USAGE
// node scripts/kb-update/check-cosmo-gate.mjs # check (exit 1 on fail)
// node scripts/kb-update/check-cosmo-gate.mjs --emit-baseline # write the baseline JSON
// node scripts/kb-update/check-cosmo-gate.mjs --validate-net # prove the net both ways
//
// --validate-net is not decoration. A gate whose null result has never been forced to
// the other answer is not a measurement (memory decision-gate-must-be-validated-both-
// ways): it injects a known-positive persona heading into a copy of a real corpus file
// and asserts G1 FAILS, then asserts the known-negative product-only files PASS.
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { join, dirname, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { globSync } from 'node:fs';
import {
classifyCosmo, findDeadAnchors, classifyPersonaSites, neutralizeLabels,
personaInDeliveryContent, DERIVATION_REGISTER,
} from './lib/cosmo-persona.mjs';
import { execFileSync } from 'node:child_process';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PLUGIN_ROOT = join(__dirname, '..', '..');
const BASELINE_PATH = join(__dirname, 'data', 'cosmo-gate-baseline.json');
// R13b lives in its OWN baseline file. The R13 one records pre-transform reference points
// (personaHeadings 401) that regenerating would destroy — never build over a reference-point
// artefact, so R13b adds a file rather than re-emitting that one.
const LABELS_BASELINE_PATH = join(__dirname, 'data', 'cosmo-labels-baseline.json');
/** Every reference file in the corpus, as repo-relative paths, sorted. */
function refFiles() {
return globSync('skills/*/references/**/*.md', { cwd: PLUGIN_ROOT }).sort();
}
/** Measure the whole corpus: persona buckets, product total, per-file detail. */
function measure() {
const files = refFiles();
const totals = { heading: 0, toc: 0, prose: 0, product: 0 };
const headingFiles = new Set();
const perFile = {};
const deadAnchors = [];
for (const rel of files) {
const src = readFileSync(join(PLUGIN_ROOT, rel), 'utf8');
const c = classifyCosmo(src);
totals.heading += c.persona.heading;
totals.toc += c.persona.toc;
totals.prose += c.persona.prose;
totals.product += c.product;
if (c.persona.heading > 0) headingFiles.add(rel);
perFile[rel] = { persona: c.personaTotal, product: c.product };
for (const d of findDeadAnchors(src)) deadAnchors.push(`${rel}#${d.anchor}`);
}
return { files, totals, headingFiles: [...headingFiles].sort(), perFile, deadAnchors };
}
/** Files carrying Azure Cosmos DB but no persona at all — must stay untouched. */
function productOnlyFiles(m) {
return m.files.filter((f) => m.perFile[f].product > 0 && m.perFile[f].persona === 0);
}
function emitBaseline() {
const m = measure();
const pof = productOnlyFiles(m);
const baseline = {
_comment: 'Ratified R13 persona-gate baseline. Measured before the heading transform; '
+ 'G1 drives personaHeadings to 0, G2 and G3 must not move. Regenerate ONLY with a '
+ 'ratified scope change — a silently moved baseline is a gate that proves nothing.',
measured: new Date().toISOString().slice(0, 10),
refFiles: m.files.length,
personaHeadings: m.totals.heading,
personaHeadingFiles: m.headingFiles.length,
personaTocOccurrences: m.totals.toc,
personaProseOccurrences: m.totals.prose,
productOccurrences: m.totals.product,
productOnlyFiles: pof,
productOnlyFileCounts: Object.fromEntries(pof.map((f) => [f, m.perFile[f].product])),
deadAnchorsExempt: m.deadAnchors,
};
writeFileSync(BASELINE_PATH, `${JSON.stringify(baseline, null, 2)}\n`, 'utf8');
console.log(`baseline skrevet: ${relative(PLUGIN_ROOT, BASELINE_PATH)}`);
console.log(JSON.stringify(baseline, null, 2));
}
function check() {
if (!existsSync(BASELINE_PATH)) {
console.error('FEIL: ingen baseline. Kjoer --emit-baseline foer transformen.');
process.exit(2);
}
const b = JSON.parse(readFileSync(BASELINE_PATH, 'utf8'));
const m = measure();
const fails = [];
// --- G1: no persona left on any heading line ---
if (m.totals.heading !== 0) {
fails.push(`G1: ${m.totals.heading} persona-forekomster staar fortsatt paa heading-linjer `
+ `i ${m.headingFiles.length} filer (baseline var ${b.personaHeadings})`);
}
// --- G2: no dead fragment links beyond the committed exemptions ---
const exempt = new Set(b.deadAnchorsExempt);
const newDead = m.deadAnchors.filter((d) => !exempt.has(d));
if (newDead.length) {
fails.push(`G2: ${newDead.length} NYE doede fragmentlenker: ${newDead.slice(0, 10).join(', ')}`);
}
const healed = b.deadAnchorsExempt.filter((d) => !m.deadAnchors.includes(d));
// --- G3: product untouched ---
if (m.totals.product !== b.productOccurrences) {
fails.push(`G3: produkt-forekomster ${m.totals.product} != baseline ${b.productOccurrences} `
+ `(differanse ${m.totals.product - b.productOccurrences})`);
}
for (const [f, n] of Object.entries(b.productOnlyFileCounts)) {
const now = m.perFile[f];
if (!now) { fails.push(`G3: kun-produkt-fila ${f} finnes ikke lenger`); continue; }
if (now.product !== n) fails.push(`G3: ${f} produkt ${now.product} != baseline ${n}`);
if (now.persona !== 0) fails.push(`G3: ${f} har faatt ${now.persona} persona-forekomst(er)`);
}
const labelLines = checkLabels(fails);
const deliveryLines = checkDelivery(fails);
console.log('=== R13 persona-gate (ratifisert 2026-09-12, alt. A) ===');
console.log(`ref-filer : ${m.files.length} (baseline ${b.refFiles})`);
console.log(`G1 persona paa heading-linjer : ${m.totals.heading} (baseline ${b.personaHeadings})`);
console.log(` persona i TOC-linjer : ${m.totals.toc} (baseline ${b.personaTocOccurrences})`);
console.log(` persona i proesa/tabell : ${m.totals.prose} (baseline ${b.personaProseOccurrences}) `
+ '— UTENFOR R13-scope, bokfoert til R13b/R14');
console.log(`G2 doede fragmentlenker : ${m.deadAnchors.length} `
+ `(${exempt.size} unntatt, ${newDead.length} nye, ${healed.length} forsvunnet)`);
console.log(`G3 produkt-forekomster : ${m.totals.product} (baseline ${b.productOccurrences})`);
console.log(` kun-produkt-filer urørt : ${Object.keys(b.productOnlyFileCounts).length}`);
console.log('--- R13b etikett/celle-gate (ratifisert 2026-09-15, alt. A) ---');
for (const l of labelLines) console.log(l);
console.log('--- R14 leveranseflate-gate (ratifisert 2026-09-15) ---');
for (const l of deliveryLines) console.log(l);
if (fails.length) {
console.log('\nGATE FAILED');
for (const f of fails) console.log(` x ${f}`);
process.exit(1);
}
console.log('\nGATE PASSED');
}
/** Force the gate to the other answer — a null result nobody could falsify is not one. */
function validateNet() {
const m = measure();
let bad = 0;
// Known-positive: inject a persona heading into a real corpus file's content and
// assert the classifier sees it. If this cannot fail the gate, the gate is blind.
const victim = m.files[0];
const injected = `${readFileSync(join(PLUGIN_ROOT, victim), 'utf8')}\n## For Cosmo\n`;
const ic = classifyCosmo(injected);
const base = classifyCosmo(readFileSync(join(PLUGIN_ROOT, victim), 'utf8'));
if (ic.persona.heading !== base.persona.heading + 1) {
console.log(`x KJENT-POSITIV: injisert "## For Cosmo" ble IKKE sett `
+ `(${base.persona.heading} -> ${ic.persona.heading})`); bad++;
} else {
console.log(`ok KJENT-POSITIV: injisert "## For Cosmo" felte G1 `
+ `(${base.persona.heading} -> ${ic.persona.heading}) i ${victim}`);
}
// Known-positive #2: the Norwegian genitive must also be caught, since it is the
// case the letter-s heuristic gets wrong in BOTH directions.
const gen = classifyCosmo('### Cosmos tonalitet\n');
if (gen.persona.heading !== 1) { console.log('x KJENT-POSITIV: genitiv "### Cosmos tonalitet" ikke sett'); bad++; }
else console.log('ok KJENT-POSITIV: genitiv "### Cosmos tonalitet" felte G1');
// Known-negative: a product heading must NOT trip the gate.
const prod = classifyCosmo('### Azure Cosmos DB for GraphRAG\n');
if (prod.personaTotal !== 0 || prod.product !== 1) {
console.log(`x KJENT-NEGATIV: produkt-heading feilklassifisert `
+ `(persona ${prod.personaTotal}, produkt ${prod.product})`); bad++;
} else console.log('ok KJENT-NEGATIV: "### Azure Cosmos DB for GraphRAG" gir 0 persona, 1 produkt');
// Known-negative #2: the real product-only files must be clean of persona.
for (const f of productOnlyFiles(m)) {
const c = classifyCosmo(readFileSync(join(PLUGIN_ROOT, f), 'utf8'));
if (c.personaTotal !== 0) { console.log(`x KJENT-NEGATIV: ${f} klassifisert med persona`); bad++; }
else console.log(`ok KJENT-NEGATIV: ${f} — 0 persona, ${c.product} produkt`);
}
console.log('--- R13b klausulene, begge veier ---');
bad += validateLabelsNet();
console.log('--- R14 G4, begge veier ---');
bad += validateDeliveryNet();
console.log(bad === 0 ? '\nNETTET VALIDERT BEGGE VEIER' : `\nNETT-VALIDERING FEILET (${bad})`);
process.exit(bad === 0 ? 0 : 1);
}
// ================================ R13b: the three decomposed clauses ========================
//
// R1 REFERENT — has the search string more than one referent? Zero persona in
// MECHANICAL bold labels and table cells, while product stays untouched.
// R2 REKKEVIDDE — does the operation reach every occurrence it claims? An in-class site
// in neither ratified table must be REPORTED, never silently skipped.
// R3 HVA SOM STAAR — the 70 editorial occurrences unchanged in count AND content, product
// 451 unchanged, R13's headings and TOC still zero (not regraded).
//
// Each clause carries BOTH a known-positive and a known-negative in --validate-net. A gate
// never forced to the other answer is not a measurement.
/** Per-corpus measurement of the R13b sites. */
function measureLabels() {
const files = refFiles();
const t = {
mechLabel: 0, mechCell: 0, edLabel: 0, edCell: 0, prose: 0, product: 0,
unmapped: [], mechFiles: [], unreachable: [],
};
for (const rel of files) {
const src = readFileSync(join(PLUGIN_ROOT, rel), 'utf8');
const s = classifyPersonaSites(src);
t.mechLabel += s.label.mechanical;
t.mechCell += s.table.mechanical;
t.edLabel += s.label.editorial;
t.edCell += s.table.editorial;
for (const u of s.unmapped) t.unmapped.push(`${rel}: ${u}`);
const c = classifyCosmo(src);
t.prose += c.persona.prose;
t.product += c.product;
// R2: a site the tables know but the transform does not actually rewrite is out of reach.
if (s.label.mechanical + s.table.mechanical > 0) {
t.mechFiles.push(rel);
let after;
try { after = neutralizeLabels(src, { relPath: rel }).content; } catch (e) {
t.unreachable.push(`${rel}: ${e.message}`); continue;
}
const a = classifyPersonaSites(after);
const left = a.label.mechanical + a.table.mechanical;
if (left) t.unreachable.push(`${rel}: ${left} site(r) utenfor transformens rekkevidde`);
}
}
return t;
}
function emitLabelsBaseline() {
const t = measureLabels();
const baseline = {
_comment: 'Ratified R13b label/cell baseline (operator 2026-09-15, alt. A). Measured BEFORE '
+ 'the label transform. R1 drives mechanical -> 0; R3 pins editorial and product as '
+ 'immovable. The 85/47 split the dispatching order carried was re-measured to 62/70 — '
+ 'the classifier was wrong, not the count. Regenerate ONLY on a ratified scope change.',
measured: new Date().toISOString().slice(0, 10),
refFiles: refFiles().length,
mechanicalLabels: t.mechLabel,
mechanicalCells: t.mechCell,
mechanicalTotal: t.mechLabel + t.mechCell,
mechanicalFiles: t.mechFiles.length,
editorialLabels: t.edLabel,
editorialCells: t.edCell,
editorialTotal: 70,
personaProseBefore: t.prose,
personaProseAfter: t.prose - (t.mechLabel + t.mechCell),
productOccurrences: t.product,
mixedLines: [
'skills/ms-ai-advisor/references/prompt-engineering/reasoning-models-o1-o3-optimization.md:549',
],
};
writeFileSync(LABELS_BASELINE_PATH, `${JSON.stringify(baseline, null, 2)}\n`, 'utf8');
console.log(`R13b-baseline skrevet: ${relative(PLUGIN_ROOT, LABELS_BASELINE_PATH)}`);
console.log(JSON.stringify(baseline, null, 2));
}
/** Evaluate R1-R3, pushing onto the shared fails list; returns the printable lines. */
function checkLabels(fails) {
if (!existsSync(LABELS_BASELINE_PATH)) return ['(R13b: ingen baseline — klausulene hoppes over)'];
const b = JSON.parse(readFileSync(LABELS_BASELINE_PATH, 'utf8'));
const t = measureLabels();
const mech = t.mechLabel + t.mechCell;
// --- R1 REFERENT ---
if (mech !== 0) {
fails.push(`R1: ${mech} mekanisk(e) persona-site(r) staar igjen `
+ `(${t.mechLabel} etiketter, ${t.mechCell} celler; baseline ${b.mechanicalTotal})`);
}
if (t.product !== b.productOccurrences) {
fails.push(`R1/R3: produkt ${t.product} != baseline ${b.productOccurrences}`);
}
// --- R2 REKKEVIDDE ---
if (t.unmapped.length) {
fails.push(`R2: ${t.unmapped.length} persona-site(r) i INGEN ratifisert tabell — `
+ `umaalt variant: ${t.unmapped.slice(0, 5).join(' | ')}`);
}
if (t.unreachable.length) {
fails.push(`R2: ${t.unreachable.length} site(r) utenfor transformens rekkevidde: `
+ `${t.unreachable.slice(0, 5).join(' | ')}`);
}
// --- R3 HVA SOM SKAL STAA ---
// R3 vernet R14s scope mens R13b kjoerte: de 70 redaksjonelle skulle staa UROERT. R14 er
// det scopet, saa naar R14 er utfoert er 0 den rette verdien -- ikke et brudd. Klausulen
// godtar derfor to tilstander og INGENTING imellom: enten holdt (16+10, R14 ikke kjoert)
// eller tomt (0+0, R14 utfoert). Et halvveis sveip er nettopp det R3 skal felle.
const edHeld = t.edLabel === b.editorialLabels && t.edCell === b.editorialCells;
const edDone = t.edLabel === 0 && t.edCell === 0;
if (!edHeld && !edDone) {
fails.push(`R3: REDAKSJONELT scope halvveis roert — etiketter ${t.edLabel} `
+ `(holdt ${b.editorialLabels}, utfoert 0), celler ${t.edCell} `
+ `(holdt ${b.editorialCells}, utfoert 0)`);
}
if (![b.personaProseBefore, b.personaProseAfter, 0].includes(t.prose)) {
fails.push(`R3: persona i proesa/tabell ${t.prose} er ingen av de tre lovlige `
+ `tilstandene (${b.personaProseBefore} foer R13b, ${b.personaProseAfter} etter, 0 etter R14)`);
}
const done = mech === 0;
return [
`R1 mekaniske sites (etikett+celle) : ${mech} (baseline ${b.mechanicalTotal} `
+ `= ${b.mechanicalLabels}+${b.mechanicalCells} i ${b.mechanicalFiles} filer)`,
`R2 umaalte varianter / utenfor rekkevidde : ${t.unmapped.length} / ${t.unreachable.length}`,
`R3 redaksjonelt holdt for R14 : ${t.edLabel} etiketter + ${t.edCell} celler `
+ `(baseline ${b.editorialLabels} + ${b.editorialCells})`,
` persona i proesa/tabell : ${t.prose} (${b.personaProseBefore} foer R13b, `
+ `${b.personaProseAfter} etter)${done ? ' — R13b UTFOERT' : ' — R13b IKKE kjoert'}`,
];
}
/** Force each R13b clause to the other answer. */
function validateLabelsNet() {
let bad = 0;
const ok = (msg) => console.log(`ok ${msg}`);
const no = (msg) => { console.log(`x ${msg}`); bad++; };
// R1 known-positive — BOTH forms, because the genitive table variant is precisely what a
// bold-only net misses.
const victim = refFiles()[0];
const base = readFileSync(join(PLUGIN_ROOT, victim), 'utf8');
const inj = classifyPersonaSites(`${base}\n**For Cosmo:** x\n\n| A | Cosmos råd |\n`);
const pre = classifyPersonaSites(base);
if (inj.label.mechanical !== pre.label.mechanical + 1) no('R1 KJENT-POSITIV: injisert "**For Cosmo:**" ble IKKE sett');
else ok('R1 KJENT-POSITIV: injisert "**For Cosmo:**" felte R1');
if (inj.table.mechanical !== pre.table.mechanical + 1) no('R1 KJENT-POSITIV: injisert "| Cosmos råd |" (genitiv) ble IKKE sett');
else ok('R1 KJENT-POSITIV: injisert "| Cosmos råd |" (genitiv) felte R1');
// R1 known-negative — the second case is the dangerous one: it LOOKS like a persona label.
const prod = classifyPersonaSites('**Cosmos DB-anbefaling:** bruk autoscale\n| Azure Cosmos DB | 8000 NOK |\n');
if (prod.label.mechanical + prod.table.mechanical !== 0 || prod.unmapped.length) {
no(`R1 KJENT-NEGATIV: produkt feilklassifisert (${JSON.stringify(prod)})`);
} else ok('R1 KJENT-NEGATIV: "**Cosmos DB-anbefaling:**" og "| Azure Cosmos DB |" passerer urort');
// R2 known-positive — an in-class site the tables do not know must be REPORTED.
const hole = classifyPersonaSites('**Cosmo-hjørnet:** noe tekst\n');
if (hole.unmapped.length !== 1) no('R2 KJENT-POSITIV: ukjent etikettform ble IKKE rapportert');
else ok('R2 KJENT-POSITIV: ukjent etikettform "**Cosmo-hjørnet:**" rapportert som umaalt');
let threw = false;
try { neutralizeLabels('**Cosmo-hjørnet:** noe tekst\n'); } catch { threw = true; }
if (!threw) no('R2 KJENT-POSITIV: transformen hoppet STILLE over en ukjent form'); else ok('R2 KJENT-POSITIV: transformen KASTER paa ukjent form, hopper ikke stille over');
// R2 known-negative — the editorial classes must be reported OUT of scope and left alone.
const ed = '**Cosmo svarer:**\n| A | B | Cosmo-syntese av verified sources |\n';
const edC = classifyPersonaSites(ed);
if (edC.label.editorial !== 1 || edC.table.editorial !== 1) no('R2 KJENT-NEGATIV: redaksjonell form ikke bokfoert som utenfor scope');
else if (neutralizeLabels(ed).content !== ed) no('R2 KJENT-NEGATIV: redaksjonell form ble ENDRET');
else ok('R2 KJENT-NEGATIV: dialog + proveniens bokfoert utenfor scope OG byte-identiske');
// R3 known-positive — break one product occurrence and the product clause must fell.
const broken = classifyCosmo('| Azure Cosmo DB | 8000 |\n');
const intact = classifyCosmo('| Azure Cosmos DB | 8000 |\n');
if (!(intact.product === 1 && broken.product === 0)) no(`R3 KJENT-POSITIV: fjernet tegn i "Cosmos DB" felte IKKE produkt-differansen (${intact.product} -> ${broken.product})`);
else ok('R3 KJENT-POSITIV: fjernet tegn i "Cosmos DB" felte produkt-differansen (1 -> 0)');
// R3 known-negative — the product-only files must be byte-identical through the transform.
for (const f of productOnlyFiles(measure())) {
const src = readFileSync(join(PLUGIN_ROOT, f), 'utf8');
if (neutralizeLabels(src, { relPath: f }).content !== src) no(`R3 KJENT-NEGATIV: kun-produkt-fila ${f} ble endret`);
else ok(`R3 KJENT-NEGATIV: ${f} byte-identisk gjennom transformen`);
}
return bad;
}
// ================================ R14: G4 leveranseflaten ==================================
//
// G4 persona i leveranseflaten = 0, med derivasjonsregisteret pinnet PER FIL PAA TALL.
//
// Skopet er `git ls-files '*.md'` (tracked), ikke arbeidstreet: den udekomponerte roadmap-
// gaten talte med 1146 gitignorerte `.kb-backup/`-filer og 9 LOCAL-ONLY-filer som aldri
// leveres. Ref-korpuset trekkes fra -- G1/G3/R1/R3 eier det allerede.
/** Tracked markdown, som repo-relative stier. */
function trackedMarkdown() {
return execFileSync('git', ['ls-files', '*.md'], { cwd: PLUGIN_ROOT, encoding: 'utf8' })
.trim().split('\n').filter(Boolean);
}
const inCorpus = (f) => /^skills\/[^/]+\/references\//.test(f);
/** Leveranseflatens persona + registerets faktiske tall. */
function measureDelivery() {
let surface = 0;
const offenders = [];
const register = {};
for (const rel of trackedMarkdown()) {
if (inCorpus(rel)) continue;
const n = personaInDeliveryContent(readFileSync(join(PLUGIN_ROOT, rel), 'utf8'));
if (rel in DERIVATION_REGISTER) { register[rel] = n; continue; }
if (n > 0) { surface += n; offenders.push(`${rel} (${n})`); }
}
return { surface, offenders, register };
}
function checkDelivery(fails) {
const d = measureDelivery();
if (d.surface !== 0) {
fails.push(`G4: ${d.surface} persona-forekomst(er) i leveranseflaten: `
+ `${d.offenders.slice(0, 10).join(', ')}`);
}
// Registeret pinnes paa TALL. Et filnavn-unntak er blindt for hva fila senere kommer til
// aa inneholde; et tall-pin feller i det en ny persona-linje legges til.
for (const [rel, want] of Object.entries(DERIVATION_REGISTER)) {
const got = d.register[rel];
if (got === undefined) { fails.push(`G4: registerfila ${rel} finnes ikke lenger`); continue; }
if (got !== want) fails.push(`G4: ${rel} har ${got} persona, pinnet ${want}`);
}
const regTotal = Object.values(DERIVATION_REGISTER).reduce((a, b) => a + b, 0);
return [
`G4 persona i leveranseflaten : ${d.surface}`,
` derivasjonsregister (tall-pinnet): ${regTotal} i `
+ `${Object.keys(DERIVATION_REGISTER).length} filer`,
];
}
/** Force G4 to the other answer. */
function validateDeliveryNet() {
let bad = 0;
const ok = (m) => console.log(`ok ${m}`);
const no = (m) => { console.log(`x ${m}`); bad++; };
// Kjent-positiv: hver form persona faktisk tok i leveranseflaten.
for (const [label, text] of [
['prosa', 'Du er Cosmo Skyberg i en anskaffelsesrolle.\n'],
['heading', '# Cosmo Skyberg - Microsoft AI Solution Architect\n'],
['norsk genitiv', 'Legg til Cosmos helhetsvurdering:\n'],
['produsent-prompt', '8. For arkitekten (Cosmo) — 5-8 noekkelspoersmaal\n'],
]) {
if (personaInDeliveryContent(text) !== 1) no(`G4 KJENT-POSITIV: ${label} ble IKKE sett`);
else ok(`G4 KJENT-POSITIV: ${label} felte G4`);
}
// Kjent-negativ: produktet, inkludert den adjudiserte genitiv-formede raden.
for (const [label, text] of [
['Azure Cosmos DB', '| Azure Cosmos DB | 8000 NOK |\n'],
['CosmosClient', 'Bruk `CosmosClient` mot cosmos_ru-kvoten.\n'],
['adjudisert URL-rad',
'| Cosmos multi-region writes | learn.microsoft.com/azure/cosmos-db/distribute-data-globally |\n'],
]) {
if (personaInDeliveryContent(text) !== 0) no(`G4 KJENT-NEGATIV: ${label} feilklassifisert som persona`);
else ok(`G4 KJENT-NEGATIV: ${label} passerer urort`);
}
// URL-regelen skal ikke bli en hvitvaskingskanal: bare genitiv-formen unnskyldes.
if (personaInDeliveryContent('Cosmo anbefaler learn.microsoft.com/azure/cosmos-db/ som kilde.\n') !== 1) {
no('G4 KJENT-POSITIV: bar `Cosmo` ble hvitvasket av en cosmos-db-URL paa samme linje');
} else ok('G4 KJENT-POSITIV: bar `Cosmo` hvitvaskes IKKE av en cosmos-db-URL');
// Registeret er tall-pinnet, ikke filnavn-pinnet: en ny persona-linje i en unntatt fil
// maa flytte tallet. Maalt paa fila selv, ikke paastaatt.
const reg = Object.keys(DERIVATION_REGISTER)[0];
const src = readFileSync(join(PLUGIN_ROOT, reg), 'utf8');
const before = personaInDeliveryContent(src);
const after = personaInDeliveryContent(`${src}\nDu er Cosmo Skyberg.\n`);
if (after !== before + 1) no(`G4 KJENT-POSITIV: injisert persona i unntatt fil ${reg} flyttet ikke tallet`);
else ok(`G4 KJENT-POSITIV: injisert persona i ${reg} flyttet tallet ${before} -> ${after} (tall-pin, ikke filnavn-hopp)`);
return bad;
}
const arg = process.argv[2];
if (arg === '--emit-baseline') emitBaseline();
else if (arg === '--emit-labels-baseline') emitLabelsBaseline();
else if (arg === '--validate-net') validateNet();
else check();