feat(okr): onboarding skriver OKF-frontmatter paa org-profil (SC6)

compose-org-profile.mjs prepender top-level OKF-noekler (type:
Organisasjonsprofil/resource/timestamp) over den nestede org-profilen i
ETT frontmatter-blokk, pipes til write-org-profile.mjs (uendret). Kun de
tre trygge noeklene top-level; FORBID nye top-level navn/id/fase/domene/
sektor som ville skygge nestede verdier hooken leser. oppsett.md (begge
profil-skriv-sites) + template oppdatert. Test: compose->pip->readback +
inject-roundtrip (4), full suite 51->55.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFW5scLL7oEwWWTv1fPQG6
This commit is contained in:
Kjell Tore Guttormsen 2026-06-26 17:19:14 +02:00
commit 5219d50d4b
4 changed files with 233 additions and 9 deletions

View file

@ -0,0 +1,65 @@
#!/usr/bin/env node
// compose-org-profile.mjs
// Purpose: wrap the NESTED org-profile YAML body (organisasjon:/program: ...,
// read from stdin) in an OKF-compatible frontmatter document, prepending the
// three top-level OKF keys the second-brain "Documents/kb" layout expects on a
// concept file: type / resource / timestamp. Emits the full document to stdout,
// meant to be piped into write-org-profile.mjs (which writes it atomically to
// the reinstall-surviving home path ~/.claude/okr/org/profil.md).
//
// Why a dedicated composer (not lib/frontmatter.mjs writeFrontmatter): the org
// profile is NESTED (organisasjon: { navn, type }, program: { ... }). The flat
// writeFrontmatter would flatten/destroy that structure. We instead keep the
// caller's nested YAML verbatim and only PREPEND OKF keys above it.
//
// Why ONLY type/resource/timestamp top-level: the home profile is read by the
// flat parser (lib/frontmatter.mjs) in inject-okr-context.mjs via
// get('navn')/get('id')/get('domene')/... A NEW top-level key colliding with
// any of those (navn/name/id/fase/domene/sektor) would be matched FIRST and
// shadow the nested value, breaking org/cycle resolution (the Critical risk).
// type/resource/timestamp are read by NO consumer, so they are safe to prepend.
// The nested organisasjon.type ("offentlig") is untouched and still unread.
//
// Single-block invariant: the OKF keys and the nested body MUST live in ONE
// frontmatter block. A stray intermediate '---' would truncate the block the
// flat parser reads (dropping organisasjon: -> get('navn') === null -> no
// injection), so any fences the caller already put around the body are stripped
// before re-wrapping.
//
// Testable clock seam: OKR_NOW (ISO-8601) overrides the wall clock so the
// emitted timestamp is deterministic under test.
//
// Zero npm dependencies (node: builtins only). ASCII-clean identifiers.
import { readFileSync } from 'node:fs';
// OKF resource pointer: where the canonical org profile lives. A bare string
// pointer (OKF: "URL/path to the source"); no consumer reads it.
const RESOURCE = '~/.claude/okr/org/profil.md';
let body = '';
try {
body = readFileSync(0, 'utf8');
} catch {
body = '';
}
// Strip a leading/trailing frontmatter fence if the caller already wrapped the
// body, so the result is exactly one frontmatter block (see single-block note).
body = body.replace(/^\s*---\s*\r?\n/, '');
body = body.replace(/\r?\n---\s*\r?\n?\s*$/, '\n');
body = body.replace(/\s+$/, '');
const timestamp = process.env.OKR_NOW || new Date().toISOString();
const lines = [
'---',
'type: Organisasjonsprofil',
`resource: ${RESOURCE}`,
`timestamp: '${timestamp}'`,
];
if (body) lines.push(body);
lines.push('---', '');
process.stdout.write(lines.join('\n'));