harness-everything 0.40.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/.agents/harness-everything/project-docs.json +9 -0
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/CHANGELOG.md +43 -0
  5. package/benchmarks/results/f1-doc-path-baseline/1-source-clean.stderr.txt +0 -0
  6. package/benchmarks/results/f1-doc-path-baseline/1-source-clean.stdout.json +20 -0
  7. package/benchmarks/results/f1-doc-path-baseline/2-injected-architecture.md +229 -0
  8. package/benchmarks/results/f1-doc-path-baseline/2-source-injected.stderr.txt +0 -0
  9. package/benchmarks/results/f1-doc-path-baseline/2-source-injected.stdout.json +33 -0
  10. package/benchmarks/results/f1-doc-path-baseline/3-source-reverted.stderr.txt +0 -0
  11. package/benchmarks/results/f1-doc-path-baseline/3-source-reverted.stdout.json +20 -0
  12. package/benchmarks/results/f1-doc-path-baseline/4a-packaged-clean.stderr.txt +0 -0
  13. package/benchmarks/results/f1-doc-path-baseline/4a-packaged-clean.stdout.json +20 -0
  14. package/benchmarks/results/f1-doc-path-baseline/4b-packaged-injected.stderr.txt +0 -0
  15. package/benchmarks/results/f1-doc-path-baseline/4b-packaged-injected.stdout.json +33 -0
  16. package/benchmarks/results/f1-doc-path-baseline/5-source-missing-config.stderr.txt +0 -0
  17. package/benchmarks/results/f1-doc-path-baseline/5-source-missing-config.stdout.json +15 -0
  18. package/benchmarks/results/f1-doc-path-baseline/5b-packaged-missing-config.stderr.txt +0 -0
  19. package/benchmarks/results/f1-doc-path-baseline/5b-packaged-missing-config.stdout.json +15 -0
  20. package/benchmarks/results/f1-doc-path-baseline/README.md +54 -0
  21. package/benchmarks/results/f1-doc-path-baseline/before-fix/1-source-clean.stderr.txt +0 -0
  22. package/benchmarks/results/f1-doc-path-baseline/before-fix/1-source-clean.stdout.json +115 -0
  23. package/benchmarks/results/f1-doc-path-baseline/before-fix/2-injected-document.md +328 -0
  24. package/benchmarks/results/f1-doc-path-baseline/before-fix/2-source-injected.stderr.txt +0 -0
  25. package/benchmarks/results/f1-doc-path-baseline/before-fix/2-source-injected.stdout.json +127 -0
  26. package/benchmarks/results/f1-doc-path-baseline/before-fix/3-source-reverted.stderr.txt +0 -0
  27. package/benchmarks/results/f1-doc-path-baseline/before-fix/3-source-reverted.stdout.json +115 -0
  28. package/benchmarks/results/f1-doc-path-baseline/before-fix/4a-packaged-clean.stderr.txt +0 -0
  29. package/benchmarks/results/f1-doc-path-baseline/before-fix/4a-packaged-clean.stdout.json +115 -0
  30. package/benchmarks/results/f1-doc-path-baseline/before-fix/4b-packaged-injected.stderr.txt +0 -0
  31. package/benchmarks/results/f1-doc-path-baseline/before-fix/4b-packaged-injected.stdout.json +127 -0
  32. package/benchmarks/results/f1-doc-path-baseline/before-fix/5-source-missing-config.stderr.txt +0 -0
  33. package/benchmarks/results/f1-doc-path-baseline/before-fix/5-source-missing-config.stdout.json +15 -0
  34. package/benchmarks/results/f1-doc-path-baseline/before-fix/5b-packaged-missing-config.stderr.txt +0 -0
  35. package/benchmarks/results/f1-doc-path-baseline/before-fix/5b-packaged-missing-config.stdout.json +15 -0
  36. package/benchmarks/results/f1-doc-path-baseline/before-fix/6-supplementary-fixture-ok.input.md +8 -0
  37. package/benchmarks/results/f1-doc-path-baseline/before-fix/6-supplementary-fixture-ok.stdout.json +16 -0
  38. package/benchmarks/results/f1-doc-path-baseline/before-fix/README.md +34 -0
  39. package/benchmarks/results/f1-doc-path-baseline/probe-31-docs/after.json +248 -0
  40. package/benchmarks/results/f1-doc-path-baseline/probe-31-docs/before.json +254 -0
  41. package/benchmarks/results/f1-doc-path-baseline/probe-31-docs/probe.js +26 -0
  42. package/benchmarks/results/f2-doc-craft-baseline/README.md +32 -0
  43. package/benchmarks/results/f2-doc-craft-baseline/arm-f1-report.md +84 -0
  44. package/benchmarks/results/f2-doc-craft-baseline/arm-f1-task.md +21 -0
  45. package/benchmarks/results/f2-doc-craft-baseline/arm-f1.diff +19 -0
  46. package/benchmarks/results/f2-doc-craft-baseline/arm-f2-report.md +110 -0
  47. package/benchmarks/results/f2-doc-craft-baseline/arm-f2-task.md +21 -0
  48. package/benchmarks/results/f2-doc-craft-baseline/arm-f2.diff +19 -0
  49. package/benchmarks/results/f3-derived-views-baseline/README.md +34 -0
  50. package/benchmarks/results/f3-derived-views-baseline/arm-f2-report.md +43 -0
  51. package/benchmarks/results/f3-derived-views-baseline/arm-f2-task.md +21 -0
  52. package/benchmarks/results/f3-derived-views-baseline/arm-f2.diff +21 -0
  53. package/benchmarks/results/f3-derived-views-baseline/arm-f3-report.md +57 -0
  54. package/benchmarks/results/f3-derived-views-baseline/arm-f3-task.md +21 -0
  55. package/benchmarks/results/f3-derived-views-baseline/arm-f3.diff +21 -0
  56. package/benchmarks/results/live-host/claude-325-3b-s2b/README.md +99 -0
  57. package/benchmarks/results/live-host/claude-325-3b-s2b/capture.sanitized.jsonl +26 -0
  58. package/ci/archive/325-prep/README.md +71 -0
  59. package/ci/{prep → archive/325-prep}/doc-reconciliation-red.js +9 -7
  60. package/ci/archive/325-prep/f4-expected-red.json +34 -0
  61. package/ci/archive/325-prep/historical-qualification.test.js +143 -0
  62. package/ci/fixtures/doc-obligations/reconciliation-prep/cases.json +17 -4
  63. package/ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json +8 -56
  64. package/ci/fixtures/doc-obligations/reconciliation-prep/trust-contract.json +2 -2
  65. package/ci/fixtures/doc-obligations/reconciliation-s1/cases.json +17 -5
  66. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/harness.js +415 -0
  67. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/off-handoff-golden.json +16 -0
  68. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-post-tool-use-failure.json +11 -0
  69. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-post-tool-use-success-minimal.json +9 -0
  70. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-post-tool-use-success.json +13 -0
  71. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-start.json +7 -0
  72. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-stop-freetext-reject.json +11 -0
  73. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-stop-reject.json +11 -0
  74. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-stop.json +11 -0
  75. package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-task-result.json +19 -0
  76. package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/off-capture-only.json +99 -0
  77. package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/off-legacy.json +67 -0
  78. package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-concurrent-writers.json +62 -0
  79. package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-lookalikes-runtime-mutations.json +67 -0
  80. package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-malformed.json +85 -0
  81. package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-normal-session.json +74 -0
  82. package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-runtime-drift-later-npm-test.json +58 -0
  83. package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-valid-direct-quoted-review.json +85 -0
  84. package/ci/fixtures/doc-obligations/reconciliation-s2b/f4/cases.json +2267 -0
  85. package/ci/fixtures/doc-obligations/reconciliation-s2b/wiring/cases.json +1110 -0
  86. package/ci/fixtures/doc-obligations/reconciliation-s2b/wiring/legacy-off-golden.json +674 -0
  87. package/ci/lib/doc-reconciliation-file-spy.js +23 -0
  88. package/ci/lib/doc-reconciliation-prep-fixtures.js +136 -10
  89. package/ci/lib/doc-reconciliation-qualification-preload.js +37 -0
  90. package/ci/lib/doc-reconciliation-replay.js +104 -0
  91. package/ci/lib/doc-reconciliation-staged-tree.js +102 -0
  92. package/ci/mechanism-2p-opencode-v2-entrypoint.test.js +179 -0
  93. package/ci/mechanism-325-3b-preparation.test.js +27 -101
  94. package/ci/mechanism-325-3b-s2b-capture.test.js +561 -0
  95. package/ci/mechanism-325-3b-s2b-wiring.test.js +668 -0
  96. package/ci/mechanism-325-f1-doc-check.test.js +674 -0
  97. package/ci/mechanism-325-f2-doc-craft.test.js +597 -0
  98. package/ci/mechanism-325-f3-derived-views.test.js +798 -0
  99. package/ci/mechanism-325-f4-invariants.test.js +606 -0
  100. package/ci/mechanism-325-f4-projections.test.js +382 -0
  101. package/ci/mechanism-325-f4-qualification.test.js +228 -0
  102. package/ci/mechanism-325-f4-trust-boundary.test.js +880 -0
  103. package/ci/mutation-check.js +26 -3
  104. package/docs/architecture.md +5 -0
  105. package/docs/doc-workflow-reconciliation.md +331 -4
  106. package/docs/document-lifecycle.md +11 -0
  107. package/docs/governed-index.md +20 -0
  108. package/docs/platform-capabilities.md +22 -2
  109. package/grill-with-docs/SKILL.md +1 -1
  110. package/grill-with-docs/references/session-playbook.md +4 -0
  111. package/harness-everything/SKILL.md +1 -1
  112. package/harness-everything/references/doc-check.md +186 -0
  113. package/harness-everything/references/documentation/_shared/maintenance.md +41 -0
  114. package/harness-everything/references/documentation/_shared/writing-craft.md +59 -0
  115. package/harness-everything/references/triage-and-tiers.md +4 -0
  116. package/harness-everything/scripts/doc-check.js +442 -0
  117. package/harness-everything/scripts/doc-lifecycle.js +3 -0
  118. package/hooks/hooks.json +46 -0
  119. package/hooks/scripts/lib/doc-reconciliation-capture.sanitized.jsonl +26 -0
  120. package/hooks/scripts/lib/doc-reconciliation-pilot.js +171 -2
  121. package/hooks/scripts/lib/doc-reconciliation-provider.js +1059 -0
  122. package/hooks/scripts/lib/doc-reconciliation-qualification.json +34 -0
  123. package/hooks/scripts/lib/workflow-obligations.js +7 -0
  124. package/hooks/scripts/state-persist.js +78 -2
  125. package/hooks/scripts/workflow-stop-gate.js +7 -2
  126. package/opencode-plugin/README.md +15 -0
  127. package/opencode-plugin/index.mjs +77 -0
  128. package/opencode-plugin/plugin.json +1 -1
  129. package/package.json +1 -1
  130. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  131. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-capture.sanitized.jsonl +26 -0
  132. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +171 -2
  133. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +1059 -0
  134. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-qualification.json +34 -0
  135. package/plugins/harness-everything/hooks/scripts/lib/workflow-obligations.js +7 -0
  136. package/plugins/harness-everything/hooks/scripts/state-persist.js +78 -2
  137. package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +7 -2
  138. package/plugins/harness-everything/plugin.json +1 -1
  139. package/plugins/harness-everything/skills/grill-with-docs/SKILL.md +1 -1
  140. package/plugins/harness-everything/skills/grill-with-docs/references/session-playbook.md +4 -0
  141. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  142. package/plugins/harness-everything/skills/harness-everything/references/doc-check.md +186 -0
  143. package/plugins/harness-everything/skills/harness-everything/references/documentation/_shared/maintenance.md +41 -0
  144. package/plugins/harness-everything/skills/harness-everything/references/documentation/_shared/writing-craft.md +59 -0
  145. package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +4 -0
  146. package/plugins/harness-everything/skills/harness-everything/scripts/doc-check.js +442 -0
  147. package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +3 -0
  148. package/plugins/harness-everything/skills/repo-docs/SKILL.md +1 -1
  149. package/plugins/harness-everything/skills/repo-docs/references/process-guide.md +4 -0
  150. package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
  151. package/plugins/harness-everything/skills/to-spec/references/process.md +4 -0
  152. package/plugins/harness-everything/skills/to-tickets/SKILL.md +1 -1
  153. package/plugins/harness-everything/skills/to-tickets/references/publishing-and-templates.md +4 -0
  154. package/repo-docs/SKILL.md +1 -1
  155. package/repo-docs/references/process-guide.md +4 -0
  156. package/sbom.cdx.json +7 -7
  157. package/to-spec/SKILL.md +1 -1
  158. package/to-spec/references/process.md +4 -0
  159. package/to-tickets/SKILL.md +1 -1
  160. package/to-tickets/references/publishing-and-templates.md +4 -0
@@ -0,0 +1,179 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const { pathToFileURL } = require('url');
5
+ const helper = require('./test-helper');
6
+
7
+ console.log('\n[2p] opencode 2.x default-export entrypoint: registration and firing behavior...');
8
+
9
+ // OpenCode 2.x requires a default export { id, setup } that registers hooks
10
+ // through ctx.tool.hook / ctx.event.subscribe. The named V1 factory still
11
+ // exists for OpenCode 1 hosts and the mechanism tests; this test covers the
12
+ // V2 adapter that the real host actually loads.
13
+
14
+ helper.check(
15
+ '2p. the plugin ships as a single self-contained module',
16
+ fs.existsSync(path.join(helper.root, 'opencode-plugin', 'index.mjs')),
17
+ 'index.mjs missing'
18
+ );
19
+
20
+ (async () => {
21
+ // Isolate state exactly like mechanism-2n: HOME -> temp, no HARNESS_STATE_HOME.
22
+ delete process.env.HARNESS_STATE_HOME;
23
+ const fakeHome = helper.tempDir('.mechanism-test-opencode-v2-home');
24
+ const workspace = helper.tempDir('.mechanism-test-opencode-v2-workspace');
25
+ fs.mkdirSync(fakeHome, { recursive: true });
26
+ fs.mkdirSync(workspace, { recursive: true });
27
+ process.env.HOME = fakeHome;
28
+ process.env.USERPROFILE = fakeHome;
29
+
30
+ // Mirrors the plugin getWorkspaceKey(): cwd-realpath + slug + 12-char sha1.
31
+ const realWorkspace = fs.realpathSync(path.resolve(workspace));
32
+ const slug = realWorkspace.split(path.sep).pop().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'workspace';
33
+ const hashInput = process.platform === 'win32' ? realWorkspace.toLowerCase() : realWorkspace;
34
+ const hash = crypto.createHash('sha1').update(hashInput).digest('hex').slice(0, 12);
35
+ const stateRoot = path.join(fakeHome, '.agents', 'harness-everything', 'workspaces', `${slug}-${hash}`, 'state', 'sessions');
36
+
37
+ fs.writeFileSync(
38
+ path.join(workspace, 'package.json'),
39
+ JSON.stringify({ name: 'fixture', version: '0.0.0', scripts: { test: 'node -e "process.exit(1)"' } }, null, 2)
40
+ );
41
+
42
+ const prompts = [];
43
+ const registered = [];
44
+ let subscribeCount = 0;
45
+ const eventQueue = [];
46
+ const fakeCtx = {
47
+ location: { directory: workspace },
48
+ tool: {
49
+ hook: async (name, callback) => {
50
+ registered.push({ name, callback });
51
+ return { dispose: async () => {} };
52
+ },
53
+ },
54
+ session: {
55
+ prompt: async (input) => {
56
+ prompts.push(input);
57
+ return {};
58
+ },
59
+ },
60
+ event: {
61
+ subscribe: (options) => {
62
+ subscribeCount++;
63
+ return {
64
+ [Symbol.asyncIterator]: () => ({
65
+ next: async () => {
66
+ while (eventQueue.length === 0) {
67
+ if (options.signal.aborted) return { done: true, value: undefined };
68
+ await new Promise((resolve) => setTimeout(resolve, 5));
69
+ }
70
+ return { done: false, value: eventQueue.shift() };
71
+ },
72
+ }),
73
+ };
74
+ },
75
+ },
76
+ };
77
+
78
+ const push = (event) => eventQueue.push(event);
79
+ const settle = () => new Promise((resolve) => setTimeout(resolve, 50));
80
+
81
+ const mod = await import(pathToFileURL(path.join(helper.root, 'opencode-plugin', 'index.mjs')).href);
82
+ const definition = mod.default;
83
+
84
+ helper.check(
85
+ '2p. the module default-exports an OpenCode 2 definition with a string id and setup function',
86
+ definition && typeof definition.setup === 'function' && typeof definition.id === 'string' && definition.id.length > 0,
87
+ `default export keys: ${JSON.stringify(definition && Object.keys(definition))}`
88
+ );
89
+
90
+ helper.check(
91
+ '2p. the module still exports the V1 HarnessEnforcement factory for OpenCode 1 hosts and tests',
92
+ typeof mod.HarnessEnforcement === 'function',
93
+ `exports: ${Object.keys(mod).join(', ')}`
94
+ );
95
+
96
+ await definition.setup(fakeCtx);
97
+
98
+ helper.check(
99
+ '2p. setup registers the V2 tool hooks under their OpenCode 2 names',
100
+ registered.some((r) => r.name === 'execute.before') && registered.some((r) => r.name === 'execute.after'),
101
+ JSON.stringify(registered.map((r) => r.name))
102
+ );
103
+
104
+ helper.check('2p. setup subscribes to the event stream exactly once', subscribeCount === 1, `subscribes: ${subscribeCount}`);
105
+
106
+ const before = registered.find((r) => r.name === 'execute.before').callback;
107
+ const after = registered.find((r) => r.name === 'execute.after').callback;
108
+
109
+ // Flow 1: a failure loop drives verification through real V2 event shapes:
110
+ // execute.before/after carry {tool, sessionID, id, input}; the event stream
111
+ // carries flat {type, sessionID}.
112
+ const sessionDir = path.join(stateRoot, 's1');
113
+ for (let i = 1; i <= 3; i++) {
114
+ await after({ tool: 'edit', sessionID: 's1', id: `c${i}`, input: {} });
115
+ push({ type: 'session.idle', sessionID: 's1' });
116
+ await settle();
117
+ }
118
+
119
+ helper.check(
120
+ '2p. three v2-shaped edit/idle cycles force reflection',
121
+ prompts.length === 3,
122
+ `prompts: ${prompts.length}`
123
+ );
124
+
125
+ helper.check(
126
+ '2p. the V2 ctx.session.prompt receives {sessionID, text}, not the V1 {path, body} shape',
127
+ prompts.every((p) => typeof p.sessionID === 'string' && typeof p.text === 'string'),
128
+ JSON.stringify(prompts)
129
+ );
130
+
131
+ const breaker = JSON.parse(fs.readFileSync(path.join(sessionDir, 'circuit-breaker.json'), 'utf8'));
132
+ const signature = Object.keys(breaker.failures)[0];
133
+ helper.check(
134
+ '2p. three same-signature failures via the v2 hooks force reflectionPending',
135
+ breaker.failures[signature].count === 3 && breaker.reflectionPending === true,
136
+ JSON.stringify(breaker)
137
+ );
138
+
139
+ let blocked = false;
140
+ try {
141
+ await before({ tool: 'edit', sessionID: 's1', id: 'blocked-edit', input: {} });
142
+ } catch {
143
+ blocked = true;
144
+ }
145
+ helper.check('2p. a pending reflection blocks a v2-shaped edit call', blocked, 'edit call was allowed during pending reflection');
146
+
147
+ // Flow 2: the reflection artifact is accepted through the V2 after hook,
148
+ // proving input-args mapping across the adapter.
149
+ const token = (prompts.map((p) => (p.text.match(/reflection token: ([A-Za-z0-9_-]+)/i) || [])[1]).filter(Boolean).pop());
150
+ const reflectionFile = path.join(sessionDir, 'zoom-out-report.md');
151
+ fs.writeFileSync(
152
+ reflectionFile,
153
+ `## Goal\nfix the fixture\n## Failed Attempts\nthree retries\n## Verified Facts\ntest stays red\n## Diagnosis\nthe fixture intentionally fails\n## Decision\nRESUME: change the fixture\nReflection token: ${token}\n`
154
+ );
155
+ await after({ tool: 'write', sessionID: 's1', id: 'reflect-write', input: { filePath: reflectionFile } });
156
+
157
+ const breakerAfter = JSON.parse(fs.readFileSync(path.join(sessionDir, 'circuit-breaker.json'), 'utf8'));
158
+ helper.check(
159
+ '2p. a v2-shaped reflection artifact completes the reflection',
160
+ breakerAfter.reflectionPending === false && Number.isFinite(breakerAfter.lastReflection) && breakerAfter.failures[signature].count === 0,
161
+ JSON.stringify(breakerAfter)
162
+ );
163
+
164
+ // Flow 3: session reset through the flat V2 event shape.
165
+ await after({ tool: 'edit', sessionID: 'reset', id: 'reset-edit', input: {} });
166
+ helper.check('2p. an edit created state for its session before the reset probe', fs.existsSync(path.join(stateRoot, 'reset', 'edit-state.json')), 'state/reset');
167
+ push({ type: 'session.created', sessionID: 'reset' });
168
+ await settle();
169
+ helper.check(
170
+ '2p. a flat v2 session.created event resets that session state',
171
+ !fs.existsSync(path.join(stateRoot, 'reset', 'edit-state.json')),
172
+ 'edit-state.json survived the reset'
173
+ );
174
+
175
+ helper.finish();
176
+ })().catch((err) => {
177
+ console.error('2p. opencode v2 entrypoint test crashed:', err);
178
+ process.exit(1);
179
+ });
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- // #325 Phase 3b preparation (H1+H2): fixture inventory, the qualified expected-RED runner, legacy controls,
5
- // switch sanitation, source/package parity and negative registry checks. This suite asserts no runtime
6
- // behaviour change and no pilot credit. Desired-contract gaps are qualified by the registry, never
7
- // asserted as passing behaviour. Full replay runs use the runner; negative checks use one case each.
4
+ // #325 Phase 3b preparation (H1+H2): fixture inventory, legacy controls, switch sanitation, source/package parity and
5
+ // the runner's infrastructure bounds. This suite asserts no runtime behaviour change and no pilot credit. The qualified
6
+ // RED runner, its frozen registry and the runtime-fingerprint and recapture checks are archived by F4-T2 under
7
+ // ci/archive/325-prep (historical evidence, see its README.md). The archived runner is still invoked here, only for its
8
+ // infrastructure path (exit 2, child deadline, output bound), which is live.
8
9
 
9
10
  const assert = require('assert');
10
11
  const crypto = require('crypto');
@@ -13,11 +14,11 @@ const os = require('os');
13
14
  const path = require('path');
14
15
  const { spawnSync } = require('child_process');
15
16
  const fx = require('./lib/doc-reconciliation-prep-fixtures');
16
- const runner = require('./prep/doc-reconciliation-red.js');
17
+ const runner = require('./archive/325-prep/doc-reconciliation-red.js');
17
18
 
18
19
  const ROOT = path.resolve(__dirname, '..');
19
20
  const PREP_DIR = path.join(__dirname, 'fixtures', 'doc-obligations', 'reconciliation-prep');
20
- const RUNNER = path.join(__dirname, 'prep', 'doc-reconciliation-red.js');
21
+ const RUNNER = path.join(__dirname, 'archive', '325-prep', 'doc-reconciliation-red.js');
21
22
  const corpus = fx.readCaseCorpus(path.join(PREP_DIR, 'cases.json'));
22
23
  const registry = JSON.parse(fs.readFileSync(path.join(PREP_DIR, 'pending-red.json'), 'utf8'));
23
24
  const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'doc-recon-prep-suite-'));
@@ -43,16 +44,10 @@ function checkCase(name, runCase) {
43
44
  function runRunner(args) {
44
45
  return spawnSync(process.execPath, [RUNNER, ...args], { cwd: ROOT, encoding: 'utf8' });
45
46
  }
46
- function writeRegistry(name, mutate) {
47
- const copy = JSON.parse(JSON.stringify(registry));
48
- mutate(copy);
49
- const file = path.join(workDir, name);
50
- fs.writeFileSync(file, `${JSON.stringify(copy, null, 2)}\n`, 'utf8');
51
- return file;
52
- }
53
47
 
54
- console.log('=== #325 3b preparation: corpus, qualified RED runner and controls (H1+H2) ===');
48
+ console.log('=== #325 3b preparation: corpus, legacy controls and infrastructure bounds (H1+H2) ===');
55
49
 
50
+ // locks: unresolved — fixture-shape control (corpus field shape); no A/B/C/R3 clause
56
51
  checkCase('inventory: the H2 corpus covers P01-P07 with the L1 field shape', () => {
57
52
  const ids = corpus.cases.map(item => item.id);
58
53
  assert.deepStrictEqual(ids, ['P01', 'P04', 'P06', 'P02a', 'P02b', 'P02c', 'P02g', 'P02d', 'P02e', 'P02f', 'P03', 'P05', 'P07-fable-staged', 'P07-fable-parallel', 'P07-fable-multi-agent-workspace', 'P07u']);
@@ -74,65 +69,6 @@ checkCase('inventory: the H2 corpus covers P01-P07 with the L1 field shape', ()
74
69
  }
75
70
  });
76
71
 
77
- // The full replay runs once, through the runner's JSON report. Every case must be a control or a qualified gap,
78
- // with matching parity between the two legs and no pilot credit.
79
- let report = null;
80
- checkCase('expected-RED mode: every case is CONTROL_PASS or EXPECTED_RED and exits 0', () => {
81
- const run = runRunner(['--expect-red', '--json']);
82
- assert.strictEqual(run.status, 0, `exit ${run.status}: ${(run.stderr || run.stdout).slice(0, 400)}`);
83
- report = JSON.parse(run.stdout);
84
- });
85
-
86
- checkCase('expected-RED report: legs agree, controls are green, no pilot credit, runtime fingerprint matches', () => {
87
- assert.ok(report, 'the report was produced');
88
- assert.strictEqual(report.pilotCredit, false, 'preparation reports never grant pilot credit');
89
- assert.strictEqual(report.runtimeFingerprintMatches, true, 'registry describes the current shared runtime bytes');
90
- for (const item of report.cases) {
91
- assert.ok(['CONTROL_PASS', 'EXPECTED_RED'].includes(item.status), `${item.caseId} is ${item.status}`);
92
- assert.strictEqual(item.parity, 'match', `${item.caseId} source and packaged legs agree`);
93
- }
94
- const controls = report.cases.filter(item => item.status === 'CONTROL_PASS').map(item => item.caseId).sort();
95
- assert.deepStrictEqual(controls, ['P01', 'P02a', 'P02b', 'P02c', 'P02d', 'P02f', 'P02g', 'P04', 'P06', 'P07-fable-multi-agent-workspace', 'P07-fable-parallel', 'P07-fable-staged', 'P07u']);
96
- });
97
-
98
- checkCase('gap registry: every entry is qualified and the expected-RED mode accounts for all of them', () => {
99
- assert.ok(report, 'the report was produced');
100
- const qualified = report.cases.flatMap(item => item.assertions.filter(entry => entry.status === 'gap-qualified'));
101
- assert.strictEqual(qualified.length, registry.entries.length, 'each registry entry is observed exactly once');
102
- assert.ok(!report.cases.some(item => item.assertions.some(entry => entry.status === 'gap-unqualified' || entry.status === 'unexpected-pass')));
103
- });
104
-
105
- checkCase('strict mode fails on the recorded gaps and preserves the RED log', () => {
106
- const log = path.join(workDir, 'p02e-red.json');
107
- const run = runRunner(['--strict', '--case', 'P02e', '--log', log]);
108
- assert.strictEqual(run.status, 1, `strict mode exits 1 on gaps: ${run.stdout}`);
109
- const saved = JSON.parse(fs.readFileSync(log, 'utf8'));
110
- assert.ok(saved.cases[0].assertions.some(entry => entry.status === 'gap-qualified'), 'the log keeps the qualified gaps');
111
- });
112
-
113
- checkCase('malformed registry is an infrastructure error, not a RED pass', () => {
114
- const file = writeRegistry('missing-reason.json', copy => { delete copy.entries[0].reasonCode; });
115
- const run = runRunner(['--expect-red', '--case', 'P01', '--registry', file]);
116
- assert.strictEqual(run.status, 2, `registry errors exit 2: ${run.stdout} ${run.stderr}`);
117
- });
118
-
119
- checkCase('a registry that omits a gap fails expected-RED mode', () => {
120
- const file = writeRegistry('omit-gap.json', copy => { copy.entries = copy.entries.filter(entry => entry.assertionId !== 'P02e.on.review-pass-exit' || entry.distribution !== 'source'); });
121
- const run = runRunner(['--expect-red', '--case', 'P02e', '--registry', file]);
122
- assert.strictEqual(run.status, 1, `an unqualified gap fails: ${run.stdout}`);
123
- });
124
-
125
- checkCase('an unexpected PASS on a control fails expected-RED mode', () => {
126
- const file = writeRegistry('unexpected-pass.json', copy => {
127
- copy.entries.push({
128
- caseId: 'P06', assertionId: 'P06.on.stop-satisfied', distribution: 'source', baselineSha: copy.baselineSha,
129
- actual: 'satisfied', desired: 'running', blocker: '#341 E0', reasonCode: 'pretend-gap'
130
- });
131
- });
132
- const run = runRunner(['--expect-red', '--case', 'P06', '--registry', file]);
133
- assert.strictEqual(run.status, 1, `a passing assertion with a registry entry fails: ${run.stdout}`);
134
- });
135
-
136
72
  // Infrastructure rule: an abnormal action result is never a qualified gap. The controls start from one healthy replay
137
73
  // set of P01 and change only the ON route result, so every other fault and assertion is the unmodified replay's.
138
74
  const p01 = corpus.cases.find(item => item.id === 'P01');
@@ -144,6 +80,7 @@ const injectIntoRoute = (patch, legs = ['source', 'packaged']) => {
144
80
  return injected;
145
81
  };
146
82
 
83
+ // locks: C — "unavailable observations ... cannot become a false PASS"; control: P01
147
84
  checkCase('negative control: an abnormal route in P01 is infrastructure, and --expect-red exits nonzero though later assertions still match', () => {
148
85
  const injected = injectIntoRoute({ exitCode: 1 });
149
86
  const later = p01.expectations.on.filter(assertion => assertion.id.startsWith('verify-obligation') || assertion.id.startsWith('stop-'));
@@ -165,6 +102,7 @@ checkCase('negative control: an abnormal route in P01 is infrastructure, and --e
165
102
  assert.strictEqual(runner.exitCodeFor({ cases: [result] }, 'expect-red'), 2, 'expected-RED mode exits nonzero');
166
103
  });
167
104
 
105
+ // locks: C — "unavailable observations ... cannot become a false PASS"; control: P01
168
106
  checkCase('negative control: a route killed by a signal or without an exit status is infrastructure', () => {
169
107
  const result = runner.classifyCase(p01, injectIntoRoute({ exitCode: null, signal: 'SIGTERM' }), p01Entries);
170
108
  assert.strictEqual(result.status, 'INFRASTRUCTURE');
@@ -172,18 +110,21 @@ checkCase('negative control: a route killed by a signal or without an exit statu
172
110
  assert.ok(result.infrastructure.includes('source.on.route: no exit status'), result.infrastructure.join('; '));
173
111
  });
174
112
 
113
+ // locks: C — "unavailable observations ... cannot become a false PASS"; control: P01
175
114
  checkCase('negative control: a spawn error on the route is infrastructure', () => {
176
115
  const result = runner.classifyCase(p01, injectIntoRoute({ exitCode: null, error: 'spawnSync node ENOENT' }), p01Entries);
177
116
  assert.strictEqual(result.status, 'INFRASTRUCTURE');
178
117
  assert.ok(result.infrastructure.includes('source.on.route: spawn error spawnSync node ENOENT'), result.infrastructure.join('; '));
179
118
  });
180
119
 
120
+ // locks: C — "unavailable observations ... cannot become a false PASS"; control: P01
181
121
  checkCase('negative control: an accepted route exit without the routing checkpoint is malformed required output', () => {
182
122
  const result = runner.classifyCase(p01, injectIntoRoute({ stdout: 'routing unavailable' }), p01Entries);
183
123
  assert.strictEqual(result.status, 'INFRASTRUCTURE');
184
124
  assert.ok(result.infrastructure.includes('source.on.route: malformed required output'), result.infrastructure.join('; '));
185
125
  });
186
126
 
127
+ // locks: C — "unavailable observations ... cannot become a false PASS"; control: P01
187
128
  checkCase('--expect-red exits nonzero from the CLI when a real route exit falls outside its policy', () => {
188
129
  const corpusCopy = JSON.parse(JSON.stringify(corpus));
189
130
  corpusCopy.cases.find(item => item.id === 'P01').actions.find(action => action.id === 'route').expectExit = { off: 1, on: 1 };
@@ -212,6 +153,7 @@ function runWithChildLimits(caseId, childLimits) {
212
153
  return { run, elapsed: Date.now() - started };
213
154
  }
214
155
 
156
+ // locks: C — "unavailable observations ... cannot become a false PASS"; control: P01
215
157
  checkCase('a hung route entrypoint cannot stall --expect-red: the CLI exits 2 promptly and removes the replay directories', () => {
216
158
  const before = replayDirsFor('P01');
217
159
  const { run, elapsed } = runWithChildLimits('P01', { timeoutMs: 500, substitutes: { route: 'hang-entrypoint.js' } });
@@ -222,6 +164,7 @@ checkCase('a hung route entrypoint cannot stall --expect-red: the CLI exits 2 pr
222
164
  assert.strictEqual(replayDirsFor('P01'), before, 'replay directories are removed after the timeout');
223
165
  });
224
166
 
167
+ // locks: C — "unavailable observations ... cannot become a false PASS"; control: P01
225
168
  checkCase('a route that floods stdout is stopped by the output bound and exits 2 through the CLI', () => {
226
169
  const before = replayDirsFor('P01');
227
170
  const { run, elapsed } = runWithChildLimits('P01', { timeoutMs: 5000, substitutes: { route: 'flood-entrypoint.js' } });
@@ -232,6 +175,7 @@ checkCase('a route that floods stdout is stopped by the output bound and exits 2
232
175
  assert.strictEqual(replayDirsFor('P01'), before, 'replay directories are removed after the output bound');
233
176
  });
234
177
 
178
+ // locks: C — "unavailable observations ... cannot become a false PASS"; control: P01
235
179
  checkCase('a faulted replay runs no action after its first infrastructure fault and still removes its directories', () => {
236
180
  const before = replayDirsFor('P01');
237
181
  const item = { ...p01, childLimits: { timeoutMs: 500, substitutes: { route: 'hang-entrypoint.js' } } };
@@ -246,6 +190,7 @@ checkCase('a faulted replay runs no action after its first infrastructure fault
246
190
  // spend one deadline per replay (64 route timeouts). The fail-fast spends one, names the rest NOT_RUN, and exits 2.
247
191
  const SHARED_HANG_DEADLINE_MS = 500;
248
192
  const SHARED_HANG_BOUND_MS = 5000;
193
+ // locks: C — "unavailable observations ... cannot become a false PASS"; control: P01
249
194
  checkCase('corpus-level: a route hung in every case costs one deadline, exits 2, and names the other cases NOT_RUN', () => {
250
195
  const everyCaseHangs = JSON.parse(JSON.stringify(corpus));
251
196
  for (const item of everyCaseHangs.cases) {
@@ -274,15 +219,18 @@ checkCase('corpus-level: a route hung in every case costs one deadline, exits 2,
274
219
  assert.strictEqual(replayDirsFor(''), before, 'replay directories are removed');
275
220
  });
276
221
 
222
+ // locks: unresolved — test-only override guard (tool control); no A/B/C/R3 clause
277
223
  checkCase('the committed corpus carries no child-limit override, so production replays use the default deadline', () => {
278
224
  assert.ok(corpus.cases.every(item => item.childLimits === undefined), 'a childLimits key is test-only');
279
225
  });
280
226
 
227
+ // locks: unresolved — deadline-shortening guard (tool control); no A/B/C/R3 clause
281
228
  checkCase('a child deadline override can only shorten the default deadline', () => {
282
229
  const item = { ...corpus.cases.find(candidate => candidate.id === 'P06'), childLimits: { timeoutMs: 60000 } };
283
230
  assert.throws(() => fx.replayCase(item, 'source'), /may only shorten/);
284
231
  });
285
232
 
233
+ // locks: C — "unavailable observations ... cannot become a false PASS"; control: P06
286
234
  checkCase('a malformed expectExit policy is an infrastructure error, not a RED pass', () => {
287
235
  const corpusCopy = JSON.parse(JSON.stringify(corpus));
288
236
  corpusCopy.cases.find(item => item.id === 'P06').actions.find(action => action.id === 'route').expectExit = { off: 'two' };
@@ -292,33 +240,7 @@ checkCase('a malformed expectExit policy is an infrastructure error, not a RED p
292
240
  assert.strictEqual(run.status, 2, `a malformed policy exits 2: ${run.stdout} ${run.stderr}`);
293
241
  });
294
242
 
295
- checkCase('a runtime fingerprint change invalidates qualification and prints the exact deliberate re-capture command', () => {
296
- const file = writeRegistry('stale-fingerprint.json', copy => { copy.runtimeFingerprint = '0'.repeat(64); });
297
- const run = runRunner(['--expect-red', '--case', 'P01', '--registry', file]);
298
- assert.strictEqual(run.status, 1, `a fingerprint mismatch fails: ${run.stdout}`);
299
- const output = run.stdout + run.stderr;
300
- assert.ok(output.includes('--recapture-candidate ci/fixtures/doc-obligations/reconciliation-prep/pending-red.candidate.json'), 'the re-capture command names the candidate');
301
- assert.ok(/--baseline-sha [0-9a-f]{40}/.test(output), 'the re-capture command names the commit');
302
- assert.ok(output.includes('nothing is auto-accepted'), 'the message says nothing is auto-accepted');
303
- });
304
-
305
- checkCase('recapture writes a reviewable candidate and never the committed registry', () => {
306
- const candidate = path.join(workDir, 'candidate.json');
307
- const before = fs.readFileSync(path.join(PREP_DIR, 'pending-red.json'), 'utf8');
308
- const run = spawnSync(process.execPath, [RUNNER, '--recapture-candidate', candidate, '--baseline-sha', registry.baselineSha, '--case', 'P06'], { cwd: ROOT, encoding: 'utf8' });
309
- assert.strictEqual(run.status, 0, run.stderr);
310
- assert.strictEqual(fs.readFileSync(path.join(PREP_DIR, 'pending-red.json'), 'utf8'), before, 'the committed registry is untouched');
311
- const saved = JSON.parse(fs.readFileSync(candidate, 'utf8'));
312
- assert.strictEqual(saved.entries.length, 0, 'a control case has no gaps to record');
313
- assert.strictEqual(saved.runtimeFingerprint, registry.runtimeFingerprint, 'the candidate describes the current runtime');
314
- });
315
-
316
- checkCase('a candidate with unreviewed reasons is rejected by the registry validator', () => {
317
- const file = writeRegistry('unreviewed.json', copy => { copy.entries[0].reasonCode = 'needs-reason'; });
318
- const run = runRunner(['--expect-red', '--case', 'P01', '--registry', file]);
319
- assert.strictEqual(run.status, 2, 'an unreviewed reason is a registry error');
320
- });
321
-
243
+ // locks: unresolved — package parity control (shared runtime bytes); not a false-PASS claim
322
244
  checkCase('P08 parity: shared runtime files are byte-identical between the source and packaged legs', () => {
323
245
  for (const [source, packaged] of SHARED_FILES) {
324
246
  const left = crypto.createHash('sha256').update(fs.readFileSync(path.join(ROOT, source))).digest('hex');
@@ -328,6 +250,7 @@ checkCase('P08 parity: shared runtime files are byte-identical between the sourc
328
250
  });
329
251
 
330
252
  // Switch sanitation: an inherited ON request does not reach an OFF replay (one replay).
253
+ // locks: unresolved — OFF isolation from an inherited ON request; the I2 invariant is not in the A/B/C/R3 table
331
254
  checkCase('switch sanitation: an inherited ON request does not reach an OFF replay', () => {
332
255
  const previous = process.env[fx.PILOT_SWITCH];
333
256
  process.env[fx.PILOT_SWITCH] = '1';
@@ -342,6 +265,7 @@ checkCase('switch sanitation: an inherited ON request does not reach an OFF repl
342
265
  }
343
266
  });
344
267
 
268
+ // locks: unresolved — temporary-directory hygiene control; no A/B/C/R3 clause
345
269
  checkCase('cleanup: replays remove their temporary workspace and state directories', () => {
346
270
  const prefix = 'doc-recon-P06-source-';
347
271
  const before = fs.readdirSync(os.tmpdir()).filter(name => name.startsWith(prefix)).length;
@@ -351,6 +275,7 @@ checkCase('cleanup: replays remove their temporary workspace and state directori
351
275
  });
352
276
 
353
277
  // F-a: separator-agnostic scrubbing. Windows-shaped strings from each leg must normalize to the same markers.
278
+ // locks: unresolved — replay path-normalisation control; no A/B/C/R3 clause
354
279
  checkCase('F-a: Windows-shaped source and packaged paths normalize identically', () => {
355
280
  const roots = { repo: 'D:\\a\\repo', tmp: ['C:\\Users\\runneradmin\\AppData\\Local\\Temp'] };
356
281
  const sourceSkill = fx.scrubText('D:\\a\\repo\\tdd\\SKILL.md', roots);
@@ -366,6 +291,7 @@ checkCase('F-a: Windows-shaped source and packaged paths normalize identically',
366
291
  });
367
292
 
368
293
  // F-b: a per-action switch override must not reach an OFF replay.
294
+ // locks: unresolved — OFF replay carries no per-action switch; the I2 invariant is not in the A/B/C/R3 table
369
295
  checkCase('F-b: an OFF replay of P05 carries no per-action switch; the ON replay keeps its overrides', () => {
370
296
  const item = corpus.cases.find(candidate => candidate.id === 'P05');
371
297
  const off = fx.replayCase(item, 'source');
@@ -378,5 +304,5 @@ checkCase('F-b: an OFF replay of P05 carries no per-action switch; the ON replay
378
304
  });
379
305
 
380
306
  fs.rmSync(workDir, { recursive: true, force: true });
381
- console.log(failed ? `\n${failed} preparation check(s) failed` : '\nAll preparation checks passed (desired-contract gaps are qualified, not asserted as behaviour)');
307
+ console.log(failed ? `\n${failed} preparation check(s) failed` : '\nAll preparation checks passed');
382
308
  process.exitCode = failed ? 1 : 0;