@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,425 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- const crypto = require('crypto');
7
- const SEMANTIC = require('./spec-semantic-model.cjs');
8
- const VALIDATOR = require('./validate-spec-output.cjs');
9
- const { canonicalProjectRoot, groundSpec } = require('./spec-ground.cjs');
10
-
11
- const REVIEW_RESULT_FIELDS = Object.freeze([
12
- 'verdict', 'findings', 'unresolved_decisions', 'graph_coverage',
13
- 'reviewed_criteria', 'counterexamples', 'reviewer_evidence',
14
- ]);
15
- const C2_RECEIPT_FIELDS = Object.freeze([
16
- 'status', 'semantic_digest', 'verdict', 'lifecycle_disposition',
17
- 'findings', 'unresolved_decisions', 'graph_coverage', 'repair_round',
18
- 'reviewed_criteria', 'counterexamples', 'reviewer_evidence',
19
- ]);
20
- const HISTORY_FIELDS = Object.freeze(['lineage_id', 'entries']);
21
- const HISTORY_ENTRY_FIELDS = Object.freeze([
22
- 'sequence', 'semantic_digest', 'review_receipt_digest', 'verdict',
23
- 'lifecycle_disposition', 'blocking_count', 'attempt_index', 'review_epoch',
24
- ]);
25
- const SHA256_RE = /^sha256:[0-9a-f]{64}$/;
26
-
27
- function plain(value) {
28
- return Boolean(value && typeof value === 'object' && !Array.isArray(value));
29
- }
30
-
31
- function exactKeys(value, fields) {
32
- return plain(value)
33
- && JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...fields].sort());
34
- }
35
-
36
- function stableDigest(value, semantic) {
37
- if (!semantic || typeof semantic.stableJson !== 'function') {
38
- throw new Error('semantic stableJson is required for readiness receipts');
39
- }
40
- const stable = semantic.stableJson(value);
41
- return `sha256:${crypto.createHash('sha256').update(Buffer.from(stable, 'utf8')).digest('hex')}`;
42
- }
43
-
44
- function requireString(value, label, minimum = 1) {
45
- if (typeof value !== 'string' || value.length < minimum) {
46
- throw new Error(`${label} must be a string with at least ${minimum} characters`);
47
- }
48
- }
49
-
50
- function validateReviewFinding(finding, index) {
51
- if (!exactKeys(finding, ['id', 'severity', 'location', 'summary', 'blocking', 'disposition'])) {
52
- throw new Error(`semantic review finding ${index} fields must be exact`);
53
- }
54
- requireString(finding.id, `semantic review finding ${index}.id`);
55
- requireString(finding.location, `semantic review finding ${index}.location`);
56
- requireString(finding.summary, `semantic review finding ${index}.summary`, 8);
57
- if (!['Critical', 'High', 'Medium', 'Low'].includes(finding.severity)) {
58
- throw new Error(`semantic review finding ${index}.severity is invalid`);
59
- }
60
- if (typeof finding.blocking !== 'boolean') {
61
- throw new Error(`semantic review finding ${index}.blocking must be boolean`);
62
- }
63
- if (!['open', 'accepted', 'mitigated'].includes(finding.disposition)) {
64
- throw new Error(`semantic review finding ${index}.disposition is invalid`);
65
- }
66
- }
67
-
68
- function validateReviewDecision(decision, index) {
69
- if (!exactKeys(decision, ['id', 'summary', 'blocking'])) {
70
- throw new Error(`semantic review unresolved decision ${index} fields must be exact`);
71
- }
72
- requireString(decision.id, `semantic review unresolved decision ${index}.id`);
73
- requireString(decision.summary, `semantic review unresolved decision ${index}.summary`, 8);
74
- if (typeof decision.blocking !== 'boolean') {
75
- throw new Error(`semantic review unresolved decision ${index}.blocking must be boolean`);
76
- }
77
- }
78
-
79
- function validateGraphCoverage(surface, index) {
80
- if (!exactKeys(surface, ['surface', 'covered', 'notes'])) {
81
- throw new Error(`semantic review graph coverage ${index} fields must be exact`);
82
- }
83
- if (![
84
- 'criterion_local', 'cross_criterion', 'runtime_path',
85
- 'assumption_provenance', 'compatibility_migration',
86
- ].includes(surface.surface)) {
87
- throw new Error(`semantic review graph coverage ${index}.surface is invalid`);
88
- }
89
- if (typeof surface.covered !== 'boolean') {
90
- throw new Error(`semantic review graph coverage ${index}.covered must be boolean`);
91
- }
92
- requireString(surface.notes, `semantic review graph coverage ${index}.notes`);
93
- }
94
-
95
- function validateCounterexample(counterexample, index) {
96
- if (!exactKeys(counterexample, [
97
- 'criterion', 'case_kind', 'scenario', 'expected', 'decision_refs', 'verification_ref',
98
- ])) {
99
- throw new Error(`semantic review counterexample ${index} fields must be exact`);
100
- }
101
- for (const field of ['criterion', 'case_kind', 'scenario', 'expected', 'verification_ref']) {
102
- requireString(counterexample[field], `semantic review counterexample ${index}.${field}`);
103
- }
104
- if (!Array.isArray(counterexample.decision_refs)
105
- || counterexample.decision_refs.some((value) => typeof value !== 'string')) {
106
- throw new Error(`semantic review counterexample ${index}.decision_refs must be string array`);
107
- }
108
- }
109
-
110
- function validateReviewerEvidence(evidence) {
111
- if (evidence === null) return;
112
- if (!exactKeys(evidence, ['assurance', 'summary'])) {
113
- throw new Error('semantic review reviewer_evidence fields must be exact');
114
- }
115
- if (!['Routine', 'Elevated', 'Strict'].includes(evidence.assurance)) {
116
- throw new Error('semantic review reviewer_evidence.assurance is invalid');
117
- }
118
- requireString(evidence.summary, 'semantic review reviewer_evidence.summary', 8);
119
- }
120
-
121
- function readReviewResult(input) {
122
- if (!exactKeys(input, REVIEW_RESULT_FIELDS)) {
123
- throw new Error(`semantic review result fields must be exactly ${REVIEW_RESULT_FIELDS.join(', ')}`);
124
- }
125
- if (input.verdict !== 'PASS' && input.verdict !== 'FAIL') {
126
- throw new Error('semantic review result verdict must be PASS or FAIL');
127
- }
128
- if (!Array.isArray(input.findings)) throw new Error('semantic review result findings must be an array');
129
- input.findings.forEach(validateReviewFinding);
130
- if (!Array.isArray(input.unresolved_decisions)) {
131
- throw new Error('semantic review result unresolved_decisions must be an array');
132
- }
133
- input.unresolved_decisions.forEach(validateReviewDecision);
134
- if (!Array.isArray(input.graph_coverage)) {
135
- throw new Error('semantic review result graph_coverage must be an array');
136
- }
137
- input.graph_coverage.forEach(validateGraphCoverage);
138
- if (!Array.isArray(input.reviewed_criteria)
139
- || input.reviewed_criteria.some((value) => typeof value !== 'string' || !/^R\d+\.\d+$/.test(value))
140
- || new Set(input.reviewed_criteria).size !== input.reviewed_criteria.length) {
141
- throw new Error('semantic review result reviewed_criteria must be unique RN.M strings');
142
- }
143
- if (!Array.isArray(input.counterexamples)) {
144
- throw new Error('semantic review result counterexamples must be an array');
145
- }
146
- input.counterexamples.forEach(validateCounterexample);
147
- validateReviewerEvidence(input.reviewer_evidence);
148
- return JSON.parse(JSON.stringify(input));
149
- }
150
-
151
- function blockingCount(review) {
152
- return review.findings.filter((finding) => finding.blocking).length
153
- + review.unresolved_decisions.filter((decision) => decision.blocking).length;
154
- }
155
-
156
- function buildCompletedReceipt(review, semanticDigest, recordedEntry) {
157
- return {
158
- status: 'completed',
159
- semantic_digest: semanticDigest,
160
- verdict: review.verdict,
161
- lifecycle_disposition: recordedEntry.lifecycle_disposition,
162
- findings: JSON.parse(JSON.stringify(review.findings)),
163
- unresolved_decisions: JSON.parse(JSON.stringify(review.unresolved_decisions)),
164
- graph_coverage: JSON.parse(JSON.stringify(review.graph_coverage)),
165
- repair_round: recordedEntry.attempt_index,
166
- reviewed_criteria: JSON.parse(JSON.stringify(review.reviewed_criteria)),
167
- counterexamples: JSON.parse(JSON.stringify(review.counterexamples)),
168
- reviewer_evidence: JSON.parse(JSON.stringify(review.reviewer_evidence)),
169
- };
170
- }
171
-
172
- function validateHistory(history) {
173
- if (!plain(history) || !exactKeys(history, HISTORY_FIELDS)) {
174
- throw new Error('spec.validation.semantic_review_history must contain exactly lineage_id and entries');
175
- }
176
- if (typeof history.lineage_id !== 'string' || !SHA256_RE.test(history.lineage_id)) {
177
- throw new Error('spec.validation.semantic_review_history.lineage_id must be a sha256 digest');
178
- }
179
- if (!Array.isArray(history.entries)) {
180
- throw new Error('spec.validation.semantic_review_history.entries must be an array');
181
- }
182
-
183
- const receipts = new Set();
184
- let priorPasses = 0;
185
- let failuresSincePass = 0;
186
- let blocked = false;
187
- for (const [index, entry] of history.entries.entries()) {
188
- if (!plain(entry) || !exactKeys(entry, HISTORY_ENTRY_FIELDS)) {
189
- throw new Error(`semantic review history entry ${index} fields diverge from the canonical entry`);
190
- }
191
- if (entry.sequence !== index) throw new Error(`semantic review history entry ${index} has a divergent sequence`);
192
- if (typeof entry.semantic_digest !== 'string' || !SHA256_RE.test(entry.semantic_digest)) {
193
- throw new Error(`semantic review history entry ${index} has an invalid semantic digest`);
194
- }
195
- if (typeof entry.review_receipt_digest !== 'string' || !SHA256_RE.test(entry.review_receipt_digest)) {
196
- throw new Error(`semantic review history entry ${index} has an invalid review receipt digest`);
197
- }
198
- if (receipts.has(entry.review_receipt_digest)) {
199
- throw new Error(`semantic review history entry ${index} duplicates a review receipt digest`);
200
- }
201
- receipts.add(entry.review_receipt_digest);
202
- if (entry.verdict !== 'PASS' && entry.verdict !== 'FAIL') {
203
- throw new Error(`semantic review history entry ${index} has an invalid verdict`);
204
- }
205
- if (entry.review_epoch !== priorPasses) {
206
- throw new Error(`semantic review history entry ${index} has a divergent review_epoch`);
207
- }
208
- if (entry.attempt_index !== failuresSincePass) {
209
- throw new Error(`semantic review history entry ${index} has a divergent attempt_index`);
210
- }
211
- if (entry.attempt_index > 2) {
212
- throw new Error(`semantic review history entry ${index} exceeds the terminal attempt limit`);
213
- }
214
- if (!Number.isInteger(entry.blocking_count) || entry.blocking_count < 0) {
215
- throw new Error(`semantic review history entry ${index} has an invalid blocking_count`);
216
- }
217
- const expectedDisposition = entry.verdict === 'PASS' || entry.attempt_index < 2
218
- ? 'CONTINUE' : 'BLOCKED';
219
- if (entry.lifecycle_disposition !== expectedDisposition) {
220
- throw new Error(`semantic review history entry ${index} has a divergent lifecycle_disposition`);
221
- }
222
- if (blocked) throw new Error('semantic review history contains an entry after BLOCKED');
223
- if (entry.verdict === 'PASS') {
224
- if (entry.blocking_count !== 0) throw new Error(`semantic review history entry ${index} PASS has blockers`);
225
- priorPasses += 1;
226
- failuresSincePass = 0;
227
- } else {
228
- failuresSincePass += 1;
229
- blocked = entry.lifecycle_disposition === 'BLOCKED';
230
- }
231
- }
232
- return { history: JSON.parse(JSON.stringify(history)), receipts, priorPasses, failuresSincePass, blocked };
233
- }
234
-
235
- function deriveLineageId(spec, semantic) {
236
- if (!plain(spec) || typeof spec.feature_name !== 'string' || spec.feature_name.length === 0) {
237
- throw new Error('spec.feature_name is required for semantic review lineage');
238
- }
239
- if (typeof spec.created_at !== 'string' || spec.created_at.length === 0) {
240
- throw new Error('spec.created_at is required for semantic review lineage');
241
- }
242
- return stableDigest({ feature_name: spec.feature_name, created_at: spec.created_at }, semantic);
243
- }
244
-
245
- function currentHistory(spec, semantic) {
246
- const lineageId = deriveLineageId(spec, semantic);
247
- const supplied = spec.validation?.semantic_review_history;
248
- if (supplied === undefined || supplied === null) {
249
- return {
250
- history: { lineage_id: lineageId, entries: [] },
251
- receipts: new Set(), priorPasses: 0, failuresSincePass: 0, blocked: false,
252
- };
253
- }
254
- const state = validateHistory(supplied);
255
- if (state.history.lineage_id !== lineageId) {
256
- throw new Error('spec.validation.semantic_review_history lineage_id diverges from spec identity');
257
- }
258
- return state;
259
- }
260
-
261
- function canonicalCheck({ directory, root, candidate, validator, grounder }) {
262
- const validation = validator.validateSpec(directory, candidate);
263
- if (validation.errors.length) throw new Error(`readiness validation failed (${validation.errors.join('; ')})`);
264
- const grounding = grounder({ specDir: directory, root, spec: candidate });
265
- if (grounding.errors.length) throw new Error(`readiness grounding failed (${grounding.errors.join('; ')})`);
266
- }
267
-
268
- function atomicReplace(file, bytes, io = fs) {
269
- const stat = io.lstatSync(file);
270
- if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('spec.json must be a regular non-symlink file');
271
- const original = io.readFileSync(file);
272
- const temporary = path.join(path.dirname(file), `.${path.basename(file)}.finalize-${process.pid}-${Date.now()}`);
273
- let descriptor;
274
- try {
275
- descriptor = io.openSync(temporary, 'wx', stat.mode);
276
- io.writeFileSync(descriptor, bytes);
277
- io.fsyncSync(descriptor);
278
- io.closeSync(descriptor);
279
- descriptor = undefined;
280
- if (process.env.CAFEKIT_FINALIZE_FAIL_BEFORE_RENAME === '1') throw new Error('injected finalization failure before rename');
281
- io.renameSync(temporary, file);
282
- } catch (error) {
283
- if (descriptor !== undefined) try { io.closeSync(descriptor); } catch {}
284
- try { io.unlinkSync(temporary); } catch {}
285
- if (!io.readFileSync(file).equals(original)) {
286
- const rollback = `${temporary}-rollback`;
287
- try {
288
- io.writeFileSync(rollback, original, { flag: 'wx', mode: stat.mode });
289
- io.renameSync(rollback, file);
290
- } finally { try { io.unlinkSync(rollback); } catch {} }
291
- }
292
- throw error;
293
- }
294
- }
295
-
296
- function finalizeReadiness({
297
- specDir,
298
- reviewResult,
299
- projectRoot,
300
- io = fs,
301
- validator = VALIDATOR,
302
- grounder = groundSpec,
303
- semantic = SEMANTIC,
304
- now = new Date().toISOString(),
305
- }) {
306
- const directory = io.realpathSync(path.resolve(specDir));
307
- const root = projectRoot ? io.realpathSync(path.resolve(projectRoot)) : canonicalProjectRoot(directory);
308
- const relative = path.relative(root, directory);
309
- if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
310
- throw new Error('spec directory escapes the canonical project root');
311
- }
312
- const specFile = path.join(directory, 'spec.json');
313
- const original = io.readFileSync(specFile);
314
- let spec;
315
- try { spec = JSON.parse(original); } catch (error) { throw new Error(`spec.json is invalid JSON (${error.message})`); }
316
- if (!plain(spec) || spec.schema_version !== '2.1') throw new Error('readiness finalization accepts canonical schema 2.1 only');
317
- const review = readReviewResult(reviewResult);
318
- const blockerCount = blockingCount(review);
319
- if (review.verdict === 'PASS' && blockerCount > 0) {
320
- throw new Error('semantic review PASS cannot contain blocking findings or unresolved decisions');
321
- }
322
- const state = currentHistory(spec, semantic);
323
- const projection = semantic.modelFromMarkdown(directory, spec);
324
- if (projection.errors.length) throw new Error(`semantic projection failed (${projection.errors.join('; ')})`);
325
-
326
- const candidate = JSON.parse(JSON.stringify(spec));
327
- candidate.semantic_model = projection.model;
328
- if (!plain(candidate.validation)) throw new Error('spec.validation must be an object');
329
- const authoringValidation = candidate.validation.authoring_validation;
330
- candidate.validation = {
331
- ...candidate.validation,
332
- status: 'completed',
333
- semantic_review_history: {
334
- lineage_id: state.history.lineage_id,
335
- entries: [...state.history.entries],
336
- },
337
- };
338
- if (JSON.stringify(candidate.validation.authoring_validation) !== JSON.stringify(authoringValidation)) {
339
- throw new Error('spec.validation.authoring_validation must be preserved');
340
- }
341
-
342
- candidate.ready_for_implementation = false;
343
- const digestResult = validator.computeSemanticDigest21(directory, candidate);
344
- if (digestResult.errors.length) throw new Error(`semantic digest failed (${digestResult.errors.join('; ')})`);
345
- for (const entry of state.history.entries) {
346
- const hypotheticalReceipt = buildCompletedReceipt(review, digestResult.digest, entry);
347
- const hypotheticalDigest = stableDigest(hypotheticalReceipt, semantic);
348
- if (entry.review_receipt_digest === hypotheticalDigest) {
349
- return {
350
- spec,
351
- semantic_digest: entry.semantic_digest,
352
- replayed: true,
353
- history_entry: entry,
354
- };
355
- }
356
- }
357
- if (state.blocked) throw new Error('readiness finalization refused: current review epoch already ends BLOCKED');
358
- const reviewEpoch = state.priorPasses;
359
- const attemptIndex = state.failuresSincePass;
360
- if (attemptIndex > 2) throw new Error('readiness finalization refused: no next attempt exists');
361
- const lifecycleDisposition = review.verdict === 'PASS' || attemptIndex < 2 ? 'CONTINUE' : 'BLOCKED';
362
- const receipt = buildCompletedReceipt(review, digestResult.digest, {
363
- attempt_index: attemptIndex,
364
- lifecycle_disposition: lifecycleDisposition,
365
- });
366
- if (!exactKeys(receipt, C2_RECEIPT_FIELDS)) {
367
- throw new Error('completed semantic review receipt fields diverge from C2');
368
- }
369
- candidate.validation.semantic_review = receipt;
370
- const reviewReceiptDigest = stableDigest(receipt, semantic);
371
- const historyEntry = {
372
- sequence: state.history.entries.length,
373
- semantic_digest: digestResult.digest,
374
- review_receipt_digest: reviewReceiptDigest,
375
- verdict: review.verdict,
376
- lifecycle_disposition: lifecycleDisposition,
377
- blocking_count: blockerCount,
378
- attempt_index: attemptIndex,
379
- review_epoch: reviewEpoch,
380
- };
381
- candidate.validation.semantic_review_history.entries.push(historyEntry);
382
- candidate.updated_at = now;
383
- canonicalCheck({ directory, root, candidate, validator, grounder });
384
-
385
- const ready = review.verdict === 'PASS'
386
- && lifecycleDisposition === 'CONTINUE'
387
- && blockerCount === 0
388
- && candidate.validation.semantic_review.semantic_digest === digestResult.digest;
389
- candidate.ready_for_implementation = ready;
390
- if (ready) {
391
- if (plain(candidate.timestamps)) {
392
- candidate.timestamps = { ...candidate.timestamps, validation_done: now };
393
- }
394
- canonicalCheck({ directory, root, candidate, validator, grounder });
395
- }
396
-
397
- const bytes = Buffer.from(`${JSON.stringify(candidate, null, 2)}\n`);
398
- atomicReplace(specFile, bytes, io);
399
- return { spec: candidate, semantic_digest: digestResult.digest, replayed: false, history_entry: historyEntry };
400
- }
401
-
402
- function usage() {
403
- process.stderr.write('Usage: node .claude/scripts/spec-readiness.cjs specs/<feature> --review-result <review.json>\n');
404
- }
405
-
406
- function main() {
407
- const args = process.argv.slice(2);
408
- const marker = args.indexOf('--review-result');
409
- if (args.length !== 3 || marker !== 1) { usage(); process.exitCode = 2; return; }
410
- try {
411
- const reviewFile = path.resolve(args[2]);
412
- const reviewStat = fs.lstatSync(reviewFile);
413
- if (!reviewStat.isFile() || reviewStat.isSymbolicLink()) throw new Error('review result must be a regular non-symlink JSON file');
414
- const reviewResult = JSON.parse(fs.readFileSync(reviewFile, 'utf8'));
415
- const result = finalizeReadiness({ specDir: args[0], reviewResult });
416
- process.stdout.write(`SPEC_READY ${args[0]} ${result.semantic_digest}\n`);
417
- } catch (error) {
418
- process.stderr.write(`SPEC_NOT_READY ${error.message}\n`);
419
- process.exitCode = 1;
420
- }
421
- }
422
-
423
- module.exports = { atomicReplace, finalizeReadiness, readReviewResult };
424
-
425
- if (require.main === module) main();