@topogram/cli 0.3.34

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 (257) hide show
  1. package/ARCHITECTURE.md +67 -0
  2. package/CHANGELOG.md +240 -0
  3. package/README.md +223 -0
  4. package/package.json +51 -0
  5. package/src/adoption/index.js +3 -0
  6. package/src/adoption/plan.js +702 -0
  7. package/src/adoption/reporting.js +464 -0
  8. package/src/adoption/review-groups.js +313 -0
  9. package/src/agent-ops/query-builders.js +5012 -0
  10. package/src/archive/archive.js +141 -0
  11. package/src/archive/compact.js +26 -0
  12. package/src/archive/jsonl.js +70 -0
  13. package/src/archive/resolver-bridge.js +82 -0
  14. package/src/archive/schema.js +87 -0
  15. package/src/archive/unarchive.js +108 -0
  16. package/src/catalog.js +752 -0
  17. package/src/cli/catalog-alias.js +166 -0
  18. package/src/cli.js +9738 -0
  19. package/src/component-behavior.js +173 -0
  20. package/src/example-implementation.js +91 -0
  21. package/src/format.js +19 -0
  22. package/src/generator/adapters.d.ts +4 -0
  23. package/src/generator/adapters.js +325 -0
  24. package/src/generator/api.d.ts +1 -0
  25. package/src/generator/api.js +1196 -0
  26. package/src/generator/check.js +355 -0
  27. package/src/generator/component-conformance.js +767 -0
  28. package/src/generator/components.js +39 -0
  29. package/src/generator/context/bundle.js +291 -0
  30. package/src/generator/context/diff.js +256 -0
  31. package/src/generator/context/digest.js +182 -0
  32. package/src/generator/context/domain-coverage.js +94 -0
  33. package/src/generator/context/domain-page.js +137 -0
  34. package/src/generator/context/index.js +42 -0
  35. package/src/generator/context/report.js +121 -0
  36. package/src/generator/context/shared.js +1397 -0
  37. package/src/generator/context/slice.js +703 -0
  38. package/src/generator/context/task-mode.js +466 -0
  39. package/src/generator/docs.js +327 -0
  40. package/src/generator/index.js +161 -0
  41. package/src/generator/native/parity-bundle.js +311 -0
  42. package/src/generator/output.js +300 -0
  43. package/src/generator/registry.js +482 -0
  44. package/src/generator/runtime/app-bundle.js +456 -0
  45. package/src/generator/runtime/bundle-shared.js +166 -0
  46. package/src/generator/runtime/compile-check.js +163 -0
  47. package/src/generator/runtime/deployment.js +287 -0
  48. package/src/generator/runtime/environment.js +635 -0
  49. package/src/generator/runtime/index.js +32 -0
  50. package/src/generator/runtime/runtime-check.js +554 -0
  51. package/src/generator/runtime/shared.js +515 -0
  52. package/src/generator/runtime/smoke.js +219 -0
  53. package/src/generator/schema.js +204 -0
  54. package/src/generator/sdlc/board.js +66 -0
  55. package/src/generator/sdlc/doc-page.js +53 -0
  56. package/src/generator/sdlc/index.js +23 -0
  57. package/src/generator/sdlc/release-notes.js +62 -0
  58. package/src/generator/sdlc/traceability-matrix.js +65 -0
  59. package/src/generator/shared.js +29 -0
  60. package/src/generator/surfaces/contracts.js +146 -0
  61. package/src/generator/surfaces/databases/contract.js +40 -0
  62. package/src/generator/surfaces/databases/index.js +84 -0
  63. package/src/generator/surfaces/databases/lifecycle-shared.d.ts +1 -0
  64. package/src/generator/surfaces/databases/lifecycle-shared.js +612 -0
  65. package/src/generator/surfaces/databases/migration-plan.js +281 -0
  66. package/src/generator/surfaces/databases/postgres/capabilities.js +14 -0
  67. package/src/generator/surfaces/databases/postgres/drizzle.js +99 -0
  68. package/src/generator/surfaces/databases/postgres/index.js +9 -0
  69. package/src/generator/surfaces/databases/postgres/lifecycle.js +16 -0
  70. package/src/generator/surfaces/databases/postgres/prisma.js +159 -0
  71. package/src/generator/surfaces/databases/postgres/sql-migration.js +102 -0
  72. package/src/generator/surfaces/databases/postgres/sql-schema.js +34 -0
  73. package/src/generator/surfaces/databases/shared.d.ts +1 -0
  74. package/src/generator/surfaces/databases/shared.js +350 -0
  75. package/src/generator/surfaces/databases/snapshot.js +96 -0
  76. package/src/generator/surfaces/databases/sqlite/capabilities.js +14 -0
  77. package/src/generator/surfaces/databases/sqlite/index.js +8 -0
  78. package/src/generator/surfaces/databases/sqlite/lifecycle.js +16 -0
  79. package/src/generator/surfaces/databases/sqlite/prisma.js +143 -0
  80. package/src/generator/surfaces/databases/sqlite/sql-migration.js +65 -0
  81. package/src/generator/surfaces/databases/sqlite/sql-schema.js +27 -0
  82. package/src/generator/surfaces/index.js +25 -0
  83. package/src/generator/surfaces/native/swiftui-app.js +38 -0
  84. package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +20 -0
  85. package/src/generator/surfaces/native/swiftui-templates/README.generated.md +26 -0
  86. package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +682 -0
  87. package/src/generator/surfaces/native/swiftui-templates/runtime/TodoAPIClient.swift +156 -0
  88. package/src/generator/surfaces/native/swiftui-templates/runtime/TodoSwiftUIApp.swift +44 -0
  89. package/src/generator/surfaces/native/swiftui-templates/runtime/Visibility.swift +183 -0
  90. package/src/generator/surfaces/services/express.d.ts +1 -0
  91. package/src/generator/surfaces/services/express.js +766 -0
  92. package/src/generator/surfaces/services/hono.d.ts +1 -0
  93. package/src/generator/surfaces/services/hono.js +204 -0
  94. package/src/generator/surfaces/services/index.js +42 -0
  95. package/src/generator/surfaces/services/persistence-wiring.js +240 -0
  96. package/src/generator/surfaces/services/runtime-helpers.js +631 -0
  97. package/src/generator/surfaces/services/server-contract.js +80 -0
  98. package/src/generator/surfaces/services/stateless.d.ts +1 -0
  99. package/src/generator/surfaces/services/stateless.js +97 -0
  100. package/src/generator/surfaces/shared.js +64 -0
  101. package/src/generator/surfaces/web/api-client.js +1 -0
  102. package/src/generator/surfaces/web/forms.js +1 -0
  103. package/src/generator/surfaces/web/index.d.ts +2 -0
  104. package/src/generator/surfaces/web/index.js +53 -0
  105. package/src/generator/surfaces/web/react-components.js +248 -0
  106. package/src/generator/surfaces/web/react.js +538 -0
  107. package/src/generator/surfaces/web/routes.js +1 -0
  108. package/src/generator/surfaces/web/screens.js +1 -0
  109. package/src/generator/surfaces/web/shared.js +369 -0
  110. package/src/generator/surfaces/web/sveltekit-actions.js +28 -0
  111. package/src/generator/surfaces/web/sveltekit-components.js +234 -0
  112. package/src/generator/surfaces/web/sveltekit.js +426 -0
  113. package/src/generator/surfaces/web/ui-web-contract.js +65 -0
  114. package/src/generator/surfaces/web/vanilla.js +239 -0
  115. package/src/generator/verification.js +84 -0
  116. package/src/generator.js +1 -0
  117. package/src/import/core/context.js +52 -0
  118. package/src/import/core/contracts.js +23 -0
  119. package/src/import/core/registry.js +81 -0
  120. package/src/import/core/runner.js +646 -0
  121. package/src/import/core/shared.js +910 -0
  122. package/src/import/enrichers/auth-session.js +18 -0
  123. package/src/import/enrichers/django-rest.js +226 -0
  124. package/src/import/enrichers/doc-linking.js +20 -0
  125. package/src/import/enrichers/rails-controllers.js +246 -0
  126. package/src/import/enrichers/rails-models.js +130 -0
  127. package/src/import/enrichers/workflow-target-state.js +10 -0
  128. package/src/import/extractors/api/aspnet-core.js +304 -0
  129. package/src/import/extractors/api/django-routes.js +318 -0
  130. package/src/import/extractors/api/express.js +154 -0
  131. package/src/import/extractors/api/fastify.js +371 -0
  132. package/src/import/extractors/api/flutter-dio.js +135 -0
  133. package/src/import/extractors/api/generic-route-fallback.js +90 -0
  134. package/src/import/extractors/api/graphql-code-first.js +565 -0
  135. package/src/import/extractors/api/graphql-sdl.js +309 -0
  136. package/src/import/extractors/api/jaxrs.js +303 -0
  137. package/src/import/extractors/api/micronaut.js +213 -0
  138. package/src/import/extractors/api/next-route.js +50 -0
  139. package/src/import/extractors/api/next-server-action.js +51 -0
  140. package/src/import/extractors/api/nextauth.js +52 -0
  141. package/src/import/extractors/api/openapi-code.js +242 -0
  142. package/src/import/extractors/api/openapi.js +232 -0
  143. package/src/import/extractors/api/rails-routes.js +230 -0
  144. package/src/import/extractors/api/react-native-repository.js +128 -0
  145. package/src/import/extractors/api/retrofit.js +103 -0
  146. package/src/import/extractors/api/spring-web.js +372 -0
  147. package/src/import/extractors/api/swift-webapi.js +116 -0
  148. package/src/import/extractors/api/trpc.js +212 -0
  149. package/src/import/extractors/db/django-models.js +232 -0
  150. package/src/import/extractors/db/dotnet-models.js +93 -0
  151. package/src/import/extractors/db/drizzle.js +242 -0
  152. package/src/import/extractors/db/ef-core.js +221 -0
  153. package/src/import/extractors/db/flutter-entities.js +120 -0
  154. package/src/import/extractors/db/jpa.js +120 -0
  155. package/src/import/extractors/db/liquibase.js +180 -0
  156. package/src/import/extractors/db/mybatis-xml.js +145 -0
  157. package/src/import/extractors/db/prisma.js +185 -0
  158. package/src/import/extractors/db/rails-schema.js +175 -0
  159. package/src/import/extractors/db/react-native-entities.js +95 -0
  160. package/src/import/extractors/db/room.js +193 -0
  161. package/src/import/extractors/db/snapshot.js +112 -0
  162. package/src/import/extractors/db/sql.js +180 -0
  163. package/src/import/extractors/db/swiftdata.js +137 -0
  164. package/src/import/extractors/ui/android-compose.js +230 -0
  165. package/src/import/extractors/ui/backend-only.js +70 -0
  166. package/src/import/extractors/ui/blazor.js +227 -0
  167. package/src/import/extractors/ui/flutter-screens.js +152 -0
  168. package/src/import/extractors/ui/maui-xaml.js +135 -0
  169. package/src/import/extractors/ui/next-app-router.js +83 -0
  170. package/src/import/extractors/ui/next-pages-router.js +141 -0
  171. package/src/import/extractors/ui/razor-pages.js +181 -0
  172. package/src/import/extractors/ui/react-native-screens.js +166 -0
  173. package/src/import/extractors/ui/react-router.js +139 -0
  174. package/src/import/extractors/ui/sveltekit.js +123 -0
  175. package/src/import/extractors/ui/swiftui.js +193 -0
  176. package/src/import/extractors/ui/uikit.js +175 -0
  177. package/src/import/extractors/verification/generic.js +290 -0
  178. package/src/import/extractors/workflows/generic.js +137 -0
  179. package/src/import/index.js +7 -0
  180. package/src/import/provenance.js +158 -0
  181. package/src/new-project.js +2107 -0
  182. package/src/parser.js +439 -0
  183. package/src/policy/review-boundaries.js +165 -0
  184. package/src/project-config.js +535 -0
  185. package/src/proofs/backend-parity.js +19 -0
  186. package/src/proofs/contract-audit.js +220 -0
  187. package/src/proofs/ios-parity.js +7 -0
  188. package/src/proofs/issues-parity.js +10 -0
  189. package/src/proofs/web-parity.js +50 -0
  190. package/src/realization/api/build-api-realization.js +5 -0
  191. package/src/realization/api/index.js +1 -0
  192. package/src/realization/backend/build-backend-runtime-realization.js +82 -0
  193. package/src/realization/backend/index.d.ts +1 -0
  194. package/src/realization/backend/index.js +4 -0
  195. package/src/realization/db/build-db-realization.js +17 -0
  196. package/src/realization/db/index.js +3 -0
  197. package/src/realization/db/migration-plan.js +5 -0
  198. package/src/realization/db/snapshot.js +5 -0
  199. package/src/realization/ui/build-ui-shared-realization.js +305 -0
  200. package/src/realization/ui/build-web-realization.js +189 -0
  201. package/src/realization/ui/index.js +2 -0
  202. package/src/reconcile/docs.js +280 -0
  203. package/src/reconcile/index.js +3 -0
  204. package/src/reconcile/journeys.js +441 -0
  205. package/src/resolver/docs.js +1 -0
  206. package/src/resolver/enrich/acceptance-criterion.js +14 -0
  207. package/src/resolver/enrich/bug.js +12 -0
  208. package/src/resolver/enrich/component.js +2 -0
  209. package/src/resolver/enrich/index.js +1 -0
  210. package/src/resolver/enrich/pitch.js +18 -0
  211. package/src/resolver/enrich/requirement.js +20 -0
  212. package/src/resolver/enrich/task.js +16 -0
  213. package/src/resolver/expressions.js +1 -0
  214. package/src/resolver/index.js +2422 -0
  215. package/src/resolver/normalize.js +1 -0
  216. package/src/resolver.js +1 -0
  217. package/src/sdlc/adopt.js +65 -0
  218. package/src/sdlc/check.js +86 -0
  219. package/src/sdlc/dod/acceptance-criterion.js +22 -0
  220. package/src/sdlc/dod/bug.js +26 -0
  221. package/src/sdlc/dod/document.js +23 -0
  222. package/src/sdlc/dod/index.js +25 -0
  223. package/src/sdlc/dod/pitch.js +23 -0
  224. package/src/sdlc/dod/requirement.js +34 -0
  225. package/src/sdlc/dod/task.js +39 -0
  226. package/src/sdlc/explain.js +116 -0
  227. package/src/sdlc/history.js +80 -0
  228. package/src/sdlc/paths.js +11 -0
  229. package/src/sdlc/release.js +106 -0
  230. package/src/sdlc/scaffold.js +89 -0
  231. package/src/sdlc/status-filter.js +54 -0
  232. package/src/sdlc/transition.js +112 -0
  233. package/src/sdlc/transitions/acceptance-criterion.js +28 -0
  234. package/src/sdlc/transitions/bug.js +31 -0
  235. package/src/sdlc/transitions/document.js +29 -0
  236. package/src/sdlc/transitions/index.js +56 -0
  237. package/src/sdlc/transitions/pitch.js +34 -0
  238. package/src/sdlc/transitions/requirement.js +31 -0
  239. package/src/sdlc/transitions/task.js +34 -0
  240. package/src/template-trust.js +597 -0
  241. package/src/validator/expressions.js +1 -0
  242. package/src/validator/index.js +3424 -0
  243. package/src/validator/kinds.js +346 -0
  244. package/src/validator/per-kind/acceptance-criterion.js +91 -0
  245. package/src/validator/per-kind/bug.js +77 -0
  246. package/src/validator/per-kind/component.js +274 -0
  247. package/src/validator/per-kind/domain.js +205 -0
  248. package/src/validator/per-kind/pitch.js +101 -0
  249. package/src/validator/per-kind/requirement.js +75 -0
  250. package/src/validator/per-kind/task.js +96 -0
  251. package/src/validator/registry.js +1 -0
  252. package/src/validator/utils.js +12 -0
  253. package/src/validator.js +1 -0
  254. package/src/workflows.js +7597 -0
  255. package/src/workspace-docs.js +265 -0
  256. package/template-helpers/react.js +5 -0
  257. package/template-helpers/sveltekit.js +5 -0
@@ -0,0 +1,3424 @@
1
+ import {
2
+ DOC_ARRAY_FIELDS,
3
+ DOC_CONFIDENCE,
4
+ DOC_KINDS,
5
+ DOC_REFERENCE_FIELDS,
6
+ DOC_STATUSES
7
+ } from "../workspace-docs.js";
8
+
9
+ import {
10
+ STATEMENT_KINDS,
11
+ IDENTIFIER_PATTERN,
12
+ DOMAIN_IDENTIFIER_PATTERN,
13
+ DOMAIN_TAGGABLE_KINDS,
14
+ PITCH_IDENTIFIER_PATTERN,
15
+ REQUIREMENT_IDENTIFIER_PATTERN,
16
+ ACCEPTANCE_CRITERION_IDENTIFIER_PATTERN,
17
+ TASK_IDENTIFIER_PATTERN,
18
+ BUG_IDENTIFIER_PATTERN,
19
+ DOCUMENT_IDENTIFIER_PATTERN,
20
+ GLOBAL_STATUSES,
21
+ DECISION_STATUSES,
22
+ RULE_SEVERITIES,
23
+ VERIFICATION_METHODS,
24
+ STATUS_SETS_BY_KIND,
25
+ PITCH_STATUSES,
26
+ REQUIREMENT_STATUSES,
27
+ ACCEPTANCE_CRITERION_STATUSES,
28
+ TASK_STATUSES,
29
+ BUG_STATUSES,
30
+ PRIORITY_VALUES,
31
+ WORK_TYPES,
32
+ BUG_SEVERITIES,
33
+ DOC_TYPES,
34
+ AUDIENCES,
35
+ UI_SCREEN_KINDS,
36
+ UI_COLLECTION_PRESENTATIONS,
37
+ UI_NAVIGATION_PATTERNS,
38
+ UI_REGION_KINDS,
39
+ UI_PATTERN_KINDS,
40
+ FIELD_SPECS
41
+ } from "./kinds.js";
42
+ import { validateComponent } from "./per-kind/component.js";
43
+ import { validateDomain, validateDomainTag } from "./per-kind/domain.js";
44
+ import { validatePitch } from "./per-kind/pitch.js";
45
+ import { validateRequirement } from "./per-kind/requirement.js";
46
+ import { validateAcceptanceCriterion } from "./per-kind/acceptance-criterion.js";
47
+ import { validateTask } from "./per-kind/task.js";
48
+ import { validateBug } from "./per-kind/bug.js";
49
+
50
+ export {
51
+ STATEMENT_KINDS,
52
+ IDENTIFIER_PATTERN,
53
+ DOMAIN_IDENTIFIER_PATTERN,
54
+ DOMAIN_TAGGABLE_KINDS,
55
+ PITCH_IDENTIFIER_PATTERN,
56
+ REQUIREMENT_IDENTIFIER_PATTERN,
57
+ ACCEPTANCE_CRITERION_IDENTIFIER_PATTERN,
58
+ TASK_IDENTIFIER_PATTERN,
59
+ BUG_IDENTIFIER_PATTERN,
60
+ DOCUMENT_IDENTIFIER_PATTERN,
61
+ GLOBAL_STATUSES,
62
+ DECISION_STATUSES,
63
+ RULE_SEVERITIES,
64
+ VERIFICATION_METHODS,
65
+ STATUS_SETS_BY_KIND,
66
+ PITCH_STATUSES,
67
+ REQUIREMENT_STATUSES,
68
+ ACCEPTANCE_CRITERION_STATUSES,
69
+ TASK_STATUSES,
70
+ BUG_STATUSES,
71
+ PRIORITY_VALUES,
72
+ WORK_TYPES,
73
+ BUG_SEVERITIES,
74
+ DOC_TYPES,
75
+ AUDIENCES,
76
+ UI_SCREEN_KINDS,
77
+ UI_COLLECTION_PRESENTATIONS,
78
+ UI_NAVIGATION_PATTERNS,
79
+ UI_REGION_KINDS,
80
+ UI_PATTERN_KINDS,
81
+ FIELD_SPECS
82
+ } from "./kinds.js";
83
+
84
+ export function pushError(errors, message, loc) {
85
+ errors.push({
86
+ message,
87
+ loc
88
+ });
89
+ }
90
+
91
+ export function formatLoc(loc) {
92
+ const line = loc?.start?.line ?? 1;
93
+ const column = loc?.start?.column ?? 1;
94
+ const file = loc?.file ?? "<unknown>";
95
+ return `${file}:${line}:${column}`;
96
+ }
97
+
98
+ export function valueAsArray(value) {
99
+ if (!value) {
100
+ return [];
101
+ }
102
+ if (value.type === "list") {
103
+ return value.items;
104
+ }
105
+ if (value.type === "sequence") {
106
+ return value.items;
107
+ }
108
+ return [value];
109
+ }
110
+
111
+ export function symbolValues(value) {
112
+ return valueAsArray(value).filter((item) => item.type === "symbol").map((item) => item.value);
113
+ }
114
+
115
+ export function collectFieldMap(statement) {
116
+ const map = new Map();
117
+ for (const field of statement.fields) {
118
+ if (!map.has(field.key)) {
119
+ map.set(field.key, []);
120
+ }
121
+ map.get(field.key).push(field);
122
+ }
123
+ return map;
124
+ }
125
+
126
+ export function getField(statement, key) {
127
+ return collectFieldMap(statement).get(key)?.[0] || null;
128
+ }
129
+
130
+ export function getFieldValue(statement, key) {
131
+ return getField(statement, key)?.value || null;
132
+ }
133
+
134
+ export function stringValue(value) {
135
+ return value?.type === "string" ? value.value : null;
136
+ }
137
+
138
+ export function symbolValue(value) {
139
+ return value?.type === "symbol" ? value.value : null;
140
+ }
141
+
142
+ export function blockEntries(value) {
143
+ return value?.type === "block" ? value.entries : [];
144
+ }
145
+
146
+ function blockSymbolItems(entry) {
147
+ return entry.items.filter((item) => item.type === "symbol" || item.type === "string");
148
+ }
149
+
150
+ function statementFieldNames(statement) {
151
+ return blockEntries(getFieldValue(statement, "fields"))
152
+ .map((entry) => entry.items[0])
153
+ .filter((item) => item?.type === "symbol")
154
+ .map((item) => item.value);
155
+ }
156
+
157
+ function resolveShapeBaseFieldNames(statement, registry) {
158
+ const explicitFieldNames = statementFieldNames(statement);
159
+ if (explicitFieldNames.length > 0) {
160
+ return explicitFieldNames;
161
+ }
162
+
163
+ if (!statement.from) {
164
+ return [];
165
+ }
166
+
167
+ const source = registry.get(statement.from.value);
168
+ if (!source || source.kind !== "entity") {
169
+ return [];
170
+ }
171
+
172
+ const sourceFieldNames = statementFieldNames(source);
173
+ const includeNames = symbolValues(getFieldValue(statement, "include"));
174
+ const excludeNames = new Set(symbolValues(getFieldValue(statement, "exclude")));
175
+ const selectedNames = includeNames.length > 0 ? includeNames.filter((name) => sourceFieldNames.includes(name)) : sourceFieldNames;
176
+
177
+ return selectedNames.filter((fieldName) => !excludeNames.has(fieldName));
178
+ }
179
+
180
+ function ensureSingleValueField(errors, statement, fieldMap, key, expectedTypes) {
181
+ const fields = fieldMap.get(key) || [];
182
+ if (fields.length > 1) {
183
+ for (const field of fields.slice(1)) {
184
+ pushError(errors, `Duplicate field '${key}' on ${statement.kind} ${statement.id}`, field.loc);
185
+ }
186
+ }
187
+
188
+ const field = fields[0];
189
+ if (!field) {
190
+ return;
191
+ }
192
+
193
+ if (!expectedTypes.includes(field.value.type)) {
194
+ pushError(
195
+ errors,
196
+ `Field '${key}' on ${statement.kind} ${statement.id} must be ${expectedTypes.join(" or ")}, found ${field.value.type}`,
197
+ field.loc
198
+ );
199
+ }
200
+ }
201
+
202
+ function validateFieldPresence(errors, statement, fieldMap) {
203
+ const spec = FIELD_SPECS[statement.kind];
204
+ if (!spec) {
205
+ return;
206
+ }
207
+
208
+ for (const key of fieldMap.keys()) {
209
+ if (!spec.allowed.includes(key)) {
210
+ pushError(errors, `Field '${key}' is not allowed on ${statement.kind} ${statement.id}`, fieldMap.get(key)[0].loc);
211
+ }
212
+ }
213
+
214
+ for (const key of spec.required) {
215
+ if (!fieldMap.has(key)) {
216
+ pushError(errors, `Missing required field '${key}' on ${statement.kind} ${statement.id}`, statement.loc);
217
+ }
218
+ }
219
+ }
220
+
221
+ function validateBlockEntryLengths(errors, statement, fieldMap, key, minimumWidth) {
222
+ const field = fieldMap.get(key)?.[0];
223
+ if (!field || field.value.type !== "block") {
224
+ return;
225
+ }
226
+
227
+ for (const entry of field.value.entries) {
228
+ if (entry.items.length < minimumWidth) {
229
+ pushError(errors, `Each '${key}' entry on ${statement.kind} ${statement.id} must have at least ${minimumWidth} token(s)`, entry.loc);
230
+ }
231
+ }
232
+ }
233
+
234
+ function validateFieldShapes(errors, statement, fieldMap) {
235
+ ensureSingleValueField(errors, statement, fieldMap, "name", ["string"]);
236
+ ensureSingleValueField(errors, statement, fieldMap, "description", ["string"]);
237
+ ensureSingleValueField(errors, statement, fieldMap, "status", ["symbol"]);
238
+ ensureSingleValueField(errors, statement, fieldMap, "platform", ["symbol"]);
239
+ ensureSingleValueField(errors, statement, fieldMap, "method", ["symbol"]);
240
+ ensureSingleValueField(errors, statement, fieldMap, "severity", ["symbol"]);
241
+ ensureSingleValueField(errors, statement, fieldMap, "category", ["symbol"]);
242
+ ensureSingleValueField(errors, statement, fieldMap, "version", ["string"]);
243
+
244
+ for (const key of [
245
+ "aliases",
246
+ "excludes",
247
+ "uses_terms",
248
+ "include",
249
+ "exclude",
250
+ "derived_from",
251
+ "applies_to",
252
+ "actors",
253
+ "roles",
254
+ "reads",
255
+ "creates",
256
+ "updates",
257
+ "deletes",
258
+ "input",
259
+ "output",
260
+ "context",
261
+ "consequences",
262
+ "realizes",
263
+ "outputs",
264
+ "inputs",
265
+ "steps",
266
+ "validates",
267
+ "scenarios",
268
+ "observes",
269
+ "metrics",
270
+ "alerts",
271
+ "source_of_truth",
272
+ "behavior",
273
+ "patterns",
274
+ "regions",
275
+ "lookups",
276
+ "dependencies",
277
+ "approvals"
278
+ ]) {
279
+ ensureSingleValueField(errors, statement, fieldMap, key, ["list"]);
280
+ }
281
+
282
+ for (const key of ["fields", "props", "events", "slots", "behaviors", "keys", "relations", "invariants", "rename", "overrides", "http", "http_errors", "http_fields", "http_responses", "http_preconditions", "http_idempotency", "http_cache", "http_delete", "http_async", "http_status", "http_download", "http_authz", "http_callbacks", "ui_screens", "ui_collections", "ui_actions", "ui_visibility", "ui_lookups", "ui_routes", "ui_web", "ui_ios", "ui_app_shell", "ui_navigation", "ui_screen_regions", "ui_components", "db_tables", "db_columns", "db_keys", "db_indexes", "db_relations", "db_lifecycle", "generator_defaults"]) {
283
+ ensureSingleValueField(errors, statement, fieldMap, key, ["block"]);
284
+ }
285
+
286
+ validateBlockEntryLengths(errors, statement, fieldMap, "fields", 2);
287
+ validateBlockEntryLengths(errors, statement, fieldMap, "props", 3);
288
+ validateBlockEntryLengths(errors, statement, fieldMap, "events", 2);
289
+ validateBlockEntryLengths(errors, statement, fieldMap, "slots", 2);
290
+ validateBlockEntryLengths(errors, statement, fieldMap, "keys", 2);
291
+ validateBlockEntryLengths(errors, statement, fieldMap, "relations", 3);
292
+ validateBlockEntryLengths(errors, statement, fieldMap, "invariants", 2);
293
+ validateBlockEntryLengths(errors, statement, fieldMap, "http", 7);
294
+ validateBlockEntryLengths(errors, statement, fieldMap, "http_errors", 3);
295
+ validateBlockEntryLengths(errors, statement, fieldMap, "http_fields", 5);
296
+ validateBlockEntryLengths(errors, statement, fieldMap, "http_responses", 3);
297
+ validateBlockEntryLengths(errors, statement, fieldMap, "http_preconditions", 9);
298
+ validateBlockEntryLengths(errors, statement, fieldMap, "http_idempotency", 7);
299
+ validateBlockEntryLengths(errors, statement, fieldMap, "http_cache", 11);
300
+ validateBlockEntryLengths(errors, statement, fieldMap, "http_delete", 7);
301
+ validateBlockEntryLengths(errors, statement, fieldMap, "http_async", 11);
302
+ validateBlockEntryLengths(errors, statement, fieldMap, "http_status", 11);
303
+ validateBlockEntryLengths(errors, statement, fieldMap, "http_download", 7);
304
+ validateBlockEntryLengths(errors, statement, fieldMap, "http_authz", 3);
305
+ validateBlockEntryLengths(errors, statement, fieldMap, "http_callbacks", 11);
306
+ validateBlockEntryLengths(errors, statement, fieldMap, "ui_screens", 4);
307
+ validateBlockEntryLengths(errors, statement, fieldMap, "ui_collections", 4);
308
+ validateBlockEntryLengths(errors, statement, fieldMap, "ui_actions", 6);
309
+ validateBlockEntryLengths(errors, statement, fieldMap, "ui_visibility", 5);
310
+ validateBlockEntryLengths(errors, statement, fieldMap, "ui_lookups", 8);
311
+ validateBlockEntryLengths(errors, statement, fieldMap, "ui_routes", 4);
312
+ validateBlockEntryLengths(errors, statement, fieldMap, "ui_web", 4);
313
+ validateBlockEntryLengths(errors, statement, fieldMap, "ui_ios", 4);
314
+ validateBlockEntryLengths(errors, statement, fieldMap, "ui_app_shell", 2);
315
+ validateBlockEntryLengths(errors, statement, fieldMap, "ui_navigation", 2);
316
+ validateBlockEntryLengths(errors, statement, fieldMap, "ui_screen_regions", 4);
317
+ validateBlockEntryLengths(errors, statement, fieldMap, "db_tables", 3);
318
+ validateBlockEntryLengths(errors, statement, fieldMap, "db_columns", 5);
319
+ validateBlockEntryLengths(errors, statement, fieldMap, "db_keys", 3);
320
+ validateBlockEntryLengths(errors, statement, fieldMap, "db_indexes", 3);
321
+ validateBlockEntryLengths(errors, statement, fieldMap, "db_relations", 6);
322
+ validateBlockEntryLengths(errors, statement, fieldMap, "db_lifecycle", 3);
323
+ validateBlockEntryLengths(errors, statement, fieldMap, "generator_defaults", 2);
324
+ }
325
+
326
+ function validateStatus(errors, statement, fieldMap) {
327
+ const field = fieldMap.get("status")?.[0];
328
+ if (!field || field.value.type !== "symbol") {
329
+ return;
330
+ }
331
+
332
+ // Per-kind status table takes precedence (decision and SDLC kinds), with
333
+ // GLOBAL_STATUSES as the default.
334
+ const allowed = STATUS_SETS_BY_KIND[statement.kind] || GLOBAL_STATUSES;
335
+ if (!allowed.has(field.value.value)) {
336
+ pushError(errors, `Invalid status '${field.value.value}' on ${statement.kind} ${statement.id}`, field.loc);
337
+ }
338
+ }
339
+
340
+ function validateRuleSeverity(errors, statement, fieldMap) {
341
+ if (statement.kind !== "rule") {
342
+ return;
343
+ }
344
+
345
+ const field = fieldMap.get("severity")?.[0];
346
+ if (!field) {
347
+ return;
348
+ }
349
+
350
+ if (field.value.type === "symbol" && !RULE_SEVERITIES.has(field.value.value)) {
351
+ pushError(errors, `Invalid severity '${field.value.value}' on rule ${statement.id}`, field.loc);
352
+ }
353
+ }
354
+
355
+ function validateVerification(errors, statement, fieldMap) {
356
+ if (statement.kind !== "verification") {
357
+ return;
358
+ }
359
+
360
+ const methodField = fieldMap.get("method")?.[0];
361
+ if (methodField?.value.type === "symbol" && !VERIFICATION_METHODS.has(methodField.value.value)) {
362
+ pushError(
363
+ errors,
364
+ `Invalid verification method '${methodField.value.value}' on verification ${statement.id}`,
365
+ methodField.loc
366
+ );
367
+ }
368
+
369
+ const scenariosField = fieldMap.get("scenarios")?.[0];
370
+ if (!scenariosField || scenariosField.value.type !== "list") {
371
+ return;
372
+ }
373
+
374
+ if (scenariosField.value.items.length === 0) {
375
+ pushError(errors, `Verification ${statement.id} must include at least one scenario`, scenariosField.loc);
376
+ return;
377
+ }
378
+
379
+ for (const item of scenariosField.value.items) {
380
+ if (item.type !== "symbol" && item.type !== "string") {
381
+ pushError(errors, `Verification ${statement.id} scenarios must use symbols or strings`, item.loc);
382
+ }
383
+ }
384
+ }
385
+
386
+ function validateShapeFrom(errors, statement, registry) {
387
+ if (statement.kind !== "shape" || !statement.from) {
388
+ return;
389
+ }
390
+
391
+ const target = registry.get(statement.from.value);
392
+ if (!target) {
393
+ pushError(errors, `Shape ${statement.id} derives from missing statement '${statement.from.value}'`, statement.from.loc);
394
+ return;
395
+ }
396
+
397
+ if (target.kind !== "entity") {
398
+ pushError(errors, `Shape ${statement.id} can only derive from an entity, found ${target.kind} '${target.id}'`, statement.from.loc);
399
+ }
400
+ }
401
+
402
+ function validateReferenceKinds(errors, statement, fieldMap, registry) {
403
+ // Phase 2: SDLC kinds add several reference fields. The `affects` field is
404
+ // polymorphic — pitches/requirements/tasks/bugs all use it, so we keep the
405
+ // target set wide. `pitch` is single-id but lives in the same map for
406
+ // uniform validation.
407
+ const expectedByField = {
408
+ uses_terms: ["term"],
409
+ derived_from: ["entity"],
410
+ applies_to: ["capability"],
411
+ source_of_truth: ["decision"],
412
+ actors: ["actor"],
413
+ roles: ["role"],
414
+ reads: ["entity"],
415
+ creates: ["entity"],
416
+ updates: ["entity"],
417
+ deletes: ["entity"],
418
+ input: ["shape"],
419
+ output: ["shape"],
420
+ dependencies: [...STATEMENT_KINDS],
421
+ realizes: ["capability", "projection", "entity"],
422
+ validates: [...STATEMENT_KINDS],
423
+ observes: [...STATEMENT_KINDS],
424
+ inputs: [...STATEMENT_KINDS],
425
+ outputs: null,
426
+ steps: null,
427
+ scenarios: null,
428
+ metrics: null,
429
+ alerts: null,
430
+ aliases: null,
431
+ excludes: null,
432
+ include: null,
433
+ exclude: null,
434
+ context: null,
435
+ consequences: null,
436
+ pitch: ["pitch"],
437
+ requirement: null,
438
+ from_requirement: ["requirement"],
439
+ affects: ["capability", "entity", "rule", "projection", "component", "orchestration", "operation"],
440
+ introduces_rules: ["rule"],
441
+ respects_rules: ["rule"],
442
+ decisions: ["decision"],
443
+ introduces_decisions: ["decision"],
444
+ satisfies: ["requirement", "acceptance_criterion"],
445
+ acceptance_refs: ["acceptance_criterion"],
446
+ requirement_refs: ["requirement"],
447
+ fixes_bugs: ["bug"],
448
+ blocks: ["task"],
449
+ blocked_by: ["task"],
450
+ claimed_by: ["actor", "role"],
451
+ violates: ["rule"],
452
+ surfaces_rule: ["rule"],
453
+ introduced_in: ["task", "bug"],
454
+ fixed_in: ["task"],
455
+ fixed_in_verification: ["verification"],
456
+ supersedes: null,
457
+ modifies: [...STATEMENT_KINDS],
458
+ introduces: [...STATEMENT_KINDS],
459
+ removes: [...STATEMENT_KINDS]
460
+ };
461
+
462
+ for (const [key, allowedKinds] of Object.entries(expectedByField)) {
463
+ const field = fieldMap.get(key)?.[0];
464
+ if (!field || !allowedKinds) {
465
+ continue;
466
+ }
467
+
468
+ for (const item of valueAsArray(field.value)) {
469
+ if (item.type !== "symbol") {
470
+ pushError(errors, `Field '${key}' on ${statement.kind} ${statement.id} must only contain symbols`, item.loc);
471
+ continue;
472
+ }
473
+
474
+ const target = registry.get(item.value);
475
+ if (!target) {
476
+ pushError(errors, `Missing reference '${item.value}' in field '${key}' on ${statement.kind} ${statement.id}`, item.loc);
477
+ continue;
478
+ }
479
+
480
+ if (!allowedKinds.includes(target.kind)) {
481
+ pushError(
482
+ errors,
483
+ `Field '${key}' on ${statement.kind} ${statement.id} must reference ${allowedKinds.join(" or ")}, found ${target.kind} '${target.id}'`,
484
+ item.loc
485
+ );
486
+ }
487
+ }
488
+ }
489
+ }
490
+
491
+ function validateEntityRelations(errors, statement, fieldMap, registry) {
492
+ if (statement.kind !== "entity") {
493
+ return;
494
+ }
495
+
496
+ const field = fieldMap.get("relations")?.[0];
497
+ if (!field || field.value.type !== "block") {
498
+ return;
499
+ }
500
+
501
+ for (const entry of field.value.entries) {
502
+ const [left, operator, target] = entry.items;
503
+ if (!left || !operator || !target) {
504
+ continue;
505
+ }
506
+
507
+ if (left.type !== "symbol" || operator.type !== "symbol" || target.type !== "symbol") {
508
+ pushError(errors, `Relation entries on entity ${statement.id} must use symbols`, entry.loc);
509
+ continue;
510
+ }
511
+
512
+ if (operator.value !== "references") {
513
+ pushError(errors, `Relation entries on entity ${statement.id} must use 'references'`, operator.loc);
514
+ }
515
+
516
+ const [entityId] = target.value.split(".");
517
+ const related = registry.get(entityId);
518
+ if (!related) {
519
+ pushError(errors, `Relation on entity ${statement.id} references missing entity '${entityId}'`, target.loc);
520
+ continue;
521
+ }
522
+
523
+ if (related.kind !== "entity") {
524
+ pushError(errors, `Relation on entity ${statement.id} must target an entity, found ${related.kind} '${related.id}'`, target.loc);
525
+ }
526
+ }
527
+ }
528
+
529
+ function isIdentifierLike(token) {
530
+ return typeof token === "string" && token.length > 0;
531
+ }
532
+
533
+ function isComparator(token) {
534
+ return ["==", "!=", "<", "<=", ">", ">="].includes(token);
535
+ }
536
+
537
+ function validateInvariantEntry(errors, statement, entry) {
538
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
539
+ if (tokens.length < 2) {
540
+ pushError(errors, `Invariant on ${statement.kind} ${statement.id} is too short`, entry.loc);
541
+ return;
542
+ }
543
+
544
+ const [left, op, ...rest] = tokens;
545
+ if (!isIdentifierLike(left)) {
546
+ pushError(errors, `Invariant on ${statement.kind} ${statement.id} must start with a field or expression target`, entry.loc);
547
+ return;
548
+ }
549
+
550
+ if (op === "requires") {
551
+ if (rest.length < 3) {
552
+ pushError(errors, `Invariant '${tokens.join(" ")}' on ${statement.kind} ${statement.id} must be '<field> requires <field> <op> <value>'`, entry.loc);
553
+ } else if (!isComparator(rest[1])) {
554
+ pushError(errors, `Invariant '${tokens.join(" ")}' on ${statement.kind} ${statement.id} uses an invalid comparator '${rest[1]}'`, entry.loc);
555
+ }
556
+ return;
557
+ }
558
+
559
+ if (op === "length") {
560
+ if (rest.length !== 2 || !["<", "<=", ">", ">=", "=="].includes(rest[0])) {
561
+ pushError(errors, `Invariant '${tokens.join(" ")}' on ${statement.kind} ${statement.id} must be '<field> length <op> <number>'`, entry.loc);
562
+ }
563
+ return;
564
+ }
565
+
566
+ if (op === "format") {
567
+ if (rest.length !== 2 || rest[0] !== "==") {
568
+ pushError(errors, `Invariant '${tokens.join(" ")}' on ${statement.kind} ${statement.id} must be '<field> format == <format>'`, entry.loc);
569
+ }
570
+ return;
571
+ }
572
+
573
+ if (isComparator(op)) {
574
+ if (rest.length < 1) {
575
+ pushError(errors, `Invariant '${tokens.join(" ")}' on ${statement.kind} ${statement.id} is missing a right-hand value`, entry.loc);
576
+ return;
577
+ }
578
+
579
+ if (rest[1] === "implies") {
580
+ const [, , impliedField, impliedOperator, impliedValue] = rest;
581
+ if (!impliedField || !impliedOperator || !impliedValue) {
582
+ pushError(errors, `Invariant '${tokens.join(" ")}' on ${statement.kind} ${statement.id} must fully specify the implied clause`, entry.loc);
583
+ } else if (!(impliedOperator === "is" || isComparator(impliedOperator))) {
584
+ pushError(errors, `Invariant '${tokens.join(" ")}' on ${statement.kind} ${statement.id} has invalid implied operator '${impliedOperator}'`, entry.loc);
585
+ }
586
+ return;
587
+ }
588
+
589
+ return;
590
+ }
591
+
592
+ pushError(errors, `Invariant '${tokens.join(" ")}' on ${statement.kind} ${statement.id} uses unsupported form`, entry.loc);
593
+ }
594
+
595
+ function validateRuleExpressionValue(errors, statement, field, label) {
596
+ if (!field) {
597
+ return;
598
+ }
599
+
600
+ const items = valueAsArray(field.value);
601
+ if (items.length !== 1) {
602
+ pushError(errors, `Field '${label}' on rule ${statement.id} must contain a single expression`, field.loc);
603
+ return;
604
+ }
605
+
606
+ const item = items[0];
607
+ if (item.type !== "string" && item.type !== "symbol") {
608
+ pushError(errors, `Field '${label}' on rule ${statement.id} must be a string or symbol expression`, field.loc);
609
+ return;
610
+ }
611
+
612
+ const text = item.value.trim();
613
+ if (text.length === 0) {
614
+ pushError(errors, `Field '${label}' on rule ${statement.id} must not be empty`, field.loc);
615
+ return;
616
+ }
617
+
618
+ if (label === "requirement" || label === "condition") {
619
+ if (!/(==|!=|<=|>=|<|>)/.test(text)) {
620
+ pushError(errors, `Field '${label}' on rule ${statement.id} must include a comparison operator`, field.loc);
621
+ }
622
+ }
623
+ }
624
+
625
+ function validateExpressions(errors, statement, fieldMap) {
626
+ if (statement.kind === "entity") {
627
+ const invariantsField = fieldMap.get("invariants")?.[0];
628
+ if (invariantsField?.value.type === "block") {
629
+ for (const entry of invariantsField.value.entries) {
630
+ validateInvariantEntry(errors, statement, entry);
631
+ }
632
+ }
633
+ }
634
+
635
+ if (statement.kind === "rule") {
636
+ validateRuleExpressionValue(errors, statement, fieldMap.get("condition")?.[0], "condition");
637
+ validateRuleExpressionValue(errors, statement, fieldMap.get("requirement")?.[0], "requirement");
638
+ }
639
+ }
640
+
641
+ function validateShapeTransforms(errors, statement, fieldMap, registry) {
642
+ if (statement.kind !== "shape") {
643
+ return;
644
+ }
645
+
646
+ const baseFieldNames = resolveShapeBaseFieldNames(statement, registry);
647
+ const baseFieldSet = new Set(baseFieldNames);
648
+ const source = statement.from ? registry.get(statement.from.value) : null;
649
+ const sourceFieldSet = new Set(source ? statementFieldNames(source) : []);
650
+ const includeField = fieldMap.get("include")?.[0];
651
+ const excludeField = fieldMap.get("exclude")?.[0];
652
+
653
+ for (const [fieldKey, field] of [
654
+ ["include", includeField],
655
+ ["exclude", excludeField]
656
+ ]) {
657
+ if (!field) {
658
+ continue;
659
+ }
660
+
661
+ for (const item of valueAsArray(field.value)) {
662
+ if (item.type !== "symbol") {
663
+ continue;
664
+ }
665
+
666
+ if (statement.from && !sourceFieldSet.has(item.value) && fieldKey === "include") {
667
+ pushError(errors, `Shape ${statement.id} includes unknown field '${item.value}' from ${statement.from.value}`, item.loc);
668
+ }
669
+
670
+ if (statement.from && fieldKey === "exclude") {
671
+ if (!sourceFieldSet.has(item.value)) {
672
+ pushError(errors, `Shape ${statement.id} excludes unknown field '${item.value}' from ${statement.from.value}`, item.loc);
673
+ }
674
+ }
675
+ }
676
+ }
677
+
678
+ const renameEntries = blockEntries(getFieldValue(statement, "rename"));
679
+ const renameFrom = new Map();
680
+ const renameTo = new Map();
681
+
682
+ for (const entry of renameEntries) {
683
+ const items = blockSymbolItems(entry);
684
+ if (items.length !== 2) {
685
+ pushError(errors, `Each 'rename' entry on shape ${statement.id} must be exactly '<from> <to>'`, entry.loc);
686
+ continue;
687
+ }
688
+
689
+ const [fromItem, toItem] = items;
690
+ if (!baseFieldSet.has(fromItem.value)) {
691
+ pushError(errors, `Shape ${statement.id} renames unknown field '${fromItem.value}'`, fromItem.loc);
692
+ }
693
+
694
+ if (renameFrom.has(fromItem.value)) {
695
+ pushError(errors, `Shape ${statement.id} renames field '${fromItem.value}' more than once`, fromItem.loc);
696
+ } else {
697
+ renameFrom.set(fromItem.value, toItem.value);
698
+ }
699
+
700
+ if (renameTo.has(toItem.value)) {
701
+ pushError(errors, `Shape ${statement.id} renames multiple fields to '${toItem.value}'`, toItem.loc);
702
+ } else {
703
+ renameTo.set(toItem.value, fromItem.value);
704
+ }
705
+ }
706
+
707
+ const finalFieldNames = baseFieldNames.map((fieldName) => renameFrom.get(fieldName) || fieldName);
708
+ const finalFieldSet = new Set();
709
+ for (const fieldName of finalFieldNames) {
710
+ if (finalFieldSet.has(fieldName)) {
711
+ pushError(errors, `Shape ${statement.id} produces duplicate projected field '${fieldName}'`, statement.loc);
712
+ continue;
713
+ }
714
+ finalFieldSet.add(fieldName);
715
+ }
716
+
717
+ const sourceNameSet = new Set(baseFieldNames);
718
+ const overrideEntries = blockEntries(getFieldValue(statement, "overrides"));
719
+ const seenOverrides = new Set();
720
+
721
+ for (const entry of overrideEntries) {
722
+ const items = blockSymbolItems(entry);
723
+ if (items.length < 2) {
724
+ pushError(errors, `Each 'overrides' entry on shape ${statement.id} must include a field and at least one override`, entry.loc);
725
+ continue;
726
+ }
727
+
728
+ const [fieldItem, ...rest] = items;
729
+ if (!finalFieldSet.has(fieldItem.value) && !sourceNameSet.has(fieldItem.value)) {
730
+ pushError(errors, `Shape ${statement.id} overrides unknown field '${fieldItem.value}'`, fieldItem.loc);
731
+ }
732
+
733
+ if (seenOverrides.has(fieldItem.value)) {
734
+ pushError(errors, `Shape ${statement.id} overrides field '${fieldItem.value}' more than once`, fieldItem.loc);
735
+ } else {
736
+ seenOverrides.add(fieldItem.value);
737
+ }
738
+
739
+ let sawChange = false;
740
+ for (let i = 0; i < rest.length; i += 1) {
741
+ const token = rest[i];
742
+ if (token.value === "required" || token.value === "optional") {
743
+ sawChange = true;
744
+ continue;
745
+ }
746
+
747
+ if (token.value === "type") {
748
+ sawChange = true;
749
+ if (!rest[i + 1]) {
750
+ pushError(errors, `Shape ${statement.id} override for '${fieldItem.value}' is missing a type value`, token.loc);
751
+ } else {
752
+ i += 1;
753
+ }
754
+ continue;
755
+ }
756
+
757
+ if (token.value === "default") {
758
+ sawChange = true;
759
+ if (!rest[i + 1]) {
760
+ pushError(errors, `Shape ${statement.id} override for '${fieldItem.value}' is missing a default value`, token.loc);
761
+ } else {
762
+ i += 1;
763
+ }
764
+ continue;
765
+ }
766
+
767
+ pushError(errors, `Shape ${statement.id} override for '${fieldItem.value}' has unknown directive '${token.value}'`, token.loc);
768
+ }
769
+
770
+ if (!sawChange) {
771
+ pushError(errors, `Shape ${statement.id} override for '${fieldItem.value}' must specify at least one valid directive`, entry.loc);
772
+ }
773
+ }
774
+ }
775
+
776
+ function validateProjectionHttp(errors, statement, fieldMap, registry) {
777
+ if (statement.kind !== "projection") {
778
+ return;
779
+ }
780
+
781
+ const httpField = fieldMap.get("http")?.[0];
782
+ if (!httpField || httpField.value.type !== "block") {
783
+ return;
784
+ }
785
+
786
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
787
+
788
+ for (const entry of httpField.value.entries) {
789
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
790
+ const capabilityId = tokens[0];
791
+ if (!capabilityId) {
792
+ continue;
793
+ }
794
+
795
+ const target = registry.get(capabilityId);
796
+ if (!target) {
797
+ pushError(errors, `Projection ${statement.id} http metadata references missing capability '${capabilityId}'`, entry.loc);
798
+ continue;
799
+ }
800
+ if (target.kind !== "capability") {
801
+ pushError(errors, `Projection ${statement.id} http metadata must target a capability, found ${target.kind} '${target.id}'`, entry.loc);
802
+ }
803
+ if (!realized.has(capabilityId)) {
804
+ pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
805
+ }
806
+
807
+ const directives = new Map();
808
+ for (let i = 1; i < tokens.length; i += 2) {
809
+ const key = tokens[i];
810
+ const value = tokens[i + 1];
811
+ if (!value) {
812
+ pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
813
+ continue;
814
+ }
815
+ directives.set(key, value);
816
+ }
817
+
818
+ for (const requiredKey of ["method", "path", "success"]) {
819
+ if (!directives.has(requiredKey)) {
820
+ pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
821
+ }
822
+ }
823
+
824
+ for (const key of directives.keys()) {
825
+ if (!["method", "path", "success", "auth", "request"].includes(key)) {
826
+ pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
827
+ }
828
+ }
829
+
830
+ const method = directives.get("method");
831
+ if (method && !["GET", "POST", "PUT", "PATCH", "DELETE"].includes(method)) {
832
+ pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' has invalid method '${method}'`, entry.loc);
833
+ }
834
+
835
+ const path = directives.get("path");
836
+ if (path && !path.startsWith("/")) {
837
+ pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' must use an absolute path`, entry.loc);
838
+ }
839
+
840
+ const success = directives.get("success");
841
+ if (success && !/^\d{3}$/.test(success)) {
842
+ pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' must use a 3-digit success status`, entry.loc);
843
+ }
844
+
845
+ const auth = directives.get("auth");
846
+ if (auth && !["none", "user", "manager", "admin"].includes(auth)) {
847
+ pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' has invalid auth mode '${auth}'`, entry.loc);
848
+ }
849
+
850
+ const request = directives.get("request");
851
+ if (request && !["body", "query", "path", "none"].includes(request)) {
852
+ pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' has invalid request placement '${request}'`, entry.loc);
853
+ }
854
+ }
855
+ }
856
+
857
+ function validateProjectionHttpErrors(errors, statement, fieldMap, registry) {
858
+ if (statement.kind !== "projection") {
859
+ return;
860
+ }
861
+
862
+ const httpErrorsField = fieldMap.get("http_errors")?.[0];
863
+ if (!httpErrorsField || httpErrorsField.value.type !== "block") {
864
+ return;
865
+ }
866
+
867
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
868
+ for (const entry of httpErrorsField.value.entries) {
869
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
870
+ const [capabilityId, errorCode, status] = tokens;
871
+
872
+ const target = registry.get(capabilityId);
873
+ if (!target) {
874
+ pushError(errors, `Projection ${statement.id} http_errors references missing capability '${capabilityId}'`, entry.loc);
875
+ continue;
876
+ }
877
+ if (target.kind !== "capability") {
878
+ pushError(errors, `Projection ${statement.id} http_errors must target a capability, found ${target.kind} '${target.id}'`, entry.loc);
879
+ }
880
+ if (!realized.has(capabilityId)) {
881
+ pushError(errors, `Projection ${statement.id} http_errors for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
882
+ }
883
+ if (!/^\d{3}$/.test(status || "")) {
884
+ pushError(errors, `Projection ${statement.id} http_errors for '${capabilityId}' must use a 3-digit status`, entry.loc);
885
+ }
886
+ if (!errorCode) {
887
+ pushError(errors, `Projection ${statement.id} http_errors for '${capabilityId}' must include an error code`, entry.loc);
888
+ }
889
+ }
890
+ }
891
+
892
+ function resolveCapabilityContractFields(registry, capabilityId, direction) {
893
+ const capability = registry.get(capabilityId);
894
+ if (!capability || capability.kind !== "capability") {
895
+ return new Set();
896
+ }
897
+
898
+ const refsField = direction === "input" ? getFieldValue(capability, "input") : getFieldValue(capability, "output");
899
+ const shapeId = symbolValues(refsField)[0];
900
+ if (!shapeId) {
901
+ return new Set();
902
+ }
903
+
904
+ const shape = registry.get(shapeId);
905
+ if (!shape || shape.kind !== "shape") {
906
+ return new Set();
907
+ }
908
+
909
+ const explicitFields = statementFieldNames(shape);
910
+ if (explicitFields.length > 0) {
911
+ return new Set(explicitFields);
912
+ }
913
+
914
+ return new Set(resolveShapeBaseFieldNames(shape, registry));
915
+ }
916
+
917
+ function validateProjectionHttpFields(errors, statement, fieldMap, registry) {
918
+ if (statement.kind !== "projection") {
919
+ return;
920
+ }
921
+
922
+ const httpFieldsField = fieldMap.get("http_fields")?.[0];
923
+ if (!httpFieldsField || httpFieldsField.value.type !== "block") {
924
+ return;
925
+ }
926
+
927
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
928
+ for (const entry of httpFieldsField.value.entries) {
929
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
930
+ const [capabilityId, direction, fieldName, keywordIn, location, maybeAs, maybeWireName] = tokens;
931
+
932
+ const capability = registry.get(capabilityId);
933
+ if (!capability) {
934
+ pushError(errors, `Projection ${statement.id} http_fields references missing capability '${capabilityId}'`, entry.loc);
935
+ continue;
936
+ }
937
+ if (capability.kind !== "capability") {
938
+ pushError(errors, `Projection ${statement.id} http_fields must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
939
+ }
940
+ if (!realized.has(capabilityId)) {
941
+ pushError(errors, `Projection ${statement.id} http_fields for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
942
+ }
943
+ if (!["input", "output"].includes(direction)) {
944
+ pushError(errors, `Projection ${statement.id} http_fields for '${capabilityId}' has invalid direction '${direction}'`, entry.loc);
945
+ }
946
+ if (keywordIn !== "in") {
947
+ pushError(errors, `Projection ${statement.id} http_fields for '${capabilityId}' must use 'in' before the location`, entry.loc);
948
+ }
949
+ if (!["path", "query", "header", "body"].includes(location)) {
950
+ pushError(errors, `Projection ${statement.id} http_fields for '${capabilityId}' has invalid location '${location}'`, entry.loc);
951
+ }
952
+ if (maybeAs && maybeAs !== "as") {
953
+ pushError(errors, `Projection ${statement.id} http_fields for '${capabilityId}' has unexpected token '${maybeAs}'`, entry.loc);
954
+ }
955
+ if (maybeAs === "as" && !maybeWireName) {
956
+ pushError(errors, `Projection ${statement.id} http_fields for '${capabilityId}' must provide a wire name after 'as'`, entry.loc);
957
+ }
958
+
959
+ const availableFields = resolveCapabilityContractFields(registry, capabilityId, direction);
960
+ if (fieldName && availableFields.size > 0 && !availableFields.has(fieldName)) {
961
+ pushError(errors, `Projection ${statement.id} http_fields references unknown ${direction} field '${fieldName}' on ${capabilityId}`, entry.loc);
962
+ }
963
+ }
964
+ }
965
+
966
+ function validateProjectionHttpResponses(errors, statement, fieldMap, registry) {
967
+ if (statement.kind !== "projection") {
968
+ return;
969
+ }
970
+
971
+ const httpResponsesField = fieldMap.get("http_responses")?.[0];
972
+ if (!httpResponsesField || httpResponsesField.value.type !== "block") {
973
+ return;
974
+ }
975
+
976
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
977
+ for (const entry of httpResponsesField.value.entries) {
978
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
979
+ const capabilityId = tokens[0];
980
+ const capability = registry.get(capabilityId);
981
+
982
+ if (!capability) {
983
+ pushError(errors, `Projection ${statement.id} http_responses references missing capability '${capabilityId}'`, entry.loc);
984
+ continue;
985
+ }
986
+ if (capability.kind !== "capability") {
987
+ pushError(errors, `Projection ${statement.id} http_responses must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
988
+ }
989
+ if (!realized.has(capabilityId)) {
990
+ pushError(errors, `Projection ${statement.id} http_responses for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
991
+ }
992
+
993
+ const directives = parseProjectionHttpResponsesDirectives(tokens.slice(1));
994
+ for (const message of directives.errors) {
995
+ pushError(errors, `Projection ${statement.id} http_responses for '${capabilityId}' ${message}`, entry.loc);
996
+ }
997
+
998
+ if (!directives.mode) {
999
+ pushError(errors, `Projection ${statement.id} http_responses for '${capabilityId}' must include 'mode'`, entry.loc);
1000
+ }
1001
+
1002
+ const mode = directives.mode;
1003
+ if (mode && !["item", "collection", "paged", "cursor"].includes(mode)) {
1004
+ pushError(errors, `Projection ${statement.id} http_responses for '${capabilityId}' has invalid mode '${mode}'`, entry.loc);
1005
+ }
1006
+
1007
+ const itemShapeId = directives.item;
1008
+ if (mode && mode !== "item" && !itemShapeId) {
1009
+ pushError(errors, `Projection ${statement.id} http_responses for '${capabilityId}' must include 'item' for mode '${mode}'`, entry.loc);
1010
+ }
1011
+ if (itemShapeId) {
1012
+ const itemShape = registry.get(itemShapeId);
1013
+ if (!itemShape) {
1014
+ pushError(errors, `Projection ${statement.id} http_responses for '${capabilityId}' references missing shape '${itemShapeId}'`, entry.loc);
1015
+ } else if (itemShape.kind !== "shape") {
1016
+ pushError(errors, `Projection ${statement.id} http_responses for '${capabilityId}' must reference a shape for 'item', found ${itemShape.kind} '${itemShape.id}'`, entry.loc);
1017
+ }
1018
+ }
1019
+
1020
+ if (mode === "cursor") {
1021
+ if (!directives.cursor?.requestAfter) {
1022
+ pushError(errors, `Projection ${statement.id} http_responses for '${capabilityId}' must include 'cursor request_after <field>'`, entry.loc);
1023
+ }
1024
+ if (!directives.cursor?.responseNext) {
1025
+ pushError(errors, `Projection ${statement.id} http_responses for '${capabilityId}' must include 'cursor response_next <wire_name>'`, entry.loc);
1026
+ }
1027
+ if (!directives.limit) {
1028
+ pushError(errors, `Projection ${statement.id} http_responses for '${capabilityId}' must include 'limit field <field> default <n> max <n>'`, entry.loc);
1029
+ }
1030
+ if (!directives.sort) {
1031
+ pushError(errors, `Projection ${statement.id} http_responses for '${capabilityId}' must include 'sort by <field> direction <asc|desc>'`, entry.loc);
1032
+ }
1033
+ }
1034
+
1035
+ if (directives.sort && !["asc", "desc"].includes(directives.sort.direction || "")) {
1036
+ pushError(errors, `Projection ${statement.id} http_responses for '${capabilityId}' has invalid sort direction '${directives.sort.direction}'`, entry.loc);
1037
+ }
1038
+
1039
+ if (directives.total && !["true", "false"].includes(directives.total.included || "")) {
1040
+ pushError(errors, `Projection ${statement.id} http_responses for '${capabilityId}' has invalid total included value '${directives.total.included}'`, entry.loc);
1041
+ }
1042
+
1043
+ if (directives.limit) {
1044
+ const defaultValue = Number.parseInt(directives.limit.defaultValue || "", 10);
1045
+ const maxValue = Number.parseInt(directives.limit.maxValue || "", 10);
1046
+ if (!Number.isInteger(defaultValue) || !Number.isInteger(maxValue)) {
1047
+ pushError(errors, `Projection ${statement.id} http_responses for '${capabilityId}' must use integer default/max values for 'limit'`, entry.loc);
1048
+ } else if (defaultValue > maxValue) {
1049
+ pushError(errors, `Projection ${statement.id} http_responses for '${capabilityId}' must use default <= max for 'limit'`, entry.loc);
1050
+ }
1051
+ }
1052
+
1053
+ const inputFields = resolveCapabilityContractFields(registry, capabilityId, "input");
1054
+ const outputFields = resolveCapabilityContractFields(registry, capabilityId, "output");
1055
+ if (directives.cursor?.requestAfter && inputFields.size > 0 && !inputFields.has(directives.cursor.requestAfter)) {
1056
+ pushError(errors, `Projection ${statement.id} http_responses references unknown input field '${directives.cursor.requestAfter}' for cursor request_after on ${capabilityId}`, entry.loc);
1057
+ }
1058
+ if (directives.limit?.field && inputFields.size > 0 && !inputFields.has(directives.limit.field)) {
1059
+ pushError(errors, `Projection ${statement.id} http_responses references unknown input field '${directives.limit.field}' for limit on ${capabilityId}`, entry.loc);
1060
+ }
1061
+ if (directives.sort?.field && outputFields.size > 0 && !outputFields.has(directives.sort.field)) {
1062
+ pushError(errors, `Projection ${statement.id} http_responses references unknown output field '${directives.sort.field}' for sort on ${capabilityId}`, entry.loc);
1063
+ }
1064
+ }
1065
+ }
1066
+
1067
+ function validateProjectionHttpPreconditions(errors, statement, fieldMap, registry) {
1068
+ if (statement.kind !== "projection") {
1069
+ return;
1070
+ }
1071
+
1072
+ const httpPreconditionsField = fieldMap.get("http_preconditions")?.[0];
1073
+ if (!httpPreconditionsField || httpPreconditionsField.value.type !== "block") {
1074
+ return;
1075
+ }
1076
+
1077
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
1078
+ for (const entry of httpPreconditionsField.value.entries) {
1079
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
1080
+ const [capabilityId] = tokens;
1081
+ const capability = registry.get(capabilityId);
1082
+
1083
+ if (!capability) {
1084
+ pushError(errors, `Projection ${statement.id} http_preconditions references missing capability '${capabilityId}'`, entry.loc);
1085
+ continue;
1086
+ }
1087
+ if (capability.kind !== "capability") {
1088
+ pushError(errors, `Projection ${statement.id} http_preconditions must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
1089
+ }
1090
+ if (!realized.has(capabilityId)) {
1091
+ pushError(errors, `Projection ${statement.id} http_preconditions for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
1092
+ }
1093
+
1094
+ const directives = new Map();
1095
+ for (let i = 1; i < tokens.length; i += 2) {
1096
+ const key = tokens[i];
1097
+ const value = tokens[i + 1];
1098
+ if (!value) {
1099
+ pushError(errors, `Projection ${statement.id} http_preconditions for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
1100
+ continue;
1101
+ }
1102
+ directives.set(key, value);
1103
+ }
1104
+
1105
+ for (const requiredKey of ["header", "required", "error", "source", "code"]) {
1106
+ if (!directives.has(requiredKey)) {
1107
+ pushError(errors, `Projection ${statement.id} http_preconditions for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
1108
+ }
1109
+ }
1110
+
1111
+ for (const key of directives.keys()) {
1112
+ if (!["header", "required", "error", "source", "code"].includes(key)) {
1113
+ pushError(errors, `Projection ${statement.id} http_preconditions for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
1114
+ }
1115
+ }
1116
+
1117
+ const required = directives.get("required");
1118
+ if (required && !["true", "false"].includes(required)) {
1119
+ pushError(errors, `Projection ${statement.id} http_preconditions for '${capabilityId}' has invalid required value '${required}'`, entry.loc);
1120
+ }
1121
+
1122
+ const errorStatus = directives.get("error");
1123
+ if (errorStatus && !/^\d{3}$/.test(errorStatus)) {
1124
+ pushError(errors, `Projection ${statement.id} http_preconditions for '${capabilityId}' must use a 3-digit error status`, entry.loc);
1125
+ }
1126
+
1127
+ const sourceField = directives.get("source");
1128
+ const outputFields = resolveCapabilityContractFields(registry, capabilityId, "output");
1129
+ if (sourceField && outputFields.size > 0 && !outputFields.has(sourceField)) {
1130
+ pushError(errors, `Projection ${statement.id} http_preconditions references unknown output field '${sourceField}' on ${capabilityId}`, entry.loc);
1131
+ }
1132
+ }
1133
+ }
1134
+
1135
+ function validateProjectionHttpIdempotency(errors, statement, fieldMap, registry) {
1136
+ if (statement.kind !== "projection") {
1137
+ return;
1138
+ }
1139
+
1140
+ const httpIdempotencyField = fieldMap.get("http_idempotency")?.[0];
1141
+ if (!httpIdempotencyField || httpIdempotencyField.value.type !== "block") {
1142
+ return;
1143
+ }
1144
+
1145
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
1146
+ for (const entry of httpIdempotencyField.value.entries) {
1147
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
1148
+ const [capabilityId] = tokens;
1149
+ const capability = registry.get(capabilityId);
1150
+
1151
+ if (!capability) {
1152
+ pushError(errors, `Projection ${statement.id} http_idempotency references missing capability '${capabilityId}'`, entry.loc);
1153
+ continue;
1154
+ }
1155
+ if (capability.kind !== "capability") {
1156
+ pushError(errors, `Projection ${statement.id} http_idempotency must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
1157
+ }
1158
+ if (!realized.has(capabilityId)) {
1159
+ pushError(errors, `Projection ${statement.id} http_idempotency for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
1160
+ }
1161
+
1162
+ const directives = new Map();
1163
+ for (let i = 1; i < tokens.length; i += 2) {
1164
+ const key = tokens[i];
1165
+ const value = tokens[i + 1];
1166
+ if (!value) {
1167
+ pushError(errors, `Projection ${statement.id} http_idempotency for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
1168
+ continue;
1169
+ }
1170
+ directives.set(key, value);
1171
+ }
1172
+
1173
+ for (const requiredKey of ["header", "required", "error", "code"]) {
1174
+ if (!directives.has(requiredKey)) {
1175
+ pushError(errors, `Projection ${statement.id} http_idempotency for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
1176
+ }
1177
+ }
1178
+
1179
+ for (const key of directives.keys()) {
1180
+ if (!["header", "required", "error", "code"].includes(key)) {
1181
+ pushError(errors, `Projection ${statement.id} http_idempotency for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
1182
+ }
1183
+ }
1184
+
1185
+ const required = directives.get("required");
1186
+ if (required && !["true", "false"].includes(required)) {
1187
+ pushError(errors, `Projection ${statement.id} http_idempotency for '${capabilityId}' has invalid required value '${required}'`, entry.loc);
1188
+ }
1189
+
1190
+ const errorStatus = directives.get("error");
1191
+ if (errorStatus && !/^\d{3}$/.test(errorStatus)) {
1192
+ pushError(errors, `Projection ${statement.id} http_idempotency for '${capabilityId}' must use a 3-digit error status`, entry.loc);
1193
+ }
1194
+ }
1195
+ }
1196
+
1197
+ function validateProjectionHttpCache(errors, statement, fieldMap, registry) {
1198
+ if (statement.kind !== "projection") {
1199
+ return;
1200
+ }
1201
+
1202
+ const httpCacheField = fieldMap.get("http_cache")?.[0];
1203
+ if (!httpCacheField || httpCacheField.value.type !== "block") {
1204
+ return;
1205
+ }
1206
+
1207
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
1208
+ const httpEntries = blockEntries(getFieldValue(statement, "http"));
1209
+ const httpMethodsByCapability = new Map();
1210
+
1211
+ for (const entry of httpEntries) {
1212
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
1213
+ const capabilityId = tokens[0];
1214
+ for (let i = 1; i < tokens.length - 1; i += 1) {
1215
+ if (tokens[i] === "method") {
1216
+ httpMethodsByCapability.set(capabilityId, tokens[i + 1]);
1217
+ break;
1218
+ }
1219
+ }
1220
+ }
1221
+
1222
+ for (const entry of httpCacheField.value.entries) {
1223
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
1224
+ const [capabilityId] = tokens;
1225
+ const capability = registry.get(capabilityId);
1226
+
1227
+ if (!capability) {
1228
+ pushError(errors, `Projection ${statement.id} http_cache references missing capability '${capabilityId}'`, entry.loc);
1229
+ continue;
1230
+ }
1231
+ if (capability.kind !== "capability") {
1232
+ pushError(errors, `Projection ${statement.id} http_cache must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
1233
+ }
1234
+ if (!realized.has(capabilityId)) {
1235
+ pushError(errors, `Projection ${statement.id} http_cache for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
1236
+ }
1237
+
1238
+ const directives = new Map();
1239
+ for (let i = 1; i < tokens.length; i += 2) {
1240
+ const key = tokens[i];
1241
+ const value = tokens[i + 1];
1242
+ if (!value) {
1243
+ pushError(errors, `Projection ${statement.id} http_cache for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
1244
+ continue;
1245
+ }
1246
+ directives.set(key, value);
1247
+ }
1248
+
1249
+ for (const requiredKey of ["response_header", "request_header", "required", "not_modified", "source", "code"]) {
1250
+ if (!directives.has(requiredKey)) {
1251
+ pushError(errors, `Projection ${statement.id} http_cache for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
1252
+ }
1253
+ }
1254
+
1255
+ for (const key of directives.keys()) {
1256
+ if (!["response_header", "request_header", "required", "not_modified", "source", "code"].includes(key)) {
1257
+ pushError(errors, `Projection ${statement.id} http_cache for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
1258
+ }
1259
+ }
1260
+
1261
+ const required = directives.get("required");
1262
+ if (required && !["true", "false"].includes(required)) {
1263
+ pushError(errors, `Projection ${statement.id} http_cache for '${capabilityId}' has invalid required value '${required}'`, entry.loc);
1264
+ }
1265
+
1266
+ const notModifiedStatus = directives.get("not_modified");
1267
+ if (notModifiedStatus && notModifiedStatus !== "304") {
1268
+ pushError(errors, `Projection ${statement.id} http_cache for '${capabilityId}' must use 304 for 'not_modified'`, entry.loc);
1269
+ }
1270
+
1271
+ const sourceField = directives.get("source");
1272
+ const outputFields = resolveCapabilityContractFields(registry, capabilityId, "output");
1273
+ if (sourceField && outputFields.size > 0 && !outputFields.has(sourceField)) {
1274
+ pushError(errors, `Projection ${statement.id} http_cache references unknown output field '${sourceField}' on ${capabilityId}`, entry.loc);
1275
+ }
1276
+
1277
+ const method = httpMethodsByCapability.get(capabilityId);
1278
+ if (method && method !== "GET") {
1279
+ pushError(errors, `Projection ${statement.id} http_cache for '${capabilityId}' requires an HTTP GET realization, found '${method}'`, entry.loc);
1280
+ }
1281
+ }
1282
+ }
1283
+
1284
+ function validateProjectionHttpDelete(errors, statement, fieldMap, registry) {
1285
+ if (statement.kind !== "projection") {
1286
+ return;
1287
+ }
1288
+
1289
+ const httpDeleteField = fieldMap.get("http_delete")?.[0];
1290
+ if (!httpDeleteField || httpDeleteField.value.type !== "block") {
1291
+ return;
1292
+ }
1293
+
1294
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
1295
+ for (const entry of httpDeleteField.value.entries) {
1296
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
1297
+ const [capabilityId] = tokens;
1298
+ const capability = registry.get(capabilityId);
1299
+
1300
+ if (!capability) {
1301
+ pushError(errors, `Projection ${statement.id} http_delete references missing capability '${capabilityId}'`, entry.loc);
1302
+ continue;
1303
+ }
1304
+ if (capability.kind !== "capability") {
1305
+ pushError(errors, `Projection ${statement.id} http_delete must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
1306
+ }
1307
+ if (!realized.has(capabilityId)) {
1308
+ pushError(errors, `Projection ${statement.id} http_delete for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
1309
+ }
1310
+
1311
+ const directives = new Map();
1312
+ for (let i = 1; i < tokens.length; i += 2) {
1313
+ const key = tokens[i];
1314
+ const value = tokens[i + 1];
1315
+ if (!value) {
1316
+ pushError(errors, `Projection ${statement.id} http_delete for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
1317
+ continue;
1318
+ }
1319
+ directives.set(key, value);
1320
+ }
1321
+
1322
+ for (const requiredKey of ["mode", "response"]) {
1323
+ if (!directives.has(requiredKey)) {
1324
+ pushError(errors, `Projection ${statement.id} http_delete for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
1325
+ }
1326
+ }
1327
+
1328
+ for (const key of directives.keys()) {
1329
+ if (!["mode", "field", "value", "response"].includes(key)) {
1330
+ pushError(errors, `Projection ${statement.id} http_delete for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
1331
+ }
1332
+ }
1333
+
1334
+ const mode = directives.get("mode");
1335
+ if (mode && !["soft", "hard"].includes(mode)) {
1336
+ pushError(errors, `Projection ${statement.id} http_delete for '${capabilityId}' has invalid mode '${mode}'`, entry.loc);
1337
+ }
1338
+
1339
+ const response = directives.get("response");
1340
+ if (response && !["none", "body"].includes(response)) {
1341
+ pushError(errors, `Projection ${statement.id} http_delete for '${capabilityId}' has invalid response '${response}'`, entry.loc);
1342
+ }
1343
+
1344
+ if (mode === "soft") {
1345
+ if (!directives.has("field") || !directives.has("value")) {
1346
+ pushError(errors, `Projection ${statement.id} http_delete for '${capabilityId}' must include 'field' and 'value' for soft deletes`, entry.loc);
1347
+ }
1348
+ const outputFields = resolveCapabilityContractFields(registry, capabilityId, "output");
1349
+ const fieldName = directives.get("field");
1350
+ if (fieldName && outputFields.size > 0 && !outputFields.has(fieldName)) {
1351
+ pushError(errors, `Projection ${statement.id} http_delete references unknown output field '${fieldName}' on ${capabilityId}`, entry.loc);
1352
+ }
1353
+ }
1354
+ }
1355
+ }
1356
+
1357
+ function validateProjectionHttpAsync(errors, statement, fieldMap, registry) {
1358
+ if (statement.kind !== "projection") {
1359
+ return;
1360
+ }
1361
+
1362
+ const httpAsyncField = fieldMap.get("http_async")?.[0];
1363
+ if (!httpAsyncField || httpAsyncField.value.type !== "block") {
1364
+ return;
1365
+ }
1366
+
1367
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
1368
+ const httpEntries = blockEntries(getFieldValue(statement, "http"));
1369
+ const httpDirectivesByCapability = new Map();
1370
+ for (const entry of httpEntries) {
1371
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
1372
+ const capabilityId = tokens[0];
1373
+ const directives = new Map();
1374
+ for (let i = 1; i < tokens.length - 1; i += 2) {
1375
+ directives.set(tokens[i], tokens[i + 1]);
1376
+ }
1377
+ httpDirectivesByCapability.set(capabilityId, directives);
1378
+ }
1379
+ for (const entry of httpAsyncField.value.entries) {
1380
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
1381
+ const [capabilityId] = tokens;
1382
+ const capability = registry.get(capabilityId);
1383
+
1384
+ if (!capability) {
1385
+ pushError(errors, `Projection ${statement.id} http_async references missing capability '${capabilityId}'`, entry.loc);
1386
+ continue;
1387
+ }
1388
+ if (capability.kind !== "capability") {
1389
+ pushError(errors, `Projection ${statement.id} http_async must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
1390
+ }
1391
+ if (!realized.has(capabilityId)) {
1392
+ pushError(errors, `Projection ${statement.id} http_async for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
1393
+ }
1394
+
1395
+ const directives = new Map();
1396
+ for (let i = 1; i < tokens.length; i += 2) {
1397
+ const key = tokens[i];
1398
+ const value = tokens[i + 1];
1399
+ if (!value) {
1400
+ pushError(errors, `Projection ${statement.id} http_async for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
1401
+ continue;
1402
+ }
1403
+ directives.set(key, value);
1404
+ }
1405
+
1406
+ for (const requiredKey of ["mode", "accepted", "location_header", "retry_after_header", "status_path", "status_capability", "job"]) {
1407
+ if (!directives.has(requiredKey)) {
1408
+ pushError(errors, `Projection ${statement.id} http_async for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
1409
+ }
1410
+ }
1411
+
1412
+ for (const key of directives.keys()) {
1413
+ if (!["mode", "accepted", "location_header", "retry_after_header", "status_path", "status_capability", "job"].includes(key)) {
1414
+ pushError(errors, `Projection ${statement.id} http_async for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
1415
+ }
1416
+ }
1417
+
1418
+ const mode = directives.get("mode");
1419
+ if (mode && mode !== "job") {
1420
+ pushError(errors, `Projection ${statement.id} http_async for '${capabilityId}' has invalid mode '${mode}'`, entry.loc);
1421
+ }
1422
+
1423
+ const accepted = directives.get("accepted");
1424
+ if (accepted && accepted !== "202") {
1425
+ pushError(errors, `Projection ${statement.id} http_async for '${capabilityId}' must use 202 for 'accepted'`, entry.loc);
1426
+ }
1427
+
1428
+ const jobShapeId = directives.get("job");
1429
+ if (jobShapeId) {
1430
+ const jobShape = registry.get(jobShapeId);
1431
+ if (!jobShape) {
1432
+ pushError(errors, `Projection ${statement.id} http_async for '${capabilityId}' references missing shape '${jobShapeId}'`, entry.loc);
1433
+ } else if (jobShape.kind !== "shape") {
1434
+ pushError(errors, `Projection ${statement.id} http_async for '${capabilityId}' must reference a shape for 'job', found ${jobShape.kind} '${jobShape.id}'`, entry.loc);
1435
+ }
1436
+ }
1437
+
1438
+ const statusCapabilityId = directives.get("status_capability");
1439
+ if (statusCapabilityId) {
1440
+ const statusCapability = registry.get(statusCapabilityId);
1441
+ if (!statusCapability) {
1442
+ pushError(errors, `Projection ${statement.id} http_async for '${capabilityId}' references missing status capability '${statusCapabilityId}'`, entry.loc);
1443
+ } else if (statusCapability.kind !== "capability") {
1444
+ pushError(errors, `Projection ${statement.id} http_async for '${capabilityId}' must reference a capability for 'status_capability', found ${statusCapability.kind} '${statusCapability.id}'`, entry.loc);
1445
+ } else if (!realized.has(statusCapabilityId)) {
1446
+ pushError(errors, `Projection ${statement.id} http_async for '${capabilityId}' status capability '${statusCapabilityId}' must also appear in 'realizes'`, entry.loc);
1447
+ }
1448
+
1449
+ const statusHttp = httpDirectivesByCapability.get(statusCapabilityId);
1450
+ if (statusHttp?.get("method") && statusHttp.get("method") !== "GET") {
1451
+ pushError(errors, `Projection ${statement.id} http_async for '${capabilityId}' status capability '${statusCapabilityId}' must use HTTP GET`, entry.loc);
1452
+ }
1453
+ if (statusHttp?.get("path") && directives.get("status_path") && statusHttp.get("path") !== directives.get("status_path")) {
1454
+ pushError(errors, `Projection ${statement.id} http_async for '${capabilityId}' status_path must match the path for '${statusCapabilityId}'`, entry.loc);
1455
+ }
1456
+ }
1457
+
1458
+ const statusPath = directives.get("status_path");
1459
+ if (statusPath && !statusPath.startsWith("/")) {
1460
+ pushError(errors, `Projection ${statement.id} http_async for '${capabilityId}' must use an absolute path for 'status_path'`, entry.loc);
1461
+ }
1462
+ }
1463
+ }
1464
+
1465
+ function validateProjectionHttpStatus(errors, statement, fieldMap, registry) {
1466
+ if (statement.kind !== "projection") {
1467
+ return;
1468
+ }
1469
+
1470
+ const httpStatusField = fieldMap.get("http_status")?.[0];
1471
+ if (!httpStatusField || httpStatusField.value.type !== "block") {
1472
+ return;
1473
+ }
1474
+
1475
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
1476
+ const httpEntries = blockEntries(getFieldValue(statement, "http"));
1477
+ const httpMethodsByCapability = new Map();
1478
+ for (const entry of httpEntries) {
1479
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
1480
+ const capabilityId = tokens[0];
1481
+ for (let i = 1; i < tokens.length - 1; i += 2) {
1482
+ if (tokens[i] === "method") {
1483
+ httpMethodsByCapability.set(capabilityId, tokens[i + 1]);
1484
+ break;
1485
+ }
1486
+ }
1487
+ }
1488
+ for (const entry of httpStatusField.value.entries) {
1489
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
1490
+ const [capabilityId] = tokens;
1491
+ const capability = registry.get(capabilityId);
1492
+
1493
+ if (!capability) {
1494
+ pushError(errors, `Projection ${statement.id} http_status references missing capability '${capabilityId}'`, entry.loc);
1495
+ continue;
1496
+ }
1497
+ if (capability.kind !== "capability") {
1498
+ pushError(errors, `Projection ${statement.id} http_status must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
1499
+ }
1500
+ if (!realized.has(capabilityId)) {
1501
+ pushError(errors, `Projection ${statement.id} http_status for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
1502
+ }
1503
+
1504
+ const directives = new Map();
1505
+ for (let i = 1; i < tokens.length; i += 2) {
1506
+ const key = tokens[i];
1507
+ const value = tokens[i + 1];
1508
+ if (!value) {
1509
+ pushError(errors, `Projection ${statement.id} http_status for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
1510
+ continue;
1511
+ }
1512
+ directives.set(key, value);
1513
+ }
1514
+
1515
+ for (const requiredKey of ["async_for", "state_field", "completed", "failed"]) {
1516
+ if (!directives.has(requiredKey)) {
1517
+ pushError(errors, `Projection ${statement.id} http_status for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
1518
+ }
1519
+ }
1520
+
1521
+ for (const key of directives.keys()) {
1522
+ if (!["async_for", "state_field", "completed", "failed", "expired", "download_capability", "download_field", "error_field"].includes(key)) {
1523
+ pushError(errors, `Projection ${statement.id} http_status for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
1524
+ }
1525
+ }
1526
+
1527
+ const asyncCapabilityId = directives.get("async_for");
1528
+ if (asyncCapabilityId) {
1529
+ const asyncCapability = registry.get(asyncCapabilityId);
1530
+ if (!asyncCapability) {
1531
+ pushError(errors, `Projection ${statement.id} http_status for '${capabilityId}' references missing async capability '${asyncCapabilityId}'`, entry.loc);
1532
+ } else if (asyncCapability.kind !== "capability") {
1533
+ pushError(errors, `Projection ${statement.id} http_status for '${capabilityId}' must reference a capability for 'async_for', found ${asyncCapability.kind} '${asyncCapability.id}'`, entry.loc);
1534
+ } else if (!realized.has(asyncCapabilityId)) {
1535
+ pushError(errors, `Projection ${statement.id} http_status for '${capabilityId}' async capability '${asyncCapabilityId}' must also appear in 'realizes'`, entry.loc);
1536
+ }
1537
+ }
1538
+
1539
+ const outputFields = resolveCapabilityContractFields(registry, capabilityId, "output");
1540
+ for (const [directive, fieldName] of [
1541
+ ["state_field", directives.get("state_field")],
1542
+ ["download_field", directives.get("download_field")],
1543
+ ["error_field", directives.get("error_field")]
1544
+ ]) {
1545
+ if (fieldName && outputFields.size > 0 && !outputFields.has(fieldName)) {
1546
+ pushError(errors, `Projection ${statement.id} http_status references unknown output field '${fieldName}' for '${directive}' on ${capabilityId}`, entry.loc);
1547
+ }
1548
+ }
1549
+
1550
+ const downloadCapabilityId = directives.get("download_capability");
1551
+ if (downloadCapabilityId) {
1552
+ const downloadCapability = registry.get(downloadCapabilityId);
1553
+ if (!downloadCapability) {
1554
+ pushError(errors, `Projection ${statement.id} http_status for '${capabilityId}' references missing download capability '${downloadCapabilityId}'`, entry.loc);
1555
+ } else if (downloadCapability.kind !== "capability") {
1556
+ pushError(errors, `Projection ${statement.id} http_status for '${capabilityId}' must reference a capability for 'download_capability', found ${downloadCapability.kind} '${downloadCapability.id}'`, entry.loc);
1557
+ } else if (!realized.has(downloadCapabilityId)) {
1558
+ pushError(errors, `Projection ${statement.id} http_status for '${capabilityId}' download capability '${downloadCapabilityId}' must also appear in 'realizes'`, entry.loc);
1559
+ }
1560
+
1561
+ const method = httpMethodsByCapability.get(downloadCapabilityId);
1562
+ if (method && method !== "GET") {
1563
+ pushError(errors, `Projection ${statement.id} http_status for '${capabilityId}' download capability '${downloadCapabilityId}' must use HTTP GET`, entry.loc);
1564
+ }
1565
+ }
1566
+ }
1567
+ }
1568
+
1569
+ function validateProjectionHttpDownload(errors, statement, fieldMap, registry) {
1570
+ if (statement.kind !== "projection") {
1571
+ return;
1572
+ }
1573
+
1574
+ const httpDownloadField = fieldMap.get("http_download")?.[0];
1575
+ if (!httpDownloadField || httpDownloadField.value.type !== "block") {
1576
+ return;
1577
+ }
1578
+
1579
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
1580
+ for (const entry of httpDownloadField.value.entries) {
1581
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
1582
+ const [capabilityId] = tokens;
1583
+ const capability = registry.get(capabilityId);
1584
+
1585
+ if (!capability) {
1586
+ pushError(errors, `Projection ${statement.id} http_download references missing capability '${capabilityId}'`, entry.loc);
1587
+ continue;
1588
+ }
1589
+ if (capability.kind !== "capability") {
1590
+ pushError(errors, `Projection ${statement.id} http_download must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
1591
+ }
1592
+ if (!realized.has(capabilityId)) {
1593
+ pushError(errors, `Projection ${statement.id} http_download for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
1594
+ }
1595
+
1596
+ const directives = new Map();
1597
+ for (let i = 1; i < tokens.length; i += 2) {
1598
+ const key = tokens[i];
1599
+ const value = tokens[i + 1];
1600
+ if (!value) {
1601
+ pushError(errors, `Projection ${statement.id} http_download for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
1602
+ continue;
1603
+ }
1604
+ directives.set(key, value);
1605
+ }
1606
+
1607
+ for (const requiredKey of ["async_for", "media", "disposition"]) {
1608
+ if (!directives.has(requiredKey)) {
1609
+ pushError(errors, `Projection ${statement.id} http_download for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
1610
+ }
1611
+ }
1612
+
1613
+ for (const key of directives.keys()) {
1614
+ if (!["async_for", "media", "filename", "disposition"].includes(key)) {
1615
+ pushError(errors, `Projection ${statement.id} http_download for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
1616
+ }
1617
+ }
1618
+
1619
+ const asyncCapabilityId = directives.get("async_for");
1620
+ if (asyncCapabilityId) {
1621
+ const asyncCapability = registry.get(asyncCapabilityId);
1622
+ if (!asyncCapability) {
1623
+ pushError(errors, `Projection ${statement.id} http_download for '${capabilityId}' references missing async capability '${asyncCapabilityId}'`, entry.loc);
1624
+ } else if (asyncCapability.kind !== "capability") {
1625
+ pushError(errors, `Projection ${statement.id} http_download for '${capabilityId}' must reference a capability for 'async_for', found ${asyncCapability.kind} '${asyncCapability.id}'`, entry.loc);
1626
+ } else if (!realized.has(asyncCapabilityId)) {
1627
+ pushError(errors, `Projection ${statement.id} http_download for '${capabilityId}' async capability '${asyncCapabilityId}' must also appear in 'realizes'`, entry.loc);
1628
+ }
1629
+ }
1630
+
1631
+ const media = directives.get("media");
1632
+ if (media && !media.includes("/")) {
1633
+ pushError(errors, `Projection ${statement.id} http_download for '${capabilityId}' must use a valid media type`, entry.loc);
1634
+ }
1635
+
1636
+ const disposition = directives.get("disposition");
1637
+ if (disposition && !["attachment", "inline"].includes(disposition)) {
1638
+ pushError(errors, `Projection ${statement.id} http_download for '${capabilityId}' has invalid disposition '${disposition}'`, entry.loc);
1639
+ }
1640
+ }
1641
+ }
1642
+
1643
+ function validateProjectionHttpAuthz(errors, statement, fieldMap, registry) {
1644
+ if (statement.kind !== "projection") {
1645
+ return;
1646
+ }
1647
+
1648
+ const httpAuthzField = fieldMap.get("http_authz")?.[0];
1649
+ if (!httpAuthzField || httpAuthzField.value.type !== "block") {
1650
+ return;
1651
+ }
1652
+
1653
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
1654
+ for (const entry of httpAuthzField.value.entries) {
1655
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
1656
+ const [capabilityId] = tokens;
1657
+ const capability = registry.get(capabilityId);
1658
+
1659
+ if (!capability) {
1660
+ pushError(errors, `Projection ${statement.id} http_authz references missing capability '${capabilityId}'`, entry.loc);
1661
+ continue;
1662
+ }
1663
+ if (capability.kind !== "capability") {
1664
+ pushError(errors, `Projection ${statement.id} http_authz must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
1665
+ }
1666
+ if (!realized.has(capabilityId)) {
1667
+ pushError(errors, `Projection ${statement.id} http_authz for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
1668
+ }
1669
+
1670
+ const directives = new Map();
1671
+ for (let i = 1; i < tokens.length; i += 2) {
1672
+ const key = tokens[i];
1673
+ const value = tokens[i + 1];
1674
+ if (!value) {
1675
+ pushError(errors, `Projection ${statement.id} http_authz for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
1676
+ continue;
1677
+ }
1678
+ directives.set(key, value);
1679
+ }
1680
+
1681
+ for (const key of directives.keys()) {
1682
+ if (!["role", "permission", "claim", "claim_value", "ownership", "ownership_field"].includes(key)) {
1683
+ pushError(errors, `Projection ${statement.id} http_authz for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
1684
+ }
1685
+ }
1686
+
1687
+ if (directives.size === 0) {
1688
+ pushError(errors, `Projection ${statement.id} http_authz for '${capabilityId}' must include at least one directive`, entry.loc);
1689
+ }
1690
+
1691
+ const ownership = directives.get("ownership");
1692
+ if (ownership && !["owner", "owner_or_admin", "project_member", "none"].includes(ownership)) {
1693
+ pushError(errors, `Projection ${statement.id} http_authz for '${capabilityId}' has invalid ownership '${ownership}'`, entry.loc);
1694
+ }
1695
+
1696
+ const ownershipField = directives.get("ownership_field");
1697
+ if (ownershipField && (!ownership || ownership === "none")) {
1698
+ pushError(errors, `Projection ${statement.id} http_authz for '${capabilityId}' cannot declare ownership_field without ownership`, entry.loc);
1699
+ }
1700
+
1701
+ const claimValue = directives.get("claim_value");
1702
+ if (claimValue && !directives.get("claim")) {
1703
+ pushError(errors, `Projection ${statement.id} http_authz for '${capabilityId}' cannot declare claim_value without claim`, entry.loc);
1704
+ }
1705
+ }
1706
+ }
1707
+
1708
+ function validateProjectionHttpCallbacks(errors, statement, fieldMap, registry) {
1709
+ if (statement.kind !== "projection") {
1710
+ return;
1711
+ }
1712
+
1713
+ const httpCallbacksField = fieldMap.get("http_callbacks")?.[0];
1714
+ if (!httpCallbacksField || httpCallbacksField.value.type !== "block") {
1715
+ return;
1716
+ }
1717
+
1718
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
1719
+ for (const entry of httpCallbacksField.value.entries) {
1720
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
1721
+ const [capabilityId] = tokens;
1722
+ const capability = registry.get(capabilityId);
1723
+
1724
+ if (!capability) {
1725
+ pushError(errors, `Projection ${statement.id} http_callbacks references missing capability '${capabilityId}'`, entry.loc);
1726
+ continue;
1727
+ }
1728
+ if (capability.kind !== "capability") {
1729
+ pushError(errors, `Projection ${statement.id} http_callbacks must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
1730
+ }
1731
+ if (!realized.has(capabilityId)) {
1732
+ pushError(errors, `Projection ${statement.id} http_callbacks for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
1733
+ }
1734
+
1735
+ const directives = new Map();
1736
+ for (let i = 1; i < tokens.length; i += 2) {
1737
+ const key = tokens[i];
1738
+ const value = tokens[i + 1];
1739
+ if (!value) {
1740
+ pushError(errors, `Projection ${statement.id} http_callbacks for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
1741
+ continue;
1742
+ }
1743
+ directives.set(key, value);
1744
+ }
1745
+
1746
+ for (const requiredKey of ["event", "target_field", "method", "payload", "success"]) {
1747
+ if (!directives.has(requiredKey)) {
1748
+ pushError(errors, `Projection ${statement.id} http_callbacks for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
1749
+ }
1750
+ }
1751
+
1752
+ for (const key of directives.keys()) {
1753
+ if (!["event", "target_field", "method", "payload", "success"].includes(key)) {
1754
+ pushError(errors, `Projection ${statement.id} http_callbacks for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
1755
+ }
1756
+ }
1757
+
1758
+ const method = directives.get("method");
1759
+ if (method && !["POST", "PUT", "PATCH"].includes(method)) {
1760
+ pushError(errors, `Projection ${statement.id} http_callbacks for '${capabilityId}' has invalid method '${method}'`, entry.loc);
1761
+ }
1762
+
1763
+ const success = directives.get("success");
1764
+ if (success && !/^\d{3}$/.test(success)) {
1765
+ pushError(errors, `Projection ${statement.id} http_callbacks for '${capabilityId}' must use a 3-digit success status`, entry.loc);
1766
+ }
1767
+
1768
+ const payloadShapeId = directives.get("payload");
1769
+ if (payloadShapeId) {
1770
+ const payloadShape = registry.get(payloadShapeId);
1771
+ if (!payloadShape) {
1772
+ pushError(errors, `Projection ${statement.id} http_callbacks for '${capabilityId}' references missing shape '${payloadShapeId}'`, entry.loc);
1773
+ } else if (payloadShape.kind !== "shape") {
1774
+ pushError(errors, `Projection ${statement.id} http_callbacks for '${capabilityId}' must reference a shape for 'payload', found ${payloadShape.kind} '${payloadShape.id}'`, entry.loc);
1775
+ }
1776
+ }
1777
+
1778
+ const targetField = directives.get("target_field");
1779
+ const inputFields = resolveCapabilityContractFields(registry, capabilityId, "input");
1780
+ if (targetField && inputFields.size > 0 && !inputFields.has(targetField)) {
1781
+ pushError(errors, `Projection ${statement.id} http_callbacks references unknown input field '${targetField}' on ${capabilityId}`, entry.loc);
1782
+ }
1783
+ }
1784
+ }
1785
+
1786
+ function parseProjectionHttpResponsesDirectives(tokens) {
1787
+ const result = {
1788
+ mode: null,
1789
+ item: null,
1790
+ cursor: null,
1791
+ limit: null,
1792
+ sort: null,
1793
+ total: null,
1794
+ errors: []
1795
+ };
1796
+
1797
+ for (let i = 0; i < tokens.length; i += 1) {
1798
+ const token = tokens[i];
1799
+ if (token === "mode") {
1800
+ result.mode = tokens[i + 1] || null;
1801
+ if (!tokens[i + 1]) {
1802
+ result.errors.push("is missing a value for 'mode'");
1803
+ }
1804
+ i += 1;
1805
+ continue;
1806
+ }
1807
+ if (token === "item") {
1808
+ result.item = tokens[i + 1] || null;
1809
+ if (!tokens[i + 1]) {
1810
+ result.errors.push("is missing a value for 'item'");
1811
+ }
1812
+ i += 1;
1813
+ continue;
1814
+ }
1815
+ if (token === "cursor") {
1816
+ const requestKeyword = tokens[i + 1];
1817
+ const requestField = tokens[i + 2];
1818
+ const responseKeyword = tokens[i + 3];
1819
+ const responseNext = tokens[i + 4];
1820
+ let responsePrev = null;
1821
+ let consumed = 4;
1822
+ if (tokens[i + 5] === "response_prev") {
1823
+ responsePrev = tokens[i + 6] || null;
1824
+ consumed = 6;
1825
+ }
1826
+ result.cursor = {
1827
+ requestAfter: requestKeyword === "request_after" ? requestField : null,
1828
+ responseNext: responseKeyword === "response_next" ? responseNext : null,
1829
+ responsePrev
1830
+ };
1831
+ if (requestKeyword !== "request_after") {
1832
+ result.errors.push("must use 'cursor request_after <field>'");
1833
+ }
1834
+ if (responseKeyword !== "response_next") {
1835
+ result.errors.push("must use 'cursor response_next <wire_name>'");
1836
+ }
1837
+ i += consumed;
1838
+ continue;
1839
+ }
1840
+ if (token === "limit") {
1841
+ result.limit = {
1842
+ field: tokens[i + 1] === "field" ? tokens[i + 2] || null : null,
1843
+ defaultValue: tokens[i + 3] === "default" ? tokens[i + 4] || null : null,
1844
+ maxValue: tokens[i + 5] === "max" ? tokens[i + 6] || null : null
1845
+ };
1846
+ if (tokens[i + 1] !== "field" || tokens[i + 3] !== "default" || tokens[i + 5] !== "max") {
1847
+ result.errors.push("must use 'limit field <field> default <n> max <n>'");
1848
+ }
1849
+ i += 6;
1850
+ continue;
1851
+ }
1852
+ if (token === "sort") {
1853
+ result.sort = {
1854
+ field: tokens[i + 1] === "by" ? tokens[i + 2] || null : null,
1855
+ direction: tokens[i + 3] === "direction" ? tokens[i + 4] || null : null
1856
+ };
1857
+ if (tokens[i + 1] !== "by" || tokens[i + 3] !== "direction") {
1858
+ result.errors.push("must use 'sort by <field> direction <asc|desc>'");
1859
+ }
1860
+ i += 4;
1861
+ continue;
1862
+ }
1863
+ if (token === "total") {
1864
+ result.total = {
1865
+ included: tokens[i + 1] === "included" ? tokens[i + 2] || null : null
1866
+ };
1867
+ if (tokens[i + 1] !== "included") {
1868
+ result.errors.push("must use 'total included <true|false>'");
1869
+ }
1870
+ i += 2;
1871
+ continue;
1872
+ }
1873
+
1874
+ result.errors.push(`has unknown directive '${token}'`);
1875
+ }
1876
+
1877
+ return result;
1878
+ }
1879
+
1880
+ function resolveCapabilityOutputShape(registry, capabilityId) {
1881
+ const capability = registry.get(capabilityId);
1882
+ if (!capability || capability.kind !== "capability") {
1883
+ return null;
1884
+ }
1885
+
1886
+ const shapeId = symbolValues(getFieldValue(capability, "output"))[0];
1887
+ const shape = shapeId ? registry.get(shapeId) : null;
1888
+ return shape?.kind === "shape" ? shape : null;
1889
+ }
1890
+
1891
+ function collectProjectionUiScreens(statement, fieldMap) {
1892
+ const screensField = fieldMap.get("ui_screens")?.[0];
1893
+ if (!screensField || screensField.value.type !== "block") {
1894
+ return new Map();
1895
+ }
1896
+
1897
+ const screens = new Map();
1898
+ for (const entry of screensField.value.entries) {
1899
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
1900
+ if (tokens[0] === "screen" && tokens[1]) {
1901
+ screens.set(tokens[1], entry);
1902
+ }
1903
+ }
1904
+ return screens;
1905
+ }
1906
+
1907
+ function resolveProjectionUiScreenFieldNames(registry, screenEntry, statement) {
1908
+ const tokens = blockSymbolItems(screenEntry).map((item) => item.value);
1909
+ const directives = parseUiDirectiveMap(tokens, 2, [], statement, screenEntry, "");
1910
+ const kind = directives.get("kind");
1911
+
1912
+ if (kind === "form") {
1913
+ const shapeId = directives.get("input_shape");
1914
+ const shape = shapeId ? registry.get(shapeId) : null;
1915
+ if (!shape || shape.kind !== "shape") {
1916
+ return new Set();
1917
+ }
1918
+ const explicitFields = statementFieldNames(shape);
1919
+ return new Set(explicitFields.length > 0 ? explicitFields : resolveShapeBaseFieldNames(shape, registry));
1920
+ }
1921
+
1922
+ if (kind === "list") {
1923
+ const loadCapabilityId = directives.get("load");
1924
+ return loadCapabilityId ? resolveCapabilityContractFields(registry, loadCapabilityId, "input") : new Set();
1925
+ }
1926
+
1927
+ if (kind === "detail" || kind === "job_status") {
1928
+ const loadCapabilityId = directives.get("load");
1929
+ return loadCapabilityId ? resolveCapabilityContractFields(registry, loadCapabilityId, "output") : new Set();
1930
+ }
1931
+
1932
+ return new Set();
1933
+ }
1934
+
1935
+ function screenIdsFromProjectionStatement(statement) {
1936
+ const screens = new Set();
1937
+ for (const entry of blockEntries(getFieldValue(statement, "ui_screens"))) {
1938
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
1939
+ if (tokens[0] === "screen" && tokens[1]) {
1940
+ screens.add(tokens[1]);
1941
+ }
1942
+ }
1943
+ return screens;
1944
+ }
1945
+
1946
+ function collectAvailableUiScreenIds(statement, fieldMap, registry) {
1947
+ const available = new Set(collectProjectionUiScreens(statement, fieldMap).keys());
1948
+ for (const targetId of symbolValues(getFieldValue(statement, "realizes"))) {
1949
+ const target = registry.get(targetId);
1950
+ if (target?.kind === "projection") {
1951
+ for (const screenId of screenIdsFromProjectionStatement(target)) {
1952
+ available.add(screenId);
1953
+ }
1954
+ }
1955
+ }
1956
+ return available;
1957
+ }
1958
+
1959
+ function collectProjectionUiRegionKeys(statement) {
1960
+ const keys = new Set();
1961
+ for (const entry of blockEntries(getFieldValue(statement, "ui_screen_regions"))) {
1962
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
1963
+ if (tokens[0] === "screen" && tokens[1] && tokens[2] === "region" && tokens[3]) {
1964
+ keys.add(`${tokens[1]}:${tokens[3]}`);
1965
+ }
1966
+ }
1967
+ return keys;
1968
+ }
1969
+
1970
+ function collectAvailableUiRegionKeys(statement, registry) {
1971
+ const available = collectProjectionUiRegionKeys(statement);
1972
+ for (const targetId of symbolValues(getFieldValue(statement, "realizes"))) {
1973
+ const target = registry.get(targetId);
1974
+ if (target?.kind === "projection") {
1975
+ for (const key of collectProjectionUiRegionKeys(target)) {
1976
+ available.add(key);
1977
+ }
1978
+ }
1979
+ }
1980
+ return available;
1981
+ }
1982
+
1983
+ function collectProjectionUiRegionPatterns(statement) {
1984
+ const patterns = new Map();
1985
+ for (const entry of blockEntries(getFieldValue(statement, "ui_screen_regions"))) {
1986
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
1987
+ if (tokens[0] !== "screen" || !tokens[1] || tokens[2] !== "region" || !tokens[3]) {
1988
+ continue;
1989
+ }
1990
+ for (let i = 4; i < tokens.length; i += 2) {
1991
+ if (tokens[i] === "pattern" && tokens[i + 1]) {
1992
+ patterns.set(`${tokens[1]}:${tokens[3]}`, tokens[i + 1]);
1993
+ }
1994
+ }
1995
+ }
1996
+ return patterns;
1997
+ }
1998
+
1999
+ function collectAvailableUiRegionPatterns(statement, registry) {
2000
+ const patterns = collectProjectionUiRegionPatterns(statement);
2001
+ for (const targetId of symbolValues(getFieldValue(statement, "realizes"))) {
2002
+ const target = registry.get(targetId);
2003
+ if (target?.kind !== "projection") {
2004
+ continue;
2005
+ }
2006
+ for (const [key, pattern] of collectProjectionUiRegionPatterns(target)) {
2007
+ if (!patterns.has(key)) {
2008
+ patterns.set(key, pattern);
2009
+ }
2010
+ }
2011
+ }
2012
+ return patterns;
2013
+ }
2014
+
2015
+ function parseUiDirectiveMap(tokens, startIndex, errors, statement, entry, context) {
2016
+ const directives = new Map();
2017
+
2018
+ for (let i = startIndex; i < tokens.length; i += 2) {
2019
+ const key = tokens[i];
2020
+ const value = tokens[i + 1];
2021
+ if (!key) {
2022
+ continue;
2023
+ }
2024
+ if (!value) {
2025
+ pushError(errors, `Projection ${statement.id} ${context} is missing a value for '${key}'`, entry.loc);
2026
+ continue;
2027
+ }
2028
+ directives.set(key, value);
2029
+ }
2030
+
2031
+ return directives;
2032
+ }
2033
+
2034
+ function validateProjectionUiScreens(errors, statement, fieldMap, registry) {
2035
+ if (statement.kind !== "projection") {
2036
+ return;
2037
+ }
2038
+
2039
+ const screensField = fieldMap.get("ui_screens")?.[0];
2040
+ if (!screensField || screensField.value.type !== "block") {
2041
+ return;
2042
+ }
2043
+
2044
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
2045
+ const seenScreens = new Set();
2046
+
2047
+ for (const entry of screensField.value.entries) {
2048
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
2049
+ const [keyword, screenId] = tokens;
2050
+
2051
+ if (keyword !== "screen") {
2052
+ pushError(errors, `Projection ${statement.id} ui_screens entries must start with 'screen'`, entry.loc);
2053
+ continue;
2054
+ }
2055
+ if (!screenId) {
2056
+ pushError(errors, `Projection ${statement.id} ui_screens entries must include a screen id`, entry.loc);
2057
+ continue;
2058
+ }
2059
+ if (!IDENTIFIER_PATTERN.test(screenId)) {
2060
+ pushError(errors, `Projection ${statement.id} ui_screens has invalid screen id '${screenId}'`, entry.loc);
2061
+ }
2062
+ if (seenScreens.has(screenId)) {
2063
+ pushError(errors, `Projection ${statement.id} ui_screens has duplicate screen id '${screenId}'`, entry.loc);
2064
+ }
2065
+ seenScreens.add(screenId);
2066
+
2067
+ const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `ui_screens for '${screenId}'`);
2068
+ const kind = directives.get("kind");
2069
+ if (!kind) {
2070
+ pushError(errors, `Projection ${statement.id} ui_screens for '${screenId}' must include 'kind'`, entry.loc);
2071
+ }
2072
+ if (kind && !UI_SCREEN_KINDS.has(kind)) {
2073
+ pushError(errors, `Projection ${statement.id} ui_screens for '${screenId}' has invalid kind '${kind}'`, entry.loc);
2074
+ }
2075
+
2076
+ for (const key of directives.keys()) {
2077
+ if (!["kind", "title", "load", "item_shape", "view_shape", "input_shape", "submit", "detail_capability", "primary_action", "secondary_action", "destructive_action", "success_navigate", "success_refresh", "empty_title", "empty_body", "terminal_action", "loading_state", "error_state", "unauthorized_state", "not_found_state", "success_state"].includes(key)) {
2078
+ pushError(errors, `Projection ${statement.id} ui_screens for '${screenId}' has unknown directive '${key}'`, entry.loc);
2079
+ }
2080
+ }
2081
+
2082
+ for (const [key, expectedKind] of [
2083
+ ["load", "capability"],
2084
+ ["submit", "capability"],
2085
+ ["detail_capability", "capability"],
2086
+ ["primary_action", "capability"],
2087
+ ["secondary_action", "capability"],
2088
+ ["destructive_action", "capability"],
2089
+ ["terminal_action", "capability"],
2090
+ ["item_shape", "shape"],
2091
+ ["view_shape", "shape"],
2092
+ ["input_shape", "shape"]
2093
+ ]) {
2094
+ const targetId = directives.get(key);
2095
+ if (!targetId) {
2096
+ continue;
2097
+ }
2098
+ const target = registry.get(targetId);
2099
+ if (!target) {
2100
+ pushError(errors, `Projection ${statement.id} ui_screens for '${screenId}' references missing ${expectedKind} '${targetId}' for '${key}'`, entry.loc);
2101
+ continue;
2102
+ }
2103
+ if (target.kind !== expectedKind) {
2104
+ pushError(errors, `Projection ${statement.id} ui_screens for '${screenId}' must reference a ${expectedKind} for '${key}', found ${target.kind} '${target.id}'`, entry.loc);
2105
+ }
2106
+ if (expectedKind === "capability" && !realized.has(targetId)) {
2107
+ pushError(errors, `Projection ${statement.id} ui_screens for '${screenId}' capability '${targetId}' for '${key}' must also appear in 'realizes'`, entry.loc);
2108
+ }
2109
+ }
2110
+
2111
+ const successNavigate = directives.get("success_navigate");
2112
+ const successRefresh = directives.get("success_refresh");
2113
+ if (successNavigate && !IDENTIFIER_PATTERN.test(successNavigate)) {
2114
+ pushError(errors, `Projection ${statement.id} ui_screens for '${screenId}' has invalid target '${successNavigate}' for 'success_navigate'`, entry.loc);
2115
+ }
2116
+ if (successRefresh && !IDENTIFIER_PATTERN.test(successRefresh)) {
2117
+ pushError(errors, `Projection ${statement.id} ui_screens for '${screenId}' has invalid target '${successRefresh}' for 'success_refresh'`, entry.loc);
2118
+ }
2119
+
2120
+ if (kind === "list" && !directives.get("load")) {
2121
+ pushError(errors, `Projection ${statement.id} ui_screens for '${screenId}' kind 'list' requires 'load'`, entry.loc);
2122
+ }
2123
+ if (kind === "detail") {
2124
+ if (!directives.get("load")) {
2125
+ pushError(errors, `Projection ${statement.id} ui_screens for '${screenId}' kind 'detail' requires 'load'`, entry.loc);
2126
+ }
2127
+ if (!directives.get("view_shape")) {
2128
+ pushError(errors, `Projection ${statement.id} ui_screens for '${screenId}' kind 'detail' requires 'view_shape'`, entry.loc);
2129
+ }
2130
+ }
2131
+ if (kind === "form") {
2132
+ if (!directives.get("input_shape")) {
2133
+ pushError(errors, `Projection ${statement.id} ui_screens for '${screenId}' kind 'form' requires 'input_shape'`, entry.loc);
2134
+ }
2135
+ if (!directives.get("submit")) {
2136
+ pushError(errors, `Projection ${statement.id} ui_screens for '${screenId}' kind 'form' requires 'submit'`, entry.loc);
2137
+ }
2138
+ }
2139
+ }
2140
+
2141
+ for (const entry of screensField.value.entries) {
2142
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
2143
+ const screenId = tokens[1];
2144
+ if (!screenId) {
2145
+ continue;
2146
+ }
2147
+ const directives = parseUiDirectiveMap(tokens, 2, [], statement, entry, "");
2148
+ for (const key of ["success_navigate", "success_refresh"]) {
2149
+ const targetScreenId = directives.get(key);
2150
+ if (targetScreenId && !seenScreens.has(targetScreenId)) {
2151
+ pushError(errors, `Projection ${statement.id} ui_screens for '${screenId}' references unknown screen '${targetScreenId}' for '${key}'`, entry.loc);
2152
+ }
2153
+ }
2154
+ }
2155
+ }
2156
+
2157
+ function validateProjectionUiCollections(errors, statement, fieldMap, registry) {
2158
+ if (statement.kind !== "projection") {
2159
+ return;
2160
+ }
2161
+
2162
+ const collectionsField = fieldMap.get("ui_collections")?.[0];
2163
+ if (!collectionsField || collectionsField.value.type !== "block") {
2164
+ return;
2165
+ }
2166
+
2167
+ const screens = collectProjectionUiScreens(statement, fieldMap);
2168
+ for (const entry of collectionsField.value.entries) {
2169
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
2170
+ const [keyword, screenId, operation, value, extra] = tokens;
2171
+
2172
+ if (keyword !== "screen") {
2173
+ pushError(errors, `Projection ${statement.id} ui_collections entries must start with 'screen'`, entry.loc);
2174
+ continue;
2175
+ }
2176
+ const screenEntry = screens.get(screenId);
2177
+ if (!screenEntry) {
2178
+ pushError(errors, `Projection ${statement.id} ui_collections references unknown screen '${screenId}'`, entry.loc);
2179
+ continue;
2180
+ }
2181
+
2182
+ const screenTokens = blockSymbolItems(screenEntry).map((item) => item.value);
2183
+ const screenDirectives = parseUiDirectiveMap(screenTokens, 2, [], statement, screenEntry, "");
2184
+ if (screenDirectives.get("kind") !== "list") {
2185
+ pushError(errors, `Projection ${statement.id} ui_collections may only target list screens, found '${screenId}'`, entry.loc);
2186
+ }
2187
+
2188
+ if (!["filter", "search", "pagination", "sort", "group", "view", "refresh"].includes(operation)) {
2189
+ pushError(errors, `Projection ${statement.id} ui_collections for '${screenId}' has invalid operation '${operation}'`, entry.loc);
2190
+ continue;
2191
+ }
2192
+
2193
+ const loadCapabilityId = screenDirectives.get("load");
2194
+ const inputFields = loadCapabilityId ? resolveCapabilityContractFields(registry, loadCapabilityId, "input") : new Set();
2195
+ const outputShape = loadCapabilityId ? resolveCapabilityOutputShape(registry, loadCapabilityId) : null;
2196
+ const outputFields = outputShape
2197
+ ? new Set((statementFieldNames(outputShape).length > 0 ? statementFieldNames(outputShape) : resolveShapeBaseFieldNames(outputShape, registry)))
2198
+ : new Set();
2199
+
2200
+ if (operation === "filter" || operation === "search") {
2201
+ if (!value) {
2202
+ pushError(errors, `Projection ${statement.id} ui_collections for '${screenId}' must include a field for '${operation}'`, entry.loc);
2203
+ } else if (inputFields.size > 0 && !inputFields.has(value)) {
2204
+ pushError(errors, `Projection ${statement.id} ui_collections references unknown input field '${value}' for '${operation}' on '${screenId}'`, entry.loc);
2205
+ }
2206
+ }
2207
+
2208
+ if (operation === "pagination" && !["cursor", "paged", "none"].includes(value || "")) {
2209
+ pushError(errors, `Projection ${statement.id} ui_collections for '${screenId}' has invalid pagination '${value}'`, entry.loc);
2210
+ }
2211
+
2212
+ if (operation === "sort") {
2213
+ if (!value || !extra) {
2214
+ pushError(errors, `Projection ${statement.id} ui_collections for '${screenId}' must use 'sort <field> <asc|desc>'`, entry.loc);
2215
+ } else {
2216
+ if (!["asc", "desc"].includes(extra)) {
2217
+ pushError(errors, `Projection ${statement.id} ui_collections for '${screenId}' has invalid sort direction '${extra}'`, entry.loc);
2218
+ }
2219
+ if (outputFields.size > 0 && !outputFields.has(value)) {
2220
+ pushError(errors, `Projection ${statement.id} ui_collections references unknown output field '${value}' for sort on '${screenId}'`, entry.loc);
2221
+ }
2222
+ }
2223
+ }
2224
+
2225
+ if (operation === "group") {
2226
+ if (!value) {
2227
+ pushError(errors, `Projection ${statement.id} ui_collections for '${screenId}' must include a field for 'group'`, entry.loc);
2228
+ } else if (outputFields.size > 0 && !outputFields.has(value)) {
2229
+ pushError(errors, `Projection ${statement.id} ui_collections references unknown output field '${value}' for group on '${screenId}'`, entry.loc);
2230
+ }
2231
+ }
2232
+
2233
+ if (operation === "view" && !UI_COLLECTION_PRESENTATIONS.has(value || "")) {
2234
+ pushError(errors, `Projection ${statement.id} ui_collections for '${screenId}' has invalid view '${value}'`, entry.loc);
2235
+ }
2236
+
2237
+ if (operation === "refresh" && !["manual", "pull_to_refresh", "auto"].includes(value || "")) {
2238
+ pushError(errors, `Projection ${statement.id} ui_collections for '${screenId}' has invalid refresh '${value}'`, entry.loc);
2239
+ }
2240
+ }
2241
+ }
2242
+
2243
+ function validateProjectionUiActions(errors, statement, fieldMap, registry) {
2244
+ if (statement.kind !== "projection") {
2245
+ return;
2246
+ }
2247
+
2248
+ const actionsField = fieldMap.get("ui_actions")?.[0];
2249
+ if (!actionsField || actionsField.value.type !== "block") {
2250
+ return;
2251
+ }
2252
+
2253
+ const screens = collectProjectionUiScreens(statement, fieldMap);
2254
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
2255
+
2256
+ for (const entry of actionsField.value.entries) {
2257
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
2258
+ const [keyword, screenId, actionKeyword, capabilityId, prominenceKeyword, prominence, placementKeyword, placement] = tokens;
2259
+
2260
+ if (keyword !== "screen") {
2261
+ pushError(errors, `Projection ${statement.id} ui_actions entries must start with 'screen'`, entry.loc);
2262
+ continue;
2263
+ }
2264
+ if (!screens.has(screenId)) {
2265
+ pushError(errors, `Projection ${statement.id} ui_actions references unknown screen '${screenId}'`, entry.loc);
2266
+ }
2267
+ if (actionKeyword !== "action") {
2268
+ pushError(errors, `Projection ${statement.id} ui_actions for '${screenId}' must use 'action'`, entry.loc);
2269
+ }
2270
+ const capability = registry.get(capabilityId);
2271
+ if (!capability) {
2272
+ pushError(errors, `Projection ${statement.id} ui_actions references missing capability '${capabilityId}'`, entry.loc);
2273
+ } else if (capability.kind !== "capability") {
2274
+ pushError(errors, `Projection ${statement.id} ui_actions must reference a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
2275
+ } else if (!realized.has(capabilityId)) {
2276
+ pushError(errors, `Projection ${statement.id} ui_actions for '${screenId}' capability '${capabilityId}' must also appear in 'realizes'`, entry.loc);
2277
+ }
2278
+ if (prominenceKeyword !== "prominence") {
2279
+ pushError(errors, `Projection ${statement.id} ui_actions for '${screenId}' must use 'prominence'`, entry.loc);
2280
+ }
2281
+ if (!["primary", "secondary", "destructive", "contextual"].includes(prominence || "")) {
2282
+ pushError(errors, `Projection ${statement.id} ui_actions for '${screenId}' has invalid prominence '${prominence}'`, entry.loc);
2283
+ }
2284
+ if (placementKeyword && placementKeyword !== "placement") {
2285
+ pushError(errors, `Projection ${statement.id} ui_actions for '${screenId}' has unknown directive '${placementKeyword}'`, entry.loc);
2286
+ }
2287
+ if (placementKeyword === "placement" && !["toolbar", "menu", "bulk", "inline", "footer"].includes(placement || "")) {
2288
+ pushError(errors, `Projection ${statement.id} ui_actions for '${screenId}' has invalid placement '${placement}'`, entry.loc);
2289
+ }
2290
+ }
2291
+ }
2292
+
2293
+ function validateProjectionUiAppShell(errors, statement, fieldMap) {
2294
+ if (statement.kind !== "projection") {
2295
+ return;
2296
+ }
2297
+
2298
+ const shellField = fieldMap.get("ui_app_shell")?.[0];
2299
+ if (!shellField || shellField.value.type !== "block") {
2300
+ return;
2301
+ }
2302
+
2303
+ const seenKeys = new Set();
2304
+ for (const entry of shellField.value.entries) {
2305
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
2306
+ const [key, value, extra] = tokens;
2307
+ if (!["brand", "shell", "primary_nav", "secondary_nav", "utility_nav", "footer", "global_search", "notifications", "account_menu", "workspace_switcher", "windowing"].includes(key || "")) {
2308
+ pushError(errors, `Projection ${statement.id} ui_app_shell has unknown key '${key}'`, entry.loc);
2309
+ continue;
2310
+ }
2311
+ if (!value) {
2312
+ pushError(errors, `Projection ${statement.id} ui_app_shell is missing a value for '${key}'`, entry.loc);
2313
+ continue;
2314
+ }
2315
+ if (extra) {
2316
+ pushError(errors, `Projection ${statement.id} ui_app_shell '${key}' accepts exactly one value`, entry.loc);
2317
+ }
2318
+ if (seenKeys.has(key)) {
2319
+ pushError(errors, `Projection ${statement.id} ui_app_shell has duplicate key '${key}'`, entry.loc);
2320
+ }
2321
+ seenKeys.add(key);
2322
+
2323
+ if (key === "shell" && !["topbar", "sidebar", "dual_nav", "workspace", "wizard", "bottom_tabs", "split_view", "menu_bar"].includes(value)) {
2324
+ pushError(errors, `Projection ${statement.id} ui_app_shell has invalid shell '${value}'`, entry.loc);
2325
+ }
2326
+ if (["global_search", "notifications", "account_menu", "workspace_switcher"].includes(key) && !["true", "false"].includes(value)) {
2327
+ pushError(errors, `Projection ${statement.id} ui_app_shell '${key}' must be true or false`, entry.loc);
2328
+ }
2329
+ if (key === "windowing" && !["single_window", "multi_window"].includes(value)) {
2330
+ pushError(errors, `Projection ${statement.id} ui_app_shell has invalid windowing '${value}'`, entry.loc);
2331
+ }
2332
+ }
2333
+ }
2334
+
2335
+ function validateProjectionUiNavigation(errors, statement, fieldMap, registry) {
2336
+ if (statement.kind !== "projection") {
2337
+ return;
2338
+ }
2339
+
2340
+ const navigationField = fieldMap.get("ui_navigation")?.[0];
2341
+ if (!navigationField || navigationField.value.type !== "block") {
2342
+ return;
2343
+ }
2344
+
2345
+ const availableScreens = collectAvailableUiScreenIds(statement, fieldMap, registry);
2346
+ const groups = new Set();
2347
+
2348
+ for (const entry of navigationField.value.entries) {
2349
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
2350
+ const [targetKind, targetId] = tokens;
2351
+
2352
+ if (targetKind === "group") {
2353
+ if (!targetId || !IDENTIFIER_PATTERN.test(targetId)) {
2354
+ pushError(errors, `Projection ${statement.id} ui_navigation group entries must include a valid group id`, entry.loc);
2355
+ continue;
2356
+ }
2357
+ groups.add(targetId);
2358
+ const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `ui_navigation group '${targetId}'`);
2359
+ for (const key of directives.keys()) {
2360
+ if (!["label", "placement", "icon", "order", "pattern"].includes(key)) {
2361
+ pushError(errors, `Projection ${statement.id} ui_navigation group '${targetId}' has unknown directive '${key}'`, entry.loc);
2362
+ }
2363
+ }
2364
+ if (directives.has("placement") && !["primary", "secondary", "utility"].includes(directives.get("placement"))) {
2365
+ pushError(errors, `Projection ${statement.id} ui_navigation group '${targetId}' has invalid placement '${directives.get("placement")}'`, entry.loc);
2366
+ }
2367
+ if (directives.has("pattern") && !UI_NAVIGATION_PATTERNS.has(directives.get("pattern"))) {
2368
+ pushError(errors, `Projection ${statement.id} ui_navigation group '${targetId}' has invalid pattern '${directives.get("pattern")}'`, entry.loc);
2369
+ }
2370
+ continue;
2371
+ }
2372
+
2373
+ if (targetKind === "screen") {
2374
+ if (!availableScreens.has(targetId)) {
2375
+ pushError(errors, `Projection ${statement.id} ui_navigation references unknown screen '${targetId}'`, entry.loc);
2376
+ }
2377
+ const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `ui_navigation screen '${targetId}'`);
2378
+ for (const key of directives.keys()) {
2379
+ if (!["group", "label", "order", "visible", "default", "breadcrumb", "sitemap", "placement", "pattern"].includes(key)) {
2380
+ pushError(errors, `Projection ${statement.id} ui_navigation screen '${targetId}' has unknown directive '${key}'`, entry.loc);
2381
+ }
2382
+ }
2383
+ if (directives.has("visible") && !["true", "false"].includes(directives.get("visible"))) {
2384
+ pushError(errors, `Projection ${statement.id} ui_navigation screen '${targetId}' has invalid visible '${directives.get("visible")}'`, entry.loc);
2385
+ }
2386
+ if (directives.has("default") && !["true", "false"].includes(directives.get("default"))) {
2387
+ pushError(errors, `Projection ${statement.id} ui_navigation screen '${targetId}' has invalid default '${directives.get("default")}'`, entry.loc);
2388
+ }
2389
+ if (directives.has("placement") && !["primary", "secondary", "utility"].includes(directives.get("placement"))) {
2390
+ pushError(errors, `Projection ${statement.id} ui_navigation screen '${targetId}' has invalid placement '${directives.get("placement")}'`, entry.loc);
2391
+ }
2392
+ if (directives.has("sitemap") && !["include", "exclude"].includes(directives.get("sitemap"))) {
2393
+ pushError(errors, `Projection ${statement.id} ui_navigation screen '${targetId}' has invalid sitemap '${directives.get("sitemap")}'`, entry.loc);
2394
+ }
2395
+ if (directives.has("pattern") && !UI_NAVIGATION_PATTERNS.has(directives.get("pattern"))) {
2396
+ pushError(errors, `Projection ${statement.id} ui_navigation screen '${targetId}' has invalid pattern '${directives.get("pattern")}'`, entry.loc);
2397
+ }
2398
+ const breadcrumb = directives.get("breadcrumb");
2399
+ if (breadcrumb && breadcrumb !== "none" && !availableScreens.has(breadcrumb)) {
2400
+ pushError(errors, `Projection ${statement.id} ui_navigation screen '${targetId}' references unknown breadcrumb screen '${breadcrumb}'`, entry.loc);
2401
+ }
2402
+ continue;
2403
+ }
2404
+
2405
+ pushError(errors, `Projection ${statement.id} ui_navigation entries must start with 'group' or 'screen'`, entry.loc);
2406
+ }
2407
+
2408
+ for (const entry of navigationField.value.entries) {
2409
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
2410
+ if (tokens[0] !== "screen") {
2411
+ continue;
2412
+ }
2413
+ const directives = parseUiDirectiveMap(tokens, 2, [], statement, entry, "");
2414
+ if (directives.has("group") && !groups.has(directives.get("group"))) {
2415
+ pushError(errors, `Projection ${statement.id} ui_navigation screen '${tokens[1]}' references unknown group '${directives.get("group")}'`, entry.loc);
2416
+ }
2417
+ }
2418
+ }
2419
+
2420
+ function validateProjectionUiScreenRegions(errors, statement, fieldMap, registry) {
2421
+ if (statement.kind !== "projection") {
2422
+ return;
2423
+ }
2424
+
2425
+ const regionField = fieldMap.get("ui_screen_regions")?.[0];
2426
+ if (!regionField || regionField.value.type !== "block") {
2427
+ return;
2428
+ }
2429
+
2430
+ const availableScreens = collectAvailableUiScreenIds(statement, fieldMap, registry);
2431
+ for (const entry of regionField.value.entries) {
2432
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
2433
+ const [keyword, screenId, regionKeyword, regionName] = tokens;
2434
+
2435
+ if (keyword !== "screen") {
2436
+ pushError(errors, `Projection ${statement.id} ui_screen_regions entries must start with 'screen'`, entry.loc);
2437
+ continue;
2438
+ }
2439
+ if (!availableScreens.has(screenId)) {
2440
+ pushError(errors, `Projection ${statement.id} ui_screen_regions references unknown screen '${screenId}'`, entry.loc);
2441
+ }
2442
+ if (regionKeyword !== "region") {
2443
+ pushError(errors, `Projection ${statement.id} ui_screen_regions for '${screenId}' must use 'region'`, entry.loc);
2444
+ }
2445
+ if (!UI_REGION_KINDS.has(regionName || "")) {
2446
+ pushError(errors, `Projection ${statement.id} ui_screen_regions for '${screenId}' has invalid region '${regionName}'`, entry.loc);
2447
+ }
2448
+
2449
+ const directives = parseUiDirectiveMap(tokens, 4, errors, statement, entry, `ui_screen_regions for '${screenId}'`);
2450
+ for (const key of directives.keys()) {
2451
+ if (!["pattern", "placement", "title", "state", "variant"].includes(key)) {
2452
+ pushError(errors, `Projection ${statement.id} ui_screen_regions for '${screenId}' has unknown directive '${key}'`, entry.loc);
2453
+ }
2454
+ }
2455
+ if (directives.has("pattern") && !UI_PATTERN_KINDS.has(directives.get("pattern"))) {
2456
+ pushError(errors, `Projection ${statement.id} ui_screen_regions for '${screenId}' has invalid pattern '${directives.get("pattern")}'`, entry.loc);
2457
+ }
2458
+ if (directives.has("placement") && !["primary", "secondary", "supporting"].includes(directives.get("placement"))) {
2459
+ pushError(errors, `Projection ${statement.id} ui_screen_regions for '${screenId}' has invalid placement '${directives.get("placement")}'`, entry.loc);
2460
+ }
2461
+ if (directives.has("state") && !["loading", "empty", "error", "unauthorized", "not_found", "success"].includes(directives.get("state"))) {
2462
+ pushError(errors, `Projection ${statement.id} ui_screen_regions for '${screenId}' has invalid state '${directives.get("state")}'`, entry.loc);
2463
+ }
2464
+ }
2465
+ }
2466
+
2467
+ function validateProjectionUiComponents(errors, statement, fieldMap, registry) {
2468
+ if (statement.kind !== "projection") {
2469
+ return;
2470
+ }
2471
+
2472
+ const componentsField = fieldMap.get("ui_components")?.[0];
2473
+ if (!componentsField || componentsField.value.type !== "block") {
2474
+ return;
2475
+ }
2476
+
2477
+ const availableScreens = collectAvailableUiScreenIds(statement, fieldMap, registry);
2478
+ const availableRegions = collectAvailableUiRegionKeys(statement, registry);
2479
+ const availableRegionPatterns = collectAvailableUiRegionPatterns(statement, registry);
2480
+
2481
+ for (const entry of componentsField.value.entries) {
2482
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
2483
+ const [screenKeyword, screenId, regionKeyword, regionName, componentKeyword, componentId] = tokens;
2484
+
2485
+ if (screenKeyword !== "screen") {
2486
+ pushError(errors, `Projection ${statement.id} ui_components entries must start with 'screen'`, entry.loc);
2487
+ continue;
2488
+ }
2489
+ if (!availableScreens.has(screenId)) {
2490
+ pushError(errors, `Projection ${statement.id} ui_components references unknown screen '${screenId}'`, entry.loc);
2491
+ }
2492
+ if (regionKeyword !== "region") {
2493
+ pushError(errors, `Projection ${statement.id} ui_components for '${screenId}' must use 'region'`, entry.loc);
2494
+ }
2495
+ if (!UI_REGION_KINDS.has(regionName || "")) {
2496
+ pushError(errors, `Projection ${statement.id} ui_components for '${screenId}' has invalid region '${regionName}'`, entry.loc);
2497
+ } else if (!availableRegions.has(`${screenId}:${regionName}`)) {
2498
+ pushError(errors, `Projection ${statement.id} ui_components for '${screenId}' references undeclared region '${regionName}'`, entry.loc);
2499
+ }
2500
+ if (componentKeyword !== "component") {
2501
+ pushError(errors, `Projection ${statement.id} ui_components for '${screenId}' must use 'component'`, entry.loc);
2502
+ }
2503
+
2504
+ const component = registry.get(componentId);
2505
+ if (!component) {
2506
+ pushError(errors, `Projection ${statement.id} ui_components references missing component '${componentId}'`, entry.loc);
2507
+ continue;
2508
+ }
2509
+ if (component.kind !== "component") {
2510
+ pushError(errors, `Projection ${statement.id} ui_components must reference a component, found ${component.kind} '${component.id}'`, entry.loc);
2511
+ continue;
2512
+ }
2513
+
2514
+ const propNames = new Set(blockEntries(getFieldValue(component, "props"))
2515
+ .map((propEntry) => propEntry.items[0])
2516
+ .filter((item) => item?.type === "symbol")
2517
+ .map((item) => item.value));
2518
+ const eventNames = new Set(blockEntries(getFieldValue(component, "events"))
2519
+ .map((eventEntry) => eventEntry.items[0])
2520
+ .filter((item) => item?.type === "symbol")
2521
+ .map((item) => item.value));
2522
+ const componentRegions = symbolValues(getFieldValue(component, "regions"));
2523
+ const componentPatterns = symbolValues(getFieldValue(component, "patterns"));
2524
+ if (componentRegions.length > 0 && !componentRegions.includes(regionName)) {
2525
+ pushError(
2526
+ errors,
2527
+ `Projection ${statement.id} ui_components uses component '${componentId}' in region '${regionName}', but the component supports regions [${componentRegions.join(", ")}]`,
2528
+ entry.loc
2529
+ );
2530
+ }
2531
+ const regionPattern = availableRegionPatterns.get(`${screenId}:${regionName}`) || null;
2532
+ if (regionPattern && componentPatterns.length > 0 && !componentPatterns.includes(regionPattern)) {
2533
+ pushError(
2534
+ errors,
2535
+ `Projection ${statement.id} ui_components uses component '${componentId}' in '${screenId}:${regionName}' with pattern '${regionPattern}', but the component supports patterns [${componentPatterns.join(", ")}]`,
2536
+ entry.loc
2537
+ );
2538
+ }
2539
+
2540
+ for (let i = 6; i < tokens.length;) {
2541
+ const directive = tokens[i];
2542
+ if (directive === "data") {
2543
+ const propName = tokens[i + 1];
2544
+ const fromKeyword = tokens[i + 2];
2545
+ const sourceId = tokens[i + 3];
2546
+ if (!propName || fromKeyword !== "from" || !sourceId) {
2547
+ pushError(errors, `Projection ${statement.id} ui_components data bindings must use 'data <prop> from <source>'`, entry.loc);
2548
+ break;
2549
+ }
2550
+ if (!propNames.has(propName)) {
2551
+ pushError(errors, `Projection ${statement.id} ui_components references unknown prop '${propName}' on component '${componentId}'`, entry.loc);
2552
+ }
2553
+ const source = registry.get(sourceId);
2554
+ if (!source || !["capability", "projection", "shape", "entity"].includes(source.kind)) {
2555
+ pushError(errors, `Projection ${statement.id} ui_components data binding for '${propName}' references missing source '${sourceId}'`, entry.loc);
2556
+ }
2557
+ i += 4;
2558
+ continue;
2559
+ }
2560
+
2561
+ if (directive === "event") {
2562
+ const eventName = tokens[i + 1];
2563
+ const action = tokens[i + 2];
2564
+ const targetId = tokens[i + 3];
2565
+ if (!eventName || !action || !targetId) {
2566
+ pushError(errors, `Projection ${statement.id} ui_components event bindings must use 'event <event> <navigate|action> <target>'`, entry.loc);
2567
+ break;
2568
+ }
2569
+ if (!eventNames.has(eventName)) {
2570
+ pushError(errors, `Projection ${statement.id} ui_components references unknown event '${eventName}' on component '${componentId}'`, entry.loc);
2571
+ }
2572
+ if (action === "navigate") {
2573
+ if (!availableScreens.has(targetId)) {
2574
+ pushError(errors, `Projection ${statement.id} ui_components event '${eventName}' references unknown navigation target '${targetId}'`, entry.loc);
2575
+ }
2576
+ } else if (action === "action") {
2577
+ const target = registry.get(targetId);
2578
+ if (!target || target.kind !== "capability") {
2579
+ pushError(errors, `Projection ${statement.id} ui_components event '${eventName}' references missing capability action '${targetId}'`, entry.loc);
2580
+ }
2581
+ } else {
2582
+ pushError(errors, `Projection ${statement.id} ui_components event '${eventName}' has unsupported action '${action}'`, entry.loc);
2583
+ }
2584
+ i += 4;
2585
+ continue;
2586
+ }
2587
+
2588
+ pushError(errors, `Projection ${statement.id} ui_components has unknown directive '${directive}'`, entry.loc);
2589
+ break;
2590
+ }
2591
+ }
2592
+ }
2593
+
2594
+ function validateProjectionUiVisibility(errors, statement, fieldMap, registry) {
2595
+ if (statement.kind !== "projection") {
2596
+ return;
2597
+ }
2598
+
2599
+ const visibilityField = fieldMap.get("ui_visibility")?.[0];
2600
+ if (!visibilityField || visibilityField.value.type !== "block") {
2601
+ return;
2602
+ }
2603
+
2604
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
2605
+ for (const entry of visibilityField.value.entries) {
2606
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
2607
+ const [keyword, capabilityId, predicateKeyword, predicateType, predicateValue] = tokens;
2608
+
2609
+ if (keyword !== "action") {
2610
+ pushError(errors, `Projection ${statement.id} ui_visibility entries must start with 'action'`, entry.loc);
2611
+ continue;
2612
+ }
2613
+
2614
+ const capability = registry.get(capabilityId);
2615
+ if (!capability) {
2616
+ pushError(errors, `Projection ${statement.id} ui_visibility references missing capability '${capabilityId}'`, entry.loc);
2617
+ } else if (capability.kind !== "capability") {
2618
+ pushError(errors, `Projection ${statement.id} ui_visibility must reference a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
2619
+ } else if (!realized.has(capabilityId)) {
2620
+ pushError(errors, `Projection ${statement.id} ui_visibility action '${capabilityId}' must also appear in 'realizes'`, entry.loc);
2621
+ }
2622
+
2623
+ if (predicateKeyword !== "visible_if") {
2624
+ pushError(errors, `Projection ${statement.id} ui_visibility for '${capabilityId}' must use 'visible_if'`, entry.loc);
2625
+ }
2626
+ if (!["permission", "ownership", "claim"].includes(predicateType || "")) {
2627
+ pushError(errors, `Projection ${statement.id} ui_visibility for '${capabilityId}' has invalid predicate '${predicateType}'`, entry.loc);
2628
+ }
2629
+ if (!predicateValue) {
2630
+ pushError(errors, `Projection ${statement.id} ui_visibility for '${capabilityId}' must include a predicate value`, entry.loc);
2631
+ }
2632
+ if (predicateType === "ownership" && !["owner", "owner_or_admin", "project_member", "none"].includes(predicateValue || "")) {
2633
+ pushError(errors, `Projection ${statement.id} ui_visibility for '${capabilityId}' has invalid ownership '${predicateValue}'`, entry.loc);
2634
+ }
2635
+ const directiveTokens = blockSymbolItems(entry).map((item) => item.value);
2636
+ const directives = new Map();
2637
+ for (let i = 5; i < directiveTokens.length; i += 2) {
2638
+ const key = directiveTokens[i];
2639
+ const value = directiveTokens[i + 1];
2640
+ if (!value) {
2641
+ pushError(errors, `Projection ${statement.id} ui_visibility for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
2642
+ continue;
2643
+ }
2644
+ directives.set(key, value);
2645
+ }
2646
+ for (const key of directives.keys()) {
2647
+ if (!["claim_value"].includes(key)) {
2648
+ pushError(errors, `Projection ${statement.id} ui_visibility for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
2649
+ }
2650
+ }
2651
+ if (directives.get("claim_value") && predicateType !== "claim") {
2652
+ pushError(errors, `Projection ${statement.id} ui_visibility for '${capabilityId}' cannot declare claim_value without claim`, entry.loc);
2653
+ }
2654
+ }
2655
+ }
2656
+
2657
+ function validateProjectionUiLookups(errors, statement, fieldMap, registry) {
2658
+ if (statement.kind !== "projection") {
2659
+ return;
2660
+ }
2661
+
2662
+ const lookupsField = fieldMap.get("ui_lookups")?.[0];
2663
+ if (!lookupsField || lookupsField.value.type !== "block") {
2664
+ return;
2665
+ }
2666
+
2667
+ const screens = collectProjectionUiScreens(statement, fieldMap);
2668
+
2669
+ for (const entry of lookupsField.value.entries) {
2670
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
2671
+ const [keyword, screenId, fieldKeyword, fieldName, entityKeyword, entityId, labelKeyword, labelField, maybeEmptyKeyword, maybeEmptyLabel] = tokens;
2672
+
2673
+ if (keyword !== "screen") {
2674
+ pushError(errors, `Projection ${statement.id} ui_lookups entries must start with 'screen'`, entry.loc);
2675
+ continue;
2676
+ }
2677
+
2678
+ const screenEntry = screens.get(screenId);
2679
+ if (!screenEntry) {
2680
+ pushError(errors, `Projection ${statement.id} ui_lookups references unknown screen '${screenId}'`, entry.loc);
2681
+ continue;
2682
+ }
2683
+
2684
+ if (fieldKeyword !== "field") {
2685
+ pushError(errors, `Projection ${statement.id} ui_lookups for '${screenId}' must use 'field'`, entry.loc);
2686
+ }
2687
+ if (!fieldName) {
2688
+ pushError(errors, `Projection ${statement.id} ui_lookups for '${screenId}' must include a field name`, entry.loc);
2689
+ }
2690
+
2691
+ if (entityKeyword !== "entity") {
2692
+ pushError(errors, `Projection ${statement.id} ui_lookups for '${screenId}' must use 'entity'`, entry.loc);
2693
+ }
2694
+ const entity = entityId ? registry.get(entityId) : null;
2695
+ if (!entity) {
2696
+ pushError(errors, `Projection ${statement.id} ui_lookups for '${screenId}' references missing entity '${entityId}'`, entry.loc);
2697
+ } else if (entity.kind !== "entity") {
2698
+ pushError(errors, `Projection ${statement.id} ui_lookups for '${screenId}' must reference an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
2699
+ }
2700
+
2701
+ if (labelKeyword !== "label_field") {
2702
+ pushError(errors, `Projection ${statement.id} ui_lookups for '${screenId}' must use 'label_field'`, entry.loc);
2703
+ }
2704
+ if (!labelField) {
2705
+ pushError(errors, `Projection ${statement.id} ui_lookups for '${screenId}' must include a label_field`, entry.loc);
2706
+ }
2707
+
2708
+ if (maybeEmptyKeyword && maybeEmptyKeyword !== "empty_label") {
2709
+ pushError(errors, `Projection ${statement.id} ui_lookups for '${screenId}' has unknown directive '${maybeEmptyKeyword}'`, entry.loc);
2710
+ }
2711
+ if (maybeEmptyKeyword === "empty_label" && !maybeEmptyLabel) {
2712
+ pushError(errors, `Projection ${statement.id} ui_lookups for '${screenId}' must include a value for 'empty_label'`, entry.loc);
2713
+ }
2714
+
2715
+ const availableFields = resolveProjectionUiScreenFieldNames(registry, screenEntry, statement);
2716
+ if (fieldName && availableFields.size > 0 && !availableFields.has(fieldName)) {
2717
+ pushError(errors, `Projection ${statement.id} ui_lookups for '${screenId}' references unknown screen field '${fieldName}'`, entry.loc);
2718
+ }
2719
+
2720
+ if (entity?.kind === "entity") {
2721
+ const entityFieldNames = new Set(statementFieldNames(entity));
2722
+ if (labelField && !entityFieldNames.has(labelField)) {
2723
+ pushError(errors, `Projection ${statement.id} ui_lookups for '${screenId}' references unknown entity field '${labelField}' on '${entity.id}'`, entry.loc);
2724
+ }
2725
+ }
2726
+ }
2727
+ }
2728
+
2729
+ function validateProjectionUiRoutes(errors, statement, fieldMap, registry) {
2730
+ if (statement.kind !== "projection") {
2731
+ return;
2732
+ }
2733
+
2734
+ const routesField = fieldMap.get("ui_routes")?.[0];
2735
+ if (!routesField || routesField.value.type !== "block") {
2736
+ return;
2737
+ }
2738
+
2739
+ const availableScreens = collectAvailableUiScreenIds(statement, fieldMap, registry);
2740
+ const seenPaths = new Set();
2741
+ const platform = symbolValue(getFieldValue(statement, "platform"));
2742
+
2743
+ for (const entry of routesField.value.entries) {
2744
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
2745
+ const [keyword, screenId, pathKeyword, routePath] = tokens;
2746
+
2747
+ if (keyword !== "screen") {
2748
+ pushError(errors, `Projection ${statement.id} ui_routes entries must start with 'screen'`, entry.loc);
2749
+ continue;
2750
+ }
2751
+ if (!availableScreens.has(screenId)) {
2752
+ pushError(errors, `Projection ${statement.id} ui_routes references unknown screen '${screenId}'`, entry.loc);
2753
+ }
2754
+ if (pathKeyword !== "path") {
2755
+ pushError(errors, `Projection ${statement.id} ui_routes for '${screenId}' must use 'path'`, entry.loc);
2756
+ }
2757
+ if (!routePath) {
2758
+ pushError(errors, `Projection ${statement.id} ui_routes for '${screenId}' must include a path`, entry.loc);
2759
+ continue;
2760
+ }
2761
+ if ((platform === "ui_web" || platform === "ui_ios") && !routePath.startsWith("/")) {
2762
+ pushError(errors, `Projection ${statement.id} ui_routes for '${screenId}' must use an absolute path`, entry.loc);
2763
+ }
2764
+ if (seenPaths.has(routePath)) {
2765
+ pushError(errors, `Projection ${statement.id} ui_routes has duplicate path '${routePath}'`, entry.loc);
2766
+ }
2767
+ seenPaths.add(routePath);
2768
+ }
2769
+ }
2770
+
2771
+ function validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry, surfaceBlockKey, expectedPlatform) {
2772
+ if (statement.kind !== "projection") {
2773
+ return;
2774
+ }
2775
+
2776
+ const surfaceField = fieldMap.get(surfaceBlockKey)?.[0];
2777
+ if (!surfaceField || surfaceField.value.type !== "block") {
2778
+ return;
2779
+ }
2780
+
2781
+ const platform = symbolValue(getFieldValue(statement, "platform"));
2782
+ if (platform !== expectedPlatform) {
2783
+ pushError(errors, `Projection ${statement.id} may only use '${surfaceBlockKey}' when platform is '${expectedPlatform}'`, surfaceField.loc);
2784
+ return;
2785
+ }
2786
+
2787
+ const availableScreens = collectAvailableUiScreenIds(statement, fieldMap, registry);
2788
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
2789
+ for (const entry of surfaceField.value.entries) {
2790
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
2791
+ const [targetKind, targetId, directive, value] = tokens;
2792
+
2793
+ if (targetKind === "screen") {
2794
+ if (!availableScreens.has(targetId)) {
2795
+ pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} references unknown screen '${targetId}'`, entry.loc);
2796
+ }
2797
+ if (!["layout", "desktop_variant", "mobile_variant", "present", "shell", "collection", "breadcrumbs", "state_style"].includes(directive || "")) {
2798
+ pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for screen '${targetId}' has unknown directive '${directive}'`, entry.loc);
2799
+ }
2800
+ if (directive === "desktop_variant" && !UI_COLLECTION_PRESENTATIONS.has(value || "")) {
2801
+ pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for screen '${targetId}' has invalid desktop_variant '${value}'`, entry.loc);
2802
+ }
2803
+ if (directive === "mobile_variant" && !UI_COLLECTION_PRESENTATIONS.has(value || "")) {
2804
+ pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for screen '${targetId}' has invalid mobile_variant '${value}'`, entry.loc);
2805
+ }
2806
+ if (directive === "collection" && !UI_COLLECTION_PRESENTATIONS.has(value || "")) {
2807
+ pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for screen '${targetId}' has invalid collection '${value}'`, entry.loc);
2808
+ }
2809
+ if (directive === "shell" && !["topbar", "sidebar", "dual_nav", "workspace", "wizard", "bottom_tabs", "split_view", "menu_bar"].includes(value || "")) {
2810
+ pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for screen '${targetId}' has invalid shell '${value}'`, entry.loc);
2811
+ }
2812
+ if (directive === "present" && !["page", "modal", "drawer", "sheet", "bottom_sheet", "popover"].includes(value || "")) {
2813
+ pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for screen '${targetId}' has invalid present '${value}'`, entry.loc);
2814
+ }
2815
+ if (directive === "breadcrumbs" && !["visible", "hidden"].includes(value || "")) {
2816
+ pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for screen '${targetId}' has invalid breadcrumbs '${value}'`, entry.loc);
2817
+ }
2818
+ if (directive === "state_style" && !["inline", "panel", "full_page"].includes(value || "")) {
2819
+ pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for screen '${targetId}' has invalid state_style '${value}'`, entry.loc);
2820
+ }
2821
+ continue;
2822
+ }
2823
+
2824
+ if (targetKind === "action") {
2825
+ const capability = registry.get(targetId);
2826
+ if (!capability) {
2827
+ pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} references missing capability '${targetId}'`, entry.loc);
2828
+ } else if (capability.kind !== "capability") {
2829
+ pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} must reference a capability for action '${targetId}', found ${capability.kind} '${capability.id}'`, entry.loc);
2830
+ } else if (!realized.has(targetId)) {
2831
+ pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} action '${targetId}' must also appear in 'realizes'`, entry.loc);
2832
+ }
2833
+ if (!["confirm", "present", "placement"].includes(directive || "")) {
2834
+ pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for action '${targetId}' has unknown directive '${directive}'`, entry.loc);
2835
+ }
2836
+ if (directive === "confirm" && !["modal", "inline", "sheet", "bottom_sheet", "popover"].includes(value || "")) {
2837
+ pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for action '${targetId}' has invalid confirm mode '${value}'`, entry.loc);
2838
+ }
2839
+ if (directive === "present" && !["button", "menu_item", "split_button", "bulk_action", "drawer", "sheet", "bottom_sheet", "fab", "popover"].includes(value || "")) {
2840
+ pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for action '${targetId}' has invalid present mode '${value}'`, entry.loc);
2841
+ }
2842
+ if (directive === "placement" && !["toolbar", "menu", "bulk", "inline", "footer"].includes(value || "")) {
2843
+ pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for action '${targetId}' has invalid placement '${value}'`, entry.loc);
2844
+ }
2845
+ continue;
2846
+ }
2847
+
2848
+ pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} entries must start with 'screen' or 'action'`, entry.loc);
2849
+ }
2850
+ }
2851
+
2852
+ function validateProjectionUiWeb(errors, statement, fieldMap, registry) {
2853
+ validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry, "ui_web", "ui_web");
2854
+ }
2855
+
2856
+ function validateProjectionUiIos(errors, statement, fieldMap, registry) {
2857
+ validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry, "ui_ios", "ui_ios");
2858
+ }
2859
+
2860
+ function validateProjectionGeneratorDefaults(errors, statement, fieldMap) {
2861
+ if (statement.kind !== "projection") {
2862
+ return;
2863
+ }
2864
+
2865
+ const generatorField = fieldMap.get("generator_defaults")?.[0];
2866
+ if (!generatorField || generatorField.value.type !== "block") {
2867
+ return;
2868
+ }
2869
+
2870
+ for (const entry of generatorField.value.entries) {
2871
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
2872
+ const [key, value] = tokens;
2873
+ if (!["profile", "language", "styling"].includes(key || "")) {
2874
+ pushError(errors, `Projection ${statement.id} generator_defaults has unknown key '${key}'`, entry.loc);
2875
+ continue;
2876
+ }
2877
+ if (!value) {
2878
+ pushError(errors, `Projection ${statement.id} generator_defaults is missing a value for '${key}'`, entry.loc);
2879
+ continue;
2880
+ }
2881
+ if (key === "profile" && !["vanilla", "sveltekit", "react", "swiftui", "postgres_sql", "sqlite_sql", "prisma", "drizzle"].includes(value)) {
2882
+ pushError(errors, `Projection ${statement.id} generator_defaults has unsupported profile '${value}'`, entry.loc);
2883
+ }
2884
+ if (key === "language" && !["typescript", "javascript", "swift", "sql"].includes(value)) {
2885
+ pushError(errors, `Projection ${statement.id} generator_defaults has unsupported language '${value}'`, entry.loc);
2886
+ }
2887
+ if (key === "styling" && !["tailwind", "css"].includes(value)) {
2888
+ pushError(errors, `Projection ${statement.id} generator_defaults has unsupported styling '${value}'`, entry.loc);
2889
+ }
2890
+ }
2891
+ }
2892
+
2893
+ function validateProjectionDbTables(errors, statement, fieldMap, registry) {
2894
+ if (statement.kind !== "projection") {
2895
+ return;
2896
+ }
2897
+
2898
+ const dbTablesField = fieldMap.get("db_tables")?.[0];
2899
+ if (!dbTablesField || dbTablesField.value.type !== "block") {
2900
+ return;
2901
+ }
2902
+
2903
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
2904
+ const seenTables = new Set();
2905
+ for (const entry of dbTablesField.value.entries) {
2906
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
2907
+ const [entityId, tableKeyword, tableName] = tokens;
2908
+ const entity = registry.get(entityId);
2909
+
2910
+ if (!entity) {
2911
+ pushError(errors, `Projection ${statement.id} db_tables references missing entity '${entityId}'`, entry.loc);
2912
+ continue;
2913
+ }
2914
+ if (entity.kind !== "entity") {
2915
+ pushError(errors, `Projection ${statement.id} db_tables must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
2916
+ }
2917
+ if (!realized.has(entityId)) {
2918
+ pushError(errors, `Projection ${statement.id} db_tables entity '${entityId}' must also appear in 'realizes'`, entry.loc);
2919
+ }
2920
+ if (tableKeyword !== "table") {
2921
+ pushError(errors, `Projection ${statement.id} db_tables for '${entityId}' must use 'table'`, entry.loc);
2922
+ }
2923
+ if (!tableName) {
2924
+ pushError(errors, `Projection ${statement.id} db_tables for '${entityId}' must include a table name`, entry.loc);
2925
+ } else if (seenTables.has(tableName)) {
2926
+ pushError(errors, `Projection ${statement.id} db_tables has duplicate table name '${tableName}'`, entry.loc);
2927
+ }
2928
+ seenTables.add(tableName);
2929
+ }
2930
+ }
2931
+
2932
+ function validateProjectionDbColumns(errors, statement, fieldMap, registry) {
2933
+ if (statement.kind !== "projection") {
2934
+ return;
2935
+ }
2936
+
2937
+ const dbColumnsField = fieldMap.get("db_columns")?.[0];
2938
+ if (!dbColumnsField || dbColumnsField.value.type !== "block") {
2939
+ return;
2940
+ }
2941
+
2942
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
2943
+ for (const entry of dbColumnsField.value.entries) {
2944
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
2945
+ const [entityId, fieldKeyword, fieldName, columnKeyword, columnName] = tokens;
2946
+ const entity = registry.get(entityId);
2947
+
2948
+ if (!entity) {
2949
+ pushError(errors, `Projection ${statement.id} db_columns references missing entity '${entityId}'`, entry.loc);
2950
+ continue;
2951
+ }
2952
+ if (entity.kind !== "entity") {
2953
+ pushError(errors, `Projection ${statement.id} db_columns must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
2954
+ }
2955
+ if (!realized.has(entityId)) {
2956
+ pushError(errors, `Projection ${statement.id} db_columns entity '${entityId}' must also appear in 'realizes'`, entry.loc);
2957
+ }
2958
+ if (fieldKeyword !== "field") {
2959
+ pushError(errors, `Projection ${statement.id} db_columns for '${entityId}' must use 'field'`, entry.loc);
2960
+ }
2961
+ if (columnKeyword !== "column") {
2962
+ pushError(errors, `Projection ${statement.id} db_columns for '${entityId}' must use 'column'`, entry.loc);
2963
+ }
2964
+ const entityFieldNames = new Set(statementFieldNames(entity));
2965
+ if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
2966
+ pushError(errors, `Projection ${statement.id} db_columns references unknown field '${fieldName}' on ${entityId}`, entry.loc);
2967
+ }
2968
+ if (!columnName) {
2969
+ pushError(errors, `Projection ${statement.id} db_columns for '${entityId}.${fieldName}' must include a column name`, entry.loc);
2970
+ }
2971
+ }
2972
+ }
2973
+
2974
+ function validateProjectionDbKeys(errors, statement, fieldMap, registry) {
2975
+ if (statement.kind !== "projection") {
2976
+ return;
2977
+ }
2978
+
2979
+ const dbKeysField = fieldMap.get("db_keys")?.[0];
2980
+ if (!dbKeysField || dbKeysField.value.type !== "block") {
2981
+ return;
2982
+ }
2983
+
2984
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
2985
+ for (const entry of dbKeysField.value.entries) {
2986
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
2987
+ const [entityId, keyType] = tokens;
2988
+ const entity = registry.get(entityId);
2989
+
2990
+ if (!entity) {
2991
+ pushError(errors, `Projection ${statement.id} db_keys references missing entity '${entityId}'`, entry.loc);
2992
+ continue;
2993
+ }
2994
+ if (entity.kind !== "entity") {
2995
+ pushError(errors, `Projection ${statement.id} db_keys must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
2996
+ }
2997
+ if (!realized.has(entityId)) {
2998
+ pushError(errors, `Projection ${statement.id} db_keys entity '${entityId}' must also appear in 'realizes'`, entry.loc);
2999
+ }
3000
+ if (!["primary", "unique"].includes(keyType || "")) {
3001
+ pushError(errors, `Projection ${statement.id} db_keys for '${entityId}' has invalid key type '${keyType}'`, entry.loc);
3002
+ }
3003
+ const fieldList = entry.items[2];
3004
+ if (!fieldList || fieldList.type !== "list" || fieldList.items.length === 0) {
3005
+ pushError(errors, `Projection ${statement.id} db_keys for '${entityId}' must include a non-empty field list`, entry.loc);
3006
+ continue;
3007
+ }
3008
+ const entityFieldNames = new Set(statementFieldNames(entity));
3009
+ for (const item of fieldList.items) {
3010
+ if (item.type === "symbol" && entityFieldNames.size > 0 && !entityFieldNames.has(item.value)) {
3011
+ pushError(errors, `Projection ${statement.id} db_keys references unknown field '${item.value}' on ${entityId}`, item.loc);
3012
+ }
3013
+ }
3014
+ }
3015
+ }
3016
+
3017
+ function validateProjectionDbIndexes(errors, statement, fieldMap, registry) {
3018
+ if (statement.kind !== "projection") {
3019
+ return;
3020
+ }
3021
+
3022
+ const dbIndexesField = fieldMap.get("db_indexes")?.[0];
3023
+ if (!dbIndexesField || dbIndexesField.value.type !== "block") {
3024
+ return;
3025
+ }
3026
+
3027
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
3028
+ for (const entry of dbIndexesField.value.entries) {
3029
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
3030
+ const [entityId, indexType] = tokens;
3031
+ const entity = registry.get(entityId);
3032
+
3033
+ if (!entity) {
3034
+ pushError(errors, `Projection ${statement.id} db_indexes references missing entity '${entityId}'`, entry.loc);
3035
+ continue;
3036
+ }
3037
+ if (entity.kind !== "entity") {
3038
+ pushError(errors, `Projection ${statement.id} db_indexes must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
3039
+ }
3040
+ if (!realized.has(entityId)) {
3041
+ pushError(errors, `Projection ${statement.id} db_indexes entity '${entityId}' must also appear in 'realizes'`, entry.loc);
3042
+ }
3043
+ if (!["index", "unique"].includes(indexType || "")) {
3044
+ pushError(errors, `Projection ${statement.id} db_indexes for '${entityId}' has invalid index type '${indexType}'`, entry.loc);
3045
+ }
3046
+ const fieldList = entry.items[2];
3047
+ if (!fieldList || fieldList.type !== "list" || fieldList.items.length === 0) {
3048
+ pushError(errors, `Projection ${statement.id} db_indexes for '${entityId}' must include a non-empty field list`, entry.loc);
3049
+ continue;
3050
+ }
3051
+ const entityFieldNames = new Set(statementFieldNames(entity));
3052
+ for (const item of fieldList.items) {
3053
+ if (item.type === "symbol" && entityFieldNames.size > 0 && !entityFieldNames.has(item.value)) {
3054
+ pushError(errors, `Projection ${statement.id} db_indexes references unknown field '${item.value}' on ${entityId}`, item.loc);
3055
+ }
3056
+ }
3057
+ }
3058
+ }
3059
+
3060
+ function validateProjectionDbRelations(errors, statement, fieldMap, registry) {
3061
+ if (statement.kind !== "projection") {
3062
+ return;
3063
+ }
3064
+
3065
+ const dbRelationsField = fieldMap.get("db_relations")?.[0];
3066
+ if (!dbRelationsField || dbRelationsField.value.type !== "block") {
3067
+ return;
3068
+ }
3069
+
3070
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
3071
+ for (const entry of dbRelationsField.value.entries) {
3072
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
3073
+ const [entityId, relationType, fieldName, referencesKeyword, targetRef, onDeleteKeyword, onDeleteValue] = tokens;
3074
+ const entity = registry.get(entityId);
3075
+
3076
+ if (!entity) {
3077
+ pushError(errors, `Projection ${statement.id} db_relations references missing entity '${entityId}'`, entry.loc);
3078
+ continue;
3079
+ }
3080
+ if (entity.kind !== "entity") {
3081
+ pushError(errors, `Projection ${statement.id} db_relations must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
3082
+ }
3083
+ if (!realized.has(entityId)) {
3084
+ pushError(errors, `Projection ${statement.id} db_relations entity '${entityId}' must also appear in 'realizes'`, entry.loc);
3085
+ }
3086
+ if (relationType !== "foreign_key") {
3087
+ pushError(errors, `Projection ${statement.id} db_relations for '${entityId}' must use 'foreign_key'`, entry.loc);
3088
+ }
3089
+ if (referencesKeyword !== "references") {
3090
+ pushError(errors, `Projection ${statement.id} db_relations for '${entityId}' must use 'references'`, entry.loc);
3091
+ }
3092
+ if (onDeleteKeyword && onDeleteKeyword !== "on_delete") {
3093
+ pushError(errors, `Projection ${statement.id} db_relations for '${entityId}' has unexpected token '${onDeleteKeyword}'`, entry.loc);
3094
+ }
3095
+ if (onDeleteValue && !["cascade", "restrict", "set_null", "no_action"].includes(onDeleteValue)) {
3096
+ pushError(errors, `Projection ${statement.id} db_relations for '${entityId}' has invalid on_delete '${onDeleteValue}'`, entry.loc);
3097
+ }
3098
+ const entityFieldNames = new Set(statementFieldNames(entity));
3099
+ if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
3100
+ pushError(errors, `Projection ${statement.id} db_relations references unknown field '${fieldName}' on ${entityId}`, entry.loc);
3101
+ }
3102
+ const [targetEntityId, targetFieldName] = (targetRef || "").split(".");
3103
+ const targetEntity = registry.get(targetEntityId);
3104
+ if (!targetEntity) {
3105
+ pushError(errors, `Projection ${statement.id} db_relations references missing target entity '${targetEntityId}'`, entry.loc);
3106
+ continue;
3107
+ }
3108
+ if (targetEntity.kind !== "entity") {
3109
+ pushError(errors, `Projection ${statement.id} db_relations must reference an entity target, found ${targetEntity.kind} '${targetEntity.id}'`, entry.loc);
3110
+ }
3111
+ const targetFieldNames = new Set(statementFieldNames(targetEntity));
3112
+ if (targetFieldName && targetFieldNames.size > 0 && !targetFieldNames.has(targetFieldName)) {
3113
+ pushError(errors, `Projection ${statement.id} db_relations references unknown target field '${targetFieldName}' on ${targetEntityId}`, entry.loc);
3114
+ }
3115
+ }
3116
+ }
3117
+
3118
+ function validateProjectionDbLifecycle(errors, statement, fieldMap, registry) {
3119
+ if (statement.kind !== "projection") {
3120
+ return;
3121
+ }
3122
+
3123
+ const dbLifecycleField = fieldMap.get("db_lifecycle")?.[0];
3124
+ if (!dbLifecycleField || dbLifecycleField.value.type !== "block") {
3125
+ return;
3126
+ }
3127
+
3128
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
3129
+ for (const entry of dbLifecycleField.value.entries) {
3130
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
3131
+ const [entityId, lifecycleType] = tokens;
3132
+ const entity = registry.get(entityId);
3133
+
3134
+ if (!entity) {
3135
+ pushError(errors, `Projection ${statement.id} db_lifecycle references missing entity '${entityId}'`, entry.loc);
3136
+ continue;
3137
+ }
3138
+ if (entity.kind !== "entity") {
3139
+ pushError(errors, `Projection ${statement.id} db_lifecycle must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
3140
+ }
3141
+ if (!realized.has(entityId)) {
3142
+ pushError(errors, `Projection ${statement.id} db_lifecycle entity '${entityId}' must also appear in 'realizes'`, entry.loc);
3143
+ }
3144
+
3145
+ const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `db_lifecycle for '${entityId}'`);
3146
+ if (!["soft_delete", "timestamps"].includes(lifecycleType || "")) {
3147
+ pushError(errors, `Projection ${statement.id} db_lifecycle for '${entityId}' has invalid lifecycle '${lifecycleType}'`, entry.loc);
3148
+ continue;
3149
+ }
3150
+
3151
+ const entityFieldNames = new Set(statementFieldNames(entity));
3152
+ if (lifecycleType === "soft_delete") {
3153
+ for (const requiredKey of ["field", "value"]) {
3154
+ if (!directives.has(requiredKey)) {
3155
+ pushError(errors, `Projection ${statement.id} db_lifecycle for '${entityId}' must include '${requiredKey}' for soft_delete`, entry.loc);
3156
+ }
3157
+ }
3158
+ const fieldName = directives.get("field");
3159
+ if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
3160
+ pushError(errors, `Projection ${statement.id} db_lifecycle references unknown field '${fieldName}' on ${entityId}`, entry.loc);
3161
+ }
3162
+ }
3163
+
3164
+ if (lifecycleType === "timestamps") {
3165
+ for (const requiredKey of ["created_at", "updated_at"]) {
3166
+ if (!directives.has(requiredKey)) {
3167
+ pushError(errors, `Projection ${statement.id} db_lifecycle for '${entityId}' must include '${requiredKey}' for timestamps`, entry.loc);
3168
+ }
3169
+ const fieldName = directives.get(requiredKey);
3170
+ if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
3171
+ pushError(errors, `Projection ${statement.id} db_lifecycle references unknown field '${fieldName}' on ${entityId}`, entry.loc);
3172
+ }
3173
+ }
3174
+ }
3175
+ }
3176
+ }
3177
+
3178
+ export function buildRegistry(workspaceAst, errors) {
3179
+ const registry = new Map();
3180
+
3181
+ for (const file of workspaceAst.files) {
3182
+ for (const statement of file.statements) {
3183
+ if (!STATEMENT_KINDS.has(statement.kind)) {
3184
+ pushError(errors, `Unknown statement kind '${statement.kind}'`, statement.loc);
3185
+ }
3186
+
3187
+ if (!IDENTIFIER_PATTERN.test(statement.id)) {
3188
+ pushError(errors, `Invalid identifier '${statement.id}'`, statement.loc);
3189
+ }
3190
+
3191
+ if (registry.has(statement.id)) {
3192
+ pushError(errors, `Duplicate statement id '${statement.id}'`, statement.loc);
3193
+ continue;
3194
+ }
3195
+
3196
+ registry.set(statement.id, statement);
3197
+ }
3198
+ }
3199
+
3200
+ return registry;
3201
+ }
3202
+
3203
+ function validateDocs(workspaceAst, registry, errors) {
3204
+ const docs = workspaceAst.docs || [];
3205
+ const docRegistry = new Map();
3206
+
3207
+ for (const doc of docs) {
3208
+ if (doc.parseError) {
3209
+ pushError(errors, doc.parseError.message, doc.parseError.loc);
3210
+ continue;
3211
+ }
3212
+
3213
+ const { metadata } = doc;
3214
+ for (const required of ["id", "kind", "title", "status"]) {
3215
+ if (!metadata[required]) {
3216
+ pushError(errors, `Missing required doc metadata '${required}'`, doc.loc);
3217
+ }
3218
+ }
3219
+
3220
+ if (metadata.id && !IDENTIFIER_PATTERN.test(metadata.id)) {
3221
+ pushError(errors, `Invalid doc identifier '${metadata.id}'`, doc.loc);
3222
+ }
3223
+
3224
+ if (metadata.kind && !DOC_KINDS.has(metadata.kind)) {
3225
+ pushError(errors, `Unsupported doc kind '${metadata.kind}'`, doc.loc);
3226
+ }
3227
+
3228
+ if (metadata.status && !DOC_STATUSES.has(metadata.status)) {
3229
+ pushError(errors, `Unsupported doc status '${metadata.status}'`, doc.loc);
3230
+ }
3231
+
3232
+ if (metadata.confidence && !DOC_CONFIDENCE.has(metadata.confidence)) {
3233
+ pushError(errors, `Unsupported doc confidence '${metadata.confidence}'`, doc.loc);
3234
+ }
3235
+
3236
+ if (metadata.review_required != null && typeof metadata.review_required !== "boolean") {
3237
+ pushError(errors, "Doc metadata 'review_required' must be a boolean", doc.loc);
3238
+ }
3239
+
3240
+ for (const key of DOC_ARRAY_FIELDS) {
3241
+ if (metadata[key] != null && !Array.isArray(metadata[key])) {
3242
+ pushError(errors, `Doc metadata '${key}' must be a list`, doc.loc);
3243
+ }
3244
+ }
3245
+
3246
+ if (metadata.id) {
3247
+ if (docRegistry.has(metadata.id)) {
3248
+ pushError(errors, `Duplicate doc id '${metadata.id}'`, doc.loc);
3249
+ } else {
3250
+ docRegistry.set(metadata.id, doc);
3251
+ }
3252
+ }
3253
+ }
3254
+
3255
+ for (const doc of docs) {
3256
+ if (doc.parseError) {
3257
+ continue;
3258
+ }
3259
+ const { metadata } = doc;
3260
+
3261
+ for (const entityId of metadata.related_entities || []) {
3262
+ const statement = registry.get(entityId);
3263
+ if (!statement || statement.kind !== "entity") {
3264
+ pushError(errors, `Doc '${metadata.id}' references missing entity '${entityId}'`, doc.loc);
3265
+ }
3266
+ }
3267
+
3268
+ for (const capabilityId of metadata.related_capabilities || []) {
3269
+ const statement = registry.get(capabilityId);
3270
+ if (!statement || statement.kind !== "capability") {
3271
+ pushError(errors, `Doc '${metadata.id}' references missing capability '${capabilityId}'`, doc.loc);
3272
+ }
3273
+ }
3274
+
3275
+ for (const actorId of metadata.related_actors || []) {
3276
+ const statement = registry.get(actorId);
3277
+ if (!statement || statement.kind !== "actor") {
3278
+ pushError(errors, `Doc '${metadata.id}' references missing actor '${actorId}'`, doc.loc);
3279
+ }
3280
+ }
3281
+
3282
+ for (const roleId of metadata.related_roles || []) {
3283
+ const statement = registry.get(roleId);
3284
+ if (!statement || statement.kind !== "role") {
3285
+ pushError(errors, `Doc '${metadata.id}' references missing role '${roleId}'`, doc.loc);
3286
+ }
3287
+ }
3288
+
3289
+ for (const ruleId of metadata.related_rules || []) {
3290
+ const statement = registry.get(ruleId);
3291
+ if (!statement || statement.kind !== "rule") {
3292
+ pushError(errors, `Doc '${metadata.id}' references missing rule '${ruleId}'`, doc.loc);
3293
+ }
3294
+ }
3295
+
3296
+ for (const workflowDocId of metadata.related_workflows || []) {
3297
+ const relatedDoc = docRegistry.get(workflowDocId);
3298
+ if (!relatedDoc || relatedDoc.metadata.kind !== "workflow") {
3299
+ pushError(errors, `Doc '${metadata.id}' references missing workflow doc '${workflowDocId}'`, doc.loc);
3300
+ }
3301
+ }
3302
+
3303
+ for (const decisionId of metadata.related_decisions || []) {
3304
+ const statement = registry.get(decisionId);
3305
+ if (!statement || statement.kind !== "decision") {
3306
+ pushError(errors, `Doc '${metadata.id}' references missing decision '${decisionId}'`, doc.loc);
3307
+ }
3308
+ }
3309
+
3310
+ for (const shapeId of metadata.related_shapes || []) {
3311
+ const statement = registry.get(shapeId);
3312
+ if (!statement || statement.kind !== "shape") {
3313
+ pushError(errors, `Doc '${metadata.id}' references missing shape '${shapeId}'`, doc.loc);
3314
+ }
3315
+ }
3316
+
3317
+ for (const projectionId of metadata.related_projections || []) {
3318
+ const statement = registry.get(projectionId);
3319
+ if (!statement || statement.kind !== "projection") {
3320
+ pushError(errors, `Doc '${metadata.id}' references missing projection '${projectionId}'`, doc.loc);
3321
+ }
3322
+ }
3323
+
3324
+ for (const relatedDocId of metadata.related_docs || []) {
3325
+ if (!docRegistry.has(relatedDocId)) {
3326
+ pushError(errors, `Doc '${metadata.id}' references missing doc '${relatedDocId}'`, doc.loc);
3327
+ }
3328
+ }
3329
+
3330
+ for (const [fieldName, expectedKind] of Object.entries(DOC_REFERENCE_FIELDS)) {
3331
+ const value = metadata[fieldName];
3332
+ if (value == null) continue;
3333
+ if (typeof value !== "string") {
3334
+ pushError(errors, `Doc metadata '${fieldName}' must be a single id`, doc.loc);
3335
+ continue;
3336
+ }
3337
+ const target = registry.get(value);
3338
+ if (!target) {
3339
+ pushError(errors, `Doc '${metadata.id}' references missing ${expectedKind} '${value}'`, doc.loc);
3340
+ continue;
3341
+ }
3342
+ if (target.kind !== expectedKind) {
3343
+ pushError(
3344
+ errors,
3345
+ `Doc '${metadata.id}' ${fieldName} must reference a ${expectedKind}, found ${target.kind} '${target.id}'`,
3346
+ doc.loc
3347
+ );
3348
+ }
3349
+ }
3350
+ }
3351
+ }
3352
+
3353
+ export function validateWorkspace(workspaceAst) {
3354
+ const errors = [];
3355
+ const registry = buildRegistry(workspaceAst, errors);
3356
+ validateDocs(workspaceAst, registry, errors);
3357
+
3358
+ for (const file of workspaceAst.files) {
3359
+ for (const statement of file.statements) {
3360
+ const fieldMap = collectFieldMap(statement);
3361
+ validateFieldPresence(errors, statement, fieldMap);
3362
+ validateFieldShapes(errors, statement, fieldMap);
3363
+ validateStatus(errors, statement, fieldMap);
3364
+ validateRuleSeverity(errors, statement, fieldMap);
3365
+ validateVerification(errors, statement, fieldMap);
3366
+ validateShapeFrom(errors, statement, registry);
3367
+ validateReferenceKinds(errors, statement, fieldMap, registry);
3368
+ validateEntityRelations(errors, statement, fieldMap, registry);
3369
+ validateShapeTransforms(errors, statement, fieldMap, registry);
3370
+ validateProjectionHttp(errors, statement, fieldMap, registry);
3371
+ validateProjectionHttpErrors(errors, statement, fieldMap, registry);
3372
+ validateProjectionHttpFields(errors, statement, fieldMap, registry);
3373
+ validateProjectionHttpResponses(errors, statement, fieldMap, registry);
3374
+ validateProjectionHttpPreconditions(errors, statement, fieldMap, registry);
3375
+ validateProjectionHttpIdempotency(errors, statement, fieldMap, registry);
3376
+ validateProjectionHttpCache(errors, statement, fieldMap, registry);
3377
+ validateProjectionHttpDelete(errors, statement, fieldMap, registry);
3378
+ validateProjectionHttpAsync(errors, statement, fieldMap, registry);
3379
+ validateProjectionHttpStatus(errors, statement, fieldMap, registry);
3380
+ validateProjectionHttpDownload(errors, statement, fieldMap, registry);
3381
+ validateProjectionHttpAuthz(errors, statement, fieldMap, registry);
3382
+ validateProjectionHttpCallbacks(errors, statement, fieldMap, registry);
3383
+ validateProjectionUiScreens(errors, statement, fieldMap, registry);
3384
+ validateProjectionUiCollections(errors, statement, fieldMap, registry);
3385
+ validateProjectionUiActions(errors, statement, fieldMap, registry);
3386
+ validateProjectionUiVisibility(errors, statement, fieldMap, registry);
3387
+ validateProjectionUiLookups(errors, statement, fieldMap, registry);
3388
+ validateProjectionUiRoutes(errors, statement, fieldMap, registry);
3389
+ validateProjectionUiAppShell(errors, statement, fieldMap);
3390
+ validateProjectionUiNavigation(errors, statement, fieldMap, registry);
3391
+ validateProjectionUiScreenRegions(errors, statement, fieldMap, registry);
3392
+ validateProjectionUiComponents(errors, statement, fieldMap, registry);
3393
+ validateProjectionUiWeb(errors, statement, fieldMap, registry);
3394
+ validateProjectionUiIos(errors, statement, fieldMap, registry);
3395
+ validateProjectionDbTables(errors, statement, fieldMap, registry);
3396
+ validateProjectionDbColumns(errors, statement, fieldMap, registry);
3397
+ validateProjectionDbKeys(errors, statement, fieldMap, registry);
3398
+ validateProjectionDbIndexes(errors, statement, fieldMap, registry);
3399
+ validateProjectionDbRelations(errors, statement, fieldMap, registry);
3400
+ validateProjectionDbLifecycle(errors, statement, fieldMap, registry);
3401
+ validateProjectionGeneratorDefaults(errors, statement, fieldMap);
3402
+ validateComponent(errors, statement, fieldMap, registry);
3403
+ validateDomain(errors, statement, fieldMap, registry);
3404
+ validateDomainTag(errors, statement, fieldMap, registry);
3405
+ validatePitch(errors, statement, fieldMap, registry);
3406
+ validateRequirement(errors, statement, fieldMap, registry);
3407
+ validateAcceptanceCriterion(errors, statement, fieldMap, registry);
3408
+ validateTask(errors, statement, fieldMap, registry);
3409
+ validateBug(errors, statement, fieldMap, registry);
3410
+ validateExpressions(errors, statement, fieldMap);
3411
+ }
3412
+ }
3413
+
3414
+ return {
3415
+ ok: errors.length === 0,
3416
+ errorCount: errors.length,
3417
+ errors,
3418
+ registry
3419
+ };
3420
+ }
3421
+
3422
+ export function formatValidationErrors(result) {
3423
+ return result.errors.map((error) => `${formatLoc(error.loc)} ${error.message}`).join("\n");
3424
+ }