#!/usr/bin/env node // syklus-status.mjs // S60: siste ledd i kjeden generator -> /okr:sporing -> status.md. // // Bruk: // node syklus-status.mjs < 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 "="` 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 < 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 '); } if (raa.trim() === '') { throw new Bruksfeil('Tom stdin. Forventet JSON: { "": "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 < 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); } }