@rafinery/cli 0.8.14 → 0.8.15

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/bin/rafa.mjs CHANGED
@@ -135,10 +135,12 @@ Commands:
135
135
  exercises the scaffold from fixture counts; --help documents the
136
136
  agent-driven run procedure. Measured — never the estimated baseline.
137
137
  review Brain-grounded review pre-pass (deterministic, zero LLM): the
138
- diff's changed lines ∩ the brain's cite graph → the exact rules/
139
- improvements the change touches (.rafa/review.json + summary).
140
- The rafa-review SOP's judge then rules on precisely that list —
141
- generic "review this PR" never happens. Advisory; never blocks.
138
+ diff's changed lines ∩ the brain's cite graph → everything the
139
+ change touches rules AND playbooks (flow-still-true), open
140
+ improvements, stale-cite risk flags, open knowledge gaps,
141
+ related plans (.rafa/review.json + summary). The rafa-review
142
+ SOP's judge rules on precisely that list — generic "review
143
+ this PR" never happens. Advisory; never blocks.
142
144
  migrate Bring this repo's structured files (plans, config) up to the schema
143
145
  the installed CLI ships. Idempotent; review the diff after.
144
146
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: rafa-review
3
- description: "rafa SOP — the brain-grounded review gate: `rafa review` computes the exact rules/improvements a diff touches (cite-intersection, zero LLM); a prism-style judge rules ONLY on that list + the active task's Done-check; findings feed atlas; the verdict emits a review-verdict loop event. Loaded before push/merge or on request."
3
+ description: "rafa SOP — the brain-grounded review gate: `rafa review` computes exactly what a diff touches (cite-intersection, zero LLM): rules AND playbooks, open improvements, stale-cite risk, open gaps, related plans' decisions; a prism-style judge rules ONLY on that list + the active task's Done-check; findings feed atlas; the verdict emits a review-verdict loop event. Loaded before push/merge or on request."
4
4
  ---
5
5
 
6
6
  # review — the brain-grounded gate (harness-arc wave 2)
@@ -16,34 +16,97 @@ split:
16
16
  ## Pass 1 — mechanical (the CLI, zero LLM)
17
17
 
18
18
  `rafa review [--json]` — diff vs merge-base(origin default) → changed line
19
- ranges ∩ the brain's cite graph (`get_code_context` per file):
19
+ ranges ∩ the brain's cite graph (`get_code_context` per file), BOTH note kinds:
20
20
 
21
- - **◆ direct** — a changed line falls inside a rule's cited anchor range: the
22
- code the rule stands on MOVED. Highest priority; also a staleness signal
21
+ - **◆ direct** — a changed line falls inside a note's cited anchor range: the
22
+ code the note stands on MOVED. Highest priority; also a staleness signal
23
23
  (the note may need a scoped refresh — the dirty queue already tracks it).
24
- - **· file** — the file changed elsewhere but the rule governs it.
24
+ - **· file** — the file changed elsewhere but the note governs it.
25
+ - **⚠ stale cites** — the freshness sweep already flagged this note's anchors
26
+ as drifted: a RISK marker (verify the note against code before relying on
27
+ it in judgment — it may describe a world that no longer exists).
25
28
  - **! open improvements** citing a touched file — is this change making the
26
29
  debt better, worse, or is it the fix itself (then bloom's close flow)?
30
+ - **open knowledge gaps** (top-missed queries) — live demand near the change.
27
31
 
28
32
  Output: `.rafa/review.json` + summary. A bounded pass SAYS what it dropped
29
33
  (never a silent cap); a mid-pass transport failure marks the list PARTIAL.
30
34
 
35
+ ## Token economy — BINDING (the gate must cost less than the bugs it catches)
36
+
37
+ The judge's spend is proportional to the DIFF, never to the brain. Owner rule
38
+ (2026-07-24): review checks only the diff; hydrated fields serve the judgment;
39
+ querying files wholesale is a waste. Concretely:
40
+
41
+ 1. **The pre-pass is free AND selective** — zero LLM; every note carries a
42
+ deterministic RELEVANCE score (direct-hit +3 · failure:silent +2 · cite
43
+ anchor token appears in the diff's added lines +2 · title/summary keyword
44
+ overlap +1 each, capped) and an **`engage` tier**:
45
+ - `deep` — body-worthy: hydrate/fetch and judge fully;
46
+ - `triage` — summary-only: rule in/out from metadata + diff, fetch a body
47
+ ONLY if that makes a violation plausible;
48
+ - `skip` — governs the file but nothing in the change relates: NEVER
49
+ judged, never fetched — listed in review.json so nothing drops silently.
50
+ (Semantic relevance stays contract-reserved via `matchKind` — keywords +
51
+ citations are the v1 scorer; vectors arrive on evidence, never blindly.)
52
+ 2. **Honor the tiers.** The judge never upgrades a `skip`, and upgrades a
53
+ `triage` to a body fetch only with a stated reason (the finding cites it).
54
+ 3. **Local-first, never re-serve.** A note already hydrated under
55
+ `.rafa/brain/**` (plan/build recall put it there) is read FROM DISK —
56
+ no MCP round-trip, no double-serving. Fetch via `get_rule`/`get_playbook`
57
+ only for shortlist notes absent locally; `rafa hydrate <kind> <id>` them
58
+ so a re-judge after atlas's fix re-reads the disk copy (disposable-cache
59
+ semantics keep them out of commits).
60
+ **Frontmatter-first (the OKF leverage):** every file type has a strict
61
+ format (contract §11), so the MACHINE fields — `cites` · `anchor` ·
62
+ `failure` · `links` · `type` — parse without touching the prose body.
63
+ Judge from frontmatter + diff first; read the body only when they can't
64
+ settle it. A `contract` note's check is half-mechanical before any prose:
65
+ does the diff touch its `anchor` token anywhere without updating every
66
+ cited site (the B2 completeness question)?
67
+ 4. **Deep-judge cap: 10 notes**, leverage-ranked (◆ direct > failure:silent >
68
+ ⚠ stale-and-direct > · file). Anything beyond the cap is ruled on metadata
69
+ only and the verdict SAYS SO ("N notes judged shallow") — an honest bound,
70
+ never a silent one.
71
+ 5. **Metadata-only lanes stay metadata-only**: gaps, decisions, relatedPlans,
72
+ improvements are judged from what the pre-pass/`get_plan` already carries —
73
+ no body fetches, no extra searches. NEVER load the whole brain, never
74
+ fetch a note the pre-pass didn't flag.
75
+
31
76
  ## Pass 2 — the judge (prism-style, scoped)
32
77
 
33
- For the work list ONLY — never the whole diff, never taste:
78
+ For the work list ONLY — never the whole diff, never taste. Each data kind has
79
+ its OWN question:
34
80
 
35
- 1. Per **rule hit**: fetch the rule (`get_rule`, with `branch:` for the
36
- working-set overlay) → does the diff still honor it? Judge against code +
37
- rule, never against the author's claims. A `failure: silent` rule gets
38
- extra scrutiny that class never announces itself.
39
- 2. Per **open improvement**: touched for better or worse? Fixed-in-passing →
81
+ 1. Per **rule hit** the OKF `type` names the violation shape:
82
+ - `convention` → does the new code FOLLOW the local pattern the rule names?
83
+ - `contract` the cross-file class: does the diff change any occurrence of
84
+ the rule's `anchor` without updating EVERY cited site (B2 completeness —
85
+ half-mechanical from frontmatter, see the economy rules)? `failure:
86
+ silent` contracts get the deepest scrutiny — that class never announces
87
+ itself.
88
+ Judge against code + rule, never against the author's claims.
89
+ 2. Per **playbook hit** (`flow` / `how-to` types): is the documented
90
+ FLOW/how-to still TRUE after this change (steps, ordering, file routes)?
91
+ A diff that silently invalidates a playbook ships a lie to the next dev —
92
+ that's an ITERATE finding (or a working-set edit updating the playbook,
93
+ authored the branch way).
94
+ 3. Per **⚠ stale-cite note**: verify the note against current code BEFORE
95
+ using it in judgment; if the diff is the thing that staled it further,
96
+ surface the scoped-refresh offer.
97
+ 4. Per **open improvement**: touched for better or worse? Fixed-in-passing →
40
98
  bloom's close flow (hydrate first, `status: fixed`, report).
41
- 3. **The active task's Done-check** (from `get_active_plan`): does the diff
99
+ 5. **Related plans' decisions** (`get_plan` for each `relatedPlans` entry):
100
+ does the diff contradict a recorded decision? Contradicting one is the
101
+ owner's call to reopen — an ITERATE finding, never silently overridden.
102
+ 6. **Open gaps**: does this change ANSWER one (author the note, close the
103
+ gap) — or deepen it?
104
+ 7. **The active task's Done-check** (from `get_active_plan`): does the diff
42
105
  satisfy what the plan said done means?
43
- 4. Verdict: **PASS** or **ITERATE** with per-finding citations (rule id +
106
+ 8. Verdict: **PASS** or **ITERATE** with per-finding citations (note id +
44
107
  file:line). Findings feed atlas (validate-and-correct — atlas fixes, the
45
108
  judge re-checks; the reviewer never edits).
46
- 5. Emit **`report_loop_event(category: "review-verdict", outcome:
109
+ 9. Emit **`report_loop_event(category: "review-verdict", outcome:
47
110
  PASS|ITERATE, subject: <task id or branch>)`** at the ruling — sage's
48
111
  evidence, monotonic, never a session-end sweep.
49
112
 
package/lib/releases.mjs CHANGED
@@ -264,19 +264,40 @@ export const RELEASES = [
264
264
  "first branch checkout (bounded; a failure never breaks the checkout). " +
265
265
  "`rafa update` re-vendors the post-checkout hook.",
266
266
  },
267
+ {
268
+ version: "0.8.15",
269
+ contract: 1,
270
+ plans: 2,
271
+ requires: "update",
272
+ summary:
273
+ "THE REVIEW GATE + ZERO-LEAK MIRRORS. `rafa review`: deterministic brain-" +
274
+ "grounded review pre-pass — diff's changed lines ∩ the cite graph → rules AND " +
275
+ "playbooks + open improvements + stale-cite risk + open gaps, each note " +
276
+ "relevance-scored (OKF type-aware: contract class weighs heavier) into " +
277
+ "deep/triage/skip engage tiers so the judge's tokens scale with the DIFF, " +
278
+ "never the brain (rafa-review SOP: local-first + frontmatter-first reads, " +
279
+ "10-note deep cap, skips listed never judged). LOCAL-STATE EXCLUSION: the " +
280
+ "post-commit hook now ensures .rafa/.gitignore AND untracks previously " +
281
+ "tracked machine state (hydration.json · sensor queues · demo benchmark · " +
282
+ "distill staging · *.theirs.md) — the next mirror commit cleans the branch. " +
283
+ "Plus wave-2 SOPs: delta briefing + decision recall at plan time, branch-" +
284
+ "aware recall (tier-labeled), compass sitback, gap lifecycle beats.",
285
+ },
267
286
  {
268
287
  version: "0.8.14",
269
288
  contract: 1,
270
289
  plans: 2,
271
290
  requires: "update",
272
291
  summary:
273
- "LOCAL-STATE EXCLUSION (zero-leak mirrors): the hydration sidecar + sensor " +
274
- "queues (hydration.json · dirty/reflex/sensor-errors.jsonl · benchmark.demo.json " +
275
- "· distill staging · *.theirs.md) are machine state, never knowledge — the " +
276
- "post-commit brain hook now ENSURES the .rafa/.gitignore entries AND untracks " +
277
- "anything an earlier window let git track (gitignore alone never untracks), so " +
278
- "the next mirror commit cleans the branch. ensureBrainRepo's ignore list gains " +
279
- "sensor-errors.jsonl + benchmark.demo.json. `rafa update` re-vendors the hook.",
292
+ "THE PROVENANCE SPINE (published 2026-07-24). Branch manifest: `rafa manifest` " +
293
+ "(zero-command the post-commit brain hook regenerates it per mirrored commit): " +
294
+ "lenient snapshot with mode:branch · base.brainSha · per-note provenance " +
295
+ "{baseBrainSha, commits[], tasks[]} the reconciler's three-way handoff doc. " +
296
+ "rafa-commit skill: subjects carry the active task id ([task-id] type: subject) " +
297
+ "and the manifest lifts it into per-note task lineage. Deterministic loop " +
298
+ "events: push gates emit gate-result from real exit codes; reflex --consume " +
299
+ "emits reflex-outcome. Benchmark is workflow-woven (scan's Prove-it step + " +
300
+ "session-start absence sensor) — never a dev-typed command.",
280
301
  },
281
302
  {
282
303
  version: "0.8.13",
package/lib/review.mjs CHANGED
@@ -1,10 +1,12 @@
1
1
  // rafa review — the brain-grounded review gate's DETERMINISTIC pre-pass
2
2
  // (harness-arc wave 2). Zero LLM here: the diff's changed line ranges are
3
3
  // intersected with the brain's cite graph (get_code_context — file → the
4
- // rules/playbooks/improvements that know about it), producing the exact,
5
- // scoped work list the rafa-review SOP's judge then rules on. Generic
6
- // "review this PR" never happens — the judge only ever sees THESE rules
7
- // against THESE lines.
4
+ // RULES *and* PLAYBOOKS and improvements that know about it), producing the
5
+ // exact, scoped work list the rafa-review SOP's judge then rules on. Generic
6
+ // "review this PR" never happens — the judge only ever sees THESE notes
7
+ // against THESE lines. Also surfaced: per-note stale-cite RISK flags (the
8
+ // freshness sweep's signal) and the repo's open knowledge gaps — the judge
9
+ // weighs whether this change answers or worsens one.
8
10
  //
9
11
  // rafa review [--base <ref>] [--json]
10
12
  //
@@ -71,6 +73,50 @@ export function citeHits(citeLine, ranges) {
71
73
  return ranges.some((r) => a <= r.end && b >= r.start);
72
74
  }
73
75
 
76
+ // Identifier tokens from the diff's ADDED lines, per file — the relevance
77
+ // vocabulary a note must intersect to earn the judge's attention.
78
+ export function addedTokens(diffText) {
79
+ const byFile = new Map();
80
+ let current = null;
81
+ for (const line of diffText.split("\n")) {
82
+ const f = line.match(/^\+\+\+ b\/(.+)$/);
83
+ if (f) {
84
+ current = f[1];
85
+ if (!byFile.has(current)) byFile.set(current, new Set());
86
+ continue;
87
+ }
88
+ if (current && line.startsWith("+") && !line.startsWith("+++"))
89
+ for (const t of line.slice(1).toLowerCase().match(/[a-z_][a-z0-9_-]{2,}/g) ?? [])
90
+ byFile.get(current).add(t);
91
+ }
92
+ return byFile;
93
+ }
94
+
95
+ // Deterministic relevance (owner 2026-07-24: never hydrate/judge blindly —
96
+ // keywords + citations decide whether a note ADDS VALUE to this diff; the OKF
97
+ // type registry is leveraged: every type has a format, so every type has a
98
+ // DEFINED violation shape and a deterministic weight):
99
+ // direct anchor hit +3 (the cited code itself moved)
100
+ // type: contract +2 (the cross-file class — breaks span files)
101
+ // failure: silent +2 (the class that never announces itself)
102
+ // cite anchor token in diff +2 (the rule's own token appears in new code)
103
+ // title/summary keyword +1 each, capped at +3
104
+ // → engage: "deep" (≥3: body-worthy) · "triage" (1–2: summary-only) ·
105
+ // "skip" (0: governs the file but nothing relates — listed, never judged).
106
+ export function relevanceOf(note, diffTokens) {
107
+ let score = 0;
108
+ if (note.hit === "direct") score += 3;
109
+ if (note.type === "contract") score += 2;
110
+ if (note.failure === "silent") score += 2;
111
+ const anchor = String(note.token ?? "").toLowerCase();
112
+ if (anchor && [...diffTokens].some((t) => anchor.includes(t) || t.includes(anchor))) score += 2;
113
+ let kw = 0;
114
+ const text = `${note.title ?? ""} ${note.summary ?? ""}`.toLowerCase();
115
+ for (const t of diffTokens) if (text.includes(t)) kw += 1;
116
+ score += Math.min(kw, 3);
117
+ return { score, engage: score >= 3 ? "deep" : score >= 1 ? "triage" : "skip" };
118
+ }
119
+
74
120
  export default async function review(args = []) {
75
121
  const ROOT = process.cwd();
76
122
  if (!existsSync(join(ROOT, "rafa.json"))) {
@@ -88,6 +134,7 @@ export default async function review(args = []) {
88
134
 
89
135
  const diff = sh(`git diff -U0 ${base}`, ROOT);
90
136
  const ranges = changedRanges(diff);
137
+ const tokensByFile = addedTokens(diff);
91
138
  // Code files only — the brain plane reviews itself at reconcile, not here.
92
139
  const files = [...ranges.keys()].filter(
93
140
  (p) => !p.startsWith(".rafa/") && !p.startsWith(".claude/") && p !== "rafa.json",
@@ -106,18 +153,43 @@ export default async function review(args = []) {
106
153
  break; // one transport failure fails them all — say so, don't half-report
107
154
  }
108
155
  const fileRanges = ranges.get(path) ?? [];
109
- const rules = (ctx.notes ?? []).map((n) => ({
110
- id: n.id,
111
- kind: n.kind,
112
- title: n.title,
113
- domain: n.domain,
114
- line: n.line,
115
- ...(n.failure ? { failure: n.failure } : {}),
116
- hit: citeHits(n.line, fileRanges) ? "direct" : "file",
117
- }));
156
+ // BOTH note kinds engage: a rule hit asks "does the diff still honor the
157
+ // convention/contract"; a playbook hit asks "is the documented flow/how-to
158
+ // still true after this change". staleCites = freshness RISK (the note may
159
+ // already describe drifted code — verify before relying on it).
160
+ const fileTokens = tokensByFile.get(path) ?? new Set();
161
+ const notes = (ctx.notes ?? []).map((n) => {
162
+ const base = {
163
+ id: n.id,
164
+ kind: n.kind,
165
+ ...(n.type ? { type: n.type } : {}),
166
+ title: n.title,
167
+ domain: n.domain,
168
+ line: n.line,
169
+ token: n.token,
170
+ ...(n.failure ? { failure: n.failure } : {}),
171
+ // The one-line summary IS the triage surface — the judge rules most
172
+ // notes in/out from this alone, fetching bodies only for the shortlist.
173
+ ...(n.summary ? { summary: n.summary } : {}),
174
+ ...(n.staleCites ? { staleCites: n.staleCites } : {}),
175
+ hit: citeHits(n.line, fileRanges) ? "direct" : "file",
176
+ };
177
+ const { score, engage } = relevanceOf(base, fileTokens);
178
+ return { ...base, relevance: score, engage };
179
+ });
118
180
  const improvements = (ctx.improvements ?? []).filter((i) => i.status === "open");
119
- if (rules.length > 0 || improvements.length > 0)
120
- out.push({ path, ranges: fileRanges, rules, improvements, relatedPlans: ctx.relatedPlans ?? [] });
181
+ if (notes.length > 0 || improvements.length > 0)
182
+ out.push({ path, ranges: fileRanges, notes, improvements, relatedPlans: ctx.relatedPlans ?? [] });
183
+ }
184
+
185
+ // Open knowledge gaps — live demand near this change (queries + counts only).
186
+ // The judge weighs: does this diff ANSWER one (close it) or deepen it?
187
+ let gaps = [];
188
+ try {
189
+ const g = await callTool(ROOT, "get_knowledge_gaps", { status: "open", limit: 5 });
190
+ gaps = (g.gaps ?? []).map((x) => ({ q: x.q, misses: x.misses }));
191
+ } catch {
192
+ /* gaps are context, not a gate — absence never fails the pass */
121
193
  }
122
194
 
123
195
  const report = {
@@ -128,6 +200,7 @@ export default async function review(args = []) {
128
200
  ...(dropped > 0 ? { dropped } : {}),
129
201
  ...(unreachable ? { unreachable } : {}),
130
202
  files: out,
203
+ ...(gaps.length > 0 ? { openGaps: gaps } : {}),
131
204
  };
132
205
  writeFileSync(join(ROOT, ".rafa", "review.json"), JSON.stringify(report, null, 2) + "\n");
133
206
 
@@ -135,9 +208,12 @@ export default async function review(args = []) {
135
208
  console.log(JSON.stringify(report, null, 2));
136
209
  return;
137
210
  }
138
- const direct = out.flatMap((f) => f.rules.filter((r) => r.hit === "direct"));
139
- const fileHits = out.flatMap((f) => f.rules.filter((r) => r.hit === "file"));
211
+ const direct = out.flatMap((f) => f.notes.filter((r) => r.hit === "direct"));
212
+ const fileHits = out.flatMap((f) => f.notes.filter((r) => r.hit === "file"));
140
213
  const imps = out.flatMap((f) => f.improvements);
214
+ const stale = out.flatMap((f) => f.notes.filter((r) => r.staleCites));
215
+ const engaged = out.flatMap((f) => f.notes.filter((r) => r.engage !== "skip"));
216
+ const skipped = out.flatMap((f) => f.notes.filter((r) => r.engage === "skip"));
141
217
  console.log(
142
218
  `✓ review pre-pass vs ${base.slice(0, 7)}: ${files.length} changed file(s)` +
143
219
  (dropped > 0 ? ` (${dropped} beyond the ${MAX_FILES}-file bound NOT reviewed)` : ""),
@@ -149,12 +225,20 @@ export default async function review(args = []) {
149
225
  }
150
226
  for (const f of out) {
151
227
  console.log(` ${f.path}`);
152
- for (const r of f.rules)
153
- console.log(` ${r.hit === "direct" ? "◆" : "·"} ${r.id} [${r.domain}] ${r.title}${r.failure ? ` (failure: ${r.failure})` : ""}`);
228
+ for (const r of f.notes.filter((n) => n.engage !== "skip"))
229
+ console.log(
230
+ ` ${r.hit === "direct" ? "◆" : "·"} [${r.engage}] ${r.kind} ${r.id} [${r.domain}] ${r.title}` +
231
+ `${r.failure ? ` (failure: ${r.failure})` : ""}${r.staleCites ? ` ⚠ ${r.staleCites} stale cite(s)` : ""}`,
232
+ );
154
233
  for (const i of f.improvements) console.log(` ! open ${i.priority}: ${i.id} ${i.title}`);
155
234
  }
235
+ if (gaps.length)
236
+ console.log(` open knowledge gaps (does this change answer one?): ${gaps.map((g) => `"${g.q}"×${g.misses}`).join(" · ")}`);
156
237
  console.log(
157
- `→ ${direct.length} direct anchor hit(s) · ${fileHits.length} same-file rule(s) · ${imps.length} open improvement(s)` +
158
- `\n The judge rules on exactly these (rafa-review SOP): does the diff still honor each rule; does it satisfy the active task's Done-check.`,
238
+ `→ engage ${engaged.length}/${engaged.length + skipped.length} note(s) (${direct.length} direct · ${fileHits.length} same-file` +
239
+ (skipped.length ? ` · ${skipped.length} governs-the-file-but-unrelated SKIPPED, listed in review.json` : "") +
240
+ `) · ${imps.length} open improvement(s)` +
241
+ (stale.length ? ` · ⚠ ${stale.length} stale-cite note(s) — verify before relying` : "") +
242
+ `\n The judge rules on exactly the engaged set (rafa-review SOP): deep = body-worthy, triage = summary-only.`,
159
243
  );
160
244
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafinery/cli",
3
- "version": "0.8.14",
3
+ "version": "0.8.15",
4
4
  "description": "rafa — the AI engineer CLI. Vendors the rafa blueprint into your repo (shadcn-style) and runs the deterministic contract gates.",
5
5
  "type": "module",
6
6
  "bin": {