@rafinery/cli 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/ci-setup.mjs CHANGED
@@ -46,6 +46,7 @@ jobs:
46
46
  - name: Mechanical fold (no LLM — rigor lives at main)
47
47
  env:
48
48
  RAFA_MCP_KEY: \${{ secrets.RAFA_MCP_KEY }}
49
+ RAFA_MCP_URL: \${{ secrets.RAFA_MCP_URL }} # deployed platform /api/mcp — localhost never works from CI
49
50
  RAFA_HOOKS_DISABLED: "1" # headless — the M5 sensors are session instruments
50
51
  run: npx -y @rafinery/cli fold --from "\${{ github.event.pull_request.head.ref }}" --to "\${{ github.event.pull_request.base.ref }}"
51
52
 
@@ -71,6 +72,7 @@ jobs:
71
72
  env:
72
73
  ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
73
74
  RAFA_MCP_KEY: \${{ secrets.RAFA_MCP_KEY }}
75
+ RAFA_MCP_URL: \${{ secrets.RAFA_MCP_URL }} # deployed platform /api/mcp — localhost never works from CI
74
76
  RAFA_BRAIN_TOKEN: \${{ secrets.RAFA_BRAIN_TOKEN }}
75
77
  RAFA_AGENT_SDK_DIR: \${{ runner.temp }}/rafa-sdk
76
78
  RAFA_HOOKS_DISABLED: "1" # headless — the M5 sensors are session instruments
@@ -103,12 +105,14 @@ export default async function ciSetup(args = []) {
103
105
  console.log(`
104
106
  Next steps (repo Settings → Secrets and variables → Actions):
105
107
  1. RAFA_MCP_KEY — mint on the platform (repo → Agent access); scope: this repo.
106
- 2. ANTHROPIC_API_KEY the ORG'S OWN LLM key. Used only inside YOUR CI for
108
+ 2. RAFA_MCP_URL your DEPLOYED platform's MCP endpoint (https://<host>/api/mcp).
109
+ A localhost dev URL can never work from a CI runner.
110
+ 3. ANTHROPIC_API_KEY — the ORG'S OWN LLM key. Used only inside YOUR CI for
107
111
  merge-to-main distillation; never stored on the rafinery
108
112
  platform (hard rule).
109
- 3. RAFA_BRAIN_TOKEN — a token with push access to the BRAIN repo (the distill
113
+ 4. RAFA_BRAIN_TOKEN — a token with push access to the BRAIN repo (the distill
110
114
  job pushes validated knowledge there).
111
- 4. Commit the workflow via your normal MR flow.
115
+ 5. Commit the workflow via your normal MR flow.
112
116
 
113
117
  The rigor gradient this wires: dev↔dev = CAS + session prompt · branch↔branch =
114
118
  free mechanical fold · branch→main = full distillation + gates. Cost tracks
@@ -21,91 +21,14 @@ import {
21
21
  import { join } from "node:path";
22
22
  import { execSync } from "node:child_process";
23
23
 
24
- const SCHEMA_VERSION = 1;
24
+ export const SCHEMA_VERSION = 1; // the contract version — single numeric source; the generator imports it
25
25
 
26
- // ── strict frontmatter parser (contract §0 grammar) ─────────────────────────
27
- // Legal value forms only: scalar | "quoted" | [flow,list] | { flow: map }.
28
- // Plus `key:` followed by ` - item` block lists and folded scalars. Anything
29
- // else error.
30
- function parseScalar(raw) {
31
- const s = raw.trim();
32
- if (s === "") return "";
33
- if (/^".*"$/.test(s) || /^'.*'$/.test(s)) return s.slice(1, -1);
34
- if (s === "true") return true;
35
- if (s === "false") return false;
36
- if (/^-?\d+$/.test(s)) return Number(s);
37
- return s;
38
- }
39
- // Strip a trailing YAML comment ( whitespace + # … ) from an UNQUOTED top-level
40
- // scalar. Not applied to block-list items (cites/links are exact strings that may
41
- // legitimately contain `#`).
42
- function stripComment(s) {
43
- if (/^["']/.test(s.trim())) return s;
44
- const i = s.search(/\s#/);
45
- return i === -1 ? s : s.slice(0, i);
46
- }
47
- function parseFlowList(raw) {
48
- const inner = raw.trim().slice(1, -1).trim();
49
- if (inner === "") return [];
50
- return inner.split(",").map((x) => parseScalar(x));
51
- }
52
- function parseFlowMap(raw) {
53
- const inner = raw.trim().slice(1, -1).trim();
54
- const out = {};
55
- if (inner === "") return out;
56
- for (const pair of inner.split(",")) {
57
- const i = pair.indexOf(":");
58
- if (i === -1) return null; // malformed
59
- out[pair.slice(0, i).trim()] = parseScalar(pair.slice(i + 1));
60
- }
61
- return out;
62
- }
63
- // Returns { data } or { error: "reason" }.
64
- export function parseFrontmatter(text) {
65
- if (!text.startsWith("---")) return { error: "no frontmatter block" };
66
- const end = text.indexOf("\n---", text.indexOf("\n"));
67
- if (end === -1) return { error: "unterminated frontmatter" };
68
- const lines = text.slice(text.indexOf("\n") + 1, end).split("\n");
69
- const data = {};
70
- for (let i = 0; i < lines.length; i++) {
71
- const line = lines[i];
72
- if (line.trim() === "" || line.trimStart().startsWith("#")) continue;
73
- const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*):(.*)$/);
74
- if (!m) return { error: `illegal line: ${JSON.stringify(line)}` };
75
- const key = m[1];
76
- // Strip a trailing comment first, so `cites: # note` reads as an empty value
77
- // (block list follows), not as the comment being the value.
78
- const rest = stripComment(m[2]).trim();
79
- if (rest === "") {
80
- // block list follows
81
- const items = [];
82
- while (i + 1 < lines.length && /^\s*-\s+/.test(lines[i + 1])) {
83
- items.push(parseScalar(lines[++i].replace(/^\s*-\s+/, "")));
84
- }
85
- data[key] = items;
86
- } else if (rest === ">-" || rest === ">") {
87
- // folded scalar (contract §0): indented continuation lines join with one space
88
- const parts = [];
89
- while (i + 1 < lines.length && /^\s+\S/.test(lines[i + 1])) {
90
- parts.push(lines[++i].trim());
91
- }
92
- data[key] = parts.join(" ");
93
- } else if (rest.startsWith("[")) {
94
- const close = rest.lastIndexOf("]"); // ignore any trailing comment after ]
95
- if (close === -1) return { error: `unclosed flow list at ${key}` };
96
- data[key] = parseFlowList(rest.slice(0, close + 1));
97
- } else if (rest.startsWith("{")) {
98
- const close = rest.lastIndexOf("}");
99
- if (close === -1) return { error: `unclosed flow map at ${key}` };
100
- const fm = parseFlowMap(rest.slice(0, close + 1));
101
- if (fm === null) return { error: `malformed flow map at ${key}` };
102
- data[key] = fm;
103
- } else {
104
- data[key] = parseScalar(rest);
105
- }
106
- }
107
- return { data };
108
- }
26
+ // The strict frontmatter parser (contract §0 grammar) lives in @rafinery/okf
27
+ // (2026-07-15, the OKF surface arc) so the gate, the emitter, the validator,
28
+ // and the platform all parse knowledge files identically. Re-exported here
29
+ // existing consumers keep importing it from the gate.
30
+ import { parseFrontmatter } from "@rafinery/okf";
31
+ export { parseFrontmatter };
109
32
 
110
33
  const CITE_RE = /^(.+):(\d+(?:-\d+)?)\s*::\s*(.+)$/;
111
34
  const DUTY_RE = /^(\S[^:]*?)\s+::\s+(\S+)\s+::\s+(\S.*)$/;
@@ -201,18 +124,73 @@ export function runCompile(argv = []) {
201
124
  if (d.id !== stem) fail(path, "id", `must equal filename stem "${stem}"`);
202
125
  return stem;
203
126
  }
127
+ // OKF surface fields (contract §11) — OPTIONAL everywhere, validated when
128
+ // present so the emitted bundle never carries a malformed value. `rafa okf`
129
+ // stamps them where derivable; a brain without them still compiles (additive,
130
+ // schemaVersion unchanged per §8). `typed` also validates the stamped
131
+ // `type`/`title` on singleton files (notes/improvements validate their own).
132
+ function checkOkfFields(d, path, { typed = false } = {}) {
133
+ if (typed) {
134
+ for (const key of ["type", "title"])
135
+ if (key in d && (typeof d[key] !== "string" || d[key].trim() === ""))
136
+ fail(path, key, "optional · non-empty string (OKF concept surface)");
137
+ }
138
+ if (
139
+ "description" in d &&
140
+ (typeof d.description !== "string" || d.description.trim() === "")
141
+ )
142
+ fail(path, "description", "optional · non-empty string");
143
+ if (
144
+ "timestamp" in d &&
145
+ !/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/.test(
146
+ String(d.timestamp),
147
+ )
148
+ )
149
+ fail(path, "timestamp", "optional · ISO 8601 date or datetime");
150
+ if (
151
+ "tags" in d &&
152
+ (!Array.isArray(d.tags) ||
153
+ d.tags.some((t) => typeof t !== "string" || t.trim() === ""))
154
+ )
155
+ fail(path, "tags", "optional · flow list of non-empty strings");
156
+ }
204
157
 
158
+ // index.md + log.md are OKF-reserved (contract §11): listings/trails at any
159
+ // level, never concept documents — the gate never parses them as data files.
160
+ // `*.theirs.md` checkpoint-conflict copies are excluded here and flagged as a
161
+ // TYPED error by sweepConflicts below (never a cryptic id mismatch).
162
+ const OKF_RESERVED = new Set(["index.md", "log.md"]);
205
163
  function walk(dir) {
206
164
  if (!existsSync(dir)) return [];
207
165
  return readdirSync(dir).flatMap((e) => {
208
166
  const p = join(dir, e);
209
167
  return statSync(p).isDirectory()
210
168
  ? walk(p)
211
- : e.endsWith(".md") && !e.startsWith("_")
169
+ : e.endsWith(".md") &&
170
+ !e.startsWith("_") &&
171
+ !e.endsWith(".theirs.md") &&
172
+ !OKF_RESERVED.has(e)
212
173
  ? [p]
213
174
  : [];
214
175
  });
215
176
  }
177
+ // Unresolved checkpoint conflicts BLOCK the gate with an explicit rule — a
178
+ // brain with a pending human decision must never compile past it silently.
179
+ function sweepConflicts(dir) {
180
+ if (!existsSync(dir)) return;
181
+ for (const e of readdirSync(dir)) {
182
+ const p = join(dir, e);
183
+ if (statSync(p).isDirectory()) {
184
+ if (e !== ".git") sweepConflicts(p);
185
+ } else if (e.endsWith(".theirs.md")) {
186
+ fail(
187
+ p,
188
+ "conflict",
189
+ "unresolved checkpoint conflict — decide (keep yours or adopt theirs), delete the .theirs.md copy, re-run",
190
+ );
191
+ }
192
+ }
193
+ }
216
194
  const read = (p) => readFileSync(p, "utf8");
217
195
 
218
196
  function compileNotes(kind, dir) {
@@ -235,6 +213,7 @@ export function runCompile(argv = []) {
235
213
  (typeof data[key] !== "string" || data[key].trim() === "")
236
214
  )
237
215
  fail(path, key, "optional · non-empty token (or `none`)");
216
+ checkOkfFields(data, path); // note `type`/`title` have their own required checks
238
217
  // Size stamps (additive optional): body cost + per-cite target-file cost. A
239
218
  // target we can't read → the cite simply carries no `targetTokens` (omitted).
240
219
  const cites = parseCites(data.cites, path);
@@ -277,6 +256,7 @@ export function runCompile(argv = []) {
277
256
  }
278
257
  checkVersion(data, path);
279
258
  const id = checkId(data, path, name);
259
+ checkOkfFields(data, path, { typed: true }); // stamped `type: Improvement` etc.
280
260
  const lev = data.leverage;
281
261
  if (
282
262
  typeof lev !== "object" ||
@@ -336,6 +316,7 @@ export function runCompile(argv = []) {
336
316
  return null;
337
317
  }
338
318
  checkVersion(data, path);
319
+ checkOkfFields(data, path, { typed: true }); // stamped OKF surface (§11)
339
320
  const g = data.gates,
340
321
  c = data.counts;
341
322
  if (
@@ -375,6 +356,7 @@ export function runCompile(argv = []) {
375
356
  return null;
376
357
  }
377
358
  checkVersion(data, path);
359
+ checkOkfFields(data, path, { typed: true }); // stamped OKF surface (§11)
378
360
  const bp = data.by_priority;
379
361
  const okBp =
380
362
  bp && ["P0", "P1", "P2", "P3"].every((k) => typeof bp[k] === "number");
@@ -400,6 +382,7 @@ export function runCompile(argv = []) {
400
382
  return null;
401
383
  }
402
384
  checkVersion(data, path);
385
+ checkOkfFields(data, path, { typed: true }); // stamped OKF surface (§11)
403
386
  const d = data.domains;
404
387
  if (typeof d !== "object" || Array.isArray(d)) {
405
388
  fail(path, "domains", "required · { domain: status } flow map");
@@ -489,6 +472,7 @@ export function runCompile(argv = []) {
489
472
  }
490
473
  checkVersion(data, path);
491
474
  const id = checkId(data, path, name);
475
+ checkOkfFields(data, path, { typed: true }); // stamped `type: Plan Epic|Task|Subtask` etc.
492
476
  if (seen.has(id))
493
477
  fail(
494
478
  path,
@@ -779,6 +763,64 @@ export function runCompile(argv = []) {
779
763
  return count;
780
764
  }
781
765
 
766
+ // Shipped SOPs are contract-governed like agent cards (§10/§11 — local gate,
767
+ // never in the manifest): the rafa-* skills the blueprint vendors and the
768
+ // conductor command. A workforce whose procedures don't parse doesn't ship.
769
+ function compileShippedSops() {
770
+ const repoRoot = join(ROOT, "..");
771
+ let count = 0;
772
+ const skillsDir = join(repoRoot, ".claude", "skills");
773
+ if (existsSync(skillsDir))
774
+ for (const e of readdirSync(skillsDir)) {
775
+ if (!e.startsWith("rafa-")) continue; // the dev's own skills are theirs
776
+ const path = join(skillsDir, e, "SKILL.md");
777
+ if (!existsSync(path)) continue;
778
+ const { data, error } = parseFrontmatter(read(path));
779
+ if (error) {
780
+ fail(path, "frontmatter", error);
781
+ continue;
782
+ }
783
+ count++;
784
+ if (data.name !== e)
785
+ fail(path, "name", `must equal skill directory "${e}"`);
786
+ reqStr(data, "description", path);
787
+ }
788
+ const conductor = join(repoRoot, ".claude", "commands", "rafa.md");
789
+ if (existsSync(conductor)) {
790
+ const { data, error } = parseFrontmatter(read(conductor));
791
+ if (error) fail(conductor, "frontmatter", error);
792
+ else {
793
+ count++;
794
+ if (!/^\d+\.\d+\.\d+$/.test(String(data.version ?? "")))
795
+ fail(conductor, "version", "required · semver MAJOR.MINOR.PATCH");
796
+ reqStr(data, "description", conductor);
797
+ }
798
+ }
799
+ return count;
800
+ }
801
+
802
+ // sage's learnings (.claude/rafa/learnings/*.md) — the same self-describing
803
+ // protocol outside the bundle (§11), mechanically gated: OKF quartet + stable
804
+ // id. ledger.md there is sage's generated index (skipped like a listing).
805
+ function compileLearnings() {
806
+ const dir = join(ROOT, "..", ".claude", "rafa", "learnings");
807
+ let count = 0;
808
+ for (const path of walk(dir)) {
809
+ const name = path.split("/").pop();
810
+ if (name === "ledger.md") continue;
811
+ const { data, error } = parseFrontmatter(read(path));
812
+ if (error) {
813
+ fail(path, "frontmatter", error);
814
+ continue;
815
+ }
816
+ count++;
817
+ checkId(data, path, name);
818
+ for (const key of ["type", "title", "description"])
819
+ reqStr(data, key, path);
820
+ }
821
+ return count;
822
+ }
823
+
782
824
  // ── assemble ─────────────────────────────────────────────────────────────
783
825
  const gitOpts = { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }; // no stderr leak
784
826
  function gitSha() {
@@ -810,6 +852,9 @@ export function runCompile(argv = []) {
810
852
  const plans = compilePlans();
811
853
  const activePlanId = compileActivePlanId(plans);
812
854
  const agentCards = compileAgentCards();
855
+ const shippedSops = compileShippedSops();
856
+ const learnings = compileLearnings();
857
+ sweepConflicts(ROOT);
813
858
 
814
859
  // Cross-check: ledger.open / by_priority must match the actual open improvements.
815
860
  if (ledger) {
@@ -865,7 +910,7 @@ export function runCompile(argv = []) {
865
910
  `${plans.length} plans validated${activePlanId ? ` (active: ${activePlanId})` : ""} (plans travel via the plans channel, not the manifest) · ` +
866
911
  `health ${health ? "ok" : "—"} · ledger ${ledger ? "ok" : "—"} · ` +
867
912
  `coverage ${coverage ? `${coverage.domains.length} domains` : "—"} · ` +
868
- `${agentCards} agent cards (local gate) → ${ROOT}/manifest.json`,
913
+ `${agentCards} agent cards + ${shippedSops} SOPs${learnings ? ` + ${learnings} learnings` : ""} (local gate) → ${ROOT}/manifest.json`,
869
914
  );
870
915
  return 0;
871
916
  }
@@ -0,0 +1,323 @@
1
+ // The OKF surface emitter — makes the brain repo a conformant Open Knowledge
2
+ // Format v0.1 bundle (contract §11). Runs inside `rafa push` BEFORE compile, so
3
+ // everything it writes goes back through the gate like any authored file.
4
+ //
5
+ // The format machinery lives in @rafinery/okf (parser · concept primitives ·
6
+ // links · citations · indexes · validator · generator); this module is the thin
7
+ // orchestrator that applies the rafa PROFILE to a bundle:
8
+ //
9
+ // stamp · missing `type`/`title`/`description`/`timestamp`/`tags` where
10
+ // DERIVABLE from authored data or git history — authored values are
11
+ // never rewritten; a value we cannot derive is OMITTED, never guessed.
12
+ // render · a generated `# Citations` body section from the `cites:` DSL
13
+ // (sha-pinned GitHub URLs when the repo is known), inside markers so
14
+ // re-emits replace their own section and never touch prose.
15
+ // links · `[[wikilink]]` → `[Title](/brain/rules/<id>.md)` bundle-relative
16
+ // markdown links, ONLY when the id resolves (a dangling wikilink is
17
+ // not-yet-written knowledge — left for the checker's WARN lane).
18
+ // index · an `index.md` at the bundle root and in every concept directory
19
+ // (OKF §6) so a foreign agent reaches any concept from the root in
20
+ // ≤ 2 hops with no rafa tooling. Root index frontmatter carries
21
+ // `okf_version: "0.1"` + provenance (repo · codeSha) — the only
22
+ // index frontmatter OKF permits.
23
+ //
24
+ // Never touched: log.md (OKF-reserved trail), active.md (one-line pointer
25
+ // protocol — documented exception, surfaced by `rafa okf check`), citation-
26
+ // check.* (the checker's artifact), contract.md (push stamps it), plans/**
27
+ // (work objects — the plans channel).
28
+ //
29
+ // CLI: rafa okf [--root=.rafa] [--repo=owner/repo] [--codeSha=<sha>]
30
+
31
+ import {
32
+ readFileSync,
33
+ writeFileSync,
34
+ readdirSync,
35
+ existsSync,
36
+ statSync,
37
+ } from "node:fs";
38
+ import { join } from "node:path";
39
+ import { execSync } from "node:child_process";
40
+ import {
41
+ parseFrontmatter,
42
+ OKF_VERSION,
43
+ OKF_RESERVED,
44
+ splitConcept,
45
+ joinConcept,
46
+ missingStamps,
47
+ transpileWikilinks,
48
+ parseCites,
49
+ citationsSection,
50
+ upsertCitations,
51
+ indexBullet,
52
+ renderIndex,
53
+ renderRootIndex,
54
+ FILE_CLASSES,
55
+ derivedStamps,
56
+ } from "@rafinery/okf";
57
+
58
+ export function runOkfEmit(argv = []) {
59
+ const arg = (k, d) => {
60
+ const m = argv.find((a) => a.startsWith(`--${k}=`));
61
+ return m ? m.slice(k.length + 3) : d;
62
+ };
63
+ const ROOT = arg("root", ".rafa");
64
+ const gitOpts = (cwd) => ({
65
+ encoding: "utf8",
66
+ cwd,
67
+ stdio: ["ignore", "pipe", "ignore"],
68
+ });
69
+ const codeGit = (cmd, d = "") => {
70
+ try {
71
+ return execSync(cmd, gitOpts(process.cwd())).trim();
72
+ } catch {
73
+ return d;
74
+ }
75
+ };
76
+ const repo =
77
+ arg("repo", "") ||
78
+ (() => {
79
+ const origin = codeGit("git remote get-url origin");
80
+ const m = origin.match(/[/:]([^/:]+)\/([^/]+?)(?:\.git)?\/?$/);
81
+ return m ? `${m[1]}/${m[2]}` : "";
82
+ })();
83
+ const codeSha = arg("codeSha", "") || codeGit("git rev-parse HEAD");
84
+ const now = new Date().toISOString();
85
+
86
+ // Last meaningful change of a bundle file — real signals only (contract §11):
87
+ // dirty/untracked in the brain repo → it is changing in THIS emit → now;
88
+ // clean → its last commit time; no git at all → now (the file is being
89
+ // materialized into a fresh bundle). Never a placeholder.
90
+ function fileTimestamp(relPath) {
91
+ try {
92
+ const dirty = execSync(
93
+ `git status --porcelain -- ${JSON.stringify(relPath)}`,
94
+ gitOpts(ROOT),
95
+ ).trim();
96
+ if (dirty) return now;
97
+ const t = execSync(
98
+ `git log -1 --format=%cI -- ${JSON.stringify(relPath)}`,
99
+ gitOpts(ROOT),
100
+ ).trim();
101
+ return t || now;
102
+ } catch {
103
+ return now;
104
+ }
105
+ }
106
+
107
+ function walk(dir) {
108
+ if (!existsSync(dir)) return [];
109
+ return readdirSync(dir)
110
+ .sort()
111
+ .flatMap((e) => {
112
+ const p = join(dir, e);
113
+ return statSync(p).isDirectory()
114
+ ? walk(p)
115
+ : e.endsWith(".md") &&
116
+ !e.startsWith("_") &&
117
+ !e.endsWith(".theirs.md") && // pending human decision — compile gates it
118
+ !OKF_RESERVED.has(e)
119
+ ? [p]
120
+ : [];
121
+ });
122
+ }
123
+ const rel = (p) => p.slice(ROOT.length + 1);
124
+ const read = (p) => readFileSync(p, "utf8");
125
+
126
+ // ── collect the bundle's concept files (per the rafa profile registry) ────
127
+ const concepts = [];
128
+ const collect = (paths, kind) => {
129
+ for (const path of paths) {
130
+ const raw = read(path);
131
+ const split = splitConcept(raw);
132
+ const parsed = split ? parseFrontmatter(raw) : { error: "no frontmatter" };
133
+ if (!split || parsed.error) {
134
+ console.log(` ! ${rel(path)} — ${parsed.error} (skipped; compile will fail it)`);
135
+ continue;
136
+ }
137
+ concepts.push({ path, kind, raw, ...split, data: parsed.data });
138
+ }
139
+ };
140
+ for (const [kind, cls] of Object.entries(FILE_CLASSES)) {
141
+ if (cls.singleton) {
142
+ const p = join(ROOT, cls.path);
143
+ if (existsSync(p)) collect([p], kind);
144
+ } else {
145
+ collect(walk(join(ROOT, cls.dir)), kind);
146
+ }
147
+ }
148
+
149
+ // id → { title, bundlePath } for wikilink transpilation.
150
+ const resolve = new Map();
151
+ for (const c of concepts) {
152
+ if (!c.data.id) continue;
153
+ resolve.set(String(c.data.id), {
154
+ title: c.data.title ?? c.data.id,
155
+ bundlePath: "/" + rel(c.path),
156
+ });
157
+ }
158
+
159
+ // ── stamp + transform every concept ───────────────────────────────────────
160
+ let stamped = 0;
161
+ let transpiled = 0;
162
+ let citesSections = 0;
163
+ for (const c of concepts) {
164
+ const derived = derivedStamps(c.kind, c.data);
165
+ // timestamp: authored created/found verbatim → brain-repo git history → now.
166
+ if (!("timestamp" in c.data))
167
+ derived.timestamp = c.data.created ?? c.data.found ?? fileTimestamp(rel(c.path));
168
+ const stamps = missingStamps(c.data, derived);
169
+ let body = c.body;
170
+ const before = body;
171
+ body = transpileWikilinks(body, resolve);
172
+ if (body !== before) transpiled++;
173
+ const cites = parseCites(c.data.cites);
174
+ if (cites.length) {
175
+ body = upsertCitations(body, citationsSection(cites, { repo, sha: codeSha }));
176
+ citesSections++;
177
+ }
178
+ const next = joinConcept(c.fmLines, stamps, body);
179
+ if (next !== c.raw) {
180
+ writeFileSync(c.path, next);
181
+ if (stamps.length) stamped++;
182
+ }
183
+ }
184
+
185
+ // ── the index.md tree (OKF §6 progressive disclosure) ─────────────────────
186
+ const byKind = (k) => concepts.filter((c) => c.kind === k);
187
+ const meta = (c) => {
188
+ // post-stamp values: authored wins, then what emit just derived
189
+ const { data } = parseFrontmatter(read(c.path));
190
+ return {
191
+ title: data.title ?? data.id ?? rel(c.path),
192
+ description: data.description ?? data.summary ?? "",
193
+ name: c.path.split("/").pop(),
194
+ };
195
+ };
196
+ const bullets = (list) =>
197
+ list.map(meta).map((m) => indexBullet(m.title, m.name, m.description));
198
+
199
+ const indexes = [];
200
+ const writeIndex = (relPath, content) => {
201
+ writeFileSync(join(ROOT, relPath), content);
202
+ indexes.push(relPath);
203
+ };
204
+
205
+ for (const [kind, cls] of Object.entries(FILE_CLASSES)) {
206
+ if (cls.singleton || !cls.indexTitle) continue;
207
+ const list = byKind(kind);
208
+ if (!list.length) continue;
209
+ writeIndex(
210
+ join(cls.dir, "index.md"),
211
+ renderIndex([{ heading: cls.indexTitle, bullets: bullets(list) }]),
212
+ );
213
+ }
214
+
215
+ // Plans index — one section per epic, items listed beneath it (work items
216
+ // nest in subdirectories, so URLs are relative to plans/).
217
+ const planItems = byKind("plan");
218
+ if (planItems.length) {
219
+ const planUrl = (c) => rel(c.path).replace(/^plans\//, "");
220
+ writeIndex(
221
+ "plans/index.md",
222
+ renderIndex(
223
+ planItems
224
+ .filter((c) => c.data.kind === "epic")
225
+ .map((epic) => ({
226
+ heading: `${epic.data.title ?? epic.data.id} (${epic.data.id})`,
227
+ bullets: planItems
228
+ .filter((c) => c.data.plan === epic.data.id)
229
+ .map((c) => {
230
+ const m = meta(c);
231
+ return indexBullet(m.title, planUrl(c), m.description);
232
+ }),
233
+ })),
234
+ ),
235
+ );
236
+ }
237
+
238
+ const singleton = (kind, url) => {
239
+ const c = byKind(kind)[0];
240
+ return c ? indexBullet(meta(c).title, url, meta(c).description) : null;
241
+ };
242
+ if (existsSync(join(ROOT, "brain"))) {
243
+ const rules = byKind("rule");
244
+ const playbooks = byKind("playbook");
245
+ writeIndex(
246
+ "brain/index.md",
247
+ renderIndex([
248
+ {
249
+ heading: "Knowledge",
250
+ bullets: [
251
+ rules.length &&
252
+ indexBullet("Rules", "rules/", `${rules.length} cited invariants/contracts`),
253
+ playbooks.length &&
254
+ indexBullet("Playbooks", "playbooks/", `${playbooks.length} cited how-to/flow notes`),
255
+ ],
256
+ },
257
+ {
258
+ heading: "Reports",
259
+ bullets: [
260
+ singleton("coverage", "coverage.md"),
261
+ singleton("health", "checklist.md"),
262
+ existsSync(join(ROOT, "brain", "citation-check.md")) &&
263
+ indexBullet("Citation check", "citation-check.md", "generated gate report — every cite re-verified"),
264
+ existsSync(join(ROOT, "brain", "log.md")) &&
265
+ indexBullet("Scan log", "log.md", "chronological trail of scans and validations"),
266
+ ],
267
+ },
268
+ ]),
269
+ );
270
+ }
271
+ if (existsSync(join(ROOT, "improve"))) {
272
+ const improvements = byKind("improvement");
273
+ writeIndex(
274
+ "improve/index.md",
275
+ renderIndex([
276
+ {
277
+ heading: "Continuous improvement",
278
+ bullets: [
279
+ improvements.length &&
280
+ indexBullet("Improvements", "improvements/", `${improvements.length} items, leverage-ranked`),
281
+ singleton("ledger", "ledger.md"),
282
+ ],
283
+ },
284
+ ]),
285
+ );
286
+ }
287
+
288
+ // Root index — the bundle's front door (frontmatter: okf_version + provenance).
289
+ writeIndex(
290
+ "index.md",
291
+ renderRootIndex({
292
+ title: `${repo || "rafa brain"} — verified knowledge bundle`,
293
+ intro: [
294
+ `A rafa brain: every concept is cited into the code${codeSha ? ` at \`${codeSha.slice(0, 12)}\`` : ""} and`,
295
+ "mechanically re-verified on every push (cite resolution · anchor completeness ·",
296
+ `absence re-grep · coverage inventory). Conformant Open Knowledge Format v${OKF_VERSION} —`,
297
+ "any OKF consumer can read it; the verification layer is rafa's.",
298
+ ],
299
+ provenance: { ...(repo ? { repo } : {}), ...(codeSha ? { codeSha } : {}) },
300
+ sections: [
301
+ {
302
+ heading: "Contents",
303
+ bullets: [
304
+ existsSync(join(ROOT, "brain")) &&
305
+ indexBullet("Brain", "brain/", "the knowledge map — cited rules + playbooks, coverage, health"),
306
+ existsSync(join(ROOT, "improve")) &&
307
+ indexBullet("Improve", "improve/", "bloom's prioritized improvement ledger (P0–P3)"),
308
+ planItems.length > 0 &&
309
+ indexBullet("Plans", "plans/", "work-item trees (epic → task → subtask), per-epic index"),
310
+ existsSync(join(ROOT, "contract.md")) &&
311
+ indexBullet("Contract", "contract.md", "the file protocol this bundle conforms to (stamped copy)"),
312
+ ],
313
+ },
314
+ ],
315
+ }),
316
+ );
317
+
318
+ console.log(
319
+ `✓ rafa okf: ${concepts.length} concepts · ${stamped} stamped · ${transpiled} bodies transpiled · ` +
320
+ `${citesSections} citations sections · ${indexes.length} indexes → ${ROOT} (OKF v${OKF_VERSION} bundle)`,
321
+ );
322
+ return 0;
323
+ }