@haposoft/cafekit 0.16.7 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/README.md +14 -22
  2. package/bin/__tests__/codex-hooks-ownership.test.js +2 -2
  3. package/bin/__tests__/codex-hooks.test.js +169 -272
  4. package/bin/__tests__/codex-native.test.js +100 -202
  5. package/bin/__tests__/develop-contract.test.js +122 -1413
  6. package/bin/__tests__/docs-sync-on-demand.test.js +93 -0
  7. package/bin/__tests__/grok-envelope.test.js +0 -56
  8. package/bin/__tests__/hook-payload.test.js +4 -6
  9. package/bin/__tests__/legacy-authority-decoupled.test.js +154 -0
  10. package/bin/__tests__/legacy-hooks-retired.test.js +221 -0
  11. package/bin/__tests__/omp-bridge.test.js +20 -17
  12. package/bin/__tests__/omp-hooks.test.js +0 -19
  13. package/bin/__tests__/optional-skill-inventory.test.js +112 -4
  14. package/bin/__tests__/orca-session.test.js +0 -2
  15. package/bin/__tests__/package-inventory.test.js +90 -925
  16. package/bin/__tests__/runtime-dir.test.js +0 -3
  17. package/bin/__tests__/runtime-schema.test.js +3 -2
  18. package/bin/__tests__/skill-routing-source.test.js +27 -15
  19. package/bin/__tests__/spec-narrowing.test.js +30 -161
  20. package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -437
  21. package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
  22. package/bin/__tests__/ui-ux-skill.test.js +101 -0
  23. package/bin/__tests__/usage-hook-retired.test.js +114 -0
  24. package/bin/__tests__/validator-grounding.test.js +18 -2949
  25. package/bin/lib/managed-writer.js +2 -3
  26. package/bin/phases/codex-runtime.js +27 -0
  27. package/bin/phases/copy-payload.js +13 -28
  28. package/bin/phases/omp-runtime.js +25 -0
  29. package/package.json +2 -3
  30. package/src/claude/CLAUDE.md +3 -10
  31. package/src/claude/agents/brainstormer.md +7 -3
  32. package/src/claude/agents/code-auditor.md +44 -56
  33. package/src/claude/agents/debugger.md +8 -88
  34. package/src/claude/agents/deployer.md +3 -5
  35. package/src/claude/agents/docs-keeper.md +6 -14
  36. package/src/claude/agents/git-ops.md +2 -4
  37. package/src/claude/agents/implementer.md +12 -16
  38. package/src/claude/agents/inspector.md +4 -3
  39. package/src/claude/agents/project-manager.md +9 -17
  40. package/src/claude/agents/researcher.md +9 -3
  41. package/src/claude/agents/spec-maker.md +15 -15
  42. package/src/claude/agents/test-runner.md +8 -11
  43. package/src/claude/agents/ui-ux-designer.md +0 -1
  44. package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -1
  45. package/src/claude/hooks/__tests__/spec-gate.test.js +244 -333
  46. package/src/claude/hooks/__tests__/spec-state-touched.test.js +160 -0
  47. package/src/claude/hooks/__tests__/state.test.js +68 -0
  48. package/src/claude/hooks/__tests__/statusline.test.js +140 -11
  49. package/src/claude/hooks/agent.cjs +2 -1
  50. package/src/claude/hooks/lib/hook-payload.cjs +2 -2
  51. package/src/claude/hooks/rules.cjs +2 -1
  52. package/src/claude/hooks/session.cjs +0 -1
  53. package/src/claude/hooks/spec-gate.cjs +17 -83
  54. package/src/claude/hooks/spec-state.cjs +142 -39
  55. package/src/claude/hooks/state.cjs +21 -2
  56. package/src/claude/migration-manifest.json +42 -22
  57. package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
  58. package/src/claude/output-styles/coding-level-1-junior.md +3 -3
  59. package/src/claude/output-styles/coding-level-2-mid.md +3 -3
  60. package/src/claude/output-styles/coding-level-3-senior.md +2 -2
  61. package/src/claude/output-styles/coding-level-4-lead.md +5 -5
  62. package/src/claude/output-styles/coding-level-5-god.md +6 -6
  63. package/src/claude/rules/ai-dev-rules.md +2 -3
  64. package/src/claude/rules/hook-protocols.md +8 -15
  65. package/src/claude/rules/manage-docs.md +9 -23
  66. package/src/claude/rules/orchestrator.md +3 -4
  67. package/src/claude/rules/review-audit-self-decision.md +3 -3
  68. package/src/claude/rules/skill-domain-routing.md +1 -3
  69. package/src/claude/rules/skill-workflow-routing.md +4 -5
  70. package/src/claude/rules/state-sync.md +3 -12
  71. package/src/claude/rules/workflow.md +11 -19
  72. package/src/claude/runtime.schema.json +5 -3
  73. package/src/claude/scripts/generate-skill-catalog.cjs +7 -6
  74. package/src/claude/scripts/provenance.cjs +23 -5
  75. package/src/claude/scripts/spec-receipt.cjs +24 -121
  76. package/src/claude/scripts/spec-resolver.cjs +41 -458
  77. package/src/claude/scripts/workflow-policy.cjs +3 -1801
  78. package/src/claude/settings/settings.json +1 -32
  79. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  80. package/src/claude/skills/{question → ask}/SKILL.md +9 -75
  81. package/src/claude/skills/brainstorm/SKILL.md +43 -131
  82. package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
  83. package/src/claude/skills/code-review/SKILL.md +29 -81
  84. package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -8
  85. package/src/claude/skills/debug/SKILL.md +29 -149
  86. package/src/claude/skills/develop/SKILL.md +26 -83
  87. package/src/claude/skills/develop/references/parallel-waves.md +0 -8
  88. package/src/claude/skills/develop/references/quality-gate.md +15 -13
  89. package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
  90. package/src/claude/skills/docs/SKILL.md +2 -2
  91. package/src/claude/skills/docs/references/update-workflow.md +1 -1
  92. package/src/claude/skills/fix/SKILL.md +153 -0
  93. package/src/claude/skills/{hotfix → fix}/references/diagnosis-protocol.md +2 -2
  94. package/src/claude/skills/{hotfix → fix}/references/parallel-patterns.md +2 -2
  95. package/src/claude/skills/{hotfix → fix}/references/prevention-gate.md +3 -3
  96. package/src/claude/skills/{hotfix → fix}/references/review-cycle.md +1 -1
  97. package/src/claude/skills/git/SKILL.md +48 -9
  98. package/src/claude/skills/git/references/commit-protocols.md +35 -16
  99. package/src/claude/skills/git/references/worktree-blueprint.md +42 -6
  100. package/src/claude/skills/orca/SKILL.md +9 -8
  101. package/src/claude/skills/research/SKILL.md +17 -46
  102. package/src/claude/skills/scout/SKILL.md +61 -0
  103. package/src/claude/skills/{inspect → scout}/references/internal-inspection.md +26 -10
  104. package/src/claude/skills/specs/SKILL.md +22 -29
  105. package/src/claude/skills/specs/references/review.md +8 -6
  106. package/src/claude/skills/specs/references/templates.md +34 -41
  107. package/src/claude/skills/sync/SKILL.md +16 -45
  108. package/src/claude/skills/sync/references/rebind-and-audit.md +59 -0
  109. package/src/claude/skills/sync/references/sync-protocols.md +4 -13
  110. package/src/claude/skills/test/SKILL.md +39 -104
  111. package/src/claude/skills/test/references/execution-strategy.md +93 -21
  112. package/src/claude/skills/test/references/failure-triage.md +1 -1
  113. package/src/claude/skills/ui-ux/LICENSE +21 -0
  114. package/src/claude/skills/ui-ux/SKILL.md +497 -0
  115. package/src/claude/skills/ui-ux/references/brand-tokens.md +90 -0
  116. package/src/claude/skills/ui-ux/references/budgets.md +113 -0
  117. package/src/claude/skills/ui-ux/references/checklist.md +271 -0
  118. package/src/claude/skills/ui-ux/references/components/accordion.md +246 -0
  119. package/src/claude/skills/ui-ux/references/components/avatar.md +153 -0
  120. package/src/claude/skills/ui-ux/references/components/banner.md +43 -0
  121. package/src/claude/skills/ui-ux/references/components/breadcrumb.md +201 -0
  122. package/src/claude/skills/ui-ux/references/components/button.md +142 -0
  123. package/src/claude/skills/ui-ux/references/components/card.md +113 -0
  124. package/src/claude/skills/ui-ux/references/components/charts.md +376 -0
  125. package/src/claude/skills/ui-ux/references/components/chat.md +132 -0
  126. package/src/claude/skills/ui-ux/references/components/choice-controls.md +369 -0
  127. package/src/claude/skills/ui-ux/references/components/comment-thread.md +17 -0
  128. package/src/claude/skills/ui-ux/references/components/description-list.md +116 -0
  129. package/src/claude/skills/ui-ux/references/components/empty-state.md +98 -0
  130. package/src/claude/skills/ui-ux/references/components/file-upload.md +107 -0
  131. package/src/claude/skills/ui-ux/references/components/inline-edit.md +128 -0
  132. package/src/claude/skills/ui-ux/references/components/input.md +161 -0
  133. package/src/claude/skills/ui-ux/references/components/list-row.md +56 -0
  134. package/src/claude/skills/ui-ux/references/components/loading.md +198 -0
  135. package/src/claude/skills/ui-ux/references/components/logo.md +147 -0
  136. package/src/claude/skills/ui-ux/references/components/otp-input.md +43 -0
  137. package/src/claude/skills/ui-ux/references/components/quantity-input.md +100 -0
  138. package/src/claude/skills/ui-ux/references/components/range-slider.md +16 -0
  139. package/src/claude/skills/ui-ux/references/components/small-controls.md +340 -0
  140. package/src/claude/skills/ui-ux/references/components/sortable-header.md +86 -0
  141. package/src/claude/skills/ui-ux/references/components/tag-input.md +14 -0
  142. package/src/claude/skills/ui-ux/references/components/timeline.md +38 -0
  143. package/src/claude/skills/ui-ux/references/components/tree.md +30 -0
  144. package/src/claude/skills/ui-ux/references/design-process.md +650 -0
  145. package/src/claude/skills/ui-ux/references/layouts/app-kanban.html +303 -0
  146. package/src/claude/skills/ui-ux/references/layouts/app.md +1369 -0
  147. package/src/claude/skills/ui-ux/references/layouts/form.md +430 -0
  148. package/src/claude/skills/ui-ux/references/layouts/overlay.md +552 -0
  149. package/src/claude/skills/ui-ux/references/layouts/pricing.md +204 -0
  150. package/src/claude/skills/ui-ux/references/locked-rules.md +35 -0
  151. package/src/claude/skills/ui-ux/references/principles.md +408 -0
  152. package/src/claude/skills/ui-ux/references/refactor.md +245 -0
  153. package/src/claude/skills/ui-ux/references/responsive.md +195 -0
  154. package/src/claude/skills/ui-ux/references/review.md +528 -0
  155. package/src/claude/skills/ui-ux/references/rules-color.md +623 -0
  156. package/src/claude/skills/ui-ux/references/rules-form.md +235 -0
  157. package/src/claude/skills/ui-ux/references/rules-state.md +680 -0
  158. package/src/claude/skills/ui-ux/references/rules-type.md +369 -0
  159. package/src/claude/skills/ui-ux/references/styles.md +311 -0
  160. package/src/claude/skills/ui-ux/references/system.md +219 -0
  161. package/src/claude/skills/ui-ux/references/tailwind-v4-traps.md +237 -0
  162. package/src/claude/skills/ui-ux/references/tokens.css +362 -0
  163. package/src/claude/skills/ui-ux/scripts/lint-skill.mjs +216 -0
  164. package/src/claude/skills/ui-ux/scripts/package.json +9 -0
  165. package/src/claude/skills/ui-ux/scripts/probe.mjs +4447 -0
  166. package/src/claude/skills/ui-ux-pro-max/SKILL.md +25 -80
  167. package/src/claude/skills/web-testing/SKILL.md +2 -20
  168. package/src/claude/status.cjs +149 -144
  169. package/src/codex/AGENTS.md +8 -13
  170. package/src/codex/hooks/agent.cjs +2 -1
  171. package/src/codex/hooks/lib/spec-receipt.cjs +4 -32
  172. package/src/codex/hooks/lib/spec-utils.cjs +6 -52
  173. package/src/codex/hooks/rules.cjs +2 -1
  174. package/src/codex/hooks/session.cjs +0 -2
  175. package/src/codex/hooks/spec-gate.cjs +13 -81
  176. package/src/codex/hooks/spec-state.cjs +37 -41
  177. package/src/codex/hooks.json +0 -20
  178. package/src/codex/rules/state-sync.md +3 -12
  179. package/src/common/AGENTS.md +6 -12
  180. package/src/omp/extensions/cafekit-bridge.mjs +6 -6
  181. package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
  182. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
  183. package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
  184. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
  185. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
  186. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
  187. package/src/claude/hooks/__tests__/completion-authority.test.js +0 -990
  188. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +0 -259
  189. package/src/claude/hooks/completion-authority-check.cjs +0 -56
  190. package/src/claude/hooks/completion-authority-state.cjs +0 -527
  191. package/src/claude/hooks/completion-authority.cjs +0 -163
  192. package/src/claude/hooks/lib/context.cjs +0 -616
  193. package/src/claude/hooks/lib/detect.cjs +0 -474
  194. package/src/claude/hooks/semantic-review-authority.cjs +0 -154
  195. package/src/claude/hooks/task-scaffold-guard.cjs +0 -114
  196. package/src/claude/hooks/usage.cjs +0 -197
  197. package/src/claude/scripts/change-firewall.cjs +0 -1748
  198. package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
  199. package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
  200. package/src/claude/scripts/spec-final-state.cjs +0 -290
  201. package/src/claude/scripts/spec-ground.cjs +0 -650
  202. package/src/claude/scripts/spec-readiness.cjs +0 -425
  203. package/src/claude/scripts/spec-scaffold.cjs +0 -2317
  204. package/src/claude/scripts/spec-semantic-model.cjs +0 -396
  205. package/src/claude/scripts/validate-spec-output.cjs +0 -3941
  206. package/src/claude/skills/code-review/references/verification-gate.md +0 -46
  207. package/src/claude/skills/delegate/SKILL.md +0 -124
  208. package/src/claude/skills/delegate/references/codex-delegation.md +0 -71
  209. package/src/claude/skills/delegate/references/grok-delegation.md +0 -84
  210. package/src/claude/skills/hotfix/SKILL.md +0 -289
  211. package/src/claude/skills/inspect/SKILL.md +0 -142
  212. package/src/claude/skills/loop/SKILL.md +0 -114
  213. package/src/claude/skills/loop/references/bounded-loop-protocol.md +0 -104
  214. package/src/claude/skills/loop/references/metric-and-guard-contract.md +0 -76
  215. package/src/claude/skills/route/SKILL.md +0 -94
  216. package/src/claude/skills/route/references/agent-timing.md +0 -46
  217. package/src/claude/skills/route/references/chaining-patterns.md +0 -53
  218. package/src/claude/skills/route/references/task-taxonomy.md +0 -48
  219. package/src/claude/skills/specs/templates/design.md +0 -82
  220. package/src/claude/skills/specs/templates/requirements-init.md +0 -8
  221. package/src/claude/skills/specs/templates/requirements.md +0 -42
  222. package/src/claude/skills/specs/templates/spec-state.json +0 -56
  223. package/src/claude/skills/specs/templates/task.md +0 -47
  224. package/src/codex/hooks/completion-authority-check.cjs +0 -67
  225. package/src/codex/hooks/completion-authority-state.cjs +0 -527
  226. package/src/codex/hooks/completion-authority.cjs +0 -109
  227. package/src/codex/hooks/semantic-review-authority.cjs +0 -130
  228. package/src/codex/hooks/task-scaffold-guard.cjs +0 -63
  229. /package/src/claude/skills/{question → ask}/templates/question.md +0 -0
  230. /package/src/claude/skills/{hotfix → fix}/references/escalation-tactics.md +0 -0
  231. /package/src/claude/skills/{hotfix → fix}/references/workflow-specialized.md +0 -0
@@ -1,665 +0,0 @@
1
- 'use strict';
2
-
3
- const assert = require('node:assert/strict');
4
- const fs = require('node:fs');
5
- const os = require('node:os');
6
- const path = require('node:path');
7
- const { spawnSync } = require('node:child_process');
8
- const test = require('node:test');
9
-
10
- const PACKAGE_ROOT = path.resolve(__dirname, '../..');
11
- const POLICY = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/workflow-policy.cjs'));
12
- const SCAFFOLD = path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-scaffold.cjs');
13
- const TEMPLATE = path.join(PACKAGE_ROOT, 'src/claude/skills/specs/templates/spec-state.json');
14
- const CANONICAL_POLICY_FIELDS = [
15
- 'version', 'planning_depth', 'assurance_level', 'classified_minimum', 'risks',
16
- ];
17
- const CANONICAL_SPEC_FIELDS_WITH_TASKS = [
18
- 'schema_version', 'feature_name', 'created_at', 'updated_at', 'language', 'status',
19
- 'scope_lock', 'authoring', 'coordination', 'validation', 'semantic_model',
20
- 'ready_for_implementation', 'workflow_policy', 'task_files', 'task_registry',
21
- ].sort();
22
-
23
- function run(root, args, env = {}) {
24
- return spawnSync(process.execPath, [SCAFFOLD, ...args], {
25
- cwd: root, encoding: 'utf8', env: { ...process.env, ...env },
26
- });
27
- }
28
-
29
- function output(result) { return `${result.stdout}\n${result.stderr}`; }
30
-
31
- function readSpec(root, feature) {
32
- return JSON.parse(fs.readFileSync(path.join(root, 'specs', feature, 'spec.json'), 'utf8'));
33
- }
34
-
35
- // A canonical 2.1 scaffold always emits the C2 semantic_review shape plus the
36
- // C13 history and C16 receipt slots. A genuine schema 2.0 record predates all
37
- // three, so a fixture that simulates legacy migration input must strip them —
38
- // relabelling schema_version alone produces a record that never existed and
39
- // trips the 2.0 allowlist before the behavior under test is ever reached.
40
- function downgradeToLegacy20(spec) {
41
- spec.schema_version = '2.0';
42
- delete spec.validation.semantic_review_history;
43
- delete spec.validation.authoring_validation;
44
- const review = spec.validation.semantic_review;
45
- spec.validation.semantic_review = {
46
- status: review.status,
47
- semantic_digest: review.semantic_digest,
48
- reviewed_criteria: review.reviewed_criteria,
49
- counterexamples: review.counterexamples,
50
- };
51
- return spec;
52
- }
53
-
54
- function ownershipBoundary() {
55
- return JSON.stringify([{
56
- id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
57
- write_sets: { 'R1-01': ['src/one.js'], 'R1-02': ['src/two.js'] },
58
- }]);
59
- }
60
-
61
- function readTree(root) {
62
- const result = {};
63
- function visit(directory) {
64
- for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
65
- const absolute = path.join(directory, entry.name);
66
- const relative = path.relative(root, absolute);
67
- if (entry.isDirectory()) visit(absolute);
68
- else result[relative] = fs.readFileSync(absolute).toString('base64');
69
- }
70
- }
71
- visit(root);
72
- return result;
73
- }
74
-
75
- function runMatchingTasksOnly(root, feature, env = {}) {
76
- return run(root, [
77
- feature, '--tasks-only', '--tasks', 'R1-01-one,R1-02-two',
78
- '--boundaries', ownershipBoundary(),
79
- ], env);
80
- }
81
-
82
- function transactionDirs(root) {
83
- return fs.readdirSync(root).filter((entry) => entry.startsWith('.cafekit-scaffold-tx-'));
84
- }
85
-
86
- test('policy 2.1 persists exactly selected axes, classified minimum, and risks', () => {
87
- const snapshot = POLICY.canonicalWorkflowPolicySnapshot({
88
- planning_depth: 'Compact', assurance_level: 'Routine',
89
- });
90
- assert.deepEqual(Object.keys(snapshot), CANONICAL_POLICY_FIELDS);
91
- assert.equal(snapshot.version, '2.1');
92
- const view = POLICY.readWorkflowPolicySnapshot({ workflow_policy: snapshot });
93
- assert.equal(view.lane, 'Standard');
94
- assert.equal(view.artifact_profile, 'bounded');
95
- assert.deepEqual(view.proof_obligations, ['needsExecutionProof']);
96
- assert.deepEqual(POLICY.validateWorkflowPolicySnapshot(snapshot), { valid: true, errors: [] });
97
- const legacyCanonical = { ...snapshot, override_receipt: null };
98
- assert.equal(POLICY.validateWorkflowPolicySnapshot(legacyCanonical).valid, false);
99
- assert.deepEqual(
100
- POLICY.canonicalWorkflowPolicySnapshot({ workflow_policy: legacyCanonical }),
101
- snapshot,
102
- );
103
- });
104
-
105
- test('legacy policy is losslessly readable while authoring persistence migrates to 2.1', () => {
106
- const legacy = {
107
- version: '2', planning_depth: 'Compact', automatic_planning_depth: 'Compact',
108
- assurance_level: 'Routine', automatic_assurance_level: 'Routine', lane: 'Standard',
109
- automatic_lane: 'Standard', risks: [], artifact_profile: 'bounded',
110
- planning_obligations: ['needsRequirements', 'needsDesign'], proof_obligations: ['needsExecutionProof'],
111
- actor_needs: [{ capability: 'execution-proof', independence: 'same-session' }], override_receipt: null,
112
- };
113
- const view = POLICY.readWorkflowPolicySnapshot({ workflow_policy: legacy });
114
- assert.equal(view.version, '2');
115
- assert.equal(view.lane, 'Standard');
116
- const migrated = POLICY.persistWorkflowPolicySnapshot({ workflow_policy: legacy }).workflow_policy;
117
- assert.equal(migrated.version, '2.1');
118
- assert.equal(migrated.planning_depth, 'Compact');
119
- assert.equal(Object.hasOwn(migrated, 'lane'), false);
120
- assert.equal(Object.hasOwn(migrated, 'override_receipt'), false);
121
- });
122
-
123
- test('canonical state template is schema 2.1 and approval-free', () => {
124
- const template = JSON.parse(fs.readFileSync(TEMPLATE, 'utf8'));
125
- assert.equal(template.schema_version, '2.1');
126
- assert.equal(template.workflow_policy.version, '2.1');
127
- assert.equal(Object.hasOwn(template, 'approvals'), false);
128
- assert.deepEqual(template.authoring, {
129
- requirements: 'draft', design: 'draft', research: 'absent', tasks: 'absent',
130
- });
131
- assert.deepEqual(template.coordination, { boundaries: [] });
132
- assert.equal(template.semantic_model, null);
133
- assert.deepEqual(Object.keys(template.workflow_policy), CANONICAL_POLICY_FIELDS);
134
- assert.equal(Object.hasOwn(template, 'override_receipt'), false);
135
- assert.doesNotMatch(
136
- fs.readFileSync(SCAFFOLD, 'utf8'),
137
- /persist only workflow_policy\.override_receipt/,
138
- );
139
- });
140
-
141
- test('new scaffold emits taskless Compact 2.1 without Routine ceremony', () => {
142
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-taskless-'));
143
- try {
144
- const result = run(root, ['compact']);
145
- assert.equal(result.status, 0, result.stderr);
146
- const spec = readSpec(root, 'compact');
147
- assert.equal(spec.schema_version, '2.1');
148
- assert.equal(spec.workflow_policy.version, '2.1');
149
- assert.deepEqual(spec.coordination, { boundaries: [] });
150
- assert.equal(spec.authoring.tasks, 'absent');
151
- assert.equal(Object.hasOwn(spec, 'task_files'), false);
152
- assert.equal(Object.hasOwn(spec, 'task_registry'), false);
153
- assert.equal(Object.hasOwn(spec, 'research'), false);
154
- } finally { fs.rmSync(root, { recursive: true, force: true }); }
155
- });
156
-
157
- test('research materializes only from a concrete uncertainty and stays minimal', () => {
158
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-research-'));
159
- try {
160
- for (const [feature, args] of [
161
- ['missing', ['--research']],
162
- ['placeholder', ['--research', '--uncertainty', 'TBD']],
163
- ['orphan', ['--uncertainty', 'Which persistence boundary is authoritative?']],
164
- ]) {
165
- const result = run(root, [feature, ...args]);
166
- assert.equal(result.status, 2, output(result));
167
- assert.equal(fs.existsSync(path.join(root, 'specs', feature)), false);
168
- }
169
-
170
- const uncertainty = 'Which persisted boundary owns retry idempotency?';
171
- const result = run(root, ['grounded', '--research', '--uncertainty', uncertainty]);
172
- assert.equal(result.status, 0, output(result));
173
- const spec = readSpec(root, 'grounded');
174
- assert.equal(spec.research, 'research.md');
175
- assert.equal(spec.authoring.research, 'draft');
176
- const research = fs.readFileSync(path.join(root, 'specs/grounded/research.md'), 'utf8');
177
- assert.deepEqual(
178
- [...research.matchAll(/^## (.+)$/gm)].map((match) => match[1]),
179
- ['Uncertainty', 'Evidence Summary', 'Decision', 'Remaining Gaps'],
180
- );
181
- assert.match(research, new RegExp(uncertainty.replace(/[?]/g, '\\?')));
182
- assert.doesNotMatch(research, /\{\{[^}]+\}\}/);
183
- assert.doesNotMatch(research, /Finding 1|Option A|example\.com|Architecture Pattern Evaluation/);
184
- } finally { fs.rmSync(root, { recursive: true, force: true }); }
185
- });
186
-
187
- test('scaffold generates typed topology and canonical task projection', () => {
188
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-owned-'));
189
- try {
190
- const result = run(root, [
191
- 'owned', '--tasks', 'R1-01-one,R1-02-two', '--boundaries', ownershipBoundary(),
192
- ]);
193
- assert.equal(result.status, 0, result.stderr);
194
- const spec = readSpec(root, 'owned');
195
- assert.deepEqual(spec.coordination.boundaries, JSON.parse(ownershipBoundary()));
196
- assert.equal(spec.authoring.tasks, 'draft');
197
- for (const taskPath of spec.task_files) {
198
- const body = fs.readFileSync(path.join(root, 'specs', 'owned', taskPath), 'utf8');
199
- for (const heading of ['Outcome', 'Scope', 'Anchors and Ownership', 'Changes', 'Acceptance', 'Dependencies', 'Verification Plan']) {
200
- assert.match(body, new RegExp(`^## ${heading}$`, 'm'));
201
- }
202
- assert.match(body, /\| ID \| Type \| Target \| Role \| Access \| Action \|/);
203
- assert.doesNotMatch(body, /^## Related Files$/m);
204
- }
205
- } finally { fs.rmSync(root, { recursive: true, force: true }); }
206
- });
207
-
208
- test('scaffold rejects marker-only evidence, single-task parallel, and parent-child contention', () => {
209
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-negative-'));
210
- try {
211
- const marker = run(root, ['marker', '--tasks', 'R1-01-one', '--task-triggers', 'separate_proof']);
212
- assert.equal(marker.status, 2);
213
- assert.match(`${marker.stdout}\n${marker.stderr}`, /only topology authority|typed coordination/);
214
-
215
- const single = run(root, [
216
- 'single', '--tasks', 'R1-01-one', '--boundaries',
217
- JSON.stringify([{ id: 'B-P', type: 'parallel', tasks: ['R1-01'], resources: { 'R1-01': ['src/one.js'] } }]),
218
- ]);
219
- assert.equal(single.status, 2);
220
- assert.match(`${single.stdout}\n${single.stderr}`, /at least 2 task/);
221
-
222
- const contention = run(root, [
223
- 'contention', '--tasks', 'R1-01-one,R1-02-two', '--boundaries',
224
- JSON.stringify([{ id: 'B-P', type: 'parallel', tasks: ['R1-01', 'R1-02'], resources: {
225
- 'R1-01': ['src/domain'], 'R1-02': ['src/domain/model.js'],
226
- } }]),
227
- ]);
228
- assert.equal(contention.status, 2);
229
- assert.match(`${contention.stdout}\n${contention.stderr}`, /resource contention/);
230
- } finally { fs.rmSync(root, { recursive: true, force: true }); }
231
- });
232
-
233
- test('scaffold rejects incomplete dependency, transition, and proof boundaries', () => {
234
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-boundaries-'));
235
- try {
236
- const cases = [
237
- ['dependency', { id: 'B-D', type: 'dependency', producer: 'R1-01', consumer: 'R1-02' }, /fields must be exactly/],
238
- ['transition', { id: 'B-T', type: 'transition', design_ref: 'T1', owner: 'R1-01', consumers: ['R1-02'], precondition: 'ready state', postcondition: 'done state', failure: 'failed state' }, /fields must be exactly/],
239
- ['proof', { id: 'B-V', type: 'proof', subject: 'R1-01', verifier: 'R1-01', verification_ref: 'V1', artifact_anchor: 'A-R1-01-02' }, /duplicate task ids|subject and verifier must differ/],
240
- ];
241
- for (const [feature, boundary, expected] of cases) {
242
- const result = run(root, [
243
- feature, '--tasks', 'R1-01-one,R1-02-two', '--boundaries', JSON.stringify([boundary]),
244
- ]);
245
- assert.equal(result.status, 2);
246
- assert.match(`${result.stdout}\n${result.stderr}`, expected);
247
- }
248
- } finally { fs.rmSync(root, { recursive: true, force: true }); }
249
- });
250
-
251
- test('scaffold transaction rolls back injected writes exactly', () => {
252
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-rollback-'));
253
- try {
254
- const result = run(root, ['rollback'], { CAFEKIT_SCAFFOLD_FAIL_AFTER_WRITES: '1' });
255
- assert.equal(result.status, 2);
256
- assert.match(`${result.stdout}\n${result.stderr}`, /transaction rolled back/);
257
- assert.equal(fs.existsSync(path.join(root, 'specs', 'rollback')), false);
258
- } finally { fs.rmSync(root, { recursive: true, force: true }); }
259
- });
260
-
261
- test('durable transaction recovery covers every fresh and replacement rename boundary', { skip: process.platform === 'win32' }, () => {
262
- const matrices = [
263
- { name: 'fresh', boundaries: 3, prepare() {}, invoke(root, feature, env) { return run(root, [feature], env); } },
264
- {
265
- name: 'replacement', boundaries: 2,
266
- prepare(root, feature) {
267
- const initial = run(root, [
268
- feature, '--tasks', 'R1-01-one,R1-02-two', '--boundaries', ownershipBoundary(),
269
- ]);
270
- assert.equal(initial.status, 0, output(initial));
271
- const specPath = path.join(root, 'specs', feature, 'spec.json');
272
- const spec = downgradeToLegacy20(readSpec(root, feature));
273
- fs.writeFileSync(specPath, `${JSON.stringify(spec, null, 2)}\n`);
274
- },
275
- invoke(root, feature, env) { return runMatchingTasksOnly(root, feature, env); },
276
- },
277
- ];
278
-
279
- for (const matrix of matrices) {
280
- for (let boundary = 1; boundary <= matrix.boundaries; boundary += 1) {
281
- const root = fs.mkdtempSync(path.join(os.tmpdir(), `cafekit-sk21-kill-${matrix.name}-`));
282
- const feature = `${matrix.name}-${boundary}`;
283
- try {
284
- matrix.prepare(root, feature);
285
- const killed = matrix.invoke(root, feature, {
286
- CAFEKIT_SCAFFOLD_KILL_AFTER_RENAMES: String(boundary),
287
- });
288
- assert.equal(killed.status, null, `${matrix.name}:${boundary}: ${output(killed)}`);
289
- assert.equal(killed.signal, 'SIGKILL', `${matrix.name}:${boundary}`);
290
- assert.equal(transactionDirs(root).length, 1, `${matrix.name}:${boundary} missing journal`);
291
-
292
- const recovered = matrix.invoke(root, feature, {});
293
- assert.equal(recovered.status, 0, `${matrix.name}:${boundary}: ${output(recovered)}`);
294
- assert.equal(transactionDirs(root).length, 0, `${matrix.name}:${boundary} leaked journal`);
295
- assert.equal(readSpec(root, feature).schema_version, '2.1');
296
- } finally { fs.rmSync(root, { recursive: true, force: true }); }
297
- }
298
- }
299
- });
300
-
301
- test('startup recovery preserves a target whose transaction ownership cannot be proven', { skip: process.platform === 'win32' }, () => {
302
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-unowned-'));
303
- try {
304
- const killed = run(root, ['unowned'], { CAFEKIT_SCAFFOLD_KILL_AFTER_RENAMES: '1' });
305
- assert.equal(killed.status, null, output(killed));
306
- assert.equal(killed.signal, 'SIGKILL');
307
- const target = path.join(root, 'specs/unowned/spec.json');
308
- const externalBytes = Buffer.from('{"external":"concurrent writer"}\n');
309
- fs.writeFileSync(target, externalBytes);
310
-
311
- const recovery = run(root, ['unowned']);
312
- assert.equal(recovery.status, 2, output(recovery));
313
- assert.match(output(recovery), /refuses to delete unowned target spec\.json/);
314
- assert.deepEqual(fs.readFileSync(target), externalBytes);
315
- assert.equal(transactionDirs(root).length, 1, 'journal must remain for explicit resolution');
316
- } finally { fs.rmSync(root, { recursive: true, force: true }); }
317
- });
318
-
319
- test('typed boundary mutation preserves legacy graph counterexamples atomically', () => {
320
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-graph-mutations-'));
321
- try {
322
- const tasks = 'R1-01-owner,R1-02-consumer';
323
- const cases = [
324
- ['unknown-task', [{ id: 'B-D', type: 'dependency', producer: 'R1-99', consumer: 'R1-02', deliverable: 'artifacts/value.json' }], /unknown task R1-99/],
325
- ['canonical-path-alias', [{ id: 'B-D', type: 'dependency', producer: 'tasks/task-R1-01-owner.md', consumer: 'R1-02', deliverable: 'artifacts/value.json' }], /unknown task tasks\/task-R1-01-owner\.md/],
326
- ['duplicate-boundary-id', [
327
- { id: 'B-DUP', type: 'dependency', producer: 'R1-01', consumer: 'R1-02', deliverable: 'artifacts/a.json' },
328
- { id: 'B-DUP', type: 'dependency', producer: 'R1-01', consumer: 'R1-02', deliverable: 'artifacts/b.json' },
329
- ], /duplicate id B-DUP/],
330
- ['dependency-cycle', [
331
- { id: 'B-D1', type: 'dependency', producer: 'R1-01', consumer: 'R1-02', deliverable: 'artifacts/a.json' },
332
- { id: 'B-D2', type: 'dependency', producer: 'R1-02', consumer: 'R1-01', deliverable: 'artifacts/b.json' },
333
- ], /dependency cycle/],
334
- ['malformed-fields', [{ id: 'B-D', type: 'dependency', producer: 'R1-01', consumer: 'R1-02' }], /fields must be exactly/],
335
- ];
336
- for (const [feature, boundaries, expected] of cases) {
337
- const before = fs.existsSync(path.join(root, 'specs'))
338
- ? fs.readdirSync(path.join(root, 'specs')).sort() : [];
339
- const result = run(root, [feature, '--tasks', tasks, '--boundaries', JSON.stringify(boundaries)]);
340
- assert.equal(result.status, 2, output(result));
341
- assert.match(output(result), expected);
342
- assert.equal(fs.existsSync(path.join(root, 'specs', feature)), false, `${feature} wrote before validation`);
343
- assert.deepEqual(fs.existsSync(path.join(root, 'specs')) ? fs.readdirSync(path.join(root, 'specs')).sort() : [], before);
344
- }
345
-
346
- const identity = run(root, [
347
- 'duplicate-task-identity', '--tasks', 'R1-01-owner,R1-01-alias', '--boundaries', '[]',
348
- ]);
349
- assert.equal(identity.status, 2);
350
- assert.match(output(identity), /duplicate task id "R1-01"/);
351
- assert.equal(fs.existsSync(path.join(root, 'specs', 'duplicate-task-identity')), false);
352
- } finally { fs.rmSync(root, { recursive: true, force: true }); }
353
- });
354
-
355
- test('tasks-only normalization is table-driven, conservative, and idempotent', () => {
356
- const cases = [
357
- {
358
- name: 'partial authoring and legacy schema',
359
- mutate(spec) {
360
- spec.schema_version = '2.0';
361
- spec.override_receipt = null;
362
- spec.workflow_policy.override_receipt = null;
363
- delete spec.feature_name;
364
- spec.authoring = { requirements: 'validated' };
365
- spec.ready_for_implementation = true;
366
- spec.validation = {
367
- status: 'completed',
368
- semantic_review: {
369
- status: 'completed', semantic_digest: 'legacy',
370
- reviewed_criteria: ['R1.1'], counterexamples: ['legacy review'],
371
- },
372
- };
373
- },
374
- verify(spec) {
375
- assert.equal(spec.feature_name, 'partial-authoring-and-legacy-schema');
376
- assert.deepEqual(Object.keys(spec).sort(), CANONICAL_SPEC_FIELDS_WITH_TASKS);
377
- assert.equal(Object.hasOwn(spec, 'override_receipt'), false);
378
- assert.deepEqual(Object.keys(spec.scope_lock).sort(), [
379
- 'expansion_policy', 'in_scope', 'out_of_scope', 'source',
380
- ]);
381
- assert.equal(Object.hasOwn(spec.task_registry['tasks/task-R1-01-one.md'], 'legacy_owner'), false);
382
- assert.deepEqual(Object.keys(spec.workflow_policy), CANONICAL_POLICY_FIELDS);
383
- // The legacy input asserts requirements: 'validated' with no C16 receipt
384
- // behind it. Migration downgrades it to draft rather than carrying the
385
- // unprovable claim forward: only spec-authoring-validation.cjs may write
386
- // `validated`, and only together with a matching digest receipt (I21).
387
- assert.deepEqual(spec.authoring, {
388
- requirements: 'draft', design: 'draft', research: 'absent', tasks: 'draft',
389
- });
390
- assert.equal(spec.ready_for_implementation, false);
391
- // A legacy validation block never satisfies the canonical 2.1 shape, so
392
- // migration replaces it with the fresh C2 review, a seeded C13 lineage,
393
- // and an empty C16 slot — the legacy digest/criteria are not carried
394
- // forward as if they had been reviewed under the current contract.
395
- assert.deepEqual(Object.keys(spec.validation).sort(), [
396
- 'authoring_validation', 'semantic_review', 'semantic_review_history', 'status',
397
- ]);
398
- assert.equal(spec.validation.status, 'not-run');
399
- assert.deepEqual(spec.validation.semantic_review, {
400
- status: 'not-run', semantic_digest: null, verdict: null, lifecycle_disposition: null,
401
- findings: [], unresolved_decisions: [], graph_coverage: [], repair_round: 0,
402
- reviewed_criteria: [], counterexamples: [], reviewer_evidence: null,
403
- });
404
- assert.deepEqual(spec.validation.semantic_review_history.entries, []);
405
- assert.match(spec.validation.semantic_review_history.lineage_id, /^sha256:[0-9a-f]{64}$/);
406
- assert.equal(spec.validation.authoring_validation, null);
407
- },
408
- },
409
- {
410
- name: 'legacy research pointer and valid done lifecycle',
411
- research: true,
412
- mutate(spec) {
413
- downgradeToLegacy20(spec);
414
- spec.research = { path: 'research.md' };
415
- delete spec.authoring.research;
416
- const task = spec.task_registry['tasks/task-R1-01-one.md'];
417
- Object.assign(task, {
418
- status: 'done', blocker: null,
419
- started_at: '2026-08-13T01:00:00.000Z',
420
- completed_at: '2026-08-13T02:00:00.000Z',
421
- last_updated_at: '2026-08-13T03:00:00.000Z',
422
- });
423
- },
424
- verify(spec) {
425
- assert.equal(spec.research, 'research.md');
426
- assert.equal(spec.authoring.research, 'draft');
427
- assert.equal(spec.task_registry['tasks/task-R1-01-one.md'].status, 'done');
428
- assert.equal(
429
- spec.task_registry['tasks/task-R1-01-one.md'].completed_at,
430
- '2026-08-13T02:00:00.000Z',
431
- );
432
- },
433
- },
434
- {
435
- name: 'legacy nested inert receipt read migration',
436
- mutate(spec) {
437
- downgradeToLegacy20(spec);
438
- spec.workflow_policy.override_receipt = null;
439
- },
440
- verify(spec) {
441
- assert.equal(Object.hasOwn(spec, 'override_receipt'), false);
442
- assert.deepEqual(Object.keys(spec.workflow_policy), CANONICAL_POLICY_FIELDS);
443
- assert.equal(spec.ready_for_implementation, false);
444
- assert.equal(spec.validation.status, 'not-run');
445
- },
446
- },
447
- {
448
- name: 'already canonical matching bundle',
449
- expectStable: true,
450
- mutate() {},
451
- verify(spec) {
452
- assert.equal(spec.schema_version, '2.1');
453
- assert.equal(spec.authoring.tasks, 'draft');
454
- },
455
- },
456
- ];
457
-
458
- for (const scenario of cases) {
459
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-normalize-'));
460
- try {
461
- const initial = run(root, [
462
- scenario.name.replaceAll(' ', '-'), '--tasks', 'R1-01-one,R1-02-two',
463
- '--boundaries', ownershipBoundary(), ...(scenario.research ? [
464
- '--research', '--uncertainty', 'Which evidence resolves the legacy research decision?',
465
- ] : []),
466
- ]);
467
- assert.equal(initial.status, 0, output(initial));
468
- const feature = scenario.name.replaceAll(' ', '-');
469
- const specPath = path.join(root, 'specs', feature, 'spec.json');
470
- const spec = readSpec(root, feature);
471
- scenario.mutate(spec);
472
- fs.writeFileSync(specPath, `${JSON.stringify(spec, null, 2)}\n`);
473
- const before = fs.readFileSync(specPath, 'utf8');
474
- const result = runMatchingTasksOnly(root, feature);
475
- assert.equal(result.status, 0, `${scenario.name}: ${output(result)}`);
476
- const after = fs.readFileSync(specPath, 'utf8');
477
- if (scenario.expectStable) assert.equal(after, before, `${scenario.name} rewrote canonical state`);
478
- scenario.verify(JSON.parse(after));
479
- } finally { fs.rmSync(root, { recursive: true, force: true }); }
480
- }
481
- });
482
-
483
- test('canonical 2.1 input rejects unknown semantics at every authored object boundary', () => {
484
- const mutations = [
485
- ['top', (spec) => { spec.dead_field = true; }],
486
- ['scope', (spec) => { spec.scope_lock.dead_field = true; }],
487
- ['authoring', (spec) => { spec.authoring.approved = true; }],
488
- ['coordination', (spec) => { spec.coordination.task_triggers = ['marker']; }],
489
- ['boundary', (spec) => { spec.coordination.boundaries[0].narrative_owner = 'self-claim'; }],
490
- ['validation', (spec) => { spec.validation.audit_status = 'PASS'; }],
491
- ['semantic-review', (spec) => { spec.validation.semantic_review.reviewer_claim = 'self'; }],
492
- ['counterexample', (spec) => {
493
- spec.validation.semantic_review.counterexamples.push({
494
- criterion: 'R1.1', case_kind: 'failure', scenario: 'request fails',
495
- expected: 'failure remains observable', decision_refs: ['D1'],
496
- verification_ref: 'V1', hidden_claim: 'trusted',
497
- });
498
- }],
499
- ['registry', (spec) => { spec.task_registry['tasks/task-R1-01-one.md'].owner_note = 'narrative'; }],
500
- ];
501
- for (const [name, mutate] of mutations) {
502
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-closed-world-'));
503
- try {
504
- const feature = `unknown-${name}`;
505
- const initial = run(root, [
506
- feature, '--tasks', 'R1-01-one,R1-02-two', '--boundaries', ownershipBoundary(),
507
- ]);
508
- assert.equal(initial.status, 0, output(initial));
509
- const specPath = path.join(root, 'specs', feature, 'spec.json');
510
- const spec = readSpec(root, feature);
511
- mutate(spec);
512
- fs.writeFileSync(specPath, `${JSON.stringify(spec, null, 2)}\n`);
513
- const before = readTree(path.dirname(specPath));
514
- const result = runMatchingTasksOnly(root, feature);
515
- assert.equal(result.status, 2, `${name}: ${output(result)}`);
516
- assert.match(output(result), /closed-world; unknown=\[[^\]]+\]/);
517
- assert.deepEqual(readTree(path.dirname(specPath)), before, `${name} mutated on rejection`);
518
- } finally { fs.rmSync(root, { recursive: true, force: true }); }
519
- }
520
- });
521
-
522
- test('tasks-only migration rejects invalid evidence without any write', () => {
523
- const cases = [
524
- {
525
- name: 'missing requirements',
526
- mutate(root, feature) { fs.unlinkSync(path.join(root, 'specs', feature, 'requirements.md')); },
527
- expected: /requirements\.md|physical requirements/,
528
- },
529
- {
530
- name: 'forged done lifecycle',
531
- mutate(root, feature, spec) {
532
- spec.task_registry['tasks/task-R1-01-one.md'].status = 'done';
533
- fs.writeFileSync(path.join(root, 'specs', feature, 'spec.json'), `${JSON.stringify(spec, null, 2)}\n`);
534
- },
535
- expected: /done lifecycle is inconsistent/,
536
- },
537
- {
538
- name: 'research pointer without artifact',
539
- mutate(root, feature, spec) {
540
- spec.research = 'research.md';
541
- spec.authoring.research = 'validated';
542
- fs.writeFileSync(path.join(root, 'specs', feature, 'spec.json'), `${JSON.stringify(spec, null, 2)}\n`);
543
- },
544
- expected: /without a physical research\.md/,
545
- },
546
- {
547
- name: 'canonical top level inert receipt',
548
- mutate(root, feature, spec) {
549
- spec.override_receipt = null;
550
- fs.writeFileSync(path.join(root, 'specs', feature, 'spec.json'), `${JSON.stringify(spec, null, 2)}\n`);
551
- },
552
- expected: /closed-world.*override_receipt/,
553
- },
554
- {
555
- name: 'ambiguous top level inert receipt',
556
- mutate(root, feature, spec) {
557
- downgradeToLegacy20(spec);
558
- delete spec.schema_version;
559
- spec.override_receipt = null;
560
- fs.writeFileSync(path.join(root, 'specs', feature, 'spec.json'), `${JSON.stringify(spec, null, 2)}\n`);
561
- },
562
- expected: /only readable as inert null during schema 2\.0 migration; omit the field/,
563
- },
564
- {
565
- name: 'legacy top level non inert receipt',
566
- mutate(root, feature, spec) {
567
- downgradeToLegacy20(spec);
568
- spec.override_receipt = { issuer: 'unsupported' };
569
- fs.writeFileSync(path.join(root, 'specs', feature, 'spec.json'), `${JSON.stringify(spec, null, 2)}\n`);
570
- },
571
- expected: /top-level override_receipt is unsupported; omit the field/,
572
- },
573
- {
574
- name: 'nested non inert receipt',
575
- mutate(root, feature, spec) {
576
- spec.workflow_policy.override_receipt = 'forged';
577
- fs.writeFileSync(path.join(root, 'specs', feature, 'spec.json'), `${JSON.stringify(spec, null, 2)}\n`);
578
- },
579
- expected: /workflow_policy\.override_receipt is unsupported; omit the field/,
580
- },
581
- ...['', 42, 'different-feature'].map((featureName) => ({
582
- name: `conflicting feature identity ${String(featureName) || 'empty'}`,
583
- mutate(root, feature, spec) {
584
- spec.feature_name = featureName;
585
- fs.writeFileSync(path.join(root, 'specs', feature, 'spec.json'), `${JSON.stringify(spec, null, 2)}\n`);
586
- },
587
- expected: /feature_name conflicts with the scaffold feature identity/,
588
- })),
589
- ];
590
-
591
- for (const scenario of cases) {
592
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk21-atomic-'));
593
- try {
594
- const feature = scenario.name.replaceAll(' ', '-');
595
- const initial = run(root, [
596
- feature, '--tasks', 'R1-01-one,R1-02-two', '--boundaries', ownershipBoundary(),
597
- ]);
598
- assert.equal(initial.status, 0, output(initial));
599
- scenario.mutate(root, feature, readSpec(root, feature));
600
- const featureDir = path.join(root, 'specs', feature);
601
- const before = readTree(featureDir);
602
- const result = runMatchingTasksOnly(root, feature);
603
- assert.equal(result.status, 2, `${scenario.name}: ${output(result)}`);
604
- assert.match(output(result), scenario.expected);
605
- assert.deepEqual(readTree(featureDir), before, `${scenario.name} mutated the bundle on rejection`);
606
- } finally { fs.rmSync(root, { recursive: true, force: true }); }
607
- }
608
- });
609
-
610
- test('schema 2.0 migration rejects unknown authority fields instead of projection-dropping them', () => {
611
- const mutations = [
612
- ['top-level', (spec) => { spec.dead_authority = true; }],
613
- ['scope-lock', (spec) => { spec.scope_lock.dead_authority = true; }],
614
- ['workflow-policy', (spec) => { spec.workflow_policy.dead_authority = true; }],
615
- ['registry', (spec) => { spec.task_registry['tasks/task-R1-01-one.md'].dead_authority = true; }],
616
- ];
617
- for (const [name, mutate] of mutations) {
618
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-sk20-unknown-'));
619
- try {
620
- const feature = `legacy-unknown-${name}`;
621
- assert.equal(run(root, [feature, '--tasks', 'R1-01-one,R1-02-two', '--boundaries', ownershipBoundary()]).status, 0);
622
- const specPath = path.join(root, 'specs', feature, 'spec.json');
623
- const spec = downgradeToLegacy20(readSpec(root, feature));
624
- mutate(spec);
625
- fs.writeFileSync(specPath, `${JSON.stringify(spec, null, 2)}\n`);
626
- const before = readTree(path.dirname(specPath));
627
- const result = runMatchingTasksOnly(root, feature);
628
- assert.equal(result.status, 2, `${name}: ${output(result)}`);
629
- // Must name the injected dead_authority field specifically: before the
630
- // legacy downgrade above, this matched on the 2.1-only validation records
631
- // instead and passed without exercising the mutation under test.
632
- assert.match(output(result), /unsupported legacy authority field.*dead_authority/);
633
- assert.deepEqual(readTree(path.dirname(specPath)), before);
634
- } finally { fs.rmSync(root, { recursive: true, force: true }); }
635
- }
636
- });
637
-
638
- test('--help and --version answer on stdout before any precondition runs', () => {
639
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-scaffold-info-flags-'));
640
- try {
641
- for (const flag of ['--help', '-h']) {
642
- const result = run(root, [flag]);
643
- assert.equal(result.status, 0, `${flag}: ${output(result)}`);
644
- assert.match(result.stdout, /^Usage: node spec-scaffold\.cjs/, `${flag} must print usage on stdout`);
645
- assert.equal(result.stderr, '', `${flag} must not write to stderr`);
646
- }
647
-
648
- for (const flag of ['--version', '-v']) {
649
- const result = run(root, [flag]);
650
- assert.equal(result.status, 0, `${flag}: ${output(result)}`);
651
- // Prerelease suffixes are normal on this package (e.g. 0.16.0-rc.4).
652
- assert.match(result.stdout.trim(), /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/, `${flag} must print a SemVer version`);
653
- assert.equal(result.stderr, '', `${flag} must not write to stderr`);
654
- }
655
-
656
- // A help flag must never be read as a feature name, and must not create a spec.
657
- assert.deepEqual(fs.readdirSync(root), [], 'info flags must not touch the work tree');
658
-
659
- // The no-argument path keeps usage-as-diagnostic: stderr, exit 2.
660
- const bare = run(root, []);
661
- assert.equal(bare.status, 2, output(bare));
662
- assert.equal(bare.stdout, '', 'bare invocation must not print usage on stdout');
663
- assert.match(bare.stderr, /^Usage: node spec-scaffold\.cjs/);
664
- } finally { fs.rmSync(root, { recursive: true, force: true }); }
665
- });