- backup.mjs restore(relPaths): SCOPED per-fil-rollback erstatter hel-tre rmSync(srcDir)+cpSync. Ruller kun tilbake kjøringens egne skriv; parallelle økters skriv til søsken-filer overlever; intet destruktivt rm→cp-vindu. Nyskapte filer slettes; prior bytes gjenopprettes atomisk. - migrate-corpus.mjs: returnert restore() er nå en null-arg closure bundet til kjøringens skrive- liste (public API uendret) → scoped. detectStaleRollback aborterer ved forrige krasj-sentinel; cleanupOldBackups pruner backups forbi retention etter vellykket batch. - 5 drivere (backfill-status/-category, dedup-plain-header, relabel-dato/-dialect): writeFileSync → atomicWriteSync (crash-safe tmp+rename) + recovery-kontrakt i header. - backfill-category.mjs: refaktorert importerbar (isMain-guard + run()/planCategoryBackfill/ categoryForFile/FOLDER_CATEGORY-eksporter, parameterisert root) → testbar uten scan-side-effekt. - Tester (+16): 6 scoped-restore (parallel-preservering, ny-fil-sletting, throws-guard) erstatter 2 hel-tre; test-backfill-category (frosset taxonomi + hermetisk plan); test-driver-atomic-writes (5 drivere); migrate stale-abort + cleanup-wiring. Suite 859→875 exit 0. validate-plugin 250/0.
198 lines
11 KiB
JavaScript
198 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
||
// Enhet 1 (decision-b dialect pass, path A, ⊥ R7) — pure value-preserving relabel of the two
|
||
// Norwegian header labels to their English equivalents:
|
||
// **Sist oppdatert:** → **Last updated:** (31 files)
|
||
// **Kategori:** → **Category:** (42 files) [22 files carry both → 73 relabels]
|
||
//
|
||
// This is a LABEL rename only: the value (date, category text, "(v1.0)" suffix) is byte-preserved,
|
||
// so no fact is ever fabricated — this is the safe core of the corrected decision-b residual.
|
||
//
|
||
// Scope is the header block (lines above the first `---` thematic break OR first `## ` heading),
|
||
// so a `**Kategori:**`/`**Sist oppdatert:**` inside a BODY template block is never touched. The
|
||
// **Dato:** dialect (16 files) is DELIBERATELY excluded: it also appears in body templates
|
||
// (`**Dato:** [YYYY-MM-DD]`) and needs a header-scoped ISO-date guard — deferred to Unit 2.
|
||
// Status backfill (27+4) and the ai-act dual-block dedup are Unit 3; decision-trees date is Unit 4.
|
||
//
|
||
// Safety: a frozen MANIFEST (ground-truth-verified 2026-07-06) + a hard per-file invariant asserted
|
||
// BEFORE any write (exactly `applied.length` lines changed, each equals the old line with only the
|
||
// label token swapped, everything else byte-identical). Idempotent: a file with no norsk header
|
||
// label is skipped, so a re-run is a no-op. Aborts and writes nothing on any drift or invariant breach.
|
||
//
|
||
// Recovery contract: writes are crash-safe (atomicWriteSync tmp+rename — a reader sees the old
|
||
// file or the new one, never a partial); an interrupted run is recovered by re-running (idempotent).
|
||
//
|
||
// Usage: node scripts/kb-update/relabel-dialect.mjs [--dry]
|
||
import { readFileSync, existsSync, realpathSync } from 'node:fs';
|
||
import { join, dirname } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { atomicWriteSync } from './lib/atomic-write.mjs';
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const PLUGIN_ROOT = join(__dirname, '..', '..');
|
||
|
||
// The two norsk→english label pairs. **Dato:** is intentionally absent (body-template trap).
|
||
export const LABEL_MAP = [
|
||
['**Sist oppdatert:**', '**Last updated:**'],
|
||
['**Kategori:**', '**Category:**'],
|
||
];
|
||
|
||
// The header block ends at the first thematic break (`---`) or the first `## ` section heading.
|
||
// Everything below is body (templates, prose) and is out of relabel scope.
|
||
function headerEnd(lines) {
|
||
for (let i = 0; i < lines.length; i++) {
|
||
if (/^---\s*$/.test(lines[i]) || /^##\s/.test(lines[i])) return i;
|
||
}
|
||
return lines.length;
|
||
}
|
||
|
||
/**
|
||
* Relabel the norsk header labels to English, in the header block only. Pure.
|
||
* Guards: a norsk label appearing >1× in the header block, or an English target label already
|
||
* present in the header block (would duplicate), abort. A norsk label only in the body is ignored.
|
||
* @param {string} content
|
||
* @param {[string,string][]} [labelMap]
|
||
* @returns {{content: string, applied: {from:string,to:string,line:number}[]}}
|
||
*/
|
||
export function relabelHeaderDialect(content, labelMap = LABEL_MAP) {
|
||
const lines = String(content ?? '').split('\n');
|
||
const end = headerEnd(lines);
|
||
const header = lines.slice(0, end);
|
||
const applied = [];
|
||
for (const [from, to] of labelMap) {
|
||
let occ = 0;
|
||
let idx = -1;
|
||
for (let i = 0; i < header.length; i++) {
|
||
const c = header[i].split(from).length - 1;
|
||
if (c > 0) { occ += c; idx = i; }
|
||
}
|
||
if (occ === 0) continue; // norsk label absent from header → nothing to do
|
||
if (occ > 1) throw new Error(`ABORT: '${from}' appears ${occ}× in header block`);
|
||
if (header.some((l) => l.includes(to))) throw new Error(`ABORT: target '${to}' already present in header (would duplicate)`);
|
||
header[idx] = header[idx].replace(from, to);
|
||
applied.push({ from, to, line: idx });
|
||
}
|
||
return { content: [...header, ...lines.slice(end)].join('\n'), applied };
|
||
}
|
||
|
||
// Frozen manifest — the 51 scope-confirmed targets (files carrying **Sist oppdatert:** and/or
|
||
// **Kategori:** in their header block), verified against ground truth 2026-07-06. The per-file
|
||
// invariant below re-proves each write; the manifest only bounds WHICH files may be touched.
|
||
export const MANIFEST = [
|
||
'skills/ms-ai-advisor/references/architecture/ai-utredning-template.md',
|
||
'skills/ms-ai-advisor/references/architecture/alternativanalyse-methodology.md',
|
||
'skills/ms-ai-advisor/references/architecture/capacity-feasibility-benchmarks.md',
|
||
'skills/ms-ai-advisor/references/architecture/diagram-prompt-templates.md',
|
||
'skills/ms-ai-advisor/references/architecture/regional-availability-verification.md',
|
||
'skills/ms-ai-advisor/references/architecture/source-traceability-assumption-register.md',
|
||
'skills/ms-ai-engineering/references/mlops-genaiops/cost-optimization-mlops-pipelines.md',
|
||
'skills/ms-ai-engineering/references/mlops-genaiops/feedback-loops-continuous-improvement.md',
|
||
'skills/ms-ai-engineering/references/mlops-genaiops/genaiops-llm-specific-practices.md',
|
||
'skills/ms-ai-engineering/references/mlops-genaiops/governance-audit-ml-operations.md',
|
||
'skills/ms-ai-engineering/references/mlops-genaiops/inferencing-optimization-caching.md',
|
||
'skills/ms-ai-engineering/references/mlops-genaiops/infrastructure-as-code-mlops.md',
|
||
'skills/ms-ai-engineering/references/mlops-genaiops/llm-evaluation-production.md',
|
||
'skills/ms-ai-engineering/references/mlops-genaiops/mlops-security-access-control.md',
|
||
'skills/ms-ai-engineering/references/mlops-genaiops/mlops-teams-collaboration-tools.md',
|
||
'skills/ms-ai-engineering/references/mlops-genaiops/monitoring-observability-ml-systems.md',
|
||
'skills/ms-ai-engineering/references/mlops-genaiops/prompt-flow-production-deployment.md',
|
||
'skills/ms-ai-engineering/references/mlops-genaiops/responsible-ai-mlops-integration.md',
|
||
'skills/ms-ai-governance/references/monitoring-observability/anomaly-detection-ai-systems.md',
|
||
'skills/ms-ai-governance/references/monitoring-observability/application-insights-llm-monitoring.md',
|
||
'skills/ms-ai-governance/references/monitoring-observability/azure-monitor-setup-ai-workloads.md',
|
||
'skills/ms-ai-governance/references/monitoring-observability/custom-dashboards-ai-operations.md',
|
||
'skills/ms-ai-governance/references/monitoring-observability/distributed-tracing-ai-pipelines.md',
|
||
'skills/ms-ai-governance/references/monitoring-observability/log-analytics-kql-ai-queries.md',
|
||
'skills/ms-ai-governance/references/monitoring-observability/token-usage-tracking-attribution.md',
|
||
'skills/ms-ai-governance/references/norwegian-public-sector-governance/gevinstrealisering-dfo-methodology.md',
|
||
'skills/ms-ai-governance/references/norwegian-public-sector-governance/norwegian-nlp-benchmarks.md',
|
||
'skills/ms-ai-governance/references/norwegian-public-sector-governance/ros-ai-threat-library.md',
|
||
'skills/ms-ai-governance/references/norwegian-public-sector-governance/ros-dpia-security-integration.md',
|
||
'skills/ms-ai-governance/references/norwegian-public-sector-governance/ros-maestro-multiagent.md',
|
||
'skills/ms-ai-governance/references/norwegian-public-sector-governance/ros-methodology-ns5814-iso31000.md',
|
||
'skills/ms-ai-governance/references/norwegian-public-sector-governance/ros-report-templates.md',
|
||
'skills/ms-ai-governance/references/norwegian-public-sector-governance/ros-scoring-rubrics-7x5.md',
|
||
'skills/ms-ai-governance/references/norwegian-public-sector-governance/ros-sector-checklists.md',
|
||
'skills/ms-ai-governance/references/norwegian-public-sector-governance/samfunnsokonomisk-analyse-nnv.md',
|
||
'skills/ms-ai-governance/references/responsible-ai/ai-act-annex-iii-checklist.md',
|
||
'skills/ms-ai-governance/references/responsible-ai/ai-center-of-excellence-setup.md',
|
||
'skills/ms-ai-governance/references/responsible-ai/ai-governance-structure-framework.md',
|
||
'skills/ms-ai-governance/references/responsible-ai/red-teaming-ai-models.md',
|
||
'skills/ms-ai-security/references/ai-security-engineering/adversarial-input-robustness-testing.md',
|
||
'skills/ms-ai-security/references/ai-security-engineering/ai-prompt-shield-network.md',
|
||
'skills/ms-ai-security/references/ai-security-engineering/ai-red-team-operations-practical.md',
|
||
'skills/ms-ai-security/references/ai-security-engineering/data-leakage-prevention-ai.md',
|
||
'skills/ms-ai-security/references/ai-security-engineering/entra-agent-id-zero-trust.md',
|
||
'skills/ms-ai-security/references/ai-security-engineering/model-fingerprinting-watermarking.md',
|
||
'skills/ms-ai-security/references/ai-security-engineering/secure-model-deployment-hardening.md',
|
||
'skills/ms-ai-security/references/ai-security-engineering/security-copilot-integration.md',
|
||
'skills/ms-ai-security/references/ai-security-engineering/security-scoring-rubrics-6x5.md',
|
||
'skills/ms-ai-security/references/ai-security-engineering/supply-chain-security-ai-models.md',
|
||
'skills/ms-ai-security/references/ai-security-engineering/zero-trust-ai-services.md',
|
||
'skills/ms-ai-security/references/cost-optimization/deterministic-cost-calculation-model.md',
|
||
];
|
||
|
||
function run({ dry }) {
|
||
const planned = [];
|
||
const missing = [];
|
||
let dateRelabels = 0;
|
||
let catRelabels = 0;
|
||
|
||
for (const rel of MANIFEST) {
|
||
const abs = join(PLUGIN_ROOT, rel);
|
||
if (!existsSync(abs)) { missing.push(rel); continue; }
|
||
const old = readFileSync(abs, 'utf8');
|
||
const { content: out, applied } = relabelHeaderDialect(old);
|
||
if (applied.length === 0) continue; // idempotent — already relabeled (re-run)
|
||
|
||
// Hard invariant: exactly `applied.length` lines changed, each is the old line with ONLY
|
||
// the label token swapped; line count unchanged; everything else byte-identical.
|
||
const o = old.split('\n');
|
||
const n = out.split('\n');
|
||
if (n.length !== o.length) throw new Error(`ABORT ${rel}: line count changed (${o.length}→${n.length})`);
|
||
let diffs = 0;
|
||
for (let i = 0; i < o.length; i++) {
|
||
if (o[i] === n[i]) continue;
|
||
diffs++;
|
||
const hit = applied.find((a) => a.line === i);
|
||
if (!hit) throw new Error(`ABORT ${rel}: line ${i} changed but not in applied set`);
|
||
if (n[i].replace(hit.to, hit.from) !== o[i]) throw new Error(`ABORT ${rel}: line ${i} changed beyond the label token`);
|
||
}
|
||
if (diffs !== applied.length) throw new Error(`ABORT ${rel}: ${diffs} lines changed ≠ ${applied.length} applied`);
|
||
|
||
for (const a of applied) {
|
||
if (a.from === '**Sist oppdatert:**') dateRelabels++;
|
||
else if (a.from === '**Kategori:**') catRelabels++;
|
||
}
|
||
planned.push({ rel, applied, out });
|
||
}
|
||
|
||
if (missing.length) {
|
||
console.error(`ABORT — ${missing.length} manifest target(s) not found (corpus drift):`);
|
||
missing.forEach((m) => console.error(' ' + m));
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log(`Manifest: ${MANIFEST.length} | files to relabel: ${planned.length} | already-english (skipped): ${MANIFEST.length - planned.length}`);
|
||
console.log(` ${dateRelabels} **Sist oppdatert:** → **Last updated:**`);
|
||
console.log(` ${catRelabels} **Kategori:** → **Category:**`);
|
||
console.log(` ${dateRelabels + catRelabels} total relabels`);
|
||
|
||
if (dry) {
|
||
console.log('\n(dry run — no writes)');
|
||
for (const p of planned) {
|
||
console.log(` ~ ${p.rel} [${p.applied.map((a) => a.from.replace(/\*/g, '')).join(', ')}]`);
|
||
}
|
||
return;
|
||
}
|
||
for (const p of planned) atomicWriteSync(join(PLUGIN_ROOT, p.rel), p.out);
|
||
console.log(`\nWrote ${planned.length} files.`);
|
||
}
|
||
|
||
const isMain = (() => {
|
||
try {
|
||
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
||
} catch {
|
||
return false;
|
||
}
|
||
})();
|
||
if (isMain) run({ dry: process.argv.includes('--dry') });
|