@haposoft/cafekit 0.16.0-rc.2 → 0.16.0-rc.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 (98) hide show
  1. package/bin/__tests__/codex-hooks.test.js +794 -21
  2. package/bin/__tests__/codex-native.test.js +228 -7
  3. package/bin/__tests__/develop-contract.test.js +1838 -112
  4. package/bin/__tests__/installer-safety.test.js +82 -29
  5. package/bin/__tests__/opencode-spec-gate.test.js +799 -0
  6. package/bin/__tests__/package-inventory.test.js +841 -10
  7. package/bin/__tests__/specs-v2-execution-closeout.test.js +421 -0
  8. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +601 -0
  9. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +952 -0
  10. package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
  11. package/bin/__tests__/validator-grounding.test.js +2469 -71
  12. package/bin/lib/managed-writer.js +12 -3
  13. package/bin/phases/codex-runtime.js +17 -1
  14. package/package.json +1 -1
  15. package/src/claude/CLAUDE.md +6 -0
  16. package/src/claude/agents/code-auditor.md +20 -0
  17. package/src/claude/agents/spec-maker.md +153 -202
  18. package/src/claude/hooks/__tests__/completion-authority.test.js +953 -0
  19. package/src/claude/hooks/__tests__/privacy-block.test.js +172 -0
  20. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
  21. package/src/claude/hooks/__tests__/spec-gate.test.js +604 -56
  22. package/src/claude/hooks/completion-authority-check.cjs +56 -0
  23. package/src/claude/hooks/completion-authority-state.cjs +527 -0
  24. package/src/claude/hooks/completion-authority.cjs +154 -0
  25. package/src/claude/hooks/docs-sync.cjs +5 -2
  26. package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
  27. package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
  28. package/src/claude/hooks/privacy-block.cjs +77 -30
  29. package/src/claude/hooks/semantic-review-authority.cjs +152 -0
  30. package/src/claude/hooks/session.cjs +33 -19
  31. package/src/claude/hooks/spec-gate.cjs +181 -139
  32. package/src/claude/hooks/spec-state.cjs +112 -47
  33. package/src/claude/migration-manifest.json +15 -2
  34. package/src/claude/rules/state-sync.md +52 -21
  35. package/src/claude/rules/workflow.md +17 -9
  36. package/src/claude/scripts/provenance.cjs +315 -0
  37. package/src/claude/scripts/spec-final-state.cjs +244 -0
  38. package/src/claude/scripts/spec-ground.cjs +535 -173
  39. package/src/claude/scripts/spec-readiness.cjs +118 -0
  40. package/src/claude/scripts/spec-receipt.cjs +162 -0
  41. package/src/claude/scripts/spec-resolver.cjs +501 -0
  42. package/src/claude/scripts/spec-scaffold.cjs +1945 -55
  43. package/src/claude/scripts/spec-semantic-model.cjs +396 -0
  44. package/src/claude/scripts/validate-spec-output.cjs +3204 -262
  45. package/src/claude/scripts/workflow-policy.cjs +1888 -203
  46. package/src/claude/settings/settings.json +12 -0
  47. package/src/claude/skills/code-review/SKILL.md +94 -66
  48. package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
  49. package/src/claude/skills/code-review/references/verification-gate.md +13 -0
  50. package/src/claude/skills/develop/SKILL.md +138 -283
  51. package/src/claude/skills/develop/references/parallel-waves.md +9 -6
  52. package/src/claude/skills/develop/references/quality-gate.md +123 -148
  53. package/src/claude/skills/develop/references/subagent-patterns.md +42 -37
  54. package/src/claude/skills/specs/SKILL.md +192 -397
  55. package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
  56. package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
  57. package/src/claude/skills/specs/references/research-strategy.md +4 -1
  58. package/src/claude/skills/specs/references/review.md +200 -260
  59. package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
  60. package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
  61. package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
  62. package/src/claude/skills/specs/rules/design-principles.md +136 -180
  63. package/src/claude/skills/specs/rules/design-review.md +42 -97
  64. package/src/claude/skills/specs/rules/ears-format.md +42 -50
  65. package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
  66. package/src/claude/skills/specs/rules/task-scoring-rubric.md +46 -46
  67. package/src/claude/skills/specs/rules/tasks-generation.md +189 -271
  68. package/src/claude/skills/specs/templates/design.md +49 -263
  69. package/src/claude/skills/specs/templates/requirements.md +26 -36
  70. package/src/claude/skills/specs/templates/research.md +9 -98
  71. package/src/claude/skills/specs/templates/spec-state.json +26 -67
  72. package/src/claude/skills/specs/templates/task.md +30 -82
  73. package/src/claude/skills/sync/SKILL.md +45 -15
  74. package/src/claude/skills/sync/references/sync-protocols.md +34 -16
  75. package/src/claude/skills/test/SKILL.md +151 -167
  76. package/src/codex/AGENTS.md +6 -0
  77. package/src/codex/hooks/completion-authority-check.cjs +63 -0
  78. package/src/codex/hooks/completion-authority-state.cjs +527 -0
  79. package/src/codex/hooks/completion-authority.cjs +106 -0
  80. package/src/codex/hooks/lib/hook-context.cjs +58 -4
  81. package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
  82. package/src/codex/hooks/lib/privacy-state.cjs +38 -16
  83. package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
  84. package/src/codex/hooks/lib/spec-receipt.cjs +86 -115
  85. package/src/codex/hooks/lib/spec-utils.cjs +104 -27
  86. package/src/codex/hooks/privacy-approval.cjs +1 -1
  87. package/src/codex/hooks/privacy-block.cjs +67 -11
  88. package/src/codex/hooks/semantic-review-authority.cjs +130 -0
  89. package/src/codex/hooks/session.cjs +2 -0
  90. package/src/codex/hooks/spec-gate.cjs +151 -24
  91. package/src/codex/hooks/spec-state.cjs +65 -12
  92. package/src/codex/hooks/state.cjs +2 -0
  93. package/src/codex/hooks.json +16 -1
  94. package/src/codex/rules/state-sync.md +47 -27
  95. package/src/common/AGENTS.md +1 -1
  96. package/src/opencode/AGENTS.md +4 -2
  97. package/src/opencode/plugins/spec-gate.ts +296 -122
  98. package/src/opencode/plugins/spec-state.ts +183 -116
@@ -0,0 +1,952 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const { spawnSync } = require('node:child_process');
8
+ const test = require('node:test');
9
+
10
+ const PACKAGE_ROOT = path.resolve(__dirname, '../..');
11
+ const SCAFFOLD = path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-scaffold.cjs');
12
+ const VALIDATOR = path.join(PACKAGE_ROOT, 'src/claude/scripts/validate-spec-output.cjs');
13
+ const GROUNDER = path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-ground.cjs');
14
+
15
+ function exec(root, script, args) {
16
+ return spawnSync(process.execPath, [script, ...args], { cwd: root, encoding: 'utf8' });
17
+ }
18
+
19
+ function output(result) { return `${result.stdout}\n${result.stderr}`; }
20
+
21
+ function task(id, title, ownedPath, action = 'modify', dependencies = 'none', criterion = 'R1.1', taskRole = 'subject') {
22
+ return `# Task ${id}: ${title}
23
+ **Status:** pending
24
+
25
+ ## Outcome
26
+ Deliver observable ${title.toLowerCase()} behavior through the declared entrypoint.
27
+
28
+ ## Scope
29
+ - **In scope:** Exact behavior for ${ownedPath}.
30
+ - **Out of scope:** Unrelated runtime behavior.
31
+
32
+ ## Anchors and Ownership
33
+ | ID | Type | Target | Role | Access | Action |
34
+ |---|---|---|---|---|---|
35
+ | A-${id}-01 | file | \`${ownedPath}\` | owner | write | ${action} |
36
+ | A-${id}-02 | artifact | \`artifacts/${id}.json\` | verifier | write | create |
37
+
38
+ ## Changes
39
+ - [ ] ${criterion ? `Implement the owned behavior. _Requirements: ${criterion.slice(1)}_` : 'Verify the subject implementation through the canonical proof boundary.'}
40
+
41
+ ## Acceptance
42
+ ${criterion ? `- **${criterion}:** The command returns the concrete expected state.` : '- The verifier produces an observable proof artifact without claiming the subject acceptance criterion.'}
43
+
44
+ ## Dependencies
45
+ - ${dependencies}
46
+
47
+ ## Verification Plan
48
+ - **Verification ref:** V1
49
+ - **Task role:** ${taskRole}
50
+ - **Command:** \`node --test test/service.test.js\`
51
+ - **Expected:** Exit code 0 and exact state enabled.
52
+ - **Negative path:** Invalid input returns the specified failure state.
53
+ - **Reachability:** \`src/entry.js\`
54
+ `;
55
+ }
56
+
57
+ function makeFixture({ ready = false } = {}) {
58
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-validator-'));
59
+ fs.mkdirSync(path.join(root, 'src'), { recursive: true });
60
+ fs.mkdirSync(path.join(root, 'test'), { recursive: true });
61
+ fs.mkdirSync(path.join(root, 'artifacts'), { recursive: true });
62
+ fs.writeFileSync(path.join(root, 'src/entry.js'), 'module.exports = { enabled: true };\n');
63
+ fs.writeFileSync(path.join(root, 'src/service.js'), 'module.exports = { enabled: true };\n');
64
+ fs.writeFileSync(path.join(root, 'src/design-boundary.js'), 'module.exports = {};\n');
65
+ fs.writeFileSync(path.join(root, 'test/service.test.js'), 'require("node:test")("service",()=>{});\n');
66
+ fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ scripts: { test: 'node --test' } }));
67
+ const boundaries = [{
68
+ id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
69
+ write_sets: {
70
+ 'R1-01': ['src/entry.js', 'artifacts/R1-01.json'],
71
+ 'R1-02': ['src/service.js', 'artifacts/R1-02.json'],
72
+ },
73
+ }, {
74
+ id: 'B-V', type: 'proof', subject: 'R1-01', verifier: 'R1-02',
75
+ verification_ref: 'V1', artifact_anchor: 'A-R1-02-02',
76
+ }];
77
+ const scaffold = exec(root, SCAFFOLD, [
78
+ 'feature', '--tasks', 'R1-01-entry,R1-02-verify', '--boundaries', JSON.stringify(boundaries),
79
+ ]);
80
+ assert.equal(scaffold.status, 0, scaffold.stderr);
81
+ const specDir = path.join(root, 'specs/feature');
82
+ fs.writeFileSync(path.join(specDir, 'requirements.md'), `# Requirements
83
+
84
+ ## Requirements
85
+ ### Requirement 1: Service behavior
86
+ - **R1.1**: When valid input arrives, the system shall return enabled state; invalid input shall return rejected state.
87
+ - **R1.2**: The verifier shall produce a separate observable proof artifact for the enabled and rejected states.
88
+ `);
89
+ fs.writeFileSync(path.join(specDir, 'design.md'), `# Design
90
+
91
+ ## Architecture
92
+ The entry delegates to the service and emits a verifier-owned artifact.
93
+
94
+ ## Typed Anchors
95
+ | ID | Type | Target | Role | Access | Action |
96
+ |---|---|---|---|---|---|
97
+ | A-D-01 | file | \`src/design-boundary.js\` | design boundary | read | read |
98
+ | A-D-02 | artifact | \`artifacts/design-proof.json\` | taskless proof evidence | write | create |
99
+
100
+ ## Canonical Contracts & Invariants
101
+ ### D1 — Delegation decision
102
+ The entry delegates exactly once.
103
+ ### I1 — Invalid-state invariant
104
+ Invalid input never returns enabled state.
105
+ ### C1 — Result contract
106
+ The result contains an enabled boolean.
107
+ ### T1 — Service transition
108
+ The transition moves rejected input to failure and valid input to enabled; recovery retries from rejected.
109
+
110
+ ## Verification Definitions
111
+ - **V1**: Criteria R1.1; Owner R1-01; Proof criteria R1.2; Proof owner R1-02; Evidence anchor A-R1-02-02; Decision refs D1, I1, C1; Method command \`node --test test/service.test.js\`; Expected exit 0 with enabled state and verifier artifact; Negative/failure invalid input returns rejected state without an enabled result; Reachability/grounding entrypoint \`src/entry.js\` via A-D-01, A-R1-02-02.
112
+ `);
113
+ fs.writeFileSync(path.join(specDir, 'tasks/task-R1-01-entry.md'), task('R1-01', 'Entry', 'src/entry.js'));
114
+ fs.writeFileSync(path.join(specDir, 'tasks/task-R1-02-verify.md'), task('R1-02', 'Verify', 'src/service.js', 'modify', 'none', 'R1.2', 'verifier'));
115
+ const promoted = exec(root, SCAFFOLD, ['feature', '--sync-semantic-model']);
116
+ assert.equal(promoted.status, 0, output(promoted));
117
+ const specPath = path.join(specDir, 'spec.json');
118
+ const spec = JSON.parse(fs.readFileSync(specPath, 'utf8'));
119
+ if (ready) {
120
+ spec.authoring.requirements = 'validated';
121
+ spec.authoring.design = 'validated';
122
+ spec.authoring.tasks = 'validated';
123
+ spec.validation.status = 'completed';
124
+ spec.validation.semantic_review.status = 'completed';
125
+ spec.validation.semantic_review.reviewed_criteria = ['R1.1', 'R1.2'];
126
+ spec.validation.semantic_review.counterexamples = [{
127
+ criterion: 'R1.1', case_kind: 'failure',
128
+ scenario: 'The service receives an invalid input object without the required discriminator.',
129
+ expected: 'The service returns rejected state and never emits enabled true.',
130
+ decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
131
+ }, {
132
+ criterion: 'R1.2', case_kind: 'adversarial',
133
+ scenario: 'The implementation reports enabled state without producing the verifier-owned artifact.',
134
+ expected: 'Verification fails because the separate proof artifact is absent.',
135
+ decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
136
+ }];
137
+ fs.writeFileSync(specPath, `${JSON.stringify(spec, null, 2)}\n`);
138
+ const digest = exec(root, VALIDATOR, [specDir, '--semantic-digest']);
139
+ assert.equal(digest.status, 0, output(digest));
140
+ spec.validation.semantic_review.semantic_digest = digest.stdout.trim();
141
+ spec.ready_for_implementation = true;
142
+ }
143
+ fs.writeFileSync(specPath, `${JSON.stringify(spec, null, 2)}\n`);
144
+ return { root, specDir, specPath };
145
+ }
146
+
147
+ function mutateFixture(fixture, mutate) {
148
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
149
+ mutate(state);
150
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
151
+ return exec(fixture.root, VALIDATOR, [fixture.specDir]);
152
+ }
153
+
154
+ function refreshSemanticDigest(fixture) {
155
+ const previous = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
156
+ const promoted = exec(fixture.root, SCAFFOLD, ['feature', '--sync-semantic-model']);
157
+ assert.equal(promoted.status, 0, output(promoted));
158
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
159
+ state.authoring = previous.authoring;
160
+ state.validation = previous.validation;
161
+ state.validation.semantic_review.semantic_digest = null;
162
+ state.ready_for_implementation = false;
163
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
164
+ const digest = exec(fixture.root, VALIDATOR, [fixture.specDir, '--semantic-digest']);
165
+ assert.equal(digest.status, 0, output(digest));
166
+ state.validation.semantic_review.semantic_digest = digest.stdout.trim();
167
+ state.ready_for_implementation = previous.ready_for_implementation;
168
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
169
+ }
170
+
171
+ function promoteSemanticModel(fixture) {
172
+ const promoted = exec(fixture.root, SCAFFOLD, ['feature', '--sync-semantic-model']);
173
+ assert.equal(promoted.status, 0, output(promoted));
174
+ }
175
+
176
+ function appendTaskAnchor(fixture, row) {
177
+ const taskPath = path.join(fixture.specDir, 'tasks/task-R1-01-entry.md');
178
+ const content = fs.readFileSync(taskPath, 'utf8').replace(
179
+ '| A-R1-01-02 | artifact | `artifacts/R1-01.json` | verifier | write | create |',
180
+ `| A-R1-01-02 | artifact | \`artifacts/R1-01.json\` | verifier | write | create |\n${row}`,
181
+ );
182
+ fs.writeFileSync(taskPath, content);
183
+ }
184
+
185
+ function canonicalResearch21() {
186
+ return `# Research
187
+
188
+ ## Uncertainty
189
+ - Which exact service boundary owns the enabled state and its rejected-input behavior?
190
+
191
+ ## Evidence Summary
192
+ - Inspection of \`src/service.js\` shows the module exports the enabled-state boundary used by this fixture.
193
+
194
+ ## Decision
195
+ - Keep the service module as canonical owner because the inspected implementation exposes the required observable state.
196
+
197
+ ## Remaining Gaps
198
+ - None — the inspected service evidence resolves the ownership uncertainty for this bounded fixture.
199
+ `;
200
+ }
201
+
202
+ function makeTasklessReadyFixture() {
203
+ const fixture = makeFixture();
204
+ fs.rmSync(path.join(fixture.specDir, 'tasks'), { recursive: true, force: true });
205
+ const designPath = path.join(fixture.specDir, 'design.md');
206
+ fs.writeFileSync(designPath, fs.readFileSync(designPath, 'utf8')
207
+ .replace('Owner R1-01', 'Owner A-D-01')
208
+ .replace('Proof owner R1-02', 'Proof owner A-D-02')
209
+ .replace('Evidence anchor A-R1-02-02', 'Evidence anchor A-D-02')
210
+ .replace('via A-D-01, A-R1-02-02', 'via A-D-01, A-D-02'));
211
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
212
+ delete state.task_files;
213
+ delete state.task_registry;
214
+ state.coordination = { boundaries: [] };
215
+ state.authoring.requirements = 'validated';
216
+ state.authoring.design = 'validated';
217
+ state.authoring.tasks = 'absent';
218
+ state.validation.status = 'completed';
219
+ state.validation.semantic_review.status = 'completed';
220
+ state.validation.semantic_review.reviewed_criteria = ['R1.1', 'R1.2'];
221
+ state.validation.semantic_review.counterexamples = [{
222
+ criterion: 'R1.1', case_kind: 'failure',
223
+ scenario: 'The service receives invalid input without the required discriminator.',
224
+ expected: 'The service returns rejected state and never emits enabled true.',
225
+ decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
226
+ }, {
227
+ criterion: 'R1.2', case_kind: 'adversarial',
228
+ scenario: 'The subject reports success without producing the design-owned proof artifact.',
229
+ expected: 'Verification fails because the separate taskless proof evidence is absent.',
230
+ decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
231
+ }];
232
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
233
+ const promoted = exec(fixture.root, SCAFFOLD, ['feature', '--sync-semantic-model']);
234
+ assert.equal(promoted.status, 0, output(promoted));
235
+ refreshSemanticDigest(fixture);
236
+ const promotedState = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
237
+ promotedState.ready_for_implementation = true;
238
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(promotedState, null, 2)}\n`);
239
+ return fixture;
240
+ }
241
+
242
+ test('generated schema 2.1 artifact validates after concrete authoring projection', () => {
243
+ const fixture = makeFixture();
244
+ try {
245
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
246
+ assert.equal(result.status, 0, output(result));
247
+ assert.match(output(result), /structural checks do not replace semantic judgment/);
248
+ const ground = exec(fixture.root, GROUNDER, [fixture.specDir, '--root', fixture.root]);
249
+ assert.equal(ground.status, 0, output(ground));
250
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
251
+ });
252
+
253
+ test('validator module API is side-effect free and matches CLI validation and digest results', () => {
254
+ const imported = spawnSync(process.execPath, ['-e', `
255
+ const api = require(${JSON.stringify(VALIDATOR)});
256
+ process.stdout.write(Object.keys(api).sort().join(','));
257
+ `], { encoding: 'utf8' });
258
+ assert.equal(imported.status, 0, output(imported));
259
+ assert.equal(imported.stderr, '');
260
+ assert.equal(imported.stdout, 'computeSemanticDigest21,validateSpec');
261
+
262
+ const fixture = makeFixture();
263
+ try {
264
+ const api = require(VALIDATOR);
265
+ const directValidation = api.validateSpec(fixture.specDir);
266
+ const cliValidation = exec(fixture.root, VALIDATOR, [fixture.specDir]);
267
+ assert.equal(cliValidation.status, directValidation.errors.length === 0 ? 0 : 1, output(cliValidation));
268
+ assert.deepEqual(directValidation.errors, []);
269
+ for (const warning of directValidation.warnings) assert.match(output(cliValidation), new RegExp(`\\[WARN\\] ${warning.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
270
+
271
+ const explicitSpec = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
272
+ const implicitDigest = api.computeSemanticDigest21(fixture.specDir);
273
+ const explicitDigest = api.computeSemanticDigest21(fixture.specDir, explicitSpec);
274
+ const cliDigest = exec(fixture.root, VALIDATOR, [fixture.specDir, '--semantic-digest']);
275
+ assert.equal(cliDigest.status, 0, output(cliDigest));
276
+ assert.deepEqual(implicitDigest, explicitDigest);
277
+ assert.deepEqual(implicitDigest.errors, []);
278
+ assert.equal(implicitDigest.digest, cliDigest.stdout.trim());
279
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
280
+ });
281
+
282
+ test('ready schema 2.1 requires exact receipt coverage and traceable V definition', () => {
283
+ const fixture = makeFixture({ ready: true });
284
+ try {
285
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
286
+ assert.equal(result.status, 0, output(result));
287
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
288
+ });
289
+
290
+ test('digest is stable on lifecycle fields and stale on authoring, policy, or topology changes', () => {
291
+ const fixture = makeFixture({ ready: true });
292
+ try {
293
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
294
+ const digest = state.validation.semantic_review.semantic_digest;
295
+ state.status = 'blocked';
296
+ state.updated_at = '2030-01-01T00:00:00Z';
297
+ state.task_registry['tasks/task-R1-01-entry.md'].status = 'in_progress';
298
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
299
+ assert.equal(exec(fixture.root, VALIDATOR, [fixture.specDir, '--semantic-digest']).stdout.trim(), digest);
300
+ state.scope_lock.in_scope.push('new semantic scope');
301
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
302
+ const stale = exec(fixture.root, VALIDATOR, [fixture.specDir]);
303
+ assert.equal(stale.status, 1);
304
+ assert.match(output(stale), /semantic_digest: stale/);
305
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
306
+ });
307
+
308
+ test('receipt rejects proof self-owner and duplicate generic counterexamples', () => {
309
+ const fixture = makeFixture({ ready: true });
310
+ try {
311
+ let result = mutateFixture(fixture, (state) => {
312
+ const proof = state.coordination.boundaries.find((boundary) => boundary.type === 'proof');
313
+ proof.verifier = proof.subject;
314
+ });
315
+ assert.equal(result.status, 1);
316
+ assert.match(output(result), /proof subject must differ from verifier/);
317
+
318
+ const fresh = makeFixture({ ready: true });
319
+ try {
320
+ result = mutateFixture(fresh, (state) => {
321
+ state.validation.semantic_review.counterexamples.push({
322
+ criterion: 'R1.1', case_kind: 'failure', scenario: 'works', expected: 'correct behavior',
323
+ decision_refs: ['D1'], verification_ref: 'V1',
324
+ });
325
+ });
326
+ assert.equal(result.status, 1);
327
+ assert.match(output(result), /generic boilerplate|duplicate counterexample fingerprint/);
328
+ } finally { fs.rmSync(fresh.root, { recursive: true, force: true }); }
329
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
330
+ });
331
+
332
+ test('validator rejects missing deliverable, recovery, and verifier-owned artifact anchor', () => {
333
+ const cases = [
334
+ [(state) => { state.coordination.boundaries.push({ id: 'B-D', type: 'dependency', producer: 'R1-01', consumer: 'R1-02' }); }, /malformed typed boundary/],
335
+ [(state) => { state.coordination.boundaries.push({ id: 'B-T', type: 'transition', design_ref: 'T1', owner: 'R1-01', consumers: ['R1-02'], precondition: 'input is accepted', postcondition: 'state is enabled', failure: 'state is rejected' }); }, /malformed typed boundary/],
336
+ [(state) => { state.coordination.boundaries.find((boundary) => boundary.type === 'proof').artifact_anchor = 'A-R1-01-02'; }, /must be owned by verifier/],
337
+ ];
338
+ for (const [mutate, expected] of cases) {
339
+ const fixture = makeFixture();
340
+ try {
341
+ const result = mutateFixture(fixture, mutate);
342
+ assert.equal(result.status, 1);
343
+ assert.match(output(result), expected);
344
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
345
+ }
346
+ });
347
+
348
+ test('dependency boundary binds exact producer and consumer anchors to the registry DAG edge', () => {
349
+ const fixture = makeFixture();
350
+ try {
351
+ const producerPath = path.join(fixture.specDir, 'tasks/task-R1-01-entry.md');
352
+ const consumerPath = path.join(fixture.specDir, 'tasks/task-R1-02-verify.md');
353
+ fs.writeFileSync(producerPath, fs.readFileSync(producerPath, 'utf8').replace(
354
+ '| A-R1-01-02 | artifact | `artifacts/R1-01.json` | verifier | write | create |',
355
+ '| A-R1-01-02 | artifact | `artifacts/R1-01.json` | verifier | write | create |\n| A-R1-01-03 | artifact | `artifacts/deliverable.json` | producer | write | create |',
356
+ ));
357
+ fs.writeFileSync(consumerPath, fs.readFileSync(consumerPath, 'utf8').replace(
358
+ '| A-R1-02-02 | artifact | `artifacts/R1-02.json` | verifier | write | create |',
359
+ '| A-R1-02-02 | artifact | `artifacts/R1-02.json` | verifier | write | create |\n| A-R1-02-03 | artifact | `artifacts/deliverable.json` | consumer | read | read |',
360
+ ).replace('## Dependencies\n- none', '## Dependencies\n- tasks/task-R1-01-entry.md'));
361
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
362
+ state.coordination.boundaries.push({
363
+ id: 'B-D', type: 'dependency', producer: 'R1-01', consumer: 'R1-02',
364
+ deliverable: 'artifacts/deliverable.json',
365
+ });
366
+ state.coordination.boundaries.find((boundary) => boundary.type === 'ownership')
367
+ .write_sets['R1-01'].push('artifacts/deliverable.json');
368
+ state.task_registry['tasks/task-R1-02-verify.md'].dependencies = ['tasks/task-R1-01-entry.md'];
369
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
370
+ promoteSemanticModel(fixture);
371
+ const valid = exec(fixture.root, VALIDATOR, [fixture.specDir]);
372
+ assert.equal(valid.status, 0, output(valid));
373
+
374
+ state.task_registry['tasks/task-R1-02-verify.md'].dependencies = [];
375
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
376
+ const drift = exec(fixture.root, VALIDATOR, [fixture.specDir]);
377
+ assert.equal(drift.status, 1);
378
+ assert.match(output(drift), /must bind the registry DAG edge|exact projection/);
379
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
380
+ });
381
+
382
+ test('schema 2.1 reuses core machine lifecycle invariants without mutating artifacts', () => {
383
+ const cases = [
384
+ ['scope-lock-shape', (state) => { state.scope_lock = true; }, /scope_lock: must be an object/],
385
+ ['scope-lock-source', (state) => { state.scope_lock.source = ''; }, /scope_lock\.source: must be a non-empty string/],
386
+ ['scope-lock-duplicates', (state) => { state.scope_lock.in_scope = ['same', 'same']; }, /in_scope: must be an array of unique/],
387
+ ['scope-lock-policy', (state) => { state.scope_lock.expansion_policy = 'silent'; }, /expansion_policy: must be requires-user-approval/],
388
+ ['validation-shape', (state) => { state.validation = 'completed'; }, /validation: must be an object|requires status and semantic_review/],
389
+ ['registry-required-field', (state) => { delete state.task_registry['tasks/task-R1-01-entry.md'].blocker; }, /missing blocker/],
390
+ ['registry-status', (state) => { state.task_registry['tasks/task-R1-01-entry.md'].status = 'ready'; }, /status: must be one of/],
391
+ ['blocked-without-blocker', (state) => {
392
+ state.task_registry['tasks/task-R1-01-entry.md'].status = 'blocked';
393
+ state.task_registry['tasks/task-R1-01-entry.md'].last_updated_at = '2026-08-13T01:00:00Z';
394
+ }, /blocked status requires a non-empty blocker/],
395
+ ['reversed-task-time', (state) => {
396
+ const entry = state.task_registry['tasks/task-R1-01-entry.md'];
397
+ entry.status = 'in_progress';
398
+ entry.started_at = '2026-08-13T02:00:00Z';
399
+ entry.last_updated_at = '2026-08-13T01:00:00Z';
400
+ }, /started_at <= last_updated_at/],
401
+ ['reversed-spec-time', (state) => { state.updated_at = '2000-01-01T00:00:00Z'; }, /created_at <= updated_at/],
402
+ ['ready-validation-fail-closed', (state) => { state.ready_for_implementation = true; }, /requires validation.status completed|requires completed semantic review/],
403
+ ];
404
+ for (const [name, mutate, expected] of cases) {
405
+ const fixture = makeFixture();
406
+ try {
407
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
408
+ mutate(state);
409
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
410
+ const before = fs.readFileSync(fixture.specPath, 'utf8');
411
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
412
+ assert.equal(result.status, 1, `${name} unexpectedly passed\n${output(result)}`);
413
+ assert.match(output(result), expected, name);
414
+ assert.equal(fs.readFileSync(fixture.specPath, 'utf8'), before, `${name} mutated spec.json`);
415
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
416
+ }
417
+ });
418
+
419
+ test('canonical task graph enforces substantive sections and exact criterion authority', () => {
420
+ const cases = [
421
+ ['generic-outcome', (fixture) => {
422
+ const file = path.join(fixture.specDir, 'tasks/task-R1-01-entry.md');
423
+ fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace(
424
+ 'Deliver observable entry behavior through the declared entrypoint.', 'Done.',
425
+ ));
426
+ }, /Outcome must be concrete and non-generic/],
427
+ ['changes-acceptance-drift', (fixture) => {
428
+ const file = path.join(fixture.specDir, 'tasks/task-R1-01-entry.md');
429
+ fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace('_Requirements: 1.1_', '_Requirements: 1.2_'));
430
+ }, /Changes and Acceptance must trace the same exact RN.M criteria/],
431
+ ['orphan-criterion', (fixture) => {
432
+ fs.appendFileSync(path.join(fixture.specDir, 'requirements.md'), '- **R1.3**: The system shall return a distinct audit state.\n');
433
+ }, /R1\.3: (?:acceptance criterion not covered by any task|requires exactly one task implementation owner)/],
434
+ ['duplicate-local-authority', (fixture) => {
435
+ const file = path.join(fixture.specDir, 'tasks/task-R1-01-entry.md');
436
+ fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace(
437
+ '- **R1.1:** The command returns the concrete expected state.',
438
+ '- **R1.1:** The command returns the concrete expected state.\n- **R1.1:** The same criterion is claimed twice.',
439
+ ));
440
+ }, /Acceptance contains duplicate RN.M authority/],
441
+ ['proof-verifier-cannot-duplicate-acceptance', (fixture) => {
442
+ const file = path.join(fixture.specDir, 'tasks/task-R1-02-verify.md');
443
+ fs.writeFileSync(file, fs.readFileSync(file, 'utf8')
444
+ .replace('_Requirements: 1.2_', '_Requirements: 1.1_')
445
+ .replace(/R1\.2/g, 'R1.1'));
446
+ }, /requires exactly one implementation owner/],
447
+ ];
448
+ for (const [name, mutate, expected] of cases) {
449
+ const fixture = makeFixture();
450
+ try {
451
+ mutate(fixture);
452
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
453
+ assert.equal(result.status, 1, `${name} unexpectedly passed\n${output(result)}`);
454
+ assert.match(output(result), expected, name);
455
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
456
+ }
457
+ });
458
+
459
+ test('research authoring is an exact pointer-file state machine and participates in digest drift', () => {
460
+ const invalidCases = [
461
+ ['absent-pointer', (fixture, state) => { state.research = 'research.md'; }, /absent requires no pointer/],
462
+ ['absent-file', (fixture) => { fs.writeFileSync(path.join(fixture.specDir, 'research.md'), '# Research\n'); }, /absent requires no physical artifact/],
463
+ ['draft-missing', (_fixture, state) => { state.authoring.research = 'draft'; }, /requires canonical pointer|requires a physical regular file/],
464
+ ];
465
+ for (const [name, mutate, expected] of invalidCases) {
466
+ const fixture = makeFixture();
467
+ try {
468
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
469
+ mutate(fixture, state);
470
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
471
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
472
+ assert.equal(result.status, 1, `${name} unexpectedly passed`);
473
+ assert.match(output(result), expected, name);
474
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
475
+ }
476
+
477
+ const structuralMutations = [
478
+ ...['Uncertainty', 'Evidence Summary', 'Decision', 'Remaining Gaps'].map((heading) => [
479
+ `missing-${heading}`, (content) => content.replace(`## ${heading}`, `## Missing ${heading}`),
480
+ new RegExp(`requires exactly one ## ${heading} section`),
481
+ ]),
482
+ ...['Uncertainty', 'Evidence Summary', 'Decision', 'Remaining Gaps'].map((heading) => [
483
+ `duplicate-${heading}`, (content) => `${content}\n## ${heading}\n- A second machine section must be rejected.\n`,
484
+ new RegExp(`requires exactly one ## ${heading} section`),
485
+ ]),
486
+ ['placeholder-uncertainty', (content) => content.replace(/## Uncertainty[\s\S]*?## Evidence Summary/, '## Uncertainty\n- TBD\n\n## Evidence Summary'), /Uncertainty must contain concrete non-placeholder content/],
487
+ ['placeholder-evidence', (content) => content.replace(/## Evidence Summary[\s\S]*?## Decision/, '## Evidence Summary\n- TODO\n\n## Decision'), /Evidence Summary must contain concrete non-placeholder content/],
488
+ ['placeholder-decision', (content) => content.replace(/## Decision[\s\S]*?## Remaining Gaps/, '## Decision\n- Pending\n\n## Remaining Gaps'), /Decision must contain concrete non-placeholder content/],
489
+ ['ungrounded-evidence', (content) => content.replace('`src/service.js`', '`src/phantom-service.js`'), /Evidence Summary must ground/],
490
+ ['bare-none', (content) => content.replace(/- None[^\n]+/, '- None'), /Remaining Gaps None requires concrete rationale or evidence/],
491
+ ['placeholder-gap', (content) => content.replace(/- None[^\n]+/, '- TODO'), /Remaining Gaps must contain a concrete gap/],
492
+ ];
493
+ for (const [name, mutate, expected] of structuralMutations) {
494
+ const fixture = makeFixture();
495
+ try {
496
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
497
+ state.authoring.research = 'draft';
498
+ state.research = 'research.md';
499
+ fs.writeFileSync(path.join(fixture.specDir, 'research.md'), mutate(canonicalResearch21()));
500
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
501
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
502
+ assert.equal(result.status, 1, `${name} unexpectedly passed\n${output(result)}`);
503
+ assert.match(output(result), expected, name);
504
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
505
+ }
506
+
507
+ for (const remainingGaps of [
508
+ '- None — the inspected repository evidence resolves the bounded ownership question.',
509
+ '- The external retry limit remains unresolved and can change the recovery design.',
510
+ ]) {
511
+ const fixture = makeFixture();
512
+ try {
513
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
514
+ state.authoring.research = 'draft';
515
+ state.research = 'research.md';
516
+ fs.writeFileSync(path.join(fixture.specDir, 'research.md'), canonicalResearch21().replace(/- None[^\n]+/, remainingGaps));
517
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
518
+ assert.equal(exec(fixture.root, VALIDATOR, [fixture.specDir]).status, 0);
519
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
520
+ }
521
+
522
+ const fixture = makeFixture({ ready: true });
523
+ try {
524
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
525
+ state.authoring.research = 'validated';
526
+ state.research = 'research.md';
527
+ fs.writeFileSync(path.join(fixture.specDir, 'research.md'), canonicalResearch21());
528
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
529
+ const digest = exec(fixture.root, VALIDATOR, [fixture.specDir, '--semantic-digest']);
530
+ assert.equal(digest.status, 0, output(digest));
531
+ state.validation.semantic_review.semantic_digest = digest.stdout.trim();
532
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
533
+ assert.equal(exec(fixture.root, VALIDATOR, [fixture.specDir]).status, 0);
534
+ fs.appendFileSync(path.join(fixture.specDir, 'research.md'), '\nA second grounded conclusion changes the authored research artifact.\n');
535
+ const stale = exec(fixture.root, VALIDATOR, [fixture.specDir]);
536
+ assert.equal(stale.status, 1);
537
+ assert.match(output(stale), /stale.*research/);
538
+ fs.unlinkSync(path.join(fixture.specDir, 'research.md'));
539
+ const refused = exec(fixture.root, VALIDATOR, [fixture.specDir, '--semantic-digest']);
540
+ assert.equal(refused.status, 1);
541
+ assert.doesNotMatch(refused.stdout, /^sha256:/m);
542
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
543
+ });
544
+
545
+ test('canonical T/V definitions, proof anchors, phases, and access normalization share one model', () => {
546
+ const invalidCases = [
547
+ ['incidental-verification', (fixture) => {
548
+ const file = path.join(fixture.specDir, 'design.md');
549
+ fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace('- **V1**:', '- Verification V1:'));
550
+ }, /Verification Definitions requires at least one canonical V definition|missing canonical V definition/],
551
+ ['source-write-as-proof', (fixture, state) => {
552
+ state.coordination.boundaries.find((boundary) => boundary.type === 'proof').artifact_anchor = 'A-R1-02-01';
553
+ }, /source write is not proof/],
554
+ ['ownership-owned-phase', (_fixture, state) => {
555
+ state.coordination.phases = [{
556
+ id: 'P1', task_ids: ['R1-01', 'R1-02'], entry_condition: 'Both tasks are authored.',
557
+ exit_condition: 'Both task outcomes are verified.', owner_boundary: 'B-OWN',
558
+ }];
559
+ }, /ownership cannot own phase progression/],
560
+ ['phase-participant-and-coverage-drift', (_fixture, state) => {
561
+ state.coordination.phases = [{
562
+ id: 'P1', task_ids: ['R1-01'], entry_condition: 'The subject task is authored.',
563
+ exit_condition: 'The subject task is verified.', owner_boundary: 'B-V',
564
+ }];
565
+ }, /participants must match phase task_ids exactly|R1-02 must appear in exactly one phase/],
566
+ ['incidental-transition-reference', (fixture, state) => {
567
+ const design = path.join(fixture.specDir, 'design.md');
568
+ fs.writeFileSync(design, fs.readFileSync(design, 'utf8').replace('### T1 — Service transition', 'The prose mentions T1 service transition'));
569
+ state.coordination.boundaries.push({
570
+ id: 'B-T', type: 'transition', design_ref: 'T1', owner: 'R1-01', consumers: ['R1-02'],
571
+ precondition: 'Valid input is accepted by the entry boundary.',
572
+ postcondition: 'Enabled state is persisted for the consumer.',
573
+ failure: 'Invalid input leaves the service rejected.',
574
+ recovery: 'Retry begins from the rejected state without duplicate output.',
575
+ });
576
+ }, /missing canonical design T definition/],
577
+ ...[
578
+ ['Criteria', 'Coverage'],
579
+ ['Owner', 'Ownership'],
580
+ ['Proof criteria', 'Proof coverage'],
581
+ ['Proof owner', 'Verifier'],
582
+ ['Evidence anchor', 'Evidence'],
583
+ ['Decision refs', 'References'],
584
+ ['Method', 'Procedure'],
585
+ ['Expected', 'Outcome'],
586
+ ['Negative/failure', 'Failure'],
587
+ ['Reachability/grounding', 'Grounding'],
588
+ ].map(([label, replacement]) => [`v-definition-missing-${label}`, (fixture) => {
589
+ const design = path.join(fixture.specDir, 'design.md');
590
+ fs.writeFileSync(design, fs.readFileSync(design, 'utf8').replace(label, replacement));
591
+ }, /V definition must use Criteria; Owner; optional Proof criteria; Proof owner; Evidence anchor; Decision refs; Method; Expected; Negative\/failure; Reachability\/grounding grammar/]),
592
+ ];
593
+ for (const [name, mutate, expected] of invalidCases) {
594
+ const fixture = makeFixture();
595
+ try {
596
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
597
+ mutate(fixture, state);
598
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
599
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
600
+ assert.equal(result.status, 1, `${name} unexpectedly passed\n${output(result)}`);
601
+ assert.match(output(result), expected, name);
602
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
603
+ }
604
+
605
+ const fixture = makeFixture();
606
+ try {
607
+ for (const name of ['tasks/task-R1-01-entry.md', 'tasks/task-R1-02-verify.md']) {
608
+ const file = path.join(fixture.specDir, name);
609
+ fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace(/\| write \| modify \|/i, '| WRITE | MODIFY |'));
610
+ }
611
+ promoteSemanticModel(fixture);
612
+ const validate = exec(fixture.root, VALIDATOR, [fixture.specDir]);
613
+ const ground = exec(fixture.root, GROUNDER, [fixture.specDir, '--root', fixture.root]);
614
+ assert.equal(validate.status, 0, output(validate));
615
+ assert.equal(ground.status, 0, output(ground));
616
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
617
+ });
618
+
619
+ test('explicit Access and Action remain authoritative when Role prose claims create', () => {
620
+ const fixture = makeFixture({ ready: true });
621
+ try {
622
+ const design = path.join(fixture.specDir, 'design.md');
623
+ fs.writeFileSync(design, fs.readFileSync(design, 'utf8').replace(
624
+ '| A-D-01 | file | `src/design-boundary.js` | design boundary | read | read |',
625
+ '| A-D-01 | file | `src/role-cannot-create.js` | create this planned file | read | read |',
626
+ ));
627
+ refreshSemanticDigest(fixture);
628
+ const validator = exec(fixture.root, VALIDATOR, [fixture.specDir]);
629
+ const grounder = exec(fixture.root, GROUNDER, [fixture.specDir, '--root', fixture.root]);
630
+ assert.equal(validator.status, 1, output(validator));
631
+ assert.equal(grounder.status, 1, output(grounder));
632
+ assert.match(output(validator), /A-D-01: read target not found.*src\/role-cannot-create\.js/);
633
+ assert.match(output(grounder), /A-D-01: read target not found.*src\/role-cannot-create\.js/);
634
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
635
+ });
636
+
637
+ test('typed boundaries require exact non-empty resource maps and participation by every task', () => {
638
+ const mapMutations = [
639
+ ['empty', (boundary) => { boundary.write_sets['R1-01'] = []; }, /must be a non-empty array of unique exact targets/],
640
+ ['extra', (boundary) => { boundary.write_sets['R1-01'].push('src/unclaimed.js'); }, /lacks exact task write anchor/],
641
+ ];
642
+ for (const [name, mutate, expected] of mapMutations) {
643
+ const fixture = makeFixture();
644
+ try {
645
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
646
+ mutate(state.coordination.boundaries.find((boundary) => boundary.type === 'ownership'));
647
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
648
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
649
+ assert.equal(result.status, 1, `${name}: ${output(result)}`);
650
+ assert.match(output(result), expected);
651
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
652
+ }
653
+
654
+ const fixture = makeFixture();
655
+ try {
656
+ const thirdPath = 'tasks/task-R1-03-third.md';
657
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
658
+ fs.writeFileSync(path.join(fixture.root, 'src/third.js'), 'module.exports = { enabled: true };\n');
659
+ fs.writeFileSync(path.join(fixture.specDir, thirdPath), task('R1-03', 'Third', 'src/third.js', 'modify', 'none', '', 'subject'));
660
+ state.task_files.push(thirdPath);
661
+ state.task_registry[thirdPath] = {
662
+ ...state.task_registry['tasks/task-R1-01-entry.md'], id: 'R1-03', title: 'Third', dependencies: [],
663
+ };
664
+ const ownership = state.coordination.boundaries.find((boundary) => boundary.type === 'ownership');
665
+ ownership.tasks.push('R1-03');
666
+ ownership.write_sets['R1-03'] = ['src/third.js', 'artifacts/R1-03.json'];
667
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
668
+ promoteSemanticModel(fixture);
669
+ assert.equal(exec(fixture.root, VALIDATOR, [fixture.specDir]).status, 0);
670
+
671
+ ownership.tasks = ownership.tasks.filter((id) => id !== 'R1-03');
672
+ delete ownership.write_sets['R1-03'];
673
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
674
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
675
+ assert.equal(result.status, 1, output(result));
676
+ assert.match(output(result), /task R1-03 must participate in at least one justified typed boundary/);
677
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
678
+ });
679
+
680
+ test('schema 2.1 rejects unknown and dead machine fields outside the explicit adapter', () => {
681
+ const cases = [
682
+ ['top-level', (state) => { state.dead_machine_field = true; }, /dead_machine_field: unknown or legacy field is not canonical in closed schema 2\.1/],
683
+ ['validation', (state) => { state.validation.legacy_digest = 'inert'; }, /validation: fields must be exactly status, semantic_review/],
684
+ ['scope-lock', (state) => { state.scope_lock.legacy_mode = true; }, /scope_lock: fields must be exactly/],
685
+ ];
686
+ for (const [name, mutate, expected] of cases) {
687
+ const fixture = makeFixture();
688
+ try {
689
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
690
+ mutate(state);
691
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
692
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
693
+ assert.equal(result.status, 1, `${name}: ${output(result)}`);
694
+ assert.match(output(result), expected);
695
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
696
+ }
697
+ });
698
+
699
+ test('digest mode refuses malformed physical topology and tracks substantive mutation classes', () => {
700
+ const mutationCases = [
701
+ ['authoring', (_fixture, state) => { state.authoring.design = 'draft'; }],
702
+ ['policy', (_fixture, state) => {
703
+ state.workflow_policy.assurance_level = 'Elevated';
704
+ state.workflow_policy.classified_minimum.assurance_level = 'Elevated';
705
+ }],
706
+ ['coordination', (_fixture, state) => { state.coordination.boundaries[0].id = 'B-OWN-RENAMED'; }],
707
+ ['task-content', (fixture) => {
708
+ const file = path.join(fixture.specDir, 'tasks/task-R1-01-entry.md');
709
+ fs.appendFileSync(file, '\nConcrete authoring note changes the owned behavior.\n');
710
+ }],
711
+ ];
712
+ for (const [name, mutate] of mutationCases) {
713
+ const fixture = makeFixture({ ready: true });
714
+ try {
715
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
716
+ const original = state.validation.semantic_review.semantic_digest;
717
+ mutate(fixture, state);
718
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
719
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir, '--semantic-digest']);
720
+ assert.equal(result.status, 0, `${name}: ${output(result)}`);
721
+ assert.notEqual(result.stdout.trim(), original, `${name} did not invalidate digest`);
722
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
723
+ }
724
+
725
+ const malformed = makeFixture();
726
+ try {
727
+ const taskPath = path.join(malformed.specDir, 'tasks/task-R1-02-verify.md');
728
+ fs.writeFileSync(taskPath, fs.readFileSync(taskPath, 'utf8').replace('## Anchors and Ownership', '## Broken Ownership'));
729
+ const result = exec(malformed.root, VALIDATOR, [malformed.specDir, '--semantic-digest']);
730
+ assert.equal(result.status, 1);
731
+ assert.doesNotMatch(result.stdout, /^sha256:/m);
732
+ assert.match(output(result), /digest requires exactly one ## Anchors and Ownership|requires one table/);
733
+ } finally { fs.rmSync(malformed.root, { recursive: true, force: true }); }
734
+ });
735
+
736
+ test('taskless ready spec rejects a phantom design anchor through shared grounding', () => {
737
+ const fixture = makeTasklessReadyFixture();
738
+ try {
739
+ const designPath = path.join(fixture.specDir, 'design.md');
740
+ fs.writeFileSync(designPath, fs.readFileSync(designPath, 'utf8')
741
+ .replace('src/design-boundary.js', 'src/phantom-boundary.js'));
742
+ refreshSemanticDigest(fixture);
743
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
744
+ assert.equal(result.status, 1, output(result));
745
+ assert.match(output(result), /grounding: design\.md: A-D-01: read target not found/);
746
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
747
+ });
748
+
749
+ test('ready task rejects a dangling canonical A-D reference', () => {
750
+ const fixture = makeFixture({ ready: true });
751
+ try {
752
+ const taskPath = path.join(fixture.specDir, 'tasks/task-R1-01-entry.md');
753
+ fs.writeFileSync(taskPath, fs.readFileSync(taskPath, 'utf8')
754
+ .replace('- **In scope:**', '- **Canonical design anchor:** A-D-99\n- **In scope:**'));
755
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
756
+ assert.equal(result.status, 1, output(result));
757
+ assert.match(output(result), /dangling canonical design anchor reference A-D-99/);
758
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
759
+ });
760
+
761
+ test('dependency topology cannot legalize duplicate criterion implementation owners', () => {
762
+ const fixture = makeFixture();
763
+ try {
764
+ const producerPath = path.join(fixture.specDir, 'tasks/task-R1-01-entry.md');
765
+ const verifierPath = path.join(fixture.specDir, 'tasks/task-R1-02-verify.md');
766
+ fs.writeFileSync(producerPath, fs.readFileSync(producerPath, 'utf8').replace(
767
+ '| A-R1-01-02 | artifact | `artifacts/R1-01.json` | verifier | write | create |',
768
+ '| A-R1-01-02 | artifact | `artifacts/R1-01.json` | verifier | write | create |\n| A-R1-01-03 | artifact | `artifacts/dependency.json` | producer | write | create |',
769
+ ));
770
+ fs.writeFileSync(verifierPath, fs.readFileSync(verifierPath, 'utf8')
771
+ .replace('_Requirements: 1.2_', '_Requirements: 1.1_')
772
+ .replace(/R1\.2/g, 'R1.1')
773
+ .replace('| A-R1-02-02 | artifact | `artifacts/R1-02.json` | verifier | write | create |', '| A-R1-02-02 | artifact | `artifacts/R1-02.json` | verifier | write | create |\n| A-R1-02-03 | artifact | `artifacts/dependency.json` | consumer | read | read |')
774
+ .replace('## Dependencies\n- none', '## Dependencies\n- tasks/task-R1-01-entry.md'));
775
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
776
+ state.coordination.boundaries = [{
777
+ id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
778
+ write_sets: {
779
+ 'R1-01': ['src/entry.js', 'artifacts/R1-01.json', 'artifacts/dependency.json'],
780
+ 'R1-02': ['src/service.js', 'artifacts/R1-02.json'],
781
+ },
782
+ }, {
783
+ id: 'B-D', type: 'dependency', producer: 'R1-01', consumer: 'R1-02',
784
+ deliverable: 'artifacts/dependency.json',
785
+ }];
786
+ state.task_registry['tasks/task-R1-02-verify.md'].dependencies = ['tasks/task-R1-01-entry.md'];
787
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
788
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
789
+ assert.equal(result.status, 1, output(result));
790
+ assert.match(output(result), /R1\.1: requires exactly one implementation owner; claimed by R1-01, R1-02/);
791
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
792
+ });
793
+
794
+ test('separate proof verifier passes without duplicating subject acceptance ownership', () => {
795
+ const fixture = makeFixture({ ready: true });
796
+ try {
797
+ const verifier = fs.readFileSync(path.join(fixture.specDir, 'tasks/task-R1-02-verify.md'), 'utf8');
798
+ assert.doesNotMatch(verifier, /^\s*[-*+]\s+(?:\*\*)?R1\.1/m);
799
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
800
+ assert.equal(result.status, 0, output(result));
801
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
802
+ });
803
+
804
+ test('readiness action matrix permits absent create but rejects absent read modify and delete', () => {
805
+ const cases = [
806
+ ['create', 'write', 0],
807
+ ['read', 'read', 1],
808
+ ['modify', 'write', 1],
809
+ ['delete', 'write', 1],
810
+ ];
811
+ for (const [action, access, expectedStatus] of cases) {
812
+ const fixture = makeFixture({ ready: true });
813
+ try {
814
+ appendTaskAnchor(fixture, `| A-R1-01-03 | file | \`src/missing-${action}.js\` | owner | ${access} | ${action} |`);
815
+ if (access === 'write') {
816
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
817
+ state.coordination.boundaries.find((boundary) => boundary.type === 'ownership')
818
+ .write_sets['R1-01'].push(`src/missing-${action}.js`);
819
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
820
+ }
821
+ refreshSemanticDigest(fixture);
822
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
823
+ assert.equal(result.status, expectedStatus, `${action}: ${output(result)}`);
824
+ if (expectedStatus === 1) assert.match(output(result), new RegExp(`grounding: .* ${action} target not found`));
825
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
826
+ }
827
+ });
828
+
829
+ test('ready semantic digest is stale after anchor topology or policy edits', () => {
830
+ const cases = [
831
+ (fixture) => {
832
+ const file = path.join(fixture.specDir, 'tasks/task-R1-01-entry.md');
833
+ fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace('src/entry.js', 'src/renamed-entry.js'));
834
+ },
835
+ (fixture, state) => { state.coordination.boundaries[0].id = 'B-OWN-EDITED'; },
836
+ (_fixture, state) => {
837
+ state.workflow_policy.assurance_level = 'Elevated';
838
+ state.workflow_policy.classified_minimum.assurance_level = 'Elevated';
839
+ },
840
+ ];
841
+ for (const mutate of cases) {
842
+ const fixture = makeFixture({ ready: true });
843
+ try {
844
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
845
+ mutate(fixture, state);
846
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
847
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
848
+ assert.equal(result.status, 1, output(result));
849
+ assert.match(output(result), /semantic_digest: stale/);
850
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
851
+ }
852
+ });
853
+
854
+ test('validator and CLI grounder reject the same missing modify counterexample', () => {
855
+ const fixture = makeFixture({ ready: true });
856
+ try {
857
+ appendTaskAnchor(fixture, '| A-R1-01-03 | file | `src/shared-missing.js` | owner | write | modify |');
858
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
859
+ state.coordination.boundaries.find((boundary) => boundary.type === 'ownership')
860
+ .write_sets['R1-01'].push('src/shared-missing.js');
861
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
862
+ refreshSemanticDigest(fixture);
863
+ const validator = exec(fixture.root, VALIDATOR, [fixture.specDir]);
864
+ const grounder = exec(fixture.root, GROUNDER, [fixture.specDir, '--root', fixture.root]);
865
+ assert.equal(validator.status, 1, output(validator));
866
+ assert.equal(grounder.status, 1, output(grounder));
867
+ const counterexample = /tasks\/task-R1-01-entry\.md: A-R1-01-03: modify target not found in work tree: src\/shared-missing\.js/;
868
+ assert.match(output(validator), counterexample);
869
+ assert.match(output(grounder), counterexample);
870
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
871
+ });
872
+
873
+ test('schema 2.1 policy is exactly five fields and rejects inert override receipt locations', () => {
874
+ const cases = [
875
+ (state) => { state.override_receipt = null; },
876
+ (state) => { state.workflow_policy.override_receipt = null; },
877
+ ];
878
+ for (const mutate of cases) {
879
+ const fixture = makeFixture();
880
+ try {
881
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
882
+ assert.deepEqual(Object.keys(state.workflow_policy).sort(), [
883
+ 'assurance_level', 'classified_minimum', 'planning_depth', 'risks', 'version',
884
+ ]);
885
+ mutate(state);
886
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
887
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
888
+ assert.equal(result.status, 1, output(result));
889
+ assert.match(output(result), /override receipt is inert|v2\.1 fields must be exactly/);
890
+ assert.doesNotMatch(output(result), /persist only workflow_policy\.override_receipt/);
891
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
892
+ }
893
+ });
894
+
895
+ test('semantic authority mutation families reject empty inventory, ambiguous owners, vague proof, and projection drift', () => {
896
+ const cases = [
897
+ ['empty-criteria', (fixture) => {
898
+ const file = path.join(fixture.specDir, 'requirements.md');
899
+ fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace(/^- \*\*R1\.[12]\*\*:[^\n]+\n/gm, ''));
900
+ }, /non-empty RN\.M inventory|criteria: must be a non-empty array/],
901
+ ['taskless-owner-ambiguity', (fixture, state) => {
902
+ state.semantic_model.verification_definitions[0].proof_owner = 'A-D-01';
903
+ state.semantic_model.verification_definitions[0].evidence_anchor = 'A-D-01';
904
+ state.semantic_model.criteria.find((criterion) => criterion.kind === 'proof').owner = 'A-D-01';
905
+ }, /subject\/proof owners must be distinct/],
906
+ ['vague-method', (fixture) => {
907
+ const file = path.join(fixture.specDir, 'design.md');
908
+ fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace(
909
+ 'Method command `node --test test/service.test.js`', 'Method inspect it',
910
+ ));
911
+ }, /Method must be exact command|structured method/],
912
+ ['vague-reachability', (fixture) => {
913
+ const file = path.join(fixture.specDir, 'design.md');
914
+ fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace(
915
+ 'Reachability/grounding entrypoint `src/entry.js` via A-D-01, A-R1-02-02',
916
+ 'Reachability/grounding reachable through the application',
917
+ ));
918
+ }, /Reachability\/grounding must be entrypoint|projection drift/],
919
+ ['machine-markdown-drift', (_fixture, state) => {
920
+ state.semantic_model.anchors[0].target = 'src/authority-only-edit.js';
921
+ }, /semantic_model: differs from Markdown projection/],
922
+ ];
923
+ for (const [name, mutate, expected] of cases) {
924
+ const fixture = name === 'taskless-owner-ambiguity' ? makeTasklessReadyFixture() : makeFixture();
925
+ try {
926
+ const state = JSON.parse(fs.readFileSync(fixture.specPath, 'utf8'));
927
+ mutate(fixture, state);
928
+ fs.writeFileSync(fixture.specPath, `${JSON.stringify(state, null, 2)}\n`);
929
+ const result = exec(fixture.root, VALIDATOR, [fixture.specDir]);
930
+ assert.equal(result.status, 1, `${name}: ${output(result)}`);
931
+ assert.match(output(result), expected, name);
932
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
933
+ }
934
+ });
935
+
936
+ test('route and schema grounding never accepts design self-reference as repository evidence', () => {
937
+ for (const type of ['route', 'schema']) for (const action of ['create', 'modify']) {
938
+ const fixture = makeFixture();
939
+ try {
940
+ const designPath = path.join(fixture.specDir, 'design.md');
941
+ const id = type === 'route' ? 'A-D-03' : 'A-D-04';
942
+ const target = type === 'route' ? 'POST /self-grounded' : 'SelfGroundedRecord';
943
+ fs.writeFileSync(designPath, fs.readFileSync(designPath, 'utf8')
944
+ .replace('| A-D-02 | artifact', `| ${id} | ${type} | \`${target}\` | registration boundary | write | ${action} |\n| A-D-02 | artifact`)
945
+ .replace('via A-D-01, A-R1-02-02', `via A-D-01, ${id}, A-R1-02-02`));
946
+ promoteSemanticModel(fixture);
947
+ const result = exec(fixture.root, GROUNDER, [fixture.specDir, '--root', fixture.root]);
948
+ assert.equal(result.status, action === 'create' ? 0 : 1, `${type}/${action}: ${output(result)}`);
949
+ if (action === 'modify') assert.match(output(result), new RegExp(`${type} target is not semantically reachable`));
950
+ } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }
951
+ }
952
+ });