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,384 +0,0 @@
1
- #!/usr/bin/env node
2
- // export-run — freeze a run's records into fact tables before the tier that holds them is wiped.
3
- //
4
- // WHY THIS EXISTS (from the storage design, not from a preference for tooling).
5
- //
6
- // Every record this exports already exists. Orders, results, trial rows, T0 verdicts, criterion
7
- // verdicts and hook decisions are all written during a normal run, all as JSON, all
8
- // schema-registered. They live in `.shapeup/<slug>/` — the LOCAL tier, which ADR-0001
9
- // defines as gitignored, machine-local and REGENERABLE. That definition is correct for run state
10
- // and fatal for measurement: the trial-row contract says it out loud — a measurement left there
11
- // "answers the question exactly once and then deletes itself".
12
- //
13
- // So the harness has been producing a complete dataset and discarding it. Not losing it to a bug;
14
- // discarding it by design, because nothing ever read it before the directory was cleaned. This
15
- // script is the read that was missing, and it is deliberately a SEPARATE STEP rather than a write
16
- // added to the pipeline: the run's writers stay exactly as they are, ``harness reduce ingest`` remains
17
- // the sole writer of shared state, and an export can be re-run against a trace at any point
18
- // without touching it.
19
- //
20
- // WHAT IT DOES NOT DO, said plainly rather than left to be discovered. It does not make the
21
- // records cross a machine boundary on its own. The default destination is `.shapeup/exports/`,
22
- // which survives a per-slug wipe but is still gitignored — because committing per-run structured
23
- // data and a machine id is precisely what ADR-0001 moved the metrics shards out of git to prevent.
24
- // A cross-machine warehouse is `--out <dir>` to a destination the operator owns and chooses. The
25
- // export makes the evidence DURABLE and PORTABLE; where it travels stays a human decision.
26
- //
27
- // READ-ONLY, AND FAIL-OPEN PER RECORD. Nothing here writes into the run trace. A file that will
28
- // not parse is skipped and COUNTED in the manifest rather than aborting the export — a warehouse
29
- // loader that dies on one bad line loses the other nine thousand, and the count is what tells an
30
- // analyst their table is short. It never grades: every column is an id, a count, a duration or a
31
- // copied enum, per the rule ``harness probe stats`` states in its own header.
32
- //
33
- // USAGE
34
- // node `harness report export` [--slug <slug>] [--cwd <dir>] [--out <dir>] [--all] [--format jsonl|json]
35
- //
36
- // --slug the run to export; defaults to the active-scope pointer's slug
37
- // --all export every run under the LOCAL root that has a readable receipt
38
- // --out destination root (default: .shapeup/exports/); each run lands in <out>/<run_id>/
39
- // --format jsonl (default — one object per line, what a warehouse reads natively) or json
40
- //
41
- // Prints the manifest(s) on stdout. Exit 0 on success, 2 on a usage error, 3 when there is
42
- // nothing to export.
43
-
44
- import { readFileSync, writeFileSync, readdirSync, mkdirSync, existsSync, statSync } from "node:fs";
45
- import { join, resolve } from "node:path";
46
- import { runArgs } from "../lib/argv.mjs";
47
- import { splitFrontmatter } from "../lib/contract.mjs";
48
- import {
49
- runIdFromReceipt, readReceipt, legLedger,
50
- } from "../lib/paths.mjs";
51
- import { TABLES, runRow, dispatchFacts } from "./facts.mjs";
52
- import { deriveRounds } from "../probe/rounds.mjs";
53
- import {
54
- localDir, activeScope, receipt as receiptPath, harnessRun, ordersDir, resultsDir,
55
- trials as trialsPath, verdictsDir, evaluationDir, decisions as decisionsPath,
56
- gates as gatesPath, roundBuildDir, exportsDir, exportRunDir,
57
- } from "../lib/paths.mjs";
58
-
59
- export const EXPORT_SCHEMA_VERSION = 1;
60
-
61
- // ---------------------------------------------------------------------------
62
- // Readers — every one of them returns data plus a defect count, never throws
63
- // ---------------------------------------------------------------------------
64
-
65
- /** A read tally threaded through every reader so the manifest can report what did not parse. */
66
- const tally = () => ({ skipped: 0 });
67
-
68
- /**
69
- * Parse a JSON file, counting rather than throwing on failure.
70
- * @param {string} path - File to read.
71
- * @param {{skipped:number}} t - Defect tally, incremented on a parse or read failure.
72
- * @returns {(*|null)} The parsed value, or null.
73
- */
74
- function readJson(path, t) {
75
- try { return JSON.parse(readFileSync(path, "utf8")); } catch { t.skipped++; return null; }
76
- }
77
-
78
- /**
79
- * Parse a JSONL file into rows, skipping (and counting) lines that do not parse.
80
- * @param {string} path - File to read; a missing file is not a defect, it is an empty table.
81
- * @param {{skipped:number}} t - Defect tally.
82
- * @returns {Array<object>} The parsed rows, in file order.
83
- */
84
- function readJsonl(path, t) {
85
- if (!existsSync(path)) return [];
86
- let text;
87
- try { text = readFileSync(path, "utf8"); } catch { t.skipped++; return []; }
88
- const out = [];
89
- for (const line of text.split("\n")) {
90
- if (!line.trim()) continue;
91
- try { out.push(JSON.parse(line)); } catch { t.skipped++; }
92
- }
93
- return out;
94
- }
95
-
96
- /**
97
- * Read every `*.json` in a directory, sorted by name so an export is byte-stable across runs.
98
- * @param {string} dir - Directory to scan; absent → empty.
99
- * @param {{skipped:number}} t - Defect tally.
100
- * @returns {Array<object>} Parsed documents, name-sorted.
101
- */
102
- function readJsonDir(dir, t) {
103
- if (!existsSync(dir)) return [];
104
- const out = [];
105
- for (const f of readdirSync(dir).filter((x) => x.endsWith(".json")).sort()) {
106
- const doc = readJson(join(dir, f), t);
107
- if (doc) out.push(doc);
108
- }
109
- return out;
110
- }
111
-
112
- /**
113
- * Flatten a T0 verdict artifact into one fact row, dropping the nested arms (they are the
114
- * `CommandResult` detail, and belong to the artifact, not to a column).
115
- * @param {object} a - A parsed T0Artifact.
116
- * @param {(string|null)} runId - Run key for artifacts written before the field existed.
117
- * @returns {object} A flat `t0_verdict` row.
118
- */
119
- function t0Row(a, runId) {
120
- const fixtures = Array.isArray(a?.fixtures) ? a.fixtures : [];
121
- return {
122
- run_id: a?.run_id ?? runId ?? null,
123
- scope_id: a?.scope_id ?? null,
124
- round: a?.round ?? null,
125
- attempt: a?.attempt ?? null,
126
- trial: a?.trial ?? null,
127
- at: a?.at ?? null,
128
- overall: a?.overall ?? null,
129
- regression: a?.regression ?? null,
130
- fixtures_green: a?.fixtures_green ?? null,
131
- db_probe_green: a?.db_probe_green ?? null,
132
- // One field a reader compares, and the block itself stays in the artifact for a human to diff:
133
- // two rows with the same tree and different env digests are two machines, not a regression.
134
- env_sha256: a?.env?.env_sha256 ?? null,
135
- tree_head: a?.env?.tree?.head ?? null,
136
- tree_dirty: a?.env?.tree?.dirty ?? null,
137
- fixtures_total: fixtures.length,
138
- fixtures_passed: fixtures.filter((f) => f?.pass === true).length,
139
- discovered_tasks: Array.isArray(a?.discovered_tasks) ? a.discovered_tasks.length : 0,
140
- };
141
- }
142
-
143
- /**
144
- * Collect every criterion-verdict row across the evaluator's per-target ledgers.
145
- * @param {string} dir - The run's `evaluation/` directory.
146
- * @param {(string|null)} runId - Run key to stamp (the ledger line carries none).
147
- * @param {{skipped:number}} t - Defect tally.
148
- * @returns {Array<object>} Flat `criterion_verdict` rows, each tagged with its target ledger.
149
- */
150
- function criterionRows(dir, runId, t) {
151
- if (!existsSync(dir)) return [];
152
- const out = [];
153
- for (const f of readdirSync(dir).filter((x) => x.startsWith(".verdicts-") && x.endsWith(".jsonl")).sort()) {
154
- const target = f.replace(/^\.verdicts-/, "").replace(/\.jsonl$/, "");
155
- for (const row of readJsonl(join(dir, f), t)) {
156
- out.push({
157
- run_id: runId ?? null, target,
158
- run: row?.run ?? null,
159
- dimension: row?.dimension ?? null,
160
- criterion: row?.criterion ?? null,
161
- verdict: row?.verdict ?? null,
162
- confidence: row?.confidence ?? null,
163
- reprobed: row?.reprobed ?? null,
164
- has_evidence: !!(row?.evidence && String(row.evidence).trim()),
165
- at: row?.at ?? null,
166
- });
167
- }
168
- }
169
- return out;
170
- }
171
-
172
- /**
173
- * Flatten one gate-crossing ledger row (`gates.jsonl`, `kernel/gate.mjs`'s sole writer) into a
174
- * flat `gate_decision` fact row. The row on disk already carries exactly these fields
175
- * (see `appendGateLedger`), so this is a pass-through with a stamped `run_id` fallback rather than
176
- * a re-derivation: two readers of "what did this gate decide" must not compute the answer twice.
177
- * @param {object} g - One parsed line of `gates.jsonl`.
178
- * @param {(string|null)} runId - The run being exported (unused for attribution — the row's own key is the only one exported).
179
- * @returns {object} A flat `gate_decision` row.
180
- */
181
- function gateDecisionRow(g, runId) {
182
- return {
183
- // The row's own key, never the current run's stamped on: a row that carries no key was written
184
- // before the ledger did, and is exported as unattributed rather than claimed.
185
- run_id: g?.run_id ?? null,
186
- gate: g?.gate ?? null,
187
- decision: g?.decision ?? null,
188
- status: g?.status ?? null,
189
- source: g?.source ?? null,
190
- round: g?.round ?? null,
191
- has_note: !!(g?.note && String(g.note).trim()),
192
- };
193
- }
194
-
195
- /**
196
- * Flatten one round build-gate artifact (`kernel/verify/build.mjs`'s `writeRoundBuild`) into a flat
197
- * `build_gate` fact row. The gate ends a round exactly as EVAL does (AGENTS.md's round
198
- * build gate ⚙), and until now had no fact table at all.
199
- * @param {object} a - A parsed round-build artifact.
200
- * @param {(string|null)} runId - Run key for an artifact written before it carried its own.
201
- * @returns {object} A flat `build_gate` row.
202
- */
203
- function buildGateRow(a, runId) {
204
- const steps = Array.isArray(a?.steps) ? a.steps : [];
205
- return {
206
- run_id: a?.run_id ?? runId ?? null,
207
- round: a?.round ?? null,
208
- trial: a?.trial ?? null,
209
- at: a?.at ?? null,
210
- overall: a?.overall ?? null,
211
- archetype: a?.archetype ?? null,
212
- steps_total: steps.length,
213
- steps_failed: steps.filter((s) => !s?.skipped && s?.pass === false).length,
214
- warnings: Array.isArray(a?.warnings) ? a.warnings.length : 0,
215
- discovered_tasks: Array.isArray(a?.discovered_tasks) ? a.discovered_tasks.length : 0,
216
- };
217
- }
218
-
219
- // ---------------------------------------------------------------------------
220
- // The export itself
221
- // ---------------------------------------------------------------------------
222
-
223
- /**
224
- * Read one run's trace and project it into fact tables. Pure-ish: reads the run trace, writes
225
- * nothing.
226
- *
227
- * @param {string} cwd - Project root.
228
- * @param {string} slug - The feature slug whose run to export.
229
- * @returns {(object|null)} `{run_id, slug, tables:{…}, defects}` — or null when the slug has no
230
- * readable receipt, which is the definition of "not a run".
231
- */
232
- export function collectRun(cwd, slug) {
233
- const t = tally();
234
- const rec = readReceipt(receiptPath(cwd, slug));
235
- if (!rec) return null;
236
- const runId = runIdFromReceipt(rec);
237
-
238
- let ledger = {};
239
- try { ledger = splitFrontmatter(readFileSync(harnessRun(cwd, slug), "utf8")).meta || {}; } catch { /* no ledger yet */ }
240
-
241
- const orders = readJsonDir(ordersDir(cwd, slug), t);
242
- const results = readJsonDir(resultsDir(cwd, slug), t);
243
-
244
- const { dispatch, ac_result, discovery, file_touched } = dispatchFacts({ orders, results, runId });
245
- // Computed here, once, from the same trace this whole function reads, and handed to
246
- // runRow rather than re-derived by it: runRow stays pure (no I/O), this function already has cwd.
247
- const rounds = deriveRounds(cwd, slug, ledger.rounds_used);
248
- const run = runRow({ receipt: rec, ledger, runId, rounds });
249
-
250
- // Hook decisions are checkout-wide, so they are FILTERED to this run rather than read from a
251
- // per-run file. Rows with a null key belong to no run (a hook that fired outside one) and are
252
- // correctly excluded here — they are not this run's, and claiming them would inflate its
253
- // enforcement counts with ambient activity.
254
- const hook_decision = readJsonl(decisionsPath(cwd), t).filter((d) => runId && d?.run_id === runId);
255
-
256
- return {
257
- run_id: runId,
258
- slug,
259
- tables: {
260
- run: run ? [run] : [],
261
- dispatch, ac_result, discovery, file_touched,
262
- trial: readJsonl(trialsPath(cwd, slug), t).map((r) => ({ ...r, run_id: r.run_id ?? runId ?? null })),
263
- t0_verdict: readJsonDir(verdictsDir(cwd, slug), t).map((a) => t0Row(a, runId)),
264
- criterion_verdict: criterionRows(evaluationDir(cwd, slug), runId, t),
265
- hook_decision,
266
- // The decision that crossed each gate, and the round build gate's own artifact.
267
- // Scoped to the run, like hook_decision one line up: gate rows over one slug accumulate across
268
- // runs, and a prior run's L4 exported under this run's key is a fabricated sign-off.
269
- gate_decision: readJsonl(gatesPath(cwd, slug), t).filter((g) => runId && g?.run_id === runId).map((g) => gateDecisionRow(g, runId)),
270
- build_gate: readJsonDir(roundBuildDir(cwd, slug), t).map((a) => buildGateRow(a, runId)),
271
- leg: readJsonl(legLedger(cwd, slug), t)
272
- .filter((r) => !runId || !r?.run_id || r.run_id === runId)
273
- .map((r) => ({ ...r, run_id: r.run_id ?? runId ?? null })),
274
- },
275
- defects: { records_skipped: t.skipped },
276
- };
277
- }
278
-
279
- /**
280
- * Write one collected run's tables to disk and return its manifest.
281
- * @param {object} collected - Output of {@link collectRun}.
282
- * @param {string} outDir - Destination directory for this run's tables.
283
- * @param {string} format - `jsonl` (one object per line) or `json` (one array per file).
284
- * @returns {object} The manifest, which is also written to `<outDir>/manifest.json`.
285
- */
286
- export function writeRun(collected, outDir, format = "jsonl") {
287
- mkdirSync(outDir, { recursive: true });
288
- const ext = format === "json" ? "json" : "jsonl";
289
- const rows = {};
290
- for (const name of TABLES) {
291
- const table = collected.tables[name] || [];
292
- rows[name] = table.length;
293
- const body = format === "json"
294
- ? JSON.stringify(table, null, 2) + "\n"
295
- : table.map((r) => JSON.stringify(r)).join("\n") + (table.length ? "\n" : "");
296
- writeFileSync(join(outDir, `${name}.${ext}`), body, "utf8");
297
- }
298
- const manifest = {
299
- schema_version: EXPORT_SCHEMA_VERSION,
300
- run_id: collected.run_id,
301
- slug: collected.slug,
302
- generated_at: new Date().toISOString(),
303
- format: ext,
304
- tables: TABLES.map((name) => ({ name, file: `${name}.${ext}`, rows: rows[name] })),
305
- rows_total: Object.values(rows).reduce((a, b) => a + b, 0),
306
- // The defect count is a first-class manifest field, not a log line. A short table with no
307
- // record of why is indistinguishable from a short run.
308
- records_skipped: collected.defects.records_skipped,
309
- };
310
- writeFileSync(join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
311
- return manifest;
312
- }
313
-
314
- /**
315
- * Every slug under the LOCAL root that has a readable receipt — i.e. every run, discovered from
316
- * the filesystem rather than from a list someone has to maintain.
317
- * @param {string} cwd - Project root.
318
- * @returns {string[]} Slugs, sorted.
319
- */
320
- export function discoverRuns(cwd) {
321
- const root = localDir(cwd);
322
- if (!existsSync(root)) return [];
323
- const out = [];
324
- for (const e of readdirSync(root)) {
325
- try { if (!statSync(join(root, e)).isDirectory()) continue; } catch { continue; }
326
- if (existsSync(receiptPath(cwd, e))) out.push(e);
327
- }
328
- return out.sort();
329
- }
330
-
331
- // ---------------------------------------------------------------------------
332
- // CLI
333
- // ---------------------------------------------------------------------------
334
-
335
- /** The typed argv contract (see `./lib/argv.mjs`). */
336
- export const ARGV_SPEC = {
337
- usage: "harness.mjs report export [--slug <slug>] [--all] [--cwd <dir>] [--out <dir>] [--format jsonl|json]",
338
- _: { arity: 0, max: 0, name: "(no positional operands)" },
339
- cwd: { type: "path" },
340
- slug: { type: "str" },
341
- out: { type: "path" },
342
- all: { type: "flag" },
343
- format: { type: "enum", values: ["jsonl", "json"], default: "jsonl" },
344
- };
345
-
346
- /**
347
- * Project a run's records as fact tables under the exports tier.
348
- *
349
- * @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
350
- * @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
351
- * call `process.exit()` with the subcommand's documented code rather than returning.
352
- */
353
- export function cli(rawArgv) {
354
- const args = runArgs(ARGV_SPEC, rawArgv);
355
- const cwd = resolve(args.cwd || process.cwd());
356
-
357
- let slugs = [];
358
- if (args.all) slugs = discoverRuns(cwd);
359
- else if (args.slug) slugs = [args.slug];
360
- else {
361
- // The active-scope pointer, same as every other run-scoped tool resolves through.
362
- const active = readJson(activeScope(cwd), tally());
363
- if (active?.slug) slugs = [active.slug];
364
- }
365
- if (!slugs.length) {
366
- console.error("✋ export-run: no run to export — pass --slug <slug>, or --all, or open a run first.");
367
- process.exit(3);
368
- }
369
-
370
- const outRoot = args.out ? resolve(cwd, args.out) : exportsDir(cwd);
371
- const manifests = [];
372
- for (const slug of slugs) {
373
- const collected = collectRun(cwd, slug);
374
- if (!collected) { console.error(`export-run: ${slug} has no readable receipt — skipped (not a run)`); continue; }
375
- // Keyed by RUN ID, not by slug: two runs of the same feature are two datasets, and filing them
376
- // under the slug would make the second silently overwrite the first — the exact collision the
377
- // run key exists to end.
378
- const dir = args.out ? join(outRoot, String(collected.run_id ?? slug)) : exportRunDir(cwd, collected.run_id ?? slug);
379
- manifests.push(writeRun(collected, dir, args.format));
380
- }
381
- if (!manifests.length) process.exit(3);
382
- console.log(JSON.stringify(manifests.length === 1 ? manifests[0] : manifests, null, 2));
383
- }
384
-
@@ -1,234 +0,0 @@
1
- // facts — the harness's records, projected into fact tables. Pure: no I/O, no clock, no grading.
2
- //
3
- // WHY THIS FILE EXISTS.
4
- //
5
- // The pipeline already writes JSON at every boundary — an order in, a result out, a decision row
6
- // per hook evaluation, a trial row per T0 run. What it never had was a way to READ them together.
7
- // Each record answers a question about itself; none of them answers "what did this run do",
8
- // because that question needs a join and nothing on disk was joinable (see `mintRunId` in
9
- // `lib/paths.mjs` for why).
10
- //
11
- // This module is the projection half. It takes parsed records and returns flat rows — a star
12
- // schema whose grain is the DISPATCH, which is the finest unit the harness actually plans in:
13
- // one compiled order, one worker, one result. Everything else hangs off it as a child table
14
- // (`ac_result`, `discovery`, `file_touched`).
15
- //
16
- // FACTS ONLY — the rule ``harness probe stats`` states in its own header. Every field below is a
17
- // count, a duration, a copied enum or an id. No field here is a score, a rate of quality, or a
18
- // judgement, because a computed grade in the read plane is a second judge behind spec-evaluator
19
- // and the architecture forbids one. `n_ac_fail` is a fact; "AC health" is not.
20
- //
21
- // A dispatch with no matching result is not dropped: `answered: false` says so, because an absent
22
- // value and a zero value must not share a signature. This module carries no cost or wall-clock
23
- // instrumentation — there is no run-scoped record of either to project.
24
-
25
- /** Every fact table this module can produce, in dependency order. Exported so the writer, the
26
- * manifest and the tests enumerate one list instead of three. */
27
- export const TABLES = [
28
- "run", "dispatch", "ac_result", "discovery", "file_touched",
29
- "trial", "t0_verdict", "criterion_verdict", "hook_decision",
30
- // The decision that shipped a run (or any other gate) had a ledger row (`gates.jsonl`)
31
- // and no table — a reader had to open the LOCAL trace itself, which the export exists so nobody
32
- // has to. `build_gate` is the round build gate's own artifact (kernel/verify/build.mjs), on the
33
- // same terms: it ends a round exactly as EVAL does, and had no table either.
34
- "gate_decision", "build_gate",
35
- // The leg-completion ledger — one row per order the single writer applied. Without it the
36
- // warehouse could join an order to its result and never say whether anyone read the result.
37
- "leg",
38
- ];
39
-
40
- /** Coerce anything to a finite number, or null. Keeps `0` and rejects `NaN`/`""`/undefined. */
41
- const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : null);
42
-
43
- /** Sum a list of numbers, returning null when NOTHING in it was a number — an absent total and a
44
- * zero total are different facts and must not share a representation. */
45
- function sumOrNull(values) {
46
- let seen = false, total = 0;
47
- for (const v of values) { const n = num(v); if (n !== null) { seen = true; total += n; } }
48
- return seen ? total : null;
49
- }
50
-
51
- /**
52
- * The order id's file stem — the name its order and result files share.
53
- * @param {string} orderId - e.g. `checkout/sc-01-r1-a2`.
54
- * @returns {(string|null)} e.g. `sc-01-r1-a2`, or null when the id has no `/`.
55
- */
56
- export function orderStem(orderId) {
57
- const s = String(orderId ?? "");
58
- const i = s.indexOf("/");
59
- return i === -1 ? null : s.slice(i + 1) || null;
60
- }
61
-
62
- /**
63
- * The round/attempt/scope an order's stem encodes, parsed back out.
64
- *
65
- * Read from the id rather than from the payload deliberately: the id is what every other record
66
- * references, so a fact table keyed on it must agree with it even if a payload disagrees.
67
- *
68
- * @param {string} orderId - The order id.
69
- * @returns {{scope_id:(string|null), round:(number|null), attempt:(number|null)}} Nulls where the
70
- * stem carries no such term (a non-build operation has no round or attempt).
71
- */
72
- export function parseOrderStem(orderId) {
73
- const stem = orderStem(orderId) || "";
74
- const m = stem.match(/^(?:(.*)-)?r(\d+)-a(\d+)$/);
75
- if (m) return { scope_id: m[1] || null, round: Number(m[2]), attempt: Number(m[3]) };
76
- const opRound = stem.match(/^(.*)-r(\d+)$/);
77
- if (opRound) return { scope_id: null, round: Number(opRound[2]), attempt: null };
78
- return { scope_id: null, round: null, attempt: null };
79
- }
80
-
81
- /**
82
- * Project the run dimension — one row, the thing every fact table's `run_id` points at.
83
- * @param {object} o - Sources (destructured):
84
- * @param {(object|null)} o.receipt - Parsed `receipt.json`.
85
- * @param {(object|null)} [o.ledger] - Parsed `harness-run.md` frontmatter (a flat scalar map).
86
- * @param {(string|null)} [o.runId] - The run key, when already resolved.
87
- * @param {({rounds_used:*, rounds_judged:(number|null)}|null)} [o.rounds] - The two-number
88
- * derivation (`probe/rounds.mjs`'s `deriveRounds`), computed by the caller because it needs the
89
- * filesystem and this function stays pure. Falls back to the ledger's own (unreliable — see
90
- * `deriveRounds`) `rounds_used` line when the caller has not derived one, so an existing caller
91
- * is unaffected rather than broken.
92
- * @returns {(object|null)} The run row, or null when there is no receipt to describe.
93
- */
94
- export function runRow({ receipt, ledger = null, runId = null, rounds = null }) {
95
- if (!receipt) return null;
96
- const c = receipt.config || {};
97
- const fm = ledger || {};
98
- return {
99
- run_id: runId ?? receipt.run_id ?? null,
100
- slug: receipt.slug ?? null,
101
- started_at: receipt.started_at ?? null,
102
- closed_at: fm.closed_at && fm.closed_at !== "~" ? fm.closed_at : null,
103
- // Why the run ended at a terminal status, written by `probe resume --close` alongside
104
- // `closed_at` — the two facts a trace needs to tell a live run from a dead one apart.
105
- close_cause: fm.close_cause && fm.close_cause !== "~" ? fm.close_cause : null,
106
- intake_sha256: receipt.intake_sha256 ?? null,
107
- intake_chars: num(receipt.intake_chars),
108
- intake_lines: num(receipt.intake_lines),
109
- auto_level: c.auto_level ?? null,
110
- lens: c.lens ?? null,
111
- lane: c.fit?.lane ?? null,
112
- lane_overridden_from: c.fit?.overridden_from ?? null,
113
- max_rounds: num(c.max_rounds),
114
- attempt_budget: num(c.attempt_budget),
115
- wall_clock_budget_s: num(c.wall_clock_budget_s),
116
- eval_dimensions: Array.isArray(c.eval_dimensions) ? c.eval_dimensions.join(" ") : (typeof c.eval_dimensions === "string" ? c.eval_dimensions : null),
117
- // Copied from the ledger, never re-derived: the run's own status line is the harness's answer,
118
- // and a read plane that recomputed it would be asserting a second one.
119
- status: fm.status ?? null,
120
- // The terminal status `closeRun` alone writes, carried BESIDE `status` rather than instead of
121
- // it. `status:` is ordinary phase traffic and a later phase may move it, so an export that
122
- // carried only that line could show a run wearing another close's cause. These two disagreeing
123
- // is itself the fact worth exporting: it says the ledger was written after the close.
124
- closed_status: fm.closed_status && fm.closed_status !== "~" ? fm.closed_status : null,
125
- final_verdict: fm.final_verdict && fm.final_verdict !== "~" ? fm.final_verdict : null,
126
- // Two fields, not one: `rounds_used` is the highest round carrying ANY build evidence,
127
- // `rounds_judged` the highest round EVAL actually returned a verdict for. A caller that has not
128
- // derived `rounds` falls back to the ledger's own (pre-fix, unreliable) line, non-regression.
129
- rounds_used: rounds ? num(Number(rounds.rounds_used)) : num(Number(fm.rounds_used)),
130
- rounds_judged: rounds ? num(rounds.rounds_judged) : null,
131
- };
132
- }
133
-
134
- /**
135
- * Project the dispatch fact table and its three child tables.
136
- *
137
- * One row per ORDER — orders are the spine, because an order with no result is the fact you most
138
- * need (a dispatch that never came back), and a result with no order cannot exist by construction.
139
- *
140
- * @param {object} o - Sources (destructured):
141
- * @param {Array<object>} o.orders - Parsed WorkOrders.
142
- * @param {Array<object>} [o.results] - Parsed WorkResults; joined on `order_id`.
143
- * @param {(string|null)} [o.runId] - Run key for orders that carry none (pre-v1.8 traces).
144
- * @returns {{dispatch:Array<object>, ac_result:Array<object>, discovery:Array<object>,
145
- * file_touched:Array<object>}} The fact table and its children, each row already carrying
146
- * `run_id` + `order_id` so every table stands alone in the warehouse.
147
- */
148
- export function dispatchFacts({ orders, results = [], runId = null }) {
149
- const byOrderId = new Map();
150
- for (const r of results) if (r?.order_id) byOrderId.set(r.order_id, r);
151
-
152
- const dispatch = [], ac_result = [], discovery = [], file_touched = [];
153
-
154
- for (const order of orders) {
155
- if (!order?.order_id) continue;
156
- const id = order.order_id;
157
- const rid = order.run_id ?? runId ?? null;
158
- const stem = orderStem(id);
159
- const { scope_id, round, attempt } = parseOrderStem(id);
160
- const result = byOrderId.get(id) || null;
161
- const taskResults = Array.isArray(result?.task_results) ? result.task_results : [];
162
- const discoveries = Array.isArray(result?.discoveries) ? result.discoveries : [];
163
- const filesTouched = Array.isArray(result?.files_touched) ? result.files_touched : [];
164
-
165
- let acPass = 0, acFail = 0, acSkip = 0;
166
- for (const tr of taskResults) {
167
- for (const ac of Array.isArray(tr?.ac_results) ? tr.ac_results : []) {
168
- if (ac?.result === "pass") acPass++;
169
- else if (ac?.result === "fail") acFail++;
170
- else acSkip++;
171
- ac_result.push({
172
- run_id: rid, order_id: id,
173
- task_id: tr?.task_id ?? null,
174
- ac: ac?.ac ?? null,
175
- result: ac?.result ?? null,
176
- // The evidence TEXT is the worker's prose and belongs in the result file, not in a fact
177
- // table. Whether it exists at all is the fact — "no evidence = fail by the worker's own
178
- // hand" is a contract the warehouse can then check without re-reading every envelope.
179
- has_evidence: !!(ac?.evidence && String(ac.evidence).trim()),
180
- });
181
- }
182
- }
183
- for (const d of discoveries) {
184
- discovery.push({
185
- run_id: rid, order_id: id,
186
- marker: d?.marker ?? null,
187
- lens: d?.lens ?? null,
188
- severity_hint: d?.severity_hint ?? null,
189
- test_gap: d?.test_gap ?? null,
190
- contradicts: d?.contradicts ?? null,
191
- has_repro: !!(d?.repro && String(d.repro).trim()),
192
- line: d?.line ?? null,
193
- });
194
- }
195
- for (const f of filesTouched) {
196
- file_touched.push({
197
- run_id: rid, order_id: id,
198
- path: f?.path ?? null,
199
- change: f?.change ?? null,
200
- lines: num(f?.lines),
201
- });
202
- }
203
-
204
- dispatch.push({
205
- run_id: rid,
206
- order_id: id,
207
- slug: id.split("/")[0] || null,
208
- stem,
209
- worker: order.worker ?? null,
210
- operation: order.operation ?? null,
211
- mode: order.mode ?? null,
212
- scope_id: scope_id ?? order.payload?.scope_contract?.scope_id ?? null,
213
- round, attempt,
214
- compiled_at: order.compiled_at ?? null,
215
- tasks_ordered: Array.isArray(order.payload?.tasks) ? order.payload.tasks.length : 0,
216
- digested_errors: Array.isArray(order.payload?.digested_errors) ? order.payload.digested_errors.length : 0,
217
- // `null`, not `"missing"`: a dispatch with no result file is the single most important row
218
- // in this table, and it must be filterable as an absence rather than as a status value that
219
- // sorts alongside real ones.
220
- result_status: result?.status ?? null,
221
- answered: !!result,
222
- task_results: taskResults.length,
223
- ac_pass: acPass, ac_fail: acFail, ac_skipped: acSkip,
224
- discoveries: discoveries.length,
225
- files_touched: filesTouched.length,
226
- lines_touched: sumOrNull(filesTouched.map((f) => f?.lines)),
227
- has_verdict: !!result?.verdict,
228
- verdict_overall: result?.verdict?.overall ?? null,
229
- assumptions: Array.isArray(result?.assumptions) ? result.assumptions.length : 0,
230
- deviations: Array.isArray(result?.deviations) ? result.deviations.length : 0,
231
- });
232
- }
233
- return { dispatch, ac_result, discovery, file_touched };
234
- }