@topogram/cli 0.3.64 → 0.3.65

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 (245) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +703 -0
  3. package/src/adoption/plan.js +12 -703
  4. package/src/agent-ops/query-builders/auth.js +375 -0
  5. package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
  6. package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
  7. package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
  8. package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
  9. package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
  10. package/src/agent-ops/query-builders/change-risk.js +25 -0
  11. package/src/agent-ops/query-builders/common.js +149 -0
  12. package/src/agent-ops/query-builders/maintained-risk.js +539 -0
  13. package/src/agent-ops/query-builders/maintained-shared.js +120 -0
  14. package/src/agent-ops/query-builders/multi-agent.js +547 -0
  15. package/src/agent-ops/query-builders/projection-impacts.js +514 -0
  16. package/src/agent-ops/query-builders/work-packets.js +417 -0
  17. package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
  18. package/src/agent-ops/query-builders/workflow-context.js +398 -0
  19. package/src/agent-ops/query-builders/workflow-presets-core.js +676 -0
  20. package/src/agent-ops/query-builders/workflow-presets.js +341 -0
  21. package/src/agent-ops/query-builders.d.ts +26 -26
  22. package/src/agent-ops/query-builders.js +42 -5021
  23. package/src/catalog/constants.js +10 -0
  24. package/src/catalog/copy.js +60 -0
  25. package/src/catalog/diagnostics.js +15 -0
  26. package/src/catalog/entries.js +42 -0
  27. package/src/catalog/files.js +67 -0
  28. package/src/catalog/provenance.js +122 -0
  29. package/src/catalog/source.js +150 -0
  30. package/src/catalog/validation.js +252 -0
  31. package/src/catalog.d.ts +2 -0
  32. package/src/catalog.js +18 -746
  33. package/src/cli/commands/catalog/check.js +31 -0
  34. package/src/cli/commands/catalog/copy.js +59 -0
  35. package/src/cli/commands/catalog/doctor.js +248 -0
  36. package/src/cli/commands/catalog/help.js +21 -0
  37. package/src/cli/commands/catalog/list.js +52 -0
  38. package/src/cli/commands/catalog/runner.js +92 -0
  39. package/src/cli/commands/catalog/shared.js +17 -0
  40. package/src/cli/commands/catalog/show.js +134 -0
  41. package/src/cli/commands/catalog.js +30 -615
  42. package/src/cli/commands/generator-policy/package-info.js +162 -0
  43. package/src/cli/commands/generator-policy/payloads.js +372 -0
  44. package/src/cli/commands/generator-policy/printers.js +159 -0
  45. package/src/cli/commands/generator-policy/runner.js +81 -0
  46. package/src/cli/commands/generator-policy/shared.js +39 -0
  47. package/src/cli/commands/generator-policy.js +15 -783
  48. package/src/cli/commands/import/adopt.js +170 -0
  49. package/src/cli/commands/import/check.js +91 -0
  50. package/src/cli/commands/import/diff.js +84 -0
  51. package/src/cli/commands/import/help.js +47 -0
  52. package/src/cli/commands/import/paths.js +277 -0
  53. package/src/cli/commands/import/plan.js +284 -0
  54. package/src/cli/commands/import/refresh.js +470 -0
  55. package/src/cli/commands/import/status-history.js +196 -0
  56. package/src/cli/commands/import/workspace.js +230 -0
  57. package/src/cli/commands/import.js +33 -1732
  58. package/src/cli/commands/package/constants.js +17 -0
  59. package/src/cli/commands/package/doctor.js +240 -0
  60. package/src/cli/commands/package/help.js +27 -0
  61. package/src/cli/commands/package/lockfile.js +135 -0
  62. package/src/cli/commands/package/npm.js +97 -0
  63. package/src/cli/commands/package/reporting.js +35 -0
  64. package/src/cli/commands/package/runner.js +33 -0
  65. package/src/cli/commands/package/shared.js +9 -0
  66. package/src/cli/commands/package/update-cli.js +252 -0
  67. package/src/cli/commands/package/versions.js +35 -0
  68. package/src/cli/commands/package.js +29 -813
  69. package/src/cli/commands/query/change-plan.js +68 -0
  70. package/src/cli/commands/query/definitions.js +202 -0
  71. package/src/cli/commands/query/import-adopt.js +121 -0
  72. package/src/cli/commands/query/runner/artifacts.js +102 -0
  73. package/src/cli/commands/query/runner/boundaries.js +211 -0
  74. package/src/cli/commands/query/runner/change.js +182 -0
  75. package/src/cli/commands/query/runner/import-adopt.js +111 -0
  76. package/src/cli/commands/query/runner/index.js +31 -0
  77. package/src/cli/commands/query/runner/output.js +12 -0
  78. package/src/cli/commands/query/runner/workflow.js +241 -0
  79. package/src/cli/commands/query/runner.js +3 -0
  80. package/src/cli/commands/query/workflow-context.js +5 -0
  81. package/src/cli/commands/query/workspace.js +274 -0
  82. package/src/cli/commands/query.js +9 -1300
  83. package/src/cli/commands/template/baseline.js +100 -0
  84. package/src/cli/commands/template/check.js +466 -0
  85. package/src/cli/commands/template/constants.js +8 -0
  86. package/src/cli/commands/template/diagnostics.js +26 -0
  87. package/src/cli/commands/template/help.js +28 -0
  88. package/src/cli/commands/template/lifecycle.js +404 -0
  89. package/src/cli/commands/template/list-show.js +287 -0
  90. package/src/cli/commands/template/policy.js +422 -0
  91. package/src/cli/commands/template/shared.js +127 -0
  92. package/src/cli/commands/template/updates.js +352 -0
  93. package/src/cli/commands/template.js +41 -2143
  94. package/src/generator/api/contracts.js +497 -0
  95. package/src/generator/api/metadata.js +221 -0
  96. package/src/generator/api/openapi.js +559 -0
  97. package/src/generator/api/schema.js +124 -0
  98. package/src/generator/api/types.d.ts +98 -0
  99. package/src/generator/api.js +3 -1195
  100. package/src/generator/context/shared/domain-sdlc.js +282 -0
  101. package/src/generator/context/shared/maintained-boundary.js +665 -0
  102. package/src/generator/context/shared/metrics.js +85 -0
  103. package/src/generator/context/shared/primitives.js +64 -0
  104. package/src/generator/context/shared/relationships.js +453 -0
  105. package/src/generator/context/shared/summaries.js +263 -0
  106. package/src/generator/context/shared/types.d.ts +207 -0
  107. package/src/generator/context/shared.d.ts +42 -0
  108. package/src/generator/context/shared.js +80 -1390
  109. package/src/generator/context/slice/core.js +397 -0
  110. package/src/generator/context/slice/sdlc.js +417 -0
  111. package/src/generator/context/slice/ui-packets.js +183 -0
  112. package/src/generator/context/slice.js +2 -859
  113. package/src/generator/registry/index.js +507 -0
  114. package/src/generator/registry.js +18 -504
  115. package/src/generator/runtime/environment/index.js +666 -0
  116. package/src/generator/runtime/environment.js +4 -666
  117. package/src/generator/runtime/runtime-check/index.js +554 -0
  118. package/src/generator/runtime/runtime-check.js +4 -554
  119. package/src/generator/runtime/shared/index.js +572 -0
  120. package/src/generator/runtime/shared.js +19 -570
  121. package/src/generator/shared.d.ts +2 -0
  122. package/src/generator/surfaces/shared.d.ts +3 -0
  123. package/src/generator/widget-conformance/behavior-report.js +258 -0
  124. package/src/generator/widget-conformance/checks.js +371 -0
  125. package/src/generator/widget-conformance/projection-context.js +200 -0
  126. package/src/generator/widget-conformance/report.js +166 -0
  127. package/src/generator/widget-conformance/types.d.ts +121 -0
  128. package/src/generator/widget-conformance.js +3 -824
  129. package/src/import/core/context.d.ts +3 -0
  130. package/src/import/core/contracts.d.ts +1 -0
  131. package/src/import/core/registry.d.ts +4 -0
  132. package/src/import/core/runner/candidates.js +217 -0
  133. package/src/import/core/runner/options.js +22 -0
  134. package/src/import/core/runner/reports.js +50 -0
  135. package/src/import/core/runner/run.js +79 -0
  136. package/src/import/core/runner/tracks.js +150 -0
  137. package/src/import/core/runner/ui-drafts.js +337 -0
  138. package/src/import/core/runner.js +3 -698
  139. package/src/import/core/shared/api-routes.js +221 -0
  140. package/src/import/core/shared/candidates.js +97 -0
  141. package/src/import/core/shared/files.js +177 -0
  142. package/src/import/core/shared/next-app.js +389 -0
  143. package/src/import/core/shared/types.d.ts +51 -0
  144. package/src/import/core/shared/ui-routes.js +230 -0
  145. package/src/import/core/shared.js +60 -861
  146. package/src/new-project/constants.js +128 -0
  147. package/src/new-project/create.js +83 -0
  148. package/src/new-project/json.js +28 -0
  149. package/src/new-project/metadata.js +96 -0
  150. package/src/new-project/package-spec.js +161 -0
  151. package/src/new-project/project-files.js +348 -0
  152. package/src/new-project/template-policy.js +269 -0
  153. package/src/new-project/template-resolution.js +368 -0
  154. package/src/new-project/template-snapshots.js +430 -0
  155. package/src/new-project/template-updates.js +512 -0
  156. package/src/new-project/types.d.ts +83 -0
  157. package/src/new-project.js +6 -2277
  158. package/src/parser.d.ts +87 -1
  159. package/src/parser.js +118 -0
  160. package/src/policy/review-boundaries.d.ts +15 -0
  161. package/src/project-config/index.js +564 -0
  162. package/src/project-config.js +19 -561
  163. package/src/resolver/enrich/acceptance-criterion.js +2 -0
  164. package/src/resolver/enrich/bug.js +2 -0
  165. package/src/resolver/enrich/pitch.js +2 -0
  166. package/src/resolver/enrich/requirement.js +2 -0
  167. package/src/resolver/enrich/task.js +2 -0
  168. package/src/resolver/index.js +19 -2089
  169. package/src/resolver/normalize.js +384 -1
  170. package/src/resolver/plans.js +168 -0
  171. package/src/resolver/projections-api.js +494 -0
  172. package/src/resolver/projections-db.js +133 -0
  173. package/src/resolver/projections-ui.js +317 -0
  174. package/src/resolver/shapes.js +251 -0
  175. package/src/resolver/shared.js +278 -0
  176. package/src/resolver/widgets.js +132 -0
  177. package/src/template-trust/constants.js +62 -0
  178. package/src/template-trust/content.js +258 -0
  179. package/src/template-trust/diff.js +92 -0
  180. package/src/template-trust/policy.js +61 -0
  181. package/src/template-trust/record.js +90 -0
  182. package/src/template-trust/status.js +182 -0
  183. package/src/template-trust.js +24 -687
  184. package/src/text-helpers.d.ts +1 -0
  185. package/src/topogram-types.d.ts +69 -0
  186. package/src/validator/common.js +488 -0
  187. package/src/validator/data-model.js +237 -0
  188. package/src/validator/docs.js +167 -0
  189. package/src/validator/expressions.js +146 -1
  190. package/src/validator/index.d.ts +23 -0
  191. package/src/validator/index.js +32 -3585
  192. package/src/validator/kinds.d.ts +41 -0
  193. package/src/validator/kinds.js +2 -0
  194. package/src/validator/model-helpers.js +46 -0
  195. package/src/validator/per-kind/acceptance-criterion.js +5 -0
  196. package/src/validator/per-kind/bug.js +6 -0
  197. package/src/validator/per-kind/domain.js +15 -2
  198. package/src/validator/per-kind/pitch.js +7 -0
  199. package/src/validator/per-kind/requirement.js +5 -0
  200. package/src/validator/per-kind/task.js +7 -0
  201. package/src/validator/per-kind/widget.js +14 -0
  202. package/src/validator/projections/api-http-async.js +410 -0
  203. package/src/validator/projections/api-http-authz.js +88 -0
  204. package/src/validator/projections/api-http-core.js +205 -0
  205. package/src/validator/projections/api-http-policies.js +339 -0
  206. package/src/validator/projections/api-http-responses.js +233 -0
  207. package/src/validator/projections/api-http.js +44 -0
  208. package/src/validator/projections/db.js +353 -0
  209. package/src/validator/projections/generator-defaults.js +45 -0
  210. package/src/validator/projections/helpers.js +87 -0
  211. package/src/validator/projections/ui-helpers.js +214 -0
  212. package/src/validator/projections/ui-navigation.js +344 -0
  213. package/src/validator/projections/ui-structure.js +364 -0
  214. package/src/validator/projections/ui-widgets.js +493 -0
  215. package/src/validator/projections/ui.js +46 -0
  216. package/src/validator/registry.js +48 -1
  217. package/src/validator/utils.d.ts +20 -0
  218. package/src/validator/utils.js +115 -12
  219. package/src/widget-behavior.d.ts +1 -0
  220. package/src/workflows/import-app/api/collect.js +221 -0
  221. package/src/workflows/import-app/api/openapi.js +257 -0
  222. package/src/workflows/import-app/api/routes.js +327 -0
  223. package/src/workflows/import-app/api/sources.js +22 -0
  224. package/src/workflows/import-app/api.js +2 -797
  225. package/src/workflows/reconcile/adoption-plan/build.js +208 -0
  226. package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
  227. package/src/workflows/reconcile/adoption-plan/outputs.js +143 -0
  228. package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
  229. package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
  230. package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
  231. package/src/workflows/reconcile/adoption-plan.js +30 -740
  232. package/src/workflows/reconcile/auth/closures.js +115 -0
  233. package/src/workflows/reconcile/auth/formatters.js +142 -0
  234. package/src/workflows/reconcile/auth/inference.js +330 -0
  235. package/src/workflows/reconcile/auth/roles.js +122 -0
  236. package/src/workflows/reconcile/auth.js +35 -690
  237. package/src/workflows/reconcile/bundle-core/index.js +600 -0
  238. package/src/workflows/reconcile/bundle-core.js +12 -598
  239. package/src/workflows/reconcile/canonical-surface.js +1 -1
  240. package/src/workflows/reconcile/impacts/adoption-plan.js +192 -0
  241. package/src/workflows/reconcile/impacts/indexes.js +101 -0
  242. package/src/workflows/reconcile/impacts/patches.js +252 -0
  243. package/src/workflows/reconcile/impacts/reports.js +80 -0
  244. package/src/workflows/reconcile/impacts.js +14 -623
  245. package/src/workspace-docs.d.ts +29 -0
@@ -0,0 +1,233 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ blockSymbolItems,
5
+ getFieldValue,
6
+ pushError,
7
+ symbolValues
8
+ } from "../utils.js";
9
+ import { resolveCapabilityContractFields } from "./helpers.js";
10
+
11
+ /**
12
+ * @param {ValidationErrors} errors
13
+ * @param {TopogramStatement} statement
14
+ * @param {TopogramFieldMap} fieldMap
15
+ * @param {TopogramRegistry} registry
16
+ * @returns {void}
17
+ */
18
+ export function validateProjectionHttpResponses(errors, statement, fieldMap, registry) {
19
+ if (statement.kind !== "projection") {
20
+ return;
21
+ }
22
+
23
+ const httpResponsesField = fieldMap.get("responses")?.[0];
24
+ if (!httpResponsesField || httpResponsesField.value.type !== "block") {
25
+ return;
26
+ }
27
+
28
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
29
+ for (const entry of httpResponsesField.value.entries) {
30
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
31
+ const capabilityId = tokens[0];
32
+ const capability = registry.get(capabilityId);
33
+
34
+ if (!capability) {
35
+ pushError(errors, `Projection ${statement.id} responses references missing capability '${capabilityId}'`, entry.loc);
36
+ continue;
37
+ }
38
+ if (capability.kind !== "capability") {
39
+ pushError(errors, `Projection ${statement.id} responses must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
40
+ }
41
+ if (!realized.has(capabilityId)) {
42
+ pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
43
+ }
44
+
45
+ const directives = parseProjectionHttpResponsesDirectives(tokens.slice(1));
46
+ for (const message of directives.errors) {
47
+ pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' ${message}`, entry.loc);
48
+ }
49
+
50
+ if (!directives.mode) {
51
+ pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'mode'`, entry.loc);
52
+ }
53
+
54
+ const mode = directives.mode;
55
+ if (mode && !["item", "collection", "paged", "cursor"].includes(mode)) {
56
+ pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' has invalid mode '${mode}'`, entry.loc);
57
+ }
58
+
59
+ const itemShapeId = directives.item;
60
+ if (mode && mode !== "item" && !itemShapeId) {
61
+ pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'item' for mode '${mode}'`, entry.loc);
62
+ }
63
+ if (itemShapeId) {
64
+ const itemShape = registry.get(itemShapeId);
65
+ if (!itemShape) {
66
+ pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' references missing shape '${itemShapeId}'`, entry.loc);
67
+ } else if (itemShape.kind !== "shape") {
68
+ pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must reference a shape for 'item', found ${itemShape.kind} '${itemShape.id}'`, entry.loc);
69
+ }
70
+ }
71
+
72
+ if (mode === "cursor") {
73
+ if (!directives.cursor?.requestAfter) {
74
+ pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'cursor request_after <field>'`, entry.loc);
75
+ }
76
+ if (!directives.cursor?.responseNext) {
77
+ pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'cursor response_next <wire_name>'`, entry.loc);
78
+ }
79
+ if (!directives.limit) {
80
+ pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'limit field <field> default <n> max <n>'`, entry.loc);
81
+ }
82
+ if (!directives.sort) {
83
+ pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'sort by <field> direction <asc|desc>'`, entry.loc);
84
+ }
85
+ }
86
+
87
+ if (directives.sort && !["asc", "desc"].includes(directives.sort.direction || "")) {
88
+ pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' has invalid sort direction '${directives.sort.direction}'`, entry.loc);
89
+ }
90
+
91
+ if (directives.total && !["true", "false"].includes(directives.total.included || "")) {
92
+ pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' has invalid total included value '${directives.total.included}'`, entry.loc);
93
+ }
94
+
95
+ if (directives.limit) {
96
+ const defaultValue = Number.parseInt(directives.limit.defaultValue || "", 10);
97
+ const maxValue = Number.parseInt(directives.limit.maxValue || "", 10);
98
+ if (!Number.isInteger(defaultValue) || !Number.isInteger(maxValue)) {
99
+ pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must use integer default/max values for 'limit'`, entry.loc);
100
+ } else if (defaultValue > maxValue) {
101
+ pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must use default <= max for 'limit'`, entry.loc);
102
+ }
103
+ }
104
+
105
+ const inputFields = resolveCapabilityContractFields(registry, capabilityId, "input");
106
+ const outputFields = resolveCapabilityContractFields(registry, capabilityId, "output");
107
+ if (directives.cursor?.requestAfter && inputFields.size > 0 && !inputFields.has(directives.cursor.requestAfter)) {
108
+ pushError(errors, `Projection ${statement.id} responses references unknown input field '${directives.cursor.requestAfter}' for cursor request_after on ${capabilityId}`, entry.loc);
109
+ }
110
+ if (directives.limit?.field && inputFields.size > 0 && !inputFields.has(directives.limit.field)) {
111
+ pushError(errors, `Projection ${statement.id} responses references unknown input field '${directives.limit.field}' for limit on ${capabilityId}`, entry.loc);
112
+ }
113
+ if (directives.sort?.field && outputFields.size > 0 && !outputFields.has(directives.sort.field)) {
114
+ pushError(errors, `Projection ${statement.id} responses references unknown output field '${directives.sort.field}' for sort on ${capabilityId}`, entry.loc);
115
+ }
116
+ }
117
+ }
118
+
119
+ /**
120
+ * @param {string[]} tokens
121
+ * @returns {{
122
+ * mode: string | null,
123
+ * item: string | null,
124
+ * cursor: { requestAfter: string | null, responseNext: string | null, responsePrev: string | null } | null,
125
+ * limit: { field: string | null, defaultValue: string | null, maxValue: string | null } | null,
126
+ * sort: { field: string | null, direction: string | null } | null,
127
+ * total: { included: string | null } | null,
128
+ * errors: string[]
129
+ * }}
130
+ */
131
+ export function parseProjectionHttpResponsesDirectives(tokens) {
132
+ /** @type {{
133
+ * mode: string | null,
134
+ * item: string | null,
135
+ * cursor: { requestAfter: string | null, responseNext: string | null, responsePrev: string | null } | null,
136
+ * limit: { field: string | null, defaultValue: string | null, maxValue: string | null } | null,
137
+ * sort: { field: string | null, direction: string | null } | null,
138
+ * total: { included: string | null } | null,
139
+ * errors: string[]
140
+ * }}
141
+ */
142
+ const result = {
143
+ mode: null,
144
+ item: null,
145
+ cursor: null,
146
+ limit: null,
147
+ sort: null,
148
+ total: null,
149
+ errors: []
150
+ };
151
+
152
+ for (let i = 0; i < tokens.length; i += 1) {
153
+ const token = tokens[i];
154
+ if (token === "mode") {
155
+ result.mode = tokens[i + 1] || null;
156
+ if (!tokens[i + 1]) {
157
+ result.errors.push("is missing a value for 'mode'");
158
+ }
159
+ i += 1;
160
+ continue;
161
+ }
162
+ if (token === "item") {
163
+ result.item = tokens[i + 1] || null;
164
+ if (!tokens[i + 1]) {
165
+ result.errors.push("is missing a value for 'item'");
166
+ }
167
+ i += 1;
168
+ continue;
169
+ }
170
+ if (token === "cursor") {
171
+ const requestKeyword = tokens[i + 1];
172
+ const requestField = tokens[i + 2];
173
+ const responseKeyword = tokens[i + 3];
174
+ const responseNext = tokens[i + 4];
175
+ let responsePrev = null;
176
+ let consumed = 4;
177
+ if (tokens[i + 5] === "response_prev") {
178
+ responsePrev = tokens[i + 6] || null;
179
+ consumed = 6;
180
+ }
181
+ result.cursor = {
182
+ requestAfter: requestKeyword === "request_after" ? requestField : null,
183
+ responseNext: responseKeyword === "response_next" ? responseNext : null,
184
+ responsePrev
185
+ };
186
+ if (requestKeyword !== "request_after") {
187
+ result.errors.push("must use 'cursor request_after <field>'");
188
+ }
189
+ if (responseKeyword !== "response_next") {
190
+ result.errors.push("must use 'cursor response_next <wire_name>'");
191
+ }
192
+ i += consumed;
193
+ continue;
194
+ }
195
+ if (token === "limit") {
196
+ result.limit = {
197
+ field: tokens[i + 1] === "field" ? tokens[i + 2] || null : null,
198
+ defaultValue: tokens[i + 3] === "default" ? tokens[i + 4] || null : null,
199
+ maxValue: tokens[i + 5] === "max" ? tokens[i + 6] || null : null
200
+ };
201
+ if (tokens[i + 1] !== "field" || tokens[i + 3] !== "default" || tokens[i + 5] !== "max") {
202
+ result.errors.push("must use 'limit field <field> default <n> max <n>'");
203
+ }
204
+ i += 6;
205
+ continue;
206
+ }
207
+ if (token === "sort") {
208
+ result.sort = {
209
+ field: tokens[i + 1] === "by" ? tokens[i + 2] || null : null,
210
+ direction: tokens[i + 3] === "direction" ? tokens[i + 4] || null : null
211
+ };
212
+ if (tokens[i + 1] !== "by" || tokens[i + 3] !== "direction") {
213
+ result.errors.push("must use 'sort by <field> direction <asc|desc>'");
214
+ }
215
+ i += 4;
216
+ continue;
217
+ }
218
+ if (token === "total") {
219
+ result.total = {
220
+ included: tokens[i + 1] === "included" ? tokens[i + 2] || null : null
221
+ };
222
+ if (tokens[i + 1] !== "included") {
223
+ result.errors.push("must use 'total included <true|false>'");
224
+ }
225
+ i += 2;
226
+ continue;
227
+ }
228
+
229
+ result.errors.push(`has unknown directive '${token}'`);
230
+ }
231
+
232
+ return result;
233
+ }
@@ -0,0 +1,44 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ validateProjectionHttp,
5
+ validateProjectionHttpErrors,
6
+ validateProjectionHttpFields
7
+ } from "./api-http-core.js";
8
+ import { validateProjectionHttpResponses } from "./api-http-responses.js";
9
+ import {
10
+ validateProjectionHttpPreconditions,
11
+ validateProjectionHttpIdempotency,
12
+ validateProjectionHttpCache,
13
+ validateProjectionHttpDelete
14
+ } from "./api-http-policies.js";
15
+ import {
16
+ validateProjectionHttpAsync,
17
+ validateProjectionHttpStatus,
18
+ validateProjectionHttpDownload,
19
+ validateProjectionHttpCallbacks
20
+ } from "./api-http-async.js";
21
+ import { validateProjectionHttpAuthz } from "./api-http-authz.js";
22
+
23
+ /**
24
+ * @param {ValidationErrors} errors
25
+ * @param {TopogramStatement} statement
26
+ * @param {TopogramFieldMap} fieldMap
27
+ * @param {TopogramRegistry} registry
28
+ * @returns {void}
29
+ */
30
+ export function validateApiHttpProjection(errors, statement, fieldMap, registry) {
31
+ validateProjectionHttp(errors, statement, fieldMap, registry);
32
+ validateProjectionHttpErrors(errors, statement, fieldMap, registry);
33
+ validateProjectionHttpFields(errors, statement, fieldMap, registry);
34
+ validateProjectionHttpResponses(errors, statement, fieldMap, registry);
35
+ validateProjectionHttpPreconditions(errors, statement, fieldMap, registry);
36
+ validateProjectionHttpIdempotency(errors, statement, fieldMap, registry);
37
+ validateProjectionHttpCache(errors, statement, fieldMap, registry);
38
+ validateProjectionHttpDelete(errors, statement, fieldMap, registry);
39
+ validateProjectionHttpAsync(errors, statement, fieldMap, registry);
40
+ validateProjectionHttpStatus(errors, statement, fieldMap, registry);
41
+ validateProjectionHttpDownload(errors, statement, fieldMap, registry);
42
+ validateProjectionHttpAuthz(errors, statement, fieldMap, registry);
43
+ validateProjectionHttpCallbacks(errors, statement, fieldMap, registry);
44
+ }
@@ -0,0 +1,353 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ blockSymbolItems,
5
+ getFieldValue,
6
+ pushError,
7
+ symbolValues
8
+ } from "../utils.js";
9
+ import { statementFieldNames } from "../model-helpers.js";
10
+ import { parseUiDirectiveMap } from "./helpers.js";
11
+
12
+ /**
13
+ * @param {ValidationErrors} errors
14
+ * @param {TopogramStatement} statement
15
+ * @param {TopogramFieldMap} fieldMap
16
+ * @param {TopogramRegistry} registry
17
+ * @returns {void}
18
+ */
19
+ function validateProjectionDbTables(errors, statement, fieldMap, registry) {
20
+ if (statement.kind !== "projection") {
21
+ return;
22
+ }
23
+
24
+ const dbTablesField = fieldMap.get("tables")?.[0];
25
+ if (!dbTablesField || dbTablesField.value.type !== "block") {
26
+ return;
27
+ }
28
+
29
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
30
+ const seenTables = new Set();
31
+ for (const entry of dbTablesField.value.entries) {
32
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
33
+ const [entityId, tableKeyword, tableName] = tokens;
34
+ const entity = registry.get(entityId);
35
+
36
+ if (!entity) {
37
+ pushError(errors, `Projection ${statement.id} tables references missing entity '${entityId}'`, entry.loc);
38
+ continue;
39
+ }
40
+ if (entity.kind !== "entity") {
41
+ pushError(errors, `Projection ${statement.id} tables must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
42
+ }
43
+ if (!realized.has(entityId)) {
44
+ pushError(errors, `Projection ${statement.id} tables entity '${entityId}' must also appear in 'realizes'`, entry.loc);
45
+ }
46
+ if (tableKeyword !== "table") {
47
+ pushError(errors, `Projection ${statement.id} tables for '${entityId}' must use 'table'`, entry.loc);
48
+ }
49
+ if (!tableName) {
50
+ pushError(errors, `Projection ${statement.id} tables for '${entityId}' must include a table name`, entry.loc);
51
+ } else if (seenTables.has(tableName)) {
52
+ pushError(errors, `Projection ${statement.id} tables has duplicate table name '${tableName}'`, entry.loc);
53
+ }
54
+ seenTables.add(tableName);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * @param {ValidationErrors} errors
60
+ * @param {TopogramStatement} statement
61
+ * @param {TopogramFieldMap} fieldMap
62
+ * @param {TopogramRegistry} registry
63
+ * @returns {void}
64
+ */
65
+ function validateProjectionDbColumns(errors, statement, fieldMap, registry) {
66
+ if (statement.kind !== "projection") {
67
+ return;
68
+ }
69
+
70
+ const dbColumnsField = fieldMap.get("columns")?.[0];
71
+ if (!dbColumnsField || dbColumnsField.value.type !== "block") {
72
+ return;
73
+ }
74
+
75
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
76
+ for (const entry of dbColumnsField.value.entries) {
77
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
78
+ const [entityId, fieldKeyword, fieldName, columnKeyword, columnName] = tokens;
79
+ const entity = registry.get(entityId);
80
+
81
+ if (!entity) {
82
+ pushError(errors, `Projection ${statement.id} columns references missing entity '${entityId}'`, entry.loc);
83
+ continue;
84
+ }
85
+ if (entity.kind !== "entity") {
86
+ pushError(errors, `Projection ${statement.id} columns must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
87
+ }
88
+ if (!realized.has(entityId)) {
89
+ pushError(errors, `Projection ${statement.id} columns entity '${entityId}' must also appear in 'realizes'`, entry.loc);
90
+ }
91
+ if (fieldKeyword !== "field") {
92
+ pushError(errors, `Projection ${statement.id} columns for '${entityId}' must use 'field'`, entry.loc);
93
+ }
94
+ if (columnKeyword !== "column") {
95
+ pushError(errors, `Projection ${statement.id} columns for '${entityId}' must use 'column'`, entry.loc);
96
+ }
97
+ const entityFieldNames = new Set(statementFieldNames(entity));
98
+ if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
99
+ pushError(errors, `Projection ${statement.id} columns references unknown field '${fieldName}' on ${entityId}`, entry.loc);
100
+ }
101
+ if (!columnName) {
102
+ pushError(errors, `Projection ${statement.id} columns for '${entityId}.${fieldName}' must include a column name`, entry.loc);
103
+ }
104
+ }
105
+ }
106
+
107
+ /**
108
+ * @param {ValidationErrors} errors
109
+ * @param {TopogramStatement} statement
110
+ * @param {TopogramFieldMap} fieldMap
111
+ * @param {TopogramRegistry} registry
112
+ * @returns {void}
113
+ */
114
+ function validateProjectionDbKeys(errors, statement, fieldMap, registry) {
115
+ if (statement.kind !== "projection") {
116
+ return;
117
+ }
118
+
119
+ const dbKeysField = fieldMap.get("keys")?.[0];
120
+ if (!dbKeysField || dbKeysField.value.type !== "block") {
121
+ return;
122
+ }
123
+
124
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
125
+ for (const entry of dbKeysField.value.entries) {
126
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
127
+ const [entityId, keyType] = tokens;
128
+ const entity = registry.get(entityId);
129
+
130
+ if (!entity) {
131
+ pushError(errors, `Projection ${statement.id} keys references missing entity '${entityId}'`, entry.loc);
132
+ continue;
133
+ }
134
+ if (entity.kind !== "entity") {
135
+ pushError(errors, `Projection ${statement.id} keys must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
136
+ }
137
+ if (!realized.has(entityId)) {
138
+ pushError(errors, `Projection ${statement.id} keys entity '${entityId}' must also appear in 'realizes'`, entry.loc);
139
+ }
140
+ if (!["primary", "unique"].includes(keyType || "")) {
141
+ pushError(errors, `Projection ${statement.id} keys for '${entityId}' has invalid key type '${keyType}'`, entry.loc);
142
+ }
143
+ const fieldList = entry.items[2];
144
+ if (!fieldList || fieldList.type !== "list" || fieldList.items.length === 0) {
145
+ pushError(errors, `Projection ${statement.id} keys for '${entityId}' must include a non-empty field list`, entry.loc);
146
+ continue;
147
+ }
148
+ const entityFieldNames = new Set(statementFieldNames(entity));
149
+ for (const item of fieldList.items) {
150
+ if (item.type === "symbol" && entityFieldNames.size > 0 && !entityFieldNames.has(item.value)) {
151
+ pushError(errors, `Projection ${statement.id} keys references unknown field '${item.value}' on ${entityId}`, item.loc);
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ /**
158
+ * @param {ValidationErrors} errors
159
+ * @param {TopogramStatement} statement
160
+ * @param {TopogramFieldMap} fieldMap
161
+ * @param {TopogramRegistry} registry
162
+ * @returns {void}
163
+ */
164
+ function validateProjectionDbIndexes(errors, statement, fieldMap, registry) {
165
+ if (statement.kind !== "projection") {
166
+ return;
167
+ }
168
+
169
+ const dbIndexesField = fieldMap.get("indexes")?.[0];
170
+ if (!dbIndexesField || dbIndexesField.value.type !== "block") {
171
+ return;
172
+ }
173
+
174
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
175
+ for (const entry of dbIndexesField.value.entries) {
176
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
177
+ const [entityId, indexType] = tokens;
178
+ const entity = registry.get(entityId);
179
+
180
+ if (!entity) {
181
+ pushError(errors, `Projection ${statement.id} indexes references missing entity '${entityId}'`, entry.loc);
182
+ continue;
183
+ }
184
+ if (entity.kind !== "entity") {
185
+ pushError(errors, `Projection ${statement.id} indexes must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
186
+ }
187
+ if (!realized.has(entityId)) {
188
+ pushError(errors, `Projection ${statement.id} indexes entity '${entityId}' must also appear in 'realizes'`, entry.loc);
189
+ }
190
+ if (!["index", "unique"].includes(indexType || "")) {
191
+ pushError(errors, `Projection ${statement.id} indexes for '${entityId}' has invalid index type '${indexType}'`, entry.loc);
192
+ }
193
+ const fieldList = entry.items[2];
194
+ if (!fieldList || fieldList.type !== "list" || fieldList.items.length === 0) {
195
+ pushError(errors, `Projection ${statement.id} indexes for '${entityId}' must include a non-empty field list`, entry.loc);
196
+ continue;
197
+ }
198
+ const entityFieldNames = new Set(statementFieldNames(entity));
199
+ for (const item of fieldList.items) {
200
+ if (item.type === "symbol" && entityFieldNames.size > 0 && !entityFieldNames.has(item.value)) {
201
+ pushError(errors, `Projection ${statement.id} indexes references unknown field '${item.value}' on ${entityId}`, item.loc);
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ /**
208
+ * @param {ValidationErrors} errors
209
+ * @param {TopogramStatement} statement
210
+ * @param {TopogramFieldMap} fieldMap
211
+ * @param {TopogramRegistry} registry
212
+ * @returns {void}
213
+ */
214
+ function validateProjectionDbRelations(errors, statement, fieldMap, registry) {
215
+ if (statement.kind !== "projection") {
216
+ return;
217
+ }
218
+
219
+ const dbRelationsField = fieldMap.get("relations")?.[0];
220
+ if (!dbRelationsField || dbRelationsField.value.type !== "block") {
221
+ return;
222
+ }
223
+
224
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
225
+ for (const entry of dbRelationsField.value.entries) {
226
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
227
+ const [entityId, relationType, fieldName, referencesKeyword, targetRef, onDeleteKeyword, onDeleteValue] = tokens;
228
+ const entity = registry.get(entityId);
229
+
230
+ if (!entity) {
231
+ pushError(errors, `Projection ${statement.id} relations references missing entity '${entityId}'`, entry.loc);
232
+ continue;
233
+ }
234
+ if (entity.kind !== "entity") {
235
+ pushError(errors, `Projection ${statement.id} relations must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
236
+ }
237
+ if (!realized.has(entityId)) {
238
+ pushError(errors, `Projection ${statement.id} relations entity '${entityId}' must also appear in 'realizes'`, entry.loc);
239
+ }
240
+ if (relationType !== "foreign_key") {
241
+ pushError(errors, `Projection ${statement.id} relations for '${entityId}' must use 'foreign_key'`, entry.loc);
242
+ }
243
+ if (referencesKeyword !== "references") {
244
+ pushError(errors, `Projection ${statement.id} relations for '${entityId}' must use 'references'`, entry.loc);
245
+ }
246
+ if (onDeleteKeyword && onDeleteKeyword !== "on_delete") {
247
+ pushError(errors, `Projection ${statement.id} relations for '${entityId}' has unexpected token '${onDeleteKeyword}'`, entry.loc);
248
+ }
249
+ if (onDeleteValue && !["cascade", "restrict", "set_null", "no_action"].includes(onDeleteValue)) {
250
+ pushError(errors, `Projection ${statement.id} relations for '${entityId}' has invalid on_delete '${onDeleteValue}'`, entry.loc);
251
+ }
252
+ const entityFieldNames = new Set(statementFieldNames(entity));
253
+ if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
254
+ pushError(errors, `Projection ${statement.id} relations references unknown field '${fieldName}' on ${entityId}`, entry.loc);
255
+ }
256
+ const [targetEntityId, targetFieldName] = (targetRef || "").split(".");
257
+ const targetEntity = registry.get(targetEntityId);
258
+ if (!targetEntity) {
259
+ pushError(errors, `Projection ${statement.id} relations references missing target entity '${targetEntityId}'`, entry.loc);
260
+ continue;
261
+ }
262
+ if (targetEntity.kind !== "entity") {
263
+ pushError(errors, `Projection ${statement.id} relations must reference an entity target, found ${targetEntity.kind} '${targetEntity.id}'`, entry.loc);
264
+ }
265
+ const targetFieldNames = new Set(statementFieldNames(targetEntity));
266
+ if (targetFieldName && targetFieldNames.size > 0 && !targetFieldNames.has(targetFieldName)) {
267
+ pushError(errors, `Projection ${statement.id} relations references unknown target field '${targetFieldName}' on ${targetEntityId}`, entry.loc);
268
+ }
269
+ }
270
+ }
271
+
272
+ /**
273
+ * @param {ValidationErrors} errors
274
+ * @param {TopogramStatement} statement
275
+ * @param {TopogramFieldMap} fieldMap
276
+ * @param {TopogramRegistry} registry
277
+ * @returns {void}
278
+ */
279
+ function validateProjectionDbLifecycle(errors, statement, fieldMap, registry) {
280
+ if (statement.kind !== "projection") {
281
+ return;
282
+ }
283
+
284
+ const dbLifecycleField = fieldMap.get("lifecycle")?.[0];
285
+ if (!dbLifecycleField || dbLifecycleField.value.type !== "block") {
286
+ return;
287
+ }
288
+
289
+ const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
290
+ for (const entry of dbLifecycleField.value.entries) {
291
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
292
+ const [entityId, lifecycleType] = tokens;
293
+ const entity = registry.get(entityId);
294
+
295
+ if (!entity) {
296
+ pushError(errors, `Projection ${statement.id} lifecycle references missing entity '${entityId}'`, entry.loc);
297
+ continue;
298
+ }
299
+ if (entity.kind !== "entity") {
300
+ pushError(errors, `Projection ${statement.id} lifecycle must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
301
+ }
302
+ if (!realized.has(entityId)) {
303
+ pushError(errors, `Projection ${statement.id} lifecycle entity '${entityId}' must also appear in 'realizes'`, entry.loc);
304
+ }
305
+
306
+ const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `lifecycle for '${entityId}'`);
307
+ if (!["soft_delete", "timestamps"].includes(lifecycleType || "")) {
308
+ pushError(errors, `Projection ${statement.id} lifecycle for '${entityId}' has invalid lifecycle '${lifecycleType}'`, entry.loc);
309
+ continue;
310
+ }
311
+
312
+ const entityFieldNames = new Set(statementFieldNames(entity));
313
+ if (lifecycleType === "soft_delete") {
314
+ for (const requiredKey of ["field", "value"]) {
315
+ if (!directives.has(requiredKey)) {
316
+ pushError(errors, `Projection ${statement.id} lifecycle for '${entityId}' must include '${requiredKey}' for soft_delete`, entry.loc);
317
+ }
318
+ }
319
+ const fieldName = directives.get("field");
320
+ if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
321
+ pushError(errors, `Projection ${statement.id} lifecycle references unknown field '${fieldName}' on ${entityId}`, entry.loc);
322
+ }
323
+ }
324
+
325
+ if (lifecycleType === "timestamps") {
326
+ for (const requiredKey of ["created_at", "updated_at"]) {
327
+ if (!directives.has(requiredKey)) {
328
+ pushError(errors, `Projection ${statement.id} lifecycle for '${entityId}' must include '${requiredKey}' for timestamps`, entry.loc);
329
+ }
330
+ const fieldName = directives.get(requiredKey);
331
+ if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
332
+ pushError(errors, `Projection ${statement.id} lifecycle references unknown field '${fieldName}' on ${entityId}`, entry.loc);
333
+ }
334
+ }
335
+ }
336
+ }
337
+ }
338
+
339
+ /**
340
+ * @param {ValidationErrors} errors
341
+ * @param {TopogramStatement} statement
342
+ * @param {TopogramFieldMap} fieldMap
343
+ * @param {TopogramRegistry} registry
344
+ * @returns {void}
345
+ */
346
+ export function validateDbProjection(errors, statement, fieldMap, registry) {
347
+ validateProjectionDbTables(errors, statement, fieldMap, registry);
348
+ validateProjectionDbColumns(errors, statement, fieldMap, registry);
349
+ validateProjectionDbKeys(errors, statement, fieldMap, registry);
350
+ validateProjectionDbIndexes(errors, statement, fieldMap, registry);
351
+ validateProjectionDbRelations(errors, statement, fieldMap, registry);
352
+ validateProjectionDbLifecycle(errors, statement, fieldMap, registry);
353
+ }
@@ -0,0 +1,45 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ blockSymbolItems,
5
+ pushError
6
+ } from "../utils.js";
7
+
8
+ /**
9
+ * @param {ValidationErrors} errors
10
+ * @param {TopogramStatement} statement
11
+ * @param {TopogramFieldMap} fieldMap
12
+ * @returns {void}
13
+ */
14
+ export function validateProjectionGeneratorDefaults(errors, statement, fieldMap) {
15
+ if (statement.kind !== "projection") {
16
+ return;
17
+ }
18
+
19
+ const generatorField = fieldMap.get("generator_defaults")?.[0];
20
+ if (!generatorField || generatorField.value.type !== "block") {
21
+ return;
22
+ }
23
+
24
+ for (const entry of generatorField.value.entries) {
25
+ const tokens = blockSymbolItems(entry).map((item) => item.value);
26
+ const [key, value] = tokens;
27
+ if (!["profile", "language", "styling"].includes(key || "")) {
28
+ pushError(errors, `Projection ${statement.id} generator_defaults has unknown key '${key}'`, entry.loc);
29
+ continue;
30
+ }
31
+ if (!value) {
32
+ pushError(errors, `Projection ${statement.id} generator_defaults is missing a value for '${key}'`, entry.loc);
33
+ continue;
34
+ }
35
+ if (key === "profile" && !["vanilla", "sveltekit", "react", "swiftui", "postgres_sql", "sqlite_sql", "prisma", "drizzle"].includes(value)) {
36
+ pushError(errors, `Projection ${statement.id} generator_defaults has unsupported profile '${value}'`, entry.loc);
37
+ }
38
+ if (key === "language" && !["typescript", "javascript", "swift", "sql"].includes(value)) {
39
+ pushError(errors, `Projection ${statement.id} generator_defaults has unsupported language '${value}'`, entry.loc);
40
+ }
41
+ if (key === "styling" && !["tailwind", "css"].includes(value)) {
42
+ pushError(errors, `Projection ${statement.id} generator_defaults has unsupported styling '${value}'`, entry.loc);
43
+ }
44
+ }
45
+ }