@rafinery/cli 0.7.1 → 0.8.1
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/CHANGELOG.md +69 -0
- package/LICENSE +21 -0
- package/bin/rafa.mjs +18 -3
- package/blueprint/.claude/agents/atlas.md +4 -3
- package/blueprint/.claude/agents/bloom.md +2 -1
- package/blueprint/.claude/agents/compass.md +2 -1
- package/blueprint/.claude/agents/prism.md +2 -1
- package/blueprint/.claude/agents/sage.md +2 -1
- package/blueprint/.claude/commands/rafa.md +4 -0
- package/blueprint/.claude/rafa/contract.md +86 -4
- package/blueprint/.claude/skills/rafa-build/SKILL.md +2 -1
- package/blueprint/.claude/skills/rafa-distill/SKILL.md +2 -1
- package/blueprint/.claude/skills/rafa-improve/SKILL.md +1 -1
- package/blueprint/.claude/skills/rafa-okf/SKILL.md +101 -0
- package/blueprint/.claude/skills/rafa-plan/SKILL.md +1 -0
- package/blueprint/.claude/skills/rafa-scan/SKILL.md +12 -7
- package/blueprint/.claude/skills/rafa-validate/SKILL.md +2 -1
- package/lib/blueprint.mjs +1 -0
- package/lib/ci-setup.mjs +7 -3
- package/lib/gate/compile.mjs +131 -86
- package/lib/gate/okf-emit.mjs +335 -0
- package/lib/gate/verify-citations.mjs +89 -3
- package/lib/mcp-client.mjs +43 -16
- package/lib/okf.mjs +120 -0
- package/lib/push.mjs +33 -17
- package/lib/releases.mjs +48 -0
- package/package.json +10 -7
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.
|
|
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
|
-
|
|
113
|
+
4. RAFA_BRAIN_TOKEN — a token with push access to the BRAIN repo (the distill
|
|
110
114
|
job pushes validated knowledge there).
|
|
111
|
-
|
|
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
|
package/lib/gate/compile.mjs
CHANGED
|
@@ -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
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
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") &&
|
|
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,335 @@
|
|
|
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
|
+
// Nothing to materialize is ABSENCE, not success: write no scaffold over an
|
|
150
|
+
// empty root (wrong --root, or a lazy .rafa before pull/scan). Standalone
|
|
151
|
+
// `rafa okf` exits 1 on this; push logs it and continues — a lazy instance
|
|
152
|
+
// is a legitimate state there, and the gates behind it stay honest.
|
|
153
|
+
if (concepts.length === 0) {
|
|
154
|
+
console.log(
|
|
155
|
+
`! rafa okf: no concepts under ${ROOT} — nothing to materialize ` +
|
|
156
|
+
`(brain-repo clone → --root=. · code repo → .rafa after \`rafa pull --full\` or a scan)`,
|
|
157
|
+
);
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// id → { title, bundlePath } for wikilink transpilation.
|
|
162
|
+
const resolve = new Map();
|
|
163
|
+
for (const c of concepts) {
|
|
164
|
+
if (!c.data.id) continue;
|
|
165
|
+
resolve.set(String(c.data.id), {
|
|
166
|
+
title: c.data.title ?? c.data.id,
|
|
167
|
+
bundlePath: "/" + rel(c.path),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── stamp + transform every concept ───────────────────────────────────────
|
|
172
|
+
let stamped = 0;
|
|
173
|
+
let transpiled = 0;
|
|
174
|
+
let citesSections = 0;
|
|
175
|
+
for (const c of concepts) {
|
|
176
|
+
const derived = derivedStamps(c.kind, c.data);
|
|
177
|
+
// timestamp: authored created/found verbatim → brain-repo git history → now.
|
|
178
|
+
if (!("timestamp" in c.data))
|
|
179
|
+
derived.timestamp = c.data.created ?? c.data.found ?? fileTimestamp(rel(c.path));
|
|
180
|
+
const stamps = missingStamps(c.data, derived);
|
|
181
|
+
let body = c.body;
|
|
182
|
+
const before = body;
|
|
183
|
+
body = transpileWikilinks(body, resolve);
|
|
184
|
+
if (body !== before) transpiled++;
|
|
185
|
+
const cites = parseCites(c.data.cites);
|
|
186
|
+
if (cites.length) {
|
|
187
|
+
body = upsertCitations(body, citationsSection(cites, { repo, sha: codeSha }));
|
|
188
|
+
citesSections++;
|
|
189
|
+
}
|
|
190
|
+
const next = joinConcept(c.fmLines, stamps, body);
|
|
191
|
+
if (next !== c.raw) {
|
|
192
|
+
writeFileSync(c.path, next);
|
|
193
|
+
if (stamps.length) stamped++;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── the index.md tree (OKF §6 progressive disclosure) ─────────────────────
|
|
198
|
+
const byKind = (k) => concepts.filter((c) => c.kind === k);
|
|
199
|
+
const meta = (c) => {
|
|
200
|
+
// post-stamp values: authored wins, then what emit just derived
|
|
201
|
+
const { data } = parseFrontmatter(read(c.path));
|
|
202
|
+
return {
|
|
203
|
+
title: data.title ?? data.id ?? rel(c.path),
|
|
204
|
+
description: data.description ?? data.summary ?? "",
|
|
205
|
+
name: c.path.split("/").pop(),
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
const bullets = (list) =>
|
|
209
|
+
list.map(meta).map((m) => indexBullet(m.title, m.name, m.description));
|
|
210
|
+
|
|
211
|
+
const indexes = [];
|
|
212
|
+
const writeIndex = (relPath, content) => {
|
|
213
|
+
writeFileSync(join(ROOT, relPath), content);
|
|
214
|
+
indexes.push(relPath);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
for (const [kind, cls] of Object.entries(FILE_CLASSES)) {
|
|
218
|
+
if (cls.singleton || !cls.indexTitle) continue;
|
|
219
|
+
const list = byKind(kind);
|
|
220
|
+
if (!list.length) continue;
|
|
221
|
+
writeIndex(
|
|
222
|
+
join(cls.dir, "index.md"),
|
|
223
|
+
renderIndex([{ heading: cls.indexTitle, bullets: bullets(list) }]),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Plans index — one section per epic, items listed beneath it (work items
|
|
228
|
+
// nest in subdirectories, so URLs are relative to plans/).
|
|
229
|
+
const planItems = byKind("plan");
|
|
230
|
+
if (planItems.length) {
|
|
231
|
+
const planUrl = (c) => rel(c.path).replace(/^plans\//, "");
|
|
232
|
+
writeIndex(
|
|
233
|
+
"plans/index.md",
|
|
234
|
+
renderIndex(
|
|
235
|
+
planItems
|
|
236
|
+
.filter((c) => c.data.kind === "epic")
|
|
237
|
+
.map((epic) => ({
|
|
238
|
+
heading: `${epic.data.title ?? epic.data.id} (${epic.data.id})`,
|
|
239
|
+
bullets: planItems
|
|
240
|
+
.filter((c) => c.data.plan === epic.data.id)
|
|
241
|
+
.map((c) => {
|
|
242
|
+
const m = meta(c);
|
|
243
|
+
return indexBullet(m.title, planUrl(c), m.description);
|
|
244
|
+
}),
|
|
245
|
+
})),
|
|
246
|
+
),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const singleton = (kind, url) => {
|
|
251
|
+
const c = byKind(kind)[0];
|
|
252
|
+
return c ? indexBullet(meta(c).title, url, meta(c).description) : null;
|
|
253
|
+
};
|
|
254
|
+
if (existsSync(join(ROOT, "brain"))) {
|
|
255
|
+
const rules = byKind("rule");
|
|
256
|
+
const playbooks = byKind("playbook");
|
|
257
|
+
writeIndex(
|
|
258
|
+
"brain/index.md",
|
|
259
|
+
renderIndex([
|
|
260
|
+
{
|
|
261
|
+
heading: "Knowledge",
|
|
262
|
+
bullets: [
|
|
263
|
+
rules.length &&
|
|
264
|
+
indexBullet("Rules", "rules/", `${rules.length} cited invariants/contracts`),
|
|
265
|
+
playbooks.length &&
|
|
266
|
+
indexBullet("Playbooks", "playbooks/", `${playbooks.length} cited how-to/flow notes`),
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
heading: "Reports",
|
|
271
|
+
bullets: [
|
|
272
|
+
singleton("coverage", "coverage.md"),
|
|
273
|
+
singleton("health", "checklist.md"),
|
|
274
|
+
existsSync(join(ROOT, "brain", "citation-check.md")) &&
|
|
275
|
+
indexBullet("Citation check", "citation-check.md", "generated gate report — every cite re-verified"),
|
|
276
|
+
existsSync(join(ROOT, "brain", "log.md")) &&
|
|
277
|
+
indexBullet("Scan log", "log.md", "chronological trail of scans and validations"),
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
]),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
if (existsSync(join(ROOT, "improve"))) {
|
|
284
|
+
const improvements = byKind("improvement");
|
|
285
|
+
writeIndex(
|
|
286
|
+
"improve/index.md",
|
|
287
|
+
renderIndex([
|
|
288
|
+
{
|
|
289
|
+
heading: "Continuous improvement",
|
|
290
|
+
bullets: [
|
|
291
|
+
improvements.length &&
|
|
292
|
+
indexBullet("Improvements", "improvements/", `${improvements.length} items, leverage-ranked`),
|
|
293
|
+
singleton("ledger", "ledger.md"),
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
]),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Root index — the bundle's front door (frontmatter: okf_version + provenance).
|
|
301
|
+
writeIndex(
|
|
302
|
+
"index.md",
|
|
303
|
+
renderRootIndex({
|
|
304
|
+
title: `${repo || "rafa brain"} — verified knowledge bundle`,
|
|
305
|
+
intro: [
|
|
306
|
+
`A rafa brain: every concept is cited into the code${codeSha ? ` at \`${codeSha.slice(0, 12)}\`` : ""} and`,
|
|
307
|
+
"mechanically re-verified on every push (cite resolution · anchor completeness ·",
|
|
308
|
+
`absence re-grep · coverage inventory). Conformant Open Knowledge Format v${OKF_VERSION} —`,
|
|
309
|
+
"any OKF consumer can read it; the verification layer is rafa's.",
|
|
310
|
+
],
|
|
311
|
+
provenance: { ...(repo ? { repo } : {}), ...(codeSha ? { codeSha } : {}) },
|
|
312
|
+
sections: [
|
|
313
|
+
{
|
|
314
|
+
heading: "Contents",
|
|
315
|
+
bullets: [
|
|
316
|
+
existsSync(join(ROOT, "brain")) &&
|
|
317
|
+
indexBullet("Brain", "brain/", "the knowledge map — cited rules + playbooks, coverage, health"),
|
|
318
|
+
existsSync(join(ROOT, "improve")) &&
|
|
319
|
+
indexBullet("Improve", "improve/", "bloom's prioritized improvement ledger (P0–P3)"),
|
|
320
|
+
planItems.length > 0 &&
|
|
321
|
+
indexBullet("Plans", "plans/", "work-item trees (epic → task → subtask), per-epic index"),
|
|
322
|
+
existsSync(join(ROOT, "contract.md")) &&
|
|
323
|
+
indexBullet("Contract", "contract.md", "the file protocol this bundle conforms to (stamped copy)"),
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
console.log(
|
|
331
|
+
`✓ rafa okf: ${concepts.length} concepts · ${stamped} stamped · ${transpiled} bodies transpiled · ` +
|
|
332
|
+
`${citesSections} citations sections · ${indexes.length} indexes → ${ROOT} (OKF v${OKF_VERSION} bundle)`,
|
|
333
|
+
);
|
|
334
|
+
return 0;
|
|
335
|
+
}
|