@rafinery/cli 0.6.0 → 0.7.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/bin/rafa.mjs +7 -0
- package/blueprint/.claude/agents/sage.md +66 -0
- package/blueprint/.claude/commands/rafa.md +205 -325
- package/blueprint/.claude/rafa/contract.md +188 -131
- package/blueprint/.claude/skills/rafa-build/SKILL.md +9 -1
- package/blueprint/.claude/skills/rafa-distill/SKILL.md +6 -1
- package/blueprint/.claude/skills/rafa-sage/SKILL.md +201 -0
- package/blueprint/.claude/skills/rafa-scan/SKILL.md +41 -2
- package/lib/benchmark.mjs +573 -0
- package/lib/blueprint.mjs +2 -0
- package/lib/gate/compile.mjs +258 -49
- package/package.json +1 -1
package/lib/gate/compile.mjs
CHANGED
|
@@ -119,6 +119,41 @@ export function runCompile(argv = []) {
|
|
|
119
119
|
// Paths in the manifest are relative to the brain-repo root (`.rafa/` IS the root),
|
|
120
120
|
// so the platform can fetch a prose body straight from the brain repo.
|
|
121
121
|
const rel = (p) => (p.startsWith(ROOT + "/") ? p.slice(ROOT.length + 1) : p);
|
|
122
|
+
// The code repo root — cite `file`s are relative to it (same anchor the checker
|
|
123
|
+
// resolves against). `.rafa/` sits inside the code repo, so `..` reaches the root.
|
|
124
|
+
const repoRoot = join(ROOT, "..");
|
|
125
|
+
|
|
126
|
+
// ── deterministic size estimation (recall-savings, additive optional) ────────
|
|
127
|
+
// Feeds the platform's recall-savings estimator: a note's token cost and each
|
|
128
|
+
// cited file's token cost, so it can price "read the note" vs "re-read the code".
|
|
129
|
+
// Estimate = ceil(chars / 4) — the ~4-bytes-per-token rule of thumb for English
|
|
130
|
+
// source/prose. DETERMINISTIC: pure function of the file bytes, no LLM, no network.
|
|
131
|
+
// These are ADDITIVE OPTIONAL manifest fields (contract §1) — a note without them
|
|
132
|
+
// still compiles and ingest tolerates their absence; a target we cannot read is
|
|
133
|
+
// OMITTED, never defaulted to a guessed 0 (contract §0 / compile's own law above).
|
|
134
|
+
const estimateTokens = (s) => Math.ceil(s.length / 4);
|
|
135
|
+
// The prose body = everything after the closing `---` fence (never parsed, only sized).
|
|
136
|
+
function noteBody(text) {
|
|
137
|
+
if (!text.startsWith("---")) return text;
|
|
138
|
+
const end = text.indexOf("\n---", text.indexOf("\n"));
|
|
139
|
+
if (end === -1) return "";
|
|
140
|
+
const nl = text.indexOf("\n", end + 1); // newline after the closing `---` line
|
|
141
|
+
return nl === -1 ? "" : text.slice(nl + 1);
|
|
142
|
+
}
|
|
143
|
+
// Cited-file token estimate, cached per path (a file is cited by many notes).
|
|
144
|
+
// null = unreadable at compile time → the caller OMITS the field (honest absence).
|
|
145
|
+
const citeTokenCache = new Map();
|
|
146
|
+
function citeTargetTokens(file) {
|
|
147
|
+
if (citeTokenCache.has(file)) return citeTokenCache.get(file);
|
|
148
|
+
let tokens = null;
|
|
149
|
+
try {
|
|
150
|
+
tokens = estimateTokens(readFileSync(join(repoRoot, file), "utf8"));
|
|
151
|
+
} catch {
|
|
152
|
+
tokens = null; // missing/unreadable → omit, never guess
|
|
153
|
+
}
|
|
154
|
+
citeTokenCache.set(file, tokens);
|
|
155
|
+
return tokens;
|
|
156
|
+
}
|
|
122
157
|
|
|
123
158
|
const errors = [];
|
|
124
159
|
const fail = (path, field, rule) => errors.push({ path, field, rule });
|
|
@@ -184,7 +219,8 @@ export function runCompile(argv = []) {
|
|
|
184
219
|
const notes = [];
|
|
185
220
|
for (const path of walk(dir)) {
|
|
186
221
|
const name = path.split("/").pop();
|
|
187
|
-
const
|
|
222
|
+
const raw = read(path);
|
|
223
|
+
const { data, error } = parseFrontmatter(raw);
|
|
188
224
|
if (error) {
|
|
189
225
|
fail(path, "frontmatter", error);
|
|
190
226
|
continue;
|
|
@@ -194,12 +230,27 @@ export function runCompile(argv = []) {
|
|
|
194
230
|
// `anchor:` / `absent:` are checker-side declarations (verify-citations gates
|
|
195
231
|
// B2/B3 on them) — optional here, but if present they must be non-empty.
|
|
196
232
|
for (const key of ["anchor", "absent"])
|
|
197
|
-
if (
|
|
233
|
+
if (
|
|
234
|
+
key in data &&
|
|
235
|
+
(typeof data[key] !== "string" || data[key].trim() === "")
|
|
236
|
+
)
|
|
198
237
|
fail(path, key, "optional · non-empty token (or `none`)");
|
|
238
|
+
// Size stamps (additive optional): body cost + per-cite target-file cost. A
|
|
239
|
+
// target we can't read → the cite simply carries no `targetTokens` (omitted).
|
|
240
|
+
const cites = parseCites(data.cites, path);
|
|
241
|
+
for (const c of cites) {
|
|
242
|
+
const t = citeTargetTokens(c.file);
|
|
243
|
+
if (t !== null) c.targetTokens = t;
|
|
244
|
+
}
|
|
199
245
|
notes.push({
|
|
200
246
|
id,
|
|
201
247
|
kind,
|
|
202
|
-
type: reqEnum(
|
|
248
|
+
type: reqEnum(
|
|
249
|
+
data,
|
|
250
|
+
"type",
|
|
251
|
+
["contract", "convention", "flow", "how-to"],
|
|
252
|
+
path,
|
|
253
|
+
),
|
|
203
254
|
domain: reqStr(data, "domain", path),
|
|
204
255
|
title: reqStr(data, "title", path),
|
|
205
256
|
summary: reqStr(data, "summary", path),
|
|
@@ -207,7 +258,8 @@ export function runCompile(argv = []) {
|
|
|
207
258
|
...(data.failure === "silent" || data.failure === "loud"
|
|
208
259
|
? { failure: data.failure }
|
|
209
260
|
: {}),
|
|
210
|
-
|
|
261
|
+
bodyTokens: estimateTokens(noteBody(raw)),
|
|
262
|
+
cites,
|
|
211
263
|
path: rel(path),
|
|
212
264
|
});
|
|
213
265
|
}
|
|
@@ -231,17 +283,33 @@ export function runCompile(argv = []) {
|
|
|
231
283
|
!isEnum(lev?.impact, ["low", "medium", "high"]) ||
|
|
232
284
|
!isEnum(lev?.effort, ["low", "medium", "high"])
|
|
233
285
|
)
|
|
234
|
-
fail(
|
|
286
|
+
fail(
|
|
287
|
+
path,
|
|
288
|
+
"leverage",
|
|
289
|
+
"required · { impact, effort } ∈ low|medium|high",
|
|
290
|
+
);
|
|
235
291
|
out.push({
|
|
236
292
|
id,
|
|
237
293
|
priority: reqEnum(data, "priority", ["P0", "P1", "P2", "P3"], path),
|
|
238
294
|
lens: reqEnum(
|
|
239
295
|
data,
|
|
240
296
|
"lens",
|
|
241
|
-
[
|
|
297
|
+
[
|
|
298
|
+
"security",
|
|
299
|
+
"correctness",
|
|
300
|
+
"performance",
|
|
301
|
+
"architecture",
|
|
302
|
+
"product",
|
|
303
|
+
"ops",
|
|
304
|
+
],
|
|
305
|
+
path,
|
|
306
|
+
),
|
|
307
|
+
status: reqEnum(
|
|
308
|
+
data,
|
|
309
|
+
"status",
|
|
310
|
+
["open", "backlog", "fixed", "wontfix"],
|
|
242
311
|
path,
|
|
243
312
|
),
|
|
244
|
-
status: reqEnum(data, "status", ["open", "backlog", "fixed", "wontfix"], path),
|
|
245
313
|
title: reqStr(data, "title", path),
|
|
246
314
|
summary: reqStr(data, "summary", path),
|
|
247
315
|
fix: reqStr(data, "fix", path),
|
|
@@ -270,7 +338,10 @@ export function runCompile(argv = []) {
|
|
|
270
338
|
checkVersion(data, path);
|
|
271
339
|
const g = data.gates,
|
|
272
340
|
c = data.counts;
|
|
273
|
-
if (
|
|
341
|
+
if (
|
|
342
|
+
!isEnum(g?.fidelity, ["pass", "fail"]) ||
|
|
343
|
+
!isEnum(g?.coverage, ["pass", "fail"])
|
|
344
|
+
)
|
|
274
345
|
fail(path, "gates", "required · { fidelity, coverage } ∈ pass|fail");
|
|
275
346
|
if (
|
|
276
347
|
typeof c?.blockers !== "number" ||
|
|
@@ -278,11 +349,15 @@ export function runCompile(argv = []) {
|
|
|
278
349
|
typeof c?.minors !== "number"
|
|
279
350
|
)
|
|
280
351
|
fail(path, "counts", "required · { blockers, majors, minors } ints");
|
|
281
|
-
if (typeof data.score !== "number")
|
|
352
|
+
if (typeof data.score !== "number")
|
|
353
|
+
fail(path, "score", "required · 0–100 int");
|
|
282
354
|
return {
|
|
283
355
|
verdict: reqEnum(data, "verdict", ["PASS", "ITERATE"], path),
|
|
284
356
|
score: typeof data.score === "number" ? data.score : 0,
|
|
285
|
-
gates: {
|
|
357
|
+
gates: {
|
|
358
|
+
fidelity: g?.fidelity ?? "fail",
|
|
359
|
+
coverage: g?.coverage ?? "fail",
|
|
360
|
+
},
|
|
286
361
|
counts: {
|
|
287
362
|
blockers: c?.blockers ?? 0,
|
|
288
363
|
majors: c?.majors ?? 0,
|
|
@@ -342,11 +417,19 @@ export function runCompile(argv = []) {
|
|
|
342
417
|
// here so a malformed declaration fails loudly at the gate, never silently.
|
|
343
418
|
if ("inventory" in data) {
|
|
344
419
|
if (!Array.isArray(data.inventory) || data.inventory.length === 0) {
|
|
345
|
-
fail(
|
|
420
|
+
fail(
|
|
421
|
+
path,
|
|
422
|
+
"inventory",
|
|
423
|
+
"optional · block list of `<name> :: <glob> :: <count>`",
|
|
424
|
+
);
|
|
346
425
|
} else {
|
|
347
426
|
for (const entry of data.inventory)
|
|
348
427
|
if (!/^.+?\s*::\s*.+?\s*::\s*\d+$/.test(String(entry)))
|
|
349
|
-
fail(
|
|
428
|
+
fail(
|
|
429
|
+
path,
|
|
430
|
+
"inventory",
|
|
431
|
+
`malformed entry: ${JSON.stringify(entry)} · must be \`<name> :: <glob> :: <count>\``,
|
|
432
|
+
);
|
|
350
433
|
}
|
|
351
434
|
}
|
|
352
435
|
return { domains };
|
|
@@ -366,8 +449,16 @@ export function runCompile(argv = []) {
|
|
|
366
449
|
fail(path, "json", `unparseable: ${e instanceof Error ? e.message : e}`);
|
|
367
450
|
return null;
|
|
368
451
|
}
|
|
369
|
-
if (
|
|
370
|
-
|
|
452
|
+
if (
|
|
453
|
+
typeof rec.checkerVersion !== "number" ||
|
|
454
|
+
typeof rec.pass !== "boolean" ||
|
|
455
|
+
typeof rec.at !== "string"
|
|
456
|
+
) {
|
|
457
|
+
fail(
|
|
458
|
+
path,
|
|
459
|
+
"shape",
|
|
460
|
+
"required · { checkerVersion: int, pass: bool, at: ISO string }",
|
|
461
|
+
);
|
|
371
462
|
return null;
|
|
372
463
|
}
|
|
373
464
|
return { checkerVersion: rec.checkerVersion, pass: rec.pass, at: rec.at };
|
|
@@ -378,7 +469,13 @@ export function runCompile(argv = []) {
|
|
|
378
469
|
// (unresolved blocked_by / blocked_reason), never stored. VALIDATED here
|
|
379
470
|
// (files are the authoring surface — determinism holds) but plans do NOT ride
|
|
380
471
|
// the manifest: they travel through the push-on-approval plans channel.
|
|
381
|
-
const PLAN_STATUSES = [
|
|
472
|
+
const PLAN_STATUSES = [
|
|
473
|
+
"todo",
|
|
474
|
+
"in-progress",
|
|
475
|
+
"done",
|
|
476
|
+
"superseded",
|
|
477
|
+
"abandoned",
|
|
478
|
+
];
|
|
382
479
|
const PLAN_KINDS = ["epic", "task", "subtask"];
|
|
383
480
|
function compilePlans() {
|
|
384
481
|
const plans = [];
|
|
@@ -393,46 +490,95 @@ export function runCompile(argv = []) {
|
|
|
393
490
|
checkVersion(data, path);
|
|
394
491
|
const id = checkId(data, path, name);
|
|
395
492
|
if (seen.has(id))
|
|
396
|
-
fail(
|
|
493
|
+
fail(
|
|
494
|
+
path,
|
|
495
|
+
"id",
|
|
496
|
+
`duplicate plan id "${id}" (also ${seen.get(id)}) · ids are globally unique across plans/**`,
|
|
497
|
+
);
|
|
397
498
|
seen.set(id, path);
|
|
398
499
|
if ("progress" in data)
|
|
399
|
-
fail(
|
|
500
|
+
fail(
|
|
501
|
+
path,
|
|
502
|
+
"progress",
|
|
503
|
+
"never stored · progress = done leaves / total leaves, derived",
|
|
504
|
+
);
|
|
400
505
|
if (data.kind === "parent" || data.kind === "child")
|
|
401
|
-
fail(
|
|
506
|
+
fail(
|
|
507
|
+
path,
|
|
508
|
+
"kind",
|
|
509
|
+
`plans v1 shape ("${data.kind}") — run \`rafa migrate\` (v2: epic|task|subtask)`,
|
|
510
|
+
);
|
|
402
511
|
const kind = reqEnum(data, "kind", PLAN_KINDS, path);
|
|
403
512
|
const plan = reqStr(data, "plan", path);
|
|
404
513
|
const status = data.status;
|
|
405
514
|
if (status === "blocked")
|
|
406
|
-
fail(
|
|
515
|
+
fail(
|
|
516
|
+
path,
|
|
517
|
+
"status",
|
|
518
|
+
"removed in v2 — blocked is DERIVED from blocked_by/blocked_reason (run `rafa migrate`)",
|
|
519
|
+
);
|
|
407
520
|
if (kind === "epic") {
|
|
408
521
|
if (data.parent !== null && data.parent !== "null")
|
|
409
522
|
fail(path, "parent", "must be null for kind: epic (the root)");
|
|
410
|
-
if (plan !== id)
|
|
523
|
+
if (plan !== id)
|
|
524
|
+
fail(path, "plan", `must equal id "${id}" for kind: epic`);
|
|
411
525
|
if (status !== undefined && !isEnum(status, PLAN_STATUSES))
|
|
412
526
|
fail(path, "status", `optional (epic) · ${PLAN_STATUSES.join("|")}`);
|
|
413
527
|
} else {
|
|
414
528
|
if (!isEnum(status, PLAN_STATUSES))
|
|
415
|
-
fail(
|
|
529
|
+
fail(
|
|
530
|
+
path,
|
|
531
|
+
"status",
|
|
532
|
+
`required (${kind}) · ${PLAN_STATUSES.join("|")}`,
|
|
533
|
+
);
|
|
416
534
|
if (typeof data.parent !== "string" || data.parent === "")
|
|
417
535
|
fail(path, "parent", `required (${kind}) · the parent item id`);
|
|
418
536
|
if (data.domains !== undefined)
|
|
419
|
-
fail(
|
|
537
|
+
fail(
|
|
538
|
+
path,
|
|
539
|
+
"domains",
|
|
540
|
+
"epic only · the blast radius belongs to the plan, not an item",
|
|
541
|
+
);
|
|
420
542
|
}
|
|
421
543
|
if (data.domains !== undefined && !Array.isArray(data.domains))
|
|
422
544
|
fail(path, "domains", "optional · flow list of domain strings");
|
|
423
545
|
if (data.blocked_by !== undefined && !Array.isArray(data.blocked_by))
|
|
424
|
-
fail(
|
|
546
|
+
fail(
|
|
547
|
+
path,
|
|
548
|
+
"blocked_by",
|
|
549
|
+
"optional · flow list of item ids this item waits on",
|
|
550
|
+
);
|
|
425
551
|
if (
|
|
426
552
|
data.priority !== undefined &&
|
|
427
|
-
!(
|
|
553
|
+
!(
|
|
554
|
+
Number.isInteger(data.priority) &&
|
|
555
|
+
data.priority >= 0 &&
|
|
556
|
+
data.priority <= 4
|
|
557
|
+
)
|
|
558
|
+
)
|
|
559
|
+
fail(
|
|
560
|
+
path,
|
|
561
|
+
"priority",
|
|
562
|
+
"optional · int 0–4 (0 none · 1 urgent · 2 high · 3 medium · 4 low)",
|
|
563
|
+
);
|
|
564
|
+
if (
|
|
565
|
+
data.estimate !== undefined &&
|
|
566
|
+
!(Number.isInteger(data.estimate) && data.estimate >= 0)
|
|
428
567
|
)
|
|
429
|
-
fail(path, "priority", "optional · int 0–4 (0 none · 1 urgent · 2 high · 3 medium · 4 low)");
|
|
430
|
-
if (data.estimate !== undefined && !(Number.isInteger(data.estimate) && data.estimate >= 0))
|
|
431
568
|
fail(path, "estimate", "optional · non-negative int (points)");
|
|
432
569
|
if (data.external !== undefined) {
|
|
433
570
|
const ex = data.external;
|
|
434
|
-
if (
|
|
435
|
-
|
|
571
|
+
if (
|
|
572
|
+
typeof ex !== "object" ||
|
|
573
|
+
Array.isArray(ex) ||
|
|
574
|
+
typeof ex.provider !== "string" ||
|
|
575
|
+
typeof ex.key !== "string"
|
|
576
|
+
)
|
|
577
|
+
fail(
|
|
578
|
+
path,
|
|
579
|
+
"external",
|
|
580
|
+
"optional · { provider, key, url? } flow map — tracker identity (read-only to sessions)",
|
|
581
|
+
);
|
|
436
582
|
}
|
|
437
583
|
plans.push({
|
|
438
584
|
id,
|
|
@@ -450,8 +596,11 @@ export function runCompile(argv = []) {
|
|
|
450
596
|
...(typeof data.assignee === "string" && data.assignee !== ""
|
|
451
597
|
? { assignee: data.assignee }
|
|
452
598
|
: {}),
|
|
453
|
-
...(Array.isArray(data.blocked_by)
|
|
454
|
-
|
|
599
|
+
...(Array.isArray(data.blocked_by)
|
|
600
|
+
? { blockedBy: data.blocked_by.map(String) }
|
|
601
|
+
: {}),
|
|
602
|
+
...(typeof data.blocked_reason === "string" &&
|
|
603
|
+
data.blocked_reason !== ""
|
|
455
604
|
? { blockedReason: data.blocked_reason }
|
|
456
605
|
: {}),
|
|
457
606
|
...(Number.isInteger(data.priority) ? { priority: data.priority } : {}),
|
|
@@ -465,7 +614,9 @@ export function runCompile(argv = []) {
|
|
|
465
614
|
...(Array.isArray(data.domains) && kind === "epic"
|
|
466
615
|
? { domains: data.domains.map(String) }
|
|
467
616
|
: {}),
|
|
468
|
-
...(typeof data.external === "object" &&
|
|
617
|
+
...(typeof data.external === "object" &&
|
|
618
|
+
!Array.isArray(data.external) &&
|
|
619
|
+
data.external !== null
|
|
469
620
|
? { external: data.external }
|
|
470
621
|
: {}),
|
|
471
622
|
path: rel(path),
|
|
@@ -477,24 +628,45 @@ export function runCompile(argv = []) {
|
|
|
477
628
|
for (const p of plans) if (p.kind === "epic") roots.set(p.id, p);
|
|
478
629
|
for (const p of plans) {
|
|
479
630
|
if (!roots.has(p.plan))
|
|
480
|
-
fail(
|
|
631
|
+
fail(
|
|
632
|
+
p.path,
|
|
633
|
+
"plan",
|
|
634
|
+
`no kind:epic "${p.plan}" in this compile (dangling item)`,
|
|
635
|
+
);
|
|
481
636
|
if (p.kind === "task") {
|
|
482
637
|
const parent = byId.get(p.parent);
|
|
483
638
|
if (!parent || parent.kind !== "epic" || parent.id !== p.plan)
|
|
484
|
-
fail(
|
|
639
|
+
fail(
|
|
640
|
+
p.path,
|
|
641
|
+
"parent",
|
|
642
|
+
`a task's parent must be its epic ("${p.plan}")`,
|
|
643
|
+
);
|
|
485
644
|
}
|
|
486
645
|
if (p.kind === "subtask") {
|
|
487
646
|
const parent = byId.get(p.parent);
|
|
488
647
|
if (!parent || parent.kind !== "task")
|
|
489
|
-
fail(
|
|
648
|
+
fail(
|
|
649
|
+
p.path,
|
|
650
|
+
"parent",
|
|
651
|
+
"a subtask's parent must be a task in the same plan",
|
|
652
|
+
);
|
|
490
653
|
else if (parent.plan !== p.plan)
|
|
491
|
-
fail(
|
|
654
|
+
fail(
|
|
655
|
+
p.path,
|
|
656
|
+
"parent",
|
|
657
|
+
`parent task belongs to plan "${parent.plan}", not "${p.plan}"`,
|
|
658
|
+
);
|
|
492
659
|
}
|
|
493
660
|
for (const b of p.blockedBy ?? []) {
|
|
494
661
|
const target = byId.get(b);
|
|
495
662
|
if (!target || target.plan !== p.plan)
|
|
496
|
-
fail(
|
|
497
|
-
|
|
663
|
+
fail(
|
|
664
|
+
p.path,
|
|
665
|
+
"blocked_by",
|
|
666
|
+
`"${b}" does not resolve to an item in plan "${p.plan}"`,
|
|
667
|
+
);
|
|
668
|
+
if (b === p.id)
|
|
669
|
+
fail(p.path, "blocked_by", "an item cannot block itself");
|
|
498
670
|
}
|
|
499
671
|
}
|
|
500
672
|
// blocked_by acyclicity (per plan; DFS with colors).
|
|
@@ -512,7 +684,11 @@ export function runCompile(argv = []) {
|
|
|
512
684
|
};
|
|
513
685
|
for (const p of plans)
|
|
514
686
|
if (!color.has(p.id) && cyc(p))
|
|
515
|
-
fail(
|
|
687
|
+
fail(
|
|
688
|
+
p.path,
|
|
689
|
+
"blocked_by",
|
|
690
|
+
"dependency cycle — blocked_by must be acyclic",
|
|
691
|
+
);
|
|
516
692
|
return plans;
|
|
517
693
|
}
|
|
518
694
|
|
|
@@ -525,13 +701,21 @@ export function runCompile(argv = []) {
|
|
|
525
701
|
const first = read(path).split("\n")[0].trim();
|
|
526
702
|
const m = first.match(/^#\s+(.+)$/);
|
|
527
703
|
if (!m) {
|
|
528
|
-
fail(
|
|
704
|
+
fail(
|
|
705
|
+
path,
|
|
706
|
+
"pointer",
|
|
707
|
+
'first line must be "# <plan-id>" or "# No active plan"',
|
|
708
|
+
);
|
|
529
709
|
return null;
|
|
530
710
|
}
|
|
531
711
|
const val = m[1].trim();
|
|
532
712
|
if (/^no active plan$/i.test(val)) return null;
|
|
533
713
|
if (!plans.some((p) => p.kind === "epic" && p.id === val)) {
|
|
534
|
-
fail(
|
|
714
|
+
fail(
|
|
715
|
+
path,
|
|
716
|
+
"pointer",
|
|
717
|
+
`active plan "${val}" is not a kind:epic plan in this compile`,
|
|
718
|
+
);
|
|
535
719
|
return null;
|
|
536
720
|
}
|
|
537
721
|
return val;
|
|
@@ -555,7 +739,8 @@ export function runCompile(argv = []) {
|
|
|
555
739
|
}
|
|
556
740
|
count++;
|
|
557
741
|
const stem = name.replace(/\.md$/, "");
|
|
558
|
-
if (data.name !== stem)
|
|
742
|
+
if (data.name !== stem)
|
|
743
|
+
fail(path, "name", `must equal filename stem "${stem}"`);
|
|
559
744
|
if (!/^\d+\.\d+\.\d+$/.test(String(data.version ?? "")))
|
|
560
745
|
fail(path, "version", "required · semver MAJOR.MINOR.PATCH");
|
|
561
746
|
reqStr(data, "model", path);
|
|
@@ -575,12 +760,20 @@ export function runCompile(argv = []) {
|
|
|
575
760
|
for (const d of duties) {
|
|
576
761
|
const m = String(d).match(DUTY_RE);
|
|
577
762
|
if (!m) {
|
|
578
|
-
fail(
|
|
763
|
+
fail(
|
|
764
|
+
path,
|
|
765
|
+
"duties",
|
|
766
|
+
`malformed duty (want "<duty> :: <sop> :: <bar>"): ${JSON.stringify(d)}`,
|
|
767
|
+
);
|
|
579
768
|
continue;
|
|
580
769
|
}
|
|
581
770
|
const sop = m[2];
|
|
582
771
|
if (!existsSync(join(repoRoot, sop)) && !existsSync(join(ROOT, sop)))
|
|
583
|
-
fail(
|
|
772
|
+
fail(
|
|
773
|
+
path,
|
|
774
|
+
"duties",
|
|
775
|
+
`sop does not resolve: ${sop} (a duty without a procedure is a claim)`,
|
|
776
|
+
);
|
|
584
777
|
}
|
|
585
778
|
}
|
|
586
779
|
return count;
|
|
@@ -607,7 +800,9 @@ export function runCompile(argv = []) {
|
|
|
607
800
|
...compileNotes("rule", join(ROOT, "brain", "rules")),
|
|
608
801
|
...compileNotes("playbook", join(ROOT, "brain", "playbooks")),
|
|
609
802
|
];
|
|
610
|
-
const improvements = compileImprovements(
|
|
803
|
+
const improvements = compileImprovements(
|
|
804
|
+
join(ROOT, "improve", "improvements"),
|
|
805
|
+
);
|
|
611
806
|
const health = compileHealth();
|
|
612
807
|
const ledger = compileLedger();
|
|
613
808
|
const coverage = compileCoverage();
|
|
@@ -622,16 +817,27 @@ export function runCompile(argv = []) {
|
|
|
622
817
|
const bp = { P0: 0, P1: 0, P2: 0, P3: 0 };
|
|
623
818
|
for (const i of open) bp[i.priority]++;
|
|
624
819
|
if (ledger.open !== open.length)
|
|
625
|
-
fail(
|
|
820
|
+
fail(
|
|
821
|
+
"improve/ledger.md",
|
|
822
|
+
"open",
|
|
823
|
+
`says ${ledger.open}, rows have ${open.length}`,
|
|
824
|
+
);
|
|
626
825
|
for (const k of ["P0", "P1", "P2", "P3"])
|
|
627
826
|
if (ledger.byPriority[k] !== bp[k])
|
|
628
|
-
fail(
|
|
827
|
+
fail(
|
|
828
|
+
"improve/ledger.md",
|
|
829
|
+
`by_priority.${k}`,
|
|
830
|
+
`says ${ledger.byPriority[k]}, rows have ${bp[k]}`,
|
|
831
|
+
);
|
|
629
832
|
}
|
|
630
833
|
|
|
631
834
|
if (errors.length) {
|
|
632
835
|
console.error(`✗ rafa compile: ${errors.length} contract violation(s)\n`);
|
|
633
|
-
for (const e of errors)
|
|
634
|
-
|
|
836
|
+
for (const e of errors)
|
|
837
|
+
console.error(` ${e.path} · ${e.field} · ${e.rule}`);
|
|
838
|
+
console.error(
|
|
839
|
+
`\nFix these files to match the contract (.claude/rafa/contract.md), then re-run compile.`,
|
|
840
|
+
);
|
|
635
841
|
return 1;
|
|
636
842
|
}
|
|
637
843
|
|
|
@@ -650,7 +856,10 @@ export function runCompile(argv = []) {
|
|
|
650
856
|
notes,
|
|
651
857
|
improvements,
|
|
652
858
|
};
|
|
653
|
-
writeFileSync(
|
|
859
|
+
writeFileSync(
|
|
860
|
+
join(ROOT, "manifest.json"),
|
|
861
|
+
JSON.stringify(manifest, null, 2) + "\n",
|
|
862
|
+
);
|
|
654
863
|
console.log(
|
|
655
864
|
`✓ rafa compile: ${notes.length} notes · ${improvements.length} improvements · ` +
|
|
656
865
|
`${plans.length} plans validated${activePlanId ? ` (active: ${activePlanId})` : ""} (plans travel via the plans channel, not the manifest) · ` +
|
package/package.json
CHANGED