#!/usr/bin/env node // okf-check.mjs // Validerer en OKF-bundle-rot. Kontrakt (de-facto OKF: kun `type` paakrevd): // - Hver konsept-fil (.md unntatt index.md) MAA ha `type:` i frontmatter. // - >= 1 fil uten type -> exit 1 + teller + navngir filene. // - 0 filer uten type -> exit 0. // Anbefalte felt (resource/title/description/timestamp) rapporteres som ADVARSEL, // ikke feil. Rotens `okf_version` ekkoes for menneskelig sammenligning mot // gjeldende standard (ingen auto-fetch — hooks/scripts er no-network; SC7 myket). // // Kjoeres PER ROT (prosjekt `.claude/okr/` + home `~/.claude/okr/org/`). // Zero npm dependencies (node:-builtins). import { readdirSync, readFileSync, existsSync } from 'node:fs'; import { join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseFrontmatter } from '../lib/frontmatter.mjs'; const RECOMMENDED = ['resource', 'title', 'description', 'timestamp']; // Alle konsept-filer (.md unntatt index.md) under root, rekursivt. function walkConcepts(root) { const out = []; const walk = (dir) => { for (const e of readdirSync(dir, { withFileTypes: true })) { const p = join(dir, e.name); if (e.isDirectory()) walk(p); else if (e.isFile() && e.name.endsWith('.md') && e.name !== 'index.md') out.push(p); } }; walk(root); return out; } // Les rotens okf_version (markdown-tekst i index.md, ikke frontmatter). null hvis fravaerende. function rootOkfVersion(root) { const idx = join(root, 'index.md'); if (!existsSync(idx)) return null; const m = readFileSync(idx, 'utf8').match(/^okf_version:\s*(.+)$/m); return m ? m[1].trim() : null; } export function checkBundle(root) { const concepts = walkConcepts(root); const missingType = []; const warnings = []; for (const f of concepts) { const { get } = parseFrontmatter(readFileSync(f, 'utf8')); const rel = relative(root, f); if (!get('type')) { missingType.push(rel); continue; } for (const field of RECOMMENDED) { if (!get(field)) warnings.push(`${rel}: mangler anbefalt felt «${field}»`); } } return { scanned: concepts.length, missingType, warnings, okfVersion: rootOkfVersion(root), }; } // --- CLI --- const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]; if (isMain) { const root = process.argv[2]; if (!root) { process.stderr.write('Bruk: node okf-check.mjs \n'); process.exit(2); } if (!existsSync(root)) { process.stderr.write(`Bundle-rot finnes ikke: ${root}\n`); process.exit(2); } const r = checkBundle(root); const out = []; out.push(`OKF-sjekk: ${root}`); out.push(` Konsept-filer skannet: ${r.scanned}`); out.push(` ${r.missingType.length} filer uten type:`); for (const f of r.missingType) out.push(` - ${f}`); out.push(` okf_version: ${r.okfVersion || 'MANGLER (rot-index uten okf_version)'}`); out.push(` Advarsler (anbefalte felt): ${r.warnings.length}`); for (const w of r.warnings) out.push(` ! ${w}`); out.push(r.missingType.length === 0 ? 'OK: gyldig OKF-bundle' : `FEIL: ${r.missingType.length} fil(er) mangler type:`); process.stdout.write(`${out.join('\n')}\n`); process.exit(r.missingType.length === 0 ? 0 : 1); }