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.
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 +29 -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,291 +0,0 @@
1
- #!/usr/bin/env node
2
- // Run snapshot — the compaction-resilience derivation (v1.2).
3
- //
4
- // Derives a RunSnapshot (domain.schema.json#/$defs/RunSnapshot) from FILES ONLY: the
5
- // active-scope pointer, harness-run.md frontmatter, board task frontmatter, t0/verdicts
6
- // filenames, and the orders/ vs results/ diff. Never from conversation memory — that is the
7
- // point: after a context compaction the summary may be lossy, but the files are not. The
8
- // danger this record exists to prevent is the orchestrator continuing on a degraded summary:
9
- // re-dispatching an already-ingested order, miscounting attempts (breaking the inner circuit
10
- // breaker), or "remembering" a hill phase instead of re-deriving it.
11
- //
12
- // Consumers: `--write` persists it as an audit anchor; `--format text` prints the rehydrate_hint
13
- // for whoever asks. Nothing fires it automatically — the orchestrator re-derives after a context
14
- // loss by running it, which answers whenever the question is asked rather than only at the two
15
- // moments a lifecycle hook happened to see.
16
- //
17
- // Output is self-validated against the registry before it is emitted — the same
18
- // refuse-to-emit-schema-drift discipline as `harness compile`.
19
- //
20
- // Usage: node `harness reduce snapshot` [--cwd <dir>] [--format json|text] [--write]
21
- // exit 0 with empty stdout when no run is active (fail-open), 1 on schema drift.
22
-
23
- import { readFileSync, readdirSync, existsSync, writeFileSync } from "node:fs";
24
- import { resolve, join } from "node:path";
25
- import { validate } from "../verify/envelope.mjs";
26
- import { runArgs } from "../lib/argv.mjs";
27
- import { localDir, localRoot, relLocal, globLocal, runSnapshot as runSnapshotPath } from "../lib/paths.mjs";
28
- import { deriveRounds } from "../probe/rounds.mjs";
29
-
30
- /**
31
- * Read a JSON file, tolerating absence/parse errors.
32
- * @param {string} p - Path to the JSON file.
33
- * @returns {(*|null)} The parsed value, or null when the file is missing or not valid JSON.
34
- */
35
- function readJSON(p) {
36
- try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; }
37
- }
38
-
39
- /**
40
- * Parse a Markdown frontmatter block into a flat scalar map.
41
- * @param {string} text - Full document text ("" / null tolerated).
42
- * @returns {Object<string,string>} Top-level `key: value` pairs (quotes stripped); {} when absent.
43
- */
44
- function frontmatter(text) {
45
- const m = /^---\n([\s\S]*?)\n---/.exec(text || "");
46
- if (!m) return {};
47
- const fm = {};
48
- for (const line of m[1].split("\n")) {
49
- const kv = /^([A-Za-z_][\w-]*):\s*(.*)$/.exec(line.trim());
50
- if (kv) fm[kv[1]] = kv[2].replace(/^['"]|['"]$/g, "");
51
- }
52
- return fm;
53
- }
54
-
55
- const MID_RUN = new Set(["orienting", "mapping", "building", "evaluating"]);
56
-
57
- /**
58
- * Is this run still open, per its own ledger? Fails CLOSED — a missing or unreadable
59
- * `harness-run.md` cannot be evidence that a run is in progress.
60
- * @param {string} root - The `.shapeup` directory.
61
- * @param {string} slug - Run slug (a child directory of root).
62
- * @returns {boolean} True when the run's frontmatter status is one of MID_RUN.
63
- */
64
- function isMidRun(root, slug) {
65
- const runPath = join(root, slug, "harness-run.md");
66
- if (!existsSync(runPath)) return false;
67
- try {
68
- return MID_RUN.has(frontmatter(readFileSync(runPath, "utf8")).status);
69
- } catch {
70
- return false; // unreadable ledger → cannot claim the run is open
71
- }
72
- }
73
-
74
- /**
75
- * Find the active run from files alone — the active-scope pointer, else a mid-run harness-run.md.
76
- * Either way the run must be OPEN per its own ledger; a pointer alone is not enough.
77
- * @param {string} cwd - Working-directory root.
78
- * @returns {({slug:string, scope_id?:string}|null)} The active run's slug (and scope when pointed
79
- * at one), or null when no run is in progress.
80
- */
81
- function findRun(cwd) {
82
- const root = localDir(cwd);
83
- if (!existsSync(root)) return null;
84
-
85
- // The pointer names the run the sandbox guard is scoping, NOT necessarily a run still open.
86
- // `.shapeup/active-scope` has existed since v0.3 and nothing has ever cleared it, because
87
- // its original reader (sandbox-guard) fails OPEN on a stale one — a pointer at a finished run
88
- // simply stops matching any substrate. `harness reduce graph --subgraph run` is the opposite kind of reader: it
89
- // turns a pointer into "a run is ALREADY OPEN in this workspace … do NOT open a new run", and
90
- // it now fires on `startup`/`clear`. Following the pointer without checking the run's status
91
- // therefore made every cold session in every repo that had EVER run the harness open with a
92
- // false claim about its own workspace — including sessions with nothing to do with the harness,
93
- // and including the case where the user's next act is to open the run the injection forbids.
94
- // This function's own contract (and `harness reduce graph --subgraph run`'s header) always said "a run only for an
95
- // active-scope pointer or a mid-run harness-run.md"; the status check is what makes that true.
96
- const pointer = readJSON(join(root, "active-scope"));
97
- if (pointer?.slug && isMidRun(root, pointer.slug)) {
98
- return { slug: pointer.slug, scope_id: pointer.scope_id };
99
- }
100
-
101
- for (const entry of readdirSync(root)) {
102
- if (isMidRun(root, entry)) return { slug: entry };
103
- }
104
- return null;
105
- }
106
-
107
- /**
108
- * Derive a RunSnapshot for the active run from FILES ONLY (never conversation memory).
109
- * @param {string} cwd - Working-directory root.
110
- * @returns {(object|null)} A RunSnapshot (domain.schema.json#/$defs/RunSnapshot): slug, optional
111
- * scope_id/status/rounds, latest T0 verdict, board done/total + unfinished, open escalates,
112
- * dispatched-not-ingested `pending_orders`, and a human `rehydrate_hint`. null when no run is active.
113
- */
114
- export function deriveSnapshot(cwd) {
115
- const run = findRun(cwd);
116
- if (!run) return null;
117
- const root = localRoot(cwd, run.slug);
118
-
119
- const snapshot = {
120
- schema_version: 1,
121
- at: new Date().toISOString(),
122
- slug: run.slug,
123
- };
124
- if (run.scope_id) snapshot.scope_id = run.scope_id;
125
-
126
- const runPath = join(root, "harness-run.md");
127
- if (existsSync(runPath)) {
128
- try {
129
- const fm = frontmatter(readFileSync(runPath, "utf8"));
130
- // The terminal allowlist mirrors kernel/probe/resume.mjs's TERMINAL_STATUSES, not just the two
131
- // members it used to carry — a schema/allowlist that disagrees with RUN_STATUSES is silent on
132
- // both sides here (findRun only ever surfaces a MID_RUN run today, so this whole branch is
133
- // unreachable in practice), but is exactly the divergence class this file's own imports exist
134
- // to close elsewhere (deriveRounds, above). "aborted" is a real RUN_STATUSES member.
135
- if (MID_RUN.has(fm.status) || ["shipped", "escalated", "aborted"].includes(fm.status)) snapshot.status = fm.status;
136
- // MIGRATED to the same mechanical derivation `reduce ship` and `report export` already use
137
- // (kernel/probe/rounds.mjs), rather than reading `harness-run.md`'s `rounds_used` literally.
138
- // That frontmatter line is written ONCE, as 0, by `init run`, and nothing in the round loop
139
- // ever rewrites it — so the third mechanical reader of "how many rounds did this run build"
140
- // was the one still reporting the pre-fix number. Measured on a two-round, no-EVAL fixture:
141
- // this line alone reported `rounds_used: 0` beside its own `round: 2` a few lines below,
142
- // the exact disagreement `deriveRounds` exists to close. `fm.rounds_used` still travels in as
143
- // the fallback for a run neither this fix nor deriveRounds can see evidence for (a `--tiny`
144
- // lane, or a run from before any of these artifacts existed).
145
- const derivedRounds = deriveRounds(cwd, run.slug, fm.rounds_used);
146
- if (Number.isFinite(Number(derivedRounds.rounds_used))) snapshot.rounds_used = Number(derivedRounds.rounds_used);
147
- // Kept as its OWN field, never folded into rounds_used — a round built is not a round judged,
148
- // and collapsing the two into one number is exactly the ambiguity this migration removes.
149
- // null (no EVAL result on disk yet) is a real answer and is not written at all, the same
150
- // optional-field discipline every other snapshot field here follows.
151
- if (Number.isFinite(derivedRounds.rounds_judged)) snapshot.rounds_judged = derivedRounds.rounds_judged;
152
- if (/^\d+$/.test(fm.max_rounds || "")) snapshot.max_rounds = Number(fm.max_rounds);
153
- if (fm.auto_level) snapshot.auto_level = fm.auto_level;
154
- if (fm.spec_folder) snapshot.spec_folder = fm.spec_folder;
155
- } catch { /* run ledger unreadable → snapshot stays partial; the files still win */ }
156
- }
157
-
158
- const verdictsDir = join(root, "t0", "verdicts");
159
- if (existsSync(verdictsDir)) {
160
- let latest = null;
161
- for (const f of readdirSync(verdictsDir)) {
162
- const m = /^r(\d+)-a(\d+)\.json$/.exec(f);
163
- if (!m) continue;
164
- const round = Number(m[1]), attempt = Number(m[2]);
165
- if (!latest || round > latest.round || (round === latest.round && attempt > latest.attempt)) {
166
- latest = { round, attempt, file: f };
167
- }
168
- }
169
- if (latest) {
170
- snapshot.round = latest.round;
171
- snapshot.attempt = latest.attempt;
172
- const verdict = readJSON(join(verdictsDir, latest.file));
173
- if (verdict?.overall === "green" || verdict?.overall === "red") {
174
- snapshot.latest_t0 = {
175
- path: relLocal(run.slug, "t0", "verdicts", latest.file),
176
- overall: verdict.overall,
177
- round: latest.round,
178
- attempt: latest.attempt,
179
- };
180
- }
181
- }
182
- }
183
- if (snapshot.round === undefined && snapshot.rounds_used !== undefined) snapshot.round = snapshot.rounds_used;
184
-
185
- const tasksDir = join(root, "tasks");
186
- if (existsSync(tasksDir)) {
187
- const board = { total: 0, done: 0, unfinished: [] };
188
- for (const f of readdirSync(tasksDir)) {
189
- if (!/^TASK-.*\.md$/.test(f)) continue;
190
- board.total++;
191
- try {
192
- const fm = frontmatter(readFileSync(join(tasksDir, f), "utf8"));
193
- if (fm.status === "done") board.done++;
194
- else if (board.unfinished.length < 10) board.unfinished.push(fm.id || f.replace(/\.md$/, ""));
195
- } catch { /* unreadable task counts as unfinished-unknown; skip naming it */ }
196
- }
197
- if (board.total > 0) snapshot.board = board;
198
- }
199
-
200
- const ordersDir = join(root, "orders");
201
- const resultsDir = join(root, "results");
202
- const results = new Set(existsSync(resultsDir) ? readdirSync(resultsDir) : []);
203
- snapshot.pending_orders = existsSync(ordersDir)
204
- ? readdirSync(ordersDir).filter((f) => f.endsWith(".json") && !results.has(f))
205
- : [];
206
-
207
- snapshot.rehydrate_hint =
208
- `mid-run harness state: slug "${snapshot.slug}"` +
209
- (snapshot.scope_id ? `, scope "${snapshot.scope_id}"` : "") +
210
- (snapshot.round !== undefined ? `, round ${snapshot.round}` : "") +
211
- (snapshot.attempt !== undefined ? `, attempt ${snapshot.attempt}` : "") +
212
- (snapshot.status ? `, status ${snapshot.status}` : "") +
213
- (snapshot.board ? `, board ${snapshot.board.done}/${snapshot.board.total} done` : "") +
214
- (snapshot.latest_t0 ? `, latest T0 ${snapshot.latest_t0.overall}` : "") +
215
- (snapshot.pending_orders.length ? `, ${snapshot.pending_orders.length} dispatched-not-ingested order(s): ${snapshot.pending_orders.join(", ")}` : "") +
216
- `. Re-read ${globLocal(snapshot.slug, "harness-run.md")} and the board before continuing — ` +
217
- `trust the files, not the conversation summary. Never re-dispatch an order that already has a result; ` +
218
- `re-derive round/attempt/hill from the files, never from memory.`;
219
-
220
- return snapshot;
221
- }
222
-
223
- /**
224
- * @param {string} cwd - Working-directory root.
225
- * @param {string} slug - Feature slug.
226
- * @returns {string} The LOCAL run-trace path `.shapeup/<slug>/run-snapshot.json`.
227
- */
228
- export function snapshotPath(cwd, slug) {
229
- return runSnapshotPath(cwd, slug);
230
- }
231
-
232
- /**
233
- * Validate then persist a snapshot to its LOCAL run-trace home.
234
- * @param {string} cwd - Working-directory root.
235
- * @param {object} snapshot - The RunSnapshot to persist (its `slug` names the path).
236
- * @returns {void} Side effect: writes the snapshot JSON.
237
- * @throws {Error} If the snapshot drifts from domain.schema.json#/$defs/RunSnapshot.
238
- */
239
- export function writeSnapshot(cwd, snapshot) {
240
- assertValid(snapshot);
241
- writeFileSync(snapshotPath(cwd, snapshot.slug), JSON.stringify(snapshot, null, 2) + "\n");
242
- }
243
-
244
- /**
245
- * Assert a snapshot conforms to its registry definition.
246
- * @param {object} snapshot - The RunSnapshot to check.
247
- * @returns {void}
248
- * @throws {Error} If it fails domain.schema.json#/$defs/RunSnapshot (message lists each error).
249
- */
250
- function assertValid(snapshot) {
251
- const { valid, errors } = validate(snapshot, { $ref: "domain.schema.json#/$defs/RunSnapshot" });
252
- if (!valid) {
253
- throw new Error(`run-snapshot drifted from domain.schema.json#/$defs/RunSnapshot:\n ${errors.join("\n ")}`);
254
- }
255
- }
256
-
257
- // --- CLI -----------------------------------------------------------------------
258
-
259
- /** The typed argv contract (see `./lib/argv.mjs`). */
260
- export const ARGV_SPEC = {
261
- usage: "harness.mjs reduce snapshot [--cwd <dir>] [--format json|text] [--write]",
262
- _: { arity: 0, max: 0, name: "(no positional operands)" },
263
- cwd: { type: "path" },
264
- format: { type: "enum", values: ["json", "text"], default: "json" },
265
- write: { type: "flag" },
266
- };
267
-
268
- /**
269
- * Derive the file-based run snapshot, and optionally freeze it to disk.
270
- *
271
- * @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
272
- * @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
273
- * call `process.exit()` with the subcommand's documented code rather than returning.
274
- */
275
- export async function cli(rawArgv) {
276
- const args = runArgs(ARGV_SPEC, rawArgv);
277
- const cwd = resolve(args.cwd || process.cwd());
278
- const format = args.format;
279
-
280
- const snapshot = deriveSnapshot(cwd);
281
- if (!snapshot) process.exit(0); // no active run → nothing to say (fail-open)
282
-
283
- try {
284
- assertValid(snapshot);
285
- if (args.write) writeSnapshot(cwd, snapshot);
286
- } catch (e) {
287
- console.error(` ✗ ${e.message}`);
288
- process.exit(1);
289
- }
290
- console.log(format === "text" ? snapshot.rehydrate_hint : JSON.stringify(snapshot, null, 2));
291
- }
@@ -1,172 +0,0 @@
1
- #!/usr/bin/env node
2
- // Verdict-ledger reference implementation — spec-evaluator's own skill-local
3
- // reference impl (ships beside SKILL.md; not invoked at runtime).
4
- //
5
- // The `spec-evaluator` skill performs verdict re-probe / confidence / flip-detection as a
6
- // self-contained procedure (`references/verdict-ledger.md`) using its own file tools — it does NOT
7
- // call this file. This is the executable proof that the documented grammar is well-defined and
8
- // actually discriminates an unstable judge from a stable one, the same way oracles/*
9
- // prove the probing grammar. Structural test #15 exercises it.
10
- //
11
- // Ledger line shape (one per criterion per run), see verdict-ledger.md:
12
- // { run, task, dimension, criterion, verdict:"PASS"|"FAIL", confidence, reprobed, flip, evidence, at }
13
- //
14
- // Library use:
15
- // import { reconcile, detectFlips, stability } from "kernel/reduce/verdict.mjs";
16
- // const { records, summary } = reconcile(priorLines, currentRecords);
17
-
18
- // Most recent prior line for a (dimension, criterion), by highest run number.
19
- import { runArgs } from "../lib/argv.mjs";
20
-
21
- /**
22
- * Find the most recent prior ledger line for a record's (dimension, criterion), by highest run.
23
- * @param {Array<{dimension:string, criterion:string, run:number, verdict:string}>} priorLines - Prior lines.
24
- * @param {{dimension:string, criterion:string}} rec - The record to match.
25
- * @returns {(object|null)} The highest-run matching prior line, or null when none exists.
26
- */
27
- function priorFor(priorLines, rec) {
28
- let best = null;
29
- for (const p of priorLines) {
30
- if (p.criterion !== rec.criterion || p.dimension !== rec.dimension) continue;
31
- if (best === null || p.run > best.run) best = p;
32
- }
33
- return best;
34
- }
35
-
36
- // Reconcile this run's records against the prior ledger: a verdict change vs the most recent prior
37
- // line for the same criterion sets flip=true and FORCES confidence="low" (a flip means the oracle
38
- // is unstable on that row, regardless of what confidence the judge proposed). Returns augmented
39
- // records + a summary. Pure: no I/O, no clock (caller stamps `at`).
40
- /**
41
- * Reconcile this run's records against the prior ledger — a verdict change vs the most recent
42
- * prior line for the same criterion sets flip=true and FORCES confidence="low". Pure: no I/O, no clock.
43
- * @param {Array<object>} priorLines - Previously recorded ledger lines.
44
- * @param {Array<{criterion:string, dimension:string, verdict:string, confidence:string}>}
45
- * currentRecords - This run's per-criterion records.
46
- * @returns {{records:Array<object>, summary:{total:number, flipped:number, stable:number,
47
- * flips:Array<{criterion:string, dimension:string, to:string}>}}} The records augmented with
48
- * `flip` (+ forced-low confidence) and a run summary.
49
- */
50
- export function reconcile(priorLines, currentRecords) {
51
- const records = currentRecords.map((rec) => {
52
- const prior = priorFor(priorLines, rec);
53
- const flip = !!prior && prior.verdict !== rec.verdict;
54
- return { ...rec, flip, confidence: flip ? "low" : rec.confidence };
55
- });
56
- const flipped = records.filter((r) => r.flip);
57
- return {
58
- records,
59
- summary: {
60
- total: records.length,
61
- flipped: flipped.length,
62
- stable: records.length - flipped.length,
63
- flips: flipped.map((r) => ({ criterion: r.criterion, dimension: r.dimension, to: r.verdict })),
64
- },
65
- };
66
- }
67
-
68
- // Walk the full ledger in run order; return every criterion whose verdict changed at any step,
69
- // with the run pair and direction. Used for the report's stability block / audit of a whole task.
70
- /**
71
- * Walk the full ledger in run order and report every verdict change.
72
- * @param {Array<{dimension:string, criterion:string, run:number, verdict:string}>} allLines - Every ledger line.
73
- * @returns {Array<{criterion:string, dimension:string, from:string, to:string, runs:[number,number]}>}
74
- * One entry per step where a criterion's verdict changed.
75
- */
76
- export function detectFlips(allLines) {
77
- const byKey = new Map();
78
- for (const l of [...allLines].sort((a, b) => a.run - b.run)) {
79
- const key = `${l.dimension}::${l.criterion}`;
80
- const seq = byKey.get(key) || [];
81
- seq.push(l);
82
- byKey.set(key, seq);
83
- }
84
- const flips = [];
85
- for (const [key, seq] of byKey) {
86
- for (let i = 1; i < seq.length; i++) {
87
- if (seq[i].verdict !== seq[i - 1].verdict) {
88
- flips.push({
89
- criterion: seq[i].criterion,
90
- dimension: seq[i].dimension,
91
- from: seq[i - 1].verdict,
92
- to: seq[i].verdict,
93
- runs: [seq[i - 1].run, seq[i].run],
94
- });
95
- }
96
- }
97
- }
98
- return flips;
99
- }
100
-
101
- // Stability of the latest run vs the run before it: fraction of the latest run's criteria whose
102
- // verdict matches their immediately-prior line. 1.0 when there is only one run (nothing to contradict).
103
- /**
104
- * Measure stability of the latest run vs the run before it.
105
- * @param {Array<{run:number, dimension:string, criterion:string, verdict:string}>} allLines - Every ledger line.
106
- * @returns {{runs:number, stable:number, total:number, ratio:number}} The max run number, how many
107
- * of the latest run's criteria match their prior verdict (new criteria count as stable), the
108
- * latest run's criterion count, and their ratio (1.0 when there is only one run).
109
- */
110
- export function stability(allLines) {
111
- if (allLines.length === 0) return { runs: 0, stable: 0, total: 0, ratio: 1 };
112
- const maxRun = Math.max(...allLines.map((l) => l.run));
113
- const latest = allLines.filter((l) => l.run === maxRun);
114
- if (maxRun === 1) return { runs: 1, stable: latest.length, total: latest.length, ratio: 1 };
115
- const prior = allLines.filter((l) => l.run < maxRun);
116
- let stable = 0;
117
- for (const rec of latest) {
118
- const p = priorFor(prior, rec);
119
- if (p && p.verdict === rec.verdict) stable++;
120
- else if (!p) stable++; // a brand-new criterion can't have flipped
121
- }
122
- return { runs: maxRun, stable, total: latest.length, ratio: latest.length ? stable / latest.length : 1 };
123
- }
124
-
125
- // Parse a .jsonl ledger string into records (blank lines ignored).
126
- /**
127
- * Parse a `.jsonl` verdict ledger into records.
128
- * @param {string} text - The ledger file contents ("" / null → []).
129
- * @returns {Array<object>} One parsed object per non-blank line.
130
- * @throws {SyntaxError} If a non-blank line is not valid JSON.
131
- */
132
- export function parseLedger(text) {
133
- return (text || "")
134
- .split(/\r?\n/)
135
- .map((l) => l.trim())
136
- .filter(Boolean)
137
- .map((l) => JSON.parse(l));
138
- }
139
-
140
- // --- CLI entry: summarize a ledger file -------------------------------------
141
- /** The typed argv contract (see `kernel/lib/argv.mjs`). */
142
- export const ARGV_SPEC = {
143
- usage: "harness.mjs reduce verdict <.verdicts-TASK.jsonl>",
144
- _: { arity: 1, max: 1, name: ".verdicts-TASK.jsonl" },
145
- };
146
-
147
- /**
148
- * Reconcile a verdict ledger and report its flips and stability.
149
- *
150
- * @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
151
- * @returns {(Promise<void>|void)} Settles when the subcommand has written its output; most paths
152
- * call `process.exit()` with the subcommand's documented code rather than returning.
153
- */
154
- export async function cli(rawArgv) {
155
- const { readFileSync } = await import("node:fs");
156
- const path = runArgs(ARGV_SPEC, rawArgv)._[0];
157
- let lines;
158
- try { lines = parseLedger(readFileSync(path, "utf8")); }
159
- catch (e) { console.error(`cannot read ledger ${path}: ${e.message}`); process.exit(2); }
160
- const flips = detectFlips(lines);
161
- const s = stability(lines);
162
- console.log(`ledger: ${lines.length} records over ${s.runs} run(s)`);
163
- console.log(`stability (latest vs prior): ${s.stable}/${s.total} stable`);
164
- if (flips.length) {
165
- console.log(`⚠ ${flips.length} flip(s):`);
166
- for (const f of flips) console.log(` ${f.dimension}/${f.criterion}: ${f.from}→${f.to} (runs ${f.runs.join("→")})`);
167
- process.exit(1);
168
- } else {
169
- console.log("✅ no verdict flips");
170
- process.exit(0);
171
- }
172
- }