@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,396 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- const MODEL_FIELDS = ['version', 'criteria', 'anchors', 'design_records', 'verification_definitions'];
7
- const CRITERION_FIELDS = ['id', 'text', 'kind', 'owner'];
8
- const DESIGN_RECORD_FIELDS = ['id', 'kind', 'title', 'text'];
9
- const ANCHOR_FIELDS = ['id', 'type', 'target', 'role', 'access', 'action'];
10
- const VERIFICATION_FIELDS = [
11
- 'id', 'subject_criteria', 'subject_owner', 'proof_criteria', 'proof_owner',
12
- 'evidence_anchor', 'decision_refs', 'method', 'expected', 'negative', 'reachability',
13
- ];
14
- const ANCHOR_TYPES = new Set([
15
- 'file', 'symbol', 'command', 'route', 'schema', 'contract', 'artifact', 'external',
16
- ]);
17
- const TASK_FILE_RE = /^tasks\/task-(R\d+-\d{2})-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/;
18
- const CRITERION_RE = /^\s*[-*+]\s+\*\*(R\d+\.\d+):?\*\*\s*:?[ \t]+(.+)$/gim;
19
-
20
- function plain(value) {
21
- return Boolean(value && typeof value === 'object' && !Array.isArray(value));
22
- }
23
-
24
- function exactKeys(value, fields) {
25
- return plain(value)
26
- && JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...fields].sort());
27
- }
28
-
29
- function stableJson(value) {
30
- if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
31
- if (plain(value)) {
32
- return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
33
- }
34
- return JSON.stringify(value);
35
- }
36
-
37
- function concrete(value, minimum = 8) {
38
- return typeof value === 'string' && value === value.trim() && value.length >= minimum
39
- && !/[{}]|\b(?:tbd|todo|placeholder|works?|handles? it|expected behavior)\b/i.test(value);
40
- }
41
-
42
- function normalizeText(value) {
43
- return String(value || '').replace(/\s+/g, ' ').trim();
44
- }
45
-
46
- function uniqueStrings(value) {
47
- return Array.isArray(value) && value.length > 0
48
- && value.every((item) => typeof item === 'string' && item.trim() === item && item !== '')
49
- && new Set(value).size === value.length;
50
- }
51
-
52
- function extractSection(content, heading) {
53
- const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
54
- const match = String(content).match(new RegExp(`^##\\s+${escaped}\\s*$`, 'im'));
55
- if (!match) return null;
56
- const after = String(content).slice(match.index + match[0].length);
57
- const next = after.match(/^##\s+/m);
58
- return next ? after.slice(0, next.index) : after;
59
- }
60
-
61
- function parseTable(section, expectedHeaders) {
62
- if (typeof section !== 'string') return [];
63
- const lines = section.split('\n');
64
- for (let index = 0; index + 1 < lines.length; index += 1) {
65
- if (!/^\s*\|/.test(lines[index]) || !/^\s*\|(?:\s*:?-{3,}:?\s*\|)+\s*$/.test(lines[index + 1])) continue;
66
- const headers = lines[index].split('|').slice(1, -1).map((cell) => cell.trim().toLowerCase());
67
- if (JSON.stringify(headers) !== JSON.stringify(expectedHeaders.map((header) => header.toLowerCase()))) continue;
68
- const rows = [];
69
- for (let row = index + 2; row < lines.length && /^\s*\|/.test(lines[row]); row += 1) {
70
- const cells = lines[row].split('|').slice(1, -1)
71
- .map((cell) => cell.trim().replace(/^`|`$/g, '').trim());
72
- if (cells.some(Boolean)) rows.push(cells);
73
- }
74
- return rows;
75
- }
76
- return [];
77
- }
78
-
79
- function normalizeAccessAction(rawAccess, rawAction) {
80
- const access = String(rawAccess || '').trim().toLowerCase();
81
- const action = String(rawAction || '').trim().toLowerCase();
82
- let error = null;
83
- if (!['read', 'write'].includes(access)) error = 'Access must be read or write';
84
- else if (access === 'read' && action !== 'read') error = 'access read requires action read';
85
- else if (access === 'write' && !['create', 'modify', 'delete'].includes(action)) {
86
- error = 'access write requires action create, modify, or delete';
87
- }
88
- return { access, action, error };
89
- }
90
-
91
- function parseAnchors(content, { label, design }, errors) {
92
- const section = extractSection(content, design ? 'Typed Anchors' : 'Anchors and Ownership') || '';
93
- const rows = parseTable(section, ['ID', 'Type', 'Target', 'Role', 'Access', 'Action']);
94
- if (rows.length === 0 && !(design === false && /\bA-D-\d{2}\b/.test(section))) {
95
- errors.push(`${label}: typed anchor table is missing or empty`);
96
- }
97
- const taskId = label.match(TASK_FILE_RE)?.[1];
98
- return rows.map(([id = '', rawType = '', target = '', role = '', rawAccess = '', rawAction = '']) => {
99
- const type = rawType.toLowerCase();
100
- const { access, action, error } = normalizeAccessAction(rawAccess, rawAction);
101
- const namespace = design ? /^A-D-\d{2}$/ : new RegExp(`^A-${taskId || 'INVALID'}-\\d{2}$`);
102
- if (!namespace.test(id)) errors.push(`${label}: anchor ${id || '(missing id)'} has invalid namespace`);
103
- if (!ANCHOR_TYPES.has(type)) errors.push(`${label}: anchor ${id || '(missing id)'} has unknown Type ${rawType || '(empty)'}`);
104
- if (!concrete(target, 1) || /[{}*?\[\]]/.test(target)) errors.push(`${label}: anchor ${id || '(missing id)'} requires one exact target`);
105
- if (!concrete(role, 2)) errors.push(`${label}: anchor ${id || '(missing id)'} Role must be concrete`);
106
- if (error) errors.push(`${label}: anchor ${id || '(missing id)'} ${error}`);
107
- return { id, type, target, role: role.toLowerCase(), access, action };
108
- });
109
- }
110
-
111
- function parseList(value, pattern, label, errors) {
112
- const values = String(value).split(',').map((item) => item.trim().toUpperCase()).filter(Boolean);
113
- if (values.length === 0 || new Set(values).size !== values.length || values.some((item) => !pattern.test(item))) {
114
- errors.push(`design.md: ${label} must be a non-empty unique canonical list`);
115
- }
116
- return values;
117
- }
118
-
119
- function parseMethod(raw, errors) {
120
- const command = raw.match(/^command\s+`([^`]+)`$/i);
121
- if (command && concrete(command[1], 3)) return { kind: 'command', value: command[1] };
122
- const inspection = raw.match(/^inspection\s+`([^`]+)`\s+via\s+(A-(?:D|R\d+-\d{2})-\d{2})$/i);
123
- if (inspection && concrete(inspection[1], 1)) {
124
- return { kind: 'inspection', target: inspection[1], anchor_ref: inspection[2].toUpperCase() };
125
- }
126
- errors.push('design.md: Method must be exact command `...` or inspection `exact target` via one anchor');
127
- return { kind: 'invalid', value: raw };
128
- }
129
-
130
- function parseReachability(raw, errors) {
131
- const match = raw.match(/^entrypoint\s+`([^`]+)`\s+via\s+(.+?)\.?$/i);
132
- if (!match || !concrete(match[1], 1)) {
133
- errors.push('design.md: Reachability/grounding must be entrypoint `exact repository path` via anchor refs');
134
- return { entrypoint: '', anchor_refs: [] };
135
- }
136
- return {
137
- entrypoint: match[1],
138
- anchor_refs: parseList(match[2], /^A-(?:D|R\d+-\d{2})-\d{2}$/, 'reachability anchor refs', errors),
139
- };
140
- }
141
-
142
- function parseVerificationDefinitions(designText, errors) {
143
- const count = (String(designText).match(/^##\s+Verification Definitions\s*$/gim) || []).length;
144
- if (count !== 1) {
145
- errors.push('design.md: requires exactly one ## Verification Definitions section');
146
- return new Map();
147
- }
148
- const section = extractSection(designText, 'Verification Definitions') || '';
149
- const definitions = new Map();
150
- const candidates = section.split('\n').filter((line) => /^\s*[-*+]\s+\*\*V\d+(?:\.\d+)?\*\*/i.test(line));
151
- const grammar = /^\s*[-*+]\s+\*\*(V\d+(?:\.\d+)?)\*\*:\s*Criteria\s+(.+?)\s*;\s*Owner\s+([^;]+)\s*;(?:\s*Proof criteria\s+(.+?)\s*;\s*Proof owner\s+([^;]+)\s*;\s*Evidence anchor\s+([^;]+)\s*;)?\s*Decision refs\s+(.+?)\s*;\s*Method\s+(.+?)\s*;\s*Expected\s+(.+?)\s*;\s*Negative\/failure\s+(.+?)\s*;\s*Reachability\/grounding\s+(.+?)\s*\.?\s*$/i;
152
- // Read-only compatibility for persisted proof specs; new authoring uses the
153
- // base grammar above and its conditional proof extension.
154
- const legacyProofGrammar = /^\s*[-*+]\s+\*\*(V\d+(?:\.\d+)?)\*\*:\s*Subject criteria\s+(.+?)\s*;\s*Subject owner\s+([^;]+)\s*;\s*Proof criteria\s+(.+?)\s*;\s*Proof owner\s+([^;]+)\s*;\s*Evidence anchor\s+([^;]+)\s*;\s*Decision refs\s+(.+?)\s*;\s*Method\s+(.+?)\s*;\s*Expected\s+(.+?)\s*;\s*Negative\/failure\s+(.+?)\s*;\s*Reachability\/grounding\s+(.+?)\s*\.?\s*$/i;
155
- for (const line of candidates) {
156
- const match = line.match(grammar) || line.match(legacyProofGrammar);
157
- if (!match) {
158
- errors.push('design.md: V definition must use Criteria; Owner; optional Proof criteria; Proof owner; Evidence anchor; Decision refs; Method; Expected; Negative/failure; Reachability/grounding grammar');
159
- continue;
160
- }
161
- const id = match[1].toUpperCase();
162
- const subjectCriteria = parseList(match[2], /^R\d+\.\d+$/, `${id} subject criteria`, errors);
163
- const proofCriteria = match[4] ? parseList(match[4], /^R\d+\.\d+$/, `${id} proof criteria`, errors) : [];
164
- const definition = {
165
- id,
166
- subject_criteria: subjectCriteria,
167
- subject_owner: match[3].trim().toUpperCase(),
168
- proof_criteria: proofCriteria,
169
- proof_owner: match[5] ? match[5].trim().toUpperCase() : null,
170
- evidence_anchor: match[6] ? match[6].trim().toUpperCase() : null,
171
- decision_refs: parseList(match[7], /^[DIC]\d+(?:\.\d+)?$/, `${id} decision refs`, errors),
172
- method: parseMethod(match[8].trim(), errors),
173
- expected: match[9].trim(),
174
- negative: match[10].trim(),
175
- reachability: parseReachability(match[11].trim(), errors),
176
- };
177
- if (definitions.has(id)) errors.push(`design.md: duplicate canonical V definition ${id}`);
178
- else definitions.set(id, definition);
179
- }
180
- if (definitions.size === 0) errors.push('design.md: Verification Definitions requires at least one canonical V definition');
181
- return definitions;
182
- }
183
-
184
- function parseDesignRecords(designText, errors) {
185
- const records = [];
186
- const semantic = String(designText);
187
- const heading = /^#{3,6}\s+([DIC]\d+(?:\.\d+)?)\s+(?:—|-)\s+(.+)$/gim;
188
- const matches = [...semantic.matchAll(heading)];
189
- for (const [index, match] of matches.entries()) {
190
- const start = match.index + match[0].length;
191
- const relativeEnd = semantic.slice(start).search(/^#{1,6}\s+/m);
192
- const text = normalizeText(semantic.slice(start, relativeEnd < 0 ? semantic.length : start + relativeEnd));
193
- const id = match[1].toUpperCase();
194
- records.push({ id, kind: { D: 'decision', I: 'invariant', C: 'contract' }[id[0]], title: normalizeText(match[2]), text });
195
- }
196
- const ids = new Set();
197
- for (const record of records) {
198
- if (ids.has(record.id)) errors.push(`design.md: duplicate semantic record ${record.id}`);
199
- ids.add(record.id);
200
- if (!concrete(record.title, 3) || !concrete(record.text, 8)) errors.push(`design.md: ${record.id} requires concrete title and semantic text`);
201
- }
202
- return records.sort((a, b) => a.id.localeCompare(b.id));
203
- }
204
-
205
- function criterionIds(requirementsText) {
206
- return [...String(requirementsText).matchAll(CRITERION_RE)].map((match) => match[1].toUpperCase());
207
- }
208
-
209
- function taskFiles(specDir) {
210
- const directory = path.join(specDir, 'tasks');
211
- if (!fs.existsSync(directory)) return [];
212
- return fs.readdirSync(directory, { withFileTypes: true })
213
- .filter((entry) => entry.isFile() && TASK_FILE_RE.test(`tasks/${entry.name}`))
214
- .map((entry) => `tasks/${entry.name}`).sort();
215
- }
216
-
217
- function ownerKind(owner) {
218
- if (/^R\d+-\d{2}$/.test(owner)) return 'task';
219
- if (/^A-D-\d{2}$/.test(owner)) return 'design';
220
- return null;
221
- }
222
-
223
- function modelFromMarkdown(specDir, spec = {}) {
224
- const errors = [];
225
- let requirementsText = '';
226
- let designText = '';
227
- try { requirementsText = fs.readFileSync(path.join(specDir, 'requirements.md'), 'utf8'); }
228
- catch (error) { errors.push(`requirements.md: cannot be read (${error.message})`); }
229
- try { designText = fs.readFileSync(path.join(specDir, 'design.md'), 'utf8'); }
230
- catch (error) { errors.push(`design.md: cannot be read (${error.message})`); }
231
- const criterionEntries = [...String(requirementsText).matchAll(CRITERION_RE)]
232
- .map((match) => ({ id: match[1].toUpperCase(), text: normalizeText(match[2]) }));
233
- const criteria = criterionEntries.map((entry) => entry.id);
234
- if (criteria.length === 0) errors.push('requirements.md: semantic model requires a non-empty RN.M inventory');
235
- if (new Set(criteria).size !== criteria.length) errors.push('requirements.md: semantic model criterion IDs must be unique');
236
- const anchors = parseAnchors(designText, { label: 'design.md', design: true }, errors);
237
- const taskOwners = new Map();
238
- for (const taskFile of taskFiles(specDir)) {
239
- const content = fs.readFileSync(path.join(specDir, taskFile), 'utf8');
240
- anchors.push(...parseAnchors(content, { label: taskFile, design: false }, errors));
241
- const taskId = taskFile.match(TASK_FILE_RE)[1];
242
- const acceptance = extractSection(content, 'Acceptance') || '';
243
- for (const criterion of criterionIds(acceptance)) {
244
- if (!taskOwners.has(criterion)) taskOwners.set(criterion, []);
245
- taskOwners.get(criterion).push(taskId);
246
- }
247
- }
248
- const definitions = parseVerificationDefinitions(designText, errors);
249
- const criterionSemantics = new Map();
250
- for (const definition of definitions.values()) {
251
- const overlap = definition.subject_criteria.filter((id) => definition.proof_criteria.includes(id));
252
- if (overlap.length) errors.push(`design.md: ${definition.id} subject/proof criteria must be distinct (${overlap.join(', ')})`);
253
- for (const [kind, ids, owner] of [
254
- ['subject', definition.subject_criteria, definition.subject_owner],
255
- ['proof', definition.proof_criteria, definition.proof_owner],
256
- ]) for (const id of ids) {
257
- if (criterionSemantics.has(id)) errors.push(`design.md: criterion ${id} is assigned by multiple V definitions`);
258
- else criterionSemantics.set(id, { kind, owner });
259
- }
260
- }
261
- const taskful = taskFiles(specDir).length > 0;
262
- const modelCriteria = criteria.map((id) => {
263
- const semantic = criterionSemantics.get(id);
264
- if (!semantic) errors.push(`requirements.md:${id}: orphan criterion has no structured V ownership`);
265
- let owner = semantic?.owner || '';
266
- const claimed = taskOwners.get(id) || [];
267
- if (taskful) {
268
- if (claimed.length !== 1) errors.push(`requirements.md:${id}: requires exactly one task implementation owner`);
269
- else owner = claimed[0];
270
- if (semantic && claimed.length === 1 && semantic.owner !== claimed[0]) {
271
- errors.push(`design.md: ${id} V owner ${semantic.owner} differs from task Acceptance owner ${claimed[0]}`);
272
- }
273
- } else if (ownerKind(owner) !== 'design') {
274
- errors.push(`requirements.md:${id}: taskless owner must be one exact A-D design anchor`);
275
- }
276
- const text = criterionEntries.find((entry) => entry.id === id)?.text || '';
277
- if (!concrete(text, 8)) errors.push(`requirements.md:${id}: criterion text must be concrete`);
278
- return { id, text, kind: semantic?.kind || 'subject', owner };
279
- });
280
- for (const id of criterionSemantics.keys()) if (!criteria.includes(id)) {
281
- errors.push(`design.md: V definition references orphan criterion ${id}`);
282
- }
283
- const model = {
284
- version: '2',
285
- criteria: modelCriteria.sort((a, b) => a.id.localeCompare(b.id)),
286
- anchors: anchors.sort((a, b) => a.id.localeCompare(b.id)),
287
- design_records: parseDesignRecords(designText, errors),
288
- verification_definitions: [...definitions.values()].sort((a, b) => a.id.localeCompare(b.id)),
289
- };
290
- errors.push(...validateSemanticModel(model, spec));
291
- return { model, errors: [...new Set(errors)] };
292
- }
293
-
294
- function validateSemanticModel(model, spec = {}) {
295
- const errors = [];
296
- if (!exactKeys(model, MODEL_FIELDS)) return ['spec.json.semantic_model: fields must be exactly version, criteria, anchors, design_records, verification_definitions'];
297
- if (model.version !== '2') errors.push('spec.json.semantic_model.version: must be "2"');
298
- if (!Array.isArray(model.criteria) || model.criteria.length === 0) errors.push('spec.json.semantic_model.criteria: must be a non-empty array');
299
- if (!Array.isArray(model.anchors) || model.anchors.length === 0) errors.push('spec.json.semantic_model.anchors: must be a non-empty array');
300
- if (!Array.isArray(model.design_records) || model.design_records.length === 0) errors.push('spec.json.semantic_model.design_records: must preserve at least one D/I/C semantic record');
301
- if (!Array.isArray(model.verification_definitions) || model.verification_definitions.length === 0) errors.push('spec.json.semantic_model.verification_definitions: must be a non-empty array');
302
- const anchorIds = new Set();
303
- for (const [index, anchor] of (model.anchors || []).entries()) {
304
- const label = `spec.json.semantic_model.anchors[${index}]`;
305
- if (!exactKeys(anchor, ANCHOR_FIELDS)) { errors.push(`${label}: fields must be exactly ${ANCHOR_FIELDS.join(', ')}`); continue; }
306
- if (!/^A-(?:D|R\d+-\d{2})-\d{2}$/.test(anchor.id) || anchorIds.has(anchor.id)) errors.push(`${label}.id: must be globally unique canonical anchor ID`);
307
- anchorIds.add(anchor.id);
308
- if (!ANCHOR_TYPES.has(anchor.type)) errors.push(`${label}.type: unsupported anchor type`);
309
- if (!concrete(anchor.target, 1) || !concrete(anchor.role, 2)) errors.push(`${label}: target and role must be concrete`);
310
- const normalized = normalizeAccessAction(anchor.access, anchor.action);
311
- if (normalized.error) errors.push(`${label}: ${normalized.error}`);
312
- }
313
- const taskIds = new Set(Object.values(spec.task_registry || {}).map((entry) => entry?.id));
314
- const designRecordIds = new Set();
315
- for (const [index, record] of (model.design_records || []).entries()) {
316
- const label = `spec.json.semantic_model.design_records[${index}]`;
317
- if (!exactKeys(record, DESIGN_RECORD_FIELDS)) { errors.push(`${label}: fields must be exactly ${DESIGN_RECORD_FIELDS.join(', ')}`); continue; }
318
- if (!/^[DIC]\d+(?:\.\d+)?$/.test(record.id) || designRecordIds.has(record.id)) errors.push(`${label}.id: must be a unique D/I/C id`);
319
- designRecordIds.add(record.id);
320
- if (record.kind !== ({ D: 'decision', I: 'invariant', C: 'contract' })[record.id?.[0]]) errors.push(`${label}.kind: does not match id`);
321
- if (!concrete(record.title, 3) || !concrete(record.text, 8)) errors.push(`${label}: title and text must be concrete`);
322
- }
323
- const criterionIdsSeen = new Set();
324
- for (const [index, criterion] of (model.criteria || []).entries()) {
325
- const label = `spec.json.semantic_model.criteria[${index}]`;
326
- if (!exactKeys(criterion, CRITERION_FIELDS)) { errors.push(`${label}: fields must be exactly ${CRITERION_FIELDS.join(', ')}`); continue; }
327
- if (!/^R\d+\.\d+$/.test(criterion.id) || criterionIdsSeen.has(criterion.id)) errors.push(`${label}.id: must be unique RN.M`);
328
- criterionIdsSeen.add(criterion.id);
329
- if (!['subject', 'proof'].includes(criterion.kind)) errors.push(`${label}.kind: must be subject or proof`);
330
- if (!concrete(criterion.text, 8)) errors.push(`${label}.text: must preserve normalized product semantics`);
331
- const kind = ownerKind(criterion.owner);
332
- if (!kind || (kind === 'task' && !taskIds.has(criterion.owner)) || (kind === 'design' && !anchorIds.has(criterion.owner))) {
333
- errors.push(`${label}.owner: must resolve to one task ID or A-D anchor`);
334
- }
335
- }
336
- const verificationIds = new Set();
337
- const coveredCriteria = new Set();
338
- const criteriaById = new Map((model.criteria || []).map((criterion) => [criterion.id, criterion]));
339
- const anchorsById = new Map((model.anchors || []).map((anchor) => [anchor.id, anchor]));
340
- for (const [index, definition] of (model.verification_definitions || []).entries()) {
341
- const label = `spec.json.semantic_model.verification_definitions[${index}]`;
342
- if (!exactKeys(definition, VERIFICATION_FIELDS)) { errors.push(`${label}: fields must be exactly ${VERIFICATION_FIELDS.join(', ')}`); continue; }
343
- if (!/^V\d+(?:\.\d+)?$/.test(definition.id) || verificationIds.has(definition.id)) errors.push(`${label}.id: must be unique canonical V ID`);
344
- verificationIds.add(definition.id);
345
- const hasProof = Array.isArray(definition.proof_criteria) && definition.proof_criteria.length > 0;
346
- for (const [kind, values] of [['subject', definition.subject_criteria], ['proof', definition.proof_criteria]]) {
347
- if ((kind === 'subject' && !uniqueStrings(values)) || (kind === 'proof' && hasProof && !uniqueStrings(values)) || !Array.isArray(values) || values.some((id) => !criterionIdsSeen.has(id))) errors.push(`${label}.${kind}_criteria: must be a unique known RN.M list; Criteria is non-empty and Proof criteria is conditional`);
348
- for (const id of values || []) {
349
- if (coveredCriteria.has(id)) errors.push(`${label}: criterion ${id} is covered by multiple V definitions`);
350
- coveredCriteria.add(id);
351
- }
352
- }
353
- if ((definition.subject_criteria || []).some((id) => (definition.proof_criteria || []).includes(id))) errors.push(`${label}: subject/proof criteria must be distinct`);
354
- if (hasProof && definition.subject_owner === definition.proof_owner) errors.push(`${label}: subject/proof owners must be distinct`);
355
- for (const [field, owner] of [['subject_owner', definition.subject_owner], ...(hasProof ? [['proof_owner', definition.proof_owner]] : [])]) {
356
- const kind = ownerKind(owner);
357
- if (!kind || (kind === 'task' && !taskIds.has(owner)) || (kind === 'design' && !anchorIds.has(owner))) errors.push(`${label}.${field}: unresolved owner`);
358
- }
359
- if (hasProof && !anchorIds.has(definition.evidence_anchor)) errors.push(`${label}.evidence_anchor: must resolve to a canonical anchor`);
360
- else if (hasProof && ((ownerKind(definition.proof_owner) === 'task' && !definition.evidence_anchor.startsWith(`A-${definition.proof_owner}-`))
361
- || (ownerKind(definition.proof_owner) === 'design' && definition.evidence_anchor !== definition.proof_owner))) {
362
- errors.push(`${label}.evidence_anchor: must be owned by the proof owner`);
363
- }
364
- for (const [kind, ids, owner] of [
365
- ['subject', definition.subject_criteria, definition.subject_owner],
366
- ['proof', definition.proof_criteria, definition.proof_owner],
367
- ]) for (const id of ids || []) {
368
- const criterion = criteriaById.get(id);
369
- if (criterion && (criterion.kind !== kind || criterion.owner !== owner)) errors.push(`${label}: ${id} ${kind} ownership differs from criteria inventory`);
370
- }
371
- if (!uniqueStrings(definition.decision_refs) || definition.decision_refs.some((id) => !designRecordIds.has(id))) errors.push(`${label}.decision_refs: invalid or absent from semantic design records`);
372
- if (!plain(definition.method) || !['command', 'inspection'].includes(definition.method.kind)) errors.push(`${label}.method: invalid structured method`);
373
- else if (definition.method.kind === 'command' && (!exactKeys(definition.method, ['kind', 'value']) || !concrete(definition.method.value, 3))) errors.push(`${label}.method: command must be exact and concrete`);
374
- else if (definition.method.kind === 'inspection' && (!exactKeys(definition.method, ['kind', 'target', 'anchor_ref'])
375
- || !anchorIds.has(definition.method.anchor_ref) || anchorsById.get(definition.method.anchor_ref)?.target !== definition.method.target)) {
376
- errors.push(`${label}.method: inspection target must exactly match one anchor`);
377
- }
378
- if (!concrete(definition.expected, 12) || !concrete(definition.negative, 12)) errors.push(`${label}: Expected and negative must be concrete non-placeholder text`);
379
- if (!exactKeys(definition.reachability, ['entrypoint', 'anchor_refs']) || !concrete(definition.reachability?.entrypoint, 1)
380
- || !uniqueStrings(definition.reachability?.anchor_refs) || definition.reachability.anchor_refs.some((id) => !anchorIds.has(id))) {
381
- errors.push(`${label}.reachability: requires exact entrypoint and non-empty grounded anchor refs`);
382
- }
383
- }
384
- for (const criterion of model.criteria || []) if (!coveredCriteria.has(criterion.id)) errors.push(`spec.json.semantic_model.criteria: orphan criterion ${criterion.id}`);
385
- return errors;
386
- }
387
-
388
- module.exports = {
389
- MODEL_FIELDS,
390
- modelFromMarkdown,
391
- normalizeAccessAction,
392
- parseAnchors,
393
- parseVerificationDefinitions,
394
- stableJson,
395
- validateSemanticModel,
396
- };