harness-everything 0.36.2 → 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 (60) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +17 -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-2aw-project-docs-config.test.js +450 -10
  24. package/ci/mechanism-325-3b-preparation.test.js +382 -0
  25. package/ci/mechanism-341-boundary-enforcement.test.js +660 -0
  26. package/ci/mechanism-341-capture-module.test.js +357 -0
  27. package/ci/mechanism-341-mutation-runner.test.js +413 -0
  28. package/ci/mechanism-341-read-boundary.test.js +388 -0
  29. package/ci/mechanism-341-snapshot-stdin.test.js +40 -0
  30. package/ci/mutation-check.js +805 -45
  31. package/ci/mutation-loader.js +544 -57
  32. package/ci/prep/doc-reconciliation-red.js +407 -0
  33. package/ci/stress-341-boundary-control.js +85 -0
  34. package/docs/doc-evidence-baseline-binding.md +105 -19
  35. package/docs/doc-evidence-boundary.md +135 -0
  36. package/docs/doc-workflow-reconciliation.md +224 -0
  37. package/docs/document-lifecycle.md +67 -8
  38. package/harness-everything/SKILL.md +1 -1
  39. package/harness-everything/scripts/doc-evidence-capture.js +331 -0
  40. package/harness-everything/scripts/doc-evidence.js +72 -223
  41. package/harness-everything/scripts/project-docs-resolver.js +30 -13
  42. package/multi-agent-workspace/SKILL.md +1 -1
  43. package/multi-agent-workspace/scripts/project-docs-resolver.js +30 -13
  44. package/opencode-plugin/plugin.json +1 -1
  45. package/package.json +5 -3
  46. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  47. package/plugins/harness-everything/plugin.json +1 -1
  48. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  49. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence-capture.js +331 -0
  50. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +72 -223
  51. package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +30 -13
  52. package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +1 -1
  53. package/plugins/harness-everything/skills/multi-agent-workspace/scripts/project-docs-resolver.js +30 -13
  54. package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
  55. package/plugins/harness-everything/skills/to-spec/scripts/check-project-docs.js +136 -4
  56. package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +30 -13
  57. package/sbom.cdx.json +43 -7
  58. package/to-spec/SKILL.md +1 -1
  59. package/to-spec/scripts/check-project-docs.js +136 -4
  60. package/to-spec/scripts/project-docs-resolver.js +30 -13
@@ -1,59 +1,819 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ const fs = require('fs');
5
+ const os = require('os');
4
6
  const path = require('path');
5
7
  const { spawnSync } = require('child_process');
8
+ const { fileURLToPath, pathToFileURL } = require('url');
9
+ const { distributionTargets, prepareMutation } = require('./mutation-loader');
6
10
 
7
11
  const ROOT = path.resolve(__dirname, '..');
8
- const loader = path.join(__dirname, 'mutation-loader.js');
9
- const checks = [
10
- ['physical path boundaries', 'scripts/lib/path-boundary.js', 'ci/mechanism-2v-verifier-boundaries.test.js'],
11
- ['exact execution commands', 'scripts/lib/execution-contract.js', 'ci/mechanism-2v-verifier-boundaries.test.js'],
12
- ['archive output boundaries', 'behavioral-evals/evidence-tool.js', 'ci/mechanism-2t-pr66-evidence-triage.test.js'],
13
- ['legacy migration boundaries', 'multi-agent-workspace/scripts/scaffold.js', 'ci/mechanism-2l-multi-agent-workspace.test.js'],
14
- ['global installer fallback', 'scripts/installer.js', 'ci/mechanism-2q-installer-global-shared-store.test.js'],
15
- ['transcript command qualifiers', 'behavioral-evals/transcript-parser.js', 'ci/mechanism-2t-transcript-parser.test.js'],
16
- ['fixture path boundaries', 'behavioral-evals/run.js', 'ci/mechanism-2j-behavioral-runner.test.js'],
17
- ['unbound workspace identity', 'scripts/lib/workspace.js', 'ci/mechanism-2u-issue-42-regressions.test.js'],
18
- ['worktree submodule reachability', 'using-git-worktrees/scripts/submodule-reachability.js', 'ci/mechanism-34-worktree-submodule-reachability.test.js'],
19
- ['verification contract precedence', 'harness-everything/scripts/verify-gate.js', 'ci/mechanism-35-verification-contract.test.js'],
20
- ['positive skill-eval relevance', 'harness-everything/scripts/tier-router.js', 'ci/skill-routing-check.js'],
12
+ const LOADER = path.join(__dirname, 'mutation-loader.js');
13
+ const SHA256 = /^[a-f0-9]{64}$/;
14
+
15
+ const LEGACY_CASES = {
16
+ pathBoundary: 'LEGACY.mechanism-2v-verifier-boundaries',
17
+ executionContract: 'LEGACY.mechanism-2v-verifier-boundaries',
18
+ evidenceTool: 'LEGACY.mechanism-2t-evidence-triage',
19
+ scaffold: 'LEGACY.mechanism-2l-multi-agent-workspace',
20
+ installer: 'LEGACY.mechanism-2q-installer-global-shared-store',
21
+ transcript: 'LEGACY.mechanism-2t-transcript-parser',
22
+ behavioralRunner: 'LEGACY.mechanism-2j-behavioral-runner',
23
+ workspace: 'LEGACY.mechanism-2u-issue-42-regressions',
24
+ submodule: 'LEGACY.mechanism-34-worktree-submodule-reachability',
25
+ verifyGate: 'LEGACY.mechanism-35-verification-contract',
26
+ skillRouting: 'LEGACY.skill-routing-check',
27
+ };
28
+
29
+ const LEGACY_CHECKS = [
30
+ {
31
+ label: 'physical path boundaries', target: 'scripts/lib/path-boundary.js', mutantId: 'path-boundary',
32
+ test: 'ci/mechanism-2v-verifier-boundaries.test.js', caseId: LEGACY_CASES.pathBoundary,
33
+ assertionId: `${LEGACY_CASES.pathBoundary}.symlink-component-rejected`,
34
+ },
35
+ {
36
+ label: 'exact execution commands', target: 'scripts/lib/execution-contract.js', mutantId: 'execution-contract',
37
+ test: 'ci/mechanism-2v-verifier-boundaries.test.js', caseId: LEGACY_CASES.executionContract,
38
+ assertionId: `${LEGACY_CASES.executionContract}.extra-arguments-rejected`,
39
+ },
40
+ {
41
+ label: 'archive output boundaries', target: 'behavioral-evals/evidence-tool.js', mutantId: 'evidence-tool',
42
+ test: 'ci/mechanism-2t-evidence-triage.test.js', caseId: LEGACY_CASES.evidenceTool,
43
+ assertionId: `${LEGACY_CASES.evidenceTool}.archive-link-rejected`,
44
+ },
45
+ {
46
+ label: 'legacy migration boundaries', target: 'multi-agent-workspace/scripts/scaffold.js', mutantId: 'scaffold-migration',
47
+ test: 'ci/mechanism-2l-multi-agent-workspace.test.js', caseId: LEGACY_CASES.scaffold,
48
+ assertionId: `${LEGACY_CASES.scaffold}.linked-legacy-source-rejected`,
49
+ },
50
+ {
51
+ label: 'global installer fallback', target: 'scripts/installer.js', mutantId: 'installer-global',
52
+ test: 'ci/mechanism-2q-installer-global-shared-store.test.js', caseId: LEGACY_CASES.installer,
53
+ assertionId: `${LEGACY_CASES.installer}.global-install-from-non-git-directory`,
54
+ },
55
+ {
56
+ label: 'transcript command qualifiers', target: 'behavioral-evals/transcript-parser.js', mutantId: 'transcript-command',
57
+ test: 'ci/mechanism-2t-transcript-parser.test.js', caseId: LEGACY_CASES.transcript,
58
+ assertionId: `${LEGACY_CASES.transcript}.command-qualifier`,
59
+ },
60
+ {
61
+ label: 'fixture path boundaries', target: 'behavioral-evals/run.js', mutantId: 'runner-fixture',
62
+ test: 'ci/mechanism-2j-behavioral-runner.test.js', caseId: LEGACY_CASES.behavioralRunner,
63
+ assertionId: `${LEGACY_CASES.behavioralRunner}.fixture-traversal-rejected`,
64
+ },
65
+ {
66
+ label: 'unbound workspace identity', target: 'scripts/lib/workspace.js', mutantId: 'workspace-identity',
67
+ test: 'ci/mechanism-2u-issue-42-regressions.test.js', caseId: LEGACY_CASES.workspace,
68
+ assertionId: `${LEGACY_CASES.workspace}.stable-unbound-workspace-key`,
69
+ },
70
+ {
71
+ label: 'worktree submodule reachability', target: 'using-git-worktrees/scripts/submodule-reachability.js', mutantId: 'submodule-reachability',
72
+ test: 'ci/mechanism-34-worktree-submodule-reachability.test.js', caseId: LEGACY_CASES.submodule,
73
+ assertionId: `${LEGACY_CASES.submodule}.unpublished-submodule-commit-rejected`,
74
+ },
75
+ {
76
+ label: 'verification contract precedence', target: 'harness-everything/scripts/verify-gate.js', mutantId: 'verify-contract-precedence',
77
+ test: 'ci/mechanism-35-verification-contract.test.js', caseId: LEGACY_CASES.verifyGate,
78
+ assertionId: `${LEGACY_CASES.verifyGate}.authoritative-contract-precedence`,
79
+ },
80
+ {
81
+ label: 'positive skill-eval relevance', target: 'harness-everything/scripts/tier-router.js', mutantId: 'knowledge-signal-matcher',
82
+ test: 'ci/skill-routing-check.js', caseId: LEGACY_CASES.skillRouting,
83
+ assertionId: `${LEGACY_CASES.skillRouting}.tdd-positive-relevance`,
84
+ },
21
85
  ];
22
86
 
23
- let failures = 0;
24
- for (const [label, target, testFile] of checks) {
25
- const result = spawnSync(process.execPath, ['--require', loader, path.join(ROOT, testFile)], {
26
- cwd: ROOT,
27
- env: {
28
- ...process.env,
29
- HARNESS_MUTATION: {
30
- 'scripts/lib/path-boundary.js': 'path-boundary',
31
- 'scripts/lib/execution-contract.js': 'execution-contract',
32
- 'behavioral-evals/evidence-tool.js': 'evidence-tool',
33
- 'multi-agent-workspace/scripts/scaffold.js': 'scaffold-migration',
34
- 'scripts/installer.js': 'installer-global',
35
- 'behavioral-evals/transcript-parser.js': 'transcript-command',
36
- 'behavioral-evals/run.js': 'runner-fixture',
37
- 'scripts/lib/workspace.js': 'workspace-identity',
38
- 'using-git-worktrees/scripts/submodule-reachability.js': 'submodule-reachability',
39
- 'harness-everything/scripts/verify-gate.js': 'verify-contract-precedence',
40
- 'harness-everything/scripts/tier-router.js': 'knowledge-signal-matcher',
41
- }[target],
42
- HARNESS_MUTATION_TARGET: path.join(ROOT, target),
43
- NODE_OPTIONS: `--require=${loader}`,
44
- },
45
- encoding: 'utf8'
87
+ const DOC_EVIDENCE_TEST = 'ci/mechanism-341-read-boundary.test.js';
88
+ const DOC_EVIDENCE_COVERAGE = require('./fixtures/doc-obligations/read-boundary/cases.json').mutantCoverage;
89
+ const DOC_EVIDENCE_CHECKS = Object.entries(DOC_EVIDENCE_COVERAGE).flatMap(([mutantId, mapping]) =>
90
+ Object.entries(distributionTargets(mutantId)).map(([distribution, target]) => ({
91
+ label: `${mutantId} ${distribution} read-boundary mutant`,
92
+ target,
93
+ mutantId,
94
+ test: DOC_EVIDENCE_TEST,
95
+ caseId: mapping.caseId,
96
+ assertionId: mapping.assertionId,
97
+ }))
98
+ );
99
+ const DOC_EVIDENCE_VARIANTS = [
100
+ { mutantId: 'M01-stateful', caseId: 'RB44', assertionId: 'RB44.R1.baseline-digest-reads' },
101
+ { mutantId: 'M06-observation', caseId: 'RB17', assertionId: 'RB17.result.observation-outcome' },
102
+ { mutantId: 'M07b-audit-output', caseId: 'RB19', assertionId: 'RB19.conformance.output-matches-expected' },
103
+ { mutantId: 'M09-descriptor-count', caseId: 'RB21', assertionId: 'RB21.R3.source-descriptor-checks' },
104
+ { mutantId: 'M10-rename-to', caseId: 'RB52', assertionId: 'RB52.result.observation-outcome' },
105
+ { mutantId: 'M11-entry-exception', caseId: 'RB11', assertionId: 'RB11.conformance.exception-matches-expected' },
106
+ { mutantId: 'M11-baseline-map', caseId: 'RB12', assertionId: 'RB12.conformance.exception-matches-expected' },
107
+ { mutantId: 'M11-candidate-descriptor', caseId: 'RB14', assertionId: 'RB14.conformance.exception-matches-expected' },
108
+ { mutantId: 'M11-destination', caseId: 'RB56', assertionId: 'RB56.conformance.exception-matches-expected' },
109
+ { mutantId: 'M11-ordinary-capture', caseId: 'RB23', assertionId: 'RB23.conformance.exception-matches-expected' },
110
+ { mutantId: 'M14-diagnostic', caseId: 'RB86', assertionId: 'RB86.result.diagnostic-codes' },
111
+ { mutantId: 'M16-trace', caseId: 'RB26', assertionId: 'RB26.conformance.trace-matches-expected' },
112
+ { mutantId: 'M19-eager', caseId: 'RB32', assertionId: 'RB32.C5.prd-enumeration-scoped' },
113
+ { mutantId: 'M20-partial-revision', caseId: 'RB70', assertionId: 'RB70.result.diagnostic-codes' },
114
+ { mutantId: 'M20-unknown-scope', caseId: 'RB34', assertionId: 'RB34.result.observation-outcome' },
115
+ ].flatMap(variant => Object.entries(distributionTargets(variant.mutantId)).map(([distribution, target]) => ({
116
+ label: `${variant.mutantId} ${distribution} variant`,
117
+ target,
118
+ test: DOC_EVIDENCE_TEST,
119
+ ...variant,
120
+ })));
121
+ // New scope mutants (plan §3). Their coverage lives here, not in cases.json, which stays pinned to the reviewed corpus.
122
+ const NEW_SCOPE_COVERAGE = {
123
+ 'B01-r5-cross-obligation': { caseId: 'RB20', assertionId: 'RB20.R5.per-obligation-sha-reads' },
124
+ 'B02-r3-per-destination-collapse': { caseId: 'RB16', assertionId: 'RB16.R3.shared-destination-reads' },
125
+ 'B02-observation': { caseId: 'RB17', assertionId: 'RB17.result.observation-outcome' },
126
+ 'B03-r3-cross-obligation': { caseId: 'RB107', assertionId: 'RB107.result.observation-outcome' },
127
+ 'B04-leak': { caseId: 'CAPTURE-B04', assertionId: 'CAPTURE-B04.detachment.no-input-references', test: 'ci/mechanism-341-capture-module.test.js' },
128
+ 'B04-mutable-view': { caseId: 'CAPTURE-B04', assertionId: 'CAPTURE-B04.detachment.views-fresh-copies', test: 'ci/mechanism-341-capture-module.test.js' },
129
+ };
130
+ const NEW_SCOPE_CHECKS = Object.entries(NEW_SCOPE_COVERAGE).flatMap(([mutantId, mapping]) =>
131
+ Object.entries(distributionTargets(mutantId)).map(([distribution, target]) => ({
132
+ label: `${mutantId} ${distribution} scope mutant`,
133
+ target,
134
+ mutantId,
135
+ test: mapping.test || DOC_EVIDENCE_TEST,
136
+ caseId: mapping.caseId,
137
+ assertionId: mapping.assertionId,
138
+ }))
139
+ );
140
+ // C3 checker-targeted tuples: the boundary checker is the mutated file, and each distribution's control runs its own
141
+ // selected assertion. The checker is one file, so both distributions share the mutant (plan §3: no packaged claim).
142
+ const BOUNDARY_ENFORCEMENT_TEST = 'ci/mechanism-341-boundary-enforcement.test.js';
143
+ const BOUNDARY_CHECKER_TARGET = 'ci/lib/doc-evidence-boundary-check.js';
144
+ const CHECKER_COVERAGE = {
145
+ 'C01-alias-rule-off': {
146
+ canonical: 'C1-N01d-canonical.boundary.intended-code-CONTEXT_ALIAS_OR_COMPUTED_USE',
147
+ packaged: 'C1-N01d-packaged.boundary.intended-code-CONTEXT_ALIAS_OR_COMPUTED_USE',
148
+ },
149
+ 'C02-destructure-rule-off': {
150
+ canonical: 'C1-N02c-canonical.boundary.intended-rule-EVALUATOR-CONTEXT-EXACT',
151
+ packaged: 'C1-N02c-packaged.boundary.intended-rule-EVALUATOR-CONTEXT-EXACT',
152
+ },
153
+ 'C03-return-rule-off': {
154
+ canonical: 'C1-N03-canonical.boundary.intended-rule-RETURNED-VIEW-OWNERSHIP',
155
+ packaged: 'C1-N03-packaged.boundary.intended-rule-RETURNED-VIEW-OWNERSHIP',
156
+ },
157
+ 'C04-exception-rule-off': {
158
+ canonical: 'C2-N05-canonical.boundary.ast-intended-rule-COMP-C2-BOUND',
159
+ packaged: 'C2-N05-packaged.boundary.ast-intended-rule-COMP-C2-BOUND',
160
+ },
161
+ };
162
+ const CHECKER_CHECKS = Object.entries(CHECKER_COVERAGE).flatMap(([mutantId, assertions]) =>
163
+ Object.entries(assertions).map(([distribution, assertionId]) => ({
164
+ label: `${mutantId} ${distribution} checker-target control`,
165
+ target: BOUNDARY_CHECKER_TARGET,
166
+ mutantId,
167
+ test: BOUNDARY_ENFORCEMENT_TEST,
168
+ caseId: assertionId.slice(0, assertionId.indexOf('.boundary.')),
169
+ assertionId,
170
+ }))
171
+ );
172
+ // Existing corpus kills for the C0 downgrade gaps (G2a-G2d). Each is registered as an explicit (c) tuple.
173
+ const DOWNGRADE_COVERAGE = {
174
+ 'G2a-unusable-destination-accepted': { caseId: 'RB07', assertionId: 'RB07.result.observation-outcome' },
175
+ 'G2b-ordinary-capture-throw-swallowed': { caseId: 'RB23', assertionId: 'RB23.result.observation-outcome' },
176
+ 'G2c-baseline-path-throw-as-absent': { caseId: 'RB12', assertionId: 'RB12.result.diagnostic-codes' },
177
+ 'G2d-entry-metadata-throw-as-valid': { caseId: 'RB11', assertionId: 'RB11.conformance.output-matches-expected' },
178
+ 'G1-unvalidated-baseline-identity': { caseId: 'RB108', assertionId: 'RB108.conformance.output-matches-expected' },
179
+ };
180
+ const DOWNGRADE_CHECKS = Object.entries(DOWNGRADE_COVERAGE).flatMap(([mutantId, mapping]) =>
181
+ Object.entries(distributionTargets(mutantId)).map(([distribution, target]) => ({
182
+ label: `${mutantId} ${distribution} downgrade mutant`,
183
+ target,
184
+ mutantId,
185
+ test: DOC_EVIDENCE_TEST,
186
+ caseId: mapping.caseId,
187
+ assertionId: mapping.assertionId,
188
+ }))
189
+ );
190
+ const CHECKS = [...LEGACY_CHECKS, ...DOC_EVIDENCE_CHECKS, ...DOC_EVIDENCE_VARIANTS, ...NEW_SCOPE_CHECKS, ...CHECKER_CHECKS, ...DOWNGRADE_CHECKS];
191
+
192
+ const LEGACY_ASSERTIONS = {
193
+ [`${LEGACY_CASES.pathBoundary}.symlink-component-rejected`]: {
194
+ kind: 'helper', label: '2v. a junction/symlink inside the boundary cannot escape it',
195
+ },
196
+ [`${LEGACY_CASES.executionContract}.extra-arguments-rejected`]: {
197
+ kind: 'helper', label: '2v. extra command arguments are not silently accepted',
198
+ },
199
+ [`${LEGACY_CASES.evidenceTool}.archive-link-rejected`]: {
200
+ kind: 'output',
201
+ pass: /^Behavioral evidence-triage regressions passed\.$/,
202
+ fail: /^FAIL archive rejects an output junction or symlink escape(?:$|:)/,
203
+ },
204
+ [`${LEGACY_CASES.scaffold}.linked-legacy-source-rejected`]: {
205
+ kind: 'helper', label: '2l. legacy migration rejects a linked source before copying or deleting outside files',
206
+ },
207
+ [`${LEGACY_CASES.installer}.global-install-from-non-git-directory`]: {
208
+ kind: 'helper', label: '2q. global installer works from a non-git directory',
209
+ },
210
+ [`${LEGACY_CASES.transcript}.command-qualifier`]: {
211
+ kind: 'helper', label: '2t. {"type":"tool_executed","command":"npm test"} does not match another completed command',
212
+ },
213
+ [`${LEGACY_CASES.behavioralRunner}.fixture-traversal-rejected`]: {
214
+ kind: 'helper', label: '2j. behavioral fixtures reject traversal for the expected boundary reason',
215
+ },
216
+ [`${LEGACY_CASES.workspace}.stable-unbound-workspace-key`]: {
217
+ kind: 'output',
218
+ pass: /^PASS 2u\. no-context non-git calls use one stable unbound workspace key$/,
219
+ fail: /^FAIL 2u\. no-context non-git calls use one stable unbound workspace key$/,
220
+ },
221
+ [`${LEGACY_CASES.submodule}.unpublished-submodule-commit-rejected`]: {
222
+ kind: 'assert', method: 'strictEqual', messageIndex: 2,
223
+ message: 'an unpublished linked-worktree submodule commit must fail',
224
+ },
225
+ [`${LEGACY_CASES.verifyGate}.authoritative-contract-precedence`]: {
226
+ kind: 'assert', method: 'strictEqual',
227
+ callSite: { file: 'mechanism-35-verification-contract.test.js', line: 198 },
228
+ },
229
+ [`${LEGACY_CASES.skillRouting}.tdd-positive-relevance`]: {
230
+ kind: 'output',
231
+ // Paths print with the host separator, so accept both / and \ (Windows).
232
+ pass: /^PASS tdd[\\/][^:]+: skill-specific relevance proven without pre-step path leaks$/,
233
+ fail: /^FAIL evals[\\/]tdd[\\/]tasks[\\/][^:]+: skill is not a candidate step binding(?: \(got:.*\))?$/,
234
+ },
235
+ };
236
+
237
+ function isWithin(root, candidate) {
238
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
239
+ return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
240
+ }
241
+
242
+ function resolveTupleCaseId(check) {
243
+ return check.caseId || `LEGACY.${path.basename(check.test).replace(/\.test\.js$|\.js$/i, '')}`;
244
+ }
245
+
246
+ // A private module contract names its case as CAPTURE-B<nn> and its assertion as CAPTURE-B<nn>.detachment.<contract>.
247
+ // It is valid only in the capture-module test, and is checked by the same event protocol as RB assertions.
248
+ // The enforcement suite's C1 (isolation) and C2 (ast) assertions are private contracts too: they emit the same event
249
+ // protocol, and their case IDs are scoped by prefix. A checker-target tuple reaches them through this predicate only.
250
+ function isPrivateContract(check) {
251
+ if (check.test === 'ci/mechanism-341-boundary-enforcement.test.js') {
252
+ return /^C[12]-[A-Za-z0-9-]+$/.test(check.caseId) && check.assertionId.startsWith(`${check.caseId}.`);
253
+ }
254
+ return check.test === 'ci/mechanism-341-capture-module.test.js'
255
+ && /^CAPTURE-B\d{2}$/.test(check.caseId)
256
+ && /^CAPTURE-B\d{2}\.detachment\.[a-z-]+$/.test(check.assertionId);
257
+ }
258
+
259
+ function validateCheck(check, root, legacyAssertions) {
260
+ if (!check || typeof check !== 'object') throw new Error('Mutation check entry must be an object');
261
+ for (const key of ['label', 'target', 'mutantId', 'test', 'assertionId']) {
262
+ if (typeof check[key] !== 'string' || !check[key]) throw new Error(`Mutation check entry is missing ${key}`);
263
+ }
264
+ const caseId = resolveTupleCaseId(check);
265
+ if (typeof caseId !== 'string' || !caseId || !check.assertionId.startsWith(`${caseId}.`)) {
266
+ throw new Error(`Assertion ID must begin with the selected case ID: ${check.assertionId}`);
267
+ }
268
+ if (check.caseId && /^RB\d{2,}$/.test(check.caseId) && !/^RB\d{2,}\./.test(check.assertionId)) {
269
+ throw new Error(`Corpus assertion ID must be case-scoped: ${check.assertionId}`);
270
+ }
271
+ const target = path.resolve(root, check.target);
272
+ const test = path.resolve(root, check.test);
273
+ if (!isWithin(root, target) || !isWithin(root, test)) throw new Error('Mutation target and test must stay within the selected root');
274
+ if (!fs.existsSync(test) || !fs.statSync(test).isFile()) throw new Error(`Mutation test does not exist: ${test}`);
275
+ const legacy = legacyAssertions[check.assertionId];
276
+ if (!check.caseId || !/^RB\d{2,}$/.test(check.caseId)) {
277
+ // Private module contracts (plan §3, B04) are event-protocol assertions in the capture-module test, not legacy checks.
278
+ if (!legacy && !isPrivateContract(check)) throw new Error(`Legacy assertion signature is not mapped: ${check.assertionId}`);
279
+ }
280
+ return { ...check, caseId, absoluteTarget: target, absoluteTest: test, legacy };
281
+ }
282
+
283
+ function tokenizeNodeOptions(value) {
284
+ const tokens = [];
285
+ let token = '';
286
+ let quote = null;
287
+ for (let index = 0; index < value.length; index++) {
288
+ const character = value[index];
289
+ if (quote) {
290
+ if (character === quote) quote = null;
291
+ else if (character === '\\' && value[index + 1] === quote) token += value[++index];
292
+ else token += character;
293
+ } else if (character === '"' || character === "'") {
294
+ quote = character;
295
+ } else if (/\s/.test(character)) {
296
+ if (token) tokens.push(token);
297
+ token = '';
298
+ } else {
299
+ token += character;
300
+ }
301
+ }
302
+ if (token) tokens.push(token);
303
+ return tokens;
304
+ }
305
+
306
+ function quoteNodeOption(value) {
307
+ return /\s/.test(value) ? `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : value;
308
+ }
309
+
310
+ function isSamePath(left, right) {
311
+ const resolvedLeft = fs.existsSync(left) ? fs.realpathSync(left) : path.resolve(left);
312
+ const resolvedRight = fs.existsSync(right) ? fs.realpathSync(right) : path.resolve(right);
313
+ return process.platform === 'win32'
314
+ ? resolvedLeft.toLowerCase() === resolvedRight.toLowerCase()
315
+ : resolvedLeft === resolvedRight;
316
+ }
317
+
318
+ function stripLoaderPreload(nodeOptions, loaderPath) {
319
+ const tokens = tokenizeNodeOptions(nodeOptions || '');
320
+ const kept = [];
321
+ for (let index = 0; index < tokens.length; index++) {
322
+ const token = tokens[index];
323
+ if (token === '--import' && tokens[index + 1]) {
324
+ if (isSameImport(tokens[index + 1], loaderPath)) {
325
+ index++;
326
+ continue;
327
+ }
328
+ kept.push(token, tokens[++index]);
329
+ continue;
330
+ }
331
+ if (token.startsWith('--import=')) {
332
+ const imported = token.slice('--import='.length);
333
+ if (isSameImport(imported, loaderPath)) continue;
334
+ }
335
+ if ((token === '--require' || token === '-r') && tokens[index + 1]) {
336
+ if (isSamePath(tokens[index + 1], loaderPath)) {
337
+ index++;
338
+ continue;
339
+ }
340
+ kept.push(token, tokens[++index]);
341
+ continue;
342
+ }
343
+ if (token.startsWith('--require=')) {
344
+ const required = token.slice('--require='.length);
345
+ if (isSamePath(required, loaderPath)) continue;
346
+ }
347
+ if (token.startsWith('-r') && token.length > 2 && isSamePath(token.slice(2), loaderPath)) continue;
348
+ kept.push(token);
349
+ }
350
+ return kept.map(quoteNodeOption).join(' ');
351
+ }
352
+
353
+ // --import specifiers may be file: URLs; compare them as paths.
354
+ function isSameImport(specifier, loaderPath) {
355
+ if (specifier.startsWith('file:')) {
356
+ try {
357
+ return isSamePath(fileURLToPath(specifier), loaderPath);
358
+ } catch (_) {
359
+ return false;
360
+ }
361
+ }
362
+ return isSamePath(specifier, loaderPath);
363
+ }
364
+
365
+ function addLoaderPreload(nodeOptions, loaderPath) {
366
+ const cleaned = stripLoaderPreload(nodeOptions, loaderPath);
367
+ // --import resolves ESM specifiers, so a Windows drive path must be a file: URL.
368
+ const preload = `--import=${pathToFileURL(loaderPath).href}`;
369
+ return [cleaned, quoteNodeOption(preload)].filter(Boolean).join(' ');
370
+ }
371
+
372
+ function childEnvironment({ check, root, tempDir, mutation = null, testEnv = {}, allowTestMutations = false }) {
373
+ const env = { ...process.env };
374
+ for (const key of Object.keys(env)) {
375
+ if (key.startsWith('HARNESS_MUTATION') || [
376
+ 'HARNESS_CASE_ID', 'HARNESS_ASSERTION_ID', 'HARNESS_ASSERTION_EVENT_FILE',
377
+ 'HARNESS_LEGACY_ASSERTION_CONFIG', 'HARNESS_TEST_HELPER_PATH',
378
+ ].includes(key)) delete env[key];
379
+ }
380
+ const cleanNodeOptions = stripLoaderPreload(env.NODE_OPTIONS, LOADER);
381
+ if (cleanNodeOptions) env.NODE_OPTIONS = cleanNodeOptions;
382
+ else delete env.NODE_OPTIONS;
383
+ for (const [key, value] of Object.entries(testEnv)) {
384
+ if (key.startsWith('HARNESS_MUTATION') || [
385
+ 'HARNESS_CASE_ID', 'HARNESS_ASSERTION_ID', 'HARNESS_ASSERTION_EVENT_FILE',
386
+ 'HARNESS_LEGACY_ASSERTION_CONFIG', 'HARNESS_TEST_HELPER_PATH',
387
+ ].includes(key)) throw new Error(`Synthetic test environment cannot override runner control: ${key}`);
388
+ env[key] = value;
389
+ }
390
+
391
+ const eventFile = path.join(tempDir, mutation ? 'mutant-assertions.jsonl' : 'control-assertions.jsonl');
392
+ env.HARNESS_ASSERTION_EVENT_FILE = eventFile;
393
+ env.HARNESS_ASSERTION_ID = check.assertionId;
394
+ if (/^RB\d{2,}$/.test(check.caseId)) env.HARNESS_CASE_ID = check.caseId;
395
+
396
+ const args = [];
397
+ let tapFile = null;
398
+ if (check.legacy && (check.legacy.kind === 'helper' || check.legacy.kind === 'assert')) {
399
+ const tapDescriptor = {
400
+ ...check.legacy,
401
+ caseId: check.caseId,
402
+ assertionId: check.assertionId,
403
+ };
404
+ env.HARNESS_LEGACY_ASSERTION_CONFIG = JSON.stringify(tapDescriptor);
405
+ if (check.legacy.kind === 'helper') env.HARNESS_TEST_HELPER_PATH = path.join(root, 'ci', 'test-helper.js');
406
+ tapFile = path.join(tempDir, 'assertion-tap.js');
407
+ fs.writeFileSync(tapFile, assertionTapSource(), 'utf8');
408
+ args.push('--require', tapFile);
409
+ }
410
+
411
+ if (mutation) {
412
+ const evidenceFile = path.join(tempDir, 'mutation-loader.jsonl');
413
+ env.HARNESS_MUTATION = check.mutantId;
414
+ env.HARNESS_MUTATION_TARGET = check.absoluteTarget;
415
+ env.HARNESS_MUTATION_ROOT = root;
416
+ env.HARNESS_MUTATION_EVIDENCE_FILE = evidenceFile;
417
+ env.HARNESS_MUTATION_EXPECT_SOURCE_HASH = mutation.sourceHash;
418
+ env.HARNESS_MUTATION_EXPECT_TRANSFORMED_HASH = mutation.transformedHash;
419
+ if (allowTestMutations) env.HARNESS_MUTATION_TEST_MODE = '1';
420
+ env.HARNESS_MUTATION_LOADER_PRELOAD = '1';
421
+ env.NODE_OPTIONS = addLoaderPreload(env.NODE_OPTIONS, LOADER);
422
+ }
423
+ args.push(check.absoluteTest);
424
+ return { env, args, eventFile, evidenceFile: mutation ? env.HARNESS_MUTATION_EVIDENCE_FILE : null, tapFile };
425
+ }
426
+
427
+ function assertionTapSource() {
428
+ return String.raw`
429
+ 'use strict';
430
+ const fs = require('fs');
431
+ const config = JSON.parse(process.env.HARNESS_LEGACY_ASSERTION_CONFIG);
432
+ const eventFile = process.env.HARNESS_ASSERTION_EVENT_FILE;
433
+ function emit(failed) {
434
+ const record = {
435
+ schemaVersion: 1,
436
+ event: failed ? 'assertion-failure' : 'assertion-pass',
437
+ caseId: config.caseId,
438
+ assertionId: config.assertionId,
439
+ };
440
+ if (failed) record.errorCode = 'ERR_ASSERTION';
441
+ fs.appendFileSync(eventFile, JSON.stringify(record) + '\n', 'utf8');
442
+ }
443
+ if (config.kind === 'helper') {
444
+ const helper = require(process.env.HARNESS_TEST_HELPER_PATH);
445
+ const original = helper.check;
446
+ helper.check = function checkWithMutationEvent(name, condition, ...rest) {
447
+ if (name === config.label) emit(!condition);
448
+ return original.call(this, name, condition, ...rest);
449
+ };
450
+ } else if (config.kind === 'assert') {
451
+ const assert = require('assert');
452
+ const original = assert[config.method];
453
+ if (typeof original !== 'function') throw new Error('Unknown legacy assert method: ' + config.method);
454
+ assert[config.method] = function assertWithMutationEvent(...args) {
455
+ const selectedCallSite = config.callSite && (new Error().stack || '').split(/\r?\n/).some(line =>
456
+ line.includes(config.callSite.file + ':' + config.callSite.line + ':'));
457
+ const selectedAssertion = config.callSite ? selectedCallSite : args[config.messageIndex] === config.message;
458
+ if (!selectedAssertion) return original.apply(this, args);
459
+ try {
460
+ const result = original.apply(this, args);
461
+ emit(false);
462
+ return result;
463
+ } catch (error) {
464
+ if (error && error.code === 'ERR_ASSERTION') emit(true);
465
+ throw error;
466
+ }
467
+ };
468
+ }
469
+ `;
470
+ }
471
+
472
+ function runChild({ check, root, tempDir, mutation = null, testEnv, allowTestMutations, timeoutMs }) {
473
+ const invocation = childEnvironment({ check, root, tempDir, mutation, testEnv, allowTestMutations });
474
+ const result = spawnSync(process.execPath, invocation.args, {
475
+ cwd: root,
476
+ env: invocation.env,
477
+ encoding: 'utf8',
478
+ windowsHide: true,
479
+ timeout: timeoutMs,
480
+ maxBuffer: 8 * 1024 * 1024,
46
481
  });
47
- if (result.status === 0) {
48
- console.error(`FAIL mutation survived: ${label} (${target})`);
49
- failures++;
50
- } else {
51
- console.log(`PASS mutation was caught: ${label} (${testFile})`);
482
+ return {
483
+ ...result,
484
+ eventFile: invocation.eventFile,
485
+ evidenceFile: invocation.evidenceFile,
486
+ output: `${result.stdout || ''}\n${result.stderr || ''}`,
487
+ };
488
+ }
489
+
490
+ function readJsonLines(file) {
491
+ if (!fs.existsSync(file)) return [];
492
+ const text = fs.readFileSync(file, 'utf8');
493
+ const events = [];
494
+ for (const line of text.split(/\r?\n/)) {
495
+ if (!line.trim()) continue;
496
+ try {
497
+ events.push(JSON.parse(line));
498
+ } catch (error) {
499
+ return { error: `malformed JSONL: ${error.message}` };
500
+ }
501
+ }
502
+ return events;
503
+ }
504
+
505
+ function isAssertionEvent(event) {
506
+ if (!event || event.schemaVersion !== 1 || typeof event.caseId !== 'string' || !event.caseId ||
507
+ typeof event.assertionId !== 'string' || !event.assertionId.startsWith(`${event.caseId}.`)) return false;
508
+ if (event.event === 'assertion-pass') return true;
509
+ return event.event === 'assertion-failure' && event.errorCode === 'ERR_ASSERTION';
510
+ }
511
+
512
+ function matchingAssertionEvents(events, check) {
513
+ if (!Array.isArray(events) || events.some(event => !isAssertionEvent(event))) return { error: 'invalid assertion event' };
514
+ const matching = events.filter(event => event.caseId === check.caseId && event.assertionId === check.assertionId);
515
+ const failures = events.filter(event => event.event === 'assertion-failure');
516
+ return { matching, failures };
517
+ }
518
+
519
+ function outputLines(output) {
520
+ return output.split(/\r?\n/).map(line => line.trimEnd());
521
+ }
522
+
523
+ function checkerDiagnosticCodes(result, check) {
524
+ if (check.test !== 'ci/mechanism-341-boundary-enforcement.test.js' || !check.caseId.startsWith('C1-')) return [];
525
+ const evidencePrefix = `EVIDENCE ${check.caseId} `;
526
+ const evidence = outputLines(result.output).find(line => line.startsWith(evidencePrefix));
527
+ const match = evidence && /\bdiagnostics=([^\s]+)/.exec(evidence);
528
+ if (!match || match[1] === 'none') return [];
529
+ return [...new Set(match[1].split(',')
530
+ .map(diagnostic => diagnostic.split('/')[0])
531
+ .filter(code => /^[A-Z][A-Z0-9_]*$/.test(code)))];
532
+ }
533
+
534
+ function controlFailureReason(result, check, reason) {
535
+ const codes = checkerDiagnosticCodes(result, check);
536
+ const details = codes.length ? [`checker diagnostic code(s)=${codes.join(',')}`] : [];
537
+ const events = readJsonLines(result.eventFile);
538
+ if (Array.isArray(events)) {
539
+ for (const event of events.filter(item => isAssertionEvent(item) && item.event === 'assertion-failure').slice(0, 5)) {
540
+ details.push(`control assertion ${event.assertionId}: ${String(event.message || event.errorCode).slice(0, 500)}`);
541
+ }
542
+ }
543
+ // An exception outside assertion() has no failure event. Preserve its header and
544
+ // first callsite rather than mistaking an earlier selected PASS for its cause.
545
+ const lines = outputLines(result.output);
546
+ const errors = lines.filter(line => /^FAIL(?:\s|$)|^[A-Za-z]*Error(?:\s|\[|:)/.test(line)).slice(0, 3);
547
+ if (errors.length) {
548
+ const frame = lines.find(line => /^\s+at /.test(line));
549
+ details.push(`control output (selected ${check.assertionId}): ${[...errors, frame].filter(Boolean).join(' | ').slice(0, 1500)}`);
550
+ }
551
+ return [reason, ...details].join('; ');
552
+ }
553
+
554
+ function regexMatches(lines, matcher) {
555
+ return lines.filter(line => {
556
+ matcher.lastIndex = 0;
557
+ return matcher.test(line);
558
+ });
559
+ }
560
+
561
+ function evaluateControl(result, check) {
562
+ if (result.error || result.signal || result.status !== 0) {
563
+ return { passed: false, reason: controlFailureReason(result, check,
564
+ `control process status=${result.status} signal=${result.signal || 'none'}${result.error ? ` error=${result.error.message}` : ''}`) };
565
+ }
566
+ if (check.legacy && check.legacy.kind === 'output') {
567
+ const lines = outputLines(result.output);
568
+ const pass = regexMatches(lines, check.legacy.pass);
569
+ const failures = lines.filter(line => /^FAIL(?:\s|$)/.test(line));
570
+ return pass.length > 0 && failures.length === 0
571
+ ? { passed: true }
572
+ : { passed: false, reason: controlFailureReason(result, check,
573
+ `expected legacy pass signature; pass hits=${pass.length}, failure lines=${failures.length}`) };
574
+ }
575
+ const events = readJsonLines(result.eventFile);
576
+ if (!Array.isArray(events)) return { passed: false, reason: controlFailureReason(result, check, events.error) };
577
+ const selected = matchingAssertionEvents(events, check);
578
+ if (selected.error) return { passed: false, reason: controlFailureReason(result, check, selected.error) };
579
+ if (selected.matching.length !== 1 || selected.matching[0].event !== 'assertion-pass' || selected.failures.length) {
580
+ return { passed: false, reason: controlFailureReason(result, check,
581
+ `expected one passing ${check.caseId}/${check.assertionId} event`) };
582
+ }
583
+ return { passed: true };
584
+ }
585
+
586
+ // Finds uncaught Node exceptions: an Error header line with stack frames after it. A multi-line message pushes
587
+ // the frames down, so the frames are searched in the lines that follow the header.
588
+ function uncaughtErrors(output) {
589
+ const lines = outputLines(output);
590
+ const found = [];
591
+ lines.forEach((line, index) => {
592
+ const header = /^([A-Za-z]*Error)(?: \[([A-Z0-9_]+)\])?: (.*)$/.exec(line);
593
+ if (!header) return;
594
+ const nearby = lines.slice(index + 1, index + 61);
595
+ if (nearby.some(next => /^\s+at \S/.test(next))) found.push({ name: header[1], code: header[2], message: header[3] });
596
+ });
597
+ return found;
598
+ }
599
+
600
+ // The one uncaught exception a selected check may end with. A legacy Node assert failure throws its own
601
+ // AssertionError, so that exact error is the intended failure. Every other kind fails through the event file or a
602
+ // status 1 exit with no stack, so any uncaught exception from it is unrelated to the selected assertion.
603
+ function expectedUncaughtError(check) {
604
+ if (check.legacy && check.legacy.kind === 'assert') {
605
+ return error => error.name === 'AssertionError' && error.code === 'ERR_ASSERTION' &&
606
+ (check.legacy.message === undefined || error.message === check.legacy.message);
607
+ }
608
+ return null;
609
+ }
610
+
611
+ function evaluateMutated(result, check) {
612
+ const processNormal = !result.error && !result.signal && (result.status === 0 || result.status === 1);
613
+ if (!processNormal) return { verdict: 'INFRA_MUTATED', intendedAssertion: 'MISSING', reason: `mutated process status=${result.status} signal=${result.signal || 'none'}${result.error ? ` error=${result.error.message}` : ''}` };
614
+ // Crash screening applies to every kind (owner review P2-2), so an unrelated uncaught exception cannot be credited.
615
+ const uncaught = uncaughtErrors(result.output);
616
+ const allowed = expectedUncaughtError(check);
617
+ if (uncaught.length > 0 && !(allowed && uncaught.length === 1 && allowed(uncaught[0]))) {
618
+ const [first] = uncaught;
619
+ return { verdict: 'INFRA_MUTATED', intendedAssertion: 'MISSING', reason: `uncaught Node exception unrelated to the selected assertion: ${first.name}${first.code ? ` [${first.code}]` : ''}` };
620
+ }
621
+
622
+ if (check.legacy && check.legacy.kind === 'output') {
623
+ const lines = outputLines(result.output);
624
+ const expected = regexMatches(lines, check.legacy.fail);
625
+ const failLines = lines.filter(line => /^FAIL(?:\s|$)/.test(line));
626
+ const unrelated = failLines.filter(line => {
627
+ check.legacy.fail.lastIndex = 0;
628
+ return !check.legacy.fail.test(line);
629
+ });
630
+ if (result.status === 1 && expected.length > 0 && unrelated.length === 0) {
631
+ return { verdict: 'KILLED', intendedAssertion: 'FAIL' };
632
+ }
633
+ if (result.status === 0 && expected.length === 0) return { verdict: 'SURVIVED', intendedAssertion: 'PASS' };
634
+ return { verdict: 'INFRA_MUTATED', intendedAssertion: expected.length ? 'FAIL' : 'MISSING', reason: `expected legacy failure signature; hits=${expected.length}, unrelated failures=${unrelated.length}, status=${result.status}` };
635
+ }
636
+
637
+ const events = readJsonLines(result.eventFile);
638
+ if (!Array.isArray(events)) return { verdict: 'INFRA_MUTATED', intendedAssertion: 'MISSING', reason: events.error };
639
+ const selected = matchingAssertionEvents(events, check);
640
+ if (selected.error) return { verdict: 'INFRA_MUTATED', intendedAssertion: 'MISSING', reason: selected.error };
641
+ if (selected.matching.length === 1 && selected.matching[0].event === 'assertion-failure' &&
642
+ result.status === 1) {
643
+ return { verdict: 'KILLED', intendedAssertion: 'FAIL' };
644
+ }
645
+ if (selected.matching.length === 1 && selected.matching[0].event === 'assertion-pass' &&
646
+ result.status === 0 && selected.failures.length === 0) {
647
+ return { verdict: 'SURVIVED', intendedAssertion: 'PASS' };
52
648
  }
649
+ return {
650
+ verdict: 'INFRA_MUTATED',
651
+ intendedAssertion: selected.matching.length ? selected.matching[0].event === 'assertion-failure' ? 'FAIL' : 'PASS' : 'MISSING',
652
+ reason: `no unique intended failure; selected events=${selected.matching.length}, failures=${selected.failures.length}, status=${result.status}`,
653
+ };
654
+ }
655
+
656
+ function readLoaderEvidence(file, check, mutation) {
657
+ const records = readJsonLines(file);
658
+ if (!Array.isArray(records) || records.length === 0) return { loaded: false, reason: Array.isArray(records) ? 'no selected target load evidence' : records.error };
659
+ for (const evidence of records) {
660
+ const targetMatches = typeof evidence.target === 'string' && isSamePath(evidence.target, check.absoluteTarget);
661
+ const hashesMatch = evidence.sourceHash === mutation.sourceHash && evidence.transformedHash === mutation.transformedHash &&
662
+ SHA256.test(evidence.sourceHash || '') && SHA256.test(evidence.transformedHash || '');
663
+ if (!targetMatches || evidence.mutantId !== check.mutantId || evidence.applied !== true || evidence.parses !== true || !hashesMatch) {
664
+ return { loaded: false, reason: 'loader evidence does not match the selected target, mutant, transform, or source hashes' };
665
+ }
666
+ }
667
+ return { loaded: true, records };
668
+ }
669
+
670
+ function failedReport(check, verdict, reason, fields = {}) {
671
+ return {
672
+ label: check.label,
673
+ target: check.target,
674
+ mutantId: check.mutantId,
675
+ test: check.test,
676
+ caseId: check.caseId,
677
+ assertionId: check.assertionId,
678
+ control: fields.control || 'NOT_RUN',
679
+ applied: fields.applied === true,
680
+ parses: fields.parses === true,
681
+ loaded: fields.loaded === true,
682
+ intendedAssertion: fields.intendedAssertion || 'NOT_REACHED',
683
+ verdict,
684
+ reason,
685
+ };
53
686
  }
54
687
 
55
- if (failures) {
56
- console.error(`Mutation check failed: ${failures} mutation(s) survived.`);
57
- process.exit(1);
688
+ function runCheck(rawCheck, {
689
+ root = ROOT,
690
+ allowTestMutations = false,
691
+ timeoutMs = 60000,
692
+ testEnv = {},
693
+ legacyAssertions = LEGACY_ASSERTIONS,
694
+ } = {}) {
695
+ const absoluteRoot = path.resolve(root);
696
+ let check;
697
+ try {
698
+ check = validateCheck(rawCheck, absoluteRoot, legacyAssertions);
699
+ } catch (error) {
700
+ return failedReport(rawCheck || {}, 'INVALID_CHECK', error.message);
701
+ }
702
+
703
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-mutation-run-'));
704
+ try {
705
+ const controlResult = runChild({ check, root: absoluteRoot, tempDir, testEnv, allowTestMutations, timeoutMs });
706
+ const control = evaluateControl(controlResult, check);
707
+ if (!control.passed) return failedReport(check, 'CONTROL_FAILED', control.reason, { control: 'FAIL' });
708
+
709
+ let mutation;
710
+ try {
711
+ mutation = prepareMutation({
712
+ root: absoluteRoot,
713
+ target: check.target,
714
+ mutantId: check.mutantId,
715
+ allowTestMutations,
716
+ });
717
+ } catch (error) {
718
+ return failedReport(check, 'INVALID_MUTANT', error.message, { control: 'PASS' });
719
+ }
720
+ if (!mutation.applied) return failedReport(check, 'INVALID_MUTANT', 'transform did not change source bytes', { control: 'PASS', applied: false, parses: mutation.parses });
721
+ if (!mutation.parses) return failedReport(check, 'INVALID_MUTANT', `transformed source does not parse: ${mutation.parseError}`, { control: 'PASS', applied: true, parses: false });
722
+
723
+ const mutantResult = runChild({
724
+ check,
725
+ root: absoluteRoot,
726
+ tempDir,
727
+ mutation,
728
+ testEnv,
729
+ allowTestMutations,
730
+ timeoutMs,
731
+ });
732
+ const loader = readLoaderEvidence(mutantResult.evidenceFile, check, mutation);
733
+ if (!loader.loaded) {
734
+ const detail = outputLines(mutantResult.output).filter(Boolean).slice(-5).join(' | ').slice(0, 700);
735
+ const processDetail = `child status=${mutantResult.status} signal=${mutantResult.signal || 'none'}${mutantResult.error ? ` error=${mutantResult.error.message}` : ''}${detail ? ` output=${detail}` : ''}`;
736
+ return failedReport(check, 'UNLOADED', `${loader.reason}; ${processDetail}`, { control: 'PASS', applied: true, parses: true, loaded: false });
737
+ }
738
+
739
+ const assessment = evaluateMutated(mutantResult, check);
740
+ return failedReport(check, assessment.verdict, assessment.reason || 'qualified', {
741
+ control: 'PASS',
742
+ applied: true,
743
+ parses: true,
744
+ loaded: true,
745
+ intendedAssertion: assessment.intendedAssertion,
746
+ });
747
+ } catch (error) {
748
+ return failedReport(check, 'INFRASTRUCTURE_ERROR', error && error.message ? error.message : String(error));
749
+ } finally {
750
+ fs.rmSync(tempDir, { recursive: true, force: true });
751
+ }
58
752
  }
59
- console.log(`Mutation check passed: ${checks.length} production mutations were rejected by focused tests.`);
753
+
754
+ function runChecks({
755
+ checks = CHECKS,
756
+ root = ROOT,
757
+ allowTestMutations = false,
758
+ timeoutMs = 60000,
759
+ testEnv = {},
760
+ legacyAssertions = LEGACY_ASSERTIONS,
761
+ } = {}) {
762
+ return checks.map(check => runCheck(check, { root, allowTestMutations, timeoutMs, testEnv, legacyAssertions }));
763
+ }
764
+
765
+ function printReport(report) {
766
+ const intended = report.intendedAssertion === 'NOT_REACHED' ? 'not-reached' : report.intendedAssertion.toLowerCase();
767
+ console.log(`${report.label}: control=${report.control.toLowerCase()} applied=${report.applied} parses=${report.parses} loaded=${report.loaded} intended=${intended} verdict=${report.verdict}`);
768
+ if (report.reason && report.verdict !== 'KILLED') console.error(` ${report.reason}`);
769
+ }
770
+
771
+ function selectChecks(argv) {
772
+ const filters = [];
773
+ for (let index = 0; index < argv.length; index++) {
774
+ const arg = argv[index];
775
+ if (arg === '--filter' && argv[index + 1]) filters.push(argv[++index]);
776
+ else if (arg.startsWith('--filter=')) filters.push(arg.slice('--filter='.length));
777
+ else throw new Error(`Unknown mutation-check argument: ${arg}`);
778
+ }
779
+ if (!filters.length) return CHECKS;
780
+ const selected = CHECKS.filter(check => filters.some(filter => check.mutantId === filter || check.label === filter));
781
+ if (!selected.length) throw new Error(`No mutation tuple matched filter: ${filters.join(', ')}`);
782
+ return selected;
783
+ }
784
+
785
+ function main(argv = process.argv.slice(2)) {
786
+ let selected;
787
+ try {
788
+ selected = selectChecks(argv);
789
+ } catch (error) {
790
+ console.error(error.message);
791
+ process.exitCode = 2;
792
+ return;
793
+ }
794
+ console.log(`Running ${selected.length} qualified mutation tuple(s)...`);
795
+ const reports = runChecks({ checks: selected });
796
+ reports.forEach(printReport);
797
+ const failures = reports.filter(report => report.verdict !== 'KILLED');
798
+ if (failures.length) {
799
+ console.error(`Mutation check failed: ${failures.length} tuple(s) did not qualify.`);
800
+ process.exitCode = 1;
801
+ } else {
802
+ console.log(`Mutation check passed: ${reports.length} production mutant(s) were rejected by their intended assertions.`);
803
+ }
804
+ }
805
+
806
+ if (require.main === module) main();
807
+
808
+ module.exports = {
809
+ CHECKS,
810
+ isPrivateContract,
811
+ LEGACY_ASSERTIONS,
812
+ LEGACY_CASES,
813
+ runCheck,
814
+ runChecks,
815
+ stripLoaderPreload,
816
+ evaluateControl,
817
+ evaluateMutated,
818
+ addLoaderPreload,
819
+ };