@sabaiway/agent-workflow-kit 2.1.0 → 3.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 (99) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +6 -5
  3. package/SKILL.md +12 -8
  4. package/bin/install.mjs +1 -1
  5. package/bridges/antigravity-cli-bridge/SKILL.md +8 -1
  6. package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +150 -0
  7. package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +76 -0
  8. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +80 -15
  9. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +95 -4
  10. package/bridges/antigravity-cli-bridge/capability.json +3 -2
  11. package/bridges/codex-cli-bridge/SKILL.md +8 -1
  12. package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +143 -0
  13. package/bridges/codex-cli-bridge/bin/codex-review.sh +71 -10
  14. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +93 -10
  15. package/bridges/codex-cli-bridge/capability.json +3 -2
  16. package/capability.json +1 -1
  17. package/migrations/3.0.0-hardened-core-loop.md +44 -0
  18. package/migrations/README.md +1 -1
  19. package/package.json +2 -2
  20. package/references/hooks/gate-approve.mjs +1 -1
  21. package/references/modes/bootstrap.md +3 -3
  22. package/references/modes/commit-guard.md +19 -0
  23. package/references/modes/core-evidence.md +15 -0
  24. package/references/modes/coverage-check.md +12 -0
  25. package/references/modes/doc-parity.md +7 -6
  26. package/references/modes/gates.md +7 -6
  27. package/references/modes/grounding.md +1 -2
  28. package/references/modes/hook.md +1 -1
  29. package/references/modes/review-state.md +3 -3
  30. package/references/modes/upgrade.md +6 -6
  31. package/references/modes/velocity.md +2 -2
  32. package/references/scripts/archive-decisions.mjs +1 -1
  33. package/references/scripts/install-git-hooks-repo-exec.test.mjs +82 -0
  34. package/references/scripts/install-git-hooks.mjs +90 -18
  35. package/references/scripts/install-git-hooks.test.mjs +102 -0
  36. package/references/scripts/migrate-gates-branches.test.mjs +157 -0
  37. package/references/scripts/migrate-gates.mjs +395 -0
  38. package/references/scripts/migrate-gates.test.mjs +284 -0
  39. package/references/templates/agent_rules.md +2 -2
  40. package/tools/ack-write.mjs +1 -1
  41. package/tools/atomic-write.mjs +2 -2
  42. package/tools/autonomy-config.mjs +1 -1
  43. package/tools/autonomy-doctor.mjs +1 -1
  44. package/tools/autonomy-write.mjs +1 -1
  45. package/tools/bridge-settings-read.mjs +1 -1
  46. package/tools/bridge-settings.mjs +1 -1
  47. package/tools/changed-surface.mjs +8 -69
  48. package/tools/cheap-agents.mjs +2 -2
  49. package/tools/commands.mjs +17 -10
  50. package/tools/commit-guard.mjs +167 -0
  51. package/tools/core-evidence.mjs +914 -0
  52. package/tools/coverage-check.mjs +260 -0
  53. package/tools/delegation.mjs +1 -1
  54. package/tools/detect-backends.mjs +3 -3
  55. package/tools/doc-parity.mjs +11 -27
  56. package/tools/engine-source.mjs +1 -1
  57. package/tools/family-members.mjs +1 -1
  58. package/tools/family-registry.mjs +1 -1
  59. package/tools/fs-safe.mjs +1 -1
  60. package/tools/gate-hook.mjs +1 -1
  61. package/tools/{seed-gates.mjs → gates-init.mjs} +58 -52
  62. package/tools/grounding.mjs +6 -83
  63. package/tools/hide-footprint.mjs +1 -1
  64. package/tools/inject-methodology.mjs +1 -1
  65. package/tools/known-footprint.mjs +1 -1
  66. package/tools/labels.mjs +1 -1
  67. package/tools/lcov.mjs +6 -10
  68. package/tools/lens-region.mjs +1 -1
  69. package/tools/manifest/validate.mjs +19 -0
  70. package/tools/migrate-adr-store.mjs +2 -2
  71. package/tools/orchestration-config.mjs +1 -1
  72. package/tools/orchestration-write.mjs +1 -1
  73. package/tools/presentation.mjs +1 -1
  74. package/tools/procedures.mjs +2 -2
  75. package/tools/recipes.mjs +60 -9
  76. package/tools/recommendations.mjs +63 -7
  77. package/tools/release-scan.mjs +1 -1
  78. package/tools/renderers.mjs +1 -1
  79. package/tools/review-state.mjs +288 -340
  80. package/tools/run-gates.mjs +216 -92
  81. package/tools/sandbox-masks.mjs +1 -1
  82. package/tools/semver-lite.mjs +1 -1
  83. package/tools/set-autonomy.mjs +1 -1
  84. package/tools/set-recipe.mjs +1 -1
  85. package/tools/setup-backends.mjs +1 -1
  86. package/tools/surface.mjs +1 -1
  87. package/tools/uninstall.mjs +1 -1
  88. package/tools/velocity-profile.mjs +2 -2
  89. package/tools/view-model.mjs +1 -1
  90. package/references/modes/fold-completeness.md +0 -30
  91. package/references/modes/review-ledger.md +0 -34
  92. package/references/templates/verification-profile.json +0 -10
  93. package/tools/fold-completeness-run.mjs +0 -1120
  94. package/tools/fold-completeness.mjs +0 -672
  95. package/tools/review-ledger-core.mjs +0 -428
  96. package/tools/review-ledger-write.mjs +0 -647
  97. package/tools/review-ledger.mjs +0 -630
  98. package/tools/sarif.mjs +0 -52
  99. package/tools/verification-profile.mjs +0 -219
@@ -1,672 +0,0 @@
1
- #!/usr/bin/env node
2
- // fold-completeness.mjs — the read-only FOLD-COMPLETENESS checker behind `/agent-workflow-kit
3
- // fold-completeness` (M3, AD-046, DEBT-TEST-COMPLETENESS). It is the sibling gate of
4
- // review-ledger.mjs: the ledger computes the review-loop crossover-stop; this tool computes whether
5
- // the tests that the loop's folds bind to ACTUALLY pin the changed code. It reads the result ledger
6
- // the runner writes (fold-completeness-run.mjs — the SOLE tree-toucher + result writer), recomputes
7
- // the canonical uncommitted-state fingerprint, resolves the plan-execution.review recipe + the
8
- // in-flight plan, and decides `--check` fail-closed. It NEVER runs tests, NEVER mutates, and NEVER
9
- // imports the runner — an import-split test pins that structural invariant (the read half owns the
10
- // result SCHEMA + reader; the runner imports them the other direction, mirroring review-ledger /
11
- // review-ledger-write).
12
- //
13
- // Normative `--check` exit contract (the single home of this list — SKILL.md / the mode file point
14
- // here). The gate is plan-EXECUTION-scoped, filtered to the in-flight plan's filename stem:
15
- // exit 0 when the resolved plan-execution.review recipe is solo (configured, or degraded there);
16
- // when no plan is in flight; when the tree is clean; when the cwd is not a git work tree;
17
- // and when the in-flight plan-execution SEGMENT — (loop, base = git rev-parse HEAD),
18
- // schema v3+ (v4 adds the (a) suite-execution evidence + the (c) reattest custody anchor,
19
- // BUGFREE-3/AD-049), AD-048 D7 — has a CURRENT run record (kind-aware: the latest RUN, never a
20
- // later red-probe or reattest) whose BOTH bindings match — the tree fingerprint AND the SEGMENT's
21
- // sorted fixable-bug testId set recorded in the run — with, per bound testId: an
22
- // N/N-green probe (D4), an observed-red receipt in this SEGMENT (a committed phase's
23
- // obligations closed with its commit; a receipt never crosses a commit boundary), that receipt
24
- // PRECEDING the latest run in ledger order (anti-post-hoc), and content CUSTODY — the run's
25
- // recorded test-file hash equals the latest custody-eligible red-probe OR reattest hash on that
26
- // file (eligible = its own testId is bound AND it precedes the run, D5; a green-only append is
27
- // re-anchored by a recorded --reattest, not a red-proof waiver); plus 0
28
- // uncovered changed lines, 0 changed unsupported-source files, a recorded tamper surface
29
- // with every tampered test-surface file covered by a recorded oracle-change override
30
- // (review ledger v3), and the reserved EMPTY mutation shape (no mutation ships). A
31
- // recorded red-proof override waives the receipt + custody proof for exactly the testId
32
- // it names (D7) — never the N/N-green requirement, never QUARANTINE.
33
- // exit 1 for any DIRTY in-flight plan-execution segment lacking such a current run record — including
34
- // the stale-fingerprint case (a tree edit moves the fingerprint), the same-fingerprint/
35
- // new-testId case (a triage recorded after the run moves the bound-testId set, Decision 9),
36
- // and a schema-1 (or tamper-less) record as the loop's latest run (an older runner; D2) —
37
- // or a tampered test-surface file (a removed/modified line in a pre-existing test file,
38
- // or a deleted one — the union of test-classified paths and bound-testId file halves)
39
- // not covered by a recorded oracle-change override, a run naming an unresolvable bound
40
- // test, a QUARANTINED bound
41
- // test (mixed or timed-out probe runs — never an N/N verdict, never converted, no override
42
- // lane; D4), a red-baseline bound test, a green bound test with NO observed-red receipt /
43
- // a post-hoc receipt / broken custody (D5), an uncovered changed line, a changed
44
- // unsupported-source file, or ANY mutation data (no mutation ships — such a record was not
45
- // produced by this runner; fail closed); when MORE THAN ONE plan is in flight (ambiguous
46
- // loop id). Fail-CLOSED (unknown state, never a fail-open pass) on a detector failure, an
47
- // unreadable/malformed result or review ledger, or a corrupt run set — the only
48
- // detector-independent green is an EXPLICIT configured solo. Changed OUT-OF-DOMAIN files
49
- // (docs/config the suite does not execute) are LOUDLY listed but never gate-blocking
50
- // (Decision 5): guarding what the tool cannot assess with a red gate is a pretend-mechanism.
51
- //
52
- // HONEST residuals (accepted, documented — exactly like review-state's / review-ledger's): coverage
53
- // proves execution, not assertion; NO mutation signal ships in v1 — the researched mutation half was
54
- // shelved, the record's `mutation` field is a reserved empty shape (a record carrying ANY mutation
55
- // data fails the check closed); the result records, testIds, and the ledger are forgeable
56
- // (`git commit --no-verify`, file editing) — a self-discipline mechanism against silent process
57
- // drift, NOT a security boundary. TS/JSX source is out of scope v1 (a changed unsupported-source
58
- // file fails the gate closed rather than be vouched for).
59
- //
60
- // Read-only: never writes, never commits, never runs a subscription CLI. It DOES spawn read-only
61
- // `git` queries (via the reused review-state fingerprint reader + a git-dir resolver). Dependency-
62
- // free, Node >= 18. No side effects on import (the isDirectRun idiom).
63
-
64
- import { readFileSync } from 'node:fs';
65
- import { pathToFileURL } from 'node:url';
66
- import { spawnSync } from 'node:child_process';
67
- import { detectBackends } from './detect-backends.mjs';
68
- import { resolveActivityRecipe, planRecipe, DISPLAY_ALIASES } from './recipes.mjs';
69
- import { CONFIG_REL, fail, loadConfig } from './orchestration-config.mjs';
70
- import { computeTreeFingerprint, isTreeClean, plansInFlight } from './review-state.mjs';
71
- import { resolveLedgerPath, resolveBase, readLedger, filterSegmentRecords, collectOverrides, isWellFormedTestId, splitTestId } from './review-ledger.mjs';
72
- // The fold-ledger locator and the D4 probe-verdict algebra live in the NEUTRAL shared module
73
- // (BUGFREE-2 / AD-048, D8): review-ledger.mjs telemetry reads the fold ledger through THAT module —
74
- // importing this checker from there would close an import cycle (this file imports review-ledger).
75
- // Re-exported here so the runner and every existing consumer keep their one entry point.
76
- import { probeVerdict, RESULTS_BASENAME, resolveResultsPath } from './changed-surface.mjs';
77
-
78
- export { probeVerdict, RESULTS_BASENAME, resolveResultsPath };
79
- // SCHEMA v2 (BUGFREE-1 / AD-047): records gain a kind discriminator — `run` (the fold-completeness
80
- // run, now with per-testId rerun counts + the test file's content hash) | `red-probe` (the
81
- // observed-red receipt --red mints). SCHEMA v3 (BUGFREE-2 / AD-048, D7): records carry `base` — the
82
- // SEGMENT frame — and the gate scopes bound testIds, receipts, custody, and tamper to
83
- // (loop, base = current HEAD): a committed phase's custody obligations close with its commit (the
84
- // cross-phase-churn override class dies). RESULT_SCHEMA_VERSION is what the WRITER emits; the
85
- // reader tolerates every SUPPORTED version under its own per-version rules (the review-ledger
86
- // v1→v2 precedent), so v1/v2 ledgers never retroactively become malformed — but only a v3 record
87
- // can enter a segment, so a dirty segment whose loop holds only pre-v3 records fails with a reason
88
- // naming the schema upgrade (D7 legacy rule).
89
- // SCHEMA v4 (BUGFREE-3 / AD-049): a v4 run record carries a `suite` execution-evidence block (the (a)
90
- // one-suite-run credit — cmd + exit + pre/post fingerprints) AND a new record kind `reattest` joins
91
- // `run`/`red-probe`: an operator-minted, RECORDED custody re-attestation that anchors a bound test
92
- // file's custody at its CURRENT bytes WITHOUT fabricating a red-observe (the honest replacement for
93
- // mis-using red-proof on a green-only test-file append, item (c)). A placed pre-v4 checker (SUPPORTED
94
- // = {1,2,3}) rejects a v4 record outright — the version-skew guard; v1/v2/v3 rules are untouched.
95
- export const RESULT_SCHEMA_VERSION = 4;
96
- export const SUPPORTED_RESULT_SCHEMAS = new Set([1, 2, 3, 4]);
97
- // The v4 record kind (c): a recorded re-attestation. Named as a const so the runner, the checker, and
98
- // the doc-parity lint share one token (no drift).
99
- export const REATTEST_KIND = 'reattest';
100
- const ACTIVITY = 'plan-execution';
101
- const SLOT = 'review';
102
-
103
- // ── git-dir resolution (read-only queries; the ledger lives in the git dir, uncommittable) ──────
104
-
105
- const gitLine = (args, cwd) => {
106
- const r = spawnSync('git', args, { cwd, windowsHide: true });
107
- if (r.error || r.status !== 0) return null;
108
- return r.stdout.toString('utf8').replace(/\r?\n$/, '');
109
- };
110
-
111
- const gitRoot = (cwd) => gitLine(['rev-parse', '--show-toplevel'], cwd);
112
-
113
- // ── the bound fixable-bug testId set (the SINGLE source of truth — runner + checker share it) ────
114
-
115
- // collectBoundTestIds(reviewRecords, { activity, loop, base }) → the sorted, de-duplicated testIds
116
- // of the SEGMENT's fixable-bug classifications (D7: pass base — a committed segment's folds are
117
- // closed obligations; `base: undefined` would match nothing, never legacy records). Pure over
118
- // review-ledger records. Both the runner (which records the set as a binding) and the checker
119
- // (which recomputes it for the staleness check) call THIS, so the two can never drift (Decision 9:
120
- // same-fingerprint/new-testId is stale).
121
- export const collectBoundTestIds = (reviewRecords, { activity = ACTIVITY, loop, base } = {}) => {
122
- const ids = new Set();
123
- for (const r of filterSegmentRecords(reviewRecords, { activity, loop, base })) {
124
- if (r.kind !== 'triage') continue;
125
- for (const c of r.classifications) if (c.class === 'fixable-bug' && typeof c.testId === 'string' && c.testId.length > 0) ids.add(c.testId);
126
- }
127
- return [...ids].sort();
128
- };
129
-
130
- // ── result-record schema (machine fields only; the runner refuses to write a malformed record) ───
131
-
132
- const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
133
- const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;
134
- const isNonNegInt = (v) => Number.isInteger(v) && v >= 0;
135
- const isStringArray = (v) => Array.isArray(v) && v.every((x) => typeof x === 'string');
136
-
137
- const HASH_RE = /^[0-9a-f]{64}$/; // sha-256 hex — the content-custody hash shape
138
-
139
- // The shared record frame (every version, both kinds). v3 adds the SEGMENT frame: base is REQUIRED
140
- // (null on an unborn branch); a v1/v2 record never carries it — an old record never grows new
141
- // surface (the review-ledger D2 discipline).
142
- const validateFrame = (obj) => {
143
- if (!isNonEmptyString(obj.loop)) return 'missing loop';
144
- if (!(obj.fingerprint === null || isNonEmptyString(obj.fingerprint))) return 'fingerprint must be null or a non-empty string';
145
- if (!isNonEmptyString(obj.timestamp)) return 'missing timestamp';
146
- if (obj.schema >= 3) {
147
- if (!(obj.base === null || isNonEmptyString(obj.base))) return 'a v3 record requires base — null (unborn branch) or the HEAD commit the dirty tree sits on';
148
- } else if (obj.base !== undefined) {
149
- return `base is a v3 frame field — a schema-${obj.schema} record never carries it`;
150
- }
151
- return null;
152
- };
153
-
154
- // A v1 per-testId probe entry (single-run booleans — the AD-046 shape, tolerated read-only).
155
- const validateV1Entry = (t) => {
156
- if (!isPlainObject(t) || !isNonEmptyString(t.id)) return 'each testId entry needs an id';
157
- if (typeof t.resolvable !== 'boolean' || typeof t.baselineGreen !== 'boolean') return `testId ${t.id} needs boolean resolvable + baselineGreen`;
158
- if (!isNonNegInt(t.executed)) return `testId ${t.id} executed must be a non-negative integer`;
159
- return null;
160
- };
161
-
162
- // A v2 per-testId probe entry: rerun counts (the D4 evidence) + the test file's content hash (the
163
- // D5 custody anchor) + the derived booleans, VALIDATED consistent with the counts so a forged
164
- // verdict cannot ride beside honest-looking evidence.
165
- const validateV2Entry = (t) => {
166
- if (!isPlainObject(t) || !isNonEmptyString(t.id)) return 'each testId entry needs an id';
167
- if (!isNonNegInt(t.executed)) return `testId ${t.id} executed must be a non-negative integer`;
168
- if (!(Number.isInteger(t.runs) && t.runs >= 1)) return `testId ${t.id} runs must be a positive integer`;
169
- if (!isNonNegInt(t.greens) || !isNonNegInt(t.reds) || !isNonNegInt(t.timeouts)) return `testId ${t.id} rerun counts (greens/reds/timeouts) must be non-negative integers`;
170
- if (t.greens + t.reds + t.timeouts > t.runs) return `testId ${t.id} rerun counts exceed runs`;
171
- // A resolved run means at least one matched (executed) test result — greens/reds with executed=0
172
- // is a forged N/N verdict carrying zero-match evidence (codex R1, BUGFREE-1 live loop).
173
- if (t.greens + t.reds > 0 && t.executed < 1) return `testId ${t.id} executed must be positive when a run resolved (greens+reds > 0)`;
174
- if (!(t.fileHash === null || (typeof t.fileHash === 'string' && HASH_RE.test(t.fileHash)))) return `testId ${t.id} fileHash must be null or a 64-hex content hash`;
175
- if (t.resolvable !== (t.greens + t.reds === t.runs)) return `testId ${t.id} resolvable must equal (greens + reds === runs)`;
176
- if (t.baselineGreen !== (t.greens === t.runs)) return `testId ${t.id} baselineGreen must equal (greens === runs)`;
177
- return null;
178
- };
179
-
180
- // The run-record body shared by v1 and v2 (surface classes, coverage, reserved mutation, budgets);
181
- // the per-testId entry rule is the per-version part.
182
- const validateRunBody = (obj, validateEntry) => {
183
- if (!isStringArray(obj.boundTestIds)) return 'boundTestIds must be an array of strings';
184
- if (!Array.isArray(obj.testIds)) return 'testIds must be an array';
185
- for (const t of obj.testIds) {
186
- const r = validateEntry(t);
187
- if (r) return r;
188
- }
189
- if (!isStringArray(obj.unsupported)) return 'unsupported must be an array of strings';
190
- if (!isStringArray(obj.outOfDomain)) return 'outOfDomain must be an array of strings';
191
- if (!isPlainObject(obj.coverage) || !Array.isArray(obj.coverage.uncoveredChanged)) return 'coverage.uncoveredChanged must be an array';
192
- for (const u of obj.coverage.uncoveredChanged) {
193
- if (!isPlainObject(u) || !isNonEmptyString(u.file)) return 'each uncoveredChanged entry needs a file';
194
- if (!(u.line === null || (Number.isInteger(u.line) && u.line >= 1))) return `uncoveredChanged ${u.file} line must be null or an integer >= 1`;
195
- }
196
- const m = obj.mutation;
197
- if (!isPlainObject(m) || !isNonNegInt(m.total) || !isNonNegInt(m.killed) || !Array.isArray(m.survived) || !isNonNegInt(m.skipped)) {
198
- return 'mutation must be { total, killed, survived[], skipped, killSetBasis }';
199
- }
200
- if (!(m.killSetBasis === null || isNonEmptyString(m.killSetBasis))) return 'mutation.killSetBasis must be null or a non-empty string';
201
- if (!isPlainObject(obj.budgets)) return 'budgets must be an object';
202
- return null;
203
- };
204
-
205
- // The red-probe receipt (v2 only): the machine attestation that testId FAILED on N/N runs at the
206
- // recorded content hash. Minted only by the runner's --red verb; reds must equal runs — a receipt
207
- // never records anything but an honest N/N red (refusals write nothing).
208
- const validateRedProbe = (obj) => {
209
- if (!isWellFormedTestId(obj.testId)) return 'red-probe testId must be "<test-file>#<test-name-pattern>" (a "#" separator, both halves non-empty)';
210
- if (!(typeof obj.fileHash === 'string' && HASH_RE.test(obj.fileHash))) return 'red-probe fileHash must be a 64-hex content hash';
211
- if (!(Number.isInteger(obj.runs) && obj.runs >= 1)) return 'red-probe runs must be a positive integer';
212
- if (obj.reds !== obj.runs) return 'red-probe reds must equal runs (a receipt attests N/N observed red)';
213
- return null;
214
- };
215
-
216
- // The (c) re-attest receipt (v4 only): the operator asserts a bound test file's CURRENT bytes are a
217
- // legitimate custody anchor (a green-only append after a --red receipt — there is no red to observe).
218
- // It mirrors the red-probe receipt MINUS the observation counts: testId + the file's current hash.
219
- // Minted only by the runner's --reattest verb (self-discipline, the same trust model as the overrides).
220
- const validateReattest = (obj) => {
221
- if (!isWellFormedTestId(obj.testId)) return 'reattest testId must be "<test-file>#<test-name-pattern>" (a "#" separator, both halves non-empty)';
222
- if (!(typeof obj.fileHash === 'string' && HASH_RE.test(obj.fileHash))) return 'reattest fileHash must be a 64-hex content hash';
223
- return null;
224
- };
225
-
226
- // The (a) suite-execution evidence (v4 run records): the ONE suite spawn per fingerprint, recorded so
227
- // run-gates --record can CREDIT the unit-tests gate from it instead of re-spawning. Here only the SHAPE
228
- // is checked (a non-empty cmd, an integer-or-null exit, null-or-string before/after fingerprints); the
229
- // credit's fingerprint/tree/cmd-identity/exit-0 conditions live in the ledger writer (review-ledger.mjs).
230
- const validateSuiteEvidence = (suite) => {
231
- if (!isPlainObject(suite)) return 'a v4 run records its suite-execution evidence ({ cmd, exit, fingerprintBefore, fingerprintAfter })';
232
- if (!isNonEmptyString(suite.cmd)) return 'suite.cmd must be a non-empty string';
233
- if (!(suite.exit === null || Number.isInteger(suite.exit))) return 'suite.exit must be an integer or null';
234
- for (const k of ['fingerprintBefore', 'fingerprintAfter']) {
235
- if (!(suite[k] === null || isNonEmptyString(suite[k]))) return `suite.${k} must be null or a non-empty fingerprint string`;
236
- }
237
- return null;
238
- };
239
-
240
- // validateRunRecord(obj) → { ok, reason }. Per-version, per-kind (D2): v1 records (no kind) keep the
241
- // AD-046 single-run rules; v2 records carry the kind discriminator. The `reason` names the exact
242
- // failed check so the malformed-line surface and the per-check named tests can assert it. Mutation
243
- // stays the reserved empty shape (the mutation half is shelved); the schema carries the fields so a
244
- // record validates uniformly and decideCheck enforces the exact reserved shape.
245
- export const validateRunRecord = (obj) => {
246
- if (!isPlainObject(obj)) return { ok: false, reason: 'not an object' };
247
- if (!SUPPORTED_RESULT_SCHEMAS.has(obj.schema)) return { ok: false, reason: `schema must be one of ${[...SUPPORTED_RESULT_SCHEMAS].join(', ')}` };
248
- const frame = validateFrame(obj);
249
- if (frame) return { ok: false, reason: frame };
250
- if (obj.schema === 1) {
251
- if (obj.kind !== undefined) return { ok: false, reason: 'a v1 record must not carry kind (kind is a v2 discriminator)' };
252
- const r = validateRunBody(obj, validateV1Entry);
253
- return r ? { ok: false, reason: r } : { ok: true };
254
- }
255
- if (obj.kind === 'run') {
256
- const r = validateRunBody(obj, validateV2Entry);
257
- if (r) return { ok: false, reason: r };
258
- // The tamper surface is OPTIONAL on a v2 read — a record written by the pre-tamper v2 runner
259
- // stays readable (never retroactively malformed); the GATE handles the staleness. A v3 runner
260
- // ALWAYS records it, so a v3 run without one is not this runner's output. When present, the
261
- // shape is exact.
262
- if (obj.schema >= 3 && obj.tamper === undefined) return { ok: false, reason: 'a v3 run records its tamper surface' };
263
- if (obj.tamper !== undefined && (!isPlainObject(obj.tamper) || !isStringArray(obj.tamper.tampered))) {
264
- return { ok: false, reason: 'tamper.tampered must be an array of strings (when the tamper surface is recorded)' };
265
- }
266
- // v4 (a): a v4 runner ALWAYS records the suite-execution evidence, so a v4 run without one is not
267
- // this runner's output (the tamper-on-v3 precedent). v1/v2/v3 runs never carry it.
268
- if (obj.schema >= 4) {
269
- const sr = validateSuiteEvidence(obj.suite);
270
- if (sr) return { ok: false, reason: sr };
271
- } else if (obj.suite !== undefined) {
272
- return { ok: false, reason: `suite is a v4 field — a schema-${obj.schema} run never carries it` };
273
- }
274
- return { ok: true };
275
- }
276
- if (obj.kind === 'red-probe') {
277
- const r = validateRedProbe(obj);
278
- return r ? { ok: false, reason: r } : { ok: true };
279
- }
280
- if (obj.kind === REATTEST_KIND) {
281
- // (c): a v4-only record kind — a v1/v2/v3 record never carries it (the version-skew guard).
282
- if (!(obj.schema >= 4)) return { ok: false, reason: `${REATTEST_KIND} is a v4 record kind — a schema-${obj.schema} record never carries it` };
283
- const r = validateReattest(obj);
284
- return r ? { ok: false, reason: r } : { ok: true };
285
- }
286
- return { ok: false, reason: `kind must be "run", "red-probe", or "${REATTEST_KIND}" (got ${JSON.stringify(obj.kind)})` };
287
- };
288
-
289
- // readResults(path) → { records, malformed, malformedReasons, readError }. Absent file → empty (no run
290
- // yet). A non-ENOENT read error surfaces as readError so callers fail CLOSED (mirrors readLedger).
291
- export const readResults = (path, readFile = readFileSync) => {
292
- let raw;
293
- try {
294
- raw = readFile(path, 'utf8');
295
- } catch (err) {
296
- if (err && err.code === 'ENOENT') return { records: [], malformed: 0, malformedReasons: [] };
297
- return { records: [], malformed: 0, malformedReasons: [], readError: (err && err.code) || (err && err.message) || 'read failed' };
298
- }
299
- const records = [];
300
- const malformedReasons = [];
301
- for (const line of raw.split('\n')) {
302
- if (line.trim() === '') continue;
303
- let parsed;
304
- try {
305
- parsed = JSON.parse(line);
306
- } catch (err) {
307
- malformedReasons.push(`unparseable JSON (${err.message})`);
308
- continue;
309
- }
310
- const v = validateRunRecord(parsed);
311
- if (v.ok) records.push(parsed);
312
- else malformedReasons.push(v.reason);
313
- }
314
- return { records, malformed: malformedReasons.length, malformedReasons };
315
- };
316
-
317
- // filterLoopResults(records, loop) → the result records of ONE loop (both kinds), order preserved
318
- // (latest is last).
319
- export const filterLoopResults = (records, loop) => records.filter((r) => r.loop === loop);
320
-
321
- // filterSegmentResults(records, loop, base) → the result records of ONE segment (D7), order
322
- // preserved. STRICT: only a v3+ record carries base, so pre-v3 history can never enter a segment
323
- // (readable, never re-scoped — the review-ledger filterSegmentRecords discipline).
324
- export const filterSegmentResults = (records, loop, base) =>
325
- filterLoopResults(records, loop).filter((r) => r.schema >= 3 && r.base === base);
326
-
327
- // ── kind-aware selectors (shared: the runner and the checker read the ledger through THESE) ──────
328
-
329
- // A v1 record IS a run (the kindless AD-046 shape); a v2 record is a run only under kind:"run".
330
- export const isRunRecord = (r) => r.schema === 1 || r.kind === 'run';
331
- export const isRedProbeRecord = (r) => r.schema >= 2 && r.kind === 'red-probe';
332
- // (c) v4: a recorded custody re-attestation. Never read as "the latest run" (latestRunRecord uses
333
- // isRunRecord); consumed by the custody chain as a valid anchor for exactly the file it names.
334
- export const isReattestRecord = (r) => r.schema >= 4 && r.kind === REATTEST_KIND;
335
-
336
- // latestRunRecord(loopRecords) → { record, index } | null over ONE loop's ordered records. Kind-aware
337
- // (codex R2): a red-probe appended after a run must never be read as the loop's "latest run" — the
338
- // index anchors the D5/order checks (a custody-eligible receipt PRECEDES the latest run).
339
- export const latestRunRecord = (loopRecords) => {
340
- for (let i = loopRecords.length - 1; i >= 0; i -= 1) {
341
- if (isRunRecord(loopRecords[i])) return { record: loopRecords[i], index: i };
342
- }
343
- return null;
344
- };
345
-
346
- // probeVerdict — the D4 verdict algebra, ONE home (now the neutral changed-surface.mjs; re-exported
347
- // above): the runner (--red mints only on 'red'), this checker (the gate passes only on 'green'),
348
- // and the review-ledger telemetry (quarantine counts) all read the SAME algebra.
349
-
350
- // foldSuiteCredit({ cwd, env, fingerprint }) → { credited, evidence, reason }. Read half of the (a)
351
- // credit: may run-gates --record credit the unit-tests gate from the latest segment fold run's suite
352
- // evidence instead of re-spawning? STRICT — the evidence must be fingerprint-bound (before === after
353
- // === the current fingerprint, which also proves the tree unchanged) and exit 0. The cmd-identity check
354
- // (suite.cmd === the gate cmd — the --only-subset defense) is the caller's. Any mismatch → credited:false.
355
- // ENV residual (bounded, documented — maintainer-signed against Decision 7): the fold V8 suite runs under
356
- // NODE_V8_COVERAGE, a plain gate spawn does not. Both now strip NODE_TEST_CONTEXT (run-gates.mjs
357
- // spawnGateViaBash mirrors the fold suite's childTestEnv), so that divergence — the real vacuous-skip
358
- // false-green — is closed. The ONE remaining delta is NODE_V8_COVERAGE: a test that FAILS under coverage
359
- // exits nonzero and never credits (the exit-0 gate above), so only a test that PASSES *only* under
360
- // coverage instrumentation (e.g. asserting process.env.NODE_V8_COVERAGE is set) could credit a green the
361
- // plain gate would fail — an AD-047-class residual, unclosable without the double spawn (a) removes.
362
- export const foldSuiteCredit = ({ cwd = process.cwd(), env = process.env, fingerprint } = {}) => {
363
- const plans = plansInFlight(gitRoot(cwd) ?? cwd);
364
- if (plans.length !== 1) return { credited: false, reason: 'no single in-flight plan (cannot resolve the loop)' };
365
- const loop = plans[0].replace(/\.md$/, '');
366
- const base = resolveBase(cwd);
367
- const resultsPath = resolveResultsPath(cwd, env);
368
- // Fail CLOSED on an unreadable / malformed fold ledger — the SAME posture decideCheck takes (a dropped
369
- // line could hide a defect); a partially-trusted ledger never credits.
370
- const read = resultsPath ? readResults(resultsPath) : { records: [], malformed: 0 };
371
- if (read.readError) return { credited: false, reason: `the fold ledger is unreadable (${read.readError}) — fail closed, re-spawn` };
372
- if (read.malformed > 0) return { credited: false, reason: `the fold ledger has ${read.malformed} malformed line(s) — a partially-read ledger never credits (fail closed), re-spawn` };
373
- const sel = latestRunRecord(filterSegmentResults(read.records, loop, base));
374
- if (sel == null) return { credited: false, reason: 'no fold run recorded for the current segment' };
375
- const { suite } = sel.record;
376
- if (!suite) return { credited: false, reason: 'the fold run carries no suite evidence (a pre-v4 record)' };
377
- if (fingerprint == null || suite.fingerprintBefore !== fingerprint || suite.fingerprintAfter !== fingerprint) {
378
- return { credited: false, reason: 'the fold suite evidence is not bound to the current tree (the tree moved) — re-spawn' };
379
- }
380
- if (suite.exit !== 0) return { credited: false, reason: `the fold suite exited ${suite.exit} (a nonzero/red suite never credits a green gate-run) — re-spawn` };
381
- return { credited: true, evidence: suite };
382
- };
383
-
384
- // ── the check + report core ─────────────────────────────────────────────────────────────────────
385
-
386
- // buildFoldState({ cwd, env, detect }) → everything both renders need. Pure I/O at the edges; every
387
- // project-relative read anchors at the git work-tree ROOT when one exists (the fingerprint is
388
- // root-anchored — the same discipline review-state / review-ledger use).
389
- export const buildFoldState = ({ cwd, env = process.env, detect = detectBackends } = {}) => {
390
- const root = gitRoot(cwd) ?? cwd;
391
- const { config, source: configSource } = loadConfig(root);
392
- let detection = [];
393
- let detectionWarning = null;
394
- try {
395
- detection = detect();
396
- } catch (err) {
397
- detectionWarning = `backend detection failed (${(err && err.message) || err}) — treating all backends as not ready; the review recipe floors at solo.`;
398
- }
399
- const resolved = resolveActivityRecipe({ config: config ?? {}, readiness: detection, activity: ACTIVITY, slot: SLOT });
400
- const { dispatch } = planRecipe(resolved.recipe, detection);
401
- const requiredBackends = dispatch.map((d) => DISPLAY_ALIASES[d.backend] ?? d.backend);
402
- const plans = plansInFlight(root);
403
- const fingerprint = computeTreeFingerprint(cwd);
404
- const clean = fingerprint == null ? null : isTreeClean(cwd);
405
- const base = resolveBase(cwd);
406
- const resultsPath = resolveResultsPath(cwd, env);
407
- const resultRead = resultsPath ? readResults(resultsPath) : { records: [], malformed: 0, malformedReasons: [] };
408
- const reviewPath = resolveLedgerPath(cwd, env);
409
- const reviewRead = reviewPath ? readLedger(reviewPath) : { records: [], malformed: 0, malformedReasons: [] };
410
- return {
411
- resolved,
412
- configSource,
413
- requiredBackends,
414
- plans,
415
- fingerprint,
416
- clean,
417
- base,
418
- resultsPath,
419
- resultRecords: resultRead.records,
420
- resultMalformed: resultRead.malformed,
421
- resultReadError: resultRead.readError,
422
- reviewPath,
423
- reviewRecords: reviewRead.records,
424
- reviewMalformed: reviewRead.malformed,
425
- reviewReadError: reviewRead.readError,
426
- detectionWarning,
427
- };
428
- };
429
-
430
- const sameSet = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
431
- // The reserved (and, in v1, only legal) mutation key set — sorted for the sameSet comparison.
432
- const RESERVED_MUTATION_KEYS = ['killSetBasis', 'killed', 'skipped', 'survived', 'total'];
433
- const renderUncovered = (u) => (u.line === null ? `${u.file} (absent from coverage)` : `${u.file}:${u.line}`);
434
-
435
- // The normative --check decision (the header contract, in order) → { code, reason }. The checker is
436
- // PURE over (record, current fingerprint, review ledger) — it recomputes NO coverage/mutation.
437
- export const decideCheck = (state) => {
438
- // A detector failure is UNKNOWN state, not "no reviewer ready" — fail closed (like review-ledger).
439
- const explicitSolo = state.resolved.recipe === 'solo' && state.resolved.source === 'config' && !state.resolved.degradedFrom;
440
- if (state.detectionWarning && !explicitSolo) return { code: 1, reason: `cannot verify fold-completeness — ${state.detectionWarning}` };
441
- if (state.resolved.recipe === 'solo') {
442
- const why = state.resolved.degradedFrom ? `resolved ${ACTIVITY}.${SLOT} recipe degrades to solo here (${state.resolved.reason})` : `resolved ${ACTIVITY}.${SLOT} recipe is solo`;
443
- return { code: 0, reason: `${why} — no fold-completeness run required` };
444
- }
445
- if (state.plans.length === 0) return { code: 0, reason: 'no plan in flight (docs/plans/ holds no active plan) — no fold-completeness run required' };
446
- if (state.plans.length > 1) return { code: 1, reason: `more than one plan in flight (${state.plans.join(', ')}) — ambiguous loop id; resolve to one active plan` };
447
- if (state.fingerprint == null) return { code: 0, reason: 'not a git work tree — nothing to assess' };
448
- if (state.clean === true) return { code: 0, reason: 'the working tree is clean — nothing to assess' };
449
- // Fail CLOSED on any ledger the reader could not fully trust (a dropped line could hide a defect).
450
- if (state.resultReadError) return { code: 1, reason: `cannot read the result ledger (${state.resultReadError}) — failing closed; inspect ${state.resultsPath}` };
451
- if (state.resultMalformed > 0) return { code: 1, reason: `the result ledger has ${state.resultMalformed} malformed line(s) — failing closed; inspect ${state.resultsPath}` };
452
- if (state.reviewReadError) return { code: 1, reason: `cannot read the review ledger (${state.reviewReadError}) — failing closed; inspect ${state.reviewPath}` };
453
- if (state.reviewMalformed > 0) return { code: 1, reason: `the review ledger has ${state.reviewMalformed} malformed line(s) — failing closed; inspect ${state.reviewPath}` };
454
-
455
- const loop = state.plans[0].replace(/\.md$/, '');
456
- // SEGMENT scope (D7): the gate judges the current segment — (loop, base = the HEAD the dirty
457
- // tree sits on). A committed phase's runs, receipts, custody chains, and tamper obligations are
458
- // closed history; only v3 records at the current base enter.
459
- const loopResults = filterSegmentResults(state.resultRecords, loop, state.base);
460
- const sel = latestRunRecord(loopResults);
461
- if (sel == null) {
462
- const allLoop = filterLoopResults(state.resultRecords, loop);
463
- const legacy = allLoop.length > 0 && allLoop.every((r) => !(r.schema >= 3))
464
- ? ' (the loop has only pre-v3 records, which never enter a segment — the schema upgrade requires a fresh run)'
465
- : '';
466
- return { code: 1, reason: `dirty plan-execution loop "${loop}" but no fold-completeness run recorded for the current segment — run fold-completeness-run.mjs${legacy}` };
467
- }
468
- const latest = sel.record;
469
- // No per-record schema re-checks here (agy Phase-2 nit): only a VALID v3 record can be a segment
470
- // member — the reader binned anything older or tamper-less before this point (a v1 never carries
471
- // base; a v3 run without a tamper surface is malformed), and a malformed line already failed the
472
- // gate closed above.
473
-
474
- // Decision 9 — the double binding. A tree edit moves the fingerprint; a new fixable-bug triage moves
475
- // the bound-testId set. Either mismatch is STALE (the run no longer describes the committable tree).
476
- if (latest.fingerprint !== state.fingerprint) return { code: 1, reason: `no fold-completeness run for the current tree (the tree was edited after the run) — re-run fold-completeness-run.mjs after the last edit` };
477
- const currentBound = collectBoundTestIds(state.reviewRecords, { activity: ACTIVITY, loop, base: state.base });
478
- if (!sameSet(latest.boundTestIds, currentBound)) return { code: 1, reason: `a fixable-bug testId was triaged after the run (the bound-testId set changed) — re-run fold-completeness-run.mjs` };
479
- // Fail CLOSED if the run's probe set does not cover its bound-testId set EXACTLY. A well-formed
480
- // runner record always probes every bound testId (one testIds[] entry per boundTestId), but a record
481
- // with missing/extra/duplicate probes is untrustworthy — proving "0 unresolvable, 0 red" over an
482
- // EMPTY (or partial) probe set proves nothing about the bound tests (codex R1).
483
- const probedIds = latest.testIds.map((t) => t.id).sort();
484
- if (!sameSet(probedIds, latest.boundTestIds)) return { code: 1, reason: `the run's probe set does not match its bound-testId set (${probedIds.length} probe(s) for ${latest.boundTestIds.length} bound testId(s)) — failing closed; re-run fold-completeness-run.mjs` };
485
-
486
- // A changed unsupported-source file → fail closed: the signal never vouches for JS-family source it
487
- // cannot assess (TS/JSX out of scope v1, Decision 5).
488
- if (latest.unsupported.length > 0) return { code: 1, reason: `changed unsupported-source file(s) the signal cannot assess (TS/JSX out of scope v1): ${latest.unsupported.join(', ')}` };
489
-
490
- // The recorded overrides of this loop (review ledger, schema v3): oracle-change lifts named
491
- // tampered files; red-proof waives the receipt + custody proof for exactly the testId it names.
492
- const overrides = collectOverrides(state.reviewRecords, { activity: ACTIVITY, loop });
493
-
494
- // Approach-3 — the oracle-tamper guard: any tampered test-surface file (a removed/modified line in
495
- // a pre-existing test file, or a deleted one) must be covered by a recorded oracle-change
496
- // override. Never silent: the override is a durable, auditable ledger entry.
497
- const tamperedUncovered = latest.tamper.tampered.filter((f) => !overrides.oracleChangeFiles.has(f));
498
- if (tamperedUncovered.length > 0) {
499
- return {
500
- code: 1,
501
- reason: `tampered test-surface file(s) — a fix diff modified/deleted pre-existing test expectations — without a recorded oracle-change override: ${tamperedUncovered.join(', ')}. If the oracle change is deliberate, record it: node review-ledger-write.mjs override --json '{"loop":"${loop}","round":<n>,"scope":"oracle-change","files":${JSON.stringify(tamperedUncovered)},"reason":"<why the expectation legitimately changed>"}'`,
502
- };
503
- }
504
-
505
- // Per-testId enforcement, in bound (sorted) order: the D4 verdict algebra first — QUARANTINE and
506
- // red/unresolvable are probe-outcome failures regardless of receipts — then, for a green test, the
507
- // red-proof chain: receipt exists (Approach-1.i) → the receipt PRECEDES the loop's latest run
508
- // (anti-post-hoc, codex R2) → content custody (D5). The checker stays PURE: the current file hash
509
- // IS the latest run's recorded hash (the run is fingerprint-bound to the current tree).
510
- const probes = new Map(latest.testIds.map((t) => [t.id, t]));
511
- const boundSet = new Set(latest.boundTestIds);
512
- const receipts = loopResults.map((r, i) => ({ r, i })).filter(({ r }) => isRedProbeRecord(r));
513
- // Custody ANCHORS (c): a red-probe OR a recorded re-attestation on the file re-anchors custody at
514
- // its hash — the honest replacement for a red-proof waiver on a green-only append. The OBSERVED-RED
515
- // requirement below stays red-probe-only (a re-attest is not a red observation).
516
- const custodyAnchors = loopResults.map((r, i) => ({ r, i })).filter(({ r }) => isRedProbeRecord(r) || isReattestRecord(r));
517
- for (const id of latest.boundTestIds) {
518
- const t = probes.get(id);
519
- const verdict = probeVerdict(t);
520
- if (verdict === 'unresolvable') return { code: 1, reason: `unresolvable bound testId(s) — the pattern selects no test: ${id}` };
521
- if (verdict === 'quarantine') return { code: 1, reason: `bound test in QUARANTINE — not an N/N verdict (${t.greens} green / ${t.reds} red / ${t.timeouts} timed out of ${t.runs} runs): ${id}. A flaky/timed-out probe proves nothing and has no override lane — replace or speed up the test, then re-run` };
522
- if (verdict === 'red') return { code: 1, reason: `bound test(s) with a red baseline (the fold is not complete): ${id}` };
523
- // D7 — a recorded red-proof override replaces the receipt + custody proof for EXACTLY this
524
- // testId (green N/N was already required above; QUARANTINE was already refused — no override
525
- // lane converts a flaky probe).
526
- if (overrides.redProofTestIds.has(id)) continue;
527
- // verdict green — the red-proof chain.
528
- const own = receipts.filter(({ r }) => r.testId === id);
529
- if (own.length === 0) return { code: 1, reason: `no observed-red receipt for ${id} — a test never seen failing proves nothing about the fix. BEFORE folding a fix, run: node fold-completeness-run.mjs --red "${id}"` };
530
- if (!own.some(({ i }) => i < sel.index)) return { code: 1, reason: `the observed-red receipt for ${id} was minted AFTER the loop's latest run — a post-hoc red proves nothing; run fold-completeness-run.mjs once more (a fresh run after the receipt)` };
531
- const { file } = splitTestId(id);
532
- // Custody eligibility (D5, codex R3): the anchor is the LATEST red-probe OR re-attest on that FILE
533
- // whose own testId is in the bound set AND which precedes the latest run — an unbound throwaway
534
- // receipt, or one minted post-run, never re-attests a file. A green-only append after a red-probe
535
- // re-anchors here via a recorded --reattest (item (c)), not a red-proof waiver.
536
- const eligible = custodyAnchors.filter(({ r, i }) => i < sel.index && boundSet.has(r.testId) && splitTestId(r.testId).file === file);
537
- const anchor = eligible[eligible.length - 1];
538
- if (!anchor || t.fileHash == null || anchor.r.fileHash !== t.fileHash) {
539
- return { code: 1, reason: `custody broken for ${id}: the test file ${file} no longer matches its last observed-red/re-attested content — after a green-only append, re-attest the file (node fold-completeness-run.mjs --reattest "<a bound testId in ${file}>"); after a real edit, re-observe red (--red "<the file's newest testId>"), or record a red-proof override if the red is genuinely unestablishable` };
540
- }
541
- }
542
- if (latest.coverage.uncoveredChanged.length > 0) return { code: 1, reason: `uncovered changed line(s) — changed code no test executed: ${latest.coverage.uncoveredChanged.map(renderUncovered).join(', ')}` };
543
- // v1 ships NO mutation (the mutation half was shelved): the shipped runner only ever writes the
544
- // reserved empty shape, so a record carrying ANY mutation data was not produced by this runner
545
- // version (forged, or a version-skewed ledger — e.g. an older placed checker reading a newer
546
- // runner's ledger) — fail closed rather than vouch for a signal v1 cannot have computed. The rule
547
- // is the EXACT reserved shape (key set + empty values): an extra key would smuggle mutation data
548
- // past a known-fields-only check.
549
- const m = latest.mutation;
550
- const emptyReservedShape =
551
- sameSet(Object.keys(m).sort(), RESERVED_MUTATION_KEYS) &&
552
- m.total === 0 && m.killed === 0 && m.skipped === 0 && m.survived.length === 0 && m.killSetBasis === null;
553
- if (!emptyReservedShape) {
554
- return { code: 1, reason: `the run record's mutation field is not the reserved empty shape (${m.total} total / ${m.killed} killed / ${m.survived?.length ?? '?'} survived / ${m.skipped} skipped) but v1 ships no mutation — not a record this runner version produced; re-run fold-completeness-run.mjs` };
555
- }
556
-
557
- const notes = [];
558
- if (latest.outOfDomain.length > 0) notes.push(`out-of-domain changes not assessed (non-blocking): ${latest.outOfDomain.join(', ')}`);
559
- return { code: 0, reason: `fold-completeness verified for loop "${loop}" (fingerprint + bound-testId set current, coverage + baseline green)${notes.length ? ` — ${notes.join('; ')}` : ''}` };
560
- };
561
-
562
- // ── rendering ─────────────────────────────────────────────────────────────────────────────────
563
-
564
- const testLine = (r, t) => {
565
- if (r.schema === 1) return `${t.id}${t.resolvable ? (t.baselineGreen ? '✓' : ' red-baseline') : ' unresolvable'}`;
566
- const v = probeVerdict(t);
567
- return v === 'green' ? `${t.id} ✓ ${t.greens}/${t.runs}` : `${t.id} ${v} (${t.greens}g/${t.reds}r/${t.timeouts}t of ${t.runs})`;
568
- };
569
-
570
- const runLine = (r, receipts) => {
571
- const tests = r.testIds.length ? r.testIds.map((t) => testLine(r, t)).join(', ') : '(no bound tests)';
572
- const uncov = r.coverage.uncoveredChanged.length ? r.coverage.uncoveredChanged.map(renderUncovered).join(', ') : 'none';
573
- return [
574
- ` latest run — fingerprint ${r.fingerprint}`,
575
- ` bound testIds: ${tests}`,
576
- ` red-probe receipts: ${receipts.length ? receipts.map((p) => `${p.testId} (${p.reds}/${p.runs} red @${p.fileHash.slice(0, 8)})`).join(', ') : 'none'}`,
577
- ` uncovered changed: ${uncov}`,
578
- ` unsupported: ${r.unsupported.length ? r.unsupported.join(', ') : 'none'} · out-of-domain: ${r.outOfDomain.length ? r.outOfDomain.join(', ') : 'none'}`,
579
- ` mutation: ${r.mutation.total} total / ${r.mutation.killed} killed / ${r.mutation.survived.length} survived / ${r.mutation.skipped} skipped`,
580
- ].join('\n');
581
- };
582
-
583
- const formatHuman = (state, check) => {
584
- const src = state.configSource === 'config' ? `from ${CONFIG_REL}` : 'computed default';
585
- const lines = [
586
- `fold-completeness — ${ACTIVITY}.${SLOT} = ${state.resolved.recipe} (${src})${state.requiredBackends.length ? ` → ${state.requiredBackends.join(' + ')}` : ''}`,
587
- ];
588
- if (state.detectionWarning) lines.push(` ⚠ ${state.detectionWarning}`);
589
- lines.push(` plan in flight: ${state.plans.length ? state.plans.join(', ') : '(none)'}`);
590
- if (state.fingerprint == null) lines.push(' tree: not a git work tree');
591
- else if (state.clean === true) lines.push(' tree: clean (nothing to assess)');
592
- else lines.push(` tree fingerprint: ${state.fingerprint}`);
593
- lines.push(` result ledger: ${state.resultsPath ?? '(unresolvable — no git dir)'} (${state.resultRecords.length} record(s)${state.resultMalformed ? `, ${state.resultMalformed} malformed — inspect the file` : ''})`);
594
- if (state.plans.length === 1) {
595
- const loop = state.plans[0].replace(/\.md$/, '');
596
- const segResults = filterSegmentResults(state.resultRecords, loop, state.base); // the current segment (D7)
597
- const sel = latestRunRecord(segResults); // kind-aware: never render a red-probe as "the run"
598
- if (sel) lines.push(runLine(sel.record, segResults.filter(isRedProbeRecord)));
599
- }
600
- lines.push(` check: ${check.code === 0 ? 'PASS' : 'FAIL'} — ${check.reason}`);
601
- return lines.join('\n');
602
- };
603
-
604
- const HELP = `fold-completeness — read-only FOLD-COMPLETENESS checker (agent-workflow family, AD-046).
605
-
606
- Usage:
607
- node fold-completeness.mjs [--check | --status | --json]
608
-
609
- Reads the result ledger the runner writes (<git dir>/${RESULTS_BASENAME}; AW_FOLD_RESULTS overrides),
610
- resolves the effective ${ACTIVITY}.${SLOT} recipe, recomputes the canonical uncommitted-state
611
- fingerprint, and decides whether the in-flight plan-execution SEGMENT's changed code is pinned by
612
- tests — (loop, base = git rev-parse HEAD), schema v3+ (v4 adds the suite-execution evidence + the
613
- reattest custody anchor, BUGFREE-3/AD-049; AD-048 D7): bound testIds, receipts, custody, and tamper
614
- all filter to the current segment; a committed phase's obligations close with its commit.
615
-
616
- --status (default) → the human report: resolved recipe, plan-in-flight, the latest run summary
617
- (per-testId D4 verdicts + rerun counts), the loop's red-probe receipts, verdict.
618
- --check → the gate exit code. The normative exit contract lives in the tool header (the single home):
619
- exit 0 for solo / no plan in flight / a clean tree / not-a-git-tree / a CURRENT segment run whose
620
- fingerprint AND segment bound-testId set both match, with — per bound testId — an N/N-green probe,
621
- an observed-red receipt in this segment that PRECEDES the run, and content custody (run hash == the
622
- latest custody-eligible red-probe OR reattest hash on that file — a green-only append is re-anchored
623
- by a recorded --reattest, not a red-proof waiver); plus 0 uncovered changed lines, 0 changed
624
- unsupported source, every tampered
625
- test-surface file covered by a recorded oracle-change override, and the reserved empty mutation
626
- shape. A recorded red-proof override (review-ledger-write override) waives receipt + custody for
627
- exactly its testId — never green-N/N, never QUARANTINE. exit 1 otherwise (stale/missing/older-runner
628
- run, an unresolvable bound test, a QUARANTINED bound test — mixed/timeout, never converted, no
629
- override lane — a red baseline, a missing/post-hoc receipt, broken custody, an uncovered tampered
630
- file, an uncovered line, changed TS/JSX, any mutation data, >1 plan, an unreadable/malformed
631
- ledger, or a detector failure). Out-of-domain changes are listed, never blocking. The honest
632
- fold-time order: classify the fixable-bug with its testId → write the test →
633
- fold-completeness-run.mjs --red "<testId>" observes red BEFORE the fix → fold the fix → the
634
- normal run observes green → --check.
635
- --json → the structured state + decision.
636
-
637
- The runner is a SEPARATE tool (fold-completeness-run.mjs) — this read-only checker never imports it.
638
- Human residual: git commit --no-verify and ledger editing remain possible — a self-discipline mechanism.
639
-
640
- Exit codes: 0 pass (or plain report); 1 check failed or config error (loud); 2 usage.`;
641
-
642
- const KNOWN_ARGS = new Set(['--help', '-h', '--check', '--status', '--json']);
643
-
644
- export const main = (argv, ctx = {}) => {
645
- const cwd = ctx.cwd ?? process.cwd();
646
- const env = ctx.env ?? process.env;
647
- const detect = ctx.detect ?? detectBackends;
648
- try {
649
- if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
650
- const unknown = argv.find((a) => !KNOWN_ARGS.has(a));
651
- if (unknown !== undefined) throw fail(2, `unknown argument: ${unknown}`);
652
- const state = buildFoldState({ cwd, env, detect });
653
- const check = decideCheck(state);
654
- if (argv.includes('--json')) {
655
- return { code: argv.includes('--check') ? check.code : 0, stdout: JSON.stringify({ ...state, check }, null, 2), stderr: '' };
656
- }
657
- if (argv.includes('--check')) {
658
- return { code: check.code, stdout: `fold-completeness check: ${check.code === 0 ? 'PASS' : 'FAIL'} — ${check.reason}`, stderr: '' };
659
- }
660
- return { code: 0, stdout: formatHuman(state, check), stderr: '' };
661
- } catch (err) {
662
- return { code: err.exitCode ?? 1, stdout: '', stderr: `fold-completeness: ${err.message}` };
663
- }
664
- };
665
-
666
- const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
667
- if (isDirectRun) {
668
- const r = main(process.argv.slice(2));
669
- if (r.stdout) process.stdout.write(r.stdout.endsWith('\n') ? r.stdout : `${r.stdout}\n`);
670
- if (r.stderr) process.stderr.write(r.stderr.endsWith('\n') ? r.stderr : `${r.stderr}\n`);
671
- process.exitCode = r.code;
672
- }