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,69 +1,556 @@
1
1
  'use strict';
2
2
 
3
+ const crypto = require('crypto');
3
4
  const fs = require('fs');
4
5
  const Module = require('module');
5
6
  const path = require('path');
7
+ const vm = require('vm');
6
8
 
7
- const target = process.env.HARNESS_MUTATION_TARGET && path.resolve(process.env.HARNESS_MUTATION_TARGET);
8
- const mutation = process.env.HARNESS_MUTATION;
9
- const originalLoader = Module._extensions['.js'];
10
-
11
- const mutations = {
12
- 'path-boundary': (source) => source.replace(
13
- ' assertNoLinkComponents(root, candidate);',
14
- ' // mutation: skip physical link-component validation'
15
- ),
16
- 'execution-contract': (source) => source.replace(
17
- ' return left !== null && left === right;',
18
- ' return left !== null; // mutation: accept every non-null command'
19
- ),
20
- 'evidence-tool': (source) => source
21
- .replace(' const containedDestination = assertContainedPath(archiveRoot, destination).target;', ' const containedDestination = destination; // mutation: skip destination boundary')
22
- .replace(' const contained = assertContainedPath(archiveRoot, target).target;', ' const contained = target; // mutation: skip archive write boundary'),
23
- 'scaffold-migration': (source) => source.replace(
24
- ' assertContainedPath(workspaceRoot, legacyRoot);',
25
- ' // mutation: skip linked legacy-root validation'
26
- ),
27
- 'installer-global': (source) => source.replace(
28
- 'const workspaceRoot = detectedWorkspaceRoot || path.resolve(process.cwd());',
29
- 'const workspaceRoot = detectedWorkspaceRoot; // mutation: restore null global workspace'
30
- ),
31
- 'transcript-command': (source) => source.replace(
32
- ' if (target.command !== undefined && (!isObject(call.input) || !commandMatches(call.input.command, target.command))) return false;',
33
- ' // mutation: ignore command qualifiers'
34
- ),
35
- 'runner-fixture': (source) => source.replace(
36
- ' const relative = normalizeRepoPath(file && file.path);',
37
- ' const relative = file && file.path; // mutation: skip fixture path normalization'
38
- ),
39
- 'workspace-identity': (source) => source.replace(
40
- ' if (!workspaceRoot) return UNBOUND_WORKSPACE_KEY;',
41
- ' if (!workspaceRoot) return path.basename(process.cwd()); // mutation: derive unbound identity from cwd'
42
- ),
43
- 'submodule-reachability': (source) => source.replace(
44
- ' const externallyReachable = !linkedWorktree || reachableFromRemote || presentInPrimary;',
45
- ' const externallyReachable = true; // mutation: accept isolated worktree-only commits'
46
- ),
47
- 'verify-contract-precedence': (source) => source.replace(
48
- ' finalize(contract ? contractPlan(contract) : discoveredPlan());',
49
- ' finalize(discoveredPlan()); // mutation: ignore the authoritative project contract'
50
- ),
51
- 'knowledge-signal-matcher': (source) => source.replace(
52
- " if (matched && typeof group.id === 'string') {",
53
- " if (matched && typeof group.id === 'string' && group.id !== 'tdd-test') { // mutation: remove the tdd-test matcher"
54
- ),
9
+ const ROOT = path.resolve(__dirname, '..');
10
+
11
+ function replacement(anchor, value) {
12
+ return { anchor, replacement: value };
13
+ }
14
+
15
+ const DOC_EVIDENCE_CANONICAL = 'harness-everything/scripts/doc-evidence.js';
16
+ const DOC_EVIDENCE_PACKAGED = 'plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js';
17
+
18
+ function docEvidenceMutation(replacements) {
19
+ return {
20
+ target: DOC_EVIDENCE_CANONICAL,
21
+ alternateTargets: [DOC_EVIDENCE_PACKAGED],
22
+ replacements,
23
+ };
24
+ }
25
+
26
+ const DOC_EVIDENCE_CAPTURE_CANONICAL = 'harness-everything/scripts/doc-evidence-capture.js';
27
+ const DOC_EVIDENCE_CAPTURE_PACKAGED = 'plugins/harness-everything/skills/harness-everything/scripts/doc-evidence-capture.js';
28
+
29
+ // C3 checker-targeted mutants: the boundary checker is the mutated file (test/CI mechanism, not packaged runtime).
30
+ const BOUNDARY_CHECKER = 'ci/lib/doc-evidence-boundary-check.js';
31
+
32
+ function checkerMutation(replacements) {
33
+ return { target: BOUNDARY_CHECKER, replacements };
34
+ }
35
+
36
+ function docCaptureMutation(replacements) {
37
+ return {
38
+ target: DOC_EVIDENCE_CAPTURE_CANONICAL,
39
+ alternateTargets: [DOC_EVIDENCE_CAPTURE_PACKAGED],
40
+ replacements,
41
+ };
42
+ }
43
+
44
+ const MUTATIONS = {
45
+ 'path-boundary': {
46
+ target: 'scripts/lib/path-boundary.js',
47
+ replacements: [replacement(
48
+ ' assertNoLinkComponents(root, candidate);',
49
+ ' // mutation: skip physical link-component validation'
50
+ )],
51
+ },
52
+ 'execution-contract': {
53
+ target: 'scripts/lib/execution-contract.js',
54
+ replacements: [replacement(
55
+ ' return left !== null && left === right;',
56
+ ' return left !== null; // mutation: accept every non-null command'
57
+ )],
58
+ },
59
+ 'evidence-tool': {
60
+ target: 'behavioral-evals/evidence-tool.js',
61
+ replacements: [
62
+ replacement(
63
+ ' const containedDestination = assertContainedPath(archiveRoot, destination).target;',
64
+ ' const containedDestination = destination; // mutation: skip destination boundary'
65
+ ),
66
+ replacement(
67
+ ' const contained = assertContainedPath(archiveRoot, target).target;',
68
+ ' const contained = target; // mutation: skip archive write boundary'
69
+ ),
70
+ ],
71
+ },
72
+ 'scaffold-migration': {
73
+ target: 'multi-agent-workspace/scripts/scaffold.js',
74
+ replacements: [replacement(
75
+ ' assertContainedPath(workspaceRoot, legacyRoot);',
76
+ ' // mutation: skip linked legacy-root validation'
77
+ )],
78
+ },
79
+ 'installer-global': {
80
+ target: 'scripts/installer.js',
81
+ replacements: [replacement(
82
+ 'const workspaceRoot = detectedWorkspaceRoot || path.resolve(process.cwd());',
83
+ 'const workspaceRoot = detectedWorkspaceRoot; // mutation: restore null global workspace'
84
+ )],
85
+ },
86
+ 'transcript-command': {
87
+ target: 'behavioral-evals/transcript-parser.js',
88
+ replacements: [replacement(
89
+ ' if (target.command !== undefined && (!isObject(call.input) || !commandMatches(call.input.command, target.command))) return false;',
90
+ ' // mutation: ignore command qualifiers'
91
+ )],
92
+ },
93
+ 'runner-fixture': {
94
+ target: 'behavioral-evals/run.js',
95
+ replacements: [replacement(
96
+ ' const relative = normalizeRepoPath(file && file.path);',
97
+ ' const relative = file && file.path; // mutation: skip fixture path normalization'
98
+ )],
99
+ },
100
+ 'workspace-identity': {
101
+ target: 'scripts/lib/workspace.js',
102
+ replacements: [replacement(
103
+ ' if (!workspaceRoot) return UNBOUND_WORKSPACE_KEY;',
104
+ ' if (!workspaceRoot) return path.basename(process.cwd()); // mutation: derive unbound identity from cwd'
105
+ )],
106
+ },
107
+ 'submodule-reachability': {
108
+ target: 'using-git-worktrees/scripts/submodule-reachability.js',
109
+ replacements: [replacement(
110
+ ' const externallyReachable = !linkedWorktree || reachableFromRemote || presentInPrimary;',
111
+ ' const externallyReachable = true; // mutation: accept isolated worktree-only commits'
112
+ )],
113
+ },
114
+ 'verify-contract-precedence': {
115
+ target: 'harness-everything/scripts/verify-gate.js',
116
+ replacements: [replacement(
117
+ ' finalize(contract ? contractPlan(contract) : discoveredPlan());',
118
+ ' finalize(discoveredPlan()); // mutation: ignore the authoritative project contract'
119
+ )],
120
+ },
121
+ 'knowledge-signal-matcher': {
122
+ target: 'harness-everything/scripts/tier-router.js',
123
+ replacements: [replacement(
124
+ " if (matched && typeof group.id === 'string') {",
125
+ " if (matched && typeof group.id === 'string' && group.id !== 'tdd-test') { // mutation: remove the tdd-test matcher"
126
+ )],
127
+ },
128
+ // Doc-evidence mutants bind to the file that owns the operation after the 0B refactor (plan §3): captures R1-R6,
129
+ // the own-key helper, the PRD digest and the candidate-inventory predicate now live in doc-evidence-capture.js;
130
+ // compatibility policy stays in doc-evidence.js. Each mutant keeps its intended assertion; only its target moved.
131
+ M01: docCaptureMutation([replacement(
132
+ 'if (nonEmptyString(declaredBaselineDigest)) baselineDigest = declaredBaselineDigest;',
133
+ 'if (nonEmptyString(declaredBaselineDigest)) baselineDigest = baseline.digest; // mutation: re-read R1'
134
+ )]),
135
+ 'M01-stateful': docCaptureMutation([replacement(
136
+ 'if (nonEmptyString(declaredBaselineDigest)) baselineDigest = declaredBaselineDigest;',
137
+ 'if (nonEmptyString(declaredBaselineDigest)) baselineDigest = baseline.digest; // mutation: re-read R1'
138
+ )]),
139
+ M02: docCaptureMutation([replacement(
140
+ ' if (!candidateInventoryInitialized) {',
141
+ ' if (true) { // mutation: repeat the R2 prototype inspection'
142
+ )]),
143
+ M03: docCaptureMutation([replacement(
144
+ 'const digest = entry.digest;',
145
+ 'const digest = artifact(entry); // mutation: re-read the R3 destination digest'
146
+ )]),
147
+ M04: docCaptureMutation([
148
+ replacement(
149
+ ' rawReason: snapshot.reason,',
150
+ ' rawReason: snapshot.reason,\n rawEntry, // mutation: retain the raw entry for audit materialization'
151
+ ),
152
+ replacement(
153
+ ' return [...validatedBaselineDigests].map(([path, digest]) => Object.freeze([path, digest]));',
154
+ ' return [...validatedBaselineDigests].map(([path, digest]) => { const live = [...entryFacts.values()].find(fact => fact.rawEntry && fact.rawEntry.path === path); return Object.freeze([live ? live.rawEntry.path : path, digest]); }); // mutation: re-read the raw R4 entry path at audit materialization'
155
+ ),
156
+ ]),
157
+ M05: docCaptureMutation([replacement(
158
+ " const trustedBaselineDigest = typeof detachedEntry.trustedBaselineDigest === 'string'\n ? detachedEntry.trustedBaselineDigest : undefined;",
159
+ " const trustedBaselineDigest = hasOwnEntry(baselineArtifactsMap, detachedEntry.path) && typeof baselineArtifactsMap[detachedEntry.path] === 'string'\n ? baselineArtifactsMap[detachedEntry.path] : undefined; // mutation: live R5 per-path read at binding"
160
+ )]),
161
+ M06: docCaptureMutation([replacement(
162
+ ' } else if (sharedDigest !== digest) {',
163
+ ' } else if (false) { // mutation: accept disagreeing R3 snapshots'
164
+ )]),
165
+ 'M06-observation': docCaptureMutation([replacement(
166
+ ' } else if (sharedDigest !== digest) {',
167
+ ' } else if (false) { // mutation: accept disagreeing R3 snapshots'
168
+ )]),
169
+ 'M07a': docCaptureMutation([replacement(
170
+ ' return failedDestinationDigests.has(path);',
171
+ ' return false; // mutation: permit a later declaring entry to read a poisoned destination'
172
+ )]),
173
+ 'M07b': docCaptureMutation([replacement(
174
+ " if (failedDestinationDigests.has(path)) return '';",
175
+ ' // mutation: allow a cached value to outrank a poisoned destination'
176
+ )]),
177
+ 'M07b-audit-output': docCaptureMutation([replacement(
178
+ " if (failedDestinationDigests.has(path)) return '';",
179
+ ' // mutation: allow a cached value to outrank a poisoned destination'
180
+ )]),
181
+ M08: docCaptureMutation([replacement(
182
+ ' && Object.prototype.hasOwnProperty.call(map, key);',
183
+ ' && key in map; // mutation: accept inherited evidence'
184
+ )]),
185
+ M09: docCaptureMutation([replacement(
186
+ ' return [...acceptedBaselineEntries.values()].map(entry => Object.freeze({ ...entry }));',
187
+ ' return [...acceptedBaselineEntries.values()].map(entry => Object.freeze({ ...entry, candidatePathPresent: hasOwnEntry(artifactMap, entry.path) })); // mutation: re-inspect the raw candidate own membership at consumer use'
188
+ )]),
189
+ 'M09-descriptor-count': docCaptureMutation([replacement(
190
+ ' return [...acceptedBaselineEntries.values()].map(entry => Object.freeze({ ...entry }));',
191
+ ' return [...acceptedBaselineEntries.values()].map(entry => Object.freeze({ ...entry, candidatePathPresent: hasOwnEntry(artifactMap, entry.path) })); // mutation: re-inspect the raw candidate own membership at consumer use'
192
+ )]),
193
+ M10: docEvidenceMutation([replacement(
194
+ "Boolean(scopeObject && Object.prototype.hasOwnProperty.call(scopeObject, 'baselinePaths'))",
195
+ 'Boolean(scopeObject && scopeObject.baselinePaths) // mutation: treat present-undefined as absent'
196
+ )]),
197
+ 'M10-rename-to': docEvidenceMutation([replacement(
198
+ "if (entryReason === 'deleted' && entryCapture.hasOwnRenameTo) {",
199
+ "if (entryReason === 'deleted' && entryCapture.renameTo) { // mutation: use renameTo truthiness"
200
+ )]),
201
+ M11: docCaptureMutation([replacement(
202
+ ' } catch {\n snapshot = null;\n }',
203
+ ' } catch (error) {\n throw error; // mutation: let entry metadata inspection escape\n }'
204
+ )]),
205
+ 'M11-entry-exception': docCaptureMutation([replacement(
206
+ ' } catch {\n snapshot = null;\n }',
207
+ ' } catch (error) {\n throw error; // mutation: let entry metadata inspection escape\n }'
208
+ )]),
209
+ 'M11-baseline-map': docCaptureMutation([replacement(
210
+ " } catch {\n result = Object.freeze({ kind: 'unknown', reason: 'inspection' });",
211
+ " } catch (error) {\n throw error; // mutation: let baseline-map inspection escape\n result = Object.freeze({ kind: 'unknown', reason: 'inspection' });"
212
+ )]),
213
+ 'M11-candidate-descriptor': docCaptureMutation([
214
+ replacement(
215
+ " let digest = '';\n try {",
216
+ " let digest = '';\n const declared = hasOwnEntry(artifactMap, path); // mutation: let descriptor inspection escape\n try {"
217
+ ),
218
+ replacement(
219
+ ' if (hasOwnEntry(artifactMap, path)) digest = ownDigestOnce(artifactMap[path]);',
220
+ ' if (declared) digest = ownDigestOnce(artifactMap[path]);'
221
+ ),
222
+ ]),
223
+ 'M11-destination': docCaptureMutation([replacement(
224
+ " } catch {\n return Object.freeze({ kind: 'inspection-failed' });\n }\n if (!nonEmptyString(digest))",
225
+ " } catch (error) {\n throw error; // mutation: let destination inspection escape\n }\n if (!nonEmptyString(digest))"
226
+ )]),
227
+ 'M11-ordinary-capture': docCaptureMutation([replacement(
228
+ ' } catch {\n inspectionFailed = true;',
229
+ ' } catch (error) {\n throw error; // mutation: let ordinary candidate capture escape\n inspectionFailed = true;'
230
+ )]),
231
+ M12: docCaptureMutation([replacement(
232
+ ' if (candidateDigestCache.has(path)) return candidateDigestCache.get(path);',
233
+ ' if (candidateDigestCache.has(path)) return artifact(artifactMap[path]); // mutation: re-read R6'
234
+ )]),
235
+ M13: docEvidenceMutation([replacement(
236
+ ' if (!item.paths.includes(entry.renameTo)) return false;',
237
+ ' return true; // mutation: let a source-only rename record bind'
238
+ )]),
239
+ M14: docCaptureMutation([replacement(
240
+ ' return prototype === Object.prototype || prototype === null;',
241
+ ' return true; // mutation: accept every object-like inventory'
242
+ )]),
243
+ 'M14-diagnostic': docCaptureMutation([replacement(
244
+ ' return prototype === Object.prototype || prototype === null;',
245
+ ' return true; // mutation: accept every object-like inventory'
246
+ )]),
247
+ M15: docEvidenceMutation([replacement(
248
+ " if (applicability === 'applicable') {",
249
+ ' if (true) { // mutation: inspect destination metadata for every applicability state'
250
+ )]),
251
+ M16: docCaptureMutation([replacement(
252
+ " return entry && typeof entry.digest === 'string' && entry.digest.length > 0 ? entry.digest : '';",
253
+ " const digest = entry && entry.digest;\n return typeof digest === 'string' && digest.length > 0 ? digest : ''; // mutation: cache legacy digest reads"
254
+ )]),
255
+ 'M16-trace': docCaptureMutation([replacement(
256
+ " return entry && typeof entry.digest === 'string' && entry.digest.length > 0 ? entry.digest : '';",
257
+ " const digest = entry && entry.digest;\n return typeof digest === 'string' && digest.length > 0 ? digest : ''; // mutation: cache legacy digest reads"
258
+ )]),
259
+ M17: docEvidenceMutation([replacement(
260
+ "function isRecord(value) {\n return isPlainObject(value)\n && typeof value.id === 'string' && value.id.length > 0\n && typeof value.obligationId === 'string' && value.obligationId.length > 0;\n}",
261
+ "function isRecord(value) {\n if (!isPlainObject(value)) return false;\n const id = value.id;\n const obligationId = value.obligationId;\n return typeof id === 'string' && id.length > 0\n && typeof obligationId === 'string' && obligationId.length > 0; // mutation: cache record identity reads\n}"
262
+ )]),
263
+ M18: docEvidenceMutation([replacement(
264
+ ' const mismatch = item.paths.find(path => obligationCapture.compatibilityStaleArtifactMismatch(item, path));',
265
+ ' const mismatch = item.paths.filter(path => scopePaths.includes(path)).find(path => obligationCapture.compatibilityStaleArtifactMismatch(item, path)); // mutation: ignore out-of-scope stale paths'
266
+ )]),
267
+ M19: docCaptureMutation([replacement(
268
+ ".filter(path => path.startsWith('docs/prd/'))",
269
+ ".filter(path => path.startsWith('docs/prd/') && path !== 'docs/prd/extra.md') // mutation: omit an out-of-scope PRD"
270
+ )]),
271
+ 'M19-eager': docEvidenceMutation([replacement(
272
+ ' const revisionSatisfied = revisionDataKnown && citedIds.length > 0\n && citedIds.every(id => updatedIds.includes(id));',
273
+ ' capture.compatibilityPrdSetDigest(); // mutation: enumerate PRDs before the revision fast path\n const revisionSatisfied = revisionDataKnown && citedIds.length > 0\n && citedIds.every(id => updatedIds.includes(id));'
274
+ )]),
275
+ M20: docEvidenceMutation([replacement(
276
+ 'citedIds.every(id => updatedIds.includes(id))',
277
+ 'citedIds.some(id => updatedIds.includes(id)) // mutation: partial revision counts as complete'
278
+ )]),
279
+ 'M20-partial-revision': docEvidenceMutation([replacement(
280
+ 'citedIds.every(id => updatedIds.includes(id))',
281
+ 'citedIds.some(id => updatedIds.includes(id)) // mutation: partial revision counts as complete'
282
+ )]),
283
+ 'M20-unknown-scope': docEvidenceMutation([replacement(
284
+ ' if (requirementScopeUnknown) {',
285
+ ' if (false) { // mutation: ignore unknown requirement scope'
286
+ )]),
287
+ // New scope mutants (plan §3, required module-specific table). Their coverage is registered in
288
+ // ci/mutation-check.js, not cases.json, so the pinned corpus bytes stay unchanged.
289
+ 'B01-r5-cross-obligation': docCaptureMutation([
290
+ replacement(
291
+ 'function createInvocationCapture({ artifacts, baseline }) {\n let artifactMap;',
292
+ 'function createInvocationCapture({ artifacts, baseline }) {\n const sharedBaselinePathReads = new Map(); // mutation: share per-path baseline reads across obligations\n let artifactMap;'
293
+ ),
294
+ replacement(
295
+ ' const baselinePathReads = new Map();',
296
+ ' const baselinePathReads = sharedBaselinePathReads;'
297
+ ),
298
+ ]),
299
+ 'B02-r3-per-destination-collapse': docCaptureMutation([replacement(
300
+ ' captureDestination(entryToken, path) {\n void entryToken;',
301
+ " captureDestination(entryToken, path) {\n void entryToken;\n const reused = [...validatedDestinationDigestMap.values()][0]; // mutation: reuse the first destination digest\n if (reused !== undefined) return Object.freeze({ kind: 'accepted', digest: reused });"
302
+ )]),
303
+ 'B02-observation': docCaptureMutation([replacement(
304
+ ' captureDestination(entryToken, path) {\n void entryToken;',
305
+ " captureDestination(entryToken, path) {\n void entryToken;\n const reused = [...validatedDestinationDigestMap.values()][0]; // mutation: reuse the first destination digest\n if (reused !== undefined) return Object.freeze({ kind: 'accepted', digest: reused });"
306
+ )]),
307
+ 'B03-r3-cross-obligation': docCaptureMutation([
308
+ replacement(
309
+ 'function createInvocationCapture({ artifacts, baseline }) {\n let artifactMap;',
310
+ 'function createInvocationCapture({ artifacts, baseline }) {\n const sharedDestinationDigests = new Map(); // mutation: share destination agreement across evaluation tokens\n const sharedFailedDestinations = new Map(); // mutation: share destination poison across evaluation tokens\n let artifactMap;'
311
+ ),
312
+ replacement(
313
+ ' const validatedDestinationDigestMap = new Map();',
314
+ ' const validatedDestinationDigestMap = sharedDestinationDigests;'
315
+ ),
316
+ replacement(
317
+ ' const failedDestinationDigests = new Map();',
318
+ ' const failedDestinationDigests = sharedFailedDestinations;'
319
+ ),
320
+ ]),
321
+ 'B04-mutable-view': docCaptureMutation([replacement(
322
+ ' return [...acceptedBaselineEntries.values()].map(entry => Object.freeze({ ...entry }));',
323
+ ' return [...acceptedBaselineEntries.values()]; // mutation: return shared mutable views'
324
+ )]),
325
+ 'B04-leak': docCaptureMutation([replacement(
326
+ ' hasOwnRenameTo: snapshot.hasOwnRenameTo,',
327
+ ' hasOwnRenameTo: snapshot.hasOwnRenameTo,\n rawEntry, // mutation: leak the caller-owned entry into the returned view'
328
+ )]),
329
+ // C3 checker-disable tuples (plan §3): each removes one emitter in the loaded checker. Each must be killed by its
330
+ // selected enforcement assertion. C00 is a qualification-only no-op: it must survive and never enters the gate.
331
+ 'C00-noop-boundary': checkerMutation([replacement(
332
+ ' if (contextPropertyUses !== 0 || contextTokens !== 2) {',
333
+ ' // C00: no-op comment, behaviour unchanged\n if (contextPropertyUses !== 0 || contextTokens !== 2) {'
334
+ )]),
335
+ 'C01-alias-rule-off': checkerMutation([replacement(
336
+ ' if (contextPropertyUses !== 0 || contextTokens !== 2) {',
337
+ ' if (false) { // C01: alias rule off'
338
+ )]),
339
+ 'C02-destructure-rule-off': checkerMutation([replacement(
340
+ ' if (!keys || !sameStringArray(keys, contextPolicy.consumerKeys)) {',
341
+ ' if (false) { // C02: consumer destructure rule off'
342
+ )]),
343
+ 'C03-return-rule-off': checkerMutation([replacement(
344
+ " add('RETURNED_VIEW_ESCAPE', file, sample.name, 'RETURNED_VIEW_OWNERSHIP');",
345
+ ' void 0; // C03: returned-view escape rule off'
346
+ )]),
347
+ 'C04-exception-rule-off': checkerMutation([replacement(
348
+ " add('AST_TARGET_DRIFT', selected[target.module], target.name, ruleId, target.exceptionId);",
349
+ " if (!target.exceptionId) add('AST_TARGET_DRIFT', selected[target.module], target.name, ruleId, target.exceptionId); // C04: exception rule off"
350
+ )]),
351
+ // Downgrades that the existing corpus already kills but no tuple registers (C0 gaps G2a-G2d). G1 needs a new case.
352
+ 'G2a-unusable-destination-accepted': docCaptureMutation([replacement(
353
+ " if (!nonEmptyString(digest)) return Object.freeze({ kind: 'unusable' });",
354
+ " if (!nonEmptyString(digest)) return Object.freeze({ kind: 'accepted', digest }); // G2a"
355
+ )]),
356
+ 'G2b-ordinary-capture-throw-swallowed': docCaptureMutation([replacement(
357
+ ' inspectionFailed = true;',
358
+ ' void 0; // G2b: inspection failure swallowed'
359
+ )]),
360
+ 'G2c-baseline-path-throw-as-absent': docCaptureMutation([replacement(
361
+ " result = Object.freeze({ kind: 'unknown', reason: 'inspection' });",
362
+ " result = Object.freeze({ kind: 'absent' }); // G2c"
363
+ )]),
364
+ // G1 (C0 gap): a whitespace-only baseline identity is accepted as valid. Killed by the RB108 case in its own fixture.
365
+ 'G1-unvalidated-baseline-identity': docCaptureMutation([replacement(
366
+ ' if (nonEmptyString(declaredBaselineDigest)) baselineDigest = declaredBaselineDigest;',
367
+ ' baselineDigest = declaredBaselineDigest; // G1: identity not validated'
368
+ )]),
369
+ 'G2d-entry-metadata-throw-as-valid': docCaptureMutation([replacement(
370
+ ' } catch {\n snapshot = null;\n }',
371
+ ' } catch {\n snapshot = { path: undefined, reason: undefined, hasOwnRenameTo: false, renameTo: undefined }; // G2d\n }'
372
+ )]),
373
+ };
374
+
375
+ const TEST_MUTATIONS = {
376
+ 'test-noop': { target: 'synthetic-target.js', replacements: [replacement('module.exports = true;', 'module.exports = true;')] },
377
+ 'test-kill': { target: 'synthetic-target.js', replacements: [replacement('module.exports = true;', 'module.exports = false;')] },
378
+ 'test-survive': { target: 'synthetic-target.js', replacements: [replacement('module.exports = true;', 'module.exports = true; // unrelated synthetic change')] },
379
+ 'test-crash': { target: 'synthetic-target.js', replacements: [replacement('module.exports = true;', "throw new Error('synthetic module crash');")] },
380
+ 'test-syntax': { target: 'synthetic-target.js', replacements: [replacement('module.exports = true;', 'module.exports = ;')] },
381
+ 'test-exit': { target: 'synthetic-target.js', replacements: [replacement('module.exports = true;', 'process.exit(1);')] },
55
382
  };
56
383
 
57
- if (target && mutation && mutations[mutation]) {
58
- const transform = mutations[mutation];
384
+ function mutationSpec(mutantId, allowTestMutations = false) {
385
+ if (Object.prototype.hasOwnProperty.call(MUTATIONS, mutantId)) return MUTATIONS[mutantId];
386
+ if (allowTestMutations && Object.prototype.hasOwnProperty.call(TEST_MUTATIONS, mutantId)) return TEST_MUTATIONS[mutantId];
387
+ throw new Error(`Unknown mutant ID: ${mutantId}`);
388
+ }
389
+
390
+ function sha256(bytes) {
391
+ return crypto.createHash('sha256').update(bytes).digest('hex');
392
+ }
393
+
394
+ function canonicalPath(filePath) {
395
+ const absolutePath = path.resolve(filePath);
396
+ return fs.existsSync(absolutePath) ? fs.realpathSync(absolutePath) : absolutePath;
397
+ }
398
+
399
+ function isWithin(root, candidate) {
400
+ const relative = path.relative(root, candidate);
401
+ return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
402
+ }
403
+
404
+ function prepareMutation({ root = ROOT, target, mutantId, allowTestMutations = false } = {}) {
405
+ if (typeof target !== 'string' || !target) throw new Error('Mutation target must be a non-empty path');
406
+ const spec = mutationSpec(mutantId, allowTestMutations);
407
+ const absoluteRoot = path.resolve(root);
408
+ const canonicalRoot = canonicalPath(absoluteRoot);
409
+ const expectedTargets = [spec.target, ...(spec.alternateTargets || [])]
410
+ .map(candidate => canonicalPath(path.resolve(canonicalRoot, candidate)));
411
+ const requestedTarget = path.resolve(absoluteRoot, target);
412
+ if (!isWithin(canonicalRoot, canonicalPath(requestedTarget))) {
413
+ throw new Error(`Mutation target is outside the selected root: ${requestedTarget}`);
414
+ }
415
+ const actualTarget = canonicalPath(requestedTarget);
416
+ const sameTarget = expectedTargets.some(expectedTarget => process.platform === 'win32'
417
+ ? expectedTarget.toLowerCase() === actualTarget.toLowerCase()
418
+ : expectedTarget === actualTarget);
419
+ if (!sameTarget) throw new Error(`Wrong target for mutant ${mutantId}: expected ${expectedTargets.join(' or ')}, received ${actualTarget}`);
420
+ if (!fs.existsSync(actualTarget) || !fs.statSync(actualTarget).isFile()) {
421
+ throw new Error(`Mutation target does not exist as a file: ${actualTarget}`);
422
+ }
423
+
424
+ const sourceBuffer = fs.readFileSync(actualTarget);
425
+ const source = sourceBuffer.toString('utf8');
426
+ const replacements = Array.isArray(spec.replacements) ? spec.replacements : [];
427
+ for (const entry of replacements) {
428
+ if (!entry || typeof entry.anchor !== 'string' || !entry.anchor) {
429
+ throw new Error(`Mutant ${mutantId} has an invalid source anchor`);
430
+ }
431
+ const count = source.split(entry.anchor).length - 1;
432
+ if (count !== 1) {
433
+ throw new Error(`Mutant ${mutantId} expected source anchor exactly once; found ${count} in ${actualTarget}`);
434
+ }
435
+ }
436
+
437
+ let transformedSource = source;
438
+ for (const entry of replacements) transformedSource = transformedSource.replace(entry.anchor, entry.replacement);
439
+ const transformedBuffer = Buffer.from(transformedSource, 'utf8');
440
+ const applied = !sourceBuffer.equals(transformedBuffer);
441
+ let parses = false;
442
+ let parseError = null;
443
+ try {
444
+ new vm.Script(transformedSource, { filename: actualTarget });
445
+ parses = true;
446
+ } catch (error) {
447
+ parseError = error && error.message ? error.message : String(error);
448
+ }
449
+
450
+ return {
451
+ target: actualTarget,
452
+ mutantId,
453
+ applied,
454
+ parses,
455
+ sourceHash: sha256(sourceBuffer),
456
+ transformedHash: sha256(transformedBuffer),
457
+ transformedSource,
458
+ ...(parseError ? { parseError } : {}),
459
+ };
460
+ }
461
+
462
+ function appendLoaderEvidence(file, evidence) {
463
+ if (!file) throw new Error('HARNESS_MUTATION_EVIDENCE_FILE is required for loader evidence');
464
+ fs.appendFileSync(file, `${JSON.stringify(evidence)}\n`, 'utf8');
465
+ }
466
+
467
+ function installMutationLoader(env = process.env) {
468
+ const mutantId = env.HARNESS_MUTATION;
469
+ const target = env.HARNESS_MUTATION_TARGET;
470
+ if (!mutantId || !target) throw new Error('HARNESS_MUTATION and HARNESS_MUTATION_TARGET are required');
471
+
472
+ const root = path.resolve(env.HARNESS_MUTATION_ROOT || ROOT);
473
+ const canonicalRoot = canonicalPath(root);
474
+ const requestedTarget = path.resolve(target);
475
+ if (!isWithin(root, requestedTarget)) {
476
+ throw new Error(`Mutation target is outside the selected root: ${requestedTarget}`);
477
+ }
478
+ const absoluteTarget = canonicalPath(requestedTarget);
479
+ if (!isWithin(canonicalRoot, absoluteTarget)) {
480
+ throw new Error(`Mutation target resolves outside the selected root: ${absoluteTarget}`);
481
+ }
482
+ const allowTestMutations = env.HARNESS_MUTATION_TEST_MODE === '1';
483
+ const expectedSourceHash = env.HARNESS_MUTATION_EXPECT_SOURCE_HASH;
484
+ const expectedTransformedHash = env.HARNESS_MUTATION_EXPECT_TRANSFORMED_HASH;
485
+ const evidenceFile = env.HARNESS_MUTATION_EVIDENCE_FILE;
486
+ const originalLoader = Module._extensions['.js'];
487
+
59
488
  Module._extensions['.js'] = function loadWithMutation(mod, filename) {
60
- const source = fs.readFileSync(filename, 'utf8');
61
- if (path.resolve(filename) === target) {
62
- const mutated = transform(source);
63
- if (mutated === source) throw new Error(`mutation was not applied to ${filename}`);
64
- mod._compile(mutated, filename);
65
- return;
489
+ const loadedPath = canonicalPath(filename);
490
+ const isTarget = process.platform === 'win32'
491
+ ? loadedPath.toLowerCase() === absoluteTarget.toLowerCase()
492
+ : loadedPath === absoluteTarget;
493
+ if (!isTarget) return originalLoader(mod, filename);
494
+
495
+ const prepared = prepareMutation({
496
+ root,
497
+ target: absoluteTarget,
498
+ mutantId,
499
+ allowTestMutations,
500
+ });
501
+ if (!prepared.applied) throw new Error(`Mutation was not applied to ${absoluteTarget}`);
502
+ if (!prepared.parses) throw new Error(`Mutated source does not parse: ${prepared.parseError}`);
503
+ if (expectedSourceHash && prepared.sourceHash !== expectedSourceHash) {
504
+ throw new Error(`Mutation source changed after preflight for ${absoluteTarget}`);
505
+ }
506
+ if (expectedTransformedHash && prepared.transformedHash !== expectedTransformedHash) {
507
+ throw new Error(`Transformed source changed after preflight for ${absoluteTarget}`);
508
+ }
509
+
510
+ let evidenceWritten = false;
511
+ const recordEvidence = () => {
512
+ if (evidenceWritten) return;
513
+ appendLoaderEvidence(evidenceFile, {
514
+ target: absoluteTarget,
515
+ mutantId,
516
+ applied: prepared.applied,
517
+ parses: prepared.parses,
518
+ sourceHash: prepared.sourceHash,
519
+ transformedHash: prepared.transformedHash,
520
+ });
521
+ evidenceWritten = true;
522
+ };
523
+
524
+ const originalExit = process.exit;
525
+ process.exit = function exitAfterTargetLoad(...args) {
526
+ recordEvidence();
527
+ return originalExit.apply(this, args);
528
+ };
529
+ try {
530
+ mod._compile(prepared.transformedSource, filename);
531
+ recordEvidence();
532
+ } finally {
533
+ process.exit = originalExit;
66
534
  }
67
- originalLoader(mod, filename);
68
535
  };
69
536
  }
537
+
538
+ if (process.env.HARNESS_MUTATION_LOADER_PRELOAD === '1') installMutationLoader();
539
+
540
+ // Each distribution gets its own registered tuple target (canonical file and its packaged twin).
541
+ function distributionTargets(mutantId) {
542
+ const spec = mutationSpec(mutantId, false);
543
+ if (!Array.isArray(spec.alternateTargets) || spec.alternateTargets.length !== 1) {
544
+ throw new Error(`Mutant ${mutantId} does not declare exactly one packaged target`);
545
+ }
546
+ return { canonical: spec.target, packaged: spec.alternateTargets[0] };
547
+ }
548
+
549
+ module.exports = {
550
+ MUTATIONS,
551
+ TEST_MUTATIONS,
552
+ distributionTargets,
553
+ mutationSpec,
554
+ prepareMutation,
555
+ installMutationLoader,
556
+ };