harness-everything 0.36.3 → 0.37.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 (51) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +18 -0
  4. package/ci/fixtures/doc-obligations/providers-s3/cases.json +535 -0
  5. package/ci/fixtures/doc-obligations/read-boundary/boundary-controls.json +211 -0
  6. package/ci/fixtures/doc-obligations/read-boundary/boundary-policy.json +325 -0
  7. package/ci/fixtures/doc-obligations/read-boundary/cases.json +14058 -0
  8. package/ci/fixtures/doc-obligations/read-boundary/expected.json +1 -0
  9. package/ci/fixtures/doc-obligations/read-boundary/identity-cases.json +119 -0
  10. package/ci/fixtures/doc-obligations/read-boundary/identity-expected.json +1 -0
  11. package/ci/fixtures/doc-obligations/read-boundary/scope-cases.json +208 -0
  12. package/ci/fixtures/doc-obligations/read-boundary/scope-expected.json +1 -0
  13. package/ci/fixtures/doc-obligations/reconciliation-prep/cases.json +2751 -0
  14. package/ci/fixtures/doc-obligations/reconciliation-prep/flood-entrypoint.js +5 -0
  15. package/ci/fixtures/doc-obligations/reconciliation-prep/hang-entrypoint.js +5 -0
  16. package/ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json +648 -0
  17. package/ci/fixtures/doc-obligations/reconciliation-prep/provider-contract.json +100 -0
  18. package/ci/fixtures/doc-obligations/reconciliation-prep/trust-contract.json +188 -0
  19. package/ci/lib/doc-evidence-boundary-check.js +1240 -0
  20. package/ci/lib/doc-evidence-fixtures.js +459 -0
  21. package/ci/lib/doc-evidence-trace.js +171 -0
  22. package/ci/lib/doc-reconciliation-prep-fixtures.js +502 -0
  23. package/ci/lib/snapshot-stdin.js +27 -0
  24. package/ci/mechanism-325-3b-preparation.test.js +382 -0
  25. package/ci/mechanism-325-3b-s3-providers.test.js +378 -0
  26. package/ci/mechanism-341-boundary-enforcement.test.js +660 -0
  27. package/ci/mechanism-341-capture-module.test.js +357 -0
  28. package/ci/mechanism-341-mutation-runner.test.js +413 -0
  29. package/ci/mechanism-341-read-boundary.test.js +388 -0
  30. package/ci/mechanism-341-snapshot-stdin.test.js +40 -0
  31. package/ci/mutation-check.js +805 -45
  32. package/ci/mutation-loader.js +544 -57
  33. package/ci/prep/doc-reconciliation-red.js +407 -0
  34. package/ci/stress-341-boundary-control.js +85 -0
  35. package/docs/doc-evidence-baseline-binding.md +105 -19
  36. package/docs/doc-evidence-boundary.md +135 -0
  37. package/docs/doc-workflow-reconciliation.md +338 -0
  38. package/docs/document-lifecycle.md +8 -1
  39. package/harness-everything/SKILL.md +1 -1
  40. package/harness-everything/scripts/doc-evidence-capture.js +331 -0
  41. package/harness-everything/scripts/doc-evidence.js +72 -223
  42. package/harness-everything/scripts/doc-providers.js +113 -0
  43. package/opencode-plugin/plugin.json +1 -1
  44. package/package.json +5 -3
  45. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  46. package/plugins/harness-everything/plugin.json +1 -1
  47. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  48. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence-capture.js +331 -0
  49. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +72 -223
  50. package/plugins/harness-everything/skills/harness-everything/scripts/doc-providers.js +113 -0
  51. package/sbom.cdx.json +43 -7
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // #325 Phase 3b preparation (H1+H2): fixture inventory, the qualified expected-RED runner, legacy controls,
5
+ // switch sanitation, source/package parity and negative registry checks. This suite asserts no runtime
6
+ // behaviour change and no pilot credit. Desired-contract gaps are qualified by the registry, never
7
+ // asserted as passing behaviour. Full replay runs use the runner; negative checks use one case each.
8
+
9
+ const assert = require('assert');
10
+ const crypto = require('crypto');
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+ const path = require('path');
14
+ const { spawnSync } = require('child_process');
15
+ const fx = require('./lib/doc-reconciliation-prep-fixtures');
16
+ const runner = require('./prep/doc-reconciliation-red.js');
17
+
18
+ const ROOT = path.resolve(__dirname, '..');
19
+ const PREP_DIR = path.join(__dirname, 'fixtures', 'doc-obligations', 'reconciliation-prep');
20
+ const RUNNER = path.join(__dirname, 'prep', 'doc-reconciliation-red.js');
21
+ const corpus = fx.readCaseCorpus(path.join(PREP_DIR, 'cases.json'));
22
+ const registry = JSON.parse(fs.readFileSync(path.join(PREP_DIR, 'pending-red.json'), 'utf8'));
23
+ const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'doc-recon-prep-suite-'));
24
+
25
+ const ALLOWED_OPS = new Set(['route', 'plan', 'start', 'binding', 'step', 'obligation', 'stop', 'provideEvidence', 'mutateIdentity', 'timestampHints']);
26
+ const ALLOWED_OPERATORS = new Set(['equals', 'notEquals', 'includes', 'absent']);
27
+ const REQUIRED_CASE_FIELDS = ['id', 'family', 'description', 'topology', 'requirements', 'input', 'actions', 'expectations'];
28
+ const SHARED_FILES = [
29
+ ['hooks/scripts/lib/workflow-obligations.js', 'plugins/harness-everything/hooks/scripts/lib/workflow-obligations.js'],
30
+ ['hooks/scripts/workflow-disposition.js', 'plugins/harness-everything/hooks/scripts/workflow-disposition.js'],
31
+ ['hooks/scripts/workflow-stop-gate.js', 'plugins/harness-everything/hooks/scripts/workflow-stop-gate.js'],
32
+ ];
33
+
34
+ let failed = 0;
35
+ function check(condition, message) {
36
+ if (condition) console.log(' PASS ' + message);
37
+ else { console.error(' FAIL ' + message); failed++; }
38
+ }
39
+ function checkCase(name, runCase) {
40
+ try { runCase(); check(true, name); } catch (error) { check(false, `${name}: ${error.message.split('\n')[0]}`); }
41
+ }
42
+ function runRunner(args) {
43
+ return spawnSync(process.execPath, [RUNNER, ...args], { cwd: ROOT, encoding: 'utf8' });
44
+ }
45
+ function writeRegistry(name, mutate) {
46
+ const copy = JSON.parse(JSON.stringify(registry));
47
+ mutate(copy);
48
+ const file = path.join(workDir, name);
49
+ fs.writeFileSync(file, `${JSON.stringify(copy, null, 2)}\n`, 'utf8');
50
+ return file;
51
+ }
52
+
53
+ console.log('=== #325 3b preparation: corpus, qualified RED runner and controls (H1+H2) ===');
54
+
55
+ checkCase('inventory: the H2 corpus covers P01-P07 with the L1 field shape', () => {
56
+ const ids = corpus.cases.map(item => item.id);
57
+ assert.deepStrictEqual(ids, ['P01', 'P04', 'P06', 'P02a', 'P02b', 'P02c', 'P02g', 'P02d', 'P02e', 'P02f', 'P03', 'P05', 'P07-fable-staged', 'P07-fable-parallel', 'P07-fable-multi-agent-workspace', 'P07u']);
58
+ assert.strictEqual(new Set(ids).size, ids.length, 'case ids are unique');
59
+ for (const item of corpus.cases) {
60
+ for (const field of REQUIRED_CASE_FIELDS) assert.ok(item[field] !== undefined, `${item.id} has ${field}`);
61
+ assert.deepStrictEqual(Object.keys(item.input).sort(), ['baseFiles', 'candidateFiles', 'config', 'hostEvents', 'task'], `${item.id} input keys`);
62
+ const actionIds = item.actions.map(action => action.id);
63
+ assert.strictEqual(new Set(actionIds).size, actionIds.length, `${item.id} action ids are unique`);
64
+ for (const action of item.actions) assert.ok(ALLOWED_OPS.has(action.op), `${item.id} op ${action.op} is allowed`);
65
+ for (const mode of ['off', 'on']) {
66
+ const ids2 = item.expectations[mode].map(assertion => assertion.id);
67
+ assert.strictEqual(new Set(ids2).size, ids2.length, `${item.id} ${mode} assertion ids are unique`);
68
+ for (const assertion of item.expectations[mode]) {
69
+ assert.ok(ALLOWED_OPERATORS.has(assertion.operator), `${item.id} operator ${assertion.operator}`);
70
+ assert.ok(Array.isArray(assertion.path) && assertion.path.length > 0, `${item.id}.${assertion.id} has a path`);
71
+ }
72
+ }
73
+ }
74
+ });
75
+
76
+ // The full replay runs once, through the runner's JSON report. Every case must be a control or a qualified gap,
77
+ // with matching parity between the two legs and no pilot credit.
78
+ let report = null;
79
+ checkCase('expected-RED mode: every case is CONTROL_PASS or EXPECTED_RED and exits 0', () => {
80
+ const run = runRunner(['--expect-red', '--json']);
81
+ assert.strictEqual(run.status, 0, `exit ${run.status}: ${(run.stderr || run.stdout).slice(0, 400)}`);
82
+ report = JSON.parse(run.stdout);
83
+ });
84
+
85
+ checkCase('expected-RED report: legs agree, controls are green, no pilot credit, runtime fingerprint matches', () => {
86
+ assert.ok(report, 'the report was produced');
87
+ assert.strictEqual(report.pilotCredit, false, 'preparation reports never grant pilot credit');
88
+ assert.strictEqual(report.runtimeFingerprintMatches, true, 'registry describes the current shared runtime bytes');
89
+ for (const item of report.cases) {
90
+ assert.ok(['CONTROL_PASS', 'EXPECTED_RED'].includes(item.status), `${item.caseId} is ${item.status}`);
91
+ assert.strictEqual(item.parity, 'match', `${item.caseId} source and packaged legs agree`);
92
+ }
93
+ const controls = report.cases.filter(item => item.status === 'CONTROL_PASS').map(item => item.caseId).sort();
94
+ assert.deepStrictEqual(controls, ['P02d', 'P06', 'P07u']);
95
+ });
96
+
97
+ checkCase('gap registry: every entry is qualified and the expected-RED mode accounts for all of them', () => {
98
+ assert.ok(report, 'the report was produced');
99
+ const qualified = report.cases.flatMap(item => item.assertions.filter(entry => entry.status === 'gap-qualified'));
100
+ assert.strictEqual(qualified.length, registry.entries.length, 'each registry entry is observed exactly once');
101
+ assert.ok(!report.cases.some(item => item.assertions.some(entry => entry.status === 'gap-unqualified' || entry.status === 'unexpected-pass')));
102
+ });
103
+
104
+ checkCase('strict mode fails on the recorded gaps and preserves the RED log', () => {
105
+ const log = path.join(workDir, 'p01-red.json');
106
+ const run = runRunner(['--strict', '--case', 'P01', '--log', log]);
107
+ assert.strictEqual(run.status, 1, `strict mode exits 1 on gaps: ${run.stdout}`);
108
+ const saved = JSON.parse(fs.readFileSync(log, 'utf8'));
109
+ assert.ok(saved.cases[0].assertions.some(entry => entry.status === 'gap-qualified'), 'the log keeps the qualified gaps');
110
+ });
111
+
112
+ checkCase('malformed registry is an infrastructure error, not a RED pass', () => {
113
+ const file = writeRegistry('missing-reason.json', copy => { delete copy.entries[0].reasonCode; });
114
+ const run = runRunner(['--expect-red', '--case', 'P01', '--registry', file]);
115
+ assert.strictEqual(run.status, 2, `registry errors exit 2: ${run.stdout} ${run.stderr}`);
116
+ });
117
+
118
+ checkCase('a registry that omits a gap fails expected-RED mode', () => {
119
+ const file = writeRegistry('omit-gap.json', copy => { copy.entries = copy.entries.filter(entry => entry.assertionId !== 'P01.on.stop-unsatisfied' || entry.distribution !== 'source'); });
120
+ const run = runRunner(['--expect-red', '--case', 'P01', '--registry', file]);
121
+ assert.strictEqual(run.status, 1, `an unqualified gap fails: ${run.stdout}`);
122
+ });
123
+
124
+ checkCase('an unexpected PASS on a control fails expected-RED mode', () => {
125
+ const file = writeRegistry('unexpected-pass.json', copy => {
126
+ copy.entries.push({
127
+ caseId: 'P06', assertionId: 'P06.on.stop-satisfied', distribution: 'source', baselineSha: copy.baselineSha,
128
+ actual: 'satisfied', desired: 'running', blocker: '#341 E0', reasonCode: 'pretend-gap'
129
+ });
130
+ });
131
+ const run = runRunner(['--expect-red', '--case', 'P06', '--registry', file]);
132
+ assert.strictEqual(run.status, 1, `a passing assertion with a registry entry fails: ${run.stdout}`);
133
+ });
134
+
135
+ // Infrastructure rule: an abnormal action result is never a qualified gap. The controls start from one healthy replay
136
+ // set of P01 and change only the ON route result, so every other fault and assertion is the unmodified replay's.
137
+ const p01 = corpus.cases.find(item => item.id === 'P01');
138
+ const p01Entries = registry.entries.filter(entry => entry.caseId === 'P01');
139
+ const healthyP01 = runner.replayAll(p01);
140
+ const injectIntoRoute = (patch, legs = ['source', 'packaged']) => {
141
+ const injected = JSON.parse(JSON.stringify(healthyP01));
142
+ for (const leg of legs) Object.assign(injected.on[leg].actions.route, patch);
143
+ return injected;
144
+ };
145
+
146
+ checkCase('negative control: an abnormal route in P01 is infrastructure, and --expect-red exits nonzero though later assertions still match', () => {
147
+ const injected = injectIntoRoute({ exitCode: 1 });
148
+ const later = p01.expectations.on.filter(assertion => assertion.id.startsWith('verify-obligation') || assertion.id.startsWith('stop-'));
149
+ for (const leg of ['source', 'packaged']) {
150
+ const before = fx.evaluateAll(healthyP01.on[leg], later);
151
+ const after = fx.evaluateAll(injected.on[leg], later);
152
+ assert.deepStrictEqual(after.map(item => [item.id, item.pass, item.actual]), before.map(item => [item.id, item.pass, item.actual]),
153
+ `${leg}: the later assertions evaluate exactly as in the unmodified replay`);
154
+ for (const item of after) {
155
+ const entry = p01Entries.find(candidate => candidate.assertionId === `P01.on.${item.id}` && candidate.distribution === leg);
156
+ assert.ok(entry && JSON.stringify(fx.normalize(entry.actual)) === JSON.stringify(fx.normalize(item.actual)),
157
+ `${leg}: ${item.id} still matches its registry actual, so only the infrastructure rule can stop qualification`);
158
+ }
159
+ }
160
+ const result = runner.classifyCase(p01, injected, p01Entries);
161
+ assert.strictEqual(result.status, 'INFRASTRUCTURE', `status ${result.status}`);
162
+ assert.deepStrictEqual(result.infrastructure, ['source.on.route: exit 1 outside policy [0]', 'packaged.on.route: exit 1 outside policy [0]']);
163
+ assert.deepStrictEqual(result.offending, { leg: 'source', mode: 'on', actionId: 'route', fault: 'exit 1 outside policy [0]' });
164
+ assert.deepStrictEqual(result.assertions, [], 'no assertion is qualified or classified for an infrastructure case');
165
+ assert.strictEqual(runner.exitCodeFor({ cases: [result] }, 'expect-red'), 2, 'expected-RED mode exits nonzero');
166
+ });
167
+
168
+ checkCase('negative control: a route killed by a signal or without an exit status is infrastructure', () => {
169
+ const result = runner.classifyCase(p01, injectIntoRoute({ exitCode: null, signal: 'SIGTERM' }), p01Entries);
170
+ assert.strictEqual(result.status, 'INFRASTRUCTURE');
171
+ assert.ok(result.infrastructure.includes('source.on.route: terminated by signal SIGTERM'), result.infrastructure.join('; '));
172
+ assert.ok(result.infrastructure.includes('source.on.route: no exit status'), result.infrastructure.join('; '));
173
+ });
174
+
175
+ checkCase('negative control: a spawn error on the route is infrastructure', () => {
176
+ const result = runner.classifyCase(p01, injectIntoRoute({ exitCode: null, error: 'spawnSync node ENOENT' }), p01Entries);
177
+ assert.strictEqual(result.status, 'INFRASTRUCTURE');
178
+ assert.ok(result.infrastructure.includes('source.on.route: spawn error spawnSync node ENOENT'), result.infrastructure.join('; '));
179
+ });
180
+
181
+ checkCase('negative control: an accepted route exit without the routing checkpoint is malformed required output', () => {
182
+ const result = runner.classifyCase(p01, injectIntoRoute({ stdout: 'routing unavailable' }), p01Entries);
183
+ assert.strictEqual(result.status, 'INFRASTRUCTURE');
184
+ assert.ok(result.infrastructure.includes('source.on.route: malformed required output'), result.infrastructure.join('; '));
185
+ });
186
+
187
+ checkCase('--expect-red exits nonzero from the CLI when a real route exit falls outside its policy', () => {
188
+ const corpusCopy = JSON.parse(JSON.stringify(corpus));
189
+ corpusCopy.cases.find(item => item.id === 'P01').actions.find(action => action.id === 'route').expectExit = { off: 1, on: 1 };
190
+ const file = path.join(workDir, 'route-policy-cases.json');
191
+ fs.writeFileSync(file, JSON.stringify(corpusCopy), 'utf8');
192
+ const run = runRunner(['--expect-red', '--case', 'P01', '--cases', file]);
193
+ assert.strictEqual(run.status, 2, `an infrastructure fault exits 2: ${run.stdout} ${run.stderr}`);
194
+ assert.ok(run.stdout.includes('INFRASTRUCTURE'), run.stdout);
195
+ });
196
+
197
+ // Real process boundary: a substituted child (hung or flooding) under a short deadline runs through the CLI. The
198
+ // outer SIGKILL deadline turns a regression (the runner never returns) into a failed check instead of a hung suite.
199
+ const OUTER_DEADLINE_MS = 90000;
200
+ const PROMPT_EXIT_MS = 30000;
201
+ // Replay directories still on disk: for one case, or for every case when caseId is empty.
202
+ const replayDirsFor = caseId => fs.readdirSync(os.tmpdir()).filter(name => name.startsWith(caseId ? `doc-recon-${caseId}-` : 'doc-recon-')).length;
203
+ function runWithChildLimits(caseId, childLimits) {
204
+ const corpusCopy = JSON.parse(JSON.stringify(corpus));
205
+ corpusCopy.cases.find(item => item.id === caseId).childLimits = childLimits;
206
+ const file = path.join(workDir, `${caseId}-child-limits.json`);
207
+ fs.writeFileSync(file, JSON.stringify(corpusCopy), 'utf8');
208
+ const started = Date.now();
209
+ const run = spawnSync(process.execPath, [RUNNER, '--expect-red', '--case', caseId, '--cases', file], {
210
+ cwd: ROOT, encoding: 'utf8', timeout: OUTER_DEADLINE_MS, killSignal: 'SIGKILL'
211
+ });
212
+ return { run, elapsed: Date.now() - started };
213
+ }
214
+
215
+ checkCase('a hung route entrypoint cannot stall --expect-red: the CLI exits 2 promptly and removes the replay directories', () => {
216
+ const before = replayDirsFor('P01');
217
+ const { run, elapsed } = runWithChildLimits('P01', { timeoutMs: 500, substitutes: { route: 'hang-entrypoint.js' } });
218
+ assert.strictEqual(run.signal, null, `the runner hung until the outer ${OUTER_DEADLINE_MS} ms deadline (signal ${run.signal})`);
219
+ assert.strictEqual(run.status, 2, `exit ${run.status}: ${run.stdout} ${run.stderr}`);
220
+ assert.ok(elapsed < PROMPT_EXIT_MS, `exit took ${elapsed} ms, bound ${PROMPT_EXIT_MS} ms`);
221
+ assert.ok(run.stdout.includes('INFRASTRUCTURE') && run.stdout.includes('timed out at the child deadline (ETIMEDOUT)'), run.stdout);
222
+ assert.strictEqual(replayDirsFor('P01'), before, 'replay directories are removed after the timeout');
223
+ });
224
+
225
+ checkCase('a route that floods stdout is stopped by the output bound and exits 2 through the CLI', () => {
226
+ const before = replayDirsFor('P01');
227
+ const { run, elapsed } = runWithChildLimits('P01', { timeoutMs: 5000, substitutes: { route: 'flood-entrypoint.js' } });
228
+ assert.strictEqual(run.signal, null, `the runner hung until the outer ${OUTER_DEADLINE_MS} ms deadline (signal ${run.signal})`);
229
+ assert.strictEqual(run.status, 2, `exit ${run.status}: ${run.stdout} ${run.stderr}`);
230
+ assert.ok(elapsed < PROMPT_EXIT_MS, `exit took ${elapsed} ms, bound ${PROMPT_EXIT_MS} ms`);
231
+ assert.ok(run.stdout.includes('child output exceeded maxBuffer (ENOBUFS)'), run.stdout);
232
+ assert.strictEqual(replayDirsFor('P01'), before, 'replay directories are removed after the output bound');
233
+ });
234
+
235
+ checkCase('a faulted replay runs no action after its first infrastructure fault and still removes its directories', () => {
236
+ const before = replayDirsFor('P01');
237
+ const item = { ...p01, childLimits: { timeoutMs: 500, substitutes: { route: 'hang-entrypoint.js' } } };
238
+ const root = fx.replayCase(item, 'source');
239
+ assert.deepStrictEqual(Object.keys(root.actions), ['route'], 'only the faulted route is recorded');
240
+ const faults = fx.infrastructureFaults(item, root, 'off');
241
+ assert.ok(faults.length > 0 && faults.every(entry => entry.actionId === 'route'), `faults name only the route: ${JSON.stringify(faults)}`);
242
+ assert.strictEqual(replayDirsFor('P01'), before, 'replay directories are removed after the fault');
243
+ });
244
+
245
+ // Corpus-level fail-fast through the real CLI. Every case hangs on the same shared route, so an unbounded runner would
246
+ // spend one deadline per replay (64 route timeouts). The fail-fast spends one, names the rest NOT_RUN, and exits 2.
247
+ const SHARED_HANG_DEADLINE_MS = 500;
248
+ const SHARED_HANG_BOUND_MS = 5000;
249
+ checkCase('corpus-level: a route hung in every case costs one deadline, exits 2, and names the other cases NOT_RUN', () => {
250
+ const everyCaseHangs = JSON.parse(JSON.stringify(corpus));
251
+ for (const item of everyCaseHangs.cases) {
252
+ item.childLimits = { timeoutMs: SHARED_HANG_DEADLINE_MS, substitutes: { route: 'hang-entrypoint.js' } };
253
+ }
254
+ const file = path.join(workDir, 'every-case-hangs.json');
255
+ fs.writeFileSync(file, JSON.stringify(everyCaseHangs), 'utf8');
256
+ const before = replayDirsFor('');
257
+ const started = Date.now();
258
+ const run = spawnSync(process.execPath, [RUNNER, '--expect-red', '--json', '--cases', file], {
259
+ cwd: ROOT, encoding: 'utf8', timeout: OUTER_DEADLINE_MS, killSignal: 'SIGKILL'
260
+ });
261
+ const elapsed = Date.now() - started;
262
+ assert.strictEqual(run.signal, null, `the runner hung until the outer ${OUTER_DEADLINE_MS} ms deadline (signal ${run.signal})`);
263
+ assert.strictEqual(run.status, 2, `exit ${run.status}: ${run.stdout} ${run.stderr}`);
264
+ assert.ok(elapsed < SHARED_HANG_BOUND_MS, `exit took ${elapsed} ms; one ${SHARED_HANG_DEADLINE_MS} ms deadline plus overhead must stay under ${SHARED_HANG_BOUND_MS} ms`);
265
+ const report = JSON.parse(run.stdout);
266
+ assert.strictEqual(report.replaysRun, 1, 'exactly one replay ran');
267
+ const [first, ...rest] = report.cases;
268
+ assert.strictEqual(first.caseId, 'P01');
269
+ assert.strictEqual(first.status, 'INFRASTRUCTURE');
270
+ assert.deepStrictEqual(first.offending, { leg: 'source', mode: 'off', actionId: 'route', fault: 'timed out at the child deadline (ETIMEDOUT)' });
271
+ assert.strictEqual(first.actionsRun, 1, 'only the hung route ran');
272
+ assert.deepStrictEqual(rest.map(item => item.caseId), corpus.cases.slice(1).map(item => item.id), 'every other case is named');
273
+ assert.ok(rest.every(item => item.status === 'NOT_RUN'), 'the other cases are NOT_RUN, never silently omitted');
274
+ assert.strictEqual(replayDirsFor(''), before, 'replay directories are removed');
275
+ });
276
+
277
+ checkCase('the committed corpus carries no child-limit override, so production replays use the default deadline', () => {
278
+ assert.ok(corpus.cases.every(item => item.childLimits === undefined), 'a childLimits key is test-only');
279
+ });
280
+
281
+ checkCase('a child deadline override can only shorten the default deadline', () => {
282
+ const item = { ...corpus.cases.find(candidate => candidate.id === 'P06'), childLimits: { timeoutMs: 60000 } };
283
+ assert.throws(() => fx.replayCase(item, 'source'), /may only shorten/);
284
+ });
285
+
286
+ checkCase('a malformed expectExit policy is an infrastructure error, not a RED pass', () => {
287
+ const corpusCopy = JSON.parse(JSON.stringify(corpus));
288
+ corpusCopy.cases.find(item => item.id === 'P06').actions.find(action => action.id === 'route').expectExit = { off: 'two' };
289
+ const file = path.join(workDir, 'malformed-policy-cases.json');
290
+ fs.writeFileSync(file, JSON.stringify(corpusCopy), 'utf8');
291
+ const run = runRunner(['--expect-red', '--case', 'P06', '--cases', file]);
292
+ assert.strictEqual(run.status, 2, `a malformed policy exits 2: ${run.stdout} ${run.stderr}`);
293
+ });
294
+
295
+ checkCase('a runtime fingerprint change invalidates qualification and prints the exact deliberate re-capture command', () => {
296
+ const file = writeRegistry('stale-fingerprint.json', copy => { copy.runtimeFingerprint = '0'.repeat(64); });
297
+ const run = runRunner(['--expect-red', '--case', 'P01', '--registry', file]);
298
+ assert.strictEqual(run.status, 1, `a fingerprint mismatch fails: ${run.stdout}`);
299
+ const output = run.stdout + run.stderr;
300
+ assert.ok(output.includes('--recapture-candidate ci/fixtures/doc-obligations/reconciliation-prep/pending-red.candidate.json'), 'the re-capture command names the candidate');
301
+ assert.ok(/--baseline-sha [0-9a-f]{40}/.test(output), 'the re-capture command names the commit');
302
+ assert.ok(output.includes('nothing is auto-accepted'), 'the message says nothing is auto-accepted');
303
+ });
304
+
305
+ checkCase('recapture writes a reviewable candidate and never the committed registry', () => {
306
+ const candidate = path.join(workDir, 'candidate.json');
307
+ const before = fs.readFileSync(path.join(PREP_DIR, 'pending-red.json'), 'utf8');
308
+ const run = spawnSync(process.execPath, [RUNNER, '--recapture-candidate', candidate, '--baseline-sha', registry.baselineSha, '--case', 'P06'], { cwd: ROOT, encoding: 'utf8' });
309
+ assert.strictEqual(run.status, 0, run.stderr);
310
+ assert.strictEqual(fs.readFileSync(path.join(PREP_DIR, 'pending-red.json'), 'utf8'), before, 'the committed registry is untouched');
311
+ const saved = JSON.parse(fs.readFileSync(candidate, 'utf8'));
312
+ assert.strictEqual(saved.entries.length, 0, 'a control case has no gaps to record');
313
+ assert.strictEqual(saved.runtimeFingerprint, registry.runtimeFingerprint, 'the candidate describes the current runtime');
314
+ });
315
+
316
+ checkCase('a candidate with unreviewed reasons is rejected by the registry validator', () => {
317
+ const file = writeRegistry('unreviewed.json', copy => { copy.entries[0].reasonCode = 'needs-reason'; });
318
+ const run = runRunner(['--expect-red', '--case', 'P01', '--registry', file]);
319
+ assert.strictEqual(run.status, 2, 'an unreviewed reason is a registry error');
320
+ });
321
+
322
+ checkCase('P08 parity: shared runtime files are byte-identical between the source and packaged legs', () => {
323
+ for (const [source, packaged] of SHARED_FILES) {
324
+ const left = crypto.createHash('sha256').update(fs.readFileSync(path.join(ROOT, source))).digest('hex');
325
+ const right = crypto.createHash('sha256').update(fs.readFileSync(path.join(ROOT, packaged))).digest('hex');
326
+ assert.strictEqual(left, right, `${source} matches ${packaged}`);
327
+ }
328
+ });
329
+
330
+ // Switch sanitation: an inherited ON request does not reach an OFF replay (one replay).
331
+ checkCase('switch sanitation: an inherited ON request does not reach an OFF replay', () => {
332
+ const previous = process.env[fx.PILOT_SWITCH];
333
+ process.env[fx.PILOT_SWITCH] = '1';
334
+ try {
335
+ const item = corpus.cases.find(candidate => candidate.id === 'P06');
336
+ const root = fx.replayCase(item, 'source');
337
+ const failures = fx.evaluateAll(root, item.expectations.off).filter(result => !result.pass);
338
+ assert.strictEqual(failures.length, 0, 'OFF expectations still hold with an inherited ON request');
339
+ } finally {
340
+ if (previous === undefined) delete process.env[fx.PILOT_SWITCH];
341
+ else process.env[fx.PILOT_SWITCH] = previous;
342
+ }
343
+ });
344
+
345
+ checkCase('cleanup: replays remove their temporary workspace and state directories', () => {
346
+ const prefix = 'doc-recon-P06-source-';
347
+ const before = fs.readdirSync(os.tmpdir()).filter(name => name.startsWith(prefix)).length;
348
+ fx.replayCase(corpus.cases.find(item => item.id === 'P06'), 'source');
349
+ const after = fs.readdirSync(os.tmpdir()).filter(name => name.startsWith(prefix)).length;
350
+ assert.strictEqual(after, before, 'no replay directory is left behind');
351
+ });
352
+
353
+ // F-a: separator-agnostic scrubbing. Windows-shaped strings from each leg must normalize to the same markers.
354
+ checkCase('F-a: Windows-shaped source and packaged paths normalize identically', () => {
355
+ const roots = { repo: 'D:\\a\\repo', tmp: ['C:\\Users\\runneradmin\\AppData\\Local\\Temp'] };
356
+ const sourceSkill = fx.scrubText('D:\\a\\repo\\tdd\\SKILL.md', roots);
357
+ const packagedSkill = fx.scrubText('D:\\a\\repo\\plugins\\harness-everything\\skills\\tdd\\SKILL.md', roots);
358
+ assert.strictEqual(sourceSkill, '<skill:tdd>', 'the source skill path is marked by skill id');
359
+ assert.strictEqual(packagedSkill, sourceSkill, 'the packaged skill path is marked the same way');
360
+ const sourceTmp = fx.scrubText('C:\\Users\\runneradmin\\AppData\\Local\\Temp\\doc-recon-P01-source-ws-0a1b2c3d-x9\\state\\workflow-run.json', roots);
361
+ const packagedTmp = fx.scrubText('C:\\Users\\runneradmin\\AppData\\Local\\Temp\\doc-recon-P01-packaged-ws-5e6f7a8b-y2\\state\\workflow-run.json', roots);
362
+ assert.strictEqual(packagedTmp, sourceTmp, 'temporary paths from each leg are marked the same way');
363
+ assert.ok(!sourceTmp.includes('\\') && !sourceTmp.includes('doc-recon'), 'no raw separator or temp name survives');
364
+ const sameAfterFold = fx.scrubText('C:/Users/runneradmin/AppData/Local/Temp/doc-recon-P01-source-ws-0a1b2c3d-x9/state/workflow-run.json', roots);
365
+ assert.strictEqual(sameAfterFold, sourceTmp, 'POSIX and Windows spellings of one path normalize identically');
366
+ });
367
+
368
+ // F-b: a per-action switch override must not reach an OFF replay.
369
+ checkCase('F-b: an OFF replay of P05 carries no per-action switch; the ON replay keeps its overrides', () => {
370
+ const item = corpus.cases.find(candidate => candidate.id === 'P05');
371
+ const off = fx.replayCase(item, 'source');
372
+ for (const [id, action] of Object.entries(off.actions)) {
373
+ assert.strictEqual(action.pilot, undefined, `OFF replay action ${id} has no switch value`);
374
+ }
375
+ const on = fx.replayCase(item, 'source', { pilot: '1' });
376
+ assert.strictEqual(on.actions['stop-on-1'].pilot, '1', 'an ON replay keeps the ON action value');
377
+ assert.strictEqual(on.actions['stop-off'].pilot, '0', 'an ON replay keeps the explicit OFF action value');
378
+ });
379
+
380
+ fs.rmSync(workDir, { recursive: true, force: true });
381
+ console.log(failed ? `\n${failed} preparation check(s) failed` : '\nAll preparation checks passed (desired-contract gaps are qualified, not asserted as behaviour)');
382
+ process.exitCode = failed ? 1 : 0;