Kjeden /okr:rapport -> /okr:sporing -> status.md var beskrevet i prosa, men manglet et mekanisk ledd: ingenting foerte en generert rad inn i status.md -- fila coaching-hook teller at-risk-KR i, /okr:moeter bygger agenda fra, og /okr:oppsett arkiver leser sluttscorer fra. Rot-aarsaken var at HELE sporings-stien var lesende (ingen Write/Edit/Bash i allowed-tools). scripts/syklus-status.mjs tar confidence som JSON paa stdin og bygger radene gjennom statusrapport() i lib/syklus-rapport.mjs -- samme kode som tertialrapporten. Aritmetikken kan derfor ikke drifte fra /okr:rapport, fordi det ikke finnes en andre implementasjon av den. En LLM skriver ikke av et tall paa veien, og en maskin utleder ikke et trafikklys: beslutning 9 staar, presist formulert som at generatoren PLASSERER en etikett den faar inn som data og UTLEDER aldri en. Haandhevet i kode, ikke i prosa: lukket vokabular (On Track/At Risk/Off Track), full dekning (en tom confidence-celle ville lest som groenn hos hver konsument), typo-fangst paa ukjente KR-navn, og kuratert-fil-vern -- en status.md uten provenans-noekkelen `kilde: sporing` er haandholdt og overskrives aldri (samme mekanikk som innboks-write bruker med `kilde: innboks`). Suite 329 -> 343. Fem vakter mutasjons-verifisert: rad-drift mot tertialrapporten, vokab, dekning (baade alene og sammen med vokab-vakten som overlapper den), og kuratert-vernet.
160 lines
6.2 KiB
JavaScript
160 lines
6.2 KiB
JavaScript
#!/usr/bin/env node
|
|
// syklus-status.mjs
|
|
// S60: siste ledd i kjeden generator -> /okr:sporing -> status.md.
|
|
//
|
|
// Bruk:
|
|
// node syklus-status.mjs <syklus-dir> < confidence.json
|
|
//
|
|
// Confidence leses fra STDIN som JSON: et oppslag fra KR-navn til etikett i det
|
|
// lukkede vokabularet (On Track | At Risk | Off Track).
|
|
//
|
|
// node scripts/syklus-status.mjs .claude/okr/syklus/T1-2026 <<'JSON'
|
|
// { "Andel digitale soeknader (prosent)": "At Risk" }
|
|
// JSON
|
|
//
|
|
// HVORFOR STDIN, og ikke flagg: KR-navn er norsk fritekst med mellomrom,
|
|
// parenteser og prosenttegn. Som `--confidence "<navn>=<etikett>"` ville hvert
|
|
// navn maattet siteres riktig gjennom et shell-lag, og en feilsitering ville
|
|
// gitt feil etikett paa riktig KR -- en stille feilrapportering. JSON har
|
|
// entydig avgrensning og feiler hoeylytt.
|
|
//
|
|
// ARBEIDSDELINGEN (laast beslutning 9): /okr:sporing eier VURDERINGEN og leverer
|
|
// den som data hit. Dette scriptet eier ARITMETIKKEN og FORMATERINGEN, og henter
|
|
// begge fra lib/syklus-rapport.mjs -- de samme radene tertialrapporten bygger.
|
|
// Ingen LLM skriver av et tall paa veien, og ingen maskin utleder et trafikklys.
|
|
//
|
|
// Exit-koder (kontrakten commands/sporing.md mapper til norsk brukertekst):
|
|
// 0 status.md er skrevet
|
|
// 1 domenefeil -- ugyldig/manglende confidence, eller kuratert fil i veien
|
|
// 2 bruksfeil -- feil aritet, katalog finnes ikke, ugyldig JSON paa stdin
|
|
//
|
|
// Klokke-soem: OKR_NOW (ISO-8601) overstyrer veggklokka, saa to kjoeringer over
|
|
// samme data gir byte-identisk fil (moenster: scripts/syklus-rapport.mjs:19).
|
|
//
|
|
// Zero npm dependencies.
|
|
|
|
import { existsSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
import { basename, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { parseFrontmatter, writeFrontmatter } from '../lib/frontmatter.mjs';
|
|
import { lesSyklus } from '../lib/syklus-data.mjs';
|
|
import { statusrapport } from '../lib/syklus-rapport.mjs';
|
|
|
|
const FILNAVN = 'status.md';
|
|
|
|
// Provenans-noekkelen kuratert-fil-vernet leser. Samme mekanikk som
|
|
// lib/innboks-write.mjs:88-91 bruker med `kilde: innboks`: en eksisterende
|
|
// maal-fil UTEN noekkelen er haandkuratert, og skrivingen avvises.
|
|
const PROVENANS = { noekkel: 'kilde', verdi: 'sporing' };
|
|
|
|
class Bruksfeil extends Error {}
|
|
|
|
// Vernet er det eneste stedet dette scriptet kan oedelegge noe for brukeren:
|
|
// status.md kan vaere skrevet for haand, og ligger i BRUKERENS syklusdata --
|
|
// ikke i repoet, altsaa ikke en commit unna. Et stille overskriv ville vaert
|
|
// datatap uten spor. Egne tidligere kjoeringer overskrives fritt: de baerer
|
|
// noekkelen, og idempotens er hele poenget med klokke-soemmen.
|
|
function krevSkrivbar(maal) {
|
|
if (!existsSync(maal)) return;
|
|
let fm;
|
|
try {
|
|
fm = parseFrontmatter(readFileSync(maal, 'utf8'));
|
|
} catch {
|
|
fm = null;
|
|
}
|
|
if (fm?.get(PROVENANS.noekkel) === PROVENANS.verdi) return;
|
|
throw new Error(
|
|
`nekter aa overskrive kuratert (ikke-generert) fil: ${maal}\n`
|
|
+ ` Fila mangler "${PROVENANS.noekkel}: ${PROVENANS.verdi}" i frontmatteren og kan vaere skrevet for haand.\n`
|
|
+ ' Flytt eller slett den hvis den skal erstattes av en generert statusrapport.',
|
|
);
|
|
}
|
|
|
|
// Atomisk skriv: temp i SAMME katalog (rename er kun atomisk innen filsystem),
|
|
// process.pid i navnet mot samtidige kjoeringer. Moenster: scripts/syklus-rapport.mjs:107.
|
|
function skrivAtomisk(maal, innhold) {
|
|
const temp = `${maal}.${process.pid}.tmp`;
|
|
try {
|
|
writeFileSync(temp, innhold, 'utf8');
|
|
renameSync(temp, maal);
|
|
} catch (e) {
|
|
if (existsSync(temp)) {
|
|
try { unlinkSync(temp); } catch { /* opprydding er best-effort */ }
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// Skrevet fil er OKF-konsept, ikke loes markdown: den lander INNE i bundlen, og
|
|
// en fil uten `type:` ville felt okf-check for hele roten. `type: Status` er
|
|
// riktig -- dette ER statusflaten, og den blir retrievbar for
|
|
// okr-second-brain-search paa kjoepet.
|
|
function komponer(syklus, naa, confidence) {
|
|
const frontmatter = writeFrontmatter({
|
|
type: 'Status',
|
|
resource: 'local',
|
|
title: `Statusrapport ${syklus.id}`,
|
|
description: 'Maskingenerert statusrapport. Confidence er satt av /okr:sporing.',
|
|
timestamp: naa,
|
|
[PROVENANS.noekkel]: PROVENANS.verdi,
|
|
});
|
|
return `${frontmatter}\n${statusrapport(syklus, { naa, confidence })}`;
|
|
}
|
|
|
|
export function skrivStatus(syklusDir, confidence, opts = {}) {
|
|
if (!syklusDir) {
|
|
throw new Bruksfeil('Bruk: node syklus-status.mjs <syklus-dir> < confidence.json');
|
|
}
|
|
if (!existsSync(syklusDir) || !statSync(syklusDir).isDirectory()) {
|
|
throw new Bruksfeil(`Syklus-katalogen finnes ikke: ${syklusDir}`);
|
|
}
|
|
|
|
const naa = opts.naa || process.env.OKR_NOW || new Date().toISOString();
|
|
const syklus = lesSyklus(syklusDir);
|
|
const maal = join(syklusDir, FILNAVN);
|
|
|
|
// Vernet foer komponeringen: en avvist skriv skal ikke koste noe, og
|
|
// rekkefolgen gjoer det umulig aa skrive foerst og oppdage vernet etterpaa.
|
|
krevSkrivbar(maal);
|
|
skrivAtomisk(maal, komponer(syklus, naa, confidence));
|
|
return { fil: maal, syklus: syklus.id, krer: syklus.okrer.reduce((n, o) => n + o.krer.length, 0) };
|
|
}
|
|
|
|
// --- CLI ---
|
|
function lesStdin() {
|
|
let raa;
|
|
try {
|
|
raa = readFileSync(0, 'utf8');
|
|
} catch {
|
|
throw new Bruksfeil('Klarte ikke aa lese confidence fra stdin. Pipe inn JSON: ... | node syklus-status.mjs <dir>');
|
|
}
|
|
if (raa.trim() === '') {
|
|
throw new Bruksfeil('Tom stdin. Forventet JSON: { "<KR-navn>": "On Track | At Risk | Off Track" }');
|
|
}
|
|
try {
|
|
return JSON.parse(raa);
|
|
} catch (e) {
|
|
throw new Bruksfeil(`Ugyldig JSON paa stdin: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
const isMain = process.argv[1]
|
|
&& fileURLToPath(import.meta.url) === process.argv[1];
|
|
if (isMain) {
|
|
const [syklusDir] = process.argv.slice(2);
|
|
try {
|
|
// Arity foer stdin: uten katalog er det ingenting aa lese confidence FOR.
|
|
if (!syklusDir) {
|
|
throw new Bruksfeil('Bruk: node syklus-status.mjs <syklus-dir> < confidence.json');
|
|
}
|
|
const r = skrivStatus(syklusDir, lesStdin());
|
|
process.stdout.write(
|
|
`Status skrevet: ${r.fil}\n Syklus: ${r.syklus}\n KR med confidence: ${r.krer}\n`,
|
|
);
|
|
process.exit(0);
|
|
} catch (e) {
|
|
process.stderr.write(`${basename(process.argv[1])}: ${e.message}\n`);
|
|
process.exit(e instanceof Bruksfeil ? 2 : 1);
|
|
}
|
|
}
|