feat(okr): deterministisk skriveledd for status.md (1.10.0)
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.
This commit is contained in:
parent
285d18ebfa
commit
3a0cbcfbf9
13 changed files with 618 additions and 50 deletions
160
scripts/syklus-status.mjs
Normal file
160
scripts/syklus-status.mjs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
#!/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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue