@haposoft/cafekit 0.18.0 → 0.18.2

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 (162) hide show
  1. package/README.md +12 -1
  2. package/bin/__tests__/claude-settings.test.js +123 -0
  3. package/bin/__tests__/codex-ask-guard.test.js +123 -0
  4. package/bin/__tests__/codex-ask-projection.test.js +51 -0
  5. package/bin/__tests__/codex-hooks-ownership.test.js +80 -1
  6. package/bin/__tests__/codex-hooks.test.js +938 -237
  7. package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
  8. package/bin/__tests__/codex-native.test.js +222 -149
  9. package/bin/__tests__/codex-projection-parity.test.js +314 -0
  10. package/bin/__tests__/codex-quiet-hooks.test.js +170 -0
  11. package/bin/__tests__/codex-research-projection.test.js +70 -0
  12. package/bin/__tests__/codex-specs-projection.test.js +76 -0
  13. package/bin/__tests__/develop-contract.test.js +102 -1362
  14. package/bin/__tests__/docs-sync-on-demand.test.js +25 -20
  15. package/bin/__tests__/inspect-paths.test.js +183 -0
  16. package/bin/__tests__/installer-safety.test.js +335 -0
  17. package/bin/__tests__/legacy-authority-decoupled.test.js +14 -19
  18. package/bin/__tests__/legacy-hooks-retired.test.js +18 -8
  19. package/bin/__tests__/omp-bridge.test.js +66 -12
  20. package/bin/__tests__/omp-hooks.test.js +11 -5
  21. package/bin/__tests__/orca-session.test.js +7 -0
  22. package/bin/__tests__/package-inventory.test.js +1 -395
  23. package/bin/__tests__/privacy-paths.test.js +317 -0
  24. package/bin/__tests__/spec-narrowing.test.js +30 -134
  25. package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -372
  26. package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
  27. package/bin/__tests__/usage-hook-retired.test.js +5 -3
  28. package/bin/__tests__/validator-grounding.test.js +17 -2929
  29. package/bin/install.js +25 -0
  30. package/bin/lib/codex-install.js +134 -12
  31. package/bin/lib/context.js +1 -1
  32. package/bin/lib/managed-writer.js +2 -3
  33. package/bin/phases/claude-runtime.js +91 -25
  34. package/bin/phases/claude-settings.js +22 -1
  35. package/bin/phases/codex-hooks.js +96 -33
  36. package/bin/phases/codex-runtime.js +45 -49
  37. package/bin/phases/copy-payload.js +11 -28
  38. package/bin/phases/omp-runtime.js +6 -3
  39. package/package.json +2 -3
  40. package/src/claude/CLAUDE.md +4 -0
  41. package/src/claude/agents/code-auditor.md +9 -6
  42. package/src/claude/agents/debugger.md +3 -9
  43. package/src/claude/agents/docs-keeper.md +4 -7
  44. package/src/claude/agents/git-ops.md +2 -4
  45. package/src/claude/agents/implementer.md +6 -7
  46. package/src/claude/agents/inspector.md +2 -0
  47. package/src/claude/agents/project-manager.md +2 -2
  48. package/src/claude/agents/spec-maker.md +4 -4
  49. package/src/claude/agents/test-runner.md +8 -11
  50. package/src/claude/agents/ui-ux-designer.md +0 -1
  51. package/src/claude/gitignore +3 -0
  52. package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
  53. package/src/claude/hooks/__tests__/precompact.test.js +206 -10
  54. package/src/claude/hooks/__tests__/privacy-block.test.js +27 -0
  55. package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
  56. package/src/claude/hooks/__tests__/spec-gate.test.js +311 -340
  57. package/src/claude/hooks/__tests__/spec-state-touched.test.js +230 -8
  58. package/src/claude/hooks/__tests__/state.test.js +207 -7
  59. package/src/claude/hooks/__tests__/update-check.test.js +88 -0
  60. package/src/claude/hooks/agent.cjs +7 -6
  61. package/src/claude/hooks/inspect-block.cjs +15 -63
  62. package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
  63. package/src/claude/hooks/lib/parser.cjs +1 -1
  64. package/src/claude/hooks/lib/privacy-command-analysis.cjs +191 -17
  65. package/src/claude/hooks/lib/privacy-paths.cjs +313 -0
  66. package/src/claude/hooks/lib/project-root.cjs +15 -0
  67. package/src/claude/hooks/lib/rules-reservation.cjs +36 -0
  68. package/src/claude/hooks/lib/spec-touch.cjs +67 -0
  69. package/src/claude/hooks/lib/update-check.cjs +77 -0
  70. package/src/claude/hooks/precompact.cjs +8 -15
  71. package/src/claude/hooks/privacy-block.cjs +23 -205
  72. package/src/claude/hooks/rules.cjs +6 -28
  73. package/src/claude/hooks/session.cjs +34 -117
  74. package/src/claude/hooks/spec-gate.cjs +17 -89
  75. package/src/claude/hooks/spec-state.cjs +89 -96
  76. package/src/claude/hooks/state.cjs +96 -51
  77. package/src/claude/migration-manifest.json +20 -8
  78. package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
  79. package/src/claude/output-styles/coding-level-1-junior.md +3 -3
  80. package/src/claude/output-styles/coding-level-2-mid.md +3 -3
  81. package/src/claude/output-styles/coding-level-3-senior.md +2 -2
  82. package/src/claude/output-styles/coding-level-4-lead.md +5 -5
  83. package/src/claude/output-styles/coding-level-5-god.md +6 -6
  84. package/src/claude/rules/ai-dev-rules.md +1 -2
  85. package/src/claude/rules/hook-protocols.md +1 -1
  86. package/src/claude/rules/workflow.md +1 -1
  87. package/src/claude/runtime.json +0 -3
  88. package/src/claude/scripts/generate-skill-catalog.cjs +2 -2
  89. package/src/claude/scripts/provenance.cjs +14 -0
  90. package/src/claude/scripts/spec-receipt.cjs +0 -114
  91. package/src/claude/scripts/spec-resolver.cjs +41 -457
  92. package/src/claude/scripts/workflow-policy.cjs +3 -1801
  93. package/src/claude/settings/settings.json +1 -1
  94. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  95. package/src/claude/skills/ai-multimodal/scripts/check_setup.py +8 -68
  96. package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +6 -28
  97. package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
  98. package/src/claude/skills/code-review/SKILL.md +25 -108
  99. package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -7
  100. package/src/claude/skills/develop/SKILL.md +22 -70
  101. package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
  102. package/src/claude/skills/git/SKILL.md +1 -1
  103. package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
  104. package/src/claude/skills/research/SKILL.md +24 -55
  105. package/src/claude/skills/sync/SKILL.md +12 -34
  106. package/src/claude/skills/sync/references/rebind-and-audit.md +2 -2
  107. package/src/claude/skills/test/SKILL.md +39 -123
  108. package/src/claude/skills/test/references/execution-strategy.md +11 -22
  109. package/src/claude/skills/ui-ux/SKILL.md +1 -1
  110. package/src/claude/skills/ui-ux-pro-max/SKILL.md +24 -79
  111. package/src/claude/skills/web-testing/SKILL.md +2 -20
  112. package/src/codex/AGENTS.md +8 -0
  113. package/src/codex/hooks/ask-guard.cjs +54 -0
  114. package/src/codex/hooks/inspect-block.cjs +11 -7
  115. package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
  116. package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
  117. package/src/codex/hooks/lib/privacy-state.cjs +98 -16
  118. package/src/codex/hooks/lib/rules-reservation.cjs +38 -0
  119. package/src/codex/hooks/lib/secret-keywords.cjs +1 -0
  120. package/src/codex/hooks/lib/spec-receipt.cjs +2 -30
  121. package/src/codex/hooks/lib/spec-session-touch.cjs +100 -0
  122. package/src/codex/hooks/lib/spec-touch.cjs +3 -0
  123. package/src/codex/hooks/lib/spec-utils.cjs +6 -45
  124. package/src/codex/hooks/lib/update-check.cjs +1 -0
  125. package/src/codex/hooks/precompact.cjs +39 -0
  126. package/src/codex/hooks/privacy-approval.cjs +28 -17
  127. package/src/codex/hooks/privacy-block.cjs +9 -215
  128. package/src/codex/hooks/rules.cjs +4 -30
  129. package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
  130. package/src/codex/hooks/session.cjs +47 -11
  131. package/src/codex/hooks/spec-gate.cjs +32 -74
  132. package/src/codex/hooks/spec-state.cjs +68 -46
  133. package/src/codex/hooks/state.cjs +25 -4
  134. package/src/codex/hooks.json +39 -7
  135. package/src/codex/rules/hook-protocols.md +13 -6
  136. package/src/common/AGENTS.md +4 -10
  137. package/src/omp/extensions/cafekit-bridge.mjs +12 -4
  138. package/src/omp/hooks/privacy-block.cjs +24 -206
  139. package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
  140. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
  141. package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
  142. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
  143. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
  144. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
  145. package/src/claude/hooks/lib/context.cjs +0 -527
  146. package/src/claude/hooks/lib/detect.cjs +0 -474
  147. package/src/claude/scripts/change-firewall.cjs +0 -1748
  148. package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
  149. package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
  150. package/src/claude/scripts/spec-final-state.cjs +0 -290
  151. package/src/claude/scripts/spec-ground.cjs +0 -650
  152. package/src/claude/scripts/spec-readiness.cjs +0 -425
  153. package/src/claude/scripts/spec-scaffold.cjs +0 -2317
  154. package/src/claude/scripts/spec-semantic-model.cjs +0 -396
  155. package/src/claude/scripts/validate-spec-output.cjs +0 -3914
  156. package/src/claude/skills/code-review/references/verification-gate.md +0 -41
  157. package/src/claude/skills/specs/templates/design.md +0 -82
  158. package/src/claude/skills/specs/templates/requirements-init.md +0 -8
  159. package/src/claude/skills/specs/templates/requirements.md +0 -42
  160. package/src/claude/skills/specs/templates/spec-state.json +0 -56
  161. package/src/claude/skills/specs/templates/task.md +0 -47
  162. package/src/codex/rules/state-sync.md +0 -60
@@ -8,75 +8,15 @@ const { spawnSync } = require('node:child_process');
8
8
  const { test } = require('node:test');
9
9
 
10
10
  const ROOT = path.resolve(__dirname, '../..');
11
- const VALIDATOR = path.join(ROOT, 'src/claude/scripts/validate-spec-output.cjs');
12
- const GROUNDING = path.join(ROOT, 'src/claude/scripts/spec-ground.cjs');
13
- const SCAFFOLD = path.join(ROOT, 'src/claude/scripts/spec-scaffold.cjs');
14
- const AUTHORING_VALIDATION = path.join(ROOT, 'src/claude/scripts/spec-authoring-validation.cjs');
15
11
  const RESOLVER = path.join(ROOT, 'src/claude/scripts/spec-resolver.cjs');
16
- const POLICY = require(path.join(ROOT, 'src/claude/scripts/workflow-policy.cjs'));
17
12
  const SPEC_RESOLVER = require(RESOLVER);
18
13
  const { normalizeCodexBody } = require(path.join(ROOT, 'bin/lib/codex-install.js'));
19
- const { copyClaudeTestRuntime } = require('./test-runtime-dependency-closure.cjs');
20
14
 
21
15
  function write(filePath, content) {
22
16
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
23
17
  fs.writeFileSync(filePath, content);
24
18
  }
25
19
 
26
- function writeRaw(filePath, content) {
27
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
28
- fs.writeFileSync(filePath, content);
29
- }
30
-
31
- function copyCodexRuntime(adapterRoot, entrypoints) {
32
- const files = copyClaudeTestRuntime(ROOT, adapterRoot, entrypoints);
33
- for (const relative of files) {
34
- if (!/\.(?:cjs|js)$/.test(relative)) continue;
35
- const target = path.join(adapterRoot, relative);
36
- writeRaw(
37
- target,
38
- normalizeCodexBody(fs.readFileSync(target, 'utf8'), `src/claude/${relative}`),
39
- );
40
- }
41
- return files;
42
- }
43
-
44
- function run(script, args, cwd, options = {}) {
45
- const fixtureRoot = script === VALIDATOR && typeof args[0] === 'string'
46
- ? path.dirname(path.dirname(path.resolve(args[0])))
47
- : null;
48
- return spawnSync(process.execPath, [script, ...args], {
49
- cwd,
50
- encoding: 'utf8',
51
- ...options,
52
- env: {
53
- ...process.env,
54
- ...(fixtureRoot ? { HOME: path.join(fixtureRoot, '.home'), USERPROFILE: path.join(fixtureRoot, '.home') } : {}),
55
- ...(options.env || {}),
56
- },
57
- });
58
- }
59
-
60
- function snapshotTree(root) {
61
- const entries = [];
62
- function visit(directory) {
63
- for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
64
- const fullPath = path.join(directory, entry.name);
65
- const relative = path.relative(root, fullPath);
66
- if (entry.isSymbolicLink()) {
67
- entries.push(`${relative}\0symlink\0${fs.readlinkSync(fullPath)}`);
68
- } else if (entry.isDirectory()) {
69
- entries.push(`${relative}\0directory`);
70
- visit(fullPath);
71
- } else {
72
- entries.push(`${relative}\0file\0${fs.readFileSync(fullPath).toString('base64')}`);
73
- }
74
- }
75
- }
76
- visit(root);
77
- return entries.sort().join('\n');
78
- }
79
-
80
20
  function task({
81
21
  id = 'R1-01',
82
22
  mapping = '1.1',
@@ -96,424 +36,16 @@ function task({
96
36
  return `# Task ${id}: One\n\n**Status:** ${status}\n\n## Outcome\n\nCalling the real fixture entrypoint returns status value 1.\n\n## Scope and Typed Anchors\n\n- **In scope:** Implement the mapped observable behavior.\n- **Out of scope:** Unrelated runtime behavior.\n- **Contracts/Invariants:** none\n- **Canonical design anchors consumed:** A-D-01\n\n| ID | Type | Target | Role |\n|---|---|---|---|\n| A-${id}-01 | command | \`node --test test/one.test.js\` | planned verification command |\n\n## Changes\n\n- [ ] ${parallel ? '(P) ' : ''}Update the source boundary and preserve its negative path.${mapping ? ` _Requirements: ${mapping}_` : ''}\n\n## Acceptance\n\n${acceptanceId ? `- **R${acceptanceId}:**` : '-'} Calling \`${relatedPath}\` returns an observable result with status value 1.\n\n## Dependencies\n\n- ${dependencies}\n\n## Verification Plan\n\n- **Command:** \`node --test test/one.test.js\`\n- **Expected:** exit code 0 and one assertion for status value 1\n- **Negative path:** invalid input returns a deterministic error result\n- **Reachability:** \`${relatedPath}\` is invoked by the focused test\n\n## Related Files\n\n| Path | Action | Description |\n|---|---|---|---|\n${lifecycleRows}\n${extra}`;
97
37
  }
98
38
 
99
- function legacyWorkflowPolicy(lane = 'Standard') {
100
- const proofObligations = lane === 'Critical'
101
- ? ['needsInspection', 'needsExecutionProof', 'needsIndependentAudit', 'needsResearchGrounding']
102
- : ['needsInspection', 'needsExecutionProof'];
103
- return {
104
- version: '1',
105
- lane,
106
- automatic_lane: lane,
107
- risks: lane === 'Critical' ? ['auth'] : [],
108
- artifact_profile: lane === 'Critical' ? 'strict' : 'bounded',
109
- proof_obligations: proofObligations,
110
- actor_needs: proofObligations.map((obligation) => ({
111
- capability: {
112
- needsInspection: 'inspection',
113
- needsExecutionProof: 'execution-proof',
114
- needsIndependentAudit: 'audit',
115
- needsResearchGrounding: 'research-grounding',
116
- }[obligation],
117
- independence: obligation === 'needsIndependentAudit' ? 'independent' : 'same-session',
118
- })),
119
- override_receipt: null,
120
- };
121
- }
122
-
123
- function legacyTask({ relatedRows = '| `src/one.js` | Read | Inspect existing behavior |', extra = '' } = {}) {
124
- return `# Task R1-01: One\n\n**Status:** pending\n**Dependencies:** none\n\n## Context\n\nThe existing fixture service returns one deterministic result.\n\n## Constraints\n\nPreserve the stable result contract and its invalid-input behavior.\n\n## Steps\n\n- [ ] Inspect the declared boundary and preserve status value 1. _Requirements: 1.1_\n\n## Requirements\n\n- 1.1 — Return status value 1 from the fixture service.\n\n## Related Files\n\n| Path | Action | Description |\n|---|---|---|\n${relatedRows}\n\n## Completion Criteria\n\n- Calling \`src/one.js\` returns result status value 1.\n\n## Evidence\n\n- **Command:** \`node --test test/one.test.js\`\n- **Expected:** exit code 0 and status value 1\n- **Inspect:** \`src/one.js\` runtime result\n- Runtime reachability verification\n - **Entrypoint/caller:** \`src/one.js\`\n\n## Risk Assessment\n\n| Risk | Severity | Mitigation |\n|---|---|---|\n| regression | low | focused test |\n${extra}`;
125
- }
126
-
127
- function lifecycleTimestamps({ tasks = true, research = false, review = false } = {}) {
128
- return {
129
- init: '2026-08-11T00:00:00+07:00',
130
- requirements_done: '2026-08-11T00:01:00+07:00',
131
- research_done: research ? '2026-08-11T00:02:00+07:00' : null,
132
- design_done: '2026-08-11T00:03:00+07:00',
133
- tasks_done: tasks ? '2026-08-11T00:04:00+07:00' : null,
134
- code_done: null,
135
- test_done: null,
136
- review_done: review ? '2026-08-11T00:05:00+07:00' : null,
137
- validation_done: '2026-08-11T00:06:00+07:00',
138
- };
139
- }
140
-
141
- function semanticReviewNotRun() {
142
- return {
143
- status: 'not-run', reviewed_artifact_digest: null, reviewed_criteria: [], counterexamples: [],
144
- };
145
- }
146
-
147
- function semanticDigest(specDir) {
148
- const result = run(VALIDATOR, [specDir, '--semantic-digest'], ROOT);
149
- assert.equal(result.status, 0, output(result));
150
- return result.stdout.trim();
151
- }
152
-
153
- function completeSemanticReview(specDir) {
154
- const statePath = path.join(specDir, 'spec.json');
155
- const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
156
- state.validation = isPlainObjectForTest(state.validation) ? state.validation : {};
157
- state.validation.semantic_review = {
158
- status: 'completed',
159
- reviewed_artifact_digest: semanticDigest(specDir),
160
- reviewed_criteria: ['R1.1'],
161
- counterexamples: [{
162
- criterion: 'R1.1',
163
- scenario: 'The fixture receives invalid input at its runtime boundary.',
164
- expected: 'The fixture returns the deterministic negative-path result.',
165
- design_reference: 'D1',
166
- }],
167
- };
168
- write(statePath, JSON.stringify(state, null, 2));
169
- return state;
170
- }
171
-
172
- function isPlainObjectForTest(value) {
173
- return Boolean(value && typeof value === 'object' && !Array.isArray(value));
174
- }
175
-
176
- function obligationClosureBlock(obligation) {
177
- const consumers = obligation.consumer_tasks.length > 0
178
- ? obligation.consumer_tasks.join(', ')
179
- : 'none';
180
- return `<!-- implementation-obligation:${obligation.id} -->\n- Requirements: ${obligation.requirements.join(', ')}\n- Canonical state: ${obligation.canonical_state}\n- Authority: ${obligation.authority}\n- Recovery: ${obligation.recovery}\n- Owner task: ${obligation.owner_task}\n- Consumers: ${consumers}\n- Verification: ${obligation.verification}\n- Evidence: ${obligation.evidence}\n`;
181
- }
182
-
183
- function executionClosure(obligation) {
184
- return `\n## Execution Closure\n\n${obligationClosureBlock(obligation)}`;
185
- }
186
-
187
- function executionClosureTable(obligation) {
188
- const consumers = obligation.consumer_tasks.length > 0
189
- ? obligation.consumer_tasks.join(', ')
190
- : 'none';
191
- return `\n## Execution Closure\n\n<!-- implementation-obligation:${obligation.id} -->\n` +
192
- '| Obligation ID / Requirements | Canonical State | Authority | Crash / Retry Recovery | Owner Task | Consumer Tasks / Dependency Order | Verification / Evidence |\n' +
193
- '|---|---|---|---|---|---|---|\n' +
194
- `| ${obligation.id} / ${obligation.requirements.join(', ')} | ${obligation.canonical_state} | ${obligation.authority} | ${obligation.recovery} | \`${obligation.owner_task}\` | ${consumers} | \`${obligation.verification}\` → ${obligation.evidence} |\n`;
195
- }
196
-
197
- function criticalNoObligationDesign() {
198
- return designFixture() +
199
- '## Execution Closure\n\n' +
200
- 'N/A — this fixture has no cross-task state, retry, migration, or authority obligation.\n';
201
- }
202
-
203
- function designFixture() {
204
- return '# Design\n\n## Boundary\n\nThe `src/one.js` module owns the fixture behavior and returns one deterministic result.\n\n' +
205
- '## Typed Anchors\n\n| ID | Type | Target | Role |\n|---|---|---|---|\n' +
206
- '| A-D-01 | file | `src/one.js` | existing owner and runtime entrypoint |\n\n' +
207
- '## Decisions and Invariants\n\n### D1 — Stable result\n\n- **Decision:** Return status value 1.\n- **Negative path:** Invalid input returns a deterministic error.\n- **Anchors:** A-D-01\n\n' +
208
- '## Verification\n\n| Requirement | Proof target | Expected result | Negative path / reachability |\n|---|---|---|---|\n' +
209
- '| R1.1 | `node --test test/one.test.js` | exit code 0 and status value 1 | invalid input through `src/one.js` |\n\n';
210
- }
211
-
212
- function designFixture21() {
213
- return '# Design\n\n## Boundary\n\nThe `src/one.js` module owns the fixture behavior and returns one deterministic result.\n\n' +
214
- '## Typed Anchors\n\n| ID | Type | Target | Role | Access | Action |\n|---|---|---|---|---|---|\n' +
215
- '| A-D-01 | file | `src/one.js` | runtime entrypoint | read | read |\n' +
216
- '| A-D-02 | command | `node --test test/one.test.js` | proof command | read | read |\n\n' +
217
- '## Decisions and Invariants\n\n### D1 — Stable result\n\nReturn status value 1 and reject invalid input.\n\n' +
218
- '### I1 — Invalid-state invariant\n\nInvalid input never reports the success status.\n\n' +
219
- '### C1 — Result contract\n\nThe result contains one numeric status.\n\n' +
220
- '## Verification Definitions\n\n' +
221
- '- **V1**: Subject criteria R1.1; Subject owner A-D-01; Proof criteria R1.2; Proof owner A-D-02; Evidence anchor A-D-02; Decision refs D1, I1, C1; Method command `node --test test/one.test.js`; Expected exit code 0 and status value 1; Negative/failure invalid input returns a deterministic error; Reachability/grounding entrypoint `src/one.js` via A-D-01, A-D-02.\n';
222
- }
223
-
224
- function createSpec(root, options = {}) {
225
- const featureName = options.name || 'spec';
226
- const specDir = path.join(root, 'specs', featureName);
227
- const taskPath = 'tasks/task-R1-01-one.md';
228
- write(
229
- path.join(specDir, 'requirements.md'),
230
- options.requirements || '# Requirements\n\n### Requirement 1: One\n\n- **R1.1** When input arrives, the service shall return one deterministic result.\n',
231
- );
232
- write(
233
- path.join(specDir, 'design.md'),
234
- options.design || designFixture(),
235
- );
236
- write(path.join(specDir, taskPath), options.task || task());
237
- write(path.join(root, 'test/one.test.js'), "'use strict';\n// Separate proof artifact for fixture validation.\n");
238
- write(path.join(specDir, 'spec.json'), JSON.stringify({
239
- schema_version: '2.0',
240
- feature_name: featureName,
241
- created_at: '2026-08-11T00:00:00+07:00',
242
- updated_at: '2026-08-11T00:05:00+07:00',
243
- language: 'en',
244
- status: 'in_progress',
245
- current_phase: 'tasks',
246
- scope_lock: { source: 'fixture', in_scope: ['1'], out_of_scope: [], expansion_policy: 'requires-user-approval' },
247
- coordination: {
248
- tasks_required: true,
249
- phases_required: false,
250
- reason: 'task_topology',
251
- task_triggers: ['separate_proof'],
252
- },
253
- task_files: [taskPath],
254
- task_registry: {
255
- [taskPath]: {
256
- id: 'R1-01', title: 'One', status: 'pending', dependencies: [], blocker: null,
257
- started_at: null, completed_at: null, last_updated_at: null,
258
- artifacts: ['test/one.test.js'],
259
- },
260
- },
261
- approvals: { requirements: { generated: true, agent_validated: true, user_approved: true }, design: { generated: true, agent_validated: true, user_approved: true }, tasks: { generated: true, agent_validated: true, user_approved: true } },
262
- validation: { status: 'not-run', semantic_review: semanticReviewNotRun() },
263
- timestamps: { ...lifecycleTimestamps(), validation_done: null },
264
- ready_for_implementation: false,
265
- workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: {} }),
266
- ...(options.spec || {}),
267
- }, null, 2));
268
- return specDir;
269
- }
270
-
271
- function createBoundedSpec(root, name = 'bounded') {
272
- const specDir = path.join(root, name);
273
- const policy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
274
- write(path.join(specDir, 'requirements.md'), '# Requirements\n\n### Requirement 1: One\n\n- **R1.1** When input arrives, the service shall return status value 1.\n');
275
- write(path.join(specDir, 'design.md'), designFixture());
276
- write(path.join(specDir, 'spec.json'), JSON.stringify({
277
- schema_version: '2.0',
278
- feature_name: name,
279
- created_at: '2026-08-11T00:00:00+07:00',
280
- updated_at: '2026-08-11T00:03:00+07:00',
281
- status: 'in_progress',
282
- current_phase: 'design',
283
- scope_lock: { source: 'fixture', in_scope: [], out_of_scope: [], expansion_policy: 'requires-user-approval' },
284
- coordination: { tasks_required: false, phases_required: false, reason: 'taskless_requested' },
285
- approvals: {
286
- requirements: { generated: true, agent_validated: true, user_approved: false },
287
- design: { generated: true, agent_validated: true, user_approved: false },
288
- },
289
- validation: { status: 'not-run', semantic_review: semanticReviewNotRun() },
290
- timestamps: { ...lifecycleTimestamps({ tasks: false }), validation_done: null },
291
- ready_for_implementation: false,
292
- workflow_policy: policy,
293
- }, null, 2));
294
- return specDir;
295
- }
296
-
297
- function legacyTasklessSpec20State(name, policy) {
298
- return {
299
- schema_version: '2.0',
300
- feature_name: name,
301
- created_at: '2026-08-11T00:00:00+07:00',
302
- updated_at: '2026-08-11T00:03:00+07:00',
303
- language: 'en',
304
- status: 'in_progress',
305
- current_phase: 'design',
306
- scope_lock: {
307
- source: 'legacy fixture',
308
- in_scope: [],
309
- out_of_scope: [],
310
- expansion_policy: 'requires-user-approval',
311
- },
312
- coordination: {
313
- tasks_required: false,
314
- phases_required: false,
315
- reason: 'taskless_requested',
316
- },
317
- validation: { status: 'not-run', semantic_review: semanticReviewNotRun() },
318
- timestamps: { ...lifecycleTimestamps({ tasks: false }), validation_done: null },
319
- ready_for_implementation: false,
320
- workflow_policy: policy,
321
- };
322
- }
323
-
324
- function createLegacyTasklessSpec20(root, name, policy) {
325
- const specDir = path.join(root, 'specs', name);
326
- write(
327
- path.join(specDir, 'requirements.md'),
328
- '# Requirements\n\n### Requirement 1: Legacy baseline\n\n- **R1.1** The fixture shall preserve one deterministic result.\n',
329
- );
330
- write(path.join(specDir, 'design.md'), designFixture());
331
- write(
332
- path.join(specDir, 'spec.json'),
333
- JSON.stringify(legacyTasklessSpec20State(name, policy), null, 2),
334
- );
335
- return specDir;
336
- }
337
-
338
39
  function output(result) {
339
40
  return `${result.stdout}\n${result.stderr}`;
340
41
  }
341
42
 
342
- function completeScaffoldedSpec(specDir) {
343
- const state = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
344
- for (const [index, taskFile] of (state.task_files || []).entries()) {
345
- const entry = state.task_registry[taskFile];
346
- write(path.join(specDir, taskFile), task({
347
- id: entry.id,
348
- related: `\`src/${index === 0 ? 'one' : `task-${index + 1}`}.js\` | Read`,
349
- dependencies: entry.dependencies.length > 0 ? entry.dependencies.join(',') : 'none',
350
- }));
351
- }
352
- write(path.join(specDir, 'requirements.md'), '# Requirements\n\n### Requirement 1: Artifact\n\n- **R1.1** The task is verifiable.\n');
353
- write(
354
- path.join(specDir, 'design.md'),
355
- state.workflow_policy?.assurance_level === 'Strict' ? criticalNoObligationDesign() : designFixture(),
356
- );
357
- if (state.research === 'research.md') {
358
- write(path.join(specDir, 'research.md'), '# Research\n\n## Evidence Summary\n- Producer fixture.\n');
359
- }
360
- return state;
361
- }
362
-
363
- test('canonical legacy v2.0 taskless migration fixture is valid without execution evidence', () => {
364
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-legacy-taskless-baseline-'));
365
- try {
366
- const specDir = createLegacyTasklessSpec20(
367
- root,
368
- 'legacy-taskless-baseline',
369
- POLICY.workflowPolicySnapshot({ riskSignals: {} }),
370
- );
371
- const state = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
372
- assert.equal(state.schema_version, '2.0');
373
- assert.equal(state.ready_for_implementation, false);
374
- assert.equal(Object.hasOwn(state, 'task_files'), false);
375
- assert.equal(Object.hasOwn(state, 'task_registry'), false);
376
- assert.equal(fs.existsSync(path.join(specDir, 'research.md')), false);
377
- assert.equal(fs.existsSync(path.join(specDir, 'receipts')), false);
378
- state.override_receipt = { legacy: true };
379
- write(path.join(specDir, 'spec.json'), JSON.stringify(state, null, 2));
380
- const validation = run(VALIDATOR, [specDir], ROOT);
381
- assert.equal(validation.status, 0, output(validation));
382
- } finally {
383
- fs.rmSync(root, { recursive: true, force: true });
384
- }
385
- });
386
-
387
- function completeScaffoldedSpec21(specDir) {
388
- const statePath = path.join(specDir, 'spec.json');
389
- let state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
390
- const fixtureRoot = path.dirname(path.dirname(specDir));
391
- const criteria = (state.task_files || []).map((_, index) => `R1.${index + 1}`);
392
- write(path.join(fixtureRoot, 'test/one.test.js'), "require('node:test')('one', () => {});\n");
393
- fs.mkdirSync(path.join(fixtureRoot, 'artifacts'), { recursive: true });
394
- for (const [index, taskFile] of (state.task_files || []).entries()) {
395
- const entry = state.task_registry[taskFile];
396
- const ownedPath = `src/task-${index + 1}.js`;
397
- const criterion = criteria[index];
398
- const taskRole = index === 0 ? 'subject' : 'verifier';
399
- write(path.join(fixtureRoot, ownedPath), 'module.exports = { status: 1 };\n');
400
- write(path.join(specDir, taskFile), `# Task ${entry.id}: ${entry.title}
401
- **Status:** pending
402
-
403
- ## Outcome
404
- Return observable status value 1 through the real fixture entrypoint.
405
-
406
- ## Scope
407
- - **In scope:** Exact behavior for ${ownedPath}.
408
- - **Out of scope:** Unrelated runtime behavior.
409
-
410
- ## Anchors and Ownership
411
- | ID | Type | Target | Role | Access | Action |
412
- |---|---|---|---|---|---|
413
- | A-${entry.id}-01 | file | \`${ownedPath}\` | owner | write | modify |
414
- ${taskRole === 'verifier' ? `| A-${entry.id}-02 | artifact | \`artifacts/${entry.id}.json\` | verifier | write | create |` : ''}
415
-
416
- ## Changes
417
- - [ ] ${taskRole === 'verifier' ? 'Implement the separate proof behavior.' : 'Implement the exact owned behavior.'} _Requirements: ${criterion.slice(1)}_
418
-
419
- ## Acceptance
420
- - **${criterion}:** ${taskRole === 'verifier' ? 'The focused proof reports status value 1 and rejects an invalid result.' : 'The fixture returns status value 1 and rejects invalid input.'}
421
-
422
- ## Dependencies
423
- - ${entry.dependencies.length > 0 ? entry.dependencies.join(', ') : 'none'}
424
-
425
- ## Verification Plan
426
- - **Verification ref:** V1
427
- - **Task role:** ${taskRole}
428
- - **Command:** \`node --test test/one.test.js\`
429
- - **Expected:** Exit code 0 and exact status value 1.
430
- - **Negative path:** Invalid input returns rejected state.
431
- - **Reachability:** \`${ownedPath}\`
432
- `);
433
- }
434
- write(path.join(specDir, 'requirements.md'), `# Requirements
435
-
436
- ### Requirement 1: Artifact
437
-
438
- ${criteria.map((criterion, index) => `- **${criterion}**: Task ${index + 1} shall expose its distinct observable status or proof outcome and reject invalid input.`).join('\n')}
439
- `);
440
- write(path.join(specDir, 'design.md'), `# Design
441
-
442
- ## Architecture
443
- The fixture entrypoint delegates to the owned task boundary.
444
-
445
- ## Typed Anchors
446
- | ID | Type | Target | Role | Access | Action |
447
- |---|---|---|---|---|---|
448
- | A-D-01 | command | \`node --test test/one.test.js\` | verification command | read | read |
449
-
450
- ## Canonical Contracts & Invariants
451
- ### D1 — Delegation decision
452
- The entrypoint delegates exactly once.
453
- ### I1 — Invalid-state invariant
454
- Invalid input never returns status value 1.
455
- ### C1 — Result contract
456
- The result contains a numeric status.
457
- ### T1 — Artifact transition
458
- The transition records success, failure, and recovery states.
459
-
460
- ## Verification Definitions
461
- - **V1**: Subject criteria ${criteria[0]}; Subject owner ${state.task_registry[state.task_files[0]].id}; Proof criteria ${criteria.slice(1).join(', ')}; Proof owner ${state.task_registry[state.task_files[1]].id}; Evidence anchor A-${state.task_registry[state.task_files[1]].id}-02; Decision refs D1, I1, C1; Method command \`node --test test/one.test.js\`; Expected exit code 0 and exact status value 1; Negative/failure invalid input returns rejected state; Reachability/grounding entrypoint \`src/task-1.js\` via A-D-01, A-${state.task_registry[state.task_files[1]].id}-02.
462
- `);
463
- const promoted = run(
464
- SCAFFOLD,
465
- [path.basename(specDir), '--sync-semantic-model'],
466
- path.dirname(path.dirname(specDir)),
467
- );
468
- assert.equal(promoted.status, 0, output(promoted));
469
- // I21: `validated` is never hand-authored here. The real C16 coordinator is
470
- // the only writer, and it flips the enums only after its own clean
471
- // validator+grounder pass, writing the matching digest receipt in the same
472
- // atomic replace. Routing the fixture through it keeps this helper honest and
473
- // exercises the production path end to end.
474
- const authored = run(AUTHORING_VALIDATION, [specDir, '--root', fixtureRoot], fixtureRoot);
475
- assert.equal(authored.status, 0, output(authored));
476
- state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
477
- assert.equal(state.authoring.requirements, 'validated');
478
- assert.equal(state.authoring.design, 'validated');
479
- state.validation.status = 'completed';
480
- state.validation.semantic_review = {
481
- status: 'completed', semantic_digest: null, verdict: 'PASS', lifecycle_disposition: 'CONTINUE',
482
- findings: [], unresolved_decisions: [],
483
- graph_coverage: [
484
- 'criterion_local', 'cross_criterion', 'runtime_path',
485
- 'assumption_provenance', 'compatibility_migration',
486
- ].map((surface) => ({ surface, covered: true, notes: `${surface} reviewed against the fixture topology.` })),
487
- repair_round: 0,
488
- reviewed_criteria: criteria,
489
- counterexamples: criteria.map((criterion, index) => ({
490
- criterion, case_kind: index % 2 === 0 ? 'failure' : 'adversarial',
491
- scenario: `Task ${index + 1} receives a distinct malformed status payload at its declared boundary.`,
492
- expected: `Task ${index + 1} rejects that payload without publishing its distinct success outcome.`,
493
- decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
494
- })),
495
- reviewer_evidence: {
496
- assurance: 'Routine',
497
- summary: 'Fixture reviewer confirmed every criterion, counterexample, and reachability anchor.',
498
- },
499
- };
500
- write(statePath, JSON.stringify(state, null, 2));
501
- state.validation.semantic_review.semantic_digest = semanticDigest(specDir);
502
- state.ready_for_implementation = true;
503
- write(statePath, JSON.stringify(state, null, 2));
504
- return state;
505
- }
506
-
507
- test('Codex installer transform preserves all owned spec assets with idempotent runtime parity', () => {
43
+ test('Codex installer transform preserves all owned spec assets idempotently', () => {
508
44
  for (const relative of [
509
45
  'src/claude/skills/specs/SKILL.md',
510
46
  'src/claude/skills/specs/references/review.md',
511
47
  'src/claude/skills/specs/references/templates.md',
512
- 'src/claude/skills/specs/templates/design.md',
513
- 'src/claude/skills/specs/templates/task.md',
514
- 'src/claude/scripts/spec-scaffold.cjs',
515
48
  'src/claude/scripts/spec-receipt.cjs',
516
- 'src/claude/scripts/validate-spec-output.cjs',
517
49
  ]) {
518
50
  const source = fs.readFileSync(path.join(ROOT, relative), 'utf8');
519
51
  const transformed = normalizeCodexBody(source, relative);
@@ -539,66 +71,38 @@ test('Codex installer transform preserves all owned spec assets with idempotent
539
71
  assert.match(templates, /\| ID \| EARS criterion \| Proof \|/);
540
72
  assert.match(templates, /## Canonical inline Receipt[\s\S]*Verification: PASS[\s\S]*Exit: 0[\s\S]*Base:[\s\S]*Head:/);
541
73
 
542
- const parityRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-validator-parity-'));
543
- try {
544
- const installedScripts = path.join(parityRoot, '.codex/scripts');
545
- const installedValidator = path.join(installedScripts, 'validate-spec-output.cjs');
546
- copyCodexRuntime(path.dirname(installedScripts), ['scripts/validate-spec-output.cjs']);
547
-
548
- const scaffolded = run(SCAFFOLD, ['runtime-parity'], parityRoot);
549
- assert.equal(scaffolded.status, 0, output(scaffolded));
550
- const specDir = path.join(parityRoot, 'specs/runtime-parity');
551
- write(path.join(specDir, 'requirements.md'), '# Requirements\n\n### Requirement 1: One\n\n- **R1.1** The runtime shall return status value 1.\n- **R1.2** The proof shall reject an invalid status result.\n');
552
- write(path.join(specDir, 'design.md'), designFixture21());
553
- const promoted = run(SCAFFOLD, ['runtime-parity', '--sync-semantic-model'], parityRoot);
554
- assert.equal(promoted.status, 0, output(promoted));
555
- const sourcePending = run(VALIDATOR, [specDir], ROOT);
556
- const codexPending = run(installedValidator, [specDir], ROOT);
557
- assert.equal(sourcePending.status, 0, output(sourcePending));
558
- assert.equal(codexPending.status, sourcePending.status, output(codexPending));
559
-
560
- const statePath = path.join(specDir, 'spec.json');
561
- const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
562
- state.status = 'paused';
563
- writeRaw(statePath, JSON.stringify(state, null, 2));
564
- const sourceDonePending = run(VALIDATOR, [specDir], ROOT);
565
- const codexDonePending = run(installedValidator, [specDir], ROOT);
566
- assert.equal(sourceDonePending.status, 0, output(sourceDonePending));
567
- assert.equal(codexDonePending.status, sourceDonePending.status, output(codexDonePending));
568
- } finally {
569
- fs.rmSync(parityRoot, { recursive: true, force: true });
570
- }
571
74
  });
572
75
 
573
- test('resolver binds feature identity and rejects cross-feature spec aliases', () => {
76
+ test('resolver binds feature identity and rejects cross-feature plan aliases', () => {
574
77
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-resolver-identity-'));
78
+ const packet = (dir) => {
79
+ write(path.join(dir, 'plan.md'), '# Plan\nSpecs-Contract: process-first-ready-v1\n');
80
+ write(path.join(dir, 'task-01-x.md'), '# Task 01\n\nStatus: pending\n\n## Dependencies\n\n- none\n');
81
+ };
575
82
  try {
576
83
  const specsDir = path.join(root, 'specs');
577
- write(path.join(specsDir, 'bar', 'spec.json'), JSON.stringify({ feature_name: 'bar', status: 'in_progress' }));
84
+ packet(path.join(specsDir, 'bar'));
578
85
  fs.mkdirSync(path.join(specsDir, 'foo'), { recursive: true });
579
- fs.symlinkSync('../bar/spec.json', path.join(specsDir, 'foo', 'spec.json'));
86
+ fs.symlinkSync('../bar/plan.md', path.join(specsDir, 'foo', 'plan.md'));
580
87
 
581
- const explicit = SPEC_RESOLVER.resolveActiveSpec({ projectRoot: root, explicitFeature: 'foo' });
88
+ const explicit = SPEC_RESOLVER.resolveWorkflowCandidate({ projectRoot: root, explicitFeature: 'foo' });
582
89
  assert.equal(explicit.error, 'explicit_malformed');
583
- assert.match(explicit.reason, /does not belong to feature directory|canonical feature/);
90
+ assert.match(explicit.reason, /plan\.md must be a regular non-symlink file/);
584
91
 
585
- const implicit = SPEC_RESOLVER.resolveActiveSpec({ projectRoot: root });
92
+ const implicit = SPEC_RESOLVER.resolveWorkflowCandidate({ projectRoot: root });
586
93
  assert.equal(implicit.error, 'invalid_specs');
587
94
  assert.deepEqual(implicit.candidates, ['foo']);
588
- assert.doesNotMatch(implicit.reason, /Multiple active specs/);
589
- assert.throws(
590
- () => SPEC_RESOLVER.findAllActiveSpecs(root),
591
- (error) => error.code === 'INVALID_SPECS' && /foo/.test(error.message),
592
- );
95
+ assert.doesNotMatch(implicit.reason, /Multiple active/);
593
96
 
97
+ // A directory alias is refused rather than resolved to the packet it points at.
594
98
  const aliasRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-resolver-directory-alias-'));
595
99
  try {
596
100
  const aliasSpecs = path.join(aliasRoot, 'specs');
597
- write(path.join(aliasSpecs, 'bar', 'spec.json'), JSON.stringify({ feature_name: 'bar', status: 'in_progress' }));
101
+ packet(path.join(aliasSpecs, 'bar'));
598
102
  fs.symlinkSync('bar', path.join(aliasSpecs, 'foo'), 'dir');
599
- const resolved = SPEC_RESOLVER.resolveActiveSpec({ projectRoot: aliasRoot });
600
- assert.equal(resolved.featureName, 'bar');
601
- assert.equal(SPEC_RESOLVER.findAllActiveSpecs(aliasRoot).length, 1);
103
+ const resolved = SPEC_RESOLVER.resolveWorkflowCandidate({ projectRoot: aliasRoot });
104
+ assert.equal(resolved.error, 'invalid_specs');
105
+ assert.deepEqual(resolved.candidates, ['foo']);
602
106
  } finally {
603
107
  fs.rmSync(aliasRoot, { recursive: true, force: true });
604
108
  }
@@ -607,2341 +111,6 @@ test('resolver binds feature identity and rejects cross-feature spec aliases', (
607
111
  }
608
112
  });
609
113
 
610
- test('validator rejects hidden task symlinks instead of filtering them', () => {
611
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-hidden-task-link-'));
612
- try {
613
- const specDir = createSpec(root, { name: 'hidden-task-link' });
614
- fs.symlinkSync('task-R1-01-one.md', path.join(specDir, 'tasks', '.hidden-task.md'));
615
- const result = run(VALIDATOR, [specDir], ROOT);
616
- assert.notEqual(result.status, 0);
617
- assert.match(output(result), /tasks\/\.hidden-task\.md: symlink is not allowed/);
618
- } finally {
619
- fs.rmSync(root, { recursive: true, force: true });
620
- }
621
- });
622
-
623
- test('validator rejects non-Markdown task artifacts instead of ignoring them', () => {
624
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-task-artifact-'));
625
- try {
626
- const specDir = createSpec(root, { name: 'non-markdown-task-artifact' });
627
- write(path.join(specDir, 'tasks', 'notes.txt'), 'worker note\n');
628
- const result = run(VALIDATOR, [specDir], ROOT);
629
- assert.notEqual(result.status, 0);
630
- assert.match(output(result), /tasks\/notes\.txt: unexpected task artifact/);
631
- } finally {
632
- fs.rmSync(root, { recursive: true, force: true });
633
- }
634
- });
635
-
636
- test('validator keeps authoring receipt-free and validates taskless execution closeout proof', () => {
637
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-feature-receipt-'));
638
- try {
639
- const specDir = createBoundedSpec(root, 'weak-completed-feature-receipt');
640
- write(path.join(specDir, 'feature-receipt.md'), [
641
- '# Feature Verification Receipt', '', 'Verification: PASS',
642
- 'Command: npm test', 'Exit: 0', 'Base: a', 'Head: b',
643
- ].join('\n'));
644
- const premature = run(VALIDATOR, [specDir], ROOT);
645
- assert.notEqual(premature.status, 0);
646
- assert.match(output(premature), /completed execution receipt is valid only on terminal status done, never for readiness/);
647
-
648
- const weakSpecPath = path.join(specDir, 'spec.json');
649
- const weakSpec = JSON.parse(fs.readFileSync(weakSpecPath, 'utf8'));
650
- weakSpec.status = 'done';
651
- weakSpec.current_phase = 'done';
652
- write(weakSpecPath, JSON.stringify(weakSpec, null, 2));
653
- const weak = run(VALIDATOR, [specDir], ROOT);
654
- assert.notEqual(weak.status, 0);
655
- assert.match(output(weak), /completed proof and spec\.json must both use status done/);
656
- assert.match(output(weak), /observable expected result/);
657
- assert.match(output(weak), /distinct 40- or 64-hex Base and Head bindings/);
658
-
659
- const completedDir = createBoundedSpec(root, 'completed-feature-receipt');
660
- const completedSpecPath = path.join(completedDir, 'spec.json');
661
- const completedSpec = JSON.parse(fs.readFileSync(completedSpecPath, 'utf8'));
662
- completedSpec.status = 'done';
663
- completedSpec.current_phase = 'done';
664
- write(completedSpecPath, JSON.stringify(completedSpec, null, 2));
665
- write(path.join(completedDir, 'feature-receipt.md'), [
666
- '# Feature Verification Receipt', '', 'Verification: PASS', 'Status: done',
667
- 'Command: `node --test test/one.test.js`',
668
- 'Expected result: exit code 0 and one test passes',
669
- 'Observed result: exit code 0 and one test passed',
670
- 'Artifact/runtime proof: `src/one.js` persisted state value 1',
671
- `Base: ${'a'.repeat(40)}`,
672
- `Head: ${'b'.repeat(40)}`,
673
- ].join('\n'));
674
- const completed = run(VALIDATOR, [completedDir], ROOT);
675
- assert.equal(completed.status, 0, output(completed));
676
-
677
- } finally {
678
- fs.rmSync(root, { recursive: true, force: true });
679
- }
680
- });
681
-
682
- test('validator rejects required and optional spec artifacts that symlink outside the spec', () => {
683
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-artifact-links-'));
684
- const external = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-external-artifacts-'));
685
- try {
686
- const required = createSpec(root, { name: 'required-artifact-link' });
687
- fs.unlinkSync(path.join(required, 'requirements.md'));
688
- fs.symlinkSync(path.join(external, 'requirements.md'), path.join(required, 'requirements.md'));
689
- write(path.join(external, 'requirements.md'), '# External requirements\n');
690
- const requiredResult = run(VALIDATOR, [required], ROOT);
691
- assert.notEqual(requiredResult.status, 0);
692
- assert.match(output(requiredResult), /requirements\.md: symlink is not allowed/);
693
-
694
- const optional = createSpec(root, { name: 'optional-artifact-link' });
695
- const optionalSpecPath = path.join(optional, 'spec.json');
696
- const optionalSpec = JSON.parse(fs.readFileSync(optionalSpecPath, 'utf8'));
697
- optionalSpec.research = 'research.md';
698
- write(optionalSpecPath, JSON.stringify(optionalSpec, null, 2));
699
- write(path.join(external, 'research.md'), '# External research\n');
700
- fs.symlinkSync(path.join(external, 'research.md'), path.join(optional, 'research.md'));
701
- const optionalResult = run(VALIDATOR, [optional], ROOT);
702
- assert.notEqual(optionalResult.status, 0);
703
- assert.match(output(optionalResult), /research\.md: symlink is not allowed/);
704
-
705
- const bounded = createBoundedSpec(root, 'receipt-artifact-link');
706
- write(path.join(external, 'feature-receipt.md'), '# External receipt\n');
707
- fs.symlinkSync(path.join(external, 'feature-receipt.md'), path.join(bounded, 'feature-receipt.md'));
708
- const boundedResult = run(VALIDATOR, [bounded], ROOT);
709
- assert.notEqual(boundedResult.status, 0);
710
- assert.match(output(boundedResult), /feature-receipt\.md: symlink is not allowed/);
711
- } finally {
712
- fs.rmSync(root, { recursive: true, force: true });
713
- fs.rmSync(external, { recursive: true, force: true });
714
- }
715
- });
716
-
717
- test('Standard bounded readiness requires canonical testable requirements and a concrete design boundary', () => {
718
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-standard-placeholders-'));
719
- try {
720
- const specDir = createBoundedSpec(root, 'standard-placeholders');
721
- const specPath = path.join(specDir, 'spec.json');
722
- const spec = JSON.parse(fs.readFileSync(specPath, 'utf8'));
723
- spec.ready_for_implementation = true;
724
- spec.approvals.requirements.user_approved = true;
725
- spec.approvals.design.user_approved = true;
726
- spec.validation = { status: 'completed' };
727
- spec.updated_at = '2026-08-11T00:06:00+07:00';
728
- spec.timestamps = lifecycleTimestamps({ tasks: false });
729
- spec.scope_lock.source = '{{PROJECT_DESCRIPTION}}';
730
- fs.writeFileSync(specPath, JSON.stringify(spec, null, 2));
731
- write(path.join(specDir, 'requirements.md'), '# Requirements\n\n{{REQUIREMENTS}}\n');
732
- write(path.join(specDir, 'design.md'), '# Design\n\nTBD\n');
733
-
734
- const rejected = run(VALIDATOR, [specDir], ROOT);
735
- assert.notEqual(rejected.status, 0);
736
- assert.match(output(rejected), /scope_lock\.source.*concrete scope source/);
737
- assert.match(output(rejected), /requirements\.md: Standard readiness rejects placeholders/);
738
- assert.match(output(rejected), /design\.md: Standard readiness rejects placeholders/);
739
- assert.doesNotMatch(output(rejected), /feature-receipt\.md: canonical feature receipt required/);
740
-
741
- spec.scope_lock.source = 'The bounded feature scope is the one described here';
742
- fs.writeFileSync(specPath, JSON.stringify(spec, null, 2));
743
- write(path.join(specDir, 'requirements.md'), '# Requirements\n\nThe bounded behavior is explicitly defined.\n');
744
- write(path.join(specDir, 'design.md'), '# Design\n\nThe bounded implementation boundary is explicitly defined.\n');
745
- const generic = run(VALIDATOR, [specDir], ROOT);
746
- assert.notEqual(generic.status, 0);
747
- assert.match(output(generic), /canonical numeric requirement id/);
748
- assert.match(output(generic), /testable acceptance criterion using RN\.M/);
749
- assert.match(output(generic), /grounded path\/code identifier/);
750
-
751
- write(
752
- path.join(specDir, 'requirements.md'),
753
- '# Requirements\n\n## Requirements\n\n### Requirement 1: Locale persistence\n\n' +
754
- '- **R1.1** When a locale is saved, the CLI shall write the selected value to `.codex/runtime.json` and return exit code 0.\n',
755
- );
756
- write(
757
- path.join(specDir, 'design.md'),
758
- '# Design\n\n## Boundary\n\nThe `.codex/runtime.json` file is owned by `saveLocale`; the CLI calls it before returning success.\n\n' +
759
- '## Typed Anchors\n\n| ID | Type | Target | Role |\n|---|---|---|---|\n' +
760
- '| A-D-01 | file | `.codex/runtime.json` | locale persistence target |\n' +
761
- '| A-D-02 | symbol | `src/runtime.js#saveLocale` | locale writer |\n\n' +
762
- '## Decisions and Invariants\n\n### D1 — Persist selected locale\n\n- **Decision:** `saveLocale` writes the selected value.\n- **Negative path:** Invalid locale leaves state unchanged.\n- **Anchors:** A-D-01, A-D-02\n\n' +
763
- '## Verification\n\n| Requirement | Proof target | Expected result | Negative path / reachability |\n|---|---|---|---|\n' +
764
- '| R1.1 | `node --test test/runtime.test.js` | exit code 0 and saved locale | invalid locale through `saveLocale` |\n',
765
- );
766
- completeSemanticReview(specDir);
767
- const accepted = run(VALIDATOR, [specDir], ROOT);
768
- assert.equal(accepted.status, 0, output(accepted));
769
- } finally {
770
- fs.rmSync(root, { recursive: true, force: true });
771
- }
772
- });
773
-
774
- test('tasks-only validates the persisted policy before any filesystem mutation', () => {
775
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-tasks-only-preflight-'));
776
- try {
777
- const standardPolicy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
778
- const boundaries = JSON.stringify([{
779
- id: 'B-OWN', type: 'ownership', tasks: ['R1-01'],
780
- write_sets: { 'R1-01': ['src/one.js'] },
781
- }]);
782
- const cases = [
783
- ['malformed', () => '{', /existing spec\.json is invalid JSON/],
784
- ['null-state', () => 'null', /existing spec\.json must contain a JSON object/],
785
- ['missing-snapshot', (state) => { delete state.workflow_policy; return JSON.stringify(state, null, 2); }, /existing spec must contain a persisted workflow_policy/],
786
- ['null-snapshot', (state) => { state.workflow_policy = null; return JSON.stringify(state, null, 2); }, /existing workflow_policy is invalid/],
787
- ['extra-field', (state) => {
788
- state.workflow_policy = { ...standardPolicy, extra: true };
789
- return JSON.stringify(state, null, 2);
790
- }, /schema 2\.0 workflow_policy contains unsupported legacy authority field\(s\): extra/],
791
- ];
792
- for (const [name, mutate, expected] of cases) {
793
- const specDir = createLegacyTasklessSpec20(root, name, standardPolicy);
794
- const specPath = path.join(specDir, 'spec.json');
795
- const state = JSON.parse(fs.readFileSync(specPath, 'utf8'));
796
- write(specPath, mutate(state));
797
- const before = fs.readFileSync(specPath, 'utf8');
798
- const result = run(SCAFFOLD, [
799
- name, '--tasks', 'R1-01-one', '--tasks-only', '--boundaries', boundaries,
800
- ], root);
801
- assert.equal(result.status, 2, output(result));
802
- assert.match(output(result), expected);
803
- assert.equal(fs.readFileSync(specPath, 'utf8'), before);
804
- assert.equal(fs.existsSync(path.join(specDir, 'tasks')), false);
805
- assert.equal(fs.existsSync(path.join(specDir, 'research.md')), false);
806
- }
807
- } finally {
808
- fs.rmSync(root, { recursive: true, force: true });
809
- }
810
- });
811
-
812
- test('scaffold accepts only one safe alphanumeric-starting feature segment', () => {
813
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-feature-name-'));
814
- try {
815
- for (const feature of ['.', 'foo/bar', 'foo\\bar', '../escape', '..\\escape']) {
816
- const before = snapshotTree(root);
817
- const result = run(SCAFFOLD, [feature, '--lane', 'Standard'], root);
818
- assert.equal(result.status, 2, `${feature}: ${output(result)}`);
819
- assert.match(output(result), /feature|safe relative|path segment/i);
820
- assert.equal(snapshotTree(root), before, `${feature} must not mutate the workspace`);
821
- }
822
-
823
- const beforeFreshSequence = snapshotTree(root);
824
- const badFreshSequence = run(SCAFFOLD, ['bad-fresh-sequence', '--tasks', 'R1-1-one'], root);
825
- assert.equal(badFreshSequence.status, 2, output(badFreshSequence));
826
- assert.match(output(badFreshSequence), /SEQ 2 digits/);
827
- assert.equal(snapshotTree(root), beforeFreshSequence, 'one-digit fresh task sequence mutated the workspace');
828
-
829
- createLegacyTasklessSpec20(root, 'bad-tasks-only-sequence', POLICY.workflowPolicySnapshot({ riskSignals: {} }));
830
- const beforeTasksOnlySequence = snapshotTree(root);
831
- const badTasksOnlySequence = run(SCAFFOLD, [
832
- 'bad-tasks-only-sequence', '--tasks', 'R1-1-one', '--tasks-only',
833
- ], root);
834
- assert.equal(badTasksOnlySequence.status, 2, output(badTasksOnlySequence));
835
- assert.match(output(badTasksOnlySequence), /SEQ 2 digits/);
836
- assert.equal(snapshotTree(root), beforeTasksOnlySequence, 'one-digit tasks-only sequence mutated the workspace');
837
- } finally {
838
- fs.rmSync(root, { recursive: true, force: true });
839
- }
840
- });
841
-
842
- test('validator requires a strict persisted workflow-policy snapshot and never falls back to prose or legacy tier', () => {
843
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-policy-boundary-'));
844
- try {
845
- const policy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
846
- const cases = [
847
- ['missing', (state) => { delete state.workflow_policy; }, /missing persisted snapshot/],
848
- ['explicit-null', (state) => { state.workflow_policy = null; }, /workflow_policy must be an object/],
849
- ['malformed', (state) => { state.workflow_policy = { ...policy, proof_obligations: { needsExecutionProof: true } }; }, /proof_obligations must be an array/],
850
- ['extra-field', (state) => { state.workflow_policy = { ...policy, extra: true }; }, /workflow_policy fields must be exactly/],
851
- ['lane-mismatch', (state) => { state.workflow_policy = { ...policy, lane: 'Critical', automatic_lane: 'Critical' }; }, /lane is not the derived compatibility lane|automatic_lane is not the derived compatibility lane/],
852
- ['risk-mismatch', (state) => { state.workflow_policy = { ...policy, risks: ['auth'] }; }, /assurance_level must be at least Elevated/],
853
- ['obligation-mismatch', (state) => { state.workflow_policy = { ...policy, proof_obligations: ['needsInspection', 'needsExecutionProof'], actor_needs: [{ capability: 'inspection', independence: 'same-session' }, { capability: 'execution-proof', independence: 'same-session' }] }; }, /proof_obligations do not match assurance_level and risks/],
854
- ];
855
- for (const [name, mutate, expected] of cases) {
856
- const specDir = createSpec(root, { name, spec: { workflow_policy: policy } });
857
- const statePath = path.join(specDir, 'spec.json');
858
- const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
859
- mutate(state);
860
- write(statePath, JSON.stringify(state, null, 2));
861
- const beforeRun = snapshotTree(specDir);
862
- const result = run(VALIDATOR, [specDir], ROOT);
863
- assert.notEqual(result.status, 0, `${name} unexpectedly passed`);
864
- assert.match(output(result), expected);
865
- assert.equal(snapshotTree(specDir), beforeRun, `${name} validator mutated the snapshot`);
866
- }
867
-
868
- const legacyDir = createSpec(root, {
869
- name: 'legacy-ready',
870
- spec: {
871
- workflow_policy: undefined,
872
- design_context: { execution_tier: 'Deep' },
873
- ready_for_implementation: true,
874
- approvals: {
875
- requirements: { generated: true, agent_validated: true, user_approved: true },
876
- design: { generated: true, agent_validated: true, user_approved: true },
877
- tasks: { generated: true, agent_validated: true, user_approved: true },
878
- },
879
- validation: { status: 'completed' },
880
- updated_at: '2026-08-11T00:06:00+07:00',
881
- timestamps: lifecycleTimestamps(),
882
- },
883
- });
884
- const legacyBefore = snapshotTree(legacyDir);
885
- const legacyResult = run(VALIDATOR, [legacyDir], ROOT);
886
- assert.notEqual(legacyResult.status, 0);
887
- assert.match(output(legacyResult), /execution_tier is a read-only legacy adapter/);
888
- assert.doesNotMatch(output(legacyResult), /^PASS/m);
889
- assert.equal(snapshotTree(legacyDir), legacyBefore);
890
- } finally {
891
- fs.rmSync(root, { recursive: true, force: true });
892
- }
893
- });
894
-
895
- test('tasks-only preflights malformed topology, duplicates, and symlinks without mutation', () => {
896
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-topology-preflight-'));
897
- try {
898
- const policy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
899
- const taskPath = 'tasks/task-R1-01-one.md';
900
- const registryEntry = {
901
- id: 'R1-01', title: 'One', status: 'pending', dependencies: [], blocker: null,
902
- started_at: null, completed_at: null, last_updated_at: null,
903
- };
904
- const cases = [
905
- ['malformed-task-files', { workflow_policy: policy, task_files: 'not-an-array', task_registry: {} }, /task_files must be an array/],
906
- ['malformed-registry', { workflow_policy: policy, task_files: [], task_registry: [] }, /task_registry must be an object/],
907
- ['duplicate-task-files', { workflow_policy: policy, task_files: [taskPath, taskPath], task_registry: { [taskPath]: registryEntry } }, /duplicate path/],
908
- ['unknown-dependency', { workflow_policy: policy, task_files: [taskPath], task_registry: { [taskPath]: { ...registryEntry, dependencies: ['tasks/task-R1-02-two.md'] } } }, /unknown task/],
909
- ['unsafe-research-path', { workflow_policy: policy, research: '../outside' }, /spec\.research must be a safe relative path/],
910
- ];
911
- for (const [name, state, expected] of cases) {
912
- const specDir = createLegacyTasklessSpec20(root, name, policy);
913
- write(path.join(specDir, 'spec.json'), JSON.stringify({
914
- ...legacyTasklessSpec20State(name, policy),
915
- ...state,
916
- }, null, 2));
917
- for (const declaredTask of new Set(Array.isArray(state.task_files) ? state.task_files : [])) {
918
- write(path.join(specDir, declaredTask), task());
919
- }
920
- const boundaryTasks = Array.isArray(state.task_files) && state.task_files.includes(taskPath)
921
- ? ['R1-01', 'R1-02', 'R1-03'] : ['R1-02', 'R1-03'];
922
- const boundaries = JSON.stringify([{
923
- id: 'B-OWN', type: 'ownership', tasks: boundaryTasks,
924
- write_sets: Object.fromEntries(boundaryTasks.map((id) => [id, [`src/${id.toLowerCase()}.js`]])),
925
- }]);
926
- const before = snapshotTree(root);
927
- const result = run(SCAFFOLD, [
928
- name, '--tasks', 'R1-02-two,R1-03-three', '--tasks-only',
929
- '--boundaries', boundaries,
930
- ], root);
931
- assert.equal(result.status, 2, output(result));
932
- assert.match(output(result), expected);
933
- assert.equal(snapshotTree(root), before, `${name} mutated the existing topology`);
934
- }
935
-
936
- const conflictDir = createLegacyTasklessSpec20(root, 'conflicting-duplicate', policy);
937
- write(path.join(conflictDir, 'tasks/task-R1-01-one.md'), 'already filled\n');
938
- write(path.join(conflictDir, 'spec.json'), JSON.stringify({
939
- ...legacyTasklessSpec20State('conflicting-duplicate', policy),
940
- task_files: [taskPath],
941
- task_registry: { [taskPath]: { ...registryEntry, title: 'Conflicting title' } },
942
- }, null, 2));
943
- const conflictBefore = snapshotTree(root);
944
- const singleBoundary = JSON.stringify([{
945
- id: 'B-OWN', type: 'ownership', tasks: ['R1-01'], write_sets: { 'R1-01': ['src/one.js'] },
946
- }]);
947
- const conflict = run(SCAFFOLD, [
948
- 'conflicting-duplicate', '--tasks', 'R1-01-one', '--tasks-only',
949
- '--boundaries', singleBoundary,
950
- ], root);
951
- assert.equal(conflict.status, 2, output(conflict));
952
- assert.match(output(conflict), /conflicting duplicate/);
953
- assert.equal(snapshotTree(root), conflictBefore);
954
-
955
- const duplicateDir = createLegacyTasklessSpec20(root, 'duplicate-cli', policy);
956
- const duplicateBefore = snapshotTree(root);
957
- const duplicate = run(SCAFFOLD, [
958
- 'duplicate-cli', '--tasks', 'R1-01-one,R1-01-two', '--tasks-only',
959
- '--boundaries', singleBoundary,
960
- ], root);
961
- assert.equal(duplicate.status, 2, output(duplicate));
962
- assert.match(output(duplicate), /duplicate task id.*R1-01/);
963
- assert.equal(snapshotTree(root), duplicateBefore);
964
-
965
- const symlinkDir = createLegacyTasklessSpec20(root, 'symlink-tasks', policy);
966
- const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-symlink-target-'));
967
- fs.symlinkSync(outside, path.join(symlinkDir, 'tasks'), 'dir');
968
- const symlinkBefore = snapshotTree(root);
969
- const symlinkResult = run(SCAFFOLD, [
970
- 'symlink-tasks', '--tasks', 'R1-01-one', '--tasks-only', '--boundaries', singleBoundary,
971
- ], root);
972
- assert.equal(symlinkResult.status, 2, output(symlinkResult));
973
- assert.match(output(symlinkResult), /symlink/);
974
- assert.equal(snapshotTree(root), symlinkBefore);
975
- fs.rmSync(outside, { recursive: true, force: true });
976
- } finally {
977
- fs.rmSync(root, { recursive: true, force: true });
978
- }
979
- });
980
-
981
- test('tasks-only commits transactionally and preserves exact bytes after an injected partial failure', () => {
982
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-transaction-'));
983
- try {
984
- const policy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
985
- createLegacyTasklessSpec20(root, 'partial-write', policy);
986
- const before = snapshotTree(root);
987
- const result = run(
988
- SCAFFOLD,
989
- ['partial-write', '--tasks', 'R1-01-one,R1-02-two', '--boundaries', JSON.stringify([{
990
- id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
991
- write_sets: { 'R1-01': ['src/one.js'], 'R1-02': ['src/two.js'] },
992
- }]), '--tasks-only'],
993
- root,
994
- { env: { CAFEKIT_SCAFFOLD_FAIL_AFTER_WRITES: '1' } },
995
- );
996
- assert.equal(result.status, 2, output(result));
997
- assert.match(output(result), /injected failure.*transaction rolled back/);
998
- assert.equal(snapshotTree(root), before);
999
- } finally {
1000
- fs.rmSync(root, { recursive: true, force: true });
1001
- }
1002
- });
1003
-
1004
- test('tasks-only migration derives only absent feature identity and rejects present conflicts atomically', () => {
1005
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-legacy-identity-'));
1006
- const policy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
1007
- const boundaries = JSON.stringify([{
1008
- id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
1009
- write_sets: { 'R1-01': ['src/one.js'], 'R1-02': ['src/two.js'] },
1010
- }]);
1011
- const migrate = (feature) => run(SCAFFOLD, [
1012
- feature, '--tasks', 'R1-01-one,R1-02-two', '--tasks-only', '--boundaries', boundaries,
1013
- ], root);
1014
- try {
1015
- const absentFeature = 'legacy-identity-absent';
1016
- const absentDir = createLegacyTasklessSpec20(root, absentFeature, policy);
1017
- const absentPath = path.join(absentDir, 'spec.json');
1018
- const absentState = JSON.parse(fs.readFileSync(absentPath, 'utf8'));
1019
- delete absentState.feature_name;
1020
- write(absentPath, JSON.stringify(absentState, null, 2));
1021
-
1022
- const migrated = migrate(absentFeature);
1023
- assert.equal(migrated.status, 0, output(migrated));
1024
- const canonical = JSON.parse(fs.readFileSync(absentPath, 'utf8'));
1025
- assert.equal(canonical.schema_version, '2.1');
1026
- assert.equal(canonical.feature_name, absentFeature);
1027
- assert.equal(canonical.created_at, '2026-08-11T00:00:00+07:00');
1028
- assert.equal(canonical.language, 'en');
1029
- assert.equal(canonical.status, 'in_progress');
1030
- assert.deepEqual(canonical.scope_lock, absentState.scope_lock);
1031
- assert.deepEqual(canonical.authoring, {
1032
- requirements: 'draft', design: 'draft', research: 'absent', tasks: 'draft',
1033
- });
1034
- assert.equal(canonical.ready_for_implementation, false);
1035
- assert.equal(canonical.validation.status, 'not-run');
1036
- assert.equal(canonical.validation.semantic_review.status, 'not-run');
1037
- assert.equal(Object.hasOwn(canonical, 'research'), false);
1038
- assert.equal(fs.existsSync(path.join(absentDir, 'research.md')), false);
1039
- assert.equal(fs.existsSync(path.join(absentDir, 'receipts')), false);
1040
- assert.equal(fs.existsSync(path.join(absentDir, 'reports')), false);
1041
- for (const entry of Object.values(canonical.task_registry)) {
1042
- assert.deepEqual(
1043
- [entry.status, entry.blocker, entry.started_at, entry.completed_at, entry.last_updated_at],
1044
- ['pending', null, null, null, null],
1045
- );
1046
- }
1047
-
1048
- for (const [label, featureName] of [
1049
- ['empty', ''], ['non-string', 42], ['different', 'another-feature'],
1050
- ]) {
1051
- const feature = `legacy-identity-${label}`;
1052
- const specDir = createLegacyTasklessSpec20(root, feature, policy);
1053
- const specPath = path.join(specDir, 'spec.json');
1054
- const state = JSON.parse(fs.readFileSync(specPath, 'utf8'));
1055
- state.feature_name = featureName;
1056
- write(specPath, JSON.stringify(state, null, 2));
1057
- const before = snapshotTree(specDir);
1058
- const rejected = migrate(feature);
1059
- assert.equal(rejected.status, 2, `${label}: ${output(rejected)}`);
1060
- assert.match(output(rejected), /feature_name conflicts with the scaffold feature identity/);
1061
- assert.equal(snapshotTree(specDir), before, `${label} identity rejection mutated the fixture`);
1062
- }
1063
- } finally {
1064
- fs.rmSync(root, { recursive: true, force: true });
1065
- }
1066
- });
1067
-
1068
- test('fresh scaffold rolls back authority rejection and injected partial writes without deleting pre-existing content', () => {
1069
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-fresh-transaction-'));
1070
- try {
1071
- const codexRoot = path.join(root, '.codex');
1072
- const installedScripts = path.join(codexRoot, 'scripts');
1073
- const installedHooks = path.join(codexRoot, 'hooks');
1074
- const installedTemplates = path.join(codexRoot, 'skills/specs/templates');
1075
- fs.mkdirSync(installedScripts, { recursive: true });
1076
- fs.mkdirSync(installedHooks, { recursive: true });
1077
- fs.mkdirSync(path.dirname(installedTemplates), { recursive: true });
1078
- fs.cpSync(path.join(ROOT, 'src/claude/skills/specs/templates'), installedTemplates, { recursive: true });
1079
- copyCodexRuntime(codexRoot, ['scripts/spec-scaffold.cjs']);
1080
- write(path.join(installedHooks, 'completion-authority-state.cjs'), 'module.exports = {};\n');
1081
- write(
1082
- path.join(installedHooks, 'completion-authority-check.cjs'),
1083
- 'module.exports = { observePolicyBaseline() { return { ok: false, reason: "fixture authority reject" }; } };\n',
1084
- );
1085
- write(path.join(root, 'specs/keep.txt'), 'pre-existing\n');
1086
-
1087
- const beforeAuthority = snapshotTree(root);
1088
- const authority = run(
1089
- path.join(installedScripts, 'spec-scaffold.cjs'),
1090
- ['authority-reject', '--lane', 'Standard'],
1091
- root,
1092
- );
1093
- assert.equal(authority.status, 2, output(authority));
1094
- assert.match(output(authority), /fixture authority reject.*transaction rolled back/);
1095
- assert.equal(snapshotTree(root), beforeAuthority);
1096
- assert.equal(fs.existsSync(path.join(root, 'specs/authority-reject')), false);
1097
-
1098
- write(
1099
- path.join(installedHooks, 'completion-authority-check.cjs'),
1100
- 'module.exports = { observePolicyBaseline() { return { ok: true }; } };\n',
1101
- );
1102
- const seeded = run(
1103
- path.join(installedScripts, 'spec-scaffold.cjs'),
1104
- ['tasks-authority', '--lane', 'Standard'],
1105
- root,
1106
- );
1107
- assert.equal(seeded.status, 0, output(seeded));
1108
- write(
1109
- path.join(installedHooks, 'completion-authority-check.cjs'),
1110
- 'module.exports = { observePolicyBaseline() { return { ok: false, reason: "fixture tasks-only authority reject" }; } };\n',
1111
- );
1112
- const beforeTasksAuthority = snapshotTree(root);
1113
- const tasksAuthority = run(
1114
- path.join(installedScripts, 'spec-scaffold.cjs'),
1115
- ['tasks-authority', '--tasks', 'R1-01-one,R1-02-two', '--tasks-only',
1116
- '--boundaries', JSON.stringify([{
1117
- id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
1118
- write_sets: { 'R1-01': ['src/one.js'], 'R1-02': ['src/two.js'] },
1119
- }])],
1120
- root,
1121
- );
1122
- assert.equal(tasksAuthority.status, 2, output(tasksAuthority));
1123
- assert.match(output(tasksAuthority), /fixture tasks-only authority reject.*transaction rolled back/);
1124
- assert.equal(snapshotTree(root), beforeTasksAuthority);
1125
- assert.equal(fs.existsSync(path.join(root, 'specs/tasks-authority/tasks/task-R1-01-one.md')), false);
1126
-
1127
- const beforeInjected = snapshotTree(root);
1128
- const injected = run(
1129
- SCAFFOLD,
1130
- ['fresh-injected', '--lane', 'Standard'],
1131
- root,
1132
- { env: { CAFEKIT_SCAFFOLD_FAIL_AFTER_WRITES: '2' } },
1133
- );
1134
- assert.equal(injected.status, 2, output(injected));
1135
- assert.match(output(injected), /injected failure.*transaction rolled back/);
1136
- assert.equal(snapshotTree(root), beforeInjected);
1137
- assert.equal(fs.readFileSync(path.join(root, 'specs/keep.txt'), 'utf8'), 'pre-existing\n');
1138
- } finally {
1139
- fs.rmSync(root, { recursive: true, force: true });
1140
- }
1141
- });
1142
-
1143
- test('installed Codex scaffold rejects a symlinked template parent before creating a spec', () => {
1144
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-template-symlink-'));
1145
- const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-template-outside-'));
1146
- try {
1147
- const codexRoot = path.join(root, '.codex');
1148
- copyCodexRuntime(codexRoot, ['scripts/spec-scaffold.cjs']);
1149
- write(
1150
- path.join(codexRoot, 'hooks/completion-authority-check.cjs'),
1151
- 'module.exports = { observePolicyBaseline() { return { ok: true }; } };\n',
1152
- );
1153
- write(path.join(codexRoot, 'hooks/completion-authority-state.cjs'), 'module.exports = {};\n');
1154
- fs.cpSync(path.join(ROOT, 'src/claude/skills/specs/templates'), outside, { recursive: true });
1155
- const templatesParent = path.join(root, '.agents/skills/specs');
1156
- fs.mkdirSync(templatesParent, { recursive: true });
1157
- fs.symlinkSync(outside, path.join(templatesParent, 'templates'));
1158
-
1159
- const result = run(
1160
- path.join(codexRoot, 'scripts/spec-scaffold.cjs'),
1161
- ['template-parent-escape', '--lane', 'Standard'],
1162
- root,
1163
- );
1164
- assert.equal(result.status, 2, output(result));
1165
- assert.match(output(result), /template check failed|symlink component rejected/i);
1166
- assert.equal(fs.existsSync(path.join(root, 'specs/template-parent-escape')), false);
1167
- } finally {
1168
- fs.rmSync(root, { recursive: true, force: true });
1169
- fs.rmSync(outside, { recursive: true, force: true });
1170
- }
1171
- });
1172
-
1173
- test('tasks-only is idempotent for an identical task request and preserves the snapshot bytes', () => {
1174
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-idempotent-'));
1175
- try {
1176
- const policy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
1177
- createLegacyTasklessSpec20(root, 'idempotent', policy);
1178
- const boundaryArgs = ['--boundaries', JSON.stringify([{
1179
- id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
1180
- write_sets: { 'R1-01': ['src/one.js'], 'R1-02': ['src/two.js'] },
1181
- }])];
1182
- const idempotentArgs = ['idempotent', '--tasks', 'R1-01-one,R1-02-two', '--tasks-only', ...boundaryArgs];
1183
- const first = run(SCAFFOLD, idempotentArgs, root);
1184
- assert.equal(first.status, 0, output(first));
1185
- const beforeSecond = snapshotTree(root);
1186
- const second = run(SCAFFOLD, idempotentArgs, root);
1187
- assert.equal(second.status, 0, output(second));
1188
- assert.match(output(second), /0 new task stub\(s\)/);
1189
- assert.equal(snapshotTree(root), beforeSecond);
1190
-
1191
- const artifactBase = run(SCAFFOLD, ['idempotent-artifact', '--lane', 'Standard'], root);
1192
- assert.equal(artifactBase.status, 0, output(artifactBase));
1193
- const proofArgs = ['--boundaries', JSON.stringify([{
1194
- id: 'B-V', type: 'proof', subject: 'R1-01', verifier: 'R1-02',
1195
- verification_ref: 'V1', artifact_anchor: 'A-R1-02-02',
1196
- }])];
1197
- const artifactArgs = [
1198
- 'idempotent-artifact', '--tasks', 'R1-01-one,R1-02-two', '--tasks-only', ...proofArgs,
1199
- ];
1200
- const artifactFirst = run(SCAFFOLD, artifactArgs, root);
1201
- assert.equal(artifactFirst.status, 0, output(artifactFirst));
1202
- const artifactDir = path.join(root, 'specs/idempotent-artifact');
1203
- completeScaffoldedSpec21(artifactDir);
1204
- const artifactValidation = run(VALIDATOR, [artifactDir], ROOT);
1205
- assert.equal(artifactValidation.status, 0, output(artifactValidation));
1206
- const beforeArtifactSecond = snapshotTree(root);
1207
- const artifactSecond = run(SCAFFOLD, artifactArgs, root);
1208
- assert.equal(artifactSecond.status, 0, output(artifactSecond));
1209
- assert.match(output(artifactSecond), /0 new task stub\(s\)/);
1210
- assert.equal(snapshotTree(root), beforeArtifactSecond);
1211
- } finally {
1212
- fs.rmSync(root, { recursive: true, force: true });
1213
- }
1214
- });
1215
-
1216
- test('tasks-only consumes the policy snapshot and escalates auth to Elevated monotonically', () => {
1217
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-tasks-only-policy-'));
1218
- try {
1219
- const specDir = createLegacyTasklessSpec20(root, 'existing', POLICY.workflowPolicySnapshot({ riskSignals: {} }));
1220
- const boundaries = JSON.stringify([{
1221
- id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
1222
- write_sets: { 'R1-01': ['src/auth.js'], 'R1-02': ['src/next.js'] },
1223
- }]);
1224
- const escalated = run(SCAFFOLD, [
1225
- 'existing', '--tasks', 'R1-01-auth,R1-02-next', '--risks', 'auth', '--tasks-only',
1226
- '--boundaries', boundaries,
1227
- ], root);
1228
- assert.equal(escalated.status, 0, output(escalated));
1229
- assert.match(output(escalated), /2 new task stub\(s\)/);
1230
- assert.doesNotMatch(output(escalated), /research\.md created/);
1231
- const first = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
1232
- assert.equal(POLICY.readWorkflowPolicySnapshot(first).lane, 'Standard');
1233
- assert.equal(POLICY.readWorkflowPolicySnapshot(first).assurance_level, 'Elevated');
1234
- assert.deepEqual(first.workflow_policy.risks, ['auth']);
1235
- assert.equal(POLICY.readWorkflowPolicySnapshot(first).artifact_profile, 'bounded');
1236
- assert.equal(POLICY.readWorkflowPolicySnapshot(first).proof_obligations.includes('needsResearchGrounding'), false);
1237
- assert.equal(fs.existsSync(path.join(specDir, 'research.md')), false);
1238
- assert.equal(fs.existsSync(path.join(specDir, 'reports')), false);
1239
-
1240
- const policyBytes = JSON.stringify(first.workflow_policy);
1241
- const noNewRisk = run(SCAFFOLD, [
1242
- 'existing', '--tasks', 'R1-01-auth,R1-02-next', '--tasks-only', '--boundaries', boundaries,
1243
- ], root);
1244
- assert.equal(noNewRisk.status, 0, output(noNewRisk));
1245
- assert.match(output(noNewRisk), /0 new task stub\(s\)/);
1246
- const second = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
1247
- assert.equal(JSON.stringify(second.workflow_policy), policyBytes);
1248
-
1249
- const beforeRejectedLane = fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8');
1250
- const rejectedBoundaries = JSON.stringify([{
1251
- id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02', 'R1-03'],
1252
- write_sets: {
1253
- 'R1-01': ['src/auth.js'], 'R1-02': ['src/next.js'], 'R1-03': ['src/invalid.js'],
1254
- },
1255
- }]);
1256
- const rejectedLane = run(SCAFFOLD, [
1257
- 'existing', '--tasks', 'R1-03-invalid', '--lane', 'Critical', '--tasks-only',
1258
- '--boundaries', rejectedBoundaries,
1259
- ], root);
1260
- assert.equal(rejectedLane.status, 2, output(rejectedLane));
1261
- assert.match(output(rejectedLane), /does not match persisted workflow_policy\.lane/);
1262
- assert.equal(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'), beforeRejectedLane);
1263
- assert.equal(fs.existsSync(path.join(specDir, 'tasks/task-R1-03-invalid.md')), false);
1264
- } finally {
1265
- fs.rmSync(root, { recursive: true, force: true });
1266
- }
1267
- });
1268
-
1269
- test('scaffold emits minimal 2.1 artifacts without default research or reports ceremony', () => {
1270
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-artifact-profile-'));
1271
- try {
1272
- const standardDefault = run(SCAFFOLD, ['standard-default', '--lane', 'Standard'], root);
1273
- assert.equal(standardDefault.status, 0, output(standardDefault));
1274
- const standardDir = path.join(root, 'specs', 'standard-default');
1275
- assert.deepEqual(fs.readdirSync(standardDir).sort(), [
1276
- 'design.md', 'requirements.md', 'spec.json',
1277
- ]);
1278
-
1279
- const strict = run(SCAFFOLD, ['strict-taskless', '--risks', 'auth', '--assurance-level', 'Strict'], root);
1280
- assert.equal(strict.status, 0, output(strict));
1281
- const strictDir = path.join(root, 'specs', 'strict-taskless');
1282
- const strictSpec = JSON.parse(fs.readFileSync(path.join(strictDir, 'spec.json'), 'utf8'));
1283
- assert.equal(strictSpec.schema_version, '2.1');
1284
- assert.equal(POLICY.readWorkflowPolicySnapshot(strictSpec).lane, 'Critical');
1285
- assert.equal(strictSpec.authoring.tasks, 'absent');
1286
- assert.equal(fs.existsSync(path.join(strictDir, 'research.md')), false);
1287
- assert.equal(fs.existsSync(path.join(strictDir, 'reports')), false);
1288
- } finally {
1289
- fs.rmSync(root, { recursive: true, force: true });
1290
- }
1291
- });
1292
-
1293
- test('strict validator requires numeric IDs, rejects phantom mappings, and grounds research content', () => {
1294
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-strict-grounding-'));
1295
- try {
1296
- const strictPolicy = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' });
1297
- const noIds = createSpec(root, {
1298
- name: 'no-numeric-ids',
1299
- requirements: '# Requirements\n\nThe behavior has no numeric identifier.\n',
1300
- spec: { workflow_policy: strictPolicy },
1301
- });
1302
- const noIdsResult = run(VALIDATOR, [noIds], ROOT);
1303
- assert.notEqual(noIdsResult.status, 0);
1304
- assert.match(output(noIdsResult), /strict workflow requires numeric requirement IDs/);
1305
-
1306
- const legacyDialect = createSpec(root, {
1307
- name: 'legacy-req-dialect',
1308
- requirements: '# Requirements\n\n## Requirements\n\n### REQ-01: One\n\n- **REQ-01.1** The service shall return one result.\n',
1309
- spec: { workflow_policy: strictPolicy },
1310
- });
1311
- const legacyDialectResult = run(VALIDATOR, [legacyDialect], ROOT);
1312
- assert.notEqual(legacyDialectResult.status, 0);
1313
- assert.match(output(legacyDialectResult), /REQ-01 is not a canonical requirement id/);
1314
-
1315
- const phantom = createSpec(root, {
1316
- name: 'phantom-mapping',
1317
- task: task({ mapping: '9.1' }),
1318
- spec: { workflow_policy: strictPolicy },
1319
- });
1320
- const phantomResult = run(VALIDATOR, [phantom], ROOT);
1321
- assert.notEqual(phantomResult.status, 0);
1322
- assert.match(output(phantomResult), /references unknown requirement R9/);
1323
-
1324
- const researchPlaceholder = createSpec(root, {
1325
- name: 'research-placeholder',
1326
- task: task({ related: '`src/strict-only.js` | Read' }),
1327
- design: criticalNoObligationDesign(),
1328
- spec: { workflow_policy: strictPolicy, research: 'research.md' },
1329
- });
1330
- write(path.join(researchPlaceholder, 'research.md'), '# Research\n\n## Evidence Summary\n- TBD\n');
1331
- const placeholderResult = run(VALIDATOR, [researchPlaceholder], ROOT);
1332
- assert.notEqual(placeholderResult.status, 0);
1333
- assert.match(output(placeholderResult), /Evidence Summary must contain concrete evidence/);
1334
-
1335
- write(path.join(researchPlaceholder, 'research.md'), '# Research\n\n## Evidence Summary\n- Scout inspected `src/one.js` and confirmed the caller path.\n');
1336
- const concreteResult = run(VALIDATOR, [researchPlaceholder], ROOT);
1337
- assert.equal(concreteResult.status, 0, output(concreteResult));
1338
- } finally {
1339
- fs.rmSync(root, { recursive: true, force: true });
1340
- }
1341
- });
1342
-
1343
- test('validator rejects unsupported or duplicate structured requirement IDs without mining incidental prose', () => {
1344
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-requirement-inventory-'));
1345
- try {
1346
- const unsupportedHeading = createSpec(root, {
1347
- name: 'unsupported-heading',
1348
- requirements: '# Requirements\n\n### R1A: One\n\n- **R1.1** The service shall return status value 1.\n',
1349
- });
1350
- let result = run(VALIDATOR, [unsupportedHeading], ROOT);
1351
- assert.notEqual(result.status, 0);
1352
- assert.match(output(result), /unsupported requirement id R1A/);
1353
-
1354
- const unsupportedCriterion = createSpec(root, {
1355
- name: 'unsupported-criterion',
1356
- requirements: '# Requirements\n\n### Requirement 1: One\n\n- **R1-1** The service shall return status value 1.\n',
1357
- });
1358
- result = run(VALIDATOR, [unsupportedCriterion], ROOT);
1359
- assert.notEqual(result.status, 0);
1360
- assert.match(output(result), /unsupported acceptance criterion id R1-1/);
1361
-
1362
- const duplicates = createSpec(root, {
1363
- name: 'duplicate-inventory',
1364
- requirements: '# Requirements\n\n### Requirement 1: First\n\n- **R1.1** The service shall return status value 1.\n\n' +
1365
- '### R1: Duplicate\n\n- **R1.1** The service shall return status value 2.\n',
1366
- });
1367
- result = run(VALIDATOR, [duplicates], ROOT);
1368
- assert.notEqual(result.status, 0);
1369
- assert.match(output(result), /duplicate canonical requirement id R1/);
1370
- assert.match(output(result), /duplicate canonical acceptance criterion id R1\.1/);
1371
-
1372
- const incidental = createSpec(root, {
1373
- name: 'incidental-rfc-adr-and-reference',
1374
- task: task({ related: '`src/one.js` | Read' }),
1375
- requirements: '# Requirements\n\n### Requirement 1: One\n\n' +
1376
- '- **R1.1** The service shall return one deterministic result with status value 1.\n\n' +
1377
- 'See RFC 9110, ADR-001, and R7.2 for historical context only.\n',
1378
- });
1379
- result = run(VALIDATOR, [incidental], ROOT);
1380
- assert.equal(result.status, 0, output(result));
1381
-
1382
- const ignoredSyntax = createSpec(root, {
1383
- name: 'ignored-fenced-and-commented-inventory',
1384
- task: task({ related: '`src/one.js` | Read' }),
1385
- requirements: '# Requirements\n\n### Requirement 1: One\n\n' +
1386
- '- **R1.1** The service shall return one deterministic result with status value 1.\n\n' +
1387
- '<!-- ### Requirement 1: commented duplicate\n- **R1.1** commented duplicate -->\n\n' +
1388
- '```markdown\n### Requirement 1: example duplicate\n- **R1.1** example duplicate\n```\n',
1389
- });
1390
- result = run(VALIDATOR, [ignoredSyntax], ROOT);
1391
- assert.equal(result.status, 0, output(result));
1392
-
1393
- for (const [name, heading] of [
1394
- ['numeric-heading', '### 1: One'],
1395
- ['bracket-heading', '### [R1]: One'],
1396
- ]) {
1397
- const invalidHeading = createSpec(root, {
1398
- name,
1399
- requirements: `# Requirements\n\n${heading}\n\n- **R1.1** The service shall return status value 1.\n`,
1400
- });
1401
- result = run(VALIDATOR, [invalidHeading], ROOT);
1402
- assert.notEqual(result.status, 0);
1403
- assert.match(output(result), /unsupported requirement id/);
1404
- }
1405
- } finally {
1406
- fs.rmSync(root, { recursive: true, force: true });
1407
- }
1408
- });
1409
-
1410
- test('validator requires substantive Specs v2 task semantics and planned executable proof', () => {
1411
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-task-semantics-'));
1412
- try {
1413
- const invalid = createSpec(root, { name: 'weak-task-semantics' });
1414
- const weakTask = task({ related: '`src/one.js` | Read' })
1415
- .replace('Calling the real fixture entrypoint returns status value 1.', 'TBD')
1416
- .replace('- **In scope:** Implement the mapped observable behavior.', '- **In scope:** TBD')
1417
- .replace('- **Out of scope:** Unrelated runtime behavior.', '- **Out of scope:** N/A')
1418
- .replace('- **R1.1:** Calling `src/one.js` returns an observable result with status value 1.', '- **R1.1:** Works')
1419
- .replace('| A-R1-01-01 | command | `node --test test/one.test.js` |', '| A-R1-01-01 | command | `npm run fixture-check` |')
1420
- .replace('- **Command:** `node --test test/one.test.js`', '- **Command:** run tests')
1421
- .replace('exit code 0 and one assertion for status value 1', 'looks good')
1422
- .replace('invalid input returns a deterministic error result', 'TBD')
1423
- .replace('`src/one.js` is invoked by the focused test', 'TBD');
1424
- writeRaw(path.join(invalid, 'tasks/task-R1-01-one.md'), weakTask);
1425
- const rejected = run(VALIDATOR, [invalid], ROOT);
1426
- assert.notEqual(rejected.status, 0);
1427
- assert.match(output(rejected), /Outcome must be substantive/);
1428
- assert.match(output(rejected), /Verification Plan requires a command-shaped invocation or justified N\/A/);
1429
- assert.match(output(rejected), /Verification Plan requires an observable Expected result/);
1430
- assert.match(output(rejected), /Verification Plan requires a negative-path disposition/);
1431
- assert.match(output(rejected), /Verification Plan requires a concrete reachability target or justified N\/A/);
1432
-
1433
- const emptyExecution = createSpec(root, {
1434
- name: 'empty-steps-mapping-only-requirements',
1435
- task: task({ related: '`src/empty.js` | Read' })
1436
- .replace(/## Changes\n[\s\S]*?\n## Acceptance/, '## Changes\n\n## Acceptance'),
1437
- });
1438
- const emptyExecutionResult = run(VALIDATOR, [emptyExecution], ROOT);
1439
- assert.notEqual(emptyExecutionResult.status, 0);
1440
- assert.match(output(emptyExecutionResult), /Changes must be substantive|Changes must contain at least one substantive actionable checkbox/);
1441
-
1442
- const justified = createSpec(root, {
1443
- name: 'justified-command-na',
1444
- task: task({ related: '`src/static-contract.js` | Read' }).replace(
1445
- '- **Command:** `node --test test/one.test.js`',
1446
- '- Command: N/A — no executable runtime exists because this task only inspects a static contract',
1447
- ),
1448
- });
1449
- const accepted = run(VALIDATOR, [justified], ROOT);
1450
- assert.equal(accepted.status, 0, output(accepted));
1451
-
1452
- const projectSpecific = createSpec(root, {
1453
- name: 'project-specific-command-shape',
1454
- task: task({ related: '`src/project-specific.js` | Read' })
1455
- .replace('node --test test/one.test.js', 'npm run project-specific-check'),
1456
- });
1457
- const projectSpecificResult = run(VALIDATOR, [projectSpecific], ROOT);
1458
- assert.equal(projectSpecificResult.status, 0, output(projectSpecificResult));
1459
- } finally {
1460
- fs.rmSync(root, { recursive: true, force: true });
1461
- }
1462
- });
1463
-
1464
- test('all ready non-Direct specs require semantic design and coherent authoring timestamps', () => {
1465
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-ready-lifecycle-'));
1466
-
1467
- function readyFixture(name, mutate = () => {}) {
1468
- const specDir = createSpec(root, {
1469
- name,
1470
- task: task({ related: '`src/one.js` | Read' }),
1471
- spec: { workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: {} }) },
1472
- });
1473
- const specPath = path.join(specDir, 'spec.json');
1474
- const state = JSON.parse(fs.readFileSync(specPath, 'utf8'));
1475
- state.ready_for_implementation = true;
1476
- state.validation = { status: 'completed' };
1477
- state.updated_at = '2026-08-11T00:06:00+07:00';
1478
- state.timestamps = lifecycleTimestamps();
1479
- mutate(state, specDir);
1480
- write(specPath, JSON.stringify(state, null, 2));
1481
- completeSemanticReview(specDir);
1482
- return specDir;
1483
- }
1484
-
1485
- try {
1486
- const critical = readyFixture('critical-generic-design', (state, specDir) => {
1487
- state.workflow_policy = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' });
1488
- state.timestamps = lifecycleTimestamps({ research: true });
1489
- write(path.join(specDir, 'design.md'), '# Design\n\n## Architecture\n\nThe implementation boundary is defined and appropriate.\n');
1490
- });
1491
- let result = run(VALIDATOR, [critical], ROOT);
1492
- assert.notEqual(result.status, 0);
1493
- assert.match(output(result), /design\.md: Critical readiness requires a grounded path\/code identifier/);
1494
-
1495
- const strictAuthoring = readyFixture('critical-one-task-authoring', (state, specDir) => {
1496
- state.workflow_policy = POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' });
1497
- state.timestamps = lifecycleTimestamps({ research: true });
1498
- write(path.join(specDir, 'design.md'), criticalNoObligationDesign());
1499
- });
1500
- result = run(VALIDATOR, [strictAuthoring], ROOT);
1501
- assert.notEqual(result.status, 0);
1502
- assert.match(output(result), /Strict assurance is no longer supported/);
1503
-
1504
- const invalidCases = [
1505
- ['boolean-created-at', (state) => { state.created_at = true; }, /created_at: must be an ISO 8601 timestamp|created_at: ready handoff requires/],
1506
- ['boolean-task-time', (state) => { state.timestamps.tasks_done = true; }, /timestamps\.tasks_done: must be null or an ISO 8601 timestamp|ready handoff requires/],
1507
- ['missing-validation-time', (state) => { delete state.timestamps.validation_done; }, /timestamps\.validation_done: ready handoff requires/],
1508
- ['reversed-phase-time', (state) => { state.timestamps.design_done = '2026-08-11T00:00:30+07:00'; }, /final lifecycle timestamps must be ordered/],
1509
- ['mismatched-final-state', (state) => { state.updated_at = '2026-08-11T00:07:00+07:00'; }, /updated_at must describe the same final state/],
1510
- ];
1511
- for (const [name, mutate, expected] of invalidCases) {
1512
- const specDir = readyFixture(name, mutate);
1513
- result = run(VALIDATOR, [specDir], ROOT);
1514
- assert.notEqual(result.status, 0, `${name} unexpectedly passed`);
1515
- assert.match(output(result), expected);
1516
- }
1517
-
1518
- const reviewDir = readyFixture('five-task-review-time', (state, specDir) => {
1519
- state.design_context = { validation_recommended: true };
1520
- for (let index = 2; index <= 5; index += 1) {
1521
- const taskPath = `tasks/task-R1-0${index}-review.md`;
1522
- state.task_files.push(taskPath);
1523
- state.task_registry[taskPath] = {
1524
- id: `R1-0${index}`, title: `Review ${index}`, status: 'pending', dependencies: [], blocker: null,
1525
- started_at: null, completed_at: null, last_updated_at: null,
1526
- };
1527
- write(path.join(specDir, taskPath), task({ id: `R1-0${index}`, related: `\`src/review-${index}.js\` | Read` }));
1528
- }
1529
- });
1530
- result = run(VALIDATOR, [reviewDir], ROOT);
1531
- assert.notEqual(result.status, 0);
1532
- assert.match(output(result), /timestamps\.review_done: required after validation for a large task graph/);
1533
-
1534
- const reviewPath = path.join(reviewDir, 'spec.json');
1535
- const reviewState = JSON.parse(fs.readFileSync(reviewPath, 'utf8'));
1536
- reviewState.timestamps.review_done = '2026-08-11T00:05:00+07:00';
1537
- write(reviewPath, JSON.stringify(reviewState, null, 2));
1538
- result = run(VALIDATOR, [reviewDir], ROOT);
1539
- assert.equal(result.status, 0, output(result));
1540
- } finally {
1541
- fs.rmSync(root, { recursive: true, force: true });
1542
- }
1543
- });
1544
-
1545
- test('spec-ready excludes execution receipts and refuses Strict as no longer supported', () => {
1546
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-spec-ready-'));
1547
- try {
1548
- const standardDir = createSpec(root, {
1549
- name: 'pending-standard',
1550
- task: task({ related: '`src/pending-only.js` | Read' }),
1551
- spec: {
1552
- workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: {} }),
1553
- ready_for_implementation: true,
1554
- approvals: {
1555
- requirements: { generated: true, agent_validated: true, user_approved: true },
1556
- design: { generated: true, agent_validated: true, user_approved: true },
1557
- tasks: { generated: true, agent_validated: true, user_approved: true },
1558
- },
1559
- validation: { status: 'completed' },
1560
- updated_at: '2026-08-11T00:06:00+07:00',
1561
- timestamps: lifecycleTimestamps(),
1562
- },
1563
- });
1564
- completeSemanticReview(standardDir);
1565
- const standardResult = run(VALIDATOR, [standardDir], ROOT);
1566
- assert.equal(standardResult.status, 0, output(standardResult));
1567
-
1568
- const criticalDir = createSpec(root, {
1569
- name: 'pending-critical',
1570
- task: task({ related: '`src/pending-only.js` | Read' }),
1571
- design: criticalNoObligationDesign(),
1572
- spec: {
1573
- workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' }),
1574
- ready_for_implementation: true,
1575
- approvals: {
1576
- requirements: { generated: true, agent_validated: true, user_approved: true },
1577
- design: { generated: true, agent_validated: true, user_approved: true },
1578
- tasks: { generated: true, agent_validated: true, user_approved: true },
1579
- },
1580
- validation: { status: 'completed' },
1581
- updated_at: '2026-08-11T00:06:00+07:00',
1582
- timestamps: lifecycleTimestamps({ research: true, review: true }),
1583
- },
1584
- });
1585
- completeSemanticReview(criticalDir);
1586
- const criticalResult = run(VALIDATOR, [criticalDir], ROOT);
1587
- assert.notEqual(criticalResult.status, 0);
1588
- assert.match(output(criticalResult), /Strict assurance is no longer supported/);
1589
- } finally {
1590
- fs.rmSync(root, { recursive: true, force: true });
1591
- }
1592
- });
1593
-
1594
- test('Specs v2 Compact validates without task registry or premature receipt and fails closed on policy shape errors', () => {
1595
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-bounded-'));
1596
- try {
1597
- const specDir = createBoundedSpec(root);
1598
- const valid = run(VALIDATOR, [specDir], ROOT);
1599
- assert.equal(valid.status, 0, output(valid));
1600
- const persisted = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
1601
- assert.equal(persisted.workflow_policy.artifact_profile, 'bounded');
1602
- assert.equal(Object.hasOwn(persisted, 'task_files'), false);
1603
- assert.equal(Object.hasOwn(persisted, 'task_registry'), false);
1604
-
1605
- write(path.join(specDir, 'feature-receipt.md'), '# Feature Verification Receipt\n\nVerification: PENDING\n\nStatus: in_progress\n\nBlocker: awaiting execution proof\n');
1606
- const prematureReceipt = run(VALIDATOR, [specDir], ROOT);
1607
- assert.notEqual(prematureReceipt.status, 0);
1608
- assert.match(output(prematureReceipt), /premature execution receipt/);
1609
- fs.unlinkSync(path.join(specDir, 'feature-receipt.md'));
1610
- const malformed = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
1611
- malformed.workflow_policy.extra = true;
1612
- fs.writeFileSync(path.join(specDir, 'spec.json'), JSON.stringify(malformed, null, 2));
1613
- const malformedResult = run(VALIDATOR, [specDir], ROOT);
1614
- assert.notEqual(malformedResult.status, 0);
1615
- assert.match(output(malformedResult), /workflow_policy fields must be exactly/);
1616
- } finally {
1617
- fs.rmSync(root, { recursive: true, force: true });
1618
- }
1619
- });
1620
-
1621
- test('Specs v2 scaffold keeps planning artifacts independent from assurance and task topology', () => {
1622
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-lanes-'));
1623
- try {
1624
- const scaffoldSource = fs.readFileSync(SCAFFOLD, 'utf8');
1625
- assert.match(scaffoldSource, /--lane Standard\|Critical/);
1626
- assert.doesNotMatch(scaffoldSource, /\[--lane Direct\|/);
1627
- const direct = run(SCAFFOLD, ['direct-scaffold', '--lane', 'Direct'], root);
1628
- assert.equal(direct.status, 2, output(direct));
1629
- assert.match(output(direct), /Direct lane does not create a spec/);
1630
- assert.doesNotMatch(output(direct), /no trusted runtime issuer/i);
1631
-
1632
- const standard = run(SCAFFOLD, ['bounded-standard', '--lane', 'Standard'], root);
1633
- assert.equal(standard.status, 0, output(standard));
1634
- const standardDir = path.join(root, 'specs', 'bounded-standard');
1635
- const standardSpec = JSON.parse(fs.readFileSync(path.join(standardDir, 'spec.json'), 'utf8'));
1636
- assert.equal(POLICY.readWorkflowPolicySnapshot(standardSpec).lane, 'Standard');
1637
- assert.deepEqual(POLICY.readWorkflowPolicySnapshot(standardSpec).proof_obligations, ['needsExecutionProof']);
1638
- assert.equal(fs.existsSync(path.join(standardDir, 'tasks')), false);
1639
- assert.equal(fs.existsSync(path.join(standardDir, 'task_registry')), false);
1640
- assert.equal(standardSpec.validation.semantic_review.status, 'not-run');
1641
- assert.equal(fs.existsSync(path.join(standardDir, 'semantic-review.md')), false);
1642
- write(path.join(standardDir, 'requirements.md'), '# Requirements\n\n### Requirement 1: One\n\n- **R1.1** The scaffolded command shall return status value 1.\n- **R1.2** The proof shall reject an invalid status result.\n');
1643
- write(path.join(standardDir, 'design.md'), designFixture21());
1644
- const standardPromoted = run(SCAFFOLD, ['bounded-standard', '--sync-semantic-model'], root);
1645
- assert.equal(standardPromoted.status, 0, output(standardPromoted));
1646
- const standardValidation = run(VALIDATOR, [standardDir], ROOT);
1647
- assert.equal(standardValidation.status, 0, output(standardValidation));
1648
-
1649
- const elevated = run(SCAFFOLD, ['elevated-risk', '--risks', 'auth'], root);
1650
- assert.equal(elevated.status, 0, output(elevated));
1651
- const elevatedTaskless = JSON.parse(fs.readFileSync(path.join(root, 'specs/elevated-risk/spec.json'), 'utf8'));
1652
- assert.equal(elevatedTaskless.workflow_policy.planning_depth, 'Compact');
1653
- assert.equal(elevatedTaskless.workflow_policy.assurance_level, 'Elevated');
1654
- assert.equal(elevatedTaskless.workflow_policy.classified_minimum.assurance_level, 'Elevated');
1655
- assert.deepEqual(elevatedTaskless.coordination, { boundaries: [] });
1656
- assert.equal(fs.existsSync(path.join(root, 'specs/elevated-risk/research.md')), false);
1657
- assert.equal(elevatedTaskless.validation.semantic_review.status, 'not-run');
1658
- assert.equal(elevatedTaskless.authoring.tasks, 'absent');
1659
-
1660
- const strict = run(SCAFFOLD, ['strict-opt-in', '--risks', 'auth', '--assurance-level', 'Strict'], root);
1661
- assert.equal(strict.status, 0, output(strict));
1662
- const strictTaskless = JSON.parse(fs.readFileSync(path.join(root, 'specs/strict-opt-in/spec.json'), 'utf8'));
1663
- assert.equal(strictTaskless.workflow_policy.assurance_level, 'Strict');
1664
- assert.equal(strictTaskless.workflow_policy.classified_minimum.assurance_level, 'Elevated');
1665
- assert.equal(strictTaskless.authoring.tasks, 'absent');
1666
- } finally {
1667
- fs.rmSync(root, { recursive: true, force: true });
1668
- }
1669
- });
1670
-
1671
- test('Full phase scaffold fills canonical 2.1 artifacts and passes E2E validation', () => {
1672
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-full-phase-'));
1673
- try {
1674
- const phase = [{
1675
- id: 'delivery',
1676
- task_ids: ['R1-01', 'R1-02'],
1677
- entry_condition: 'requirements and design are approved',
1678
- exit_condition: 'task verification plan is complete',
1679
- owner_boundary: 'B-T',
1680
- }];
1681
- const result = run(SCAFFOLD, [
1682
- 'full-phase', '--planning-depth', 'Full', '--tasks', 'R1-01-artifact,R1-02-consumer',
1683
- '--boundaries', JSON.stringify([{
1684
- id: 'B-T', type: 'transition', design_ref: 'T1', owner: 'R1-01', consumers: ['R1-02'],
1685
- precondition: 'Input is ready for transition', postcondition: 'Output is durably complete',
1686
- failure: 'Failure state is recorded', recovery: 'Recovery resumes from recorded state',
1687
- }]), '--phases', JSON.stringify(phase),
1688
- ], root);
1689
- assert.equal(result.status, 0, output(result));
1690
- const specDir = path.join(root, 'specs/full-phase');
1691
- const spec = JSON.parse(fs.readFileSync(path.join(specDir, 'spec.json'), 'utf8'));
1692
- assert.deepEqual(spec.coordination.phases, phase);
1693
- assert.equal(spec.coordination.boundaries[0].type, 'transition');
1694
- completeScaffoldedSpec21(specDir);
1695
- const validation = run(VALIDATOR, [specDir], ROOT);
1696
- assert.equal(validation.status, 0, output(validation));
1697
- } finally {
1698
- fs.rmSync(root, { recursive: true, force: true });
1699
- }
1700
- });
1701
-
1702
- test('producer scaffold projects typed dependency deliverable into registry edges', () => {
1703
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-artifacts-'));
1704
- try {
1705
- const ordinary = run(SCAFFOLD, [
1706
- 'ordinary', '--tasks', 'R1-01-artifact,R1-02-consumer',
1707
- '--boundaries', JSON.stringify([{
1708
- id: 'B-D', type: 'dependency', producer: 'R1-01', consumer: 'R1-02',
1709
- deliverable: 'artifacts/output.json',
1710
- }]),
1711
- ], root);
1712
- assert.equal(ordinary.status, 0, output(ordinary));
1713
- const ordinarySpec = JSON.parse(fs.readFileSync(path.join(root, 'specs/ordinary/spec.json'), 'utf8'));
1714
- assert.equal(Object.hasOwn(ordinarySpec.task_registry['tasks/task-R1-01-artifact.md'], 'artifacts'), false);
1715
- assert.deepEqual(ordinarySpec.task_registry['tasks/task-R1-02-consumer.md'].dependencies, [
1716
- 'tasks/task-R1-01-artifact.md',
1717
- ]);
1718
- assert.equal(ordinarySpec.coordination.boundaries[0].deliverable, 'artifacts/output.json');
1719
- } finally {
1720
- fs.rmSync(root, { recursive: true, force: true });
1721
- }
1722
- });
1723
-
1724
- test('validator rejects missing explicit requirement and sub-criterion mappings', () => {
1725
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-mapping-'));
1726
- try {
1727
- const specDir = createSpec(root, { task: task({ mapping: '' }) });
1728
- const result = run(VALIDATOR, [specDir], ROOT);
1729
- assert.notEqual(result.status, 0);
1730
- assert.match(output(result), /R1: not covered|R1\.1: acceptance criterion not covered/);
1731
-
1732
- const incidental = createSpec(root, {
1733
- name: 'incidental',
1734
- task: task({ mapping: '', file: 'task-R1-01-one.md' }).replace('Update source', 'Update R1 source'),
1735
- });
1736
- const incidentalResult = run(VALIDATOR, [incidental], ROOT);
1737
- assert.notEqual(incidentalResult.status, 0);
1738
- assert.match(output(incidentalResult), /R1: not covered/);
1739
-
1740
- const partial = createSpec(root, {
1741
- name: 'partial',
1742
- requirements: '# Requirements\n\n### Requirement 1: One\n\n- **R1.1** First.\n- **R1.2** Second.\n',
1743
- });
1744
- const partialResult = run(VALIDATOR, [partial], ROOT);
1745
- assert.notEqual(partialResult.status, 0);
1746
- assert.match(output(partialResult), /R1\.2: acceptance criterion not covered/);
1747
-
1748
- const standardUnknown = createSpec(root, {
1749
- name: 'standard-unknown-mapping',
1750
- task: task({ mapping: '1.1, 9.1' }),
1751
- spec: { workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: {} }) },
1752
- });
1753
- const standardUnknownResult = run(VALIDATOR, [standardUnknown], ROOT);
1754
- assert.notEqual(standardUnknownResult.status, 0);
1755
- assert.match(output(standardUnknownResult), /references unknown requirement R9/);
1756
- } finally {
1757
- fs.rmSync(root, { recursive: true, force: true });
1758
- }
1759
- });
1760
-
1761
- test('v1 adapter rejects unknown, missing, and divergent contract copies', () => {
1762
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-contract-'));
1763
- const design = `${designFixture()}<!-- contract:PAYLOAD -->\n\`\`\`json\n{ "id": 1 }\n\`\`\`\n`;
1764
- function contractFixture(name, { designText = design, extra = '' } = {}) {
1765
- return createSpec(root, {
1766
- name,
1767
- design: designText,
1768
- task: legacyTask({ extra }),
1769
- spec: { workflow_policy: legacyWorkflowPolicy() },
1770
- });
1771
- }
1772
- try {
1773
- const unknown = contractFixture('unknown', { extra: 'Contracts: MISSING\n' });
1774
- let result = run(VALIDATOR, [unknown], ROOT);
1775
- assert.notEqual(result.status, 0);
1776
- assert.match(output(result), /unknown contract "MISSING"/);
1777
-
1778
- const noCanonical = contractFixture('no-canonical', {
1779
- designText: designFixture(),
1780
- extra: 'Contracts: PAYLOAD\n',
1781
- });
1782
- result = run(VALIDATOR, [noCanonical], ROOT);
1783
- assert.notEqual(result.status, 0);
1784
- assert.match(output(result), /defines no canonical contract blocks/);
1785
-
1786
- const missing = contractFixture('missing', { extra: 'Contracts: PAYLOAD\n' });
1787
- result = run(VALIDATOR, [missing], ROOT);
1788
- assert.notEqual(result.status, 0);
1789
- assert.match(output(result), /contract "PAYLOAD" is missing/);
1790
-
1791
- const divergent = contractFixture('divergent', {
1792
- extra: 'Contracts: PAYLOAD\n\n```json\n{ "id": 2 }\n```\n',
1793
- });
1794
- result = run(VALIDATOR, [divergent], ROOT);
1795
- assert.notEqual(result.status, 0);
1796
- assert.match(output(result), /contract "PAYLOAD" body diverges/);
1797
-
1798
- const duplicateDesign = contractFixture('duplicate-design-contract', {
1799
- designText: `${design}\n<!-- contract:PAYLOAD -->\n\`\`\`json\n{ "id": 1 }\n\`\`\`\n`,
1800
- });
1801
- result = run(VALIDATOR, [duplicateDesign], ROOT);
1802
- assert.notEqual(result.status, 0);
1803
- assert.match(output(result), /duplicate canonical contract definition "PAYLOAD"/);
1804
-
1805
- const repeatedDeclaration = contractFixture('repeated-task-contract-line', {
1806
- extra: 'Contracts: PAYLOAD\nContracts: PAYLOAD\n\n```json\n{ "id": 1 }\n```\n',
1807
- });
1808
- result = run(VALIDATOR, [repeatedDeclaration], ROOT);
1809
- assert.notEqual(result.status, 0);
1810
- assert.match(output(result), /Contracts declaration must appear at most once/);
1811
-
1812
- const duplicateName = contractFixture('duplicate-task-contract-name', {
1813
- extra: 'Contracts: PAYLOAD, PAYLOAD\n\n```json\n{ "id": 1 }\n```\n',
1814
- });
1815
- result = run(VALIDATOR, [duplicateName], ROOT);
1816
- assert.notEqual(result.status, 0);
1817
- assert.match(output(result), /Contracts declaration contains duplicate name "PAYLOAD"/);
1818
-
1819
- const orphanTagged = contractFixture('orphan-tagged-contract', {
1820
- extra: '<!-- contract:PAYLOAD -->\n```json\n{ "id": 1 }\n```\n',
1821
- });
1822
- result = run(VALIDATOR, [orphanTagged], ROOT);
1823
- assert.notEqual(result.status, 0);
1824
- assert.match(output(result), /must declare it on one Contracts: line/);
1825
- } finally {
1826
- fs.rmSync(root, { recursive: true, force: true });
1827
- }
1828
- });
1829
-
1830
- test('validator rejects invalid ready approval and validation transitions', () => {
1831
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-state-'));
1832
- try {
1833
- const specDir = createSpec(root, {
1834
- spec: {
1835
- ready_for_implementation: true,
1836
- approvals: { requirements: { generated: false, agent_validated: true, user_approved: true }, design: { generated: true, agent_validated: false, user_approved: true }, tasks: { generated: true, agent_validated: true, user_approved: false } },
1837
- validation: { status: 'not-run' },
1838
- },
1839
- });
1840
- const result = run(VALIDATOR, [specDir], ROOT);
1841
- assert.notEqual(result.status, 0);
1842
- assert.match(output(result), /requires generated and agent_validated requirements evidence/);
1843
- assert.match(output(result), /requires generated and agent_validated design evidence/);
1844
- assert.match(output(result), /validation evidence is incomplete/);
1845
-
1846
- // Legacy approved field must fail closed with migration guidance
1847
- const legacyDir = createSpec(root, {
1848
- name: 'legacy',
1849
- spec: {
1850
- schema_version: undefined,
1851
- approvals: { requirements: { generated: true, approved: true }, design: { generated: true, agent_validated: true, user_approved: true }, tasks: { generated: true, agent_validated: true, user_approved: true } },
1852
- },
1853
- });
1854
- // Remove schema_version to simulate legacy
1855
- const legacySpecPath = path.join(legacyDir, 'spec.json');
1856
- const legacySpec = JSON.parse(fs.readFileSync(legacySpecPath, 'utf8'));
1857
- delete legacySpec.schema_version;
1858
- fs.writeFileSync(legacySpecPath, JSON.stringify(legacySpec, null, 2));
1859
- const legacyResult = run(VALIDATOR, [legacyDir], ROOT);
1860
- assert.notEqual(legacyResult.status, 0);
1861
- assert.match(output(legacyResult), /Legacy approval field "approved"/);
1862
-
1863
- // Unsupported schema_version must fail closed
1864
- const unsupportedDir = createSpec(root, {
1865
- name: 'unsupported',
1866
- spec: { schema_version: '9.9' },
1867
- });
1868
- const unsupportedResult = run(VALIDATOR, [unsupportedDir], ROOT);
1869
- assert.notEqual(unsupportedResult.status, 0);
1870
- assert.match(output(unsupportedResult), /unsupported "9\.9"/);
1871
- } finally {
1872
- fs.rmSync(root, { recursive: true, force: true });
1873
- }
1874
- });
1875
-
1876
- test('validator rejects precise square-bracket placeholders without flagging links or checkboxes', () => {
1877
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-square-placeholders-'));
1878
- try {
1879
- for (const [name, placeholder] of [
1880
- ['specific-value', '[specific value]'],
1881
- ['target-users', '[target users]'],
1882
- ['component-name', '[Component Name]'],
1883
- ]) {
1884
- const specDir = createSpec(root, {
1885
- name,
1886
- task: task({ related: '`src/placeholder.js` | Read' }).replace(
1887
- 'Implement the mapped observable behavior.',
1888
- `Implement the mapped ${placeholder} behavior.`,
1889
- ),
1890
- });
1891
- const result = run(VALIDATOR, [specDir], ROOT);
1892
- assert.notEqual(result.status, 0);
1893
- assert.match(output(result), /unresolved square-bracket placeholder/);
1894
- }
1895
-
1896
- const safe = createSpec(root, {
1897
- name: 'safe-link-checkbox',
1898
- task: `${task({ related: '`src/safe.js` | Read' })}\n[Component Name](https://example.invalid)\n- [ ] Keep this real checkbox.\n`,
1899
- });
1900
- const safeResult = run(VALIDATOR, [safe], ROOT);
1901
- assert.equal(safeResult.status, 0, output(safeResult));
1902
- } finally {
1903
- fs.rmSync(root, { recursive: true, force: true });
1904
- }
1905
- });
1906
-
1907
- test('validator rejects malformed Related Files and dependency topology', () => {
1908
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-shape-'));
1909
- try {
1910
- for (const [name, related, expected] of [
1911
- ['empty', '', /Related Files section must not be empty/],
1912
- ['action', '`src/one.js` | Rename', /unsupported Related Files action/],
1913
- ['absolute', '`\/tmp\/one.js` | Modify', /must be relative/],
1914
- ['traversal', '`..\/one.js` | Modify', /must be relative/],
1915
- ]) {
1916
- const specDir = createSpec(root, {
1917
- name,
1918
- task: legacyTask({ relatedRows: related ? `| ${related} | Invalid fixture row |` : '' }),
1919
- spec: { workflow_policy: legacyWorkflowPolicy() },
1920
- });
1921
- const result = run(VALIDATOR, [specDir], ROOT);
1922
- assert.notEqual(result.status, 0);
1923
- assert.match(output(result), expected);
1924
- }
1925
-
1926
- const cycle = createSpec(root, { name: 'cycle' });
1927
- const statePath = path.join(cycle, 'spec.json');
1928
- const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
1929
- state.coordination.task_triggers = ['distinct_ownership', 'real_dependency'];
1930
- state.task_files.push('tasks/task-R1-02-two.md');
1931
- state.task_registry['tasks/task-R1-01-one.md'].dependencies = ['tasks/task-R1-02-two.md'];
1932
- state.task_registry['tasks/task-R1-02-two.md'] = { id: 'R1-02', title: 'Two', status: 'pending', dependencies: ['tasks/task-R1-01-one.md'], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
1933
- write(path.join(cycle, 'tasks/task-R1-01-one.md'), task({
1934
- related: '`src/one.js` | Read',
1935
- dependencies: 'tasks/task-R1-02-two.md',
1936
- }));
1937
- write(path.join(cycle, 'tasks/task-R1-02-two.md'), task({
1938
- id: 'R1-02',
1939
- related: '`src/two.js` | Read',
1940
- dependencies: 'tasks/task-R1-01-one.md',
1941
- }));
1942
- write(statePath, JSON.stringify(state, null, 2));
1943
- const cycleResult = run(VALIDATOR, [cycle], ROOT);
1944
- assert.notEqual(cycleResult.status, 0);
1945
- assert.match(output(cycleResult), /dependency cycle detected/);
1946
-
1947
- const ordering = createSpec(root, {
1948
- name: 'ordering',
1949
- task: task({ related: '`src/generated.js` | Create' }),
1950
- });
1951
- const orderingStatePath = path.join(ordering, 'spec.json');
1952
- const orderingState = JSON.parse(fs.readFileSync(orderingStatePath, 'utf8'));
1953
- orderingState.task_files.push('tasks/task-R1-02-two.md');
1954
- orderingState.task_registry['tasks/task-R1-02-two.md'] = { id: 'R1-02', title: 'Two', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
1955
- write(path.join(ordering, 'tasks/task-R1-02-two.md'), task({
1956
- id: 'R1-02',
1957
- related: '`src/generated.js` | Modify',
1958
- relatedRows: '| `src/generated.js` | Modify | Consume generated implementation |',
1959
- }));
1960
- write(orderingStatePath, JSON.stringify(orderingState, null, 2));
1961
- const orderingResult = run(VALIDATOR, [ordering], ROOT);
1962
- assert.notEqual(orderingResult.status, 0);
1963
- assert.match(output(orderingResult), /must depend on creator/);
1964
- } finally {
1965
- fs.rmSync(root, { recursive: true, force: true });
1966
- }
1967
- });
1968
-
1969
- test('scaffold validates one dependency map before writes and persists canonical registry edges', () => {
1970
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-dependencies-'));
1971
- const taskList = 'R1-01-owner,R1-02-consumer';
1972
- const dependencyMap = JSON.stringify([{
1973
- id: 'B-D', type: 'dependency', producer: 'R1-01', consumer: 'R1-02',
1974
- deliverable: 'artifacts/owner-output.json',
1975
- }]);
1976
- try {
1977
- const standard = run(SCAFFOLD, [
1978
- 'dependency-positive', '--lane', 'Standard', '--tasks', taskList,
1979
- '--boundaries', dependencyMap,
1980
- ], root);
1981
- assert.equal(standard.status, 0, output(standard));
1982
- const standardDir = path.join(root, 'specs/dependency-positive');
1983
- const standardSpec = JSON.parse(fs.readFileSync(path.join(standardDir, 'spec.json'), 'utf8'));
1984
- assert.deepEqual(
1985
- standardSpec.task_registry['tasks/task-R1-02-consumer.md'].dependencies,
1986
- ['tasks/task-R1-01-owner.md'],
1987
- );
1988
- assert.deepEqual(standardSpec.coordination.boundaries, JSON.parse(dependencyMap));
1989
- const standardTask = fs.readFileSync(
1990
- path.join(standardDir, 'tasks/task-R1-02-consumer.md'),
1991
- 'utf8',
1992
- );
1993
- assert.doesNotMatch(standardTask, /^## Execution Closure/m);
1994
- assert.doesNotMatch(
1995
- fs.readFileSync(path.join(standardDir, 'design.md'), 'utf8'),
1996
- /^## Execution Closure/m,
1997
- );
1998
-
1999
- const invalid = run(SCAFFOLD, [
2000
- 'dependency-invalid', '--tasks', taskList, '--boundaries', JSON.stringify([{
2001
- id: 'B-D', type: 'dependency', producer: 'R1-01', consumer: 'R1-02',
2002
- }]),
2003
- ], root);
2004
- assert.equal(invalid.status, 2, output(invalid));
2005
- assert.match(output(invalid), /fields must be exactly.*deliverable|dependency fields must be exactly/);
2006
- assert.equal(fs.existsSync(path.join(root, 'specs/dependency-invalid')), false);
2007
-
2008
- } finally {
2009
- fs.rmSync(root, { recursive: true, force: true });
2010
- }
2011
- });
2012
-
2013
- test('task-level (P) requires dependency-free and non-overlapping task ownership', () => {
2014
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-parallel-'));
2015
- const ownerPath = 'tasks/task-R1-01-one.md';
2016
- const siblingPath = 'tasks/task-R1-02-two.md';
2017
-
2018
- function parallelFixture(name, { dependency = false, overlap = false } = {}) {
2019
- const ownerTask = task({
2020
- related: overlap ? '`src/shared.js` | Read' : '`src/parallel-a.js` | Read',
2021
- relatedRows: overlap
2022
- ? '| `src/shared.js` | Read | Inspect existing |\n| `src/shared.js` | Modify | Update owned behavior |'
2023
- : null,
2024
- parallel: true,
2025
- });
2026
- const specDir = createSpec(root, { name, task: ownerTask });
2027
- const statePath = path.join(specDir, 'spec.json');
2028
- const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
2029
- state.coordination.task_triggers = dependency
2030
- ? ['parallel_coordination', 'real_dependency', 'separate_proof']
2031
- : ['parallel_coordination', 'separate_proof'];
2032
- state.task_files.push(siblingPath);
2033
- state.task_registry[siblingPath] = {
2034
- id: 'R1-02', title: 'Two', status: 'pending',
2035
- dependencies: dependency ? [ownerPath] : [],
2036
- blocker: null, started_at: null, completed_at: null, last_updated_at: null,
2037
- };
2038
- const sibling = task({
2039
- id: 'R1-02',
2040
- related: overlap ? '`src/shared.js` | Read' : '`src/parallel-b.js` | Read',
2041
- dependencies: dependency ? ownerPath : 'none',
2042
- parallel: dependency,
2043
- });
2044
- write(path.join(specDir, siblingPath), sibling);
2045
- write(statePath, JSON.stringify(state, null, 2));
2046
- return specDir;
2047
- }
2048
-
2049
- try {
2050
- const valid = parallelFixture('parallel-valid');
2051
- assert.equal(run(VALIDATOR, [valid], ROOT).status, 0);
2052
-
2053
- const dependency = parallelFixture('parallel-dependency', { dependency: true });
2054
- const dependencyResult = run(VALIDATOR, [dependency], ROOT);
2055
- assert.notEqual(dependencyResult.status, 0);
2056
- assert.match(output(dependencyResult), /task-level \(P\) cannot declare sibling dependencies/);
2057
-
2058
- const overlap = parallelFixture('parallel-overlap', { overlap: true });
2059
- const overlapResult = run(VALIDATOR, [overlap], ROOT);
2060
- assert.notEqual(overlapResult.status, 0);
2061
- assert.match(output(overlapResult), /task-level \(P\) has Related Files contention on src\/shared\.js/);
2062
- } finally {
2063
- fs.rmSync(root, { recursive: true, force: true });
2064
- }
2065
- });
2066
-
2067
- test('validator rejects dependency drift and non-canonical readiness statuses', () => {
2068
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-readiness-'));
2069
- const ownerPath = 'tasks/task-R1-01-one.md';
2070
- const consumerPath = 'tasks/task-R1-02-two.md';
2071
- try {
2072
- const driftDir = createSpec(root, {
2073
- name: 'dependency-aligned',
2074
- task: task({ related: '`src/owner.js` | Read' }),
2075
- });
2076
- const driftStatePath = path.join(driftDir, 'spec.json');
2077
- const driftState = JSON.parse(fs.readFileSync(driftStatePath, 'utf8'));
2078
- driftState.coordination.task_triggers = ['real_dependency', 'separate_proof'];
2079
- driftState.task_files.push(consumerPath);
2080
- driftState.task_registry[consumerPath] = {
2081
- id: 'R1-02', title: 'Two', status: 'pending', dependencies: [ownerPath], blocker: null,
2082
- started_at: null, completed_at: null, last_updated_at: null,
2083
- };
2084
- write(
2085
- path.join(driftDir, consumerPath),
2086
- task({
2087
- id: 'R1-02',
2088
- related: '`src/consumer.js` | Read',
2089
- dependencies: ownerPath,
2090
- }),
2091
- );
2092
- write(driftStatePath, JSON.stringify(driftState, null, 2));
2093
- const aligned = run(VALIDATOR, [driftDir], ROOT);
2094
- assert.equal(aligned.status, 0, output(aligned));
2095
-
2096
- const consumerFile = path.join(driftDir, consumerPath);
2097
- write(
2098
- consumerFile,
2099
- fs.readFileSync(consumerFile, 'utf8').replace(
2100
- `- ${ownerPath}\n\n## Verification Plan`,
2101
- '- none\n\n## Verification Plan',
2102
- ),
2103
- );
2104
- const drifted = run(VALIDATOR, [driftDir], ROOT);
2105
- assert.notEqual(drifted.status, 0);
2106
- assert.match(output(drifted), /dependency drift between spec\.json task_registry/);
2107
-
2108
- const duplicateHeader = createSpec(root, {
2109
- name: 'duplicate-dependency-section',
2110
- task: `${task({ related: '`src/duplicate.js` | Read' })}\n## Dependencies\n\n- none\n`,
2111
- });
2112
- const duplicateHeaderResult = run(VALIDATOR, [duplicateHeader], ROOT);
2113
- assert.notEqual(duplicateHeaderResult.status, 0);
2114
- assert.match(output(duplicateHeaderResult), /machine-read section Dependencies.*must appear at most once/);
2115
-
2116
- const duplicateSections = createSpec(root, {
2117
- name: 'duplicate-machine-sections',
2118
- task: `${task({ related: '`src/duplicate-sections.js` | Read' })}\n` +
2119
- '## Verification Plan\n- **Command:** `npm test`\n\n' +
2120
- '## Acceptance\n- **R1.1:** conflicting duplicate mapping\n',
2121
- });
2122
- const duplicateSectionsResult = run(VALIDATOR, [duplicateSections], ROOT);
2123
- assert.notEqual(duplicateSectionsResult.status, 0);
2124
- assert.match(output(duplicateSectionsResult), /machine-read section Verification Plan.*must appear at most once/);
2125
- assert.match(output(duplicateSectionsResult), /machine-read section Acceptance.*must appear at most once/);
2126
-
2127
- const ordinary = createSpec(root, {
2128
- name: 'ordinary-in-progress',
2129
- task: task({ related: '`src/ordinary.js` | Read', status: 'in_progress' }),
2130
- spec: {
2131
- task_registry: {
2132
- [ownerPath]: {
2133
- id: 'R1-01', title: 'One', status: 'in_progress', dependencies: [], blocker: null,
2134
- started_at: '2026-08-12T00:00:00+07:00', completed_at: null,
2135
- last_updated_at: '2026-08-12T00:01:00+07:00',
2136
- artifacts: ['test/one.test.js'],
2137
- },
2138
- },
2139
- },
2140
- });
2141
- const ordinaryResult = run(VALIDATOR, [ordinary], ROOT);
2142
- assert.equal(ordinaryResult.status, 0, output(ordinaryResult));
2143
-
2144
- const ordinaryStatePath = path.join(ordinary, 'spec.json');
2145
- const ordinaryState = JSON.parse(fs.readFileSync(ordinaryStatePath, 'utf8'));
2146
- const ordinaryTaskPath = path.join(ordinary, ownerPath);
2147
- write(
2148
- ordinaryTaskPath,
2149
- fs.readFileSync(ordinaryTaskPath, 'utf8')
2150
- .replace(/\n## Dependencies\n[\s\S]*?(?=\n## Verification Plan)/, ''),
2151
- );
2152
- ordinaryState.ready_for_implementation = true;
2153
- ordinaryState.validation = { status: 'completed' };
2154
- ordinaryState.timestamps = { validation_done: '2026-08-12T00:00:00+07:00' };
2155
- write(ordinaryStatePath, JSON.stringify(ordinaryState, null, 2));
2156
- const readyWithoutHeader = run(VALIDATOR, [ordinary], ROOT);
2157
- assert.notEqual(readyWithoutHeader.status, 0);
2158
- assert.match(output(readyWithoutHeader), /missing Dependencies/);
2159
-
2160
- const invalidCases = [
2161
- ['bad-ready-type', (state) => { state.ready_for_implementation = 'true'; }, /ready_for_implementation: must be a boolean/],
2162
- ['bad-spec-status', (state) => { state.status = 'ready'; }, /spec\.json\.status: must be one of/],
2163
- ['bad-task-status', (state) => { state.task_registry[ownerPath].status = 'ready'; }, /task_registry.*status: must be one of/],
2164
- ['bad-validation-shape', (state) => { state.validation = 'not-run'; }, /validation: must be an object/],
2165
- [
2166
- 'registry-id-path-drift',
2167
- (state) => { state.task_registry[ownerPath].id = 'R9-99'; },
2168
- /task_registry id must match path-derived id R1-01/,
2169
- ],
2170
- [
2171
- 'markdown-heading-id-drift',
2172
- (_state, specDir) => {
2173
- const taskFile = path.join(specDir, ownerPath);
2174
- write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('# Task R1-01:', '# Task R9-99:'));
2175
- },
2176
- /Markdown heading id R9-99 must match path-derived id R1-01/,
2177
- ],
2178
- [
2179
- 'registry-markdown-status-drift',
2180
- (state) => {
2181
- state.task_registry[ownerPath].status = 'done';
2182
- state.task_registry[ownerPath].started_at = '2026-08-12T00:00:00+07:00';
2183
- state.task_registry[ownerPath].completed_at = '2026-08-12T00:01:00+07:00';
2184
- state.task_registry[ownerPath].last_updated_at = '2026-08-12T00:01:00+07:00';
2185
- },
2186
- /status drift between spec\.json task_registry done and Markdown Status header pending/,
2187
- ],
2188
- [
2189
- 'done-missing-lifecycle-timestamps',
2190
- (state, specDir) => {
2191
- state.task_registry[ownerPath].status = 'done';
2192
- const taskFile = path.join(specDir, ownerPath);
2193
- write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** done'));
2194
- },
2195
- /done status requires started_at, completed_at, and last_updated_at timestamps/,
2196
- ],
2197
- [
2198
- 'blocked-missing-blocker',
2199
- (state, specDir) => {
2200
- state.task_registry[ownerPath].status = 'blocked';
2201
- state.task_registry[ownerPath].last_updated_at = '2026-08-12T00:01:00+07:00';
2202
- const taskFile = path.join(specDir, ownerPath);
2203
- write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** blocked'));
2204
- },
2205
- /blocked status requires a non-empty blocker/,
2206
- ],
2207
- [
2208
- 'in-progress-with-blocker',
2209
- (state, specDir) => {
2210
- state.task_registry[ownerPath].status = 'in_progress';
2211
- state.task_registry[ownerPath].blocker = 'waiting on owner';
2212
- state.task_registry[ownerPath].started_at = '2026-08-12T00:00:00+07:00';
2213
- state.task_registry[ownerPath].last_updated_at = '2026-08-12T00:01:00+07:00';
2214
- const taskFile = path.join(specDir, ownerPath);
2215
- write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** in_progress'));
2216
- },
2217
- /in_progress status requires blocker null/,
2218
- ],
2219
- [
2220
- 'in-progress-reversed-timestamps',
2221
- (state, specDir) => {
2222
- state.task_registry[ownerPath].status = 'in_progress';
2223
- state.task_registry[ownerPath].started_at = '2026-08-12T00:02:00+07:00';
2224
- state.task_registry[ownerPath].last_updated_at = '2026-08-12T00:01:00+07:00';
2225
- const taskFile = path.join(specDir, ownerPath);
2226
- write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** in_progress'));
2227
- },
2228
- /in_progress timestamps must satisfy started_at <= last_updated_at/,
2229
- ],
2230
- [
2231
- 'started-blocked-reversed-timestamps',
2232
- (state, specDir) => {
2233
- state.task_registry[ownerPath].status = 'blocked';
2234
- state.task_registry[ownerPath].blocker = 'waiting for the declared external authority';
2235
- state.task_registry[ownerPath].started_at = '2026-08-12T00:02:00+07:00';
2236
- state.task_registry[ownerPath].last_updated_at = '2026-08-12T00:01:00+07:00';
2237
- const taskFile = path.join(specDir, ownerPath);
2238
- write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** blocked'));
2239
- },
2240
- /started blocked timestamps must satisfy started_at <= last_updated_at/,
2241
- ],
2242
- [
2243
- 'bad-validation-status',
2244
- (state) => {
2245
- state.ready_for_implementation = true;
2246
- state.validation = { status: 'ready' };
2247
- state.timestamps = { validation_done: '2026-08-12T00:00:00+07:00' };
2248
- },
2249
- /validation\.status: must be one of|requires validation\.status completed/,
2250
- ],
2251
- [
2252
- 'blocked-ready',
2253
- (state, specDir) => {
2254
- state.ready_for_implementation = true;
2255
- state.status = 'blocked';
2256
- state.blocker = 'owner unavailable';
2257
- state.validation = { status: 'completed' };
2258
- state.timestamps = { validation_done: '2026-08-12T00:00:00+07:00' };
2259
- state.task_registry[ownerPath].status = 'blocked';
2260
- const taskFile = path.join(specDir, ownerPath);
2261
- write(taskFile, fs.readFileSync(taskFile, 'utf8').replace('**Status:** pending', '**Status:** blocked'));
2262
- },
2263
- /cannot be blocked while ready_for_implementation is true/,
2264
- ],
2265
- ];
2266
- for (const [name, mutate, expected] of invalidCases) {
2267
- const specDir = createSpec(root, {
2268
- name,
2269
- task: task({ related: '`src/status.js` | Read' }),
2270
- });
2271
- const specPath = path.join(specDir, 'spec.json');
2272
- const state = JSON.parse(fs.readFileSync(specPath, 'utf8'));
2273
- mutate(state, specDir);
2274
- write(specPath, JSON.stringify(state, null, 2));
2275
- const result = run(VALIDATOR, [specDir], ROOT);
2276
- assert.notEqual(result.status, 0, `${name} unexpectedly passed`);
2277
- assert.match(output(result), expected);
2278
- }
2279
- } finally {
2280
- fs.rmSync(root, { recursive: true, force: true });
2281
- }
2282
- });
2283
-
2284
- test('v1 Critical implementation obligations validate ownership, references, closure, and proof', () => {
2285
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-obligations-'));
2286
- const ownerPath = 'tasks/task-R1-01-one.md';
2287
- const consumerPath = 'tasks/task-R1-02-two.md';
2288
- const criticalPolicy = legacyWorkflowPolicy('Critical');
2289
-
2290
- function makeFixture(name, mutate = () => {}, policy = criticalPolicy) {
2291
- const obligation = {
2292
- id: 'canonical-transition',
2293
- requirements: ['R1.1'],
2294
- canonical_state: 'Persisted transition state and allowed values',
2295
- authority: 'Owner module is the sole transition authority',
2296
- recovery: 'Retry reconciles the persisted state before continuing',
2297
- owner_task: ownerPath,
2298
- consumer_tasks: [consumerPath],
2299
- verification: 'node --test focused.test.js',
2300
- evidence: 'Exit 0 and the declared state transition is observed',
2301
- };
2302
- const specDir = createSpec(root, {
2303
- name,
2304
- task: task({ related: '`src/owner.js` | Read' }),
2305
- spec: { workflow_policy: policy },
2306
- });
2307
- const specPath = path.join(specDir, 'spec.json');
2308
- write(path.join(specDir, 'research.md'), '# Research\n\n## Evidence Summary\n\n- The auth-risk fixture requires independent grounding before implementation.\n');
2309
- const state = JSON.parse(fs.readFileSync(specPath, 'utf8'));
2310
- state.task_files.push(consumerPath);
2311
- state.task_registry[consumerPath] = {
2312
- id: 'R1-02', title: 'Two', status: 'pending', dependencies: [ownerPath], blocker: null,
2313
- started_at: null, completed_at: null, last_updated_at: null,
2314
- };
2315
- write(
2316
- path.join(specDir, consumerPath),
2317
- task({
2318
- id: 'R1-02',
2319
- related: '`src/consumer.js` | Read',
2320
- dependencies: ownerPath,
2321
- extra: executionClosure(obligation),
2322
- })
2323
- .replace('# Task R1-01: One', '# Task R1-02: Two'),
2324
- );
2325
- const ownerFile = path.join(specDir, ownerPath);
2326
- write(
2327
- ownerFile,
2328
- `${fs.readFileSync(ownerFile, 'utf8')}${executionClosure(obligation)}`,
2329
- );
2330
- const designFile = path.join(specDir, 'design.md');
2331
- write(
2332
- designFile,
2333
- `${fs.readFileSync(designFile, 'utf8')}${executionClosure(obligation)}`,
2334
- );
2335
- state.implementation_obligations = [obligation];
2336
- mutate(state, specDir);
2337
- write(specPath, JSON.stringify(state, null, 2));
2338
- return specDir;
2339
- }
2340
-
2341
- try {
2342
- const omitted = createSpec(root, {
2343
- name: 'critical-omitted-obligation-decision',
2344
- spec: { workflow_policy: criticalPolicy },
2345
- });
2346
- write(path.join(omitted, 'research.md'), '# Research\n\n## Evidence Summary\n\n- The auth-risk fixture requires independent grounding before implementation.\n');
2347
- const omittedResult = run(VALIDATOR, [omitted], ROOT);
2348
- assert.notEqual(omittedResult.status, 0);
2349
- assert.match(output(omittedResult), /Critical scope must declare non-empty.*or one justified N\/A/);
2350
-
2351
- const valid = makeFixture('valid-obligation');
2352
- const validResult = run(VALIDATOR, [valid], ROOT);
2353
- assert.equal(validResult.status, 0, output(validResult));
2354
-
2355
- const tableClosure = makeFixture('valid-table-closure', (state, specDir) => {
2356
- const designFile = path.join(specDir, 'design.md');
2357
- const designPrefix = fs.readFileSync(designFile, 'utf8').split('\n## Execution Closure\n')[0];
2358
- write(designFile, `${designPrefix}${executionClosureTable(state.implementation_obligations[0])}`);
2359
- });
2360
- const tableClosureResult = run(VALIDATOR, [tableClosure], ROOT);
2361
- assert.equal(tableClosureResult.status, 0, output(tableClosureResult));
2362
-
2363
- const swapped = makeFixture('marker-local-swapped-obligations', (state, specDir) => {
2364
- const first = state.implementation_obligations[0];
2365
- const second = {
2366
- ...first,
2367
- id: 'secondary-transition',
2368
- canonical_state: 'Secondary persisted state and terminal values',
2369
- authority: 'Secondary module is the sole transition authority',
2370
- recovery: 'Restart rolls the secondary state back before retry',
2371
- verification: 'node --test secondary-focused.test.js',
2372
- evidence: 'Exit 0 and the secondary persisted transition is observed',
2373
- };
2374
- state.implementation_obligations.push(second);
2375
- for (const taskFile of [ownerPath, consumerPath]) {
2376
- const filePath = path.join(specDir, taskFile);
2377
- write(filePath, `${fs.readFileSync(filePath, 'utf8')}\n${obligationClosureBlock(second)}`);
2378
- }
2379
- const designFile = path.join(specDir, 'design.md');
2380
- const designPrefix = fs.readFileSync(designFile, 'utf8').split('\n## Execution Closure\n')[0];
2381
- const firstWithSecondValues = { ...second, id: first.id };
2382
- const secondWithFirstValues = { ...first, id: second.id };
2383
- write(
2384
- designFile,
2385
- `${designPrefix}\n## Execution Closure\n\n` +
2386
- `${obligationClosureBlock(firstWithSecondValues)}\n${obligationClosureBlock(secondWithFirstValues)}`,
2387
- );
2388
- });
2389
- const swappedResult = run(VALIDATOR, [swapped], ROOT);
2390
- assert.notEqual(swappedResult.status, 0);
2391
- assert.match(output(swappedResult), /marker-local field/);
2392
-
2393
- const cases = [
2394
- [
2395
- 'standard-only-rejected',
2396
- () => {},
2397
- legacyWorkflowPolicy(),
2398
- /allowed only for Critical workflow policy/,
2399
- ],
2400
- [
2401
- 'owner-must-be-one-task',
2402
- (state) => { state.implementation_obligations[0].owner_task = [ownerPath]; },
2403
- criticalPolicy,
2404
- /owner_task: must name exactly one registered task/,
2405
- ],
2406
- [
2407
- 'unknown-requirement',
2408
- (state) => { state.implementation_obligations[0].requirements = ['R9.1']; },
2409
- criticalPolicy,
2410
- /unknown requirement reference R9\.1/,
2411
- ],
2412
- [
2413
- 'unknown-consumer',
2414
- (state) => { state.implementation_obligations[0].consumer_tasks = ['tasks/task-R1-99-missing.md']; },
2415
- criticalPolicy,
2416
- /unknown task reference tasks\/task-R1-99-missing\.md/,
2417
- ],
2418
- [
2419
- 'missing-dependency-closure',
2420
- (state, specDir) => {
2421
- state.task_registry[consumerPath].dependencies = [];
2422
- const taskFile = path.join(specDir, consumerPath);
2423
- write(
2424
- taskFile,
2425
- fs.readFileSync(taskFile, 'utf8').replace(
2426
- `- ${ownerPath}\n\n## Verification Plan`,
2427
- '- none\n\n## Verification Plan',
2428
- ),
2429
- );
2430
- },
2431
- criticalPolicy,
2432
- /must depend on owner tasks\/task-R1-01-one\.md/,
2433
- ],
2434
- [
2435
- 'missing-proof',
2436
- (state) => {
2437
- state.implementation_obligations[0].verification = 'TBD';
2438
- state.implementation_obligations[0].evidence = 'PENDING';
2439
- },
2440
- criticalPolicy,
2441
- /verification: must be an executable command|evidence: must name an observable result/,
2442
- ],
2443
- [
2444
- 'missing-state-authority-recovery',
2445
- (state) => {
2446
- delete state.implementation_obligations[0].canonical_state;
2447
- state.implementation_obligations[0].authority = 'authority';
2448
- state.implementation_obligations[0].recovery = 'recovery';
2449
- },
2450
- criticalPolicy,
2451
- /canonical_state: must be concrete|authority: must be concrete|recovery: must be concrete/,
2452
- ],
2453
- [
2454
- 'missing-design-correspondence',
2455
- (_state, specDir) => {
2456
- const designFile = path.join(specDir, 'design.md');
2457
- write(
2458
- designFile,
2459
- fs.readFileSync(designFile, 'utf8').replace(
2460
- '\n<!-- implementation-obligation:canonical-transition -->\n',
2461
- '\n',
2462
- ),
2463
- );
2464
- },
2465
- criticalPolicy,
2466
- /design\.md: missing implementation obligation marker canonical-transition/,
2467
- ],
2468
- [
2469
- 'missing-owner-correspondence',
2470
- (_state, specDir) => {
2471
- const ownerFile = path.join(specDir, ownerPath);
2472
- write(
2473
- ownerFile,
2474
- fs.readFileSync(ownerFile, 'utf8').replace(
2475
- '\n<!-- implementation-obligation:canonical-transition -->\n',
2476
- '\n',
2477
- ),
2478
- );
2479
- },
2480
- criticalPolicy,
2481
- /task-R1-01-one\.md: missing implementation obligation marker canonical-transition/,
2482
- ],
2483
- [
2484
- 'marker-outside-execution-closure',
2485
- (_state, specDir) => {
2486
- const designFile = path.join(specDir, 'design.md');
2487
- const body = fs.readFileSync(designFile, 'utf8')
2488
- .replace('\n<!-- implementation-obligation:canonical-transition -->\n', '\n');
2489
- write(designFile, `${body}\n## Outside Closure\n\n<!-- implementation-obligation:canonical-transition -->\n`);
2490
- },
2491
- criticalPolicy,
2492
- /implementation obligation marker canonical-transition must be inside Execution Closure/,
2493
- ],
2494
- [
2495
- 'duplicate-closure-marker',
2496
- (_state, specDir) => {
2497
- const designFile = path.join(specDir, 'design.md');
2498
- write(
2499
- designFile,
2500
- fs.readFileSync(designFile, 'utf8').replace(
2501
- '<!-- implementation-obligation:canonical-transition -->',
2502
- '<!-- implementation-obligation:canonical-transition -->\n<!-- implementation-obligation:canonical-transition -->',
2503
- ),
2504
- );
2505
- },
2506
- criticalPolicy,
2507
- /duplicate implementation obligation marker canonical-transition/,
2508
- ],
2509
- [
2510
- 'duplicate-execution-closure',
2511
- (_state, specDir) => {
2512
- const designFile = path.join(specDir, 'design.md');
2513
- write(designFile, `${fs.readFileSync(designFile, 'utf8')}\n## Execution Closure\n\nNo duplicate allowed.\n`);
2514
- },
2515
- criticalPolicy,
2516
- /implementation obligation markers require exactly one Execution Closure section/,
2517
- ],
2518
- [
2519
- 'closure-json-drift',
2520
- (_state, specDir) => {
2521
- const ownerFile = path.join(specDir, ownerPath);
2522
- write(
2523
- ownerFile,
2524
- fs.readFileSync(ownerFile, 'utf8').replace(
2525
- 'Persisted transition state and allowed values',
2526
- 'A divergent local transition description',
2527
- ),
2528
- );
2529
- },
2530
- criticalPolicy,
2531
- /task-R1-01-one\.md: Execution Closure for canonical-transition must mirror spec\.json canonical_state/,
2532
- ],
2533
- [
2534
- 'closure-requirements-drift',
2535
- (_state, specDir) => {
2536
- const designFile = path.join(specDir, 'design.md');
2537
- write(
2538
- designFile,
2539
- fs.readFileSync(designFile, 'utf8').replace('- Requirements: R1.1', '- Requirements: R9.1'),
2540
- );
2541
- },
2542
- criticalPolicy,
2543
- /design\.md: Execution Closure for canonical-transition must mirror spec\.json requirements/,
2544
- ],
2545
- [
2546
- 'duplicate-obligation-id',
2547
- (state) => {
2548
- state.implementation_obligations.push({ ...state.implementation_obligations[0] });
2549
- },
2550
- criticalPolicy,
2551
- /duplicate obligation id canonical-transition/,
2552
- ],
2553
- [
2554
- 'whitespace-obligation-id',
2555
- (state) => { state.implementation_obligations[0].id = ' canonical-transition '; },
2556
- criticalPolicy,
2557
- /must be lowercase without surrounding whitespace/,
2558
- ],
2559
- ];
2560
- for (const [name, mutate, policy, expected] of cases) {
2561
- const specDir = makeFixture(name, mutate, policy);
2562
- const result = run(VALIDATOR, [specDir], ROOT);
2563
- assert.notEqual(result.status, 0, `${name} unexpectedly passed`);
2564
- assert.match(output(result), expected);
2565
- }
2566
- } finally {
2567
- fs.rmSync(root, { recursive: true, force: true });
2568
- }
2569
- });
2570
-
2571
- test('grounding rejects empty sections, unsafe paths, unsupported actions, and zero-match globs', () => {
2572
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-grounding-'));
2573
- try {
2574
- write(path.join(root, 'src/one.js'), 'source\n');
2575
- const cases = [
2576
- ['empty', '# Task\n\n## Related Files\n\n## Completion Criteria\n', /Related Files section must not be empty/],
2577
- ['action', task({ related: '`src/one.js` | Rename' }), /unsupported Related Files action/],
2578
- ['absolute', task({ related: '`\/tmp\/one.js` | Read' }), /must be relative/],
2579
- ['traversal', task({ related: '`..\/one.js` | Read' }), /must be relative/],
2580
- ['glob', task({ related: '`src\/missing-*.js` | Read' }), /glob matches no paths/],
2581
- ];
2582
- for (const [name, content, expected] of cases) {
2583
- const specDir = path.join(root, name);
2584
- write(path.join(specDir, 'tasks/task-R1-01-one.md'), content);
2585
- const result = run(GROUNDING, [specDir, '--root', root], ROOT);
2586
- assert.notEqual(result.status, 0);
2587
- assert.match(output(result), expected);
2588
- }
2589
-
2590
- const valid = path.join(root, 'valid');
2591
- write(path.join(valid, 'tasks/task-R1-01-one.md'), task({ related: '`src/*.js` | Read' }));
2592
- const result = run(GROUNDING, [valid, '--root', root], ROOT);
2593
- assert.equal(result.status, 0, output(result));
2594
- assert.match(result.stdout, /GROUNDED/);
2595
- } finally {
2596
- fs.rmSync(root, { recursive: true, force: true });
2597
- }
2598
- });
2599
-
2600
- test('validator enforces Read before Modify for existing implementation', () => {
2601
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-lifecycle-'));
2602
- try {
2603
- // Existing file Modify without preceding Read must fail
2604
- const missingRead = createSpec(root, {
2605
- name: 'missing-read',
2606
- task: task({
2607
- related: '`src/existing.js` | Modify',
2608
- relatedRows: '| `src/existing.js` | Modify | Update existing implementation |',
2609
- }),
2610
- });
2611
- const missingResult = run(VALIDATOR, [missingRead], ROOT);
2612
- assert.notEqual(missingResult.status, 0);
2613
- assert.match(output(missingResult), /has no preceding Read/);
2614
-
2615
- // Same path with Read in same task earlier row should pass
2616
- const withReadSameTask = createSpec(root, {
2617
- name: 'with-read-same',
2618
- task: task({ related: '`src/existing.js` | Modify' }),
2619
- });
2620
- const sameResult = run(VALIDATOR, [withReadSameTask], ROOT);
2621
- assert.equal(sameResult.status, 0, output(sameResult));
2622
-
2623
- // Modify should fail if Read is only in unrelated task without dependency
2624
- const depRead = createSpec(root, { name: 'dep-read' });
2625
- const depStatePath = path.join(depRead, 'spec.json');
2626
- const depState = JSON.parse(fs.readFileSync(depStatePath, 'utf8'));
2627
- depState.task_files.push('tasks/task-R1-02-two.md');
2628
- depState.task_registry['tasks/task-R1-02-two.md'] = { id: 'R1-02', title: 'Two', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
2629
- // Task 1 has Read, Task 2 modifies without depending
2630
- write(path.join(depRead, 'tasks/task-R1-01-one.md'), task({ related: '`src/existing.js` | Read' }));
2631
- write(path.join(depRead, 'tasks/task-R1-02-two.md'), task({
2632
- id: 'R1-02',
2633
- related: '`src/existing.js` | Modify',
2634
- relatedRows: '| `src/existing.js` | Modify | Update existing implementation |',
2635
- dependencies: 'tasks/task-R1-01-one.md',
2636
- }));
2637
- write(depStatePath, JSON.stringify(depState, null, 2));
2638
- const depMissingResult = run(VALIDATOR, [depRead], ROOT);
2639
- assert.notEqual(depMissingResult.status, 0);
2640
- assert.match(output(depMissingResult), /has no preceding Read/);
2641
-
2642
- // If Modify task depends on Read task, should pass
2643
- depState.task_registry['tasks/task-R1-02-two.md'].dependencies = ['tasks/task-R1-01-one.md'];
2644
- depState.coordination.task_triggers = ['real_dependency', 'separate_proof'];
2645
- write(depStatePath, JSON.stringify(depState, null, 2));
2646
- const depPass = run(VALIDATOR, [depRead], ROOT);
2647
- assert.equal(depPass.status, 0, output(depPass));
2648
- } finally {
2649
- fs.rmSync(root, { recursive: true, force: true });
2650
- }
2651
- });
2652
-
2653
- test('validator rejects reachability without concrete anchor and accepts with path', () => {
2654
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-reachability-'));
2655
- try {
2656
- const noAnchor = createSpec(root, {
2657
- name: 'no-anchor',
2658
- task: task({ related: '`src/one.js` | Modify' }).replace('Runtime reachability verification\n - Entrypoint/caller: src/one.js', 'Runtime reachability verification\n - Some vague phrase'),
2659
- });
2660
- // Add a Read to avoid lifecycle error, so we can isolate reachability check
2661
- const noAnchorContent = fs.readFileSync(path.join(noAnchor, 'tasks/task-R1-01-one.md'), 'utf8')
2662
- .replace('`src/one.js` | Modify', '`src/one.js` | Read\n| `src/one.js` | Modify');
2663
- // Keep lifecycle satisfied: Read before Modify in same task
2664
- write(path.join(noAnchor, 'tasks/task-R1-01-one.md'), noAnchorContent.replace('Some vague phrase', 'Runtime reachability verification'));
2665
- // Actually construct valid task with missing concrete anchor explicitly
2666
- write(path.join(noAnchor, 'tasks/task-R1-01-one.md'), `# Task R1-01: One\n\n## Context\n- X\n\n## Constraints\n- Y\n\n## Steps\n- [ ] Update\n - _Requirements: 1.1_\n\n## Requirements\n- 1.1 — One\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/one.js\` | Read | Inspect |\n| \`src/one.js\` | Modify | Update |\n\n## Completion Criteria\n- [ ] Works\n\n## Evidence\n- Runtime reachability verification\n - Note: verified somehow\n\n## Risk Assessment\n| Risk | Severity | Mitigation |\n|---|---|---|\n| None | Low | Tests |\n`);
2667
- const noAnchorResult = run(VALIDATOR, [noAnchor], ROOT);
2668
- assert.notEqual(noAnchorResult.status, 0);
2669
- assert.match(output(noAnchorResult), /Runtime reachability verification must reference a concrete file path/);
2670
- } finally {
2671
- fs.rmSync(root, { recursive: true, force: true });
2672
- }
2673
- });
2674
-
2675
- test('validator keeps contract blocks opt-in regardless of task count', () => {
2676
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-contract-closed-'));
2677
- try {
2678
- const specDir = createSpec(root, { name: 'five-no-contract' });
2679
- const statePath = path.join(specDir, 'spec.json');
2680
- const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
2681
- state.design_context = { validation_recommended: true };
2682
- for (let i = 2; i <= 5; i += 1) {
2683
- const taskFile = `tasks/task-R1-0${i}-extra.md`;
2684
- state.task_files.push(taskFile);
2685
- state.task_registry[taskFile] = { id: `R1-0${i}`, title: `Extra ${i}`, status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
2686
- write(path.join(specDir, taskFile), task({
2687
- id: `R1-0${i}`,
2688
- related: '`src/extra.js` | Modify',
2689
- mapping: '1.1',
2690
- }));
2691
- }
2692
- // Need to ensure each extra task has Read before Modify to avoid lifecycle error
2693
- for (let i = 2; i <= 5; i += 1) {
2694
- const p = path.join(specDir, `tasks/task-R1-0${i}-extra.md`);
2695
- const c = fs.readFileSync(p, 'utf8').replace('`src/extra.js` | Modify', '`src/extra.js` | Read\n| `src/extra.js` | Modify');
2696
- const updated = c.replace('Entrypoint/caller: src/one.js', 'Entrypoint/caller: src/extra.js');
2697
- write(p, updated);
2698
- }
2699
- // Also fix first task to have Read
2700
- const first = fs.readFileSync(path.join(specDir, 'tasks/task-R1-01-one.md'), 'utf8').replace('`src/one.js` | Modify', '`src/one.js` | Read\n| `src/one.js` | Modify');
2701
- write(path.join(specDir, 'tasks/task-R1-01-one.md'), first);
2702
- write(statePath, JSON.stringify(state, null, 2));
2703
- const result = run(VALIDATOR, [specDir], ROOT);
2704
- assert.equal(result.status, 0, output(result));
2705
- } finally {
2706
- fs.rmSync(root, { recursive: true, force: true });
2707
- }
2708
- });
2709
-
2710
- test('grounding treats Create on existing file as error', () => {
2711
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-grounding-create-existing-'));
2712
- try {
2713
- write(path.join(root, 'src/existing.js'), 'existing\n');
2714
- const specDir = path.join(root, 'spec-create-existing');
2715
- write(path.join(specDir, 'tasks/task-R1-01-one.md'), task({ related: '`src/existing.js` | Create' }));
2716
- const result = run(GROUNDING, [specDir, '--root', root], ROOT);
2717
- assert.notEqual(result.status, 0);
2718
- assert.match(output(result), /Create path already exists/);
2719
- } finally {
2720
- fs.rmSync(root, { recursive: true, force: true });
2721
- }
2722
- });
2723
-
2724
- test('validator scopes requirement mapping to structured section, incidental mention does not cover', () => {
2725
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-scope-'));
2726
- try {
2727
- // Task mentions R1 in Context but has no structured _Requirements mapping — should fail
2728
- const incidentalOnly = createSpec(root, {
2729
- name: 'incidental-scope',
2730
- task: `# Task R1-01: One\n\n## Context\nMention R1 and Requirement 1 incidental.\n\n## Constraints\n- Keep\n\n## Steps\n- [ ] Do work\n\n## Requirements\n- 1.1 — One\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/one.js\` | Read | Inspect |\n| \`src/one.js\` | Modify | Update |\n\n## Completion Criteria\n- [ ] Works\n\n## Evidence\n- Runtime reachability verification\n - Entrypoint/caller: src/one.js\n\n## Risk Assessment\n| Risk | Severity | Mitigation |\n|---|---|---|\n| None | Low | Tests |\n`,
2731
- });
2732
- const res = run(VALIDATOR, [incidentalOnly], ROOT);
2733
- assert.notEqual(res.status, 0);
2734
- assert.match(output(res), /R1: not covered/);
2735
- } finally {
2736
- fs.rmSync(root, { recursive: true, force: true });
2737
- }
2738
- });
2739
-
2740
- test('validator enforces Create before Read/Modify/Delete within same task and cross-task dependencies', () => {
2741
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-validator-create-order-'));
2742
- try {
2743
- // Same-task: Create after Read must fail (dead-code regression)
2744
- const createAfterRead = createSpec(root, {
2745
- name: 'create-after-read',
2746
- task: task({
2747
- related: '`src/new.js` | Read',
2748
- relatedRows: '| `src/new.js` | Read | Read before create |\n| `src/new.js` | Create | Create after read |',
2749
- }),
2750
- });
2751
- const carResult = run(VALIDATOR, [createAfterRead], ROOT);
2752
- assert.notEqual(carResult.status, 0);
2753
- assert.match(output(carResult), /Create must precede read for src\/new\.js/);
2754
-
2755
- // Same-task: Create after Modify must fail
2756
- const createAfterModify = createSpec(root, {
2757
- name: 'create-after-modify',
2758
- task: task({
2759
- related: '`src/new.js` | Modify',
2760
- relatedRows: '| `src/new.js` | Modify | Modify before create |\n| `src/new.js` | Create | Create after modify |',
2761
- }),
2762
- });
2763
- const camResult = run(VALIDATOR, [createAfterModify], ROOT);
2764
- assert.notEqual(camResult.status, 0);
2765
- assert.match(output(camResult), /Create must precede modify for src\/new\.js/);
2766
-
2767
- // Same-task: Create before Read/Modify must pass; repeated Reads must not be rejected
2768
- const createBefore = createSpec(root, {
2769
- name: 'create-before',
2770
- task: task({
2771
- related: '`src/new.js` | Create',
2772
- relatedRows: '| `src/new.js` | Create | Create first |\n| `src/new.js` | Read | Read after create |\n| `src/new.js` | Modify | Modify after create |\n| `src/new.js` | Read | Second read |',
2773
- }),
2774
- });
2775
- const cbResult = run(VALIDATOR, [createBefore], ROOT);
2776
- assert.equal(cbResult.status, 0, output(cbResult));
2777
-
2778
- // Valid same-task repeated reads without create must pass (not rejected unnecessarily)
2779
- const repeatedReads = createSpec(root, {
2780
- name: 'repeated-reads',
2781
- task: task({
2782
- related: '`src/existing.js` | Read',
2783
- relatedRows: '| `src/existing.js` | Read | First read |\n| `src/existing.js` | Read | Second read |\n| `src/existing.js` | Modify | Modify after reads |',
2784
- }),
2785
- });
2786
- const rrResult = run(VALIDATOR, [repeatedReads], ROOT);
2787
- assert.equal(rrResult.status, 0, output(rrResult));
2788
-
2789
- // Cross-task: Read without dependency must fail
2790
- const crossRead = createSpec(root, { name: 'cross-read-base' });
2791
- const crossReadStatePath = path.join(crossRead, 'spec.json');
2792
- const crossReadState = JSON.parse(fs.readFileSync(crossReadStatePath, 'utf8'));
2793
- // Task1 creates src/gen.js
2794
- write(path.join(crossRead, 'tasks/task-R1-01-one.md'), task({ related: '`src/gen.js` | Create' }));
2795
- crossReadState.task_files.push('tasks/task-R1-02-two.md');
2796
- crossReadState.task_registry['tasks/task-R1-02-two.md'] = { id: 'R1-02', title: 'Two', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
2797
- write(path.join(crossRead, 'tasks/task-R1-02-two.md'), task({
2798
- id: 'R1-02',
2799
- related: '`src/gen.js` | Read',
2800
- }));
2801
- write(crossReadStatePath, JSON.stringify(crossReadState, null, 2));
2802
- const crossReadFail = run(VALIDATOR, [crossRead], ROOT);
2803
- assert.notEqual(crossReadFail.status, 0);
2804
- assert.match(output(crossReadFail), /read of src\/gen\.js must depend on creator/);
2805
-
2806
- // Cross-task Read with dependency must pass
2807
- crossReadState.task_registry['tasks/task-R1-02-two.md'].dependencies = ['tasks/task-R1-01-one.md'];
2808
- crossReadState.coordination.task_triggers = ['real_dependency', 'separate_proof'];
2809
- const crossReadTaskPath = path.join(crossRead, 'tasks/task-R1-02-two.md');
2810
- write(crossReadTaskPath, task({
2811
- id: 'R1-02',
2812
- related: '`src/gen.js` | Read',
2813
- dependencies: 'tasks/task-R1-01-one.md',
2814
- }));
2815
- write(crossReadStatePath, JSON.stringify(crossReadState, null, 2));
2816
- const crossReadPass = run(VALIDATOR, [crossRead], ROOT);
2817
- assert.equal(crossReadPass.status, 0, output(crossReadPass));
2818
-
2819
- // Cross-task Modify without dependency must fail, with dep must pass
2820
- const crossMod = createSpec(root, { name: 'cross-mod-base' });
2821
- const crossModStatePath = path.join(crossMod, 'spec.json');
2822
- const crossModState = JSON.parse(fs.readFileSync(crossModStatePath, 'utf8'));
2823
- write(path.join(crossMod, 'tasks/task-R1-01-one.md'), task({ related: '`src/gen2.js` | Create' }));
2824
- crossModState.task_files.push('tasks/task-R1-02-two.md');
2825
- crossModState.task_registry['tasks/task-R1-02-two.md'] = { id: 'R1-02', title: 'Two', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null };
2826
- write(path.join(crossMod, 'tasks/task-R1-02-two.md'), task({
2827
- id: 'R1-02',
2828
- related: '`src/gen2.js` | Modify',
2829
- relatedRows: '| `src/gen2.js` | Modify | Modify without dependency |',
2830
- }));
2831
- write(crossModStatePath, JSON.stringify(crossModState, null, 2));
2832
- const crossModFail = run(VALIDATOR, [crossMod], ROOT);
2833
- assert.notEqual(crossModFail.status, 0);
2834
- assert.match(output(crossModFail), /modify of src\/gen2\.js must depend on creator/);
2835
- crossModState.task_registry['tasks/task-R1-02-two.md'].dependencies = ['tasks/task-R1-01-one.md'];
2836
- crossModState.coordination.task_triggers = ['real_dependency', 'separate_proof'];
2837
- const crossModTaskPath = path.join(crossMod, 'tasks/task-R1-02-two.md');
2838
- write(crossModTaskPath, task({
2839
- id: 'R1-02',
2840
- related: '`src/gen2.js` | Modify',
2841
- relatedRows: '| `src/gen2.js` | Modify | Modify generated implementation |',
2842
- dependencies: 'tasks/task-R1-01-one.md',
2843
- }));
2844
- write(crossModStatePath, JSON.stringify(crossModState, null, 2));
2845
- const crossModPass = run(VALIDATOR, [crossMod], ROOT);
2846
- assert.equal(crossModPass.status, 0, output(crossModPass));
2847
- } finally {
2848
- fs.rmSync(root, { recursive: true, force: true });
2849
- }
2850
- });
2851
-
2852
- test('grounding enforces Create ordering and cross-task dependency', () => {
2853
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-grounding-order-'));
2854
- try {
2855
- // Same-task Create after Read must fail in grounding
2856
- const specDir = path.join(root, 'spec-order');
2857
- write(path.join(specDir, 'tasks/task-R1-01-one.md'), `# Task\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/new.js\` | Read | Read before create |\n| \`src/new.js\` | Create | Create after read |\n`);
2858
- write(path.join(specDir, 'spec.json'), JSON.stringify({
2859
- schema_version: '2.0',
2860
- task_files: ['tasks/task-R1-01-one.md'],
2861
- task_registry: { 'tasks/task-R1-01-one.md': { id: 'R1-01', title: 'One', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null } }
2862
- }));
2863
- const r1 = run(GROUNDING, [specDir, '--root', root], ROOT);
2864
- assert.notEqual(r1.status, 0);
2865
- assert.match(`${r1.stdout}\n${r1.stderr}`, /Create must precede read/);
2866
-
2867
- // Cross-task Read without dependency must fail in grounding
2868
- const specDir2 = path.join(root, 'spec-cross');
2869
- write(path.join(specDir2, 'tasks/task-R1-01-one.md'), `# Task\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/gen.js\` | Create | Create |\n`);
2870
- write(path.join(specDir2, 'tasks/task-R1-02-two.md'), `# Task\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/gen.js\` | Read | Read without dep |\n`);
2871
- write(path.join(specDir2, 'spec.json'), JSON.stringify({
2872
- schema_version: '2.0',
2873
- task_files: ['tasks/task-R1-01-one.md', 'tasks/task-R1-02-two.md'],
2874
- task_registry: {
2875
- 'tasks/task-R1-01-one.md': { id: 'R1-01', title: 'One', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null },
2876
- 'tasks/task-R1-02-two.md': { id: 'R1-02', title: 'Two', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null }
2877
- }
2878
- }));
2879
- const r2 = run(GROUNDING, [specDir2, '--root', root], ROOT);
2880
- assert.notEqual(r2.status, 0);
2881
- assert.match(`${r2.stdout}\n${r2.stderr}`, /read of src\/gen\.js must depend on creator/);
2882
-
2883
- // Cross-task Read with dependency must pass (if file not found but created, dependency satisfies)
2884
- const spec2Json = JSON.parse(fs.readFileSync(path.join(specDir2, 'spec.json'), 'utf8'));
2885
- spec2Json.task_registry['tasks/task-R1-02-two.md'].dependencies = ['tasks/task-R1-01-one.md'];
2886
- fs.writeFileSync(path.join(specDir2, 'spec.json'), JSON.stringify(spec2Json, null, 2));
2887
- const r3 = run(GROUNDING, [specDir2, '--root', root], ROOT);
2888
- assert.equal(r3.status, 0, `${r3.stdout}\n${r3.stderr}`);
2889
-
2890
- // Repeated reads without create must not be rejected unnecessarily (grounding should pass if files exist or fail only for missing file, not ordering)
2891
- write(path.join(root, 'src/existing.js'), 'x\n');
2892
- const specDir3 = path.join(root, 'spec-repeated');
2893
- write(path.join(specDir3, 'tasks/task-R1-01-one.md'), `# Task\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/existing.js\` | Read | First |\n| \`src/existing.js\` | Read | Second |\n`);
2894
- const r4 = run(GROUNDING, [specDir3, '--root', root], ROOT);
2895
- assert.equal(r4.status, 0, `${r4.stdout}\n${r4.stderr}`);
2896
-
2897
- // Create on existing must remain error in grounding
2898
- const specDir4 = path.join(root, 'spec-create-existing2');
2899
- write(path.join(specDir4, 'tasks/task-R1-01-one.md'), `# Task\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/existing.js\` | Create | Create existing |\n`);
2900
- const r5 = run(GROUNDING, [specDir4, '--root', root], ROOT);
2901
- assert.notEqual(r5.status, 0);
2902
- assert.match(`${r5.stdout}\n${r5.stderr}`, /Create path already exists/);
2903
- } finally {
2904
- fs.rmSync(root, { recursive: true, force: true });
2905
- }
2906
- });
2907
-
2908
- test('grounding fails closed when cross-task producer exists but registry missing', () => {
2909
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-grounding-missing-registry-'));
2910
- try {
2911
- // Cross-task Create -> Read with missing spec.json must fail closed
2912
- const specDir = path.join(root, 'spec-missing-registry');
2913
- write(path.join(specDir, 'tasks/task-R1-01-one.md'), `# Task\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/gen.js\` | Create | Create |\n`);
2914
- write(path.join(specDir, 'tasks/task-R1-02-two.md'), `# Task\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/gen.js\` | Read | Read without registry |\n`);
2915
- // Intentionally no spec.json
2916
- const noRegistry = run(GROUNDING, [specDir, '--root', root], ROOT);
2917
- assert.notEqual(noRegistry.status, 0);
2918
- assert.match(`${noRegistry.stdout}\n${noRegistry.stderr}`, /task_registry is missing.*fail-closed|fail-closed/);
2919
-
2920
- // Missing registry but also empty registry object must fail closed
2921
- write(path.join(specDir, 'spec.json'), JSON.stringify({ task_files: ['tasks/task-R1-01-one.md', 'tasks/task-R1-02-two.md'], task_registry: {} }));
2922
- const emptyRegistry = run(GROUNDING, [specDir, '--root', root], ROOT);
2923
- assert.notEqual(emptyRegistry.status, 0);
2924
- assert.match(`${emptyRegistry.stdout}\n${emptyRegistry.stderr}`, /fail-closed|must depend on creator|task_registry/);
2925
-
2926
- // Valid single-task Create+Read with missing registry must still PASS (no cross-task)
2927
- const singleDir = path.join(root, 'spec-single-missing-registry');
2928
- write(path.join(singleDir, 'tasks/task-R1-01-one.md'), `# Task\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/single.js\` | Create | Create |\n| \`src/single.js\` | Read | Read same task |\n`);
2929
- // No spec.json for single-task
2930
- const singlePass = run(GROUNDING, [singleDir, '--root', root], ROOT);
2931
- assert.equal(singlePass.status, 0, `${singlePass.stdout}\n${singlePass.stderr}`);
2932
-
2933
- // Existing-file Read without producer and missing registry must PASS (if file exists)
2934
- write(path.join(root, 'src/existing2.js'), 'x\n');
2935
- const existingDir = path.join(root, 'spec-existing-missing-registry');
2936
- write(path.join(existingDir, 'tasks/task-R1-01-one.md'), `# Task\n\n## Related Files\n| Path | Action | Description |\n|---|---|---|\n| \`src/existing2.js\` | Read | Read existing |\n`);
2937
- // No spec.json
2938
- const existingPass = run(GROUNDING, [existingDir, '--root', root], ROOT);
2939
- assert.equal(existingPass.status, 0, `${existingPass.stdout}\n${existingPass.stderr}`);
2940
- } finally {
2941
- fs.rmSync(root, { recursive: true, force: true });
2942
- }
2943
- });
2944
-
2945
114
  test('benchmark gate fails Critical when both arms correctness is zero', () => {
2946
115
  const BENCHMARK = path.join(ROOT, 'scripts/benchmark-workflow.mjs');
2947
116
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-benchmark-critical-zero-'));
@@ -3108,84 +277,3 @@ test('benchmark validate handles empty and invalid evidence as not-ready/invalid
3108
277
  fs.rmSync(root, { recursive: true, force: true });
3109
278
  }
3110
279
  });
3111
-
3112
- test('D12: semantic-firewall test discovery finds R0-01\'s required basenames in the real checkout', () => {
3113
- const DISCOVERY = require(path.join(ROOT, 'scripts/semantic-firewall-test-discovery.cjs'));
3114
- const testsDir = path.join(ROOT, 'bin/__tests__');
3115
- const discovered = DISCOVERY.discoverSemanticFirewallTests(testsDir);
3116
- assert.ok(discovered.includes('change-firewall.semantic-firewall.test.js'), 'change-firewall.semantic-firewall.test.js must be discovered');
3117
- assert.ok(discovered.includes('release-preflight.semantic-firewall.test.js'), 'release-preflight.semantic-firewall.test.js must be discovered');
3118
- assert.deepEqual(discovered, [...discovered].sort(), 'discovery must return sorted basenames');
3119
-
3120
- const requiredOk = DISCOVERY.assertRequiredSemanticTests(testsDir, [
3121
- 'change-firewall.semantic-firewall.test.js',
3122
- 'release-preflight.semantic-firewall.test.js',
3123
- ]);
3124
- assert.equal(requiredOk.ok, true);
3125
- assert.deepEqual(requiredOk.missing, []);
3126
-
3127
- const requiredMissing = DISCOVERY.assertRequiredSemanticTests(testsDir, ['does-not-exist.semantic-firewall.test.js']);
3128
- assert.equal(requiredMissing.ok, false);
3129
- assert.deepEqual(requiredMissing.missing, ['does-not-exist.semantic-firewall.test.js']);
3130
- });
3131
-
3132
- test('D12: run-skill-self-tests.mjs sentinel exits nonzero immediately when a required semantic-firewall basename is missing', () => {
3133
- const RUNNER = path.join(ROOT, 'scripts/run-skill-self-tests.mjs');
3134
- const result = spawnSync(process.execPath, [
3135
- RUNNER, '--require-semantic-test', 'does-not-exist.semantic-firewall.test.js',
3136
- ], { cwd: ROOT, encoding: 'utf8', timeout: 15000 });
3137
- assert.notEqual(result.status, 0);
3138
- assert.match(`${result.stdout}${result.stderr}`, /required semantic-firewall test not discovered: does-not-exist\.semantic-firewall\.test\.js/);
3139
- });
3140
-
3141
- test('R0-01 owned change-firewall contract surface (D1/C15) is reachable: every named export and owned anchor target exists', () => {
3142
- const CHANGE_FIREWALL = require(path.join(ROOT, 'src/claude/scripts/change-firewall.cjs'));
3143
- const requiredExports = [
3144
- 'loadBaselineAuthority', 'assertBaselineUsable', 'bootstrapBaseline', 'advanceBaseline',
3145
- 'deriveProtectedChangedPaths', 'computeProtectedTreeDigest', 'computeProposalDigest',
3146
- 'computeCandidateDigest', 'assertHotspotChangeAllowed', 'createFreezeManifest',
3147
- 'verifyFreezeManifest', 'invalidateFreeze', 'NO_CHANGE_ATTESTATION_DIGEST',
3148
- 'loadFailureLedger', 'assertFailureLedgerUsable', 'openFailure', 'classifyFailure',
3149
- 'reclassifyFailure', 'resolveFailure', 'computeFailureLedgerDigest', 'deriveFailureState',
3150
- 'NO_FAILURES_ATTESTATION_DIGEST', 'createChangeFirewall', 'assertLegacyBridgeArtifact',
3151
- ];
3152
- for (const name of requiredExports) {
3153
- assert.ok(Object.prototype.hasOwnProperty.call(CHANGE_FIREWALL, name), `change-firewall.cjs must export ${name}`);
3154
- }
3155
-
3156
- const ownedAnchorTargets = [
3157
- 'src/claude/scripts/change-firewall.cjs',
3158
- 'src/claude/scripts/spec-authoring-digest.cjs',
3159
- 'benchmarks/change-proposal.schema.json',
3160
- 'benchmarks/change-firewall-freeze.schema.json',
3161
- 'benchmarks/release-baseline.schema.json',
3162
- 'benchmarks/migration-manifest.json',
3163
- 'benchmarks/bootstrap-attestation.schema.json',
3164
- 'benchmarks/benchmark-failure-ledger.schema.json',
3165
- 'scripts/release-preflight.mjs',
3166
- 'scripts/benchmark-workflow.mjs',
3167
- 'scripts/semantic-firewall-test-discovery.cjs',
3168
- 'scripts/run-skill-self-tests.mjs',
3169
- 'bin/__tests__/change-firewall.semantic-firewall.test.js',
3170
- 'bin/__tests__/release-preflight.semantic-firewall.test.js',
3171
- 'package.json',
3172
- '.gitignore',
3173
- ];
3174
- for (const relative of ownedAnchorTargets) {
3175
- assert.ok(fs.existsSync(path.join(ROOT, relative)), `owned anchor target must exist: ${relative}`);
3176
- }
3177
- });
3178
-
3179
- test('D11/R1.14: the legacy-bridge precondition artifact is reachable and change-firewall.cjs asserts its exact schema', () => {
3180
- const CHANGE_FIREWALL = require(path.join(ROOT, 'src/claude/scripts/change-firewall.cjs'));
3181
- // The bridge artifact lives under the monorepo root's specs/ (change-firewall.cjs
3182
- // resolves its own production root the same way, via git from its own file
3183
- // location) -- ROOT here is packages/spec, one level below that root.
3184
- const MONOREPO_ROOT = path.resolve(ROOT, '..', '..');
3185
- const bridgePath = path.join(MONOREPO_ROOT, 'specs/archive/cafekit-semantic-eval-firewall/reports/bootstrap-legacy-bridge-review.json');
3186
- assert.ok(fs.existsSync(bridgePath), 'the legacy-bridge artifact must exist before any R0-01 change-firewall assertion runs');
3187
- const parsed = CHANGE_FIREWALL.assertLegacyBridgeArtifact();
3188
- assert.equal(parsed.schema_version, '1');
3189
- assert.equal(parsed.verdict, 'PASS');
3190
- assert.equal(parsed.blocking_count, 0);
3191
- });