@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,535 @@
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
+
12
+ /**
13
+ * @typedef {Object} GeneratorBinding
14
+ * @property {string} id
15
+ * @property {string} version
16
+ * @property {string} [package]
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} RuntimeTopologyComponent
21
+ * @property {string} id
22
+ * @property {"api"|"web"|"database"|"native"} type
23
+ * @property {string} projection
24
+ * @property {GeneratorBinding} generator
25
+ * @property {number|null} [port]
26
+ * @property {string} [api]
27
+ * @property {string} [database]
28
+ * @property {Record<string, string>} [env]
29
+ */
30
+
31
+ /**
32
+ * @typedef {Object} ProjectConfig
33
+ * @property {string} version
34
+ * @property {Record<string, { path: string, ownership: "generated"|"maintained" }>} outputs
35
+ * @property {{ components: RuntimeTopologyComponent[] }} topology
36
+ * @property {{ id?: string, module?: string, export?: string, implementation_module?: string, implementation_export?: string }} [implementation]
37
+ */
38
+
39
+ /**
40
+ * @typedef {Object} ProjectConfigInfo
41
+ * @property {ProjectConfig} config
42
+ * @property {string|null} configPath
43
+ * @property {string} configDir
44
+ * @property {boolean} [compatibility]
45
+ */
46
+
47
+ /**
48
+ * @typedef {Object} ValidationError
49
+ * @property {string} message
50
+ * @property {any} loc
51
+ */
52
+
53
+ const PROJECT_CONFIG_FILE = "topogram.project.json";
54
+ const LEGACY_IMPLEMENTATION_FILE = "topogram.implementation.json";
55
+ const GENERATED_OUTPUT_SENTINEL = ".topogram-generated.json";
56
+ const IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
57
+
58
+ /**
59
+ * @param {string|null|undefined} root
60
+ * @returns {string|null}
61
+ */
62
+ function normalizeSearchRoot(root) {
63
+ if (!root) {
64
+ return null;
65
+ }
66
+ const absolute = path.resolve(root);
67
+ try {
68
+ return fs.realpathSync(absolute);
69
+ } catch {
70
+ return absolute;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * @param {string} root
76
+ * @returns {string}
77
+ */
78
+ function normalizeRoot(root) {
79
+ return String(root || "").replace(/\\/g, "/");
80
+ }
81
+
82
+ /**
83
+ * @param {string} filePath
84
+ * @returns {string}
85
+ */
86
+ function resolveComparablePath(filePath) {
87
+ const absolute = path.resolve(filePath);
88
+ try {
89
+ return fs.existsSync(absolute)
90
+ ? fs.realpathSync(absolute)
91
+ : path.join(fs.realpathSync(path.dirname(absolute)), path.basename(absolute));
92
+ } catch {
93
+ return absolute;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * @param {string} filePath
99
+ * @returns {any}
100
+ */
101
+ function readJson(filePath) {
102
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
103
+ }
104
+
105
+ /**
106
+ * @param {string} root
107
+ * @param {string} fileName
108
+ * @returns {{ config: any, configPath: string, configDir: string }|null}
109
+ */
110
+ function findConfigFile(root, fileName) {
111
+ let current = normalizeSearchRoot(root);
112
+ while (current && current !== path.dirname(current)) {
113
+ const candidate = path.join(current, fileName);
114
+ if (fs.existsSync(candidate)) {
115
+ return {
116
+ config: readJson(candidate),
117
+ configPath: candidate,
118
+ configDir: path.dirname(candidate)
119
+ };
120
+ }
121
+ current = path.dirname(current);
122
+ }
123
+ return null;
124
+ }
125
+
126
+ /**
127
+ * @param {string} root
128
+ * @returns {{ config: any, configPath: string, configDir: string }|null}
129
+ */
130
+ export function findProjectConfig(root) {
131
+ return findConfigFile(root, PROJECT_CONFIG_FILE);
132
+ }
133
+
134
+ /**
135
+ * @param {string} root
136
+ * @returns {{ config: any, configPath: string, configDir: string }|null}
137
+ */
138
+ export function findLegacyImplementationConfig(root) {
139
+ return findConfigFile(root, LEGACY_IMPLEMENTATION_FILE);
140
+ }
141
+
142
+ /**
143
+ * @param {Record<string, any>} graph
144
+ * @param {Record<string, any>|null} [implementation]
145
+ * @returns {ProjectConfig}
146
+ */
147
+ export function defaultProjectConfigForGraph(graph, implementation = null) {
148
+ const runtimeReference = implementation?.runtime?.reference || {};
149
+ /** @type {Array<Record<string, any>>} */
150
+ const projections = graph.byKind.projection || [];
151
+ const apiProjection = projections.find((projection) => (projection.http || []).length > 0);
152
+ const webProjection =
153
+ projections.find((projection) => projection.id === "proj_ui_web") ||
154
+ projections.find((projection) => projection.platform === "ui_web");
155
+ const dbProjection =
156
+ projections.find((projection) => projection.id === runtimeReference.localDbProjectionId) ||
157
+ projections.find((projection) => projection.platform === "db_postgres") ||
158
+ projections.find((projection) => projection.platform === "db_sqlite");
159
+ const ports = runtimeReference.ports || {};
160
+ const dbGenerator = dbProjection?.platform === "db_sqlite" ? "topogram/sqlite" : "topogram/postgres";
161
+ const dbComponentId = dbProjection?.platform === "db_sqlite" ? "app_sqlite" : "app_postgres";
162
+ /** @type {RuntimeTopologyComponent[]} */
163
+ const components = [
164
+ ...(apiProjection
165
+ ? [{
166
+ id: "app_api",
167
+ type: /** @type {"api"} */ ("api"),
168
+ projection: apiProjection.id,
169
+ generator: { id: "topogram/hono", version: "1" },
170
+ port: ports.server || 3000,
171
+ ...(dbProjection ? { database: dbComponentId } : {})
172
+ }]
173
+ : []),
174
+ ...(webProjection
175
+ ? [{
176
+ id: "app_sveltekit",
177
+ type: /** @type {"web"} */ ("web"),
178
+ projection: webProjection.id,
179
+ generator: { id: "topogram/sveltekit", version: "1" },
180
+ port: ports.web || 5173,
181
+ ...(apiProjection ? { api: "app_api" } : {})
182
+ }]
183
+ : []),
184
+ ...(dbProjection
185
+ ? [{
186
+ id: dbComponentId,
187
+ type: /** @type {"database"} */ ("database"),
188
+ projection: dbProjection.id,
189
+ generator: { id: dbGenerator, version: "1" },
190
+ port: dbProjection.platform === "db_sqlite" ? null : 5432
191
+ }]
192
+ : [])
193
+ ];
194
+
195
+ return {
196
+ version: "0.1",
197
+ implementation: implementation?.exampleId
198
+ ? {
199
+ id: implementation.exampleId
200
+ }
201
+ : undefined,
202
+ outputs: {
203
+ app: {
204
+ path: "./app",
205
+ ownership: "generated"
206
+ }
207
+ },
208
+ topology: {
209
+ components
210
+ }
211
+ };
212
+ }
213
+
214
+ /**
215
+ * @param {string} root
216
+ * @returns {ProjectConfigInfo|null}
217
+ */
218
+ export function loadProjectConfig(root) {
219
+ const found = findProjectConfig(root);
220
+ if (!found) {
221
+ return null;
222
+ }
223
+ return {
224
+ ...found,
225
+ compatibility: false
226
+ };
227
+ }
228
+
229
+ /**
230
+ * @param {string} root
231
+ * @param {Record<string, any>|null} [graph]
232
+ * @param {Record<string, any>|null} [implementation]
233
+ * @returns {ProjectConfigInfo|null}
234
+ */
235
+ export function projectConfigOrDefault(root, graph = null, implementation = null) {
236
+ const found = loadProjectConfig(root);
237
+ if (found) {
238
+ return found;
239
+ }
240
+ if (!graph) {
241
+ return null;
242
+ }
243
+ return {
244
+ config: defaultProjectConfigForGraph(graph, implementation),
245
+ configPath: null,
246
+ configDir: path.dirname(path.resolve(root)),
247
+ compatibility: true
248
+ };
249
+ }
250
+
251
+ /**
252
+ * @param {ValidationError[]} errors
253
+ * @param {string} message
254
+ * @param {any} [loc]
255
+ * @returns {void}
256
+ */
257
+ function pushError(errors, message, loc = null) {
258
+ errors.push({ message, loc });
259
+ }
260
+
261
+ /**
262
+ * @param {Record<string, any>} graph
263
+ * @returns {Map<string, Record<string, any>>}
264
+ */
265
+ function projectionById(graph) {
266
+ /** @type {Array<Record<string, any>>} */
267
+ const projections = graph?.byKind?.projection || [];
268
+ return new Map(projections.map((projection) => [projection.id, projection]));
269
+ }
270
+
271
+ /**
272
+ * @param {ValidationError[]} errors
273
+ * @param {any} config
274
+ * @returns {void}
275
+ */
276
+ function validateOutputConfig(errors, config) {
277
+ if (!config.outputs || typeof config.outputs !== "object" || Array.isArray(config.outputs)) {
278
+ pushError(errors, "topogram.project.json outputs must be an object");
279
+ return;
280
+ }
281
+ for (const [name, output] of Object.entries(config.outputs)) {
282
+ if (!output || typeof output !== "object" || Array.isArray(output)) {
283
+ pushError(errors, `Output '${name}' must be an object`);
284
+ continue;
285
+ }
286
+ if (!["generated", "maintained"].includes(output.ownership)) {
287
+ pushError(errors, `Output '${name}' ownership must be generated or maintained`);
288
+ }
289
+ if (typeof output.path !== "string" || output.path.length === 0) {
290
+ pushError(errors, `Output '${name}' path must be a non-empty string`);
291
+ }
292
+ }
293
+ }
294
+
295
+ /**
296
+ * @param {any} component
297
+ * @returns {string}
298
+ */
299
+ function componentLabel(component) {
300
+ return component?.id ? `Component '${component.id}'` : "Topology component";
301
+ }
302
+
303
+ /**
304
+ * @param {ValidationError[]} errors
305
+ * @param {any} component
306
+ * @param {Set<string>} seenIds
307
+ * @returns {boolean}
308
+ */
309
+ function validateComponentShape(errors, component, seenIds) {
310
+ if (!component || typeof component !== "object" || Array.isArray(component)) {
311
+ pushError(errors, "Topology component must be an object");
312
+ return false;
313
+ }
314
+ if (typeof component.id !== "string" || !IDENTIFIER_PATTERN.test(component.id)) {
315
+ pushError(errors, `${componentLabel(component)} id must match ${IDENTIFIER_PATTERN}`);
316
+ } else if (seenIds.has(component.id)) {
317
+ pushError(errors, `Duplicate topology component id '${component.id}'`);
318
+ } else {
319
+ seenIds.add(component.id);
320
+ }
321
+ if (!["api", "web", "database", "native"].includes(component.type)) {
322
+ pushError(errors, `${componentLabel(component)} type must be api, web, database, or native`);
323
+ }
324
+ if (typeof component.projection !== "string" || component.projection.length === 0) {
325
+ pushError(errors, `${componentLabel(component)} projection must be a non-empty string`);
326
+ }
327
+ if (!component.generator || typeof component.generator !== "object") {
328
+ pushError(errors, `${componentLabel(component)} generator must be an object`);
329
+ } else {
330
+ if (typeof component.generator.id !== "string" || component.generator.id.length === 0) {
331
+ pushError(errors, `${componentLabel(component)} generator.id must be a non-empty string`);
332
+ }
333
+ if (typeof component.generator.version !== "string" || component.generator.version.length === 0) {
334
+ pushError(errors, `${componentLabel(component)} generator.version must be a non-empty string`);
335
+ }
336
+ if (component.generator.package != null && (typeof component.generator.package !== "string" || component.generator.package.length === 0)) {
337
+ pushError(errors, `${componentLabel(component)} generator.package must be a non-empty string when provided`);
338
+ }
339
+ }
340
+ if (component.port != null && (!Number.isInteger(component.port) || component.port <= 0 || component.port > 65535)) {
341
+ pushError(errors, `${componentLabel(component)} port must be an integer from 1 to 65535`);
342
+ }
343
+ return true;
344
+ }
345
+
346
+ /**
347
+ * @param {ValidationError[]} errors
348
+ * @param {RuntimeTopologyComponent} component
349
+ * @param {Map<string, Record<string, any>>} projections
350
+ * @param {{ configDir?: string|null, rootDir?: string|null }} [options]
351
+ * @returns {void}
352
+ */
353
+ function validateComponentCompatibility(errors, component, projections, options = {}) {
354
+ const projection = projections.get(component.projection);
355
+ if (!projection) {
356
+ pushError(errors, `${componentLabel(component)} references missing projection '${component.projection}'`);
357
+ return;
358
+ }
359
+
360
+ const resolvedManifest = resolveGeneratorManifestForBinding(component.generator, options);
361
+ const manifest = resolvedManifest.manifest;
362
+ if (!manifest) {
363
+ const details = resolvedManifest.errors.length > 0 ? `: ${resolvedManifest.errors.join("; ")}` : "";
364
+ pushError(errors, `${componentLabel(component)} for projection '${projection.id}' uses unknown generator '${component.generator?.id}' version '${component.generator?.version || "unknown"}'${details}`);
365
+ return;
366
+ }
367
+ const manifestValidation = validateGeneratorManifest(manifest);
368
+ if (!manifestValidation.ok) {
369
+ for (const message of manifestValidation.errors) {
370
+ pushError(errors, `${componentLabel(component)} generator manifest invalid: ${message}`);
371
+ }
372
+ }
373
+ if (manifest.planned) {
374
+ pushError(errors, `${componentLabel(component)} for projection '${projection.id}' uses planned generator '${manifest.id}@${manifest.version}', which is not implemented yet`);
375
+ }
376
+ if (manifest.version !== component.generator.version) {
377
+ pushError(errors, `${componentLabel(component)} for projection '${projection.id}' generator '${manifest.id}' version '${component.generator.version}' is unsupported; expected '${manifest.version}'`);
378
+ }
379
+ if (!isGeneratorCompatible(manifest, component.type, projection)) {
380
+ pushError(errors, `${componentLabel(component)} for projection '${projection.id}' generator '${manifest.id}@${manifest.version}' is incompatible with component surface '${component.type}' and projection platform '${projection.platform || "api"}'`);
381
+ }
382
+ }
383
+
384
+ /**
385
+ * @param {ValidationError[]} errors
386
+ * @param {RuntimeTopologyComponent[]} components
387
+ * @returns {void}
388
+ */
389
+ function validateTopologyReferences(errors, components) {
390
+ const byId = new Map(components.map((component) => [component.id, component]));
391
+ const usedPorts = new Map();
392
+ for (const component of components) {
393
+ if (component.port != null) {
394
+ const existing = usedPorts.get(component.port);
395
+ if (existing) {
396
+ pushError(errors, `Port ${component.port} is used by both '${existing}' and '${component.id}'`);
397
+ } else {
398
+ usedPorts.set(component.port, component.id);
399
+ }
400
+ }
401
+ if (component.type === "api") {
402
+ if (component.database && byId.get(component.database)?.type !== "database") {
403
+ pushError(errors, `${componentLabel(component)} references missing database component '${component.database}'`);
404
+ }
405
+ }
406
+ if (component.type === "web") {
407
+ if (component.api && byId.get(component.api)?.type !== "api") {
408
+ pushError(errors, `${componentLabel(component)} references missing api component '${component.api}'`);
409
+ }
410
+ }
411
+ }
412
+ }
413
+
414
+ /**
415
+ * @param {any} config
416
+ * @param {Record<string, any>|null} [graph]
417
+ * @param {{ configDir?: string|null, rootDir?: string|null }} [options]
418
+ * @returns {{ ok: boolean, errors: ValidationError[] }}
419
+ */
420
+ export function validateProjectConfig(config, graph = null, options = {}) {
421
+ /** @type {ValidationError[]} */
422
+ const errors = [];
423
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
424
+ return { ok: false, errors: [{ message: "topogram.project.json must contain a JSON object", loc: null }] };
425
+ }
426
+ if (typeof config.version !== "string" || config.version.length === 0) {
427
+ pushError(errors, "topogram.project.json version must be a non-empty string");
428
+ }
429
+ validateOutputConfig(errors, config);
430
+ if (!config.topology || typeof config.topology !== "object" || !Array.isArray(config.topology.components)) {
431
+ pushError(errors, "topogram.project.json topology.components must be an array");
432
+ } else {
433
+ const seenIds = new Set();
434
+ for (const component of config.topology.components) {
435
+ validateComponentShape(errors, component, seenIds);
436
+ }
437
+ if (graph) {
438
+ const projections = projectionById(graph);
439
+ for (const component of config.topology.components) {
440
+ validateComponentCompatibility(errors, component, projections, options);
441
+ }
442
+ validateTopologyReferences(errors, config.topology.components);
443
+ }
444
+ }
445
+ return {
446
+ ok: errors.length === 0,
447
+ errors
448
+ };
449
+ }
450
+
451
+ /**
452
+ * @param {{ errors: ValidationError[] }} result
453
+ * @param {string} [configPath]
454
+ * @returns {string}
455
+ */
456
+ export function formatProjectConfigErrors(result, configPath = PROJECT_CONFIG_FILE) {
457
+ return result.errors
458
+ .map((error) => `${normalizeRoot(configPath)} ${error.message}`)
459
+ .join("\n");
460
+ }
461
+
462
+ /**
463
+ * @param {ProjectConfigInfo|null|undefined} configInfo
464
+ * @param {string} outputName
465
+ * @returns {string|null}
466
+ */
467
+ export function resolveOutputPath(configInfo, outputName) {
468
+ const output = configInfo?.config?.outputs?.[outputName];
469
+ if (!configInfo || !output?.path) {
470
+ return null;
471
+ }
472
+ const baseDir = configInfo.configDir || process.cwd();
473
+ return resolveComparablePath(path.resolve(baseDir, output.path));
474
+ }
475
+
476
+ /**
477
+ * @param {ProjectConfigInfo|null|undefined} configInfo
478
+ * @param {string} outDir
479
+ * @returns {{ name: string, ownership: string, path: string }|null}
480
+ */
481
+ export function outputOwnershipForPath(configInfo, outDir) {
482
+ if (!configInfo?.config?.outputs) {
483
+ return null;
484
+ }
485
+ const resolvedOutDir = resolveComparablePath(outDir);
486
+ for (const [name, output] of Object.entries(configInfo.config.outputs)) {
487
+ if (!output?.path) {
488
+ continue;
489
+ }
490
+ const resolvedOutput = resolveComparablePath(path.resolve(configInfo.configDir || process.cwd(), output.path));
491
+ if (resolvedOutput === resolvedOutDir) {
492
+ return {
493
+ name,
494
+ ownership: output.ownership,
495
+ path: resolvedOutput
496
+ };
497
+ }
498
+ }
499
+ return null;
500
+ }
501
+
502
+ /**
503
+ * @param {ProjectConfigInfo|null|undefined} configInfo
504
+ * @returns {{ ok: boolean, errors: ValidationError[] }}
505
+ */
506
+ export function validateProjectOutputOwnership(configInfo) {
507
+ /** @type {ValidationError[]} */
508
+ const errors = [];
509
+ if (!configInfo?.config?.outputs) {
510
+ return { ok: true, errors };
511
+ }
512
+ for (const [name, output] of Object.entries(configInfo.config.outputs)) {
513
+ if (!output?.path || !["generated", "maintained"].includes(output.ownership)) {
514
+ continue;
515
+ }
516
+ const resolvedOutput = resolveComparablePath(path.resolve(configInfo.configDir || process.cwd(), output.path));
517
+ const sentinelPath = path.join(resolvedOutput, GENERATED_OUTPUT_SENTINEL);
518
+ if (output.ownership === "generated" && fs.existsSync(resolvedOutput) && !fs.existsSync(sentinelPath)) {
519
+ pushError(
520
+ errors,
521
+ `Generated output '${name}' at '${normalizeRoot(resolvedOutput)}' is missing ${GENERATED_OUTPUT_SENTINEL}`
522
+ );
523
+ }
524
+ if (output.ownership === "maintained" && fs.existsSync(sentinelPath)) {
525
+ pushError(
526
+ errors,
527
+ `Maintained output '${name}' at '${normalizeRoot(resolvedOutput)}' contains ${GENERATED_OUTPUT_SENTINEL}`
528
+ );
529
+ }
530
+ }
531
+ return {
532
+ ok: errors.length === 0,
533
+ errors
534
+ };
535
+ }
@@ -0,0 +1,19 @@
1
+ import { generateBackendTarget } from "../generator/surfaces/services/index.js";
2
+
3
+ export function buildBackendParityEvidence(graph, projectionId = "proj_api") {
4
+ const hono = generateBackendTarget("hono-server", graph, { projectionId });
5
+ const express = generateBackendTarget("express-server", graph, { projectionId });
6
+ const honoServerContract = hono["src/lib/topogram/server-contract.ts"] || "";
7
+ const expressServerContract = express["src/lib/topogram/server-contract.ts"] || "";
8
+ const honoApp = hono["src/lib/server/app.ts"] || "";
9
+ const expressApp = express["src/lib/server/app.ts"] || "";
10
+
11
+ return {
12
+ projectionId,
13
+ honoServerContract,
14
+ expressServerContract,
15
+ sharedServerContract: honoServerContract === expressServerContract,
16
+ honoTargetMarker: honoApp.includes('import { Hono } from "hono";'),
17
+ expressTargetMarker: expressApp.includes('import express, { type Request, type Response } from "express";')
18
+ };
19
+ }