feat(okr): rapport-kommando og CLI for syklusgeneratorene
This commit is contained in:
parent
50933d839d
commit
7d1de8049d
6 changed files with 298 additions and 0 deletions
117
scripts/syklus-rapport.mjs
Normal file
117
scripts/syklus-rapport.mjs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
#!/usr/bin/env node
|
||||
// syklus-rapport.mjs
|
||||
// D5 steg 10: orkestrator for syklus-generatorene. Leser en syklus-katalog,
|
||||
// bygger den forespurte rapportformen og persisterer den atomisk.
|
||||
//
|
||||
// Bruk:
|
||||
// node syklus-rapport.mjs <syklus-dir> <form>
|
||||
//
|
||||
// Former: tertial | arsrapport | etatsstyring
|
||||
// EN kommandoflate med argument, ikke tre kommandoer -- samme moenster som
|
||||
// /okr:oppsett full|mvp|arkiver|oppdater|vis. Bare `tertial` er implementert
|
||||
// i D5; arsrapport og etatsstyring kommer i steg 11-12 og svarer inntil da
|
||||
// med exit 2 og en forklarende melding, aldri stum feil.
|
||||
//
|
||||
// Exit-koder (kontrakten commands/rapport.md mapper til norsk brukertekst):
|
||||
// 0 rapporten er skrevet
|
||||
// 1 domenefeil -- syklusdataene er ufullstendige eller ikke rapporterbare
|
||||
// 2 bruksfeil -- feil aritet, ukjent/uimplementert form, katalog finnes ikke
|
||||
//
|
||||
// Klokke-soem: OKR_NOW (ISO-8601) overstyrer veggklokka, saa to kjoeringer over
|
||||
// samme data gir byte-identisk fil (moenster: scripts/compose-org-profile.mjs:61).
|
||||
//
|
||||
// Zero npm dependencies.
|
||||
|
||||
import { existsSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
||||
import { basename, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { writeFrontmatter } from '../lib/frontmatter.mjs';
|
||||
import { lesSyklus } from '../lib/syklus-data.mjs';
|
||||
import { tertialrapport } from '../lib/syklus-rapport.mjs';
|
||||
|
||||
const FORMER = {
|
||||
tertial: {
|
||||
filnavn: 'rapport-tertial.md',
|
||||
tittel: (id) => `Tertialrapport ${id}`,
|
||||
beskrivelse: 'Maskingenerert tertialrapport med committed og aspirational holdt fra hverandre.',
|
||||
bygg: tertialrapport,
|
||||
},
|
||||
// Steg 11-12. Oppfoert her, ikke skjult, saa flaten er synlig foer den virker.
|
||||
arsrapport: null,
|
||||
etatsstyring: null,
|
||||
};
|
||||
|
||||
class Bruksfeil extends Error {}
|
||||
|
||||
// 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
|
||||
// dessuten riktig -- rapporten ER en statusflate, og blir retrievbar for
|
||||
// okr-second-brain-search paa kjoepet.
|
||||
// Filnavnet starter bevisst IKKE med `okr-`: ellers ville neste lesSyklus()
|
||||
// plukket rapporten opp som en OKR-definisjon.
|
||||
function komponer(form, syklus, naa) {
|
||||
const frontmatter = writeFrontmatter({
|
||||
type: 'Status',
|
||||
resource: 'local',
|
||||
title: form.tittel(syklus.id),
|
||||
description: form.beskrivelse,
|
||||
timestamp: naa,
|
||||
});
|
||||
return `${frontmatter}\n${form.bygg(syklus, { naa })}`;
|
||||
}
|
||||
|
||||
// Atomisk skriv: temp i SAMME katalog (rename er kun atomisk innen filsystem),
|
||||
// process.pid i navnet mot samtidige kjoeringer. Moenster: lib/innboks-write.mjs.
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export function genererRapport(syklusDir, formNavn, opts = {}) {
|
||||
if (!syklusDir || !formNavn) {
|
||||
throw new Bruksfeil('Bruk: node syklus-rapport.mjs <syklus-dir> <form>\n'
|
||||
+ `Former: ${Object.keys(FORMER).join(' | ')}`);
|
||||
}
|
||||
if (!(formNavn in FORMER)) {
|
||||
throw new Bruksfeil(`Ukjent form: ${formNavn}. Former: ${Object.keys(FORMER).join(' | ')}`);
|
||||
}
|
||||
const form = FORMER[formNavn];
|
||||
if (form === null) {
|
||||
throw new Bruksfeil(`Rapportformen «${formNavn}» er ikke implementert ennaa.`);
|
||||
}
|
||||
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, form.filnavn);
|
||||
skrivAtomisk(maal, komponer(form, syklus, naa));
|
||||
return { fil: maal, syklus: syklus.id, okrer: syklus.okrer.length };
|
||||
}
|
||||
|
||||
// --- CLI ---
|
||||
const isMain = process.argv[1]
|
||||
&& fileURLToPath(import.meta.url) === process.argv[1];
|
||||
if (isMain) {
|
||||
const [syklusDir, form] = process.argv.slice(2);
|
||||
try {
|
||||
const r = genererRapport(syklusDir, form);
|
||||
process.stdout.write(
|
||||
`Rapport skrevet: ${r.fil}\n Syklus: ${r.syklus}\n OKR rapportert: ${r.okrer}\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