harness-everything 0.40.0 → 0.41.1
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.
- package/.agents/harness-everything/project-docs.json +9 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +51 -0
- package/benchmarks/results/f1-doc-path-baseline/1-source-clean.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/1-source-clean.stdout.json +20 -0
- package/benchmarks/results/f1-doc-path-baseline/2-injected-architecture.md +229 -0
- package/benchmarks/results/f1-doc-path-baseline/2-source-injected.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/2-source-injected.stdout.json +33 -0
- package/benchmarks/results/f1-doc-path-baseline/3-source-reverted.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/3-source-reverted.stdout.json +20 -0
- package/benchmarks/results/f1-doc-path-baseline/4a-packaged-clean.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/4a-packaged-clean.stdout.json +20 -0
- package/benchmarks/results/f1-doc-path-baseline/4b-packaged-injected.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/4b-packaged-injected.stdout.json +33 -0
- package/benchmarks/results/f1-doc-path-baseline/5-source-missing-config.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/5-source-missing-config.stdout.json +15 -0
- package/benchmarks/results/f1-doc-path-baseline/5b-packaged-missing-config.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/5b-packaged-missing-config.stdout.json +15 -0
- package/benchmarks/results/f1-doc-path-baseline/README.md +54 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/1-source-clean.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/1-source-clean.stdout.json +115 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/2-injected-document.md +328 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/2-source-injected.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/2-source-injected.stdout.json +127 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/3-source-reverted.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/3-source-reverted.stdout.json +115 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/4a-packaged-clean.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/4a-packaged-clean.stdout.json +115 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/4b-packaged-injected.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/4b-packaged-injected.stdout.json +127 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/5-source-missing-config.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/5-source-missing-config.stdout.json +15 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/5b-packaged-missing-config.stderr.txt +0 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/5b-packaged-missing-config.stdout.json +15 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/6-supplementary-fixture-ok.input.md +8 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/6-supplementary-fixture-ok.stdout.json +16 -0
- package/benchmarks/results/f1-doc-path-baseline/before-fix/README.md +34 -0
- package/benchmarks/results/f1-doc-path-baseline/probe-31-docs/after.json +248 -0
- package/benchmarks/results/f1-doc-path-baseline/probe-31-docs/before.json +254 -0
- package/benchmarks/results/f1-doc-path-baseline/probe-31-docs/probe.js +26 -0
- package/benchmarks/results/f2-doc-craft-baseline/README.md +32 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f1-report.md +84 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f1-task.md +21 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f1.diff +19 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f2-report.md +110 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f2-task.md +21 -0
- package/benchmarks/results/f2-doc-craft-baseline/arm-f2.diff +19 -0
- package/benchmarks/results/f3-derived-views-baseline/README.md +34 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f2-report.md +43 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f2-task.md +21 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f2.diff +21 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f3-report.md +57 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f3-task.md +21 -0
- package/benchmarks/results/f3-derived-views-baseline/arm-f3.diff +21 -0
- package/benchmarks/results/live-host/claude-325-3b-s2b/README.md +99 -0
- package/benchmarks/results/live-host/claude-325-3b-s2b/capture.sanitized.jsonl +26 -0
- package/ci/archive/325-prep/README.md +71 -0
- package/ci/{prep → archive/325-prep}/doc-reconciliation-red.js +9 -7
- package/ci/archive/325-prep/f4-expected-red.json +34 -0
- package/ci/archive/325-prep/historical-qualification.test.js +143 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/cases.json +17 -4
- package/ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json +8 -56
- package/ci/fixtures/doc-obligations/reconciliation-prep/trust-contract.json +2 -2
- package/ci/fixtures/doc-obligations/reconciliation-s1/cases.json +17 -5
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/harness.js +415 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/off-handoff-golden.json +16 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-post-tool-use-failure.json +11 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-post-tool-use-success-minimal.json +9 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-post-tool-use-success.json +13 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-start.json +7 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-stop-freetext-reject.json +11 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-stop-reject.json +11 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-subagent-stop.json +11 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/capture/payload-task-result.json +19 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/off-capture-only.json +99 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/off-legacy.json +67 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-concurrent-writers.json +62 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-lookalikes-runtime-mutations.json +67 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-malformed.json +85 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-normal-session.json +74 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-runtime-drift-later-npm-test.json +58 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/corpus/on-valid-direct-quoted-review.json +85 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/f4/cases.json +2267 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/wiring/cases.json +1110 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2b/wiring/legacy-off-golden.json +674 -0
- package/ci/lib/doc-reconciliation-file-spy.js +23 -0
- package/ci/lib/doc-reconciliation-prep-fixtures.js +136 -10
- package/ci/lib/doc-reconciliation-qualification-preload.js +37 -0
- package/ci/lib/doc-reconciliation-replay.js +104 -0
- package/ci/lib/doc-reconciliation-staged-tree.js +102 -0
- package/ci/mechanism-2p-opencode-v2-entrypoint.test.js +179 -0
- package/ci/mechanism-325-3b-preparation.test.js +27 -101
- package/ci/mechanism-325-3b-s2b-capture.test.js +561 -0
- package/ci/mechanism-325-3b-s2b-wiring.test.js +668 -0
- package/ci/mechanism-325-f1-doc-check.test.js +674 -0
- package/ci/mechanism-325-f2-doc-craft.test.js +597 -0
- package/ci/mechanism-325-f3-derived-views.test.js +798 -0
- package/ci/mechanism-325-f4-invariants.test.js +606 -0
- package/ci/mechanism-325-f4-projections.test.js +382 -0
- package/ci/mechanism-325-f4-qualification.test.js +228 -0
- package/ci/mechanism-325-f4-trust-boundary.test.js +880 -0
- package/ci/mechanism-365-step-edit-accounting.test.js +395 -0
- package/ci/mutation-check.js +26 -3
- package/docs/architecture.md +5 -0
- package/docs/doc-workflow-reconciliation.md +331 -4
- package/docs/document-lifecycle.md +11 -0
- package/docs/governed-index.md +20 -0
- package/docs/platform-capabilities.md +22 -2
- package/grill-with-docs/SKILL.md +1 -1
- package/grill-with-docs/references/session-playbook.md +4 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/references/doc-check.md +186 -0
- package/harness-everything/references/documentation/_shared/maintenance.md +41 -0
- package/harness-everything/references/documentation/_shared/writing-craft.md +59 -0
- package/harness-everything/references/triage-and-tiers.md +4 -0
- package/harness-everything/scripts/doc-check.js +442 -0
- package/harness-everything/scripts/doc-lifecycle.js +3 -0
- package/hooks/hooks.json +46 -0
- package/hooks/scripts/lib/doc-reconciliation-capture.sanitized.jsonl +26 -0
- package/hooks/scripts/lib/doc-reconciliation-pilot.js +171 -2
- package/hooks/scripts/lib/doc-reconciliation-provider.js +1059 -0
- package/hooks/scripts/lib/doc-reconciliation-qualification.json +34 -0
- package/hooks/scripts/lib/workflow-obligations.js +7 -0
- package/hooks/scripts/state-persist.js +105 -3
- package/hooks/scripts/workflow-stop-gate.js +7 -2
- package/opencode-plugin/README.md +15 -0
- package/opencode-plugin/index.mjs +77 -0
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-capture.sanitized.jsonl +26 -0
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +171 -2
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +1059 -0
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-qualification.json +34 -0
- package/plugins/harness-everything/hooks/scripts/lib/workflow-obligations.js +7 -0
- package/plugins/harness-everything/hooks/scripts/state-persist.js +105 -3
- package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +7 -2
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/grill-with-docs/SKILL.md +1 -1
- package/plugins/harness-everything/skills/grill-with-docs/references/session-playbook.md +4 -0
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/references/doc-check.md +186 -0
- package/plugins/harness-everything/skills/harness-everything/references/documentation/_shared/maintenance.md +41 -0
- package/plugins/harness-everything/skills/harness-everything/references/documentation/_shared/writing-craft.md +59 -0
- package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +4 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-check.js +442 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +3 -0
- package/plugins/harness-everything/skills/repo-docs/SKILL.md +1 -1
- package/plugins/harness-everything/skills/repo-docs/references/process-guide.md +4 -0
- package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-spec/references/process.md +4 -0
- package/plugins/harness-everything/skills/to-tickets/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-tickets/references/publishing-and-templates.md +4 -0
- package/repo-docs/SKILL.md +1 -1
- package/repo-docs/references/process-guide.md +4 -0
- package/sbom.cdx.json +7 -7
- package/to-spec/SKILL.md +1 -1
- package/to-spec/references/process.md +4 -0
- package/to-tickets/SKILL.md +1 -1
- 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,
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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,
|
|
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
|
-
|
|
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
|
|
307
|
+
console.log(failed ? `\n${failed} preparation check(s) failed` : '\nAll preparation checks passed');
|
|
382
308
|
process.exitCode = failed ? 1 : 0;
|