harness-everything 0.36.3 → 0.36.4

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 (47) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +10 -0
  4. package/ci/fixtures/doc-obligations/read-boundary/boundary-controls.json +211 -0
  5. package/ci/fixtures/doc-obligations/read-boundary/boundary-policy.json +325 -0
  6. package/ci/fixtures/doc-obligations/read-boundary/cases.json +14058 -0
  7. package/ci/fixtures/doc-obligations/read-boundary/expected.json +1 -0
  8. package/ci/fixtures/doc-obligations/read-boundary/identity-cases.json +119 -0
  9. package/ci/fixtures/doc-obligations/read-boundary/identity-expected.json +1 -0
  10. package/ci/fixtures/doc-obligations/read-boundary/scope-cases.json +208 -0
  11. package/ci/fixtures/doc-obligations/read-boundary/scope-expected.json +1 -0
  12. package/ci/fixtures/doc-obligations/reconciliation-prep/cases.json +2751 -0
  13. package/ci/fixtures/doc-obligations/reconciliation-prep/flood-entrypoint.js +5 -0
  14. package/ci/fixtures/doc-obligations/reconciliation-prep/hang-entrypoint.js +5 -0
  15. package/ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json +648 -0
  16. package/ci/fixtures/doc-obligations/reconciliation-prep/provider-contract.json +100 -0
  17. package/ci/fixtures/doc-obligations/reconciliation-prep/trust-contract.json +188 -0
  18. package/ci/lib/doc-evidence-boundary-check.js +1240 -0
  19. package/ci/lib/doc-evidence-fixtures.js +459 -0
  20. package/ci/lib/doc-evidence-trace.js +171 -0
  21. package/ci/lib/doc-reconciliation-prep-fixtures.js +502 -0
  22. package/ci/lib/snapshot-stdin.js +27 -0
  23. package/ci/mechanism-325-3b-preparation.test.js +382 -0
  24. package/ci/mechanism-341-boundary-enforcement.test.js +660 -0
  25. package/ci/mechanism-341-capture-module.test.js +357 -0
  26. package/ci/mechanism-341-mutation-runner.test.js +413 -0
  27. package/ci/mechanism-341-read-boundary.test.js +388 -0
  28. package/ci/mechanism-341-snapshot-stdin.test.js +40 -0
  29. package/ci/mutation-check.js +805 -45
  30. package/ci/mutation-loader.js +544 -57
  31. package/ci/prep/doc-reconciliation-red.js +407 -0
  32. package/ci/stress-341-boundary-control.js +85 -0
  33. package/docs/doc-evidence-baseline-binding.md +105 -19
  34. package/docs/doc-evidence-boundary.md +135 -0
  35. package/docs/doc-workflow-reconciliation.md +224 -0
  36. package/docs/document-lifecycle.md +8 -1
  37. package/harness-everything/SKILL.md +1 -1
  38. package/harness-everything/scripts/doc-evidence-capture.js +331 -0
  39. package/harness-everything/scripts/doc-evidence.js +72 -223
  40. package/opencode-plugin/plugin.json +1 -1
  41. package/package.json +5 -3
  42. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  43. package/plugins/harness-everything/plugin.json +1 -1
  44. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  45. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence-capture.js +331 -0
  46. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +72 -223
  47. package/sbom.cdx.json +43 -7
@@ -0,0 +1,388 @@
1
+ 'use strict';
2
+
3
+ // #341 Phase 0A read-boundary corpus: replay conformance for the caller-graph read boundary.
4
+ //
5
+ // Each case in ci/fixtures/doc-obligations/read-boundary/cases.json is built fresh for every run and
6
+ // executed against both explicit distributions (canonical and packaged), unwrapped and traced. Its
7
+ // output, exception, semantic counters and full ordered trace are compared with the reviewed values
8
+ // in expected.json, and its matrix-level role claims (result outcome, diagnostic codes, read budgets)
9
+ // are checked from cases.json. Nothing is accepted or updated during test execution.
10
+ //
11
+ // IDs follow the Phase 0A fixture interface: caseId is `RB` plus digits; assertionId is
12
+ // `<caseId>.<contract>.<property>`. Each claim emits one JSONL event to HARNESS_ASSERTION_EVENT_FILE,
13
+ // and HARNESS_CASE_ID selects a single case.
14
+
15
+ const assert = require('assert');
16
+ const crypto = require('crypto');
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { buildCase, runChecker } = require('./lib/doc-evidence-fixtures');
20
+
21
+ const FIXTURE_DIR = path.join(__dirname, 'fixtures', 'doc-obligations', 'read-boundary');
22
+ const corpus = JSON.parse(fs.readFileSync(path.join(FIXTURE_DIR, 'cases.json'), 'utf8'));
23
+ const expected = JSON.parse(fs.readFileSync(path.join(FIXTURE_DIR, 'expected.json'), 'utf8'));
24
+ // Additive scope cases (plan §3). They live in their own append-only fixture, so the pinned cases.json and
25
+ // expected.json bytes never change. They were captured from the unmodified 0A evaluator (0f475ab).
26
+ const scopeCorpus = JSON.parse(fs.readFileSync(path.join(FIXTURE_DIR, 'scope-cases.json'), 'utf8'));
27
+ const scopeExpected = JSON.parse(fs.readFileSync(path.join(FIXTURE_DIR, 'scope-expected.json'), 'utf8'));
28
+ corpus.cases.push(...scopeCorpus.cases);
29
+ expected.cases = { ...expected.cases, ...scopeExpected.cases };
30
+ // Additive whitespace-identity case (C0 gap G1), captured from the unmodified 0A evaluator (0f475ab) in its own file.
31
+ const identityCorpus = JSON.parse(fs.readFileSync(path.join(FIXTURE_DIR, 'identity-cases.json'), 'utf8'));
32
+ const identityExpected = JSON.parse(fs.readFileSync(path.join(FIXTURE_DIR, 'identity-expected.json'), 'utf8'));
33
+ corpus.cases.push(...identityCorpus.cases);
34
+ expected.cases = { ...expected.cases, ...identityExpected.cases };
35
+
36
+ // Explicit distributions. Each is loaded from its own path and must be a distinct file.
37
+ const DISTRIBUTIONS = Object.freeze({
38
+ canonical: path.join(__dirname, '..', 'harness-everything', 'scripts', 'doc-evidence.js'),
39
+ packaged: path.join(__dirname, '..', 'plugins', 'harness-everything', 'skills', 'harness-everything', 'scripts', 'doc-evidence.js')
40
+ });
41
+
42
+ const EVENT_FILE = process.env.HARNESS_ASSERTION_EVENT_FILE;
43
+ const SELECTED_ASSERTION = process.env.HARNESS_ASSERTION_ID;
44
+ // The selected assertion names its case, so a selection without HARNESS_CASE_ID still runs one case.
45
+ const SELECTED_CASE = process.env.HARNESS_CASE_ID
46
+ || (SELECTED_ASSERTION ? SELECTED_ASSERTION.slice(0, SELECTED_ASSERTION.indexOf('.')) : undefined);
47
+ const claimedAssertions = new Set();
48
+
49
+ const failures = [];
50
+ let passed = 0;
51
+
52
+ function emit(event) {
53
+ if (!EVENT_FILE) return;
54
+ fs.appendFileSync(EVENT_FILE, `${JSON.stringify({ schemaVersion: 1, ...event })}\n`, 'utf8');
55
+ }
56
+
57
+ // Only an assertion-library comparison produces an assertion-failure event. Any other error is an
58
+ // infrastructure failure: it propagates, the process terminates abnormally, and no event is written.
59
+ function assertion(caseId, contract, run) {
60
+ const assertionId = `${caseId}.${contract}`;
61
+ claimedAssertions.add(assertionId);
62
+ // Only the selected claim runs: a selected result or exception claim fails on its own, and later
63
+ // claims never dereference an output the selected claim did not need.
64
+ if (SELECTED_ASSERTION && SELECTED_ASSERTION !== assertionId) return;
65
+ try {
66
+ run();
67
+ } catch (error) {
68
+ if (!(error instanceof assert.AssertionError)) throw error;
69
+ failures.push(assertionId);
70
+ emit({
71
+ event: 'assertion-failure',
72
+ caseId,
73
+ assertionId,
74
+ errorCode: 'ERR_ASSERTION',
75
+ message: String(error.message).split('\n')[0]
76
+ });
77
+ console.error(`❌ ${assertionId}`);
78
+ return;
79
+ }
80
+ passed += 1;
81
+ emit({ event: 'assertion-pass', caseId, assertionId });
82
+ console.log(`✅ ${assertionId}`);
83
+ }
84
+
85
+ function sha256(file) {
86
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
87
+ }
88
+
89
+ const checkers = Object.fromEntries(Object.entries(DISTRIBUTIONS).map(([name, file]) => [
90
+ name,
91
+ require(file).checkDocObligations
92
+ ]));
93
+
94
+ // Self-test (owner review P2-1): one checker run in two invocations must record two separate windows. The
95
+ // second invocation's trace and semantic counters describe only the second invocation.
96
+ const WINDOW_RECIPE = {
97
+ nodes: { entry: { kind: 'object', props: { digest: 'a'.repeat(64) } } },
98
+ input: { artifacts: { $ref: 'entry' }, obligations: { kind: 'array', items: [] } },
99
+ detached: true
100
+ };
101
+ function windowChecker(input) {
102
+ void input.artifacts.digest;
103
+ return { results: [], diagnostics: [] };
104
+ }
105
+ function selfTest(name, run) {
106
+ try {
107
+ run();
108
+ console.log(`self-test PASS ${name}`);
109
+ } catch (error) {
110
+ failures.push(`self-test:${name}`);
111
+ console.error(`self-test FAIL ${name}: ${String(error.message).split('\n')[0]}`);
112
+ }
113
+ }
114
+ selfTest('two invocations yield separate traces', () => {
115
+ const built = buildCase(WINDOW_RECIPE, { traced: true });
116
+ const run = runChecker(windowChecker, built);
117
+ assert.strictEqual(run.events.length, 1, 'the first invocation records its one read');
118
+ assert.strictEqual(run.postReturn.secondEvents.length, 1, 'the second invocation records only its own read');
119
+ assert.strictEqual(run.postReturn.secondEvents[0].seq, 0, 'the second window starts at sequence zero');
120
+ });
121
+ selfTest('second-invocation semantic counters describe only the second invocation', () => {
122
+ const recipe = {
123
+ nodes: { entry: { kind: 'object', props: { digest: { $accessor: { sequence: ['a'.repeat(64)], enumerable: true } } } } },
124
+ input: WINDOW_RECIPE.input,
125
+ detached: true
126
+ };
127
+ const run = runChecker(windowChecker, buildCase(recipe, { traced: true }));
128
+ assert.strictEqual(run.counters.accessors['entry.digest'].reads, 1, 'the first invocation reads the accessor once');
129
+ assert.strictEqual(run.postReturn.secondCounters.accessors['entry.digest'].reads, 1,
130
+ 'the second window reads the accessor once, not cumulatively');
131
+ assert.strictEqual(run.postReturn.secondCounters.accessors['entry.digest'].receivers.length, 1,
132
+ 'the second window lists only its own receiver');
133
+ });
134
+
135
+ // Owner review P3: a post-return delete must remove the own key, while the own-undefined transition keeps it.
136
+ const AFTER_RETURN_RECIPE = {
137
+ nodes: { candidateArtifacts: { kind: 'object', props: { 'docs/adr/a.md': 'b'.repeat(64) } } },
138
+ input: { artifacts: { $ref: 'candidateArtifacts' }, obligations: { kind: 'array', items: [] } },
139
+ detached: true
140
+ };
141
+ selfTest('a post-return delete removes the own key and records it as a prescribed write', () => {
142
+ const recipe = { ...AFTER_RETURN_RECIPE, afterReturn: [{ node: 'candidateArtifacts', key: 'docs/adr/a.md', to: { $delete: true } }] };
143
+ const built = buildCase(recipe, { traced: false });
144
+ built.applyAfterReturn();
145
+ assert.strictEqual(Object.hasOwn(built.input.artifacts, 'docs/adr/a.md'), false, 'absent, not own-undefined');
146
+ assert.deepStrictEqual(built.prescribed(), [{ node: 'candidateArtifacts', key: 'docs/adr/a.md' }]);
147
+ });
148
+ selfTest('the own-undefined transition keeps the own key with value undefined', () => {
149
+ const recipe = { ...AFTER_RETURN_RECIPE, afterReturn: [{ node: 'candidateArtifacts', key: 'docs/adr/a.md', to: { $undefined: true } }] };
150
+ const built = buildCase(recipe, { traced: false });
151
+ built.applyAfterReturn();
152
+ assert.strictEqual(Object.hasOwn(built.input.artifacts, 'docs/adr/a.md'), true, 'the key stays own');
153
+ assert.strictEqual(built.input.artifacts['docs/adr/a.md'], undefined, 'its value is undefined');
154
+ });
155
+
156
+ function countEvents(events, where) {
157
+ return events.filter(event => (where.id === undefined || event.id === where.id)
158
+ && (where.op === undefined || event.op === where.op)
159
+ && (where.key === undefined || JSON.stringify(event.key) === JSON.stringify(where.key))).length;
160
+ }
161
+
162
+ function budgetValue(budget, run) {
163
+ if (budget.kind === 'accessor') return run.counters.accessors[budget.path] ? run.counters.accessors[budget.path].reads : 0;
164
+ if (budget.kind === 'trace') return countEvents(run.events, budget.where);
165
+ if (budget.kind === 'trap') return run.counters.traps[budget.node] ? run.counters.traps[budget.node].calls : 0;
166
+ if (budget.kind === 'receiver') {
167
+ const entry = run.counters.accessors[budget.path];
168
+ return entry ? entry.receivers.filter(item => item === budget.node).length : 0;
169
+ }
170
+ throw new Error(`unknown budget kind ${budget.kind}`);
171
+ }
172
+
173
+ function budgetClaim(budget) {
174
+ return `${budget.role}.${budget.name}`;
175
+ }
176
+
177
+ function budgetHolds(budget, value) {
178
+ if (budget.kind === 'receiver') return value >= (budget.min || 1);
179
+ if (budget.count !== undefined) return value === budget.count;
180
+ return value >= budget.min;
181
+ }
182
+
183
+ // Every assertion a case may emit, derived from its declared claims. The mutation mapping must name
184
+ // one of these, so a mutant can only be credited to a claim the corpus actually makes.
185
+ function claimsFor(item) {
186
+ const claims = [
187
+ 'setup.events-excluded',
188
+ 'conformance.output-matches-expected',
189
+ 'conformance.exception-matches-expected',
190
+ 'conformance.counters-match-expected',
191
+ 'conformance.trace-matches-expected',
192
+ 'conformance.traced-matches-unwrapped',
193
+ 'trace.deterministic',
194
+ 'integrity.inputs-unchanged',
195
+ 'aliasing.no-input-leak',
196
+ 'identity.all-references-named',
197
+ 'distribution.parity'
198
+ ];
199
+ if (item.recipe.detached) {
200
+ claims.push('detached.result-unchanged-after-mutation', 'detached.fresh-invocation-matches-expected', 'integrity.post-return-unchanged',
201
+ 'detached.fresh-invocation-exception-matches-expected', 'detached.fresh-invocation-counters-match-expected',
202
+ 'detached.fresh-invocation-alias-leaks-match-expected');
203
+ if (item.expect && item.expect.secondDiffers) claims.push('detached.fresh-invocation-differs');
204
+ }
205
+ if (item.expect) claims.push('result.observation-outcome', 'result.diagnostic-codes');
206
+ for (const budget of item.budgets) claims.push(budgetClaim(budget));
207
+ return claims;
208
+ }
209
+
210
+ const casesById = new Map(corpus.cases.map(item => [item.caseId, item]));
211
+
212
+ if (SELECTED_CASE && !casesById.has(SELECTED_CASE)) {
213
+ console.error(`infrastructure: unknown HARNESS_CASE_ID ${SELECTED_CASE}`);
214
+ process.exit(2);
215
+ }
216
+ if (SELECTED_ASSERTION && (!SELECTED_CASE || !SELECTED_ASSERTION.startsWith(`${SELECTED_CASE}.`))) {
217
+ console.error(`infrastructure: HARNESS_ASSERTION_ID ${SELECTED_ASSERTION} does not belong to case ${SELECTED_CASE}`);
218
+ process.exit(2);
219
+ }
220
+
221
+ for (const [mutantId, mapping] of Object.entries(corpus.mutantCoverage)) {
222
+ const item = casesById.get(mapping.caseId);
223
+ const claimed = item && claimsFor(item).includes(mapping.assertionId.slice(`${mapping.caseId}.`.length));
224
+ if (!claimed) {
225
+ console.error(`infrastructure: mutant ${mutantId} maps to ${mapping.assertionId}, which the corpus does not make`);
226
+ process.exit(2);
227
+ }
228
+ }
229
+
230
+ // One replay of a case under one distribution: unwrapped, traced, and a repeat traced build. Each
231
+ // build is independent; setup and integrity snapshots must produce no trace events.
232
+ function replay(item, distribution) {
233
+ const checker = checkers[distribution];
234
+ const untracedBuild = buildCase(item.recipe, { traced: false });
235
+ const untraced = runChecker(checker, untracedBuild);
236
+ const tracedBuild = buildCase(item.recipe, { traced: true });
237
+ const setupEvents = tracedBuild.recorder.events.length;
238
+ tracedBuild.integrity();
239
+ const setupEventsAfterSnapshot = tracedBuild.recorder.events.length;
240
+ const traced = runChecker(checker, tracedBuild);
241
+ const repeat = runChecker(checker, buildCase(item.recipe, { traced: true }));
242
+ return { untraced, traced, repeat, tracedBuild, setupEvents, setupEventsAfterSnapshot };
243
+ }
244
+
245
+ for (const item of corpus.cases) {
246
+ const caseId = item.caseId;
247
+ if (SELECTED_CASE && SELECTED_CASE !== caseId) continue;
248
+ const pinned = expected.cases[caseId];
249
+ assert.ok(pinned, `${caseId} has no pinned expectation`);
250
+
251
+ const runs = {};
252
+ for (const distribution of Object.keys(DISTRIBUTIONS)) runs[distribution] = replay(item, distribution);
253
+ const each = fn => {
254
+ for (const [distribution, run] of Object.entries(runs)) fn(distribution, run);
255
+ };
256
+
257
+ assertion(caseId, 'setup.events-excluded', () => each((distribution, run) => {
258
+ assert.strictEqual(run.setupEvents, 0, `${distribution}: building a traced graph must record no events`);
259
+ assert.strictEqual(run.setupEventsAfterSnapshot, 0, `${distribution}: integrity snapshots must record no events`);
260
+ }));
261
+
262
+ assertion(caseId, 'conformance.output-matches-expected', () => each((distribution, run) => {
263
+ assert.deepStrictEqual(run.traced.output, pinned.output, `${distribution}: traced output differs from the reviewed output`);
264
+ assert.deepStrictEqual(run.untraced.output, pinned.output, `${distribution}: unwrapped output differs from the reviewed output`);
265
+ }));
266
+
267
+ assertion(caseId, 'conformance.exception-matches-expected', () => each((distribution, run) => {
268
+ assert.deepStrictEqual(run.traced.exception, pinned.exception, `${distribution}: traced exception differs from the reviewed exception`);
269
+ assert.deepStrictEqual(run.untraced.exception, pinned.exception, `${distribution}: unwrapped exception differs from the reviewed exception`);
270
+ }));
271
+
272
+ assertion(caseId, 'conformance.counters-match-expected', () => each((distribution, run) => {
273
+ assert.deepStrictEqual(run.traced.counters, pinned.counters, `${distribution}: traced counters differ from the reviewed counters`);
274
+ assert.deepStrictEqual(run.untraced.counters, pinned.counters, `${distribution}: unwrapped counters differ from the reviewed counters`);
275
+ }));
276
+
277
+ assertion(caseId, 'conformance.trace-matches-expected', () => each((distribution, run) => {
278
+ assert.deepStrictEqual(run.traced.events, pinned.trace, `${distribution}: traced access trace differs from the reviewed trace`);
279
+ }));
280
+
281
+ assertion(caseId, 'conformance.traced-matches-unwrapped', () => each((distribution, run) => {
282
+ assert.deepStrictEqual(run.traced.output, run.untraced.output, `${distribution}: traced output must equal the unwrapped output`);
283
+ assert.deepStrictEqual(run.traced.exception, run.untraced.exception, `${distribution}: traced exception must equal the unwrapped one`);
284
+ assert.deepStrictEqual(run.traced.counters, run.untraced.counters, `${distribution}: traced counters must equal the unwrapped counters`);
285
+ }));
286
+
287
+ assertion(caseId, 'trace.deterministic', () => each((distribution, run) => {
288
+ assert.deepStrictEqual(run.repeat.events, run.traced.events, `${distribution}: two traced builds must produce identical traces`);
289
+ }));
290
+
291
+ assertion(caseId, 'integrity.inputs-unchanged', () => each((distribution, run) => {
292
+ assert.deepStrictEqual(run.traced.integrityChangedTwins, [], `${distribution}: traced checker must not mutate its inputs`);
293
+ assert.deepStrictEqual(run.untraced.integrityChangedTwins, [], `${distribution}: unwrapped checker must not mutate its inputs`);
294
+ }));
295
+
296
+ assertion(caseId, 'aliasing.no-input-leak', () => each((distribution, run) => {
297
+ assert.strictEqual(run.traced.aliasLeaks, 0, `${distribution}: checker output must not carry live input objects`);
298
+ }));
299
+
300
+ assertion(caseId, 'identity.all-references-named', () => each((distribution, run) => {
301
+ assert.strictEqual(run.traced.unregisteredReferences, 0, `${distribution}: every caller-visible reference must carry a stable identity`);
302
+ assert.strictEqual(pinned.unregisteredReferences, 0, 'the reviewed corpus must name every reference');
303
+ }));
304
+
305
+ assertion(caseId, 'distribution.parity', () => {
306
+ assert.strictEqual(sha256(DISTRIBUTIONS.canonical), sha256(DISTRIBUTIONS.packaged),
307
+ 'canonical and packaged doc-evidence.js must be byte-identical in 0A');
308
+ assert.deepStrictEqual(runs.packaged.traced.events, runs.canonical.traced.events, 'packaged trace must equal canonical trace');
309
+ assert.deepStrictEqual(runs.packaged.traced.output, runs.canonical.traced.output, 'packaged output must equal canonical output');
310
+ assert.deepStrictEqual(runs.packaged.traced.counters, runs.canonical.traced.counters, 'packaged counters must equal canonical counters');
311
+ assert.deepStrictEqual(runs.packaged.traced.postReturn, runs.canonical.traced.postReturn, 'packaged post-return run must equal canonical');
312
+ });
313
+
314
+ if (item.expect) {
315
+ assertion(caseId, 'result.observation-outcome', () => each((distribution, run) => {
316
+ assert.notStrictEqual(run.traced.output, null, `${distribution}: the checker threw, so no observation/outcome exists`);
317
+ const actual = run.traced.output.results.map(result => [result.observation, result.outcome]);
318
+ assert.deepStrictEqual(actual, item.expect.results, `${distribution}: observation/outcome differs from the matrix expectation`);
319
+ }));
320
+
321
+ assertion(caseId, 'result.diagnostic-codes', () => each((distribution, run) => {
322
+ assert.notStrictEqual(run.traced.output, null, `${distribution}: the checker threw, so no diagnostic codes exist`);
323
+ const actual = run.traced.output.diagnostics.map(diagnostic => diagnostic.code).sort();
324
+ assert.deepStrictEqual(actual, [...item.expect.codes].sort(), `${distribution}: diagnostic codes differ from the matrix expectation`);
325
+ }));
326
+ }
327
+
328
+ if (pinned.postReturn) {
329
+ assertion(caseId, 'detached.result-unchanged-after-mutation', () => each((distribution, run) => {
330
+ assert.ok(run.traced.postReturn, `${distribution}: a detached case must record a post-return run`);
331
+ assert.strictEqual(run.traced.postReturn.outputUnchangedAfterMutation, true,
332
+ `${distribution}: the first result must not change when caller state changes`);
333
+ assert.deepStrictEqual(run.traced.output, pinned.output, `${distribution}: the first result differs from the reviewed result`);
334
+ }));
335
+
336
+ assertion(caseId, 'detached.fresh-invocation-matches-expected', () => each((distribution, run) => {
337
+ assert.deepStrictEqual(run.traced.postReturn.secondOutput, pinned.postReturn.secondOutput,
338
+ `${distribution}: fresh invocation output differs from the reviewed output`);
339
+ assert.deepStrictEqual(run.traced.postReturn.secondEvents, pinned.postReturn.secondEvents,
340
+ `${distribution}: fresh invocation trace differs from the reviewed trace`);
341
+ }));
342
+
343
+ assertion(caseId, 'detached.fresh-invocation-exception-matches-expected', () => each((distribution, run) => {
344
+ assert.deepStrictEqual(run.traced.postReturn.secondException, pinned.postReturn.secondException,
345
+ `${distribution}: fresh invocation exception differs from the reviewed exception`);
346
+ }));
347
+
348
+ assertion(caseId, 'detached.fresh-invocation-counters-match-expected', () => each((distribution, run) => {
349
+ assert.deepStrictEqual(run.traced.postReturn.secondCounters, pinned.postReturn.secondCounters,
350
+ `${distribution}: fresh invocation counters differ from the reviewed counters`);
351
+ }));
352
+
353
+ assertion(caseId, 'detached.fresh-invocation-alias-leaks-match-expected', () => each((distribution, run) => {
354
+ assert.strictEqual(run.traced.postReturn.secondAliasLeaks, pinned.postReturn.secondAliasLeaks,
355
+ `${distribution}: fresh invocation alias leakage differs from the reviewed count`);
356
+ }));
357
+
358
+ if (item.expect && item.expect.secondDiffers) {
359
+ assertion(caseId, 'detached.fresh-invocation-differs', () => each((distribution, run) => {
360
+ assert.notDeepStrictEqual(run.traced.postReturn.secondOutput, run.traced.output,
361
+ `${distribution}: a fresh invocation must see the mutated caller state`);
362
+ }));
363
+ }
364
+
365
+ assertion(caseId, 'integrity.post-return-unchanged', () => each((distribution, run) => {
366
+ assert.deepStrictEqual(run.traced.postReturn.secondIntegrityChangedTwins, [],
367
+ `${distribution}: the fresh invocation must not mutate its inputs beyond the factory's writes`);
368
+ }));
369
+ }
370
+
371
+ for (const budget of item.budgets) {
372
+ assertion(caseId, budgetClaim(budget), () => each((distribution, run) => {
373
+ const value = budgetValue(budget, run.traced);
374
+ assert.ok(budgetHolds(budget, value), `${distribution}: ${budgetClaim(budget)} observed ${value}, expected ${budget.count !== undefined ? `=${budget.count}` : budget.kind === 'receiver' ? 'receiver identity' : `>=${budget.min}`}`);
375
+ }));
376
+ }
377
+ }
378
+
379
+ if (SELECTED_ASSERTION && !claimedAssertions.has(SELECTED_ASSERTION)) {
380
+ console.error(`infrastructure: HARNESS_ASSERTION_ID ${SELECTED_ASSERTION} is not a claim the corpus makes`);
381
+ process.exit(2);
382
+ }
383
+
384
+ console.log(`\n[341 read-boundary] ${passed} passed, ${failures.length} failed`);
385
+ if (failures.length) {
386
+ console.error(`FAILED: ${failures.join(', ')}`);
387
+ process.exitCode = 1;
388
+ }
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const { spawnWithSnapshotInput } = require('./lib/snapshot-stdin');
8
+
9
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-341-stdin-test-'));
10
+ const saved = new Map(['TMPDIR', 'TEMP', 'TMP'].map(key => [key, process.env[key]]));
11
+ try {
12
+ for (const key of saved.keys()) process.env[key] = root;
13
+ const text = 'finite JSON snapshot: 中\n' + 'x'.repeat(300000);
14
+ const child = spawnWithSnapshotInput(process.execPath, ['-e', `
15
+ const fs = require('node:fs');
16
+ process.stdout.write(JSON.stringify({ regular: fs.fstatSync(0).isFile(), text: fs.readFileSync(0, 'utf8') }));
17
+ `], { input: text, encoding: 'utf8', timeout: 10000 });
18
+ assert.equal(child.status, 0, child.stderr);
19
+ assert.deepEqual(JSON.parse(child.stdout), { regular: true, text });
20
+ assert.deepEqual(fs.readdirSync(root), [], 'a completed child leaves no input snapshot');
21
+
22
+ const failed = spawnWithSnapshotInput(process.execPath, ['-e', 'process.exit(7)'], {
23
+ input: '{}', encoding: 'utf8', timeout: 10000,
24
+ });
25
+ assert.equal(failed.status, 7, 'a child failure is preserved, never credited as success');
26
+ assert.deepEqual(fs.readdirSync(root), [], 'a failed child leaves no input snapshot');
27
+
28
+ const timedOut = spawnWithSnapshotInput(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {
29
+ input: '{}', encoding: 'utf8', timeout: 100, killSignal: 'SIGKILL',
30
+ });
31
+ assert.equal(timedOut.error.code, 'ETIMEDOUT', 'the external deadline still terminates a stuck child');
32
+ assert.deepEqual(fs.readdirSync(root), [], 'a terminated child leaves no input snapshot');
33
+ console.log('PASS snapshot stdin: finite UTF-8 input, failure/timeout propagation, and cleanup');
34
+ } finally {
35
+ for (const [key, value] of saved) {
36
+ if (value === undefined) delete process.env[key];
37
+ else process.env[key] = value;
38
+ }
39
+ fs.rmSync(root, { recursive: true, force: true });
40
+ }