harness-everything 0.38.0 → 0.40.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 (79) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +79 -63
  4. package/CHANGELOG.md +32 -0
  5. package/benchmarks/results/reviews/pr353-r6-2026-10-22/README.md +76 -0
  6. package/benchmarks/results/reviews/pr353-r6-2026-10-22/a-reviews-b.md +191 -0
  7. package/benchmarks/results/reviews/pr353-r6-2026-10-22/b-reviews-a.md +100 -0
  8. package/benchmarks/results/reviews/pr353-r6-2026-10-22/baseline-probes.log +47 -0
  9. package/benchmarks/results/reviews/pr353-r6-2026-10-22/bom-red.log +85 -0
  10. package/benchmarks/results/reviews/pr353-r6-2026-10-22/bom-transition.txt +25 -0
  11. package/benchmarks/results/reviews/pr353-r6-2026-10-22/coordinator-probes.txt +84 -0
  12. package/benchmarks/results/reviews/pr353-r6-2026-10-22/cross-suite.txt +18 -0
  13. package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-bom.log +3 -0
  14. package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-consistency.log.gz +0 -0
  15. package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-core.log +198 -0
  16. package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-probes.log +27 -0
  17. package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-producer.log +83 -0
  18. package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-review.md +89 -0
  19. package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-transitions.log +5 -0
  20. package/benchmarks/results/reviews/pr353-r6-2026-10-22/implementation-a.md +166 -0
  21. package/benchmarks/results/reviews/pr353-r6-2026-10-22/implementation-b.md +264 -0
  22. package/benchmarks/results/reviews/pr353-r6-2026-10-22/logs.sha256 +15 -0
  23. package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-a-probe.log +62 -0
  24. package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-a-probe.txt +313 -0
  25. package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-b-probe.log +51 -0
  26. package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-b-probe.txt +94 -0
  27. package/benchmarks/results/reviews/pr353-r6-2026-10-22/semantic-transitions.txt +25 -0
  28. package/benchmarks/results/reviews/pr353-r6-2026-10-22/source.sha256 +9 -0
  29. package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-a-on-b-core.log +201 -0
  30. package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-a-on-b-producer.log +82 -0
  31. package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-b-on-a-core.log +197 -0
  32. package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-b-on-a-producer.log +82 -0
  33. package/benchmarks/results/reviews/pr353-r6-2026-10-22/verify-all.log.gz +0 -0
  34. package/benchmarks/results/reviews/pr353-r6-2026-10-22/verify-gate-summary.json +22 -0
  35. package/ci/fixtures/doc-obligations/reconciliation-s2a/harness.js +489 -0
  36. package/ci/fixtures/doc-obligations/reconciliation-s2a/vectors.json +617 -0
  37. package/ci/mechanism-325-3b-s2a-core.test.js +840 -0
  38. package/ci/mechanism-325-3b-s2a-producer.test.js +564 -0
  39. package/code-review/SKILL.md +3 -3
  40. package/code-review/references/review-playbook.md +4 -0
  41. package/code-review/templates/finding.template.md +6 -0
  42. package/docs/doc-workflow-reconciliation.md +357 -0
  43. package/evals/zoom-out/tasks/negative-first-edge-case-review.yaml +16 -0
  44. package/evals/zoom-out/tasks/positive-repair-loop.yaml +15 -0
  45. package/harness-everything/SKILL.md +1 -1
  46. package/harness-everything/scripts/project-docs-resolver.js +42 -8
  47. package/harness-everything/scripts/routing-keywords.json +4 -0
  48. package/hooks/scripts/lib/doc-reconciliation-core.js +280 -0
  49. package/hooks/scripts/lib/doc-reconciliation-events.js +141 -0
  50. package/hooks/scripts/lib/doc-reconciliation-producer.js +206 -0
  51. package/multi-agent-workspace/SKILL.md +1 -1
  52. package/multi-agent-workspace/scripts/project-docs-resolver.js +42 -8
  53. package/opencode-plugin/plugin.json +1 -1
  54. package/package.json +2 -1
  55. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  56. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-core.js +280 -0
  57. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-events.js +141 -0
  58. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-producer.js +206 -0
  59. package/plugins/harness-everything/plugin.json +1 -1
  60. package/plugins/harness-everything/skills/code-review/SKILL.md +3 -3
  61. package/plugins/harness-everything/skills/code-review/references/review-playbook.md +4 -0
  62. package/plugins/harness-everything/skills/code-review/templates/finding.template.md +6 -0
  63. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  64. package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +42 -8
  65. package/plugins/harness-everything/skills/harness-everything/scripts/routing-keywords.json +4 -0
  66. package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +1 -1
  67. package/plugins/harness-everything/skills/multi-agent-workspace/scripts/project-docs-resolver.js +42 -8
  68. package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
  69. package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +42 -8
  70. package/plugins/harness-everything/skills/zoom-out/SKILL.md +4 -3
  71. package/plugins/harness-everything/skills/zoom-out/references/circuit-breaker.md +7 -0
  72. package/plugins/harness-everything/skills/zoom-out/templates/zoom-out-report.template.md +4 -2
  73. package/sbom.cdx.json +7 -7
  74. package/scripts/errors-sync.js +90 -0
  75. package/to-spec/SKILL.md +1 -1
  76. package/to-spec/scripts/project-docs-resolver.js +42 -8
  77. package/zoom-out/SKILL.md +4 -3
  78. package/zoom-out/references/circuit-breaker.md +7 -0
  79. package/zoom-out/templates/zoom-out-report.template.md +4 -2
@@ -0,0 +1,564 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // #325 Phase 3b S2a RED suite: producer snapshot identity, captured config, baseline pinning, ignored scope and framing.
5
+ //
6
+ // Every assertion runs on both legs (canonical hooks/scripts/lib and the packaged copy) against real temporary Git
7
+ // repositories built by the shared harness. Missing modules or exports are FAIL with a module-missing or export-missing
8
+ // reason; the harness never crashes. Expected obligations come from the unchanged derive API. Everything else is
9
+ // hand-derived from docs/doc-workflow-reconciliation.md (S2a). INFRA (fixture or harness faults) is distinct from FAIL.
10
+ // Exit 1 means at least one assertion FAILED; exit 2 means an infrastructure fault.
11
+
12
+ const fs = require('fs');
13
+ const os = require('os');
14
+ const path = require('path');
15
+ const H = require('./fixtures/doc-obligations/reconciliation-s2a/harness.js');
16
+
17
+ const PRODUCER = 'doc-reconciliation-producer.js';
18
+ const EVENTS = 'doc-reconciliation-events.js';
19
+ const DERIVE = 'doc-obligations.js';
20
+ const RESOLVER = 'project-docs-resolver.js';
21
+ const REQ_PRODUCER = { where: 'lib', file: PRODUCER, exports: ['produceDocSnapshot'] };
22
+ const REQ_EVENTS = { where: 'lib', file: EVENTS, exports: ['acceptDocEvents'] };
23
+ const REQ_DERIVE = { where: 'scripts', file: DERIVE, exports: ['deriveDocFacts', 'deriveDocObligations'] };
24
+ const REQ_RESOLVER = { where: 'scripts', file: RESOLVER, exports: ['resolveDocsConfig', 'resolveDocsConfigCaptured'] };
25
+ const { VECTORS } = H;
26
+ const HEX64 = /^[0-9a-f]{64}$/;
27
+
28
+ const runner = H.createRunner('S2a producer');
29
+
30
+ function produce(mods, repo, overrides = {}, origin = null) {
31
+ const o = origin || new H.FakeOrigin({ context: H.fakeContext(overrides.context, repo) });
32
+ return { result: mods[PRODUCER].produceDocSnapshot(H.producerInput(repo, overrides), { originProvider: o }), origin: o };
33
+ }
34
+
35
+ function readyOrThrow(produced, label) {
36
+ if (produced.state !== 'ready' || !produced.snapshot) {
37
+ throw new Error(`${label}: state ${produced.state} with codes ${JSON.stringify(H.codesOf(produced))}`);
38
+ }
39
+ return produced.snapshot;
40
+ }
41
+
42
+ function recordAt(snapshot, side, rel) {
43
+ const records = snapshot[side] && snapshot[side].records;
44
+ if (Array.isArray(records)) return records.find(record => record && record.path === rel) || null;
45
+ if (records && typeof records === 'object') return records[rel] || null;
46
+ return null;
47
+ }
48
+
49
+ function withRepo(files, fn) {
50
+ const repo = H.makeRepo(files.base);
51
+ try {
52
+ H.writeFiles(repo.root, files.candidate);
53
+ return fn(repo);
54
+ } finally {
55
+ H.removeRepo(repo.root);
56
+ }
57
+ }
58
+
59
+ function normativeFiles() {
60
+ return H.vectorFiles('normative');
61
+ }
62
+
63
+ // ---- Snapshot contract: required fields, identity types and equalities, derivation reasons -----------------------
64
+
65
+ runner.run('snapshot:required-fields-present', [REQ_PRODUCER], mods => withRepo(normativeFiles(), repo => {
66
+ const snapshot = readyOrThrow(produce(mods, repo).result, 'normative capture');
67
+ for (const field of ['identity', 'baseline', 'candidate', 'config', 'originContext', 'taskSemantics', 'facts', 'obligations', 'derivationReasons']) {
68
+ if (snapshot[field] === undefined) throw new Error(`snapshot lacks required field ${field}`);
69
+ }
70
+ for (const field of ['commit', 'treeRef', 'records', 'digest']) {
71
+ if (snapshot.baseline[field] === undefined) throw new Error(`baseline lacks ${field}`);
72
+ }
73
+ for (const field of ['records', 'coverage', 'digest']) {
74
+ if (snapshot.candidate[field] === undefined) throw new Error(`candidate lacks ${field}`);
75
+ }
76
+ for (const field of ['resolution', 'capture', 'digest']) {
77
+ if (snapshot.config[field] === undefined) throw new Error(`config lacks ${field}`);
78
+ }
79
+ }));
80
+
81
+ runner.run('snapshot:identity-tuple-types-and-digest-equalities', [REQ_PRODUCER], mods => withRepo(normativeFiles(), repo => {
82
+ const snapshot = readyOrThrow(produce(mods, repo).result, 'normative capture');
83
+ const id = snapshot.identity;
84
+ for (const field of ['workspaceId', 'sessionId', 'workflowId', 'taskId']) {
85
+ if (typeof id[field] !== 'string' || !id[field].trim()) throw new Error(`identity.${field} must be a nonempty string`);
86
+ }
87
+ if (!Number.isInteger(id.workflowRevision) || id.workflowRevision < 0) throw new Error('identity.workflowRevision must be a nonnegative integer');
88
+ for (const field of ['taskScopeDigest', 'baselineDigest', 'candidateDigest', 'configDigest']) {
89
+ if (!HEX64.test(id[field] || '')) throw new Error(`identity.${field} must be lowercase SHA256 hex`);
90
+ }
91
+ H.sameStable(id.prdRoot, 'docs/prd/', 'identity.prdRoot');
92
+ H.sameStable(snapshot.baseline.digest, id.baselineDigest, 'baseline.digest equals identity.baselineDigest');
93
+ H.sameStable(snapshot.candidate.digest, id.candidateDigest, 'candidate.digest equals identity.candidateDigest');
94
+ H.sameStable(snapshot.config.digest, id.configDigest, 'config.digest equals identity.configDigest');
95
+ }));
96
+
97
+ runner.run('snapshot:obligations-equal-unchanged-derive-api-on-captured-bytes', [REQ_PRODUCER, REQ_DERIVE], (mods) => withRepo(normativeFiles(), repo => {
98
+ const snapshot = readyOrThrow(produce(mods, repo).result, 'normative capture');
99
+ const files = normativeFiles();
100
+ const facts = mods[DERIVE].deriveDocFacts({ base: { files: files.base }, candidate: { files: files.candidate }, config: H.DERIVE_CONFIG });
101
+ const obligations = mods[DERIVE].deriveDocObligations(facts.facts);
102
+ H.sameStable(snapshot.obligations, obligations, 'snapshot.obligations');
103
+ }));
104
+
105
+ runner.run('snapshot:derivation-reasons-cover-every-obligation', [REQ_PRODUCER], mods => withRepo(normativeFiles(), repo => {
106
+ const snapshot = readyOrThrow(produce(mods, repo).result, 'normative capture');
107
+ for (const obligation of snapshot.obligations) {
108
+ const reason = snapshot.derivationReasons[obligation.id];
109
+ if (!reason) throw new Error(`no derivationReasons entry for ${obligation.id}`);
110
+ H.sameStable(reason.applicability, obligation.applicability, `${obligation.id} applicability`);
111
+ if (typeof reason.reason !== 'string' || !reason.reason.trim()) throw new Error(`${obligation.id} lacks a nonempty reason`);
112
+ if (!Array.isArray(reason.factRefs) || reason.factRefs.length === 0) throw new Error(`${obligation.id} lacks factRefs`);
113
+ }
114
+ }));
115
+
116
+ // ---- A04: docs/prd/ is the only supported root -------------------------------------------------------------------
117
+
118
+ runner.run('A04:docs-prd-root-positive-produces-ready-snapshot', [REQ_PRODUCER], mods => withRepo(normativeFiles(), repo => {
119
+ const snapshot = readyOrThrow(produce(mods, repo).result, 'docs/prd/ root');
120
+ H.sameStable(snapshot.identity.prdRoot, 'docs/prd/', 'identity.prdRoot');
121
+ }));
122
+
123
+ runner.run('A04:custom-prd-root-unavailable-with-no-fallback', [REQ_PRODUCER], mods => withRepo(normativeFiles(), repo => {
124
+ const { result } = produce(mods, repo, { top: { prdRoot: 'docs/other/' } });
125
+ if (result.snapshot) throw new Error('a custom PRD root must not yield a snapshot, even with otherwise identical inputs');
126
+ H.expectCodes(H.codesOf(result), ['unsupported-prd-root'], 'custom PRD root');
127
+ }));
128
+
129
+ // ---- A05: whole-candidate identity, deletion and rename endpoints, tombstones --------------------------------------
130
+
131
+ runner.run('A05:whole-candidate-identity-is-not-the-prd-set-digest', [REQ_PRODUCER], mods => withRepo(normativeFiles(), repo => {
132
+ const snapshot = readyOrThrow(produce(mods, repo).result, 'normative capture');
133
+ if (!HEX64.test(snapshot.identity.candidateDigest)) throw new Error('candidateDigest must be lowercase SHA256 hex');
134
+ if (snapshot.identity.candidateDigest === H.prdSetDigest(repo.root)) {
135
+ throw new Error('candidateDigest must cover the whole candidate, not only the PRD-set envelope');
136
+ }
137
+ }));
138
+
139
+ const renameFiles = () => ({
140
+ base: { 'docs/prd/a.md': 'rename me\n' },
141
+ candidate: { 'docs/prd/a.md': null, 'docs/prd/b.md': 'rename me\n' }
142
+ });
143
+
144
+ runner.run('A05:deletion-and-rename-endpoints-are-both-bound', [REQ_PRODUCER], mods => withRepo(renameFiles(), repo => {
145
+ const snapshot = readyOrThrow(produce(mods, repo).result, 'rename capture');
146
+ const source = recordAt(snapshot, 'candidate', 'docs/prd/a.md');
147
+ const destination = recordAt(snapshot, 'candidate', 'docs/prd/b.md');
148
+ if (!source || source.presence !== 'deleted') throw new Error('deleted source endpoint must be a deleted tombstone');
149
+ if (!destination || destination.presence !== 'present') throw new Error('rename destination must be a present record');
150
+ const baseSource = recordAt(snapshot, 'baseline', 'docs/prd/a.md');
151
+ if (!baseSource || baseSource.presence !== 'present') throw new Error('baseline must retain the source endpoint');
152
+ }));
153
+
154
+ runner.run('A05:deleted-tombstones-omitted-from-checker-artifacts', [REQ_PRODUCER, REQ_EVENTS], mods => withRepo(renameFiles(), repo => {
155
+ const snapshot = readyOrThrow(produce(mods, repo).result, 'rename capture');
156
+ const origin = new H.FakeOrigin({ context: H.fakeContext({}, repo) });
157
+ const accepted = mods[EVENTS].acceptDocEvents({ snapshot, eventRefs: [] }, { originProvider: origin });
158
+ const artifacts = accepted.checkerInputs && accepted.checkerInputs.artifacts;
159
+ if (!artifacts || typeof artifacts !== 'object') throw new Error('checkerInputs.artifacts is missing');
160
+ if (Object.prototype.hasOwnProperty.call(artifacts, 'docs/prd/a.md')) throw new Error('a deleted tombstone must not appear as a candidate artifact');
161
+ if (!Object.prototype.hasOwnProperty.call(artifacts, 'docs/prd/b.md')) throw new Error('the rename destination must appear as a candidate artifact');
162
+ const baselineArtifacts = accepted.checkerInputs.baseline && accepted.checkerInputs.baseline.artifacts;
163
+ if (!baselineArtifacts || !Object.prototype.hasOwnProperty.call(baselineArtifacts, 'docs/prd/a.md')) {
164
+ throw new Error('the baseline artifact for the deleted source must be retained');
165
+ }
166
+ }));
167
+
168
+ runner.run('A05:deleted-path-differs-from-empty-file-in-candidate-digest', [REQ_PRODUCER], mods => {
169
+ const deleted = withRepo(renameFiles(), repo => readyOrThrow(produce(mods, repo).result, 'deleted').identity.candidateDigest);
170
+ const emptyFile = withRepo({ base: { 'docs/prd/a.md': 'rename me\n' }, candidate: { 'docs/prd/a.md': '', 'docs/prd/b.md': 'rename me\n' } },
171
+ repo => readyOrThrow(produce(mods, repo).result, 'empty').identity.candidateDigest);
172
+ if (deleted === emptyFile) throw new Error('a deletion and an empty file must produce different candidate identities');
173
+ });
174
+
175
+ runner.run('framing:path-boundary-concatenation-does-not-collide', [REQ_PRODUCER], mods => {
176
+ const first = withRepo({ base: { 'docs/prd/x.md': 'seed\n', 'docs/prd/y.md': 'seed\n' }, candidate: { 'docs/prd/x.md': 'ab', 'docs/prd/y.md': 'c' } },
177
+ repo => readyOrThrow(produce(mods, repo).result, 'split one').identity.candidateDigest);
178
+ const second = withRepo({ base: { 'docs/prd/x.md': 'seed\n', 'docs/prd/y.md': 'seed\n' }, candidate: { 'docs/prd/x.md': 'a', 'docs/prd/y.md': 'bc' } },
179
+ repo => readyOrThrow(produce(mods, repo).result, 'split two').identity.candidateDigest);
180
+ if (first === second) throw new Error('path-boundary concatenation collision in candidateDigest');
181
+ });
182
+
183
+ runner.run('framing:mode-drift-changes-candidate-digest', [REQ_PRODUCER], mods => {
184
+ if (process.platform === 'win32') throw new H.UncheckedError('win32: no POSIX execute bit (core.fileMode=false), mode drift is not observable');
185
+ const plain = withRepo(normativeFiles(), repo => readyOrThrow(produce(mods, repo).result, 'plain').identity.candidateDigest);
186
+ const drifted = withRepo(normativeFiles(), repo => {
187
+ fs.chmodSync(path.join(repo.root, 'docs/prd/graph.md'), 0o755);
188
+ return readyOrThrow(produce(mods, repo).result, 'mode drift').identity.candidateDigest;
189
+ });
190
+ if (plain === drifted) throw new Error('a mode change (100644 to 100755) must change candidateDigest');
191
+ });
192
+
193
+ // ---- A09: pinned baseline after a fixture commit; rename endpoints stable across captures -------------------------
194
+
195
+ runner.run('A09:pinned-baseline-remains-after-fixture-commit', [REQ_PRODUCER, REQ_DERIVE], mods => {
196
+ const files = normativeFiles();
197
+ const repo = H.makeRepo(files.base);
198
+ try {
199
+ H.writeFiles(repo.root, files.candidate);
200
+ const first = readyOrThrow(produce(mods, repo).result, 'capture before commit');
201
+ H.git(repo.root, ['add', '-A']);
202
+ H.git(repo.root, ['-c', 'user.name=s2a-fixture', '-c', 'user.email=s2a-fixture@invalid', '-c', 'commit.gpgsign=false', 'commit', '-q', '--no-verify', '-m', 'documentation change']);
203
+ const afterCommit = readyOrThrow(produce(mods, repo, {}).result, 'pinned capture after commit');
204
+ H.sameStable(afterCommit.baseline.commit, repo.commit, 'pinned baseline commit');
205
+ H.sameStable(afterCommit.identity.baselineDigest, first.identity.baselineDigest, 'baselineDigest after commit');
206
+ H.sameStable(afterCommit.obligations.filter(item => item.strength === 'MUST' && item.applicability !== 'N/A').map(item => item.id).sort(),
207
+ [...VECTORS.vectors.normative.expectedMustIds].sort(), 'governed MUSTs after the commit');
208
+ } finally {
209
+ H.removeRepo(repo.root);
210
+ }
211
+ });
212
+
213
+ // A09 oracle fix (PR #353 review P1): the previous form accepted the HEAD-anchored capture and only asserted a
214
+ // different digest — but a different digest cannot establish that the captured baseline is the authorized
215
+ // workflow-start commit, so this oracle encoded the bug it was meant to guard. The authorized pin is the
216
+ // provider's start pin (the workflow-start baselineReference, unchanged since workflow start): a capture whose
217
+ // input baseline is re-anchored to the moved HEAD must be baseline-unavailable, never a snapshot.
218
+ runner.run('A09:head-anchored-capture-has-a-different-baseline-identity', [REQ_PRODUCER], mods => {
219
+ const files = normativeFiles();
220
+ const repo = H.makeRepo(files.base);
221
+ try {
222
+ H.writeFiles(repo.root, files.candidate);
223
+ H.git(repo.root, ['add', '-A']);
224
+ H.git(repo.root, ['-c', 'user.name=s2a-fixture', '-c', 'user.email=s2a-fixture@invalid', '-c', 'commit.gpgsign=false', 'commit', '-q', '--no-verify', '-m', 'documentation change']);
225
+ const head = H.git(repo.root, ['rev-parse', 'HEAD']).trim();
226
+ const moved = mods[PRODUCER].produceDocSnapshot({ ...H.producerInput(repo), baseline: { root: repo.root, commit: head } },
227
+ { originProvider: new H.FakeOrigin({ context: H.fakeContext({}, repo) }) });
228
+ if (moved.snapshot) throw new Error('a HEAD-anchored input baseline must not capture: the authorized pin is the provider start pin, not the moving HEAD');
229
+ H.expectCodes(H.codesOf(moved), ['baseline-unavailable'], 'HEAD-anchored input against the workflow-start pin');
230
+ } finally {
231
+ H.removeRepo(repo.root);
232
+ }
233
+ });
234
+
235
+ runner.run('A09:rename-endpoints-stable-across-repeated-captures', [REQ_PRODUCER], mods => withRepo(renameFiles(), repo => {
236
+ const first = readyOrThrow(produce(mods, repo).result, 'first rename capture');
237
+ const second = readyOrThrow(produce(mods, repo).result, 'second rename capture');
238
+ H.sameStable(second.identity, first.identity, 'identity across repeated rename captures');
239
+ }));
240
+
241
+ // ---- R2d P1: provider baseline pinning (PR #353 review) ------------------------------------------------------------
242
+
243
+ runner.run('R2d-P1:reference-only-provider-context-is-baseline-unavailable', [REQ_PRODUCER], mods => withRepo(normativeFiles(), repo => {
244
+ // doc-workflow-reconciliation.md: the provider context MUST carry the concrete pinned `baseline: { commit, root }`;
245
+ // a bare baselineReference is not enough, so a reference-only context cannot authorize any capture.
246
+ const origin = new H.FakeOrigin({ context: H.fakeContext() });
247
+ const { result } = produce(mods, repo, {}, origin);
248
+ if (result.snapshot) throw new Error('a provider context without a concrete pinned baseline must not yield a snapshot');
249
+ H.expectCodes(H.codesOf(result), ['baseline-unavailable'], 'reference-only provider context');
250
+ }));
251
+
252
+ runner.run('R2d-P1:input-baseline-re-anchored-to-moved-head-is-never-ready', [REQ_PRODUCER], mods => {
253
+ const files = normativeFiles();
254
+ const repo = H.makeRepo(files.base);
255
+ try {
256
+ H.writeFiles(repo.root, files.candidate);
257
+ H.git(repo.root, ['add', '-A']);
258
+ H.git(repo.root, ['-c', 'user.name=s2a-fixture', '-c', 'user.email=s2a-fixture@invalid', '-c', 'commit.gpgsign=false', 'commit', '-q', '--no-verify', '-m', 'documentation change']);
259
+ const head = H.git(repo.root, ['rev-parse', 'HEAD']).trim();
260
+ // The provider's start pin stays where the workflow started (the start baselineReference above); the input
261
+ // baseline is re-anchored to the moved HEAD. The authorized workflow-start pin never follows HEAD.
262
+ const origin = new H.FakeOrigin({ context: H.fakeContext({}, repo) }); // pinned at the original start commit
263
+ const { result } = produce(mods, repo, { top: { baseline: { root: repo.root, commit: head } } }, origin);
264
+ if (result.snapshot) throw new Error('an input baseline re-anchored to a moved HEAD must never yield a ready snapshot');
265
+ H.expectCodes(H.codesOf(result), ['baseline-unavailable'], 're-anchored input baseline');
266
+ } finally {
267
+ H.removeRepo(repo.root);
268
+ }
269
+ });
270
+
271
+ runner.run('R2d-P1:input-baseline-equal-to-the-provider-pin-stays-ready-after-head-moved', [REQ_PRODUCER], mods => {
272
+ // Control: the provider context concretely pins the workflow-start baseline and the input baseline equals that
273
+ // pin even though HEAD moved; this capture stays ready.
274
+ const files = normativeFiles();
275
+ const repo = H.makeRepo(files.base);
276
+ try {
277
+ H.writeFiles(repo.root, files.candidate);
278
+ H.git(repo.root, ['add', '-A']);
279
+ H.git(repo.root, ['-c', 'user.name=s2a-fixture', '-c', 'user.email=s2a-fixture@invalid', '-c', 'commit.gpgsign=false', 'commit', '-q', '--no-verify', '-m', 'documentation change']);
280
+ const origin = new H.FakeOrigin({ context: H.fakeContext({ baseline: { root: repo.root, commit: repo.commit } }) });
281
+ const { result } = produce(mods, repo, {}, origin);
282
+ readyOrThrow(result, 'equal-pin capture after HEAD moved');
283
+ } finally {
284
+ H.removeRepo(repo.root);
285
+ }
286
+ });
287
+
288
+ // ---- R2d P2a: nested semantic task/scope changes must change the task-scope digest (PR #353 review P2) --------------
289
+
290
+ runner.run('R2d-P2a:nested-scope-policy-status-is-a-semantic-change', [REQ_PRODUCER], mods => withRepo(normativeFiles(), repo => {
291
+ // Progress-key stripping is location-bound: it applies to the per-step/per-binding progress records, never to
292
+ // every nested object with the same key name. A nested scope policy flag is semantic and always re-hashes.
293
+ const strict = readyOrThrow(produce(mods, repo, { top: { scope: { prdRoot: 'docs/prd/', policy: { status: 'strict' } } } }).result, 'strict scope capture');
294
+ const relaxed = readyOrThrow(produce(mods, repo, { top: { scope: { prdRoot: 'docs/prd/', policy: { status: 'relaxed' } } } }).result, 'relaxed scope capture');
295
+ if (strict.identity.taskScopeDigest === relaxed.identity.taskScopeDigest) {
296
+ throw new Error('a nested scope policy change (policy.status) must change taskScopeDigest');
297
+ }
298
+ }));
299
+
300
+ runner.run('R2d-P2a:nested-requirement-and-binding-semantics-change-the-digest', [REQ_PRODUCER], mods => withRepo(normativeFiles(), repo => {
301
+ // Nested objects inside requirement/binding definitions carry semantics; only the per-step/per-binding progress
302
+ // record locations above them (requirement.status/evidence, binding status records) stay progress-exempt
303
+ // (the A14 status-only control keeps passing).
304
+ const base = H.clone(VECTORS.fixedTask.requirements);
305
+ const gateA = H.clone(base); gateA[0].gate = { state: 'blocked-on-review' };
306
+ const gateB = H.clone(base); gateB[0].gate = { state: 'clear' };
307
+ const first = readyOrThrow(produce(mods, repo, { task: { requirements: gateA } }).result, 'requirement gate capture');
308
+ const second = readyOrThrow(produce(mods, repo, { task: { requirements: gateB } }).result, 'other requirement gate capture');
309
+ if (first.identity.taskScopeDigest === second.identity.taskScopeDigest) {
310
+ throw new Error('a nested requirement semantic (gate.state) must change taskScopeDigest');
311
+ }
312
+ const bindingA = H.clone(base); bindingA[1].bindings = [{ id: 'verification-loop', report: { kind: 'summary' } }];
313
+ const bindingB = H.clone(base); bindingB[1].bindings = [{ id: 'verification-loop', report: { kind: 'detailed' } }];
314
+ const third = readyOrThrow(produce(mods, repo, { task: { requirements: bindingA } }).result, 'binding report capture');
315
+ const fourth = readyOrThrow(produce(mods, repo, { task: { requirements: bindingB } }).result, 'other binding report capture');
316
+ if (third.identity.taskScopeDigest === fourth.identity.taskScopeDigest) {
317
+ throw new Error('a nested binding semantic (report.kind) must change taskScopeDigest');
318
+ }
319
+ }));
320
+
321
+ // ---- A10: Git failure and normal capture ------------------------------------------------------------------------
322
+
323
+ runner.run('A10:missing-baseline-commit-yields-baseline-unavailable', [REQ_PRODUCER], mods => withRepo(normativeFiles(), repo => {
324
+ const { result } = produce(mods, repo, { top: { baseline: { root: repo.root, commit: '0'.repeat(40) } } });
325
+ if (result.snapshot) throw new Error('a missing baseline commit must not yield a snapshot');
326
+ H.expectCodes(H.codesOf(result), ['baseline-unavailable'], 'missing baseline commit');
327
+ }));
328
+
329
+ runner.run('A10:non-repository-baseline-root-yields-baseline-unavailable', [REQ_PRODUCER], mods => withRepo(normativeFiles(), repo => {
330
+ const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'doc-s2a-not-a-repo-'));
331
+ try {
332
+ const { result } = produce(mods, repo, { top: { baseline: { root: outside, commit: repo.commit } } });
333
+ if (result.snapshot) throw new Error('a non-repository baseline root must not yield a snapshot');
334
+ H.expectCodes(H.codesOf(result), ['baseline-unavailable'], 'non-repository baseline root');
335
+ } finally {
336
+ fs.rmSync(outside, { recursive: true, force: true });
337
+ }
338
+ }));
339
+
340
+ runner.run('A10:normal-git-capture-succeeds', [REQ_PRODUCER], mods => withRepo(normativeFiles(), repo => {
341
+ readyOrThrow(produce(mods, repo).result, 'normal Git capture');
342
+ }));
343
+
344
+ // ---- A11: ignored paths (F4): included faithfully or unavailable, never silently absent --------------------------
345
+
346
+ const IGNORE_BASE = { '.gitignore': 'docs/prd/private/\n' };
347
+
348
+ runner.run('A11:ignored-governed-path-included-or-unavailable-never-silent', [REQ_PRODUCER], mods => withRepo({
349
+ base: { ...normativeFiles().base, ...IGNORE_BASE },
350
+ candidate: { ...normativeFiles().candidate, 'docs/prd/private/new.md': 'ignored governed PRD\n' }
351
+ }, repo => {
352
+ const { result } = produce(mods, repo);
353
+ if (result.state === 'ready' && result.snapshot) {
354
+ const record = recordAt(result.snapshot, 'candidate', 'docs/prd/private/new.md');
355
+ if (!record || record.presence !== 'present') throw new Error('an ignored governed path must be included when capture is ready');
356
+ return;
357
+ }
358
+ H.expectCodes(H.codesOf(result), ['inventory-incomplete', 'inventory-unsupported'], 'ignored governed path without faithful inventory');
359
+ }));
360
+
361
+ runner.run('A11:index-removed-ignored-file-included-or-unavailable', [REQ_PRODUCER], mods => {
362
+ const files = { base: { ...normativeFiles().base, ...IGNORE_BASE, 'docs/prd/tracked.md': 'tracked then ignored\n' }, candidate: { ...normativeFiles().candidate } };
363
+ withRepo(files, repo => {
364
+ fs.appendFileSync(path.join(repo.root, '.gitignore'), 'docs/prd/tracked.md\n');
365
+ H.git(repo.root, ['add', '-A']);
366
+ H.git(repo.root, ['-c', 'user.name=s2a-fixture', '-c', 'user.email=s2a-fixture@invalid', '-c', 'commit.gpgsign=false', 'commit', '-q', '--no-verify', '-m', 'ignore tracked path']);
367
+ H.git(repo.root, ['rm', '--cached', '-q', 'docs/prd/tracked.md']);
368
+ const { result } = produce(mods, repo);
369
+ if (result.state === 'ready' && result.snapshot) {
370
+ const record = recordAt(result.snapshot, 'candidate', 'docs/prd/tracked.md');
371
+ if (!record || record.presence !== 'present') throw new Error('a file removed from the index but still present must not be silently absent');
372
+ return;
373
+ }
374
+ H.expectCodes(H.codesOf(result), ['inventory-incomplete', 'inventory-unsupported'], 'index-removed ignored file without faithful inventory');
375
+ });
376
+ });
377
+
378
+ runner.run('A11:ignored-irrelevant-control-has-no-new-must-and-a-justified-exclusion', [REQ_PRODUCER], mods => withRepo({
379
+ base: { ...normativeFiles().base, '.gitignore': 'tmp/\n' },
380
+ candidate: { ...normativeFiles().candidate, 'tmp/notes.log': 'scratch output\n' }
381
+ }, repo => {
382
+ const { result } = produce(mods, repo);
383
+ const snapshot = readyOrThrow(result, 'ignored irrelevant control');
384
+ H.sameStable(snapshot.obligations.filter(item => item.strength === 'MUST' && item.applicability !== 'N/A').map(item => item.id).sort(),
385
+ [...VECTORS.vectors.normative.expectedMustIds].sort(), 'MUST set with an ignored irrelevant file');
386
+ const present = recordAt(snapshot, 'candidate', 'tmp/notes.log');
387
+ const justified = (snapshot.diagnostics || []).some(item => /ignored/i.test(String(item.message || item.code || '')));
388
+ if (!(present && present.presence === 'present') && !justified) {
389
+ throw new Error('an ignored irrelevant file must be included or excluded with a justified diagnostic');
390
+ }
391
+ }));
392
+
393
+ // ---- A03: captured config seam (resolver) and producer behaviour on config bytes ----------------------------------
394
+
395
+ const HOST_MANIFEST = '.claude/harness-everything/manifest.json';
396
+
397
+ runner.run('A03:captured-resolution-equals-existing-resolver-project-config', [REQ_RESOLVER], mods => withRepo(normativeFiles(), repo => {
398
+ const captured = mods[RESOLVER].resolveDocsConfigCaptured(repo.root, { activeHost: 'claude' });
399
+ const existing = mods[RESOLVER].resolveDocsConfig(repo.root, { activeHost: 'claude' });
400
+ H.sameStable(captured.resolution, existing, 'captured resolution equals the existing resolver result');
401
+ }));
402
+
403
+ runner.run('A03:project-overrides-host-positive-equals-existing-resolver', [REQ_RESOLVER], mods => {
404
+ const host = { ...normativeFiles().base, [HOST_MANIFEST]: JSON.stringify({ projectDocs: { governed: [{ path: 'docs/host/**', role: 'reference' }] } }, null, 2) + '\n' };
405
+ withRepo({ base: host, candidate: {} }, repo => {
406
+ const captured = mods[RESOLVER].resolveDocsConfigCaptured(repo.root, { activeHost: 'claude' });
407
+ const existing = mods[RESOLVER].resolveDocsConfig(repo.root, { activeHost: 'claude' });
408
+ H.sameStable(captured.resolution, existing, 'project-overrides-host resolution');
409
+ });
410
+ });
411
+
412
+ runner.run('A03:equal-host-positive-equals-existing-resolver', [REQ_RESOLVER], mods => {
413
+ const host = { ...normativeFiles().base, [HOST_MANIFEST]: JSON.stringify({ projectDocs: H.DERIVE_CONFIG }, null, 2) + '\n' };
414
+ withRepo({ base: host, candidate: {} }, repo => {
415
+ const captured = mods[RESOLVER].resolveDocsConfigCaptured(repo.root, { activeHost: 'claude' });
416
+ const existing = mods[RESOLVER].resolveDocsConfig(repo.root, { activeHost: 'claude' });
417
+ H.sameStable(captured.resolution, existing, 'equal-host resolution');
418
+ });
419
+ });
420
+
421
+ runner.run('A03:capture-binds-exact-config-bytes-and-selection', [REQ_RESOLVER], mods => withRepo(normativeFiles(), repo => {
422
+ const captured = mods[RESOLVER].resolveDocsConfigCaptured(repo.root, { activeHost: 'claude' });
423
+ const bytes = fs.readFileSync(path.join(repo.root, H.CONFIG_REL));
424
+ const digest = H.sha256(bytes);
425
+ if (!JSON.stringify(captured.capture && captured.capture.sources).includes(digest)) {
426
+ throw new Error('the capture must carry the SHA256 of the exact config bytes that were parsed');
427
+ }
428
+ if (!captured.capture || !Array.isArray(captured.capture.selectionInputs)) throw new Error('capture.selectionInputs must be an array');
429
+ }));
430
+
431
+ runner.run('A03:absent-project-config-yields-config-unavailable', [REQ_PRODUCER], mods => {
432
+ const files = normativeFiles();
433
+ const repo = H.makeRepo(files.base);
434
+ try {
435
+ H.writeFiles(repo.root, files.candidate);
436
+ H.git(repo.root, ['rm', '-q', H.CONFIG_REL]);
437
+ H.git(repo.root, ['-c', 'user.name=s2a-fixture', '-c', 'user.email=s2a-fixture@invalid', '-c', 'commit.gpgsign=false', 'commit', '-q', '--no-verify', '-m', 'remove config']);
438
+ const { result } = produce(mods, repo);
439
+ if (result.snapshot) throw new Error('an absent project config must not yield a usable producer identity');
440
+ H.expectCodes(H.codesOf(result), ['config-unavailable'], 'absent project config');
441
+ } finally {
442
+ H.removeRepo(repo.root);
443
+ }
444
+ });
445
+
446
+ // ---- Purity: producer capture writes nothing and spawns only Git; no network -----------------------------------------
447
+
448
+ runner.run('purity:producer-capture-writes-nothing-spawns-only-git', [REQ_PRODUCER], (mods, leg) => withRepo(normativeFiles(), repo => {
449
+ const abs = path.join(leg.lib, PRODUCER);
450
+ const log = H.recordingIo(rec => {
451
+ const fresh = H.freshModule(abs);
452
+ fresh.produceDocSnapshot(H.producerInput(repo), { originProvider: new H.FakeOrigin({ context: H.fakeContext({}, repo) }) });
453
+ return rec;
454
+ });
455
+ H.sameStable(log.writes, [], 'file writes during capture');
456
+ H.sameStable(log.spawns.filter(command => command !== 'git'), [], 'spawned commands other than git');
457
+ H.sameStable(log.net, [], 'network connects during capture');
458
+ }));
459
+
460
+ // ---- PR #353 review P2: a whole-project inventory that contains binary records ------------------------------
461
+ //
462
+ // The inventory is whole-project, so a normal tracked binary asset must not make the capture unusable. Its
463
+ // stored bytes stay in the identity framing; only semantic derivation needs decoded text.
464
+
465
+ runner.run('R3-P2:unchanged-binary-asset-captures-without-changing-applicability', [REQ_PRODUCER], mods => {
466
+ const files = normativeFiles();
467
+ const repo = H.makeRepo(files.base, { 'assets/logo.png': H.BINARY_BYTES });
468
+ try {
469
+ H.writeFiles(repo.root, files.candidate);
470
+ const snapshot = readyOrThrow(produce(mods, repo).result, 'capture with an unchanged binary asset');
471
+ const record = recordAt(snapshot, 'candidate', 'assets/logo.png');
472
+ if (!record || record.presence !== 'present') throw new Error('the binary asset must stay in the candidate inventory');
473
+ H.sameStable(record.digest, H.sha256(H.BINARY_BYTES), 'binary record digest of its stored bytes');
474
+ H.sameStable(snapshot.obligations.filter(item => item.strength === 'MUST' && item.applicability !== 'N/A').map(item => item.id).sort(),
475
+ [...VECTORS.vectors.normative.expectedMustIds].sort(), 'MUST set with an unchanged binary asset');
476
+ } finally { H.removeRepo(repo.root); }
477
+ });
478
+
479
+ runner.run('R3-P2:changed-binary-asset-changes-the-candidate-digest-without-changing-applicability', [REQ_PRODUCER], mods => {
480
+ const files = normativeFiles();
481
+ const repo = H.makeRepo(files.base, { 'assets/logo.png': H.BINARY_BYTES });
482
+ try {
483
+ H.writeFiles(repo.root, files.candidate);
484
+ const first = readyOrThrow(produce(mods, repo).result, 'capture with an unchanged binary asset');
485
+ H.writeBinary(repo.root, { 'assets/logo.png': H.BINARY_BYTES_CHANGED });
486
+ const second = readyOrThrow(produce(mods, repo).result, 'capture with a changed binary asset');
487
+ if (first.identity.candidateDigest === second.identity.candidateDigest) {
488
+ throw new Error('a changed binary asset must change the candidateDigest');
489
+ }
490
+ H.sameStable(second.obligations.filter(item => item.strength === 'MUST' && item.applicability !== 'N/A').map(item => item.id).sort(),
491
+ [...VECTORS.vectors.normative.expectedMustIds].sort(), 'MUST set with a changed binary asset');
492
+ } finally { H.removeRepo(repo.root); }
493
+ });
494
+
495
+ for (const [label, rel] of [['governed-document', 'docs/prd/binary.md'], ['derived-output', 'docs/roadmap/index.md'],
496
+ ['code-input', 'src/app.js'], ['test-input', 'tests/app.test.js']]) {
497
+ runner.run(`R3-P2:undecodable-${label}-record-fails-closed`, [REQ_PRODUCER], mods => {
498
+ const files = normativeFiles();
499
+ const repo = H.makeRepo(files.base);
500
+ try {
501
+ H.writeFiles(repo.root, files.candidate);
502
+ H.writeBinary(repo.root, { [rel]: H.BINARY_BYTES });
503
+ const { result } = produce(mods, repo);
504
+ if (result.snapshot) throw new Error(`an undecodable ${label} must not yield a snapshot`);
505
+ H.expectCodes(H.codesOf(result), ['inventory-unsupported'], `undecodable ${label}`);
506
+ } finally { H.removeRepo(repo.root); }
507
+ });
508
+ }
509
+
510
+ // TextDecoder removes a leading UTF-8 BOM; relevance detection must see the same frontmatter.
511
+ for (const prefix of ['', '\uFEFF']) {
512
+ runner.run(`R3-P2:undecodable-frontmatter-${prefix ? 'bom' : 'plain'}-fails-closed`, [REQ_PRODUCER], mods => {
513
+ const files = normativeFiles();
514
+ const repo = H.makeRepo(files.base);
515
+ try {
516
+ H.writeFiles(repo.root, files.candidate);
517
+ H.writeBinary(repo.root, { 'assets/role.dat': Buffer.concat([
518
+ Buffer.from(`${prefix}---\nrole: prd\n---\n`), H.BINARY_BYTES
519
+ ]) });
520
+ const { result } = produce(mods, repo);
521
+ if (result.snapshot) throw new Error('undecodable frontmatter must not disappear from semantic inputs');
522
+ H.expectCodes(H.codesOf(result), ['inventory-unsupported'], 'undecodable frontmatter');
523
+ } finally { H.removeRepo(repo.root); }
524
+ });
525
+ }
526
+
527
+ // Root __proto__ is not representable by the unchanged derive API's ordinary object maps.
528
+ for (const side of ['baseline', 'candidate']) {
529
+ runner.run(`R6-P2:${side}-proto-document-fails-closed`, [REQ_PRODUCER], mods => {
530
+ const files = normativeFiles();
531
+ const document = '---\nrole: prd\nstatus: draft\n---\n# Prototype document\n';
532
+ if (side === 'baseline') files.base = { ...files.base, ['__proto__']: document };
533
+ const repo = H.makeRepo(files.base);
534
+ try {
535
+ H.writeFiles(repo.root, files.candidate);
536
+ if (side === 'candidate') H.writeFiles(repo.root, { ['__proto__']: document });
537
+ const { result } = produce(mods, repo);
538
+ H.sameStable(result.state, 'unavailable', 'prototype-sensitive inventory state');
539
+ H.sameStable(result.snapshot, null, 'unsupported inventory earns no snapshot');
540
+ H.expectCodes(H.codesOf(result), ['inventory-unsupported'], 'prototype-sensitive document');
541
+ } finally { H.removeRepo(repo.root); }
542
+ });
543
+ }
544
+
545
+ runner.run('R6-P2:ordinary-path-frontmatter-retains-document-obligations', [REQ_PRODUCER], mods => {
546
+ const files = normativeFiles();
547
+ withRepo(files, repo => {
548
+ H.writeFiles(repo.root, { 'role.dat': '---\nrole: prd\nstatus: draft\n---\n# Prototype document\n' });
549
+ const snapshot = readyOrThrow(produce(mods, repo).result, 'ordinary-path control');
550
+ if (!snapshot.obligations.some(item => item.strength === 'MUST' && item.applicability === 'applicable'
551
+ && item.scope?.paths?.includes('role.dat'))) throw new Error('ordinary-path document lost its MUST obligations');
552
+ });
553
+ });
554
+
555
+ const counts = runner.summary();
556
+ if (counts.INFRA > 0) {
557
+ console.error('S2a producer suite: infrastructure faults present; these are not assertion results.');
558
+ process.exitCode = 2;
559
+ } else if (counts.FAIL > 0) {
560
+ console.error('S2a producer suite: assertion FAIL present (expected on the RED base; GREEN must clear every FAIL).');
561
+ process.exitCode = 1;
562
+ } else {
563
+ console.log('S2a producer suite: all assertions PASS.');
564
+ }
@@ -4,7 +4,7 @@ description: "Independently review a PR, diff or finished implementation for cor
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.33.0
7
+ version: 0.40.0
8
8
  ---
9
9
 
10
10
  # Code Review
@@ -30,10 +30,10 @@ Independently review completed changes for correctness, contract violations, and
30
30
 
31
31
  ## Workflow
32
32
 
33
- 1. Pin repository, contract, base/head, changed scope, and CI state; inspect relevant callers and consumers.
33
+ 1. Pin repository, verbatim acceptance contract, base/head, changed scope, and CI state; inspect relevant callers and consumers.
34
34
  2. Independently trace behavior and test applicable counterexamples. Separate severity from safety conditions.
35
35
  3. Report only supported findings with evidence tier, smallest correction, regression, and positive control. Use `<this-skill-dir>/templates/finding.template.md`.
36
- 4. On re-review, disposition every prior finding at the new HEAD, then inspect for regressions.
36
+ 4. On re-review, disposition every prior finding at the new HEAD, re-trace the acceptance path, then inspect for regressions.
37
37
  5. Keep review read-only unless explicitly authorized. Do not claim “bug-free.”
38
38
 
39
39
  In the interim, bind explicitly on an existing review step: `{"stepType":"review","optionalBindings":["code-review"]}`.
@@ -5,6 +5,7 @@ Use this checklist for the applicable risks; do not force irrelevant categories.
5
5
  ## Establish scope and evidence
6
6
 
7
7
  - [ ] Pin repository, PR, base/head SHA, user requirement or PRD/ADR, changed files, relevant callers/callees, and current CI state. Avoid unrelated checkout areas.
8
+ - [ ] Quote the acceptance clauses and owner bounds verbatim, with their source. Keep them apart from the chosen implementation (designs, existing tests) and the evidence: a design choice or a test expectation is not a requirement.
8
9
  - [ ] Make an independent pass. Treat author explanations and tests as leads, not proof; derive expected behavior from contracts and source.
9
10
  - [ ] Label evidence honestly: **executed/reproduced** (command, result, environment, SHA); **source-traced** (concrete path and preconditions); **inconclusive** (missing facts or ambiguous semantics; do not present as a proven blocker).
10
11
  - [ ] Inventory user-facing entry points and configuration handoffs (CLI flags, cwd, path spellings, config loading). Exercise them end-to-end, not only the recently edited internals.
@@ -24,10 +25,13 @@ Use this checklist for the applicable risks; do not force irrelevant categories.
24
25
  - [ ] When output text changes, compare the intended changed lines with literal templates; do not exclude changed lines from assertions.
25
26
  - [ ] Dedupe findings. Use P1 for severe merge blockers, P2 for meaningful correctness/contract defects, P3 for low-risk issues; avoid speculation.
26
27
  - [ ] For each finding, include exact path:line or diff anchor at HEAD SHA, trigger, observed behavior, violated contract and impact, evidence tier, smallest fix, negative regression, positive control, and disposition.
28
+ - [ ] Name the exact acceptance clause each finding violates, with a witness (starting state, input, expected result, actual result) and the trust assumption. A finding that maps to no clause gets scope `unresolved` for the owner; do not drop it, and do not present it as a blocker. Record severity and scope separately.
27
29
 
28
30
  ## Re-review and conclude
29
31
 
30
32
  - [ ] Pin the new HEAD. Disposition every prior finding separately: resolved, still open, superseded, or inconclusive. Then inspect fix-induced side effects and new changes.
33
+ - [ ] Re-trace one normal acceptance path end to end plus the consumers the fix affects, even when the request names only the last fix. A diff-only re-review misses unchanged sites that make the same decision.
34
+ - [ ] If this round and the previous one each found new defects in the same decision class, say so and recommend `zoom-out` (site inventory against the violated clause) before another point fix.
31
35
  - [ ] Stay read-only unless the user/workflow explicitly authorizes action. Do not edit, comment, label, or merge on review authority alone.
32
36
  - [ ] Summarize scope/SHA, CI, covered and unexamined risks, fixed/open/new counts, and evidence-grounded merge recommendation. “No actionable findings in examined scope” does not mean “bug-free.” Distinguish repository tests from live-host evidence.
33
37
 
@@ -5,6 +5,8 @@
5
5
  - Repository / PR:
6
6
  - Base SHA → reviewed HEAD SHA:
7
7
  - Requirement / PRD / ADR:
8
+ - Scope anchor (acceptance clauses and owner bounds, verbatim, with source):
9
+ - Prior rounds (findings per decision class, for re-review):
8
10
  - Changed files and relevant callers/consumers:
9
11
  - CI state and commands observed:
10
12
  - Counterexample categories covered:
@@ -17,6 +19,9 @@
17
19
  - **Location:** `<path>:<line or diff anchor> @ <HEAD SHA>`
18
20
  - **Trigger:** <minimal input, state transition, or interleaving>
19
21
  - **Behavior → impact:** <observed or source-traced behavior and violated contract>
22
+ - **Violates:** <exact acceptance clause from the scope anchor>
23
+ - **Witness:** <starting state → input → expected result → actual result; trust assumption>
24
+ - **Scope:** <in-scope | out-of-scope (known limit) | unresolved (owner decision)>
20
25
  - **Evidence tier:** <executed/reproduced | source-traced | inconclusive; include exact command/result or preconditions>
21
26
  - **Minimal correction:** <smallest feasible fix>
22
27
  - **Regression:** Negative case: <case that fails before fix>; positive control: <valid behavior that remains valid>
@@ -30,4 +35,5 @@
30
35
  - **CI evidence:**
31
36
  - **Covered / unexamined risks:**
32
37
  - **Findings:** <fixed / open / new counts on re-review>
38
+ - **Convergence:** <new findings per decision class this round vs the previous round; recommend `zoom-out` when a class keeps producing new findings>
33
39
  - **Conclusion:** <merge recommendation grounded in evidence; “no actionable findings in examined scope” is not a claim of bug-free code>