@topogram/cli 0.3.64 → 0.3.66

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 (278) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +716 -0
  3. package/src/adoption/plan.js +12 -703
  4. package/src/adoption/reporting.js +1 -1
  5. package/src/agent-brief.js +7 -21
  6. package/src/agent-ops/query-builders/auth.js +375 -0
  7. package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
  8. package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
  9. package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
  10. package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
  11. package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
  12. package/src/agent-ops/query-builders/change-risk.js +25 -0
  13. package/src/agent-ops/query-builders/common.js +149 -0
  14. package/src/agent-ops/query-builders/maintained-risk.js +539 -0
  15. package/src/agent-ops/query-builders/maintained-shared.js +120 -0
  16. package/src/agent-ops/query-builders/multi-agent.js +547 -0
  17. package/src/agent-ops/query-builders/projection-impacts.js +514 -0
  18. package/src/agent-ops/query-builders/work-packets.js +417 -0
  19. package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
  20. package/src/agent-ops/query-builders/workflow-context.js +398 -0
  21. package/src/agent-ops/query-builders/workflow-presets-core.js +677 -0
  22. package/src/agent-ops/query-builders/workflow-presets.js +341 -0
  23. package/src/agent-ops/query-builders.d.ts +26 -26
  24. package/src/agent-ops/query-builders.js +42 -5021
  25. package/src/archive/jsonl.js +2 -2
  26. package/src/archive/resolver-bridge.js +1 -1
  27. package/src/archive/unarchive.js +2 -1
  28. package/src/catalog/constants.js +10 -0
  29. package/src/catalog/copy.js +65 -0
  30. package/src/catalog/diagnostics.js +15 -0
  31. package/src/catalog/entries.js +42 -0
  32. package/src/catalog/files.js +67 -0
  33. package/src/catalog/provenance.js +123 -0
  34. package/src/catalog/source.js +150 -0
  35. package/src/catalog/validation.js +252 -0
  36. package/src/catalog.d.ts +2 -0
  37. package/src/catalog.js +18 -746
  38. package/src/cli/command-parsers/project.js +3 -0
  39. package/src/cli/command-parsers/shared.js +1 -1
  40. package/src/cli/commands/agent.js +2 -2
  41. package/src/cli/commands/catalog/check.js +31 -0
  42. package/src/cli/commands/catalog/copy.js +59 -0
  43. package/src/cli/commands/catalog/doctor.js +248 -0
  44. package/src/cli/commands/catalog/help.js +21 -0
  45. package/src/cli/commands/catalog/list.js +52 -0
  46. package/src/cli/commands/catalog/runner.js +92 -0
  47. package/src/cli/commands/catalog/shared.js +17 -0
  48. package/src/cli/commands/catalog/show.js +134 -0
  49. package/src/cli/commands/catalog.js +30 -615
  50. package/src/cli/commands/check.js +3 -3
  51. package/src/cli/commands/doctor.js +2 -9
  52. package/src/cli/commands/generator-policy/package-info.js +162 -0
  53. package/src/cli/commands/generator-policy/payloads.js +372 -0
  54. package/src/cli/commands/generator-policy/printers.js +159 -0
  55. package/src/cli/commands/generator-policy/runner.js +81 -0
  56. package/src/cli/commands/generator-policy/shared.js +39 -0
  57. package/src/cli/commands/generator-policy.js +15 -783
  58. package/src/cli/commands/import/adopt.js +170 -0
  59. package/src/cli/commands/import/check.js +91 -0
  60. package/src/cli/commands/import/diff.js +84 -0
  61. package/src/cli/commands/import/help.js +47 -0
  62. package/src/cli/commands/import/paths.js +269 -0
  63. package/src/cli/commands/import/plan.js +292 -0
  64. package/src/cli/commands/import/refresh.js +471 -0
  65. package/src/cli/commands/import/status-history.js +196 -0
  66. package/src/cli/commands/import/workspace.js +233 -0
  67. package/src/cli/commands/import.js +33 -1732
  68. package/src/cli/commands/migrate.js +153 -0
  69. package/src/cli/commands/package/constants.js +17 -0
  70. package/src/cli/commands/package/doctor.js +240 -0
  71. package/src/cli/commands/package/help.js +27 -0
  72. package/src/cli/commands/package/lockfile.js +135 -0
  73. package/src/cli/commands/package/npm.js +97 -0
  74. package/src/cli/commands/package/reporting.js +35 -0
  75. package/src/cli/commands/package/runner.js +33 -0
  76. package/src/cli/commands/package/shared.js +9 -0
  77. package/src/cli/commands/package/update-cli.js +252 -0
  78. package/src/cli/commands/package/versions.js +35 -0
  79. package/src/cli/commands/package.js +29 -813
  80. package/src/cli/commands/query/change-plan.js +68 -0
  81. package/src/cli/commands/query/definitions.js +202 -0
  82. package/src/cli/commands/query/import-adopt.js +121 -0
  83. package/src/cli/commands/query/runner/artifacts.js +102 -0
  84. package/src/cli/commands/query/runner/boundaries.js +211 -0
  85. package/src/cli/commands/query/runner/change.js +182 -0
  86. package/src/cli/commands/query/runner/import-adopt.js +111 -0
  87. package/src/cli/commands/query/runner/index.js +31 -0
  88. package/src/cli/commands/query/runner/output.js +12 -0
  89. package/src/cli/commands/query/runner/workflow.js +241 -0
  90. package/src/cli/commands/query/runner.js +3 -0
  91. package/src/cli/commands/query/workflow-context.js +5 -0
  92. package/src/cli/commands/query/workspace.js +270 -0
  93. package/src/cli/commands/query.js +9 -1300
  94. package/src/cli/commands/source.js +3 -12
  95. package/src/cli/commands/template/baseline.js +100 -0
  96. package/src/cli/commands/template/check.js +467 -0
  97. package/src/cli/commands/template/constants.js +8 -0
  98. package/src/cli/commands/template/diagnostics.js +26 -0
  99. package/src/cli/commands/template/help.js +28 -0
  100. package/src/cli/commands/template/lifecycle.js +404 -0
  101. package/src/cli/commands/template/list-show.js +287 -0
  102. package/src/cli/commands/template/policy.js +422 -0
  103. package/src/cli/commands/template/shared.js +127 -0
  104. package/src/cli/commands/template/updates.js +352 -0
  105. package/src/cli/commands/template-runner.js +6 -6
  106. package/src/cli/commands/template.js +41 -2143
  107. package/src/cli/commands/trust.js +1 -1
  108. package/src/cli/commands/workflow.js +6 -1
  109. package/src/cli/dispatcher.js +6 -1
  110. package/src/cli/help.js +15 -14
  111. package/src/cli/migration-guidance.js +1 -1
  112. package/src/cli/output-safety.js +2 -1
  113. package/src/cli/path-normalization.js +3 -13
  114. package/src/generator/api/contracts.js +497 -0
  115. package/src/generator/api/metadata.js +221 -0
  116. package/src/generator/api/openapi.js +559 -0
  117. package/src/generator/api/schema.js +124 -0
  118. package/src/generator/api/types.d.ts +98 -0
  119. package/src/generator/api.js +3 -1195
  120. package/src/generator/context/domain-page.js +1 -1
  121. package/src/generator/context/shared/domain-sdlc.js +282 -0
  122. package/src/generator/context/shared/maintained-boundary.js +665 -0
  123. package/src/generator/context/shared/metrics.js +85 -0
  124. package/src/generator/context/shared/primitives.js +64 -0
  125. package/src/generator/context/shared/relationships.js +453 -0
  126. package/src/generator/context/shared/summaries.js +263 -0
  127. package/src/generator/context/shared/types.d.ts +207 -0
  128. package/src/generator/context/shared.d.ts +42 -0
  129. package/src/generator/context/shared.js +80 -1390
  130. package/src/generator/context/slice/core.js +397 -0
  131. package/src/generator/context/slice/sdlc.js +417 -0
  132. package/src/generator/context/slice/ui-packets.js +183 -0
  133. package/src/generator/context/slice.js +2 -859
  134. package/src/generator/context/task-mode.js +2 -2
  135. package/src/generator/registry/index.js +507 -0
  136. package/src/generator/registry.js +18 -504
  137. package/src/generator/runtime/environment/index.js +666 -0
  138. package/src/generator/runtime/environment.js +4 -666
  139. package/src/generator/runtime/runtime-check/index.js +554 -0
  140. package/src/generator/runtime/runtime-check.js +4 -554
  141. package/src/generator/runtime/shared/index.js +572 -0
  142. package/src/generator/runtime/shared.js +19 -570
  143. package/src/generator/sdlc/doc-page.js +1 -1
  144. package/src/generator/shared.d.ts +2 -0
  145. package/src/generator/surfaces/databases/lifecycle-shared.js +1 -1
  146. package/src/generator/surfaces/native/swiftui-templates/README.generated.md +1 -1
  147. package/src/generator/surfaces/shared.d.ts +3 -0
  148. package/src/generator/widget-conformance/behavior-report.js +258 -0
  149. package/src/generator/widget-conformance/checks.js +371 -0
  150. package/src/generator/widget-conformance/projection-context.js +200 -0
  151. package/src/generator/widget-conformance/report.js +166 -0
  152. package/src/generator/widget-conformance/types.d.ts +121 -0
  153. package/src/generator/widget-conformance.js +3 -824
  154. package/src/import/core/context.d.ts +3 -0
  155. package/src/import/core/context.js +5 -7
  156. package/src/import/core/contracts.d.ts +1 -0
  157. package/src/import/core/registry.d.ts +4 -0
  158. package/src/import/core/runner/candidates.js +337 -0
  159. package/src/import/core/runner/options.js +22 -0
  160. package/src/import/core/runner/reports.js +51 -0
  161. package/src/import/core/runner/run.js +79 -0
  162. package/src/import/core/runner/tracks.js +150 -0
  163. package/src/import/core/runner/ui-drafts.js +393 -0
  164. package/src/import/core/runner.js +3 -698
  165. package/src/import/core/shared/api-routes.js +221 -0
  166. package/src/import/core/shared/candidates.js +97 -0
  167. package/src/import/core/shared/files.js +177 -0
  168. package/src/import/core/shared/next-app.js +389 -0
  169. package/src/import/core/shared/types.d.ts +51 -0
  170. package/src/import/core/shared/ui-routes.js +230 -0
  171. package/src/import/core/shared.js +60 -861
  172. package/src/new-project/constants.js +128 -0
  173. package/src/new-project/create.js +90 -0
  174. package/src/new-project/json.js +28 -0
  175. package/src/new-project/metadata.js +96 -0
  176. package/src/new-project/package-spec.js +161 -0
  177. package/src/new-project/project-files.js +351 -0
  178. package/src/new-project/template-policy.js +269 -0
  179. package/src/new-project/template-resolution.js +370 -0
  180. package/src/new-project/template-snapshots.js +442 -0
  181. package/src/new-project/template-updates.js +512 -0
  182. package/src/new-project/types.d.ts +83 -0
  183. package/src/new-project.js +6 -2277
  184. package/src/parser.d.ts +87 -1
  185. package/src/parser.js +118 -0
  186. package/src/policy/review-boundaries.d.ts +15 -0
  187. package/src/project-config/index.js +591 -0
  188. package/src/project-config.js +19 -561
  189. package/src/resolver/enrich/acceptance-criterion.js +2 -0
  190. package/src/resolver/enrich/bug.js +2 -0
  191. package/src/resolver/enrich/pitch.js +2 -0
  192. package/src/resolver/enrich/requirement.js +2 -0
  193. package/src/resolver/enrich/task.js +2 -0
  194. package/src/resolver/index.js +19 -2089
  195. package/src/resolver/normalize.js +384 -1
  196. package/src/resolver/plans.js +168 -0
  197. package/src/resolver/projections-api.js +494 -0
  198. package/src/resolver/projections-db.js +133 -0
  199. package/src/resolver/projections-ui.js +317 -0
  200. package/src/resolver/shapes.js +251 -0
  201. package/src/resolver/shared.js +278 -0
  202. package/src/resolver/widgets.js +132 -0
  203. package/src/sdlc/adopt.js +6 -5
  204. package/src/sdlc/paths.js +3 -5
  205. package/src/sdlc/scaffold.js +2 -1
  206. package/src/template-trust/constants.js +62 -0
  207. package/src/template-trust/content.js +258 -0
  208. package/src/template-trust/diff.js +92 -0
  209. package/src/template-trust/policy.js +61 -0
  210. package/src/template-trust/record.js +90 -0
  211. package/src/template-trust/status.js +182 -0
  212. package/src/template-trust.js +24 -687
  213. package/src/text-helpers.d.ts +1 -0
  214. package/src/topogram-types.d.ts +69 -0
  215. package/src/validator/common.js +488 -0
  216. package/src/validator/data-model.js +237 -0
  217. package/src/validator/docs.js +167 -0
  218. package/src/validator/expressions.js +146 -1
  219. package/src/validator/index.d.ts +23 -0
  220. package/src/validator/index.js +32 -3585
  221. package/src/validator/kinds.d.ts +41 -0
  222. package/src/validator/kinds.js +2 -0
  223. package/src/validator/model-helpers.js +46 -0
  224. package/src/validator/per-kind/acceptance-criterion.js +5 -0
  225. package/src/validator/per-kind/bug.js +6 -0
  226. package/src/validator/per-kind/domain.js +15 -2
  227. package/src/validator/per-kind/pitch.js +7 -0
  228. package/src/validator/per-kind/requirement.js +5 -0
  229. package/src/validator/per-kind/task.js +7 -0
  230. package/src/validator/per-kind/widget.js +14 -0
  231. package/src/validator/projections/api-http-async.js +410 -0
  232. package/src/validator/projections/api-http-authz.js +88 -0
  233. package/src/validator/projections/api-http-core.js +205 -0
  234. package/src/validator/projections/api-http-policies.js +339 -0
  235. package/src/validator/projections/api-http-responses.js +233 -0
  236. package/src/validator/projections/api-http.js +44 -0
  237. package/src/validator/projections/db.js +353 -0
  238. package/src/validator/projections/generator-defaults.js +45 -0
  239. package/src/validator/projections/helpers.js +87 -0
  240. package/src/validator/projections/ui-helpers.js +214 -0
  241. package/src/validator/projections/ui-navigation.js +344 -0
  242. package/src/validator/projections/ui-structure.js +364 -0
  243. package/src/validator/projections/ui-widgets.js +493 -0
  244. package/src/validator/projections/ui.js +46 -0
  245. package/src/validator/registry.js +48 -1
  246. package/src/validator/utils.d.ts +20 -0
  247. package/src/validator/utils.js +115 -12
  248. package/src/widget-behavior.d.ts +1 -0
  249. package/src/workflows/import-app/api/collect.js +221 -0
  250. package/src/workflows/import-app/api/openapi.js +257 -0
  251. package/src/workflows/import-app/api/routes.js +327 -0
  252. package/src/workflows/import-app/api/sources.js +22 -0
  253. package/src/workflows/import-app/api.js +2 -797
  254. package/src/workflows/reconcile/adoption-plan/build.js +212 -0
  255. package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
  256. package/src/workflows/reconcile/adoption-plan/outputs.js +153 -0
  257. package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
  258. package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
  259. package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
  260. package/src/workflows/reconcile/adoption-plan.js +30 -740
  261. package/src/workflows/reconcile/auth/closures.js +115 -0
  262. package/src/workflows/reconcile/auth/formatters.js +142 -0
  263. package/src/workflows/reconcile/auth/inference.js +330 -0
  264. package/src/workflows/reconcile/auth/roles.js +122 -0
  265. package/src/workflows/reconcile/auth.js +35 -690
  266. package/src/workflows/reconcile/bundle-core/index.js +600 -0
  267. package/src/workflows/reconcile/bundle-core.js +12 -598
  268. package/src/workflows/reconcile/candidate-model.js +18 -2
  269. package/src/workflows/reconcile/canonical-surface.js +1 -1
  270. package/src/workflows/reconcile/impacts/adoption-plan.js +196 -0
  271. package/src/workflows/reconcile/impacts/indexes.js +105 -0
  272. package/src/workflows/reconcile/impacts/patches.js +252 -0
  273. package/src/workflows/reconcile/impacts/reports.js +80 -0
  274. package/src/workflows/reconcile/impacts.js +14 -623
  275. package/src/workflows/reconcile/renderers.js +41 -6
  276. package/src/workflows/shared.js +5 -11
  277. package/src/workspace-docs.d.ts +29 -0
  278. package/src/workspace-paths.js +328 -0
@@ -0,0 +1,591 @@
1
+ // @ts-check
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ import {
7
+ isGeneratorCompatible,
8
+ resolveGeneratorManifestForBinding,
9
+ validateGeneratorManifest
10
+ } from "../generator/registry.js";
11
+ import { validateProjectGeneratorPolicy } from "../generator-policy.js";
12
+ import { DEFAULT_WORKSPACE_PATH, normalizeWorkspaceConfigPath } from "../workspace-paths.js";
13
+
14
+ /**
15
+ * @typedef {Object} GeneratorBinding
16
+ * @property {string} id
17
+ * @property {string} version
18
+ * @property {string} [package]
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} RuntimeTopologyRuntime
23
+ * @property {string} id
24
+ * @property {"api_service"|"web_surface"|"ios_surface"|"android_surface"|"database"} kind
25
+ * @property {string} projection
26
+ * @property {GeneratorBinding} generator
27
+ * @property {number|null} [port]
28
+ * @property {string} [uses_api]
29
+ * @property {string} [uses_database]
30
+ * @property {Record<string, string>} [env]
31
+ */
32
+
33
+ /**
34
+ * @typedef {Object} ProjectConfig
35
+ * @property {string} version
36
+ * @property {string} [workspace]
37
+ * @property {Record<string, { path: string, ownership: "generated"|"maintained" }>} outputs
38
+ * @property {{ runtimes: RuntimeTopologyRuntime[] }} topology
39
+ * @property {{ id?: string, module?: string, export?: string, implementation_module?: string, implementation_export?: string }} [implementation]
40
+ * @property {Record<string, any>} [template]
41
+ */
42
+
43
+ /**
44
+ * @typedef {Object} ProjectConfigInfo
45
+ * @property {ProjectConfig} config
46
+ * @property {string|null} configPath
47
+ * @property {string} configDir
48
+ * @property {boolean} [compatibility]
49
+ */
50
+
51
+ /**
52
+ * @typedef {Object} ValidationError
53
+ * @property {string} message
54
+ * @property {any} loc
55
+ */
56
+
57
+ const PROJECT_CONFIG_FILE = "topogram.project.json";
58
+ const LEGACY_IMPLEMENTATION_FILE = "topogram.implementation.json";
59
+ const GENERATED_OUTPUT_SENTINEL = ".topogram-generated.json";
60
+ const IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
61
+
62
+ /**
63
+ * @param {string|null|undefined} root
64
+ * @returns {string|null}
65
+ */
66
+ function normalizeSearchRoot(root) {
67
+ if (!root) {
68
+ return null;
69
+ }
70
+ const absolute = path.resolve(root);
71
+ try {
72
+ return fs.realpathSync(absolute);
73
+ } catch {
74
+ return absolute;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * @param {string} root
80
+ * @returns {string}
81
+ */
82
+ function normalizeRoot(root) {
83
+ return String(root || "").replace(/\\/g, "/");
84
+ }
85
+
86
+ /**
87
+ * @param {string} filePath
88
+ * @returns {string}
89
+ */
90
+ function resolveComparablePath(filePath) {
91
+ const absolute = path.resolve(filePath);
92
+ try {
93
+ return fs.existsSync(absolute)
94
+ ? fs.realpathSync(absolute)
95
+ : path.join(fs.realpathSync(path.dirname(absolute)), path.basename(absolute));
96
+ } catch {
97
+ return absolute;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * @param {string} filePath
103
+ * @returns {any}
104
+ */
105
+ function readJson(filePath) {
106
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
107
+ }
108
+
109
+ /**
110
+ * @param {string} oldName
111
+ * @param {string} newName
112
+ * @param {string} example
113
+ * @returns {string}
114
+ */
115
+ function renameDiagnostic(oldName, newName, example) {
116
+ return `${oldName} was renamed to ${newName}. Example fix: ${example}`;
117
+ }
118
+
119
+ /**
120
+ * @param {string} root
121
+ * @param {string} fileName
122
+ * @returns {{ config: any, configPath: string, configDir: string }|null}
123
+ */
124
+ function findConfigFile(root, fileName) {
125
+ let current = normalizeSearchRoot(root);
126
+ while (current && current !== path.dirname(current)) {
127
+ const candidate = path.join(current, fileName);
128
+ if (fs.existsSync(candidate)) {
129
+ return {
130
+ config: readJson(candidate),
131
+ configPath: candidate,
132
+ configDir: path.dirname(candidate)
133
+ };
134
+ }
135
+ current = path.dirname(current);
136
+ }
137
+ return null;
138
+ }
139
+
140
+ /**
141
+ * @param {string} root
142
+ * @returns {{ config: any, configPath: string, configDir: string }|null}
143
+ */
144
+ export function findProjectConfig(root) {
145
+ return findConfigFile(root, PROJECT_CONFIG_FILE);
146
+ }
147
+
148
+ /**
149
+ * @param {string} root
150
+ * @returns {{ config: any, configPath: string, configDir: string }|null}
151
+ */
152
+ export function findLegacyImplementationConfig(root) {
153
+ return findConfigFile(root, LEGACY_IMPLEMENTATION_FILE);
154
+ }
155
+
156
+ /**
157
+ * @param {Record<string, any>} graph
158
+ * @param {Record<string, any>|null} [implementation]
159
+ * @returns {ProjectConfig}
160
+ */
161
+ export function defaultProjectConfigForGraph(graph, implementation = null) {
162
+ const runtimeReference = implementation?.runtime?.reference || {};
163
+ /** @type {Array<Record<string, any>>} */
164
+ const projections = graph.byKind.projection || [];
165
+ const apiProjection = projections.find((projection) => (projection.http || []).length > 0 || projection.type === "api_contract");
166
+ const webProjection =
167
+ projections.find((projection) => projection.id === "proj_web") ||
168
+ projections.find((projection) => projection.type === "web_surface");
169
+ const dbProjection =
170
+ projections.find((projection) => projection.id === runtimeReference.localDbProjectionId) ||
171
+ projections.find((projection) => projection.type === "db_contract");
172
+ const ports = runtimeReference.ports || {};
173
+ const dbProfile = (dbProjection?.generatorDefaults || []).find((/** @type {Record<string, any>} */ entry) => entry.key === "profile")?.value;
174
+ const dbGenerator = dbProfile === "sqlite_sql" ? "topogram/sqlite" : "topogram/postgres";
175
+ const dbRuntimeId = dbProfile === "sqlite_sql" ? "app_sqlite" : "app_postgres";
176
+ /** @type {RuntimeTopologyRuntime[]} */
177
+ const runtimes = [
178
+ ...(apiProjection
179
+ ? [{
180
+ id: "app_api",
181
+ kind: /** @type {"api_service"} */ ("api_service"),
182
+ projection: apiProjection.id,
183
+ generator: { id: "topogram/hono", version: "1" },
184
+ port: ports.server || 3000,
185
+ ...(dbProjection ? { uses_database: dbRuntimeId } : {})
186
+ }]
187
+ : []),
188
+ ...(webProjection
189
+ ? [{
190
+ id: "app_sveltekit",
191
+ kind: /** @type {"web_surface"} */ ("web_surface"),
192
+ projection: webProjection.id,
193
+ generator: { id: "topogram/sveltekit", version: "1" },
194
+ port: ports.web || 5173,
195
+ ...(apiProjection ? { uses_api: "app_api" } : {})
196
+ }]
197
+ : []),
198
+ ...(dbProjection
199
+ ? [{
200
+ id: dbRuntimeId,
201
+ kind: /** @type {"database"} */ ("database"),
202
+ projection: dbProjection.id,
203
+ generator: { id: dbGenerator, version: "1" },
204
+ port: dbProfile === "sqlite_sql" ? null : 5432
205
+ }]
206
+ : [])
207
+ ];
208
+
209
+ return {
210
+ version: "0.1",
211
+ workspace: DEFAULT_WORKSPACE_PATH,
212
+ implementation: implementation?.exampleId
213
+ ? {
214
+ id: implementation.exampleId
215
+ }
216
+ : undefined,
217
+ outputs: {
218
+ app: {
219
+ path: "./app",
220
+ ownership: "generated"
221
+ }
222
+ },
223
+ topology: {
224
+ runtimes
225
+ }
226
+ };
227
+ }
228
+
229
+ /**
230
+ * @param {ValidationError[]} errors
231
+ * @param {any} config
232
+ * @returns {void}
233
+ */
234
+ function validateWorkspaceConfig(errors, config) {
235
+ if (Object.prototype.hasOwnProperty.call(config, "workspaces")) {
236
+ pushError(errors, "topogram.project.json workspaces[] is not supported yet; use workspace instead");
237
+ }
238
+ if (!Object.prototype.hasOwnProperty.call(config, "workspace")) {
239
+ return;
240
+ }
241
+ if (typeof config.workspace !== "string") {
242
+ pushError(errors, "topogram.project.json workspace must be a non-empty relative path");
243
+ return;
244
+ }
245
+ try {
246
+ normalizeWorkspaceConfigPath(config.workspace);
247
+ } catch (error) {
248
+ pushError(errors, error instanceof Error ? error.message : String(error));
249
+ }
250
+ }
251
+
252
+ /**
253
+ * @param {string} root
254
+ * @returns {ProjectConfigInfo|null}
255
+ */
256
+ export function loadProjectConfig(root) {
257
+ const found = findProjectConfig(root);
258
+ if (!found) {
259
+ return null;
260
+ }
261
+ return {
262
+ ...found,
263
+ config: found.config,
264
+ compatibility: false
265
+ };
266
+ }
267
+
268
+ /**
269
+ * @param {string} root
270
+ * @param {Record<string, any>|null} [graph]
271
+ * @param {Record<string, any>|null} [implementation]
272
+ * @returns {ProjectConfigInfo|null}
273
+ */
274
+ export function projectConfigOrDefault(root, graph = null, implementation = null) {
275
+ const found = loadProjectConfig(root);
276
+ if (found) {
277
+ return found;
278
+ }
279
+ if (!graph) {
280
+ return null;
281
+ }
282
+ return {
283
+ config: defaultProjectConfigForGraph(graph, implementation),
284
+ configPath: null,
285
+ configDir: path.dirname(path.resolve(root)),
286
+ compatibility: true
287
+ };
288
+ }
289
+
290
+ /**
291
+ * @param {ValidationError[]} errors
292
+ * @param {string} message
293
+ * @param {any} [loc]
294
+ * @returns {void}
295
+ */
296
+ function pushError(errors, message, loc = null) {
297
+ errors.push({ message, loc });
298
+ }
299
+
300
+ /**
301
+ * @param {Record<string, any>} graph
302
+ * @returns {Map<string, Record<string, any>>}
303
+ */
304
+ function projectionById(graph) {
305
+ /** @type {Array<Record<string, any>>} */
306
+ const projections = graph?.byKind?.projection || [];
307
+ return new Map(projections.map((projection) => [projection.id, projection]));
308
+ }
309
+
310
+ /**
311
+ * @param {ValidationError[]} errors
312
+ * @param {any} config
313
+ * @returns {void}
314
+ */
315
+ function validateOutputConfig(errors, config) {
316
+ if (!config.outputs || typeof config.outputs !== "object" || Array.isArray(config.outputs)) {
317
+ pushError(errors, "topogram.project.json outputs must be an object");
318
+ return;
319
+ }
320
+ for (const [name, output] of Object.entries(config.outputs)) {
321
+ if (!output || typeof output !== "object" || Array.isArray(output)) {
322
+ pushError(errors, `Output '${name}' must be an object`);
323
+ continue;
324
+ }
325
+ if (!["generated", "maintained"].includes(output.ownership)) {
326
+ pushError(errors, `Output '${name}' ownership must be generated or maintained`);
327
+ }
328
+ if (typeof output.path !== "string" || output.path.length === 0) {
329
+ pushError(errors, `Output '${name}' path must be a non-empty string`);
330
+ }
331
+ }
332
+ }
333
+
334
+ /**
335
+ * @param {any} runtime
336
+ * @returns {string}
337
+ */
338
+ function runtimeLabel(runtime) {
339
+ return runtime?.id ? `Runtime '${runtime.id}'` : "Topology runtime";
340
+ }
341
+
342
+ /**
343
+ * @param {ValidationError[]} errors
344
+ * @param {any} runtime
345
+ * @param {Set<string>} seenIds
346
+ * @returns {boolean}
347
+ */
348
+ function validateRuntimeShape(errors, runtime, seenIds) {
349
+ if (!runtime || typeof runtime !== "object" || Array.isArray(runtime)) {
350
+ pushError(errors, "Topology runtime must be an object");
351
+ return false;
352
+ }
353
+ if (typeof runtime.id !== "string" || !IDENTIFIER_PATTERN.test(runtime.id)) {
354
+ pushError(errors, `${runtimeLabel(runtime)} id must match ${IDENTIFIER_PATTERN}`);
355
+ } else if (seenIds.has(runtime.id)) {
356
+ pushError(errors, `Duplicate topology runtime id '${runtime.id}'`);
357
+ } else {
358
+ seenIds.add(runtime.id);
359
+ }
360
+ if (runtime.type != null) {
361
+ pushError(errors, `${runtimeLabel(runtime)} ${renameDiagnostic("'type'", "'kind'", `"kind": "api_service"`)}`);
362
+ }
363
+ if (runtime.database != null) {
364
+ pushError(errors, `${runtimeLabel(runtime)} ${renameDiagnostic("'database'", "'uses_database'", `"uses_database": "app_db"`)}`);
365
+ }
366
+ if (runtime.api != null) {
367
+ pushError(errors, `${runtimeLabel(runtime)} ${renameDiagnostic("'api'", "'uses_api'", `"uses_api": "app_api"`)}`);
368
+ }
369
+ if (!["api_service", "web_surface", "ios_surface", "android_surface", "database"].includes(runtime.kind)) {
370
+ pushError(errors, `${runtimeLabel(runtime)} kind must be api_service, web_surface, ios_surface, android_surface, or database`);
371
+ }
372
+ if (typeof runtime.projection !== "string" || runtime.projection.length === 0) {
373
+ pushError(errors, `${runtimeLabel(runtime)} projection must be a non-empty string`);
374
+ }
375
+ if (!runtime.generator || typeof runtime.generator !== "object") {
376
+ pushError(errors, `${runtimeLabel(runtime)} generator must be an object`);
377
+ } else {
378
+ if (typeof runtime.generator.id !== "string" || runtime.generator.id.length === 0) {
379
+ pushError(errors, `${runtimeLabel(runtime)} generator.id must be a non-empty string`);
380
+ }
381
+ if (typeof runtime.generator.version !== "string" || runtime.generator.version.length === 0) {
382
+ pushError(errors, `${runtimeLabel(runtime)} generator.version must be a non-empty string`);
383
+ }
384
+ if (runtime.generator.package != null && (typeof runtime.generator.package !== "string" || runtime.generator.package.length === 0)) {
385
+ pushError(errors, `${runtimeLabel(runtime)} generator.package must be a non-empty string when provided`);
386
+ }
387
+ }
388
+ if (runtime.port != null && (!Number.isInteger(runtime.port) || runtime.port <= 0 || runtime.port > 65535)) {
389
+ pushError(errors, `${runtimeLabel(runtime)} port must be an integer from 1 to 65535`);
390
+ }
391
+ return true;
392
+ }
393
+
394
+ /**
395
+ * @param {ValidationError[]} errors
396
+ * @param {RuntimeTopologyRuntime} runtime
397
+ * @param {Map<string, Record<string, any>>} projections
398
+ * @param {{ configDir?: string|null, rootDir?: string|null }} [options]
399
+ * @returns {void}
400
+ */
401
+ function validateRuntimeCompatibility(errors, runtime, projections, options = {}) {
402
+ const projection = projections.get(runtime.projection);
403
+ if (!projection) {
404
+ pushError(errors, `${runtimeLabel(runtime)} references missing projection '${runtime.projection}'`);
405
+ return;
406
+ }
407
+
408
+ const resolvedManifest = resolveGeneratorManifestForBinding(runtime.generator, options);
409
+ const manifest = resolvedManifest.manifest;
410
+ if (!manifest) {
411
+ const details = resolvedManifest.errors.length > 0 ? `: ${resolvedManifest.errors.join("; ")}` : "";
412
+ pushError(errors, `${runtimeLabel(runtime)} for projection '${projection.id}' uses unknown generator '${runtime.generator?.id}' version '${runtime.generator?.version || "unknown"}'${details}`);
413
+ return;
414
+ }
415
+ const manifestValidation = validateGeneratorManifest(manifest);
416
+ if (!manifestValidation.ok) {
417
+ for (const message of manifestValidation.errors) {
418
+ pushError(errors, `${runtimeLabel(runtime)} generator manifest invalid: ${message}`);
419
+ }
420
+ }
421
+ if (manifest.planned) {
422
+ pushError(errors, `${runtimeLabel(runtime)} for projection '${projection.id}' uses planned generator '${manifest.id}@${manifest.version}', which is not implemented yet`);
423
+ }
424
+ if (manifest.version !== runtime.generator.version) {
425
+ pushError(errors, `${runtimeLabel(runtime)} for projection '${projection.id}' generator '${manifest.id}' version '${runtime.generator.version}' is unsupported; expected '${manifest.version}'`);
426
+ }
427
+ if (!isGeneratorCompatible(manifest, runtime.kind, projection)) {
428
+ pushError(errors, `${runtimeLabel(runtime)} for projection '${projection.id}' generator '${manifest.id}@${manifest.version}' is incompatible with runtime kind '${runtime.kind}' and projection type '${projection.type || "api_contract"}'`);
429
+ }
430
+ }
431
+
432
+ /**
433
+ * @param {ValidationError[]} errors
434
+ * @param {RuntimeTopologyRuntime[]} runtimes
435
+ * @returns {void}
436
+ */
437
+ function validateTopologyReferences(errors, runtimes) {
438
+ const byId = new Map(runtimes.map((runtime) => [runtime.id, runtime]));
439
+ const usedPorts = new Map();
440
+ for (const runtime of runtimes) {
441
+ if (runtime.port != null) {
442
+ const existing = usedPorts.get(runtime.port);
443
+ if (existing) {
444
+ pushError(errors, `Port ${runtime.port} is used by both '${existing}' and '${runtime.id}'`);
445
+ } else {
446
+ usedPorts.set(runtime.port, runtime.id);
447
+ }
448
+ }
449
+ if (runtime.kind === "api_service") {
450
+ if (runtime.uses_database && byId.get(runtime.uses_database)?.kind !== "database") {
451
+ pushError(errors, `${runtimeLabel(runtime)} references missing database runtime '${runtime.uses_database}'`);
452
+ }
453
+ }
454
+ if (["web_surface", "ios_surface", "android_surface"].includes(runtime.kind)) {
455
+ if (runtime.uses_api && byId.get(runtime.uses_api)?.kind !== "api_service") {
456
+ pushError(errors, `${runtimeLabel(runtime)} references missing api runtime '${runtime.uses_api}'`);
457
+ }
458
+ }
459
+ }
460
+ }
461
+
462
+ /**
463
+ * @param {any} config
464
+ * @param {Record<string, any>|null} [graph]
465
+ * @param {{ configDir?: string|null, rootDir?: string|null }} [options]
466
+ * @returns {{ ok: boolean, errors: ValidationError[] }}
467
+ */
468
+ export function validateProjectConfig(config, graph = null, options = {}) {
469
+ /** @type {ValidationError[]} */
470
+ const errors = [];
471
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
472
+ return { ok: false, errors: [{ message: "topogram.project.json must contain a JSON object", loc: null }] };
473
+ }
474
+ if (typeof config.version !== "string" || config.version.length === 0) {
475
+ pushError(errors, "topogram.project.json version must be a non-empty string");
476
+ }
477
+ validateWorkspaceConfig(errors, config);
478
+ validateOutputConfig(errors, config);
479
+ if (config.topology?.components != null) {
480
+ pushError(errors, `topogram.project.json ${renameDiagnostic("'topology.components'", "'topology.runtimes'", `"topology": { "runtimes": [] }`)}`);
481
+ }
482
+ if (!config.topology || typeof config.topology !== "object" || !Array.isArray(config.topology.runtimes)) {
483
+ pushError(errors, "topogram.project.json topology.runtimes must be an array");
484
+ } else {
485
+ const seenIds = new Set();
486
+ for (const runtime of config.topology.runtimes) {
487
+ validateRuntimeShape(errors, runtime, seenIds);
488
+ }
489
+ const generatorPolicy = validateProjectGeneratorPolicy(config, options);
490
+ for (const error of generatorPolicy.errors) {
491
+ pushError(errors, error.message, error.loc);
492
+ }
493
+ if (graph) {
494
+ const projections = projectionById(graph);
495
+ for (const runtime of config.topology.runtimes) {
496
+ validateRuntimeCompatibility(errors, runtime, projections, options);
497
+ }
498
+ validateTopologyReferences(errors, config.topology.runtimes);
499
+ }
500
+ }
501
+ return {
502
+ ok: errors.length === 0,
503
+ errors
504
+ };
505
+ }
506
+
507
+ /**
508
+ * @param {{ errors: ValidationError[] }} result
509
+ * @param {string} [configPath]
510
+ * @returns {string}
511
+ */
512
+ export function formatProjectConfigErrors(result, configPath = PROJECT_CONFIG_FILE) {
513
+ return result.errors
514
+ .map((error) => `${normalizeRoot(configPath)} ${error.message}`)
515
+ .join("\n");
516
+ }
517
+
518
+ /**
519
+ * @param {ProjectConfigInfo|null|undefined} configInfo
520
+ * @param {string} outputName
521
+ * @returns {string|null}
522
+ */
523
+ export function resolveOutputPath(configInfo, outputName) {
524
+ const output = configInfo?.config?.outputs?.[outputName];
525
+ if (!configInfo || !output?.path) {
526
+ return null;
527
+ }
528
+ const baseDir = configInfo.configDir || process.cwd();
529
+ return resolveComparablePath(path.resolve(baseDir, output.path));
530
+ }
531
+
532
+ /**
533
+ * @param {ProjectConfigInfo|null|undefined} configInfo
534
+ * @param {string} outDir
535
+ * @returns {{ name: string, ownership: string, path: string }|null}
536
+ */
537
+ export function outputOwnershipForPath(configInfo, outDir) {
538
+ if (!configInfo?.config?.outputs) {
539
+ return null;
540
+ }
541
+ const resolvedOutDir = resolveComparablePath(outDir);
542
+ for (const [name, output] of Object.entries(configInfo.config.outputs)) {
543
+ if (!output?.path) {
544
+ continue;
545
+ }
546
+ const resolvedOutput = resolveComparablePath(path.resolve(configInfo.configDir || process.cwd(), output.path));
547
+ if (resolvedOutput === resolvedOutDir) {
548
+ return {
549
+ name,
550
+ ownership: output.ownership,
551
+ path: resolvedOutput
552
+ };
553
+ }
554
+ }
555
+ return null;
556
+ }
557
+
558
+ /**
559
+ * @param {ProjectConfigInfo|null|undefined} configInfo
560
+ * @returns {{ ok: boolean, errors: ValidationError[] }}
561
+ */
562
+ export function validateProjectOutputOwnership(configInfo) {
563
+ /** @type {ValidationError[]} */
564
+ const errors = [];
565
+ if (!configInfo?.config?.outputs) {
566
+ return { ok: true, errors };
567
+ }
568
+ for (const [name, output] of Object.entries(configInfo.config.outputs)) {
569
+ if (!output?.path || !["generated", "maintained"].includes(output.ownership)) {
570
+ continue;
571
+ }
572
+ const resolvedOutput = resolveComparablePath(path.resolve(configInfo.configDir || process.cwd(), output.path));
573
+ const sentinelPath = path.join(resolvedOutput, GENERATED_OUTPUT_SENTINEL);
574
+ if (output.ownership === "generated" && fs.existsSync(resolvedOutput) && !fs.existsSync(sentinelPath)) {
575
+ pushError(
576
+ errors,
577
+ `Generated output '${name}' at '${normalizeRoot(resolvedOutput)}' is missing ${GENERATED_OUTPUT_SENTINEL}`
578
+ );
579
+ }
580
+ if (output.ownership === "maintained" && fs.existsSync(sentinelPath)) {
581
+ pushError(
582
+ errors,
583
+ `Maintained output '${name}' at '${normalizeRoot(resolvedOutput)}' contains ${GENERATED_OUTPUT_SENTINEL}`
584
+ );
585
+ }
586
+ }
587
+ return {
588
+ ok: errors.length === 0,
589
+ errors
590
+ };
591
+ }