shapeup-sdlc 3.18.2 → 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 +29 -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
package/kernel/reduce/ship.mjs
DELETED
|
@@ -1,529 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SHIP REPORT — freeze one reviewable markdown report at GATE L4 (ADR-0001).
|
|
3
|
-
//
|
|
4
|
-
// WHY THIS EXISTS.
|
|
5
|
-
//
|
|
6
|
-
// Everything a reviewer wants from a run is in the LOCAL tier and therefore gitignored: the
|
|
7
|
-
// verdict and its refuted criteria, the QA findings, the T0 artifacts, the adjudicated decisions,
|
|
8
|
-
// the cut list. A pull request showed the spec and the code and NONE of the evidence that the one
|
|
9
|
-
// matched the other. The obvious fix — commit those artifacts — is the wrong one: they mutate
|
|
10
|
-
// throughout a run, so the deliverable tier would churn on every attempt and every round.
|
|
11
|
-
//
|
|
12
|
-
// So the split is by TIME rather than by artifact. They stay local while they change; their
|
|
13
|
-
// conclusions are frozen ONCE, when the run ends, into `shapeup/<slug>/REPORT.md`.
|
|
14
|
-
//
|
|
15
|
-
// EVERY NUMBER HERE IS DERIVED, NEVER PASSED IN. The report reduces over files the harness already
|
|
16
|
-
// wrote — the trial ledger, the verdict artifacts, the board, the discovery ledger, the round
|
|
17
|
-
// ledger. Nothing in it is a claim the orchestrator makes about its own run, which is the whole
|
|
18
|
-
// discipline this harness exists to enforce; a ship report that accepted a summary as input would
|
|
19
|
-
// be the "agent reports done" pathology wearing a deliverable's clothes.
|
|
20
|
-
//
|
|
21
|
-
// Zero dependencies, zero network.
|
|
22
|
-
//
|
|
23
|
-
// Usage:
|
|
24
|
-
// node `harness reduce ship` --slug <slug> [--cwd <dir>] [--verdict PASS|FAIL|not-evaluated]
|
|
25
|
-
// [--qa run|skipped] [--stdout]
|
|
26
|
-
//
|
|
27
|
-
// Exit: 0 written (path on stdout), 2 usage error.
|
|
28
|
-
|
|
29
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, rmSync } from "node:fs";
|
|
30
|
-
import { join, dirname } from "node:path";
|
|
31
|
-
import { runArgs } from "../lib/argv.mjs";
|
|
32
|
-
import {
|
|
33
|
-
report as reportPath, tasksDir, verdictsDir, trials, evaluationDir, qaDir,
|
|
34
|
-
roundLedger, discoveryLedger, receipt as receiptPath, harnessRun, relShared,
|
|
35
|
-
activeOrder, runArgsPath, readReceipt, runIdFromReceipt, hammerCensus, LOCAL,
|
|
36
|
-
} from "../lib/paths.mjs";
|
|
37
|
-
import { readTrials } from "../verify/t0.mjs";
|
|
38
|
-
import { ratchetReport } from "../probe/stats.mjs";
|
|
39
|
-
import { projectRequirements, summaryLine } from "../probe/requirements.mjs";
|
|
40
|
-
import { deriveRounds } from "../probe/rounds.mjs";
|
|
41
|
-
import { collectDiff, scanDiff, summarize } from "./leftovers.mjs";
|
|
42
|
-
import { chartersRun } from "../probe/hunt.mjs";
|
|
43
|
-
|
|
44
|
-
/** @returns {string} Today as `YYYY-MM-DD` (UTC). */
|
|
45
|
-
const today = () => new Date().toISOString().slice(0, 10);
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Read a file, tolerating absence — every section of this report is optional, because a run can
|
|
49
|
-
* legitimately end at GATE H with no QA pass or no evaluation.
|
|
50
|
-
* @param {string} p - Path.
|
|
51
|
-
* @returns {(string|null)} Contents, or null.
|
|
52
|
-
*/
|
|
53
|
-
function readIf(p) {
|
|
54
|
-
try { return existsSync(p) ? readFileSync(p, "utf8") : null; } catch { return null; }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Parse a Markdown frontmatter block into a flat scalar map.
|
|
59
|
-
* @param {string} text - Document text.
|
|
60
|
-
* @returns {Object<string,string>} Top-level `key: value` pairs.
|
|
61
|
-
*/
|
|
62
|
-
export function frontmatter(text) {
|
|
63
|
-
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text || "");
|
|
64
|
-
if (!m) return {};
|
|
65
|
-
const fm = {};
|
|
66
|
-
for (const line of m[1].split(/\r?\n/)) {
|
|
67
|
-
const kv = /^([A-Za-z_][\w-]*):\s*(.*)$/.exec(line.trim());
|
|
68
|
-
if (kv) fm[kv[1]] = kv[2].replace(/^["']|["']$/g, "");
|
|
69
|
-
}
|
|
70
|
-
return fm;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Board census, from task frontmatter — the same two-source discipline the GATE L2 block uses, reduced to
|
|
75
|
-
* the authoritative one.
|
|
76
|
-
* @param {string} cwd - Project root.
|
|
77
|
-
* @param {string} slug - Feature slug.
|
|
78
|
-
* @returns {{total:number, done:number, unfinished:string[]}} Counts and the ids still open.
|
|
79
|
-
*/
|
|
80
|
-
export function boardCensus(cwd, slug) {
|
|
81
|
-
const dir = tasksDir(cwd, slug);
|
|
82
|
-
const out = { total: 0, done: 0, unfinished: [], anchors: {} };
|
|
83
|
-
if (!existsSync(dir)) return out;
|
|
84
|
-
for (const f of readdirSync(dir)) {
|
|
85
|
-
if (!/^TASK-[\w.-]+\.md$/i.test(f)) continue;
|
|
86
|
-
const body = readIf(join(dir, f)) || "";
|
|
87
|
-
const fm = frontmatter(body);
|
|
88
|
-
const id = fm.id || f.replace(/\.md$/, "");
|
|
89
|
-
out.total++;
|
|
90
|
-
// The COMMITTED anchor for this board id. `use_case_refs` is the tier-direction rule's own
|
|
91
|
-
// sanctioned direction (LOCAL names SHARED), and it is what the frozen report cites instead of
|
|
92
|
-
// the id — boards renumber per machine, use cases do not.
|
|
93
|
-
const ucs = String(fm.use_case_refs ?? "").replace(/^\[|\]$/g, "")
|
|
94
|
-
.split(",").map((x) => x.trim()).filter(Boolean);
|
|
95
|
-
out.anchors[id] = ucs;
|
|
96
|
-
if (fm.status === "done") out.done++;
|
|
97
|
-
else out.unfinished.push(id);
|
|
98
|
-
}
|
|
99
|
-
out.unfinished.sort();
|
|
100
|
-
return out;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Replace every board id in free prose with its committed anchor.
|
|
105
|
-
*
|
|
106
|
-
* THE WRITE BOUNDARY, not the column, and that is the whole point. Board ids reached the frozen
|
|
107
|
-
* report three different ways — the unfinished-task callout, the covering-AC column's own prefix,
|
|
108
|
-
* and INSIDE acceptance-criterion prose a planner wrote ("given the seeded todos (TASK-006)"). The
|
|
109
|
-
* third is upstream free text, so a fix that only changes what the columns interpolate still
|
|
110
|
-
* commits a file the next run's spec-lint reds. Everything written into the committed report passes
|
|
111
|
-
* through here.
|
|
112
|
-
*
|
|
113
|
-
* An id with no resolvable use case becomes a neutral phrase rather than the id: the report loses a
|
|
114
|
-
* pointer that never resolved off this machine anyway, and keeps the sentence around it.
|
|
115
|
-
*
|
|
116
|
-
* A path into the run trace is the same leak by another spelling: the QA findings section quotes
|
|
117
|
-
* the hunt report, which points at the discovery ledger by its local path, and the next run's
|
|
118
|
-
* spec-lint refused the report for it. The path becomes "the run trace", and the sentence stays.
|
|
119
|
-
*
|
|
120
|
-
* @param {*} text - Any value destined for the committed report.
|
|
121
|
-
* @param {Record<string, string[]>} anchors - Board id → its `use_case_refs`.
|
|
122
|
-
* @returns {string} The text with every `TASK-…` replaced by a stable anchor and every run-trace
|
|
123
|
-
* path by a phrase.
|
|
124
|
-
*/
|
|
125
|
-
export function deboard(text, anchors = {}) {
|
|
126
|
-
const esc = LOCAL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
127
|
-
return String(text ?? "")
|
|
128
|
-
.replace(/\bTASK-[A-Za-z0-9][\w.-]*/g, (id) => {
|
|
129
|
-
const ucs = anchors[id];
|
|
130
|
-
if (ucs && ucs.length) return ucs.join("/");
|
|
131
|
-
return "a board task";
|
|
132
|
-
})
|
|
133
|
-
.replace(new RegExp(`\`?${esc}/[^\\s\`]+\`?`, "g"), (m) => {
|
|
134
|
-
const tail = (m.replace(/`/g, "").match(/[)\]},.;:'"]+$/) || [""])[0];
|
|
135
|
-
return `the run trace${tail}`;
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Per-scope T0 outcome, reduced from the trial ledger.
|
|
141
|
-
*
|
|
142
|
-
* The LAST trial per scope whose tree survived (`kept`/`rebased`) is the one describing the code
|
|
143
|
-
* that actually shipped — a `reverted` trial's tree was thrown away, so reporting its score would
|
|
144
|
-
* describe work nobody can see.
|
|
145
|
-
*
|
|
146
|
-
* @param {string} cwd - Project root.
|
|
147
|
-
* @param {string} slug - Feature slug.
|
|
148
|
-
* @returns {Array<{scope_id:string, score:object, status:string, delta:string, trials:number}>}
|
|
149
|
-
* One row per scope, ordered by scope id.
|
|
150
|
-
*/
|
|
151
|
-
export function t0Summary(cwd, slug) {
|
|
152
|
-
const rows = readTrials(trials(cwd, slug));
|
|
153
|
-
const byScope = new Map();
|
|
154
|
-
for (const t of rows) {
|
|
155
|
-
if (!t.scope_id) continue;
|
|
156
|
-
const e = byScope.get(t.scope_id) || { scope_id: t.scope_id, trials: 0, last: null };
|
|
157
|
-
e.trials++;
|
|
158
|
-
if (t.status === "kept" || t.status === "rebased") e.last = t;
|
|
159
|
-
byScope.set(t.scope_id, e);
|
|
160
|
-
}
|
|
161
|
-
return [...byScope.values()]
|
|
162
|
-
.map((e) => ({
|
|
163
|
-
scope_id: e.scope_id,
|
|
164
|
-
trials: e.trials,
|
|
165
|
-
score: e.last?.score ?? null,
|
|
166
|
-
status: e.last?.status ?? "no surviving trial",
|
|
167
|
-
delta: e.last?.delta ?? "",
|
|
168
|
-
}))
|
|
169
|
-
.sort((a, b) => a.scope_id.localeCompare(b.scope_id));
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Count the immutable verdict artifacts on disk — the evidence the evaluator had to cite.
|
|
174
|
-
* @param {string} cwd - Project root.
|
|
175
|
-
* @param {string} slug - Feature slug.
|
|
176
|
-
* @returns {number} How many artifacts exist.
|
|
177
|
-
*/
|
|
178
|
-
export function verdictArtifactCount(cwd, slug) {
|
|
179
|
-
const dir = verdictsDir(cwd, slug);
|
|
180
|
-
if (!existsSync(dir)) return 0;
|
|
181
|
-
try { return readdirSync(dir).filter((f) => f.endsWith(".json")).length; } catch { return 0; }
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Lift a named section out of a markdown document.
|
|
186
|
-
* @param {(string|null)} md - Document text.
|
|
187
|
-
* @param {RegExp} heading - Matches the heading line that opens the section.
|
|
188
|
-
* @returns {(string|null)} The section body, trimmed, or null when absent/empty.
|
|
189
|
-
*/
|
|
190
|
-
export function section(md, heading) {
|
|
191
|
-
if (!md) return null;
|
|
192
|
-
const lines = md.split(/\r?\n/);
|
|
193
|
-
const start = lines.findIndex((l) => heading.test(l));
|
|
194
|
-
if (start === -1) return null;
|
|
195
|
-
const level = (lines[start].match(/^#+/) || ["#"])[0].length;
|
|
196
|
-
const out = [];
|
|
197
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
198
|
-
const h = lines[i].match(/^(#+)\s/);
|
|
199
|
-
if (h && h[1].length <= level) break;
|
|
200
|
-
out.push(lines[i]);
|
|
201
|
-
}
|
|
202
|
-
const body = out.join("\n").trim();
|
|
203
|
-
return body || null;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* The report's opening line when the ship is not a passing one — null for a PASS.
|
|
208
|
-
*
|
|
209
|
-
* @param {string} verdict - The run's final verdict (`PASS`, `FAIL`, `not-evaluated`, …).
|
|
210
|
-
* @param {({verdict?:string, cut_list?:Array}|null)} census - GATE H's census as data, or null.
|
|
211
|
-
* @returns {(string|null)} One markdown line naming the verdict, and the cut list that cleared it
|
|
212
|
-
* when a census is on record; null when the verdict is PASS.
|
|
213
|
-
*/
|
|
214
|
-
export function shipHeadline(verdict, census) {
|
|
215
|
-
if (verdict === "PASS") return null;
|
|
216
|
-
const cuts = Array.isArray(census?.cut_list) ? census.cut_list.length : null;
|
|
217
|
-
const why = census
|
|
218
|
-
? `GATE H's baseline comparison cleared it${census.verdict ? ` (census: ${census.verdict})` : ""}` +
|
|
219
|
-
(cuts != null ? ` with ${cuts} item${cuts === 1 ? "" : "s"} cut` : "")
|
|
220
|
-
: "no GATE H census was on record when it shipped";
|
|
221
|
-
// A FAIL is a judgement that some criterion failed; anything else here means no judgement was
|
|
222
|
-
// made at all, and the line must not claim criteria failed that nobody graded.
|
|
223
|
-
if (verdict === "FAIL") {
|
|
224
|
-
return `> **Shipped with a FAIL verdict.** Not every criterion passed; ${why}. Read the failed ` +
|
|
225
|
-
"criteria and the cut list below before treating this feature as done.";
|
|
226
|
-
}
|
|
227
|
-
return `> **Shipped without a passing verdict (${verdict}).** No criterion was graded as passing; ${why}. ` +
|
|
228
|
-
"Nothing below is evidence that this feature works.";
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Assemble the report. Pure given its inputs, so the structural tests can assert its shape
|
|
233
|
-
* without a filesystem.
|
|
234
|
-
* @param {object} facts - Derived facts (slug, verdict, board, t0, decisions, findings, …).
|
|
235
|
-
* @returns {string} The markdown document.
|
|
236
|
-
*/
|
|
237
|
-
export function buildReport(facts) {
|
|
238
|
-
const {
|
|
239
|
-
slug, at, verdict, qa, rounds, roundsJudged, board, t0, artifacts, ratchet,
|
|
240
|
-
evalCriteria, evalBugs, qaFindings, decisions, discovered, intakeSha, leftovers, requirements,
|
|
241
|
-
census = null,
|
|
242
|
-
} = facts;
|
|
243
|
-
|
|
244
|
-
const L = [];
|
|
245
|
-
L.push("---", "type: ship-report", `feature: ${slug}`, `date: ${at}`,
|
|
246
|
-
`verdict: ${verdict}`, `rounds_used: ${rounds ?? "~"}`, `rounds_judged: ${roundsJudged ?? "~"}`, `qa: ${qa}`,
|
|
247
|
-
`intake_sha256: ${intakeSha ?? "~"}`, "---", "");
|
|
248
|
-
L.push(`# ${slug} — ship report`, "");
|
|
249
|
-
// A SHIP IS NOT A PASS, AND THE FIRST LINE SAYS WHICH ONE THIS IS. A run whose verdict FAILed can
|
|
250
|
-
// still ship — GATE H compares against the baseline, not the ideal, and a cut list can clear it —
|
|
251
|
-
// and its status then reads `shipped` like any other. The verdict sat in a table cell below the
|
|
252
|
-
// fold, so a reader who stopped at the title took a shipped FAIL for a passing build.
|
|
253
|
-
const headline = shipHeadline(verdict, census);
|
|
254
|
-
if (headline) L.push(headline, "");
|
|
255
|
-
L.push("Frozen at GATE L4. Every figure below is derived from run artifacts on disk — the trial",
|
|
256
|
-
"ledger, the verdict artifacts, the board — never from a summary of the run.", "");
|
|
257
|
-
|
|
258
|
-
L.push("## Outcome", "");
|
|
259
|
-
L.push("| | |", "|---|---|");
|
|
260
|
-
L.push(`| Verdict | **${verdict}** |`);
|
|
261
|
-
L.push(`| Rounds used | ${rounds ?? "—"} |`);
|
|
262
|
-
// A round built and a round judged are different facts — a round can die before EVAL ever sees
|
|
263
|
-
// it, so this row is its own line rather than folded into "Rounds used" above. Omitted when EVAL
|
|
264
|
-
// never ran at all, the same way the sections below it are.
|
|
265
|
-
if (roundsJudged != null) L.push(`| Rounds judged | ${roundsJudged} |`);
|
|
266
|
-
L.push(`| Board | ${board.done}/${board.total} tasks done |`);
|
|
267
|
-
L.push(`| T0 artifacts | ${artifacts} |`);
|
|
268
|
-
L.push(`| QA | ${qa} |`);
|
|
269
|
-
L.push("");
|
|
270
|
-
|
|
271
|
-
if (board.unfinished.length) {
|
|
272
|
-
// Anchored, never enumerated by board id: the ids renumber per machine, and a committed file
|
|
273
|
-
// carrying one reds the NEXT run of this pitch at L1b.
|
|
274
|
-
const unfinishedAnchors = [...new Set(board.unfinished.flatMap((id) => board.anchors?.[id] ?? []))];
|
|
275
|
-
L.push(`> **${board.unfinished.length} task(s) did not finish**${unfinishedAnchors.length ? ` — use cases: ${unfinishedAnchors.join(", ")}` : ""}.`,
|
|
276
|
-
"> The verdict above grades what was built, not what was planned.", "");
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Leftovers — advisory, and a SECTION rather than a verdict. It was a Stop hook that printed once
|
|
280
|
-
// into a transcript; here it lands in the artifact a human reads at GATE L4 and a teammate finds
|
|
281
|
-
// on `git pull`, which is the difference between a note and a record.
|
|
282
|
-
if (leftovers?.length) {
|
|
283
|
-
L.push("## Leftovers (advisory)", "");
|
|
284
|
-
L.push("Markers in lines this run ADDED. Not a gate and not part of the verdict — a cleanup list.", "");
|
|
285
|
-
for (const line of leftovers) L.push(`- ${line}`);
|
|
286
|
-
L.push("");
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (t0.length) {
|
|
290
|
-
L.push("## Verification (T0)", "");
|
|
291
|
-
L.push("The surviving trial per scope — the one describing code that is actually on the branch.", "");
|
|
292
|
-
L.push("| scope | fixtures | regressions | trials | last status | delta |", "|---|---|---|---|---|---|");
|
|
293
|
-
for (const s of t0) {
|
|
294
|
-
const f = s.score ? `${s.score.fixtures_passed}/${s.score.fixtures_total}` : "—";
|
|
295
|
-
const r = s.score?.regressions != null ? String(s.score.regressions) : "—";
|
|
296
|
-
L.push(`| ${s.scope_id} | ${f} | ${r} | ${s.trials} | ${s.status} | ${s.delta || "—"} |`);
|
|
297
|
-
}
|
|
298
|
-
L.push("");
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// The requirement matrix — the way back from a verdict to the clause the pitch asked for, frozen
|
|
302
|
-
// at the one moment the run's local evidence still exists. Omitted entirely when the run has no
|
|
303
|
-
// registry: a table of nothing reads as "no requirements", which is a different claim from "this
|
|
304
|
-
// run predates the registry". Derived like every other figure here, by the same probe the L4 line
|
|
305
|
-
// and GATE H's census read, so the three cannot disagree.
|
|
306
|
-
if (requirements?.registry && requirements.rows.length) {
|
|
307
|
-
L.push("## Requirements", "");
|
|
308
|
-
L.push("One row per registered clause. A requirement has evidence when an acceptance criterion",
|
|
309
|
-
"covers it AND a criterion grading it passed — `covers:` is the join, the judge's anchor is the",
|
|
310
|
-
"path back. This is a projection, never a verdict: it never blocked this ship.", "");
|
|
311
|
-
L.push(`**${summaryLine(requirements)}** · run \`${requirements.run_id ?? "unknown"}\``, "");
|
|
312
|
-
// A clause, an AC and a criterion are all free prose, and a literal pipe in any of them breaks
|
|
313
|
-
// the row into columns nobody wrote — a frozen report that misrenders its own evidence.
|
|
314
|
-
/**
|
|
315
|
-
* Escape a free-prose value for a Markdown table cell.
|
|
316
|
-
* @param {string} s - The value.
|
|
317
|
-
* @returns {string} The value with every literal pipe escaped.
|
|
318
|
-
*/
|
|
319
|
-
const cell = (s) => String(s).replace(/\|/g, "\\|");
|
|
320
|
-
L.push("| REQ | source | evidence | covering AC | criterion | T0 |", "|---|---|---|---|---|---|");
|
|
321
|
-
for (const r of requirements.rows) {
|
|
322
|
-
const ac = r.covering_acs.length
|
|
323
|
-
? `${deboard(r.covering_acs[0].task_id, facts.board?.anchors)}: ${deboard(r.covering_acs[0].ac, facts.board?.anchors)}${r.covering_acs.length > 1 ? ` (+${r.covering_acs.length - 1})` : ""}`
|
|
324
|
-
: "—";
|
|
325
|
-
const crit = r.criteria.length ? `${r.criteria[0].criterion}${r.criteria.length > 1 ? ` (+${r.criteria.length - 1})` : ""} → ${r.criteria.map((c) => c.verdict).join(",")}` : "—";
|
|
326
|
-
const t0h = r.t0.length ? r.t0.map((h) => String(h).slice(0, 12)).join(", ") : "—";
|
|
327
|
-
L.push(`| ${r.id} | ${cell(r.source || "—")} | ${r.evidence} | ${cell(ac)} | ${cell(crit)} | ${t0h} |`);
|
|
328
|
-
}
|
|
329
|
-
L.push("");
|
|
330
|
-
if (requirements.inconsistencies.length) {
|
|
331
|
-
L.push("Anchored to a requirement no acceptance criterion covers — reconcile, do not count as evidence:", "");
|
|
332
|
-
for (const i of requirements.inconsistencies) L.push(`- ${i.requirement} ← "${i.criterion}" (${i.verdict})`);
|
|
333
|
-
L.push("");
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// The ratchet aggregate is derived, ~10 scalars that do not grow with the run, which is why it
|
|
338
|
-
// can live in the committed tier while `metrics/` correctly stays gitignored (ADR-0001: a
|
|
339
|
-
// committed shard keyed on $HOSTNAME only grows). Without this the instrument existed and was
|
|
340
|
-
// never read — the trial ledger it reduces is harvested at SHIP or lost with the local tier.
|
|
341
|
-
if (ratchet && ratchet.trials > 0) {
|
|
342
|
-
L.push("## Ratchet", "");
|
|
343
|
-
L.push("Measured over this run's trial ledger. A monotone series is a ratchet working; a flat or",
|
|
344
|
-
"sawtooth series says the loop is still a budgeted retry loop wearing a ratchet's shape.", "");
|
|
345
|
-
L.push("| | |", "|---|---|");
|
|
346
|
-
L.push(`| Trials | ${ratchet.trials} across ${ratchet.scopes} scope(s), ${ratchet.scopes_multi_trial} with more than one attempt |`);
|
|
347
|
-
L.push(`| Improvement rate | ${ratchet.improvement_rate} — kept ÷ trials after the first |`);
|
|
348
|
-
L.push(`| Monotone rate | ${ratchet.monotone_rate} — multi-trial scopes whose score never decreased |`);
|
|
349
|
-
L.push(`| Sawtooth count | ${ratchet.sawtooth_count} — a revert immediately after a keep |`);
|
|
350
|
-
L.push(`| Mean trials to green | ${ratchet.mean_trials_to_green ?? "— (no scope reached green)"} |`);
|
|
351
|
-
const hist = Object.entries(ratchet.status_histogram).map(([k, v]) => `${k} ${v}`).join(", ");
|
|
352
|
-
L.push(`| Statuses | ${hist || "—"} |`);
|
|
353
|
-
L.push("");
|
|
354
|
-
// A zero improvement_rate means one of two opposite things, and the number alone cannot say
|
|
355
|
-
// which: the loop tried again and failed to improve, or nothing ever needed a second attempt.
|
|
356
|
-
// Measured on the first two real runs — every scope went green on attempt 1 — so the reading a
|
|
357
|
-
// reviewer meets by default is the degenerate one. `stats --ratchet` says so on the terminal;
|
|
358
|
-
// the committed report has to say it too, or a 0 here is read as "the ratchet did not work".
|
|
359
|
-
if (ratchet.scopes_multi_trial === 0) {
|
|
360
|
-
L.push("> No scope needed a second attempt, so the rates above are vacuous rather than bad:",
|
|
361
|
-
"> the ratchet was never asked to climb. The Day-1 question — does the loop measurably",
|
|
362
|
-
"> improve across attempts — needs a run where at least one scope retries.", "");
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (evalCriteria) L.push("## Evaluation", "", evalCriteria, "");
|
|
367
|
-
if (evalBugs) L.push("### Refuted criteria and bugs", "", evalBugs, "");
|
|
368
|
-
if (qaFindings) L.push("## QA findings", "", qaFindings, "");
|
|
369
|
-
if (decisions) L.push("## Decisions adjudicated during the run", "", decisions, "");
|
|
370
|
-
if (discovered) L.push("## Discovered, not built", "", discovered, "");
|
|
371
|
-
|
|
372
|
-
L.push("---", "",
|
|
373
|
-
"*Run state (board, orders, results, T0 artifacts, evaluation and QA reports) stays in the",
|
|
374
|
-
"gitignored local tier (ADR-0001). This report",
|
|
375
|
-
"is the frozen conclusion of it.*", "");
|
|
376
|
-
// THE WHOLE REPORT, ONCE. Board ids were anchored in three places and leaked through a fourth: a
|
|
377
|
-
// discovery-ledger entry copied verbatim into "Discovered, not built" carried `[TASK-005]`, this
|
|
378
|
-
// kernel wrote it straight to the committed tier past the hook that guards the model's edits, and
|
|
379
|
-
// the next run's L1b lint red'd the file the previous run had frozen. A committed report may not
|
|
380
|
-
// name a board id anywhere, so the rule is applied to the finished text rather than section by
|
|
381
|
-
// section.
|
|
382
|
-
return deboard(L.join("\n"), board.anchors);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* The QA line the report may print. The run says whether it dispatched the hunt; the hunt's own
|
|
387
|
-
* report says whether anything was hunted. A hunter that could not reach the app still returns, and
|
|
388
|
-
* a report opening `charters: 0/…` over a dispatched hunt read as "QA: run" with no findings — which
|
|
389
|
-
* a reader takes for an app with nothing wrong, not for an app nobody drove.
|
|
390
|
-
* @param {string|undefined} passed - What the caller recorded (`run`, `skipped`), if anything.
|
|
391
|
-
* @param {string|null} huntReport - The hunt report's text, or null when there is none.
|
|
392
|
-
* @returns {string} `skipped`, `not-hunted` (dispatched, zero charters run), or `run`.
|
|
393
|
-
*/
|
|
394
|
-
export function qaStatus(passed, huntReport) {
|
|
395
|
-
if (passed === "skipped" || (!passed && !huntReport)) return passed || "skipped";
|
|
396
|
-
if (huntReport && chartersRun(huntReport) === 0) return "not-hunted";
|
|
397
|
-
return passed || "run";
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Gather every fact from disk and render the report.
|
|
402
|
-
* @param {{cwd:string, slug:string, verdict?:string, qa?:string}} opts - Inputs.
|
|
403
|
-
* @returns {{markdown:string, path:string, facts:object}} The document, its destination, the facts.
|
|
404
|
-
*/
|
|
405
|
-
export function generate({ cwd, slug, verdict, qa }) {
|
|
406
|
-
const run = frontmatter(readIf(harnessRun(cwd, slug)) || "");
|
|
407
|
-
const receipt = (() => { try { return JSON.parse(readIf(receiptPath(cwd, slug)) || "{}"); } catch { return {}; } })();
|
|
408
|
-
|
|
409
|
-
const evalReport = readIf(join(evaluationDir(cwd, slug), `EVAL-FEATURE-${slug}.md`));
|
|
410
|
-
const huntReport = readIf(join(qaDir(cwd, slug), "hunt-report.md"));
|
|
411
|
-
const ledger = readIf(roundLedger(cwd, slug));
|
|
412
|
-
const discovery = readIf(discoveryLedger(cwd, slug));
|
|
413
|
-
|
|
414
|
-
// Two numbers, not one: `rounds` (built — an order, a T0 verdict, a build-gate artifact or an
|
|
415
|
-
// EVAL result) and `roundsJudged` (EVAL actually returned a verdict for), kept separate so a run
|
|
416
|
-
// whose later rounds never reached EVAL still reports the rounds it built.
|
|
417
|
-
const derivedRounds = deriveRounds(cwd, slug, run.rounds_used);
|
|
418
|
-
|
|
419
|
-
const facts = {
|
|
420
|
-
slug,
|
|
421
|
-
at: today(),
|
|
422
|
-
verdict: verdict || run.final_verdict || "not-evaluated",
|
|
423
|
-
census: (() => { try { return JSON.parse(readIf(hammerCensus(cwd, slug)) || "null"); } catch { return null; } })(),
|
|
424
|
-
qa: qaStatus(qa, huntReport),
|
|
425
|
-
rounds: derivedRounds.rounds_used,
|
|
426
|
-
roundsJudged: derivedRounds.rounds_judged,
|
|
427
|
-
intakeSha: receipt.intake_sha256,
|
|
428
|
-
board: boardCensus(cwd, slug),
|
|
429
|
-
t0: t0Summary(cwd, slug),
|
|
430
|
-
ratchet: ratchetReport(readTrials(trials(cwd, slug))),
|
|
431
|
-
requirements: projectRequirements({ cwd, slug }),
|
|
432
|
-
artifacts: verdictArtifactCount(cwd, slug),
|
|
433
|
-
evalCriteria: section(evalReport, /^#+\s.*criteria/i) || section(evalReport, /^#+\s*spec-conformance/i),
|
|
434
|
-
evalBugs: section(evalReport, /^#+\s*Bugs?\b/i),
|
|
435
|
-
qaFindings: section(huntReport, /^#+\s*Findings?\b/i),
|
|
436
|
-
decisions: section(ledger, /^#+\s*Decisions?\b/i),
|
|
437
|
-
discovered: section(discovery, /^#+\s*Discovered\b/i),
|
|
438
|
-
leftovers: (() => {
|
|
439
|
-
// Advisory, and derived like everything else here: the run's own diff, added lines only.
|
|
440
|
-
const diff = collectDiff(cwd, slug);
|
|
441
|
-
return diff ? summarize(scanDiff(diff)) : [];
|
|
442
|
-
})(),
|
|
443
|
-
};
|
|
444
|
-
return { markdown: buildReport(facts), path: reportPath(cwd, slug), facts };
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// ---------------------------------------------------------------------------
|
|
448
|
-
/** The typed argv contract (see `./lib/argv.mjs`). */
|
|
449
|
-
export const ARGV_SPEC = {
|
|
450
|
-
usage: "harness.mjs reduce ship --slug <slug> [--cwd <dir>] [--verdict PASS|FAIL|not-evaluated] " +
|
|
451
|
-
"[--qa run|skipped] [--stdout]",
|
|
452
|
-
_: { arity: 0, max: 0, name: "(no positional operands)" },
|
|
453
|
-
slug: { type: "str", required: true },
|
|
454
|
-
cwd: { type: "path" },
|
|
455
|
-
verdict: { type: "str" },
|
|
456
|
-
qa: { type: "str" },
|
|
457
|
-
stdout: { type: "flag" },
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Generate the ship report from the artifacts on disk.
|
|
462
|
-
*
|
|
463
|
-
* @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
|
|
464
|
-
* @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
|
|
465
|
-
* call `process.exit()` with the subcommand's documented code rather than returning.
|
|
466
|
-
*/
|
|
467
|
-
/**
|
|
468
|
-
* Did THIS run skip evaluation? Read from the run's own recorded arguments, and believed only when
|
|
469
|
-
* the record can be shown to belong to this run.
|
|
470
|
-
*
|
|
471
|
-
* WHY THE KERNEL ASKS AT ALL. A run launched with `--no-eval` verified nothing, and the protocol
|
|
472
|
-
* has always said such a run ships as `not-evaluated`, "recorded plainly — never silently
|
|
473
|
-
* upgraded". That promise lived entirely inside the orchestrator's control flow, where one
|
|
474
|
-
* assignment downstream of the branch restores the old behaviour with every check still green —
|
|
475
|
-
* measured, not supposed. The gate block tells the human the truth either way; the committed report
|
|
476
|
-
* a teammate inherits on `git pull` is the artifact that was lying, so the refusal belongs at the
|
|
477
|
-
* writer of that artifact, where a future orchestrator edit cannot reach it.
|
|
478
|
-
*
|
|
479
|
-
* POSITIVELY PROVEN OR NOT AT ALL. The record is written at launch, later than the receipt that
|
|
480
|
-
* mints the run key, so a freshly opened run can still find the PREVIOUS run's record on disk. A
|
|
481
|
-
* stale flag would refuse a ship that verified everything — a worse failure than the one this
|
|
482
|
-
* closes. The flag therefore counts only when the record names the same run the receipt does;
|
|
483
|
-
* a missing, unreadable or differently-keyed record proves nothing and permits.
|
|
484
|
-
*
|
|
485
|
-
* @param {string} cwd - Project root.
|
|
486
|
-
* @param {string} slug - Feature slug.
|
|
487
|
-
* @returns {boolean} True only when this run's own record says evaluation was skipped.
|
|
488
|
-
*/
|
|
489
|
-
function evalWasSkipped(cwd, slug) {
|
|
490
|
-
try {
|
|
491
|
-
const record = JSON.parse(readFileSync(runArgsPath(cwd, slug), "utf8"));
|
|
492
|
-
if (record?.noEval !== true) return false;
|
|
493
|
-
const mine = runIdFromReceipt(readReceipt(receiptPath(cwd, slug)));
|
|
494
|
-
return Boolean(mine) && record.runId === mine;
|
|
495
|
-
} catch { return false; }
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/** The verdict values that assert the feature was graded and passed. */
|
|
499
|
-
const PASSING = new Set(["PASS", "pass"]);
|
|
500
|
-
|
|
501
|
-
export async function cli(rawArgv) {
|
|
502
|
-
const args = runArgs(ARGV_SPEC, rawArgv);
|
|
503
|
-
const cwd = args.cwd || process.cwd();
|
|
504
|
-
if (PASSING.has(String(args.verdict ?? "")) && evalWasSkipped(cwd, args.slug)) {
|
|
505
|
-
console.error(
|
|
506
|
-
"✋ reduce ship: this run was launched with --no-eval, so nothing graded it — refusing to freeze a report " +
|
|
507
|
-
`that says ${args.verdict}. Ship it as --verdict not-evaluated, which the report, the ledger and the ` +
|
|
508
|
-
"sign-off block all carry.",
|
|
509
|
-
);
|
|
510
|
-
process.exit(3);
|
|
511
|
-
}
|
|
512
|
-
const { markdown, path } = generate({ cwd, slug: args.slug, verdict: args.verdict, qa: args.qa });
|
|
513
|
-
if (args.stdout) {
|
|
514
|
-
process.stdout.write(markdown);
|
|
515
|
-
} else {
|
|
516
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
517
|
-
writeFileSync(path, markdown, "utf8");
|
|
518
|
-
|
|
519
|
-
// THE RUN IS OVER, SO RETIRE ITS POINTER. `harness compile` publishes `.shapeup/active-order`
|
|
520
|
-
// as it writes each order and nothing ever erased it, so the pointer outlived every run that
|
|
521
|
-
// produced one. That is harmless to the guard now — liveness comes from the order set, and a
|
|
522
|
-
// shipped run has no unanswered orders — but a pointer naming a finished run is a fact on disk
|
|
523
|
-
// that is no longer true, and the next reader to trust it inherits the same class of bug.
|
|
524
|
-
// Only on the writing path: `--stdout` is a preview of the report, not the end of the run.
|
|
525
|
-
rmSync(activeOrder(cwd), { force: true });
|
|
526
|
-
|
|
527
|
-
console.log(relShared(args.slug, "REPORT.md"));
|
|
528
|
-
}
|
|
529
|
-
}
|