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.
- package/.claude/settings.local.example.json +2 -3
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +115 -56
- package/README.md +101 -143
- package/SECURITY.md +16 -25
- package/agents/orient.md +14 -0
- package/agents/qa-edge-hunter.md +13 -0
- package/agents/spec-evaluator.md +14 -0
- package/agents/task-executor.md +14 -0
- package/bin/init.mjs +13 -16
- package/bin/lib/grant.mjs +8 -19
- package/commands/build.md +9 -14
- package/commands/eval.md +3 -3
- package/commands/hammer.md +9 -5
- package/commands/retro.md +12 -11
- package/commands/scopes.md +1 -12
- package/commands/ship.md +45 -88
- package/commands/wire.md +1 -8
- package/hooks/dispatch-receipt.mjs +94 -8
- package/hooks/hooks.json +0 -41
- package/hooks/lib/decision.mjs +6 -9
- package/hooks/lib/tier.mjs +50 -0
- package/hooks/sandbox-guard.mjs +140 -64
- package/kernel/gate.mjs +203 -22
- package/kernel/harness.mjs +20 -67
- package/kernel/init/run.mjs +175 -523
- package/kernel/lib/contract.mjs +9 -93
- package/kernel/lib/floor.mjs +64 -0
- package/kernel/lib/paths.mjs +39 -112
- package/kernel/order.mjs +157 -0
- package/kernel/probe/digest.mjs +2 -38
- package/kernel/schemas/domain.schema.json +146 -1125
- package/kernel/schemas/work-order.schema.json +8 -4
- package/kernel/schemas/work-result.schema.json +1 -1
- package/kernel/verify/envelope.mjs +2 -2
- package/kernel/verify/skills.mjs +27 -8
- package/kernel/verify/t0.mjs +2 -2
- package/package.json +4 -2
- package/skills/building/SKILL.md +144 -0
- package/skills/building/resources/gates.md +76 -0
- package/skills/building/resources/guidance.md +23 -0
- package/skills/{translator/references/preservation-rules.md → building/resources/intake-preservation.md} +1 -1
- package/skills/building/resources/intake.md +157 -0
- package/skills/building/resources/orders.md +69 -0
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/doc-schemas.md +1 -1
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/task-generation.md +13 -13
- package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/test-surface.md +6 -7
- package/skills/building/resources/plan-scopes.md +95 -0
- package/skills/building/resources/plan-spec.md +102 -0
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/_index.tmpl.md +1 -1
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/assess-report.tmpl.md +10 -11
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/run-state.tmpl.md +4 -4
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/scope-summary.tmpl.md +2 -2
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/synthesis.tmpl.md +4 -4
- package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/usecase.tmpl.md +2 -2
- package/skills/building/resources/plan-wire.md +82 -0
- package/skills/building/resources/plan.md +98 -0
- package/skills/building/resources/retro-research.md +60 -0
- package/skills/building/resources/retro-scan.md +49 -0
- package/skills/building/resources/retro.md +225 -0
- package/skills/building/resources/rounds.md +78 -0
- package/skills/building/resources/stop.md +127 -0
- package/skills/orient/SKILL.md +37 -37
- package/skills/qa-edge-hunter/SKILL.md +14 -14
- package/skills/shapeup/SKILL.md +5 -5
- package/skills/shapeup/resources/breadboarding.md +1 -1
- package/skills/shapeup/resources/context-compaction.md +5 -5
- package/skills/shapeup/resources/shaping.md +1 -1
- package/skills/spec-evaluator/SKILL.md +17 -21
- package/skills/spec-evaluator/references/dimension-contract.md +1 -1
- package/skills/spec-evaluator/references/dimensions/completeness.md +3 -3
- package/skills/spec-evaluator/references/dimensions/spec-conformance.md +1 -1
- package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +5 -5
- package/skills/task-executor/SKILL.md +11 -15
- package/commands/hill.md +0 -10
- package/hooks/gate-intake.mjs +0 -109
- package/hooks/gate-zerowork.mjs +0 -349
- package/hooks/tier-guard.mjs +0 -162
- package/kernel/compile.mjs +0 -1419
- package/kernel/init/fit.mjs +0 -199
- package/kernel/init/run-args.mjs +0 -206
- package/kernel/lib/breadboard.mjs +0 -165
- package/kernel/probe/attempts.mjs +0 -161
- package/kernel/probe/concurrency.mjs +0 -535
- package/kernel/probe/eval.mjs +0 -405
- package/kernel/probe/hunt.mjs +0 -104
- package/kernel/probe/leg.mjs +0 -229
- package/kernel/probe/owner.mjs +0 -142
- package/kernel/probe/requirements.mjs +0 -328
- package/kernel/probe/resume.mjs +0 -1110
- package/kernel/probe/rounds.mjs +0 -115
- package/kernel/probe/staged.mjs +0 -83
- package/kernel/probe/stats.mjs +0 -477
- package/kernel/probe/t0.mjs +0 -115
- package/kernel/reduce/board.mjs +0 -350
- package/kernel/reduce/graph.mjs +0 -482
- package/kernel/reduce/hill.mjs +0 -322
- package/kernel/reduce/ingest.mjs +0 -767
- package/kernel/reduce/leftovers.mjs +0 -159
- package/kernel/reduce/ship.mjs +0 -529
- package/kernel/reduce/snapshot.mjs +0 -291
- package/kernel/reduce/verdict.mjs +0 -172
- package/kernel/report/export.mjs +0 -384
- package/kernel/report/facts.mjs +0 -234
- package/kernel/verify/budget.mjs +0 -153
- package/kernel/verify/build.mjs +0 -428
- package/kernel/verify/dispatch.mjs +0 -114
- package/kernel/verify/spec.mjs +0 -975
- package/kernel/verify/trace.mjs +0 -604
- package/skills/ba-pitch-analyzer/SKILL.md +0 -204
- package/skills/coach/SKILL.md +0 -397
- package/skills/hill-chart/SKILL.md +0 -154
- package/skills/hill-chart/assets/dashboard.template.html +0 -1432
- package/skills/scope-architect/SKILL.md +0 -174
- package/skills/scope-hammer/SKILL.md +0 -231
- package/skills/solution-architect/SKILL.md +0 -158
- package/skills/tech-lead/SKILL.md +0 -151
- package/skills/tech-lead/references/gates.md +0 -605
- package/skills/tech-lead/references/protocol.md +0 -889
- package/skills/tech-lead/references/tiny-lane.md +0 -53
- package/skills/tech-lead/workflows/shapeup-run.js +0 -2187
- package/skills/translator/SKILL.md +0 -258
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/contract-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ddd-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/integration-analysis.md +0 -0
- /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ux-behavior-patterns.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/api-feasibility.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/be-service.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/offline-storage.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/third-party-api.contract.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/context-map.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/event-choreography.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/migration-plan.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/team-handoff.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/domain-model.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/feedback.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/integration.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-board.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-spike.tmpl.md +0 -0
- /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task.tmpl.md +0 -0
- /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));
|