shapeup-sdlc 3.18.3 → 4.0.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.
Files changed (141) hide show
  1. package/.claude/settings.local.example.json +2 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +115 -56
  4. package/README.md +101 -143
  5. package/SECURITY.md +16 -25
  6. package/agents/orient.md +14 -0
  7. package/agents/qa-edge-hunter.md +13 -0
  8. package/agents/spec-evaluator.md +14 -0
  9. package/agents/task-executor.md +14 -0
  10. package/bin/init.mjs +13 -16
  11. package/bin/lib/grant.mjs +8 -19
  12. package/commands/build.md +9 -14
  13. package/commands/eval.md +3 -3
  14. package/commands/hammer.md +9 -5
  15. package/commands/retro.md +12 -11
  16. package/commands/scopes.md +1 -12
  17. package/commands/ship.md +45 -88
  18. package/commands/wire.md +1 -8
  19. package/hooks/dispatch-receipt.mjs +94 -8
  20. package/hooks/hooks.json +0 -41
  21. package/hooks/lib/decision.mjs +6 -9
  22. package/hooks/lib/tier.mjs +50 -0
  23. package/hooks/sandbox-guard.mjs +140 -64
  24. package/kernel/gate.mjs +203 -22
  25. package/kernel/harness.mjs +20 -67
  26. package/kernel/init/run.mjs +175 -523
  27. package/kernel/lib/contract.mjs +9 -93
  28. package/kernel/lib/floor.mjs +64 -0
  29. package/kernel/lib/paths.mjs +39 -112
  30. package/kernel/order.mjs +157 -0
  31. package/kernel/probe/digest.mjs +2 -38
  32. package/kernel/schemas/domain.schema.json +146 -1125
  33. package/kernel/schemas/work-order.schema.json +8 -4
  34. package/kernel/schemas/work-result.schema.json +1 -1
  35. package/kernel/verify/envelope.mjs +2 -2
  36. package/kernel/verify/skills.mjs +27 -8
  37. package/kernel/verify/t0.mjs +2 -2
  38. package/package.json +4 -2
  39. package/skills/building/SKILL.md +144 -0
  40. package/skills/building/resources/gates.md +76 -0
  41. package/skills/building/resources/guidance.md +23 -0
  42. package/skills/{translator/references/preservation-rules.md → building/resources/intake-preservation.md} +1 -1
  43. package/skills/building/resources/intake.md +157 -0
  44. package/skills/building/resources/orders.md +69 -0
  45. package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/doc-schemas.md +1 -1
  46. package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/task-generation.md +13 -13
  47. package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/test-surface.md +6 -7
  48. package/skills/building/resources/plan-scopes.md +95 -0
  49. package/skills/building/resources/plan-spec.md +102 -0
  50. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/_index.tmpl.md +1 -1
  51. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/assess-report.tmpl.md +10 -11
  52. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/run-state.tmpl.md +4 -4
  53. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/scope-summary.tmpl.md +2 -2
  54. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/synthesis.tmpl.md +4 -4
  55. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/usecase.tmpl.md +2 -2
  56. package/skills/building/resources/plan-wire.md +82 -0
  57. package/skills/building/resources/plan.md +98 -0
  58. package/skills/building/resources/retro-research.md +60 -0
  59. package/skills/building/resources/retro-scan.md +49 -0
  60. package/skills/building/resources/retro.md +225 -0
  61. package/skills/building/resources/rounds.md +78 -0
  62. package/skills/building/resources/stop.md +127 -0
  63. package/skills/orient/SKILL.md +37 -37
  64. package/skills/qa-edge-hunter/SKILL.md +14 -14
  65. package/skills/shapeup/SKILL.md +5 -5
  66. package/skills/shapeup/resources/breadboarding.md +1 -1
  67. package/skills/shapeup/resources/context-compaction.md +5 -5
  68. package/skills/shapeup/resources/shaping.md +1 -1
  69. package/skills/spec-evaluator/SKILL.md +17 -21
  70. package/skills/spec-evaluator/references/dimension-contract.md +1 -1
  71. package/skills/spec-evaluator/references/dimensions/completeness.md +3 -3
  72. package/skills/spec-evaluator/references/dimensions/spec-conformance.md +1 -1
  73. package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +5 -5
  74. package/skills/task-executor/SKILL.md +11 -15
  75. package/commands/hill.md +0 -10
  76. package/hooks/gate-intake.mjs +0 -109
  77. package/hooks/gate-zerowork.mjs +0 -349
  78. package/hooks/tier-guard.mjs +0 -162
  79. package/kernel/compile.mjs +0 -1419
  80. package/kernel/init/fit.mjs +0 -199
  81. package/kernel/init/run-args.mjs +0 -206
  82. package/kernel/lib/breadboard.mjs +0 -165
  83. package/kernel/probe/attempts.mjs +0 -161
  84. package/kernel/probe/concurrency.mjs +0 -535
  85. package/kernel/probe/eval.mjs +0 -405
  86. package/kernel/probe/hunt.mjs +0 -104
  87. package/kernel/probe/leg.mjs +0 -229
  88. package/kernel/probe/owner.mjs +0 -142
  89. package/kernel/probe/requirements.mjs +0 -328
  90. package/kernel/probe/resume.mjs +0 -1110
  91. package/kernel/probe/rounds.mjs +0 -115
  92. package/kernel/probe/staged.mjs +0 -83
  93. package/kernel/probe/stats.mjs +0 -477
  94. package/kernel/probe/t0.mjs +0 -115
  95. package/kernel/reduce/board.mjs +0 -350
  96. package/kernel/reduce/graph.mjs +0 -482
  97. package/kernel/reduce/hill.mjs +0 -322
  98. package/kernel/reduce/ingest.mjs +0 -767
  99. package/kernel/reduce/leftovers.mjs +0 -159
  100. package/kernel/reduce/ship.mjs +0 -529
  101. package/kernel/reduce/snapshot.mjs +0 -291
  102. package/kernel/reduce/verdict.mjs +0 -172
  103. package/kernel/report/export.mjs +0 -384
  104. package/kernel/report/facts.mjs +0 -234
  105. package/kernel/verify/budget.mjs +0 -153
  106. package/kernel/verify/build.mjs +0 -428
  107. package/kernel/verify/dispatch.mjs +0 -114
  108. package/kernel/verify/spec.mjs +0 -975
  109. package/kernel/verify/trace.mjs +0 -604
  110. package/skills/ba-pitch-analyzer/SKILL.md +0 -204
  111. package/skills/coach/SKILL.md +0 -397
  112. package/skills/hill-chart/SKILL.md +0 -154
  113. package/skills/hill-chart/assets/dashboard.template.html +0 -1432
  114. package/skills/scope-architect/SKILL.md +0 -174
  115. package/skills/scope-hammer/SKILL.md +0 -231
  116. package/skills/solution-architect/SKILL.md +0 -158
  117. package/skills/tech-lead/SKILL.md +0 -151
  118. package/skills/tech-lead/references/gates.md +0 -605
  119. package/skills/tech-lead/references/protocol.md +0 -889
  120. package/skills/tech-lead/references/tiny-lane.md +0 -53
  121. package/skills/tech-lead/workflows/shapeup-run.js +0 -2187
  122. package/skills/translator/SKILL.md +0 -258
  123. /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/contract-patterns.md +0 -0
  124. /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ddd-patterns.md +0 -0
  125. /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/integration-analysis.md +0 -0
  126. /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ux-behavior-patterns.md +0 -0
  127. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/api-feasibility.tmpl.md +0 -0
  128. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/be-service.contract.tmpl.md +0 -0
  129. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/offline-storage.contract.tmpl.md +0 -0
  130. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/third-party-api.contract.tmpl.md +0 -0
  131. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/context-map.tmpl.md +0 -0
  132. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/event-choreography.tmpl.md +0 -0
  133. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/migration-plan.tmpl.md +0 -0
  134. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/team-handoff.tmpl.md +0 -0
  135. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/domain-model.tmpl.md +0 -0
  136. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/feedback.tmpl.md +0 -0
  137. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/integration.tmpl.md +0 -0
  138. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-board.tmpl.md +0 -0
  139. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-spike.tmpl.md +0 -0
  140. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task.tmpl.md +0 -0
  141. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/ux-behavior.tmpl.md +0 -0
@@ -1,328 +0,0 @@
1
- #!/usr/bin/env node
2
- // probe requirements — the way back: a pitch clause, the criterion that graded it, the verdict.
3
- //
4
- // WHY THIS IS A QUERY AND NOT A SENTENCE, which is the same reason `probe owner` is one. The
5
- // requirement matrix is read at GATE L4 and cited by GATE H's census, and both are places where a
6
- // narrated figure is indistinguishable from a measured one. Measured on the first verdict any run
7
- // of the spine produced: 85 of 97 criterion rows carried a `traces_to` anchor in the WorkResult and
8
- // 0 of 97 survived into the projection ingest wrote — so a matrix assembled from memory would have
9
- // been assembled from an edge that no longer existed on disk. Every figure below is derived from
10
- // files: the committed registry, the board's `covers:` clauses, the verdict ledger, and the EVAL
11
- // result's own T0 citations. Nothing is passed in and nothing is written.
12
- //
13
- // THE JOIN, and which half is authoritative. A requirement has EVIDENCE when an acceptance
14
- // criterion covers it (`(covers: REQ-…)` on the AC line — the planning-time edge, reviewed at L1b)
15
- // AND a criterion that names it passed. `traces_to[]` is the navigation path from the judge's
16
- // criterion back to the requirement, exactly what the schema already calls it: an anchor, never a
17
- // grading input. A criterion whose `traces_to` names a REQ that no AC covers is printed as an
18
- // INCONSISTENCY row and counted as nothing — folding it in would derive one L4 line from two
19
- // unreconciled sources, which is the failure `probe owner` exists to prevent.
20
- //
21
- // IT PROJECTS ONE RUN. `order_id`, round and attempt all repeat across runs of one feature; the
22
- // run key is the only thing that separates them, so a projection that ignored it would silently mix
23
- // two runs of one slug. Rows written before the key reached the ledger carry no `run_id`; they are
24
- // reported as unknown rather than folded into the run being projected.
25
- //
26
- // AN EMPTY JOIN READS AS CLEARLY AS A FULL ONE. A tree with no registry, a board with no `covers:`
27
- // and a run with no verdict are all legitimate states, and each answers "no evidence" rather than
28
- // failing — that answer is the point of the query, not an error in it.
29
- //
30
- // Usage:
31
- // node "${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" probe requirements --slug <slug> [--run-id <id>] [--format json|table] [--cwd <dir>]
32
- //
33
- // Exit code: 0 = answered (an empty projection is an answer), 2 = bad argv.
34
-
35
- import { existsSync, readFileSync, readdirSync } from "node:fs";
36
- import { resolve, join } from "node:path";
37
- import { runArgs, isMain } from "../lib/argv.mjs";
38
- import { requirements as requirementsFile, evaluationDir, resultsDir, readRunId } from "../lib/paths.mjs";
39
- import { parseRequirements, coveredReqIds } from "../verify/trace.mjs";
40
- import { readBoard } from "../compile.mjs";
41
- import { reqId } from "../lib/contract.mjs";
42
-
43
- /**
44
- * Read a file, tolerating absence — every input to this projection is optional by design.
45
- * @param {string} p - Absolute path.
46
- * @returns {(string|null)} The contents, or null when the file is missing or unreadable.
47
- */
48
- function readIf(p) {
49
- try { return existsSync(p) ? readFileSync(p, "utf8") : null; } catch { return null; }
50
- }
51
-
52
- /**
53
- * The acceptance criteria on the LOCAL board that cover each requirement.
54
- *
55
- * The planning-time half of the join, and the authoritative one: a `covers:` clause is written when
56
- * the plan is still cheap to change and is reviewed at L1b, whereas `traces_to` is written by the
57
- * judge after the fact.
58
- *
59
- * NOT A SECOND COVERS-CLOSURE. Whether a requirement is covered is still decided by
60
- * `coveredReqIds` in the oracle — there is one implementation of that question and this is not it.
61
- * This walk exists only to keep the AC the closure discards, so the matrix can print WHICH
62
- * criterion covers the clause instead of only that one does.
63
- *
64
- * @param {Array<object>} board - Task entries from `readBoard` (the only parser that carries
65
- * `acceptance_criteria`; the scheduling view does not).
66
- * @returns {Map<string, Array<{task_id:string, ac:string}>>} REQ-id → the ACs naming it, in board
67
- * order. A requirement no AC covers is simply absent.
68
- */
69
- export function coveringAcs(board) {
70
- const out = new Map();
71
- for (const task of board || []) {
72
- for (const ac of task.acceptance_criteria || []) {
73
- if (typeof ac !== "object" || !Array.isArray(ac.covers)) continue;
74
- for (const raw of ac.covers) {
75
- const id = reqId(raw).toUpperCase();
76
- if (!out.has(id)) out.set(id, []);
77
- out.get(id).push({ task_id: task.id, ac: ac.text });
78
- }
79
- }
80
- }
81
- return out;
82
- }
83
-
84
- /**
85
- * Every criterion row ingest projected, tagged with the EVAL target its ledger belongs to.
86
- *
87
- * The target is read off the FILE NAME rather than the row: `.verdicts-<target>.jsonl` is written
88
- * per order, so the file is what says which dispatch produced the rows, and the same file is what
89
- * locates the WorkResult carrying that dispatch's T0 citations.
90
- *
91
- * @param {string} cwd - Project root.
92
- * @param {string} slug - Feature slug.
93
- * @returns {Array<object>} One entry per ledger line, each the stored row plus `target`. Malformed
94
- * lines are skipped; a missing evaluation directory yields [].
95
- */
96
- export function readVerdictRows(cwd, slug) {
97
- const dir = evaluationDir(cwd, slug);
98
- let files = [];
99
- try { files = readdirSync(dir).filter((f) => /^\.verdicts-.+\.jsonl$/.test(f)).sort(); } catch { return []; }
100
- const rows = [];
101
- for (const f of files) {
102
- const target = f.replace(/^\.verdicts-/, "").replace(/\.jsonl$/, "");
103
- for (const line of (readIf(join(dir, f)) || "").split(/\r?\n/)) {
104
- if (!line.trim()) continue;
105
- try { rows.push({ ...JSON.parse(line), target }); } catch { /* a truncated line is not a verdict */ }
106
- }
107
- }
108
- return rows;
109
- }
110
-
111
- /**
112
- * The T0 artifacts an EVAL dispatch re-hashed, read from the WorkResult it wrote.
113
- *
114
- * The verdict ledger records criteria, not citations, so the machine fact a generator cannot
115
- * fabricate lives one file over — in `results/<target>.json`. Read here rather than recomputed: the
116
- * evaluator's own citation is what the round was accepted on.
117
- *
118
- * @param {string} cwd - Project root.
119
- * @param {string} slug - Feature slug.
120
- * @param {string} target - The EVAL order suffix (`evaluate-r1`), from the ledger's file name.
121
- * @returns {Array<{scope_id:(string|null), path:(string|null), sha256:(string|null)}>} The cited
122
- * artifacts; [] when the result is absent, unreadable or cites none.
123
- */
124
- export function t0Citations(cwd, slug, target) {
125
- const body = readIf(join(resultsDir(cwd, slug), `${target}.json`));
126
- if (!body) return [];
127
- try {
128
- const cites = JSON.parse(body)?.verdict?.t0_citations;
129
- if (!Array.isArray(cites)) return [];
130
- return cites.map((c) => ({ scope_id: c?.scope_id ?? null, path: c?.path ?? null, sha256: c?.sha256 ?? null }));
131
- } catch { return []; }
132
- }
133
-
134
- /**
135
- * Project one run's requirement matrix from the artifacts on disk.
136
- *
137
- * @param {{cwd:string, slug:string, runId?:(string|null)}} opts - Project root, feature slug, and
138
- * the run to project; omitted, it is resolved from the run's own receipt.
139
- * @returns {{slug:string, run_id:(string|null), registry:boolean, totals:object,
140
- * rows:Array<object>, inconsistencies:Array<object>, ledger:object}} `rows` is one entry per
141
- * registered clause — its source, status, covering ACs, the criteria that named it and their
142
- * verdicts, and each criterion's T0 citations — with `evidence` one of `PASS`, `no evidence` or
143
- * `cut`. `inconsistencies` holds criteria tracing to a REQ no AC covers. `ledger` says how many
144
- * rows were read, projected, skipped as another run's, and left unkeyed. No registry ⇒ `rows` is
145
- * empty and `registry` is false: an empty projection, not an error.
146
- */
147
- export function projectRequirements({ cwd, slug, runId = undefined }) {
148
- const regText = readIf(requirementsFile(cwd, slug));
149
- const clauses = regText === null ? [] : parseRequirements(regText);
150
- const board = readBoard(cwd, slug);
151
- // `covered` decides; `acs` only says which criterion did the covering.
152
- const covered = coveredReqIds(board);
153
- const acs = coveringAcs(board);
154
- const run = runId === undefined ? readRunId(cwd, slug) : runId;
155
-
156
- const all = readVerdictRows(cwd, slug);
157
- const ledger = { rows_read: all.length, rows_projected: 0, rows_other_run: 0, rows_unknown_run: 0 };
158
- const mine = [];
159
- for (const r of all) {
160
- if (r.run_id === undefined || r.run_id === null || r.run_id === "") { ledger.rows_unknown_run++; continue; }
161
- if (run !== null && r.run_id === run) { ledger.rows_projected++; mine.push(r); continue; }
162
- ledger.rows_other_run++;
163
- }
164
-
165
- // The criteria that name each requirement, and the T0 artifacts the round that graded them cited.
166
- const t0Cache = new Map();
167
- /**
168
- * The cited T0 artifacts for one EVAL target, read once per target.
169
- * @param {string} target - The EVAL order suffix.
170
- * @returns {Array<object>} The citation rows.
171
- */
172
- const t0For = (target) => {
173
- if (!t0Cache.has(target)) t0Cache.set(target, t0Citations(cwd, slug, target));
174
- return t0Cache.get(target);
175
- };
176
-
177
- const byReq = new Map();
178
- const inconsistencies = [];
179
- for (const r of mine) {
180
- const anchors = Array.isArray(r.traces_to) ? r.traces_to : [];
181
- for (const raw of anchors) {
182
- const id = reqId(raw).toUpperCase();
183
- const entry = {
184
- criterion: r.criterion ?? "", dimension: r.dimension ?? "", verdict: r.verdict ?? "",
185
- confidence: r.confidence ?? null, evidence: r.evidence ?? "", target: r.target,
186
- run: r.run ?? null, run_id: r.run_id ?? null, t0: t0For(r.target),
187
- };
188
- // THE AUTHORITATIVE HALF DECIDES. An anchor pointing at a requirement no acceptance criterion
189
- // covers is a claim the plan never made; it is printed so somebody can reconcile it, and
190
- // counted as nothing.
191
- if (!covered.has(id)) { inconsistencies.push({ requirement: id, ...entry, why: "no acceptance criterion covers this requirement — the anchor resolves to nothing the plan claimed" }); continue; }
192
- if (!byReq.has(id)) byReq.set(id, []);
193
- byReq.get(id).push(entry);
194
- }
195
- }
196
-
197
- const rows = clauses.map((c) => {
198
- const id = c.id.toUpperCase();
199
- const covering = acs.get(id) || [];
200
- const criteria = byReq.get(id) || [];
201
- const cut = c.status !== "covered";
202
- const passed = criteria.some((x) => String(x.verdict).toUpperCase() === "PASS");
203
- return {
204
- id: c.id,
205
- source: c.source || "",
206
- clause: c.clause || "",
207
- status: c.status,
208
- covering_acs: covering,
209
- criteria,
210
- t0: [...new Set(criteria.flatMap((x) => x.t0.map((t) => t.sha256).filter(Boolean)))],
211
- evidence: cut ? "cut" : passed ? "PASS" : "no evidence",
212
- };
213
- });
214
-
215
- const totals = {
216
- total: rows.length,
217
- pass: rows.filter((r) => r.evidence === "PASS").length,
218
- cut: rows.filter((r) => r.evidence === "cut").length,
219
- no_evidence: rows.filter((r) => r.evidence === "no evidence").length,
220
- inconsistencies: inconsistencies.length,
221
- };
222
- return { slug, run_id: run, registry: regText !== null, totals, rows, inconsistencies, ledger };
223
- }
224
-
225
- /**
226
- * The one-line summary GATE L4 prints and GATE H's census cites.
227
- *
228
- * @param {object} r - A report from {@link projectRequirements}.
229
- * @returns {string} `15/17 PASS · 1 CUT (PO) · 1 no evidence (REQ-12 ← R12)`, or the empty-run
230
- * phrasing when there is no registry to project.
231
- */
232
- export function summaryLine(r) {
233
- if (!r.registry) return "n/a (no registry)";
234
- if (!r.totals.total) return "0 requirements registered";
235
- const gaps = r.rows.filter((x) => x.evidence === "no evidence");
236
- const named = gaps.slice(0, 3).map((x) => (x.source ? `${x.id} ← ${x.source}` : x.id)).join(", ");
237
- const parts = [`${r.totals.pass}/${r.totals.total} PASS`];
238
- if (r.totals.cut) parts.push(`${r.totals.cut} CUT (PO)`);
239
- if (r.totals.no_evidence) parts.push(`${r.totals.no_evidence} no evidence (${named}${gaps.length > 3 ? ", …" : ""})`);
240
- if (r.totals.inconsistencies) parts.push(`${r.totals.inconsistencies} inconsistency ${r.totals.inconsistencies === 1 ? "row" : "rows"}`);
241
- return parts.join(" · ");
242
- }
243
-
244
- /**
245
- * Render the matrix as a fixed-width table for a human reading a gate block.
246
- * @param {object} r - A report from {@link projectRequirements}.
247
- * @returns {string} The header, one row per requirement, the summary line and any inconsistencies.
248
- */
249
- export function renderTable(r) {
250
- const out = [];
251
- const head = ["REQ", "source", "status", "covering AC", "criterion", "verdict", "T0"];
252
- const rows = r.rows.map((x) => [
253
- x.id, x.source || "—", x.evidence,
254
- x.covering_acs.length ? `${x.covering_acs[0].task_id}: ${x.covering_acs[0].ac.slice(0, 40)}${x.covering_acs.length > 1 ? ` (+${x.covering_acs.length - 1})` : ""}` : "—",
255
- x.criteria.length ? `${x.criteria[0].criterion.slice(0, 40)}${x.criteria.length > 1 ? ` (+${x.criteria.length - 1})` : ""}` : "—",
256
- x.criteria.length ? x.criteria.map((c) => c.verdict).join(",") : "—",
257
- x.t0.length ? x.t0.map((h) => String(h).slice(0, 12)).join(",") : "—",
258
- ]);
259
- if (rows.length) {
260
- const w = head.map((h, i) => Math.max(h.length, ...rows.map((row) => String(row[i]).length)));
261
- const line = (row) => row.map((c, i) => String(c).padEnd(w[i])).join(" ").trimEnd();
262
- out.push(line(head), line(w.map((n) => "-".repeat(n))), ...rows.map(line), "");
263
- }
264
- out.push(`Requirements: ${summaryLine(r)}`);
265
- out.push(`run_id: ${r.run_id ?? "unknown"} · ledger rows read ${r.ledger.rows_read}, projected ${r.ledger.rows_projected}, other run ${r.ledger.rows_other_run}, unkeyed (run_id: unknown) ${r.ledger.rows_unknown_run}`);
266
- for (const i of r.inconsistencies) {
267
- out.push(` ⚠ ${i.requirement}: "${String(i.criterion).slice(0, 60)}" ${i.verdict} — ${i.why}`);
268
- }
269
- return out.join("\n");
270
- }
271
-
272
- /**
273
- * Whether a board that has tasks carries any `covers:` clause while a requirements registry exists.
274
- *
275
- * Two bars answer "is this requirement planned": L1b is satisfied by a scope contract's own covers
276
- * list, while the matrix reads only the acceptance criteria's `(covers: REQ-…)` clauses. A board
277
- * regenerated with none crossed every gate, and the matrix then read "no evidence" under every
278
- * requirement of a run whose criteria had passed. Measured on two consecutive runs of one pitch:
279
- * thirty clauses on one board, zero on the next, the same instruction both times.
280
- *
281
- * @param {string} cwd - Project root.
282
- * @param {string} slug - Feature slug.
283
- * @returns {(string|null)} A reason phrased for the board's writer, or null when there is no
284
- * registry, no board yet, or at least one clause.
285
- */
286
- export function boardCoversProblem(cwd, slug) {
287
- const clauses = parseRequirements(readIf(requirementsFile(cwd, slug)) || "");
288
- if (!clauses.length) return null;
289
- const board = readBoard(cwd, slug);
290
- if (!Array.isArray(board) || !board.length) return null;
291
- if (coveringAcs(board).size) return null;
292
- return `the board's ${board.length} tasks carry no \`(covers: REQ-…)\` clause while the registry holds ${clauses.length} ` +
293
- "requirements — every acceptance criterion that grades a requirement carries its covers clause, or the requirements " +
294
- "matrix reads no evidence for any of them";
295
- }
296
-
297
- export const ARGV_SPEC = {
298
- usage: "harness.mjs probe requirements --slug <slug> [--run-id <id>] [--format json|table] [--board-check] [--cwd <dir>]",
299
- _: { arity: 0, max: 0, name: "(no positional operands)" },
300
- slug: { type: "str", required: true },
301
- "run-id": { type: "str" },
302
- format: { type: "enum", values: ["json", "table"], default: "json" },
303
- "board-check": { type: "flag" },
304
- cwd: { type: "path" },
305
- };
306
-
307
- /**
308
- * Answer "which requirement has evidence, and from which criterion" from the artifacts on disk.
309
- *
310
- * @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
311
- * @returns {Promise<void>} Exits 0 with the projection — an absent run, an absent registry and an
312
- * absent verdict are all answers, never errors.
313
- */
314
- export async function cli(rawArgv) {
315
- const args = runArgs(ARGV_SPEC, rawArgv);
316
- const cwd = resolve(args.cwd || process.cwd());
317
- if (args.boardCheck) {
318
- // `--board-check` answers one question for the run's send-back: may this board stand?
319
- const reason = boardCoversProblem(cwd, args.slug);
320
- console.log(JSON.stringify({ ok: !reason, reason }));
321
- process.exit(reason ? 1 : 0);
322
- }
323
- const report = projectRequirements({ cwd, slug: args.slug, runId: args.runId ?? undefined });
324
- console.log(args.format === "table" ? renderTable(report) : JSON.stringify(report, null, 2));
325
- process.exit(0);
326
- }
327
-
328
- if (isMain(import.meta.url)) cli(process.argv.slice(2));