@topogram/cli 0.3.51 → 0.3.53

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 (77) hide show
  1. package/ARCHITECTURE.md +4 -4
  2. package/CHANGELOG.md +11 -11
  3. package/package.json +1 -1
  4. package/src/adoption/plan.js +2 -2
  5. package/src/agent-ops/query-builders.js +42 -33
  6. package/src/cli.js +174 -129
  7. package/src/generator/adapters.d.ts +1 -0
  8. package/src/generator/adapters.js +64 -39
  9. package/src/generator/check.js +19 -12
  10. package/src/generator/context/diff.js +9 -9
  11. package/src/generator/context/domain-coverage.js +11 -10
  12. package/src/generator/context/domain-page.js +6 -6
  13. package/src/generator/context/shared.js +37 -21
  14. package/src/generator/context/slice.js +70 -65
  15. package/src/generator/index.js +12 -12
  16. package/src/generator/output.js +21 -20
  17. package/src/generator/registry.js +71 -49
  18. package/src/generator/runtime/app-bundle.js +15 -15
  19. package/src/generator/runtime/compile-check.js +7 -7
  20. package/src/generator/runtime/deployment.js +9 -9
  21. package/src/generator/runtime/environment.js +39 -39
  22. package/src/generator/runtime/runtime-check.js +5 -5
  23. package/src/generator/runtime/shared.js +40 -38
  24. package/src/generator/runtime/smoke.js +5 -5
  25. package/src/generator/surfaces/databases/contract.js +1 -1
  26. package/src/generator/surfaces/databases/lifecycle-shared.js +6 -5
  27. package/src/generator/surfaces/databases/postgres/drizzle.js +3 -2
  28. package/src/generator/surfaces/databases/postgres/prisma.js +3 -2
  29. package/src/generator/surfaces/databases/shared.js +3 -2
  30. package/src/generator/surfaces/databases/snapshot.js +1 -1
  31. package/src/generator/surfaces/databases/sqlite/prisma.js +3 -2
  32. package/src/generator/surfaces/native/swiftui-app.js +3 -3
  33. package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +1 -1
  34. package/src/generator/surfaces/native/swiftui-templates/README.generated.md +3 -3
  35. package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +3 -3
  36. package/src/generator/surfaces/services/persistence-wiring.js +3 -2
  37. package/src/generator/surfaces/services/server-contract.js +4 -4
  38. package/src/generator/surfaces/shared.js +2 -2
  39. package/src/generator/surfaces/web/design-intent.js +1 -1
  40. package/src/generator/surfaces/web/index.js +7 -7
  41. package/src/generator/surfaces/web/{react-components.js → react-widgets.js} +53 -53
  42. package/src/generator/surfaces/web/react.js +36 -36
  43. package/src/generator/surfaces/web/{sveltekit-components.js → sveltekit-widgets.js} +53 -53
  44. package/src/generator/surfaces/web/sveltekit.js +34 -34
  45. package/src/generator/surfaces/web/{ui-web-contract.js → ui-surface-contract.js} +8 -8
  46. package/src/generator/surfaces/web/vanilla.js +6 -6
  47. package/src/generator/{component-conformance.js → widget-conformance.js} +129 -128
  48. package/src/generator/widgets.js +40 -0
  49. package/src/generator-policy.js +10 -12
  50. package/src/import/core/runner.js +34 -34
  51. package/src/import/core/shared.js +1 -1
  52. package/src/import/extractors/ui/android-compose.js +1 -1
  53. package/src/import/extractors/ui/blazor.js +1 -1
  54. package/src/import/extractors/ui/razor-pages.js +1 -1
  55. package/src/import/extractors/ui/react-router.js +4 -4
  56. package/src/import/extractors/ui/sveltekit.js +4 -4
  57. package/src/import/extractors/ui/swiftui.js +1 -1
  58. package/src/import/extractors/ui/uikit.js +1 -1
  59. package/src/new-project.js +19 -18
  60. package/src/project-config.js +104 -44
  61. package/src/proofs/contract-audit.js +1 -1
  62. package/src/proofs/ios-parity.js +1 -1
  63. package/src/proofs/issues-parity.js +1 -1
  64. package/src/realization/backend/build-backend-runtime-realization.js +2 -2
  65. package/src/realization/ui/build-ui-shared-realization.js +33 -33
  66. package/src/realization/ui/build-web-realization.js +23 -20
  67. package/src/reconcile/journeys.js +1 -1
  68. package/src/resolver/index.js +148 -65
  69. package/src/validator/index.js +509 -423
  70. package/src/validator/kinds.js +36 -36
  71. package/src/validator/per-kind/{component.js → widget.js} +47 -47
  72. package/src/{component-behavior.js → widget-behavior.js} +3 -3
  73. package/src/workflows.js +39 -38
  74. package/template-helpers/react.js +4 -4
  75. package/template-helpers/sveltekit.js +4 -4
  76. package/src/generator/components.js +0 -39
  77. /package/src/resolver/enrich/{component.js → widget.js} +0 -0
@@ -10,13 +10,13 @@ import { UI_GENERATOR_RENDERED_COMPONENT_PATTERNS } from "../ui/taxonomy.js";
10
10
  * @property {string} id
11
11
  * @property {string} version
12
12
  * @property {"api"|"web"|"database"|"native"} surface
13
- * @property {"api"|"web"|"database"|"native"} [targetKind]
14
- * @property {string[]} projectionPlatforms
13
+ * @property {string[]} runtimeKinds
14
+ * @property {string[]} projectionTypes
15
15
  * @property {string[]} inputs
16
16
  * @property {string[]} outputs
17
17
  * @property {Record<string, string>} stack
18
18
  * @property {Record<string, boolean>} capabilities
19
- * @property {{ patterns?: string[], behaviors?: string[], unsupported?: "error"|"warning"|"contract-only" }} [componentSupport]
19
+ * @property {{ patterns?: string[], behaviors?: string[], unsupported?: "error"|"warning"|"contract-only" }} [widgetSupport]
20
20
  * @property {"bundled"|"package"} source
21
21
  * @property {string} [profile]
22
22
  * @property {string} [package]
@@ -32,8 +32,8 @@ export const GENERATOR_MANIFESTS = [
32
32
  id: "topogram/hono",
33
33
  version: "1",
34
34
  surface: "api",
35
- targetKind: "api",
36
- projectionPlatforms: ["api"],
35
+ runtimeKinds: ["api_service"],
36
+ projectionTypes: ["api_contract"],
37
37
  inputs: ["server-contract", "api-contracts"],
38
38
  outputs: ["api-service"],
39
39
  stack: { runtime: "node", framework: "hono", language: "typescript" },
@@ -45,8 +45,8 @@ export const GENERATOR_MANIFESTS = [
45
45
  id: "topogram/express",
46
46
  version: "1",
47
47
  surface: "api",
48
- targetKind: "api",
49
- projectionPlatforms: ["api"],
48
+ runtimeKinds: ["api_service"],
49
+ projectionTypes: ["api_contract"],
50
50
  inputs: ["server-contract", "api-contracts"],
51
51
  outputs: ["api-service"],
52
52
  stack: { runtime: "node", framework: "express", language: "typescript" },
@@ -58,13 +58,13 @@ export const GENERATOR_MANIFESTS = [
58
58
  id: "topogram/vanilla-web",
59
59
  version: "1",
60
60
  surface: "web",
61
- targetKind: "web",
62
- projectionPlatforms: ["ui_web"],
63
- inputs: ["ui-web-contract"],
61
+ runtimeKinds: ["web_surface"],
62
+ projectionTypes: ["web_surface"],
63
+ inputs: ["ui-surface-contract"],
64
64
  outputs: ["web-app", "generation-coverage"],
65
65
  stack: { runtime: "browser", framework: "vanilla", language: "javascript" },
66
66
  capabilities: { routes: true, components: false, coverage: true },
67
- componentSupport: { patterns: [], behaviors: [], unsupported: "contract-only" },
67
+ widgetSupport: { patterns: [], behaviors: [], unsupported: "contract-only" },
68
68
  source: "bundled",
69
69
  profile: "vanilla"
70
70
  },
@@ -72,13 +72,13 @@ export const GENERATOR_MANIFESTS = [
72
72
  id: "topogram/sveltekit",
73
73
  version: "1",
74
74
  surface: "web",
75
- targetKind: "web",
76
- projectionPlatforms: ["ui_web"],
77
- inputs: ["ui-web-contract", "api-contracts"],
75
+ runtimeKinds: ["web_surface"],
76
+ projectionTypes: ["web_surface"],
77
+ inputs: ["ui-surface-contract", "api-contracts"],
78
78
  outputs: ["web-app", "generation-coverage"],
79
79
  stack: { runtime: "node", framework: "sveltekit", language: "typescript" },
80
80
  capabilities: { routes: true, components: true, coverage: true },
81
- componentSupport: {
81
+ widgetSupport: {
82
82
  patterns: RENDERED_COMPONENT_PATTERNS,
83
83
  behaviors: ["selection", "sorting", "filtering", "search", "pagination", "bulk_action", "optimistic_update"],
84
84
  unsupported: "warning"
@@ -90,13 +90,13 @@ export const GENERATOR_MANIFESTS = [
90
90
  id: "topogram/react",
91
91
  version: "1",
92
92
  surface: "web",
93
- targetKind: "web",
94
- projectionPlatforms: ["ui_web"],
95
- inputs: ["ui-web-contract", "api-contracts"],
93
+ runtimeKinds: ["web_surface"],
94
+ projectionTypes: ["web_surface"],
95
+ inputs: ["ui-surface-contract", "api-contracts"],
96
96
  outputs: ["web-app", "generation-coverage"],
97
97
  stack: { runtime: "browser", framework: "react", language: "typescript" },
98
98
  capabilities: { routes: true, components: true, coverage: true },
99
- componentSupport: {
99
+ widgetSupport: {
100
100
  patterns: RENDERED_COMPONENT_PATTERNS,
101
101
  behaviors: ["selection", "sorting", "filtering", "search", "pagination", "bulk_action", "optimistic_update"],
102
102
  unsupported: "warning"
@@ -108,13 +108,13 @@ export const GENERATOR_MANIFESTS = [
108
108
  id: "topogram/swiftui",
109
109
  version: "1",
110
110
  surface: "native",
111
- targetKind: "native",
112
- projectionPlatforms: ["ui_ios"],
113
- inputs: ["ui-web-contract", "api-contracts"],
111
+ runtimeKinds: ["ios_surface"],
112
+ projectionTypes: ["ios_surface"],
113
+ inputs: ["ui-surface-contract", "api-contracts"],
114
114
  outputs: ["native-app"],
115
115
  stack: { platform: "ios", framework: "swiftui", language: "swift" },
116
116
  capabilities: { routes: true, components: false, coverage: false },
117
- componentSupport: { patterns: [], behaviors: [], unsupported: "contract-only" },
117
+ widgetSupport: { patterns: [], behaviors: [], unsupported: "contract-only" },
118
118
  source: "bundled",
119
119
  profile: "swiftui"
120
120
  },
@@ -122,8 +122,8 @@ export const GENERATOR_MANIFESTS = [
122
122
  id: "topogram/postgres",
123
123
  version: "1",
124
124
  surface: "database",
125
- targetKind: "database",
126
- projectionPlatforms: ["db_postgres"],
125
+ runtimeKinds: ["database"],
126
+ projectionTypes: ["db_contract"],
127
127
  inputs: ["db-contract", "db-lifecycle-plan"],
128
128
  outputs: ["db-lifecycle-bundle", "sql-schema", "sql-migration", "prisma-schema", "drizzle-schema"],
129
129
  stack: { database: "postgres", language: "sql" },
@@ -135,8 +135,8 @@ export const GENERATOR_MANIFESTS = [
135
135
  id: "topogram/sqlite",
136
136
  version: "1",
137
137
  surface: "database",
138
- targetKind: "database",
139
- projectionPlatforms: ["db_sqlite"],
138
+ runtimeKinds: ["database"],
139
+ projectionTypes: ["db_contract"],
140
140
  inputs: ["db-contract", "db-lifecycle-plan"],
141
141
  outputs: ["db-lifecycle-bundle", "sql-schema", "sql-migration", "prisma-schema"],
142
142
  stack: { database: "sqlite", language: "sql" },
@@ -148,13 +148,13 @@ export const GENERATOR_MANIFESTS = [
148
148
  id: "topogram/android-compose",
149
149
  version: "1",
150
150
  surface: "native",
151
- targetKind: "native",
152
- projectionPlatforms: ["ui_android"],
153
- inputs: ["ui-web-contract", "api-contracts"],
151
+ runtimeKinds: ["android_surface"],
152
+ projectionTypes: ["android_surface"],
153
+ inputs: ["ui-surface-contract", "api-contracts"],
154
154
  outputs: ["native-app"],
155
155
  stack: { platform: "android", framework: "compose", language: "kotlin" },
156
156
  capabilities: { routes: true, components: false, coverage: false },
157
- componentSupport: { patterns: [], behaviors: [], unsupported: "contract-only" },
157
+ widgetSupport: { patterns: [], behaviors: [], unsupported: "contract-only" },
158
158
  source: "bundled",
159
159
  profile: "compose",
160
160
  planned: true
@@ -190,6 +190,16 @@ function isStringArray(value, nonEmpty = false) {
190
190
  value.every((entry) => typeof entry === "string" && entry.length > 0);
191
191
  }
192
192
 
193
+ /**
194
+ * @param {string} oldName
195
+ * @param {string} newName
196
+ * @param {string} example
197
+ * @returns {string}
198
+ */
199
+ function renameDiagnostic(oldName, newName, example) {
200
+ return `${oldName} was renamed to ${newName}. Example fix: ${example}`;
201
+ }
202
+
193
203
  /**
194
204
  * @param {string} generatorId
195
205
  * @returns {GeneratorManifest|null}
@@ -392,8 +402,17 @@ export function validateGeneratorManifest(manifest) {
392
402
  if (!["api", "web", "database", "native"].includes(manifest.surface)) {
393
403
  errors.push(`${label} surface must be api, web, database, or native`);
394
404
  }
395
- if (!isStringArray(manifest.projectionPlatforms, true)) {
396
- errors.push(`${label} projectionPlatforms must be a non-empty string array`);
405
+ if (manifest.targetKind != null) {
406
+ errors.push(`${label} ${renameDiagnostic("'targetKind'", "'runtimeKinds'", `"runtimeKinds": ["web_surface"]`)}`);
407
+ }
408
+ if (!isStringArray(manifest.runtimeKinds, true)) {
409
+ errors.push(`${label} runtimeKinds must be a non-empty string array`);
410
+ }
411
+ if (manifest["projectionPlatforms"] != null) {
412
+ errors.push(`${label} ${renameDiagnostic("'projectionPlatforms'", "'projectionTypes'", `"projectionTypes": ["web_surface"]`)}`);
413
+ }
414
+ if (!isStringArray(manifest.projectionTypes, true)) {
415
+ errors.push(`${label} projectionTypes must be a non-empty string array`);
397
416
  }
398
417
  if (!isStringArray(manifest.inputs)) {
399
418
  errors.push(`${label} inputs must be a string array`);
@@ -407,21 +426,24 @@ export function validateGeneratorManifest(manifest) {
407
426
  if (!manifest.capabilities || typeof manifest.capabilities !== "object" || Array.isArray(manifest.capabilities)) {
408
427
  errors.push(`${label} capabilities must be an object`);
409
428
  }
410
- if (manifest.componentSupport != null) {
411
- if (typeof manifest.componentSupport !== "object" || Array.isArray(manifest.componentSupport)) {
412
- errors.push(`${label} componentSupport must be an object when present`);
429
+ if (manifest["componentSupport"] != null) {
430
+ errors.push(`${label} ${renameDiagnostic("'componentSupport'", "'widgetSupport'", `"widgetSupport": { "patterns": ["resource_table"] }`)}`);
431
+ }
432
+ if (manifest.widgetSupport != null) {
433
+ if (typeof manifest.widgetSupport !== "object" || Array.isArray(manifest.widgetSupport)) {
434
+ errors.push(`${label} widgetSupport must be an object when present`);
413
435
  } else {
414
- if (manifest.componentSupport.patterns != null && !isStringArray(manifest.componentSupport.patterns)) {
415
- errors.push(`${label} componentSupport.patterns must be a string array`);
436
+ if (manifest.widgetSupport.patterns != null && !isStringArray(manifest.widgetSupport.patterns)) {
437
+ errors.push(`${label} widgetSupport.patterns must be a string array`);
416
438
  }
417
- if (manifest.componentSupport.behaviors != null && !isStringArray(manifest.componentSupport.behaviors)) {
418
- errors.push(`${label} componentSupport.behaviors must be a string array`);
439
+ if (manifest.widgetSupport.behaviors != null && !isStringArray(manifest.widgetSupport.behaviors)) {
440
+ errors.push(`${label} widgetSupport.behaviors must be a string array`);
419
441
  }
420
442
  if (
421
- manifest.componentSupport.unsupported != null &&
422
- !["error", "warning", "contract-only"].includes(manifest.componentSupport.unsupported)
443
+ manifest.widgetSupport.unsupported != null &&
444
+ !["error", "warning", "contract-only"].includes(manifest.widgetSupport.unsupported)
423
445
  ) {
424
- errors.push(`${label} componentSupport.unsupported must be error, warning, or contract-only`);
446
+ errors.push(`${label} widgetSupport.unsupported must be error, warning, or contract-only`);
425
447
  }
426
448
  }
427
449
  }
@@ -466,20 +488,20 @@ export function isApiProjection(projection) {
466
488
  */
467
489
  export function projectionCompatibilityKey(projection) {
468
490
  if (isApiProjection(projection)) {
469
- return "api";
491
+ return "api_contract";
470
492
  }
471
- return projection?.platform || "";
493
+ return projection?.type || projection?.platform || "";
472
494
  }
473
495
 
474
496
  /**
475
497
  * @param {GeneratorManifest|null|undefined} manifest
476
- * @param {string} componentType
498
+ * @param {string} runtimeKind
477
499
  * @param {Record<string, any>|null|undefined} projection
478
500
  * @returns {boolean}
479
501
  */
480
- export function isGeneratorCompatible(manifest, componentType, projection) {
481
- if (!manifest || manifest.planned || manifest.surface !== componentType) {
502
+ export function isGeneratorCompatible(manifest, runtimeKind, projection) {
503
+ if (!manifest || manifest.planned || !manifest.runtimeKinds.includes(runtimeKind)) {
482
504
  return false;
483
505
  }
484
- return manifest.projectionPlatforms.includes(projectionCompatibilityKey(projection));
506
+ return manifest.projectionTypes.includes(projectionCompatibilityKey(projection));
485
507
  }
@@ -66,14 +66,14 @@ function buildAppBundlePlan(graph, options = {}) {
66
66
  db: dbProjection?.id || null
67
67
  },
68
68
  topology: {
69
- components: topology.components.map((component) => ({
70
- id: component.id,
71
- type: component.type,
72
- projection: component.projection.id,
73
- generator: component.generator,
74
- port: component.port ?? null,
75
- api: component.api || null,
76
- database: component.database || null
69
+ runtimes: topology.runtimes.map((runtime) => ({
70
+ id: runtime.id,
71
+ kind: runtime.kind,
72
+ projection: runtime.projection.id,
73
+ generator: runtime.generator,
74
+ port: runtime.port ?? null,
75
+ uses_api: runtime.api || null,
76
+ uses_database: runtime.database || null
77
77
  }))
78
78
  },
79
79
  runtimeReference,
@@ -108,8 +108,8 @@ function renderAppBundleEnvExample(plan) {
108
108
  const demoUserId = runtimeDemoUserId(plan.runtimeReference);
109
109
  const databaseName = plan.runtimeReference.environment.databaseName || "topogram_app";
110
110
  const topology = {
111
- primaryApi: { port: plan.topology.components.find((component) => component.type === "api")?.port },
112
- primaryWeb: { port: plan.topology.components.find((component) => component.type === "web")?.port }
111
+ primaryApi: { port: plan.topology.runtimes.find((runtime) => runtime.kind === "api_service")?.port },
112
+ primaryWeb: { port: plan.topology.runtimes.find((runtime) => runtime.kind === "web_surface")?.port }
113
113
  };
114
114
  const ports = runtimePorts(plan.runtimeReference, topology);
115
115
  const urls = runtimeUrls(plan.runtimeReference, topology);
@@ -126,7 +126,7 @@ ${plan.runtimeReference.environment.envExample || ""}
126
126
  # Smoke-test defaults
127
127
  ${plan.projections.api ? `TOPOGRAM_API_BASE_URL=${urls.api}\n` : ""}${plan.projections.ui ? `TOPOGRAM_WEB_BASE_URL=${urls.web}\n` : ""}`;
128
128
  }
129
- if (plan.projections.dbPlatform === "db_sqlite") {
129
+ if (plan.projections.dbPlatform === "db_contract") {
130
130
  return `# App bundle defaults
131
131
  TOPOGRAM_ENVIRONMENT_PROFILE=${plan.profiles.environment}
132
132
  TOPOGRAM_DEPLOY_PROFILE=${plan.profiles.deployment}
@@ -173,8 +173,8 @@ TOPOGRAM_WEB_BASE_URL=${urls.web}
173
173
 
174
174
  function renderAppBundleReadme(plan) {
175
175
  const urls = runtimeUrls(plan.runtimeReference, {
176
- primaryApi: { port: plan.topology.components.find((component) => component.type === "api")?.port },
177
- primaryWeb: { port: plan.topology.components.find((component) => component.type === "web")?.port }
176
+ primaryApi: { port: plan.topology.runtimes.find((runtime) => runtime.kind === "api_service")?.port },
177
+ primaryWeb: { port: plan.topology.runtimes.find((runtime) => runtime.kind === "web_surface")?.port }
178
178
  });
179
179
  return `# ${plan.name}
180
180
 
@@ -311,8 +311,8 @@ bash "$SCRIPT_DIR/runtime-check.sh"
311
311
 
312
312
  function renderAppBundleWaitForStackScript(plan) {
313
313
  const topology = {
314
- primaryApi: { port: plan.topology.components.find((component) => component.type === "api")?.port },
315
- primaryWeb: { port: plan.topology.components.find((component) => component.type === "web")?.port }
314
+ primaryApi: { port: plan.topology.runtimes.find((runtime) => runtime.kind === "api_service")?.port },
315
+ primaryWeb: { port: plan.topology.runtimes.find((runtime) => runtime.kind === "web_surface")?.port }
316
316
  };
317
317
  const ports = runtimePorts(plan.runtimeReference, topology);
318
318
  const urls = runtimeUrls(plan.runtimeReference, topology);
@@ -57,11 +57,11 @@ function buildCompileCheckPlan(graph, options = {}) {
57
57
  db: dbProjection?.id || null
58
58
  },
59
59
  topology: {
60
- components: topology.components.map((component) => ({
61
- id: component.id,
62
- type: component.type,
63
- projection: component.projection.id,
64
- generator: component.generator
60
+ runtimes: topology.runtimes.map((runtime) => ({
61
+ id: runtime.id,
62
+ kind: runtime.kind,
63
+ projection: runtime.projection.id,
64
+ generator: runtime.generator
65
65
  }))
66
66
  },
67
67
  checks: [...apiChecks, ...webChecks]
@@ -73,7 +73,7 @@ function renderCompileCheckEnvExample(graph, options = {}) {
73
73
  const topology = resolveRuntimeTopology(graph, options);
74
74
  const { dbProjection } = getDefaultEnvironmentProjections(graph, options);
75
75
  const urls = runtimeUrls(runtimeReference, topology);
76
- if (dbProjection?.platform === "db_sqlite") {
76
+ if (dbProjection?.platform === "db_contract") {
77
77
  return `DATABASE_URL=./var/${runtimeReference.environment.databaseName || "topogram_app"}.sqlite
78
78
  PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}
79
79
  PUBLIC_TOPOGRAM_DEMO_USER_ID=${runtimeDemoUserId(runtimeReference)}
@@ -122,7 +122,7 @@ function renderCompileCheckScript(plan) {
122
122
  ""
123
123
  ];
124
124
  if (plan.checks.length === 0) {
125
- lines.push('echo "No API or web components are configured; compile check is a no-op."');
125
+ lines.push('echo "No API or web runtimes are configured; compile check is a no-op."');
126
126
  }
127
127
  for (const check of plan.checks) {
128
128
  const label = check.id.includes("web")
@@ -33,7 +33,7 @@ function buildDeploymentPlan(graph, options = {}) {
33
33
  const profile = options.profileId || "fly_io";
34
34
  const supportedProfiles = ["fly_io", "railway"];
35
35
  const webProfile = manifestGeneratorProfile(topology.primaryWeb?.generator?.id, null) || projectionHintProfile(uiProjection, "sveltekit");
36
- const databaseTarget = dbProjection.platform === "db_sqlite"
36
+ const databaseTarget = dbProjection.platform === "db_contract"
37
37
  ? "sqlite_file"
38
38
  : profile === "fly_io"
39
39
  ? "managed_postgres"
@@ -54,14 +54,14 @@ function buildDeploymentPlan(graph, options = {}) {
54
54
  db: dbProjection.id
55
55
  },
56
56
  topology: {
57
- components: topology.components.map((component) => ({
58
- id: component.id,
59
- type: component.type,
60
- projection: component.projection.id,
61
- generator: component.generator,
62
- port: component.port ?? null,
63
- api: component.api || null,
64
- database: component.database || null
57
+ runtimes: topology.runtimes.map((runtime) => ({
58
+ id: runtime.id,
59
+ kind: runtime.kind,
60
+ projection: runtime.projection.id,
61
+ generator: runtime.generator,
62
+ port: runtime.port ?? null,
63
+ uses_api: runtime.api || null,
64
+ uses_database: runtime.database || null
65
65
  }))
66
66
  },
67
67
  directories: {
@@ -43,10 +43,10 @@ function buildEnvironmentPlan(graph, options = {}) {
43
43
  const topology = resolveRuntimeTopology(graph, options);
44
44
  const { apiProjection, uiProjection, dbProjection } = getDefaultEnvironmentProjections(graph, options);
45
45
  const dbLifecycle = dbProjection ? generateDbLifecyclePlan(graph, { ...options, projectionId: dbProjection.id }) : null;
46
- const profile = options.profileId || (dbProjection?.platform === "db_sqlite" || !dbProjection ? "local_process" : "local_docker");
46
+ const profile = options.profileId || (dbProjection?.platform === "db_contract" || !dbProjection ? "local_process" : "local_docker");
47
47
  const usesDocker = profile === "local_docker";
48
48
  const webProfile = manifestGeneratorProfile(topology.primaryWeb?.generator?.id, null) || projectionHintProfile(uiProjection, "sveltekit");
49
- const isSqlite = dbProjection?.platform === "db_sqlite";
49
+ const isSqlite = dbProjection?.platform === "db_contract";
50
50
  const ports = runtimePorts(runtimeReference, topology);
51
51
 
52
52
  return {
@@ -58,14 +58,14 @@ function buildEnvironmentPlan(graph, options = {}) {
58
58
  profile
59
59
  },
60
60
  topology: {
61
- components: topology.components.map((component) => ({
62
- id: component.id,
63
- type: component.type,
64
- projection: component.projection.id,
65
- generator: component.generator,
66
- port: component.port ?? null,
67
- api: component.api || null,
68
- database: component.database || null
61
+ runtimes: topology.runtimes.map((runtime) => ({
62
+ id: runtime.id,
63
+ kind: runtime.kind,
64
+ projection: runtime.projection.id,
65
+ generator: runtime.generator,
66
+ port: runtime.port ?? null,
67
+ uses_api: runtime.api || null,
68
+ uses_database: runtime.database || null
69
69
  }))
70
70
  },
71
71
  projections: {
@@ -110,7 +110,7 @@ function buildEnvironmentPlan(graph, options = {}) {
110
110
  db: topology.primaryDb ? topology.dbDir(topology.primaryDb) : null,
111
111
  scripts: "scripts"
112
112
  },
113
- components: {
113
+ runtimes: {
114
114
  apis: topology.apiComponents.map((component) => ({
115
115
  id: component.id,
116
116
  projection: component.projection.id,
@@ -165,11 +165,11 @@ function renderEnvironmentEnvExample(plan) {
165
165
  primaryApi: { port: plan.ports.server },
166
166
  primaryWeb: { port: plan.ports.web }
167
167
  });
168
- const extraDatabaseLines = plan.components.databases
169
- .filter((component) => component.id !== plan.components.databases[0]?.id)
168
+ const extraDatabaseLines = plan.runtimes.databases
169
+ .filter((component) => component.id !== plan.runtimes.databases[0]?.id)
170
170
  .map((component) => {
171
171
  const fallbackName = `${databaseName}_${component.id}`;
172
- if (component.platform === "db_sqlite") {
172
+ if (component.platform === "db_contract") {
173
173
  return `${component.env.databaseUrl}=file:./var/${component.id}.sqlite`;
174
174
  }
175
175
  return [
@@ -185,7 +185,7 @@ function renderEnvironmentEnvExample(plan) {
185
185
  TOPOGRAM_ENVIRONMENT_PROFILE=${plan.environment.profile}
186
186
 
187
187
  # Local stack ports
188
- ${plan.components.apis.length ? `SERVER_PORT=${plan.ports.server}\n` : ""}${plan.components.webs.length ? `WEB_PORT=${plan.ports.web}\n` : ""}${plan.components.webs.length && plan.components.apis.length ? `PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}\n` : ""}${plan.components.webs.length ? `TOPOGRAM_CORS_ORIGINS=${urls.web},http://127.0.0.1:${plan.ports.web}\n` : ""}PUBLIC_TOPOGRAM_DEMO_USER_ID=${demoUserId}
188
+ ${plan.runtimes.apis.length ? `SERVER_PORT=${plan.ports.server}\n` : ""}${plan.runtimes.webs.length ? `WEB_PORT=${plan.ports.web}\n` : ""}${plan.runtimes.webs.length && plan.runtimes.apis.length ? `PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}\n` : ""}${plan.runtimes.webs.length ? `TOPOGRAM_CORS_ORIGINS=${urls.web},http://127.0.0.1:${plan.ports.web}\n` : ""}PUBLIC_TOPOGRAM_DEMO_USER_ID=${demoUserId}
189
189
  TOPOGRAM_DEMO_USER_ID=${demoUserId}
190
190
  ${plan.runtimeReference.environment.envExample || ""}
191
191
  TOPOGRAM_SEED_DEMO=true
@@ -193,7 +193,7 @@ TOPOGRAM_SEED_DEMO=true
193
193
  if (!plan.projections.db.platform) {
194
194
  return commonLines;
195
195
  }
196
- if (plan.projections.db.platform === "db_sqlite") {
196
+ if (plan.projections.db.platform === "db_contract") {
197
197
  return `# Environment profile
198
198
  TOPOGRAM_ENVIRONMENT_PROFILE=${plan.environment.profile}
199
199
 
@@ -267,12 +267,12 @@ function apiDatabaseExportLines(component) {
267
267
  }
268
268
 
269
269
  function renderEnvironmentReadme(plan) {
270
- const hasDb = plan.components.databases.length > 0;
271
- const hasApi = plan.components.apis.length > 0;
272
- const hasWeb = plan.components.webs.length > 0;
270
+ const hasDb = plan.runtimes.databases.length > 0;
271
+ const hasApi = plan.runtimes.apis.length > 0;
272
+ const hasWeb = plan.runtimes.webs.length > 0;
273
273
  const localProcessNotes = !hasDb
274
274
  ? "- This bundle has no generated database surface."
275
- : plan.projections.db.platform === "db_sqlite"
275
+ : plan.projections.db.platform === "db_contract"
276
276
  ? "- SQLite is file-backed for this bundle; no separate DB server is required."
277
277
  : `- Make sure the Postgres server is already running before \`${plan.commands.bootstrapDb}\`.\n- \`DATABASE_URL\` and \`DATABASE_ADMIN_URL\` should point at your local or managed Postgres instance.`;
278
278
  const dockerSection = plan.orchestration.usesDocker
@@ -291,13 +291,13 @@ ${localProcessNotes}
291
291
 
292
292
  This bundle packages the generated runtime into one local environment:
293
293
 
294
- ${hasApi ? "- `services/<api-id>/`: generated API service scaffolds\n" : ""}${hasWeb ? `- \`web/<web-id>/\`: generated ${plan.runtimeProfiles.web === "react" ? "Vite + React Router" : plan.runtimeProfiles.web === "vanilla" ? "vanilla HTML/CSS/JS" : "SvelteKit"} web scaffolds\n` : ""}${hasDb ? "- `db/<db-id>/`: generated DB lifecycle bundles\n" : ""}${plan.files.dockerCompose ? `- \`${plan.files.dockerCompose}\`: local Postgres container` : hasDb ? (plan.projections.db.platform === "db_sqlite" ? "- local SQLite file orchestration (no Docker files generated)" : "- local-process Postgres orchestration (no Docker files generated)") : "- no DB orchestration is generated"}
294
+ ${hasApi ? "- `services/<api-id>/`: generated API service scaffolds\n" : ""}${hasWeb ? `- \`web/<web-id>/\`: generated ${plan.runtimeProfiles.web === "react" ? "Vite + React Router" : plan.runtimeProfiles.web === "vanilla" ? "vanilla HTML/CSS/JS" : "SvelteKit"} web scaffolds\n` : ""}${hasDb ? "- `db/<db-id>/`: generated DB lifecycle bundles\n" : ""}${plan.files.dockerCompose ? `- \`${plan.files.dockerCompose}\`: local Postgres container` : hasDb ? (plan.projections.db.platform === "db_contract" ? "- local SQLite file orchestration (no Docker files generated)" : "- local-process Postgres orchestration (no Docker files generated)") : "- no DB orchestration is generated"}
295
295
 
296
296
  ## Quick Start
297
297
 
298
298
  1. Copy \`.env.example\` to \`.env\` if you want to customize defaults
299
299
  2. Start the database:
300
- - ${!hasDb ? "not applicable" : plan.projections.db.platform === "db_sqlite" ? "no separate DB service is required" : plan.orchestration.usesDocker ? `\`${plan.commands.dockerDb}\`` : "use your local Postgres service"}
300
+ - ${!hasDb ? "not applicable" : plan.projections.db.platform === "db_contract" ? "no separate DB service is required" : plan.orchestration.usesDocker ? `\`${plan.commands.dockerDb}\`` : "use your local Postgres service"}
301
301
  3. Bootstrap or migrate the database:
302
302
  - \`${plan.commands.bootstrapDb}\`
303
303
  4. Start the stack:
@@ -313,7 +313,7 @@ ${dockerSection}
313
313
 
314
314
  ## Notes
315
315
 
316
- - ${hasApi && hasDb ? `The generated server expects ${plan.projections.db.platform === "db_sqlite" ? "SQLite plus Prisma." : "Postgres plus Prisma."}` : hasApi ? "The generated server is stateless." : "No server surface is generated."}
316
+ - ${hasApi && hasDb ? `The generated server expects ${plan.projections.db.platform === "db_contract" ? "SQLite plus Prisma." : "Postgres plus Prisma."}` : hasApi ? "The generated server is stateless." : "No server surface is generated."}
317
317
  - ${hasWeb && hasApi ? "The generated web app talks to `PUBLIC_TOPOGRAM_API_BASE_URL`." : hasWeb ? "The generated web app is standalone." : "No web surface is generated."}
318
318
  - If \`.env\` is missing, generated scripts fall back to \`.env.example\`.
319
319
  - The DB lifecycle scripts remain the source of truth for greenfield bootstrap and brownfield migration.
@@ -325,9 +325,9 @@ function renderEnvironmentLoadEnvScript() {
325
325
  }
326
326
 
327
327
  function renderEnvironmentBootstrapDbScript(plan) {
328
- const dbBootstrapLines = plan.components.databases.map((component) => {
328
+ const dbBootstrapLines = plan.runtimes.databases.map((component) => {
329
329
  const env = component.env;
330
- const runtimeApi = plan.components.apis.find((apiComponent) => apiComponent.database === component.id);
330
+ const runtimeApi = plan.runtimes.apis.find((apiComponent) => apiComponent.database === component.id);
331
331
  const assignments = [
332
332
  `DATABASE_URL="\${${env.databaseUrl}:-}"`,
333
333
  `DATABASE_ADMIN_URL="\${${env.databaseAdminUrl}:-}"`,
@@ -335,10 +335,10 @@ function renderEnvironmentBootstrapDbScript(plan) {
335
335
  ].filter(Boolean).join(" ");
336
336
  return `(cd "$ROOT_DIR/${component.dir}" && TOPOGRAM_ENV_FILE=/dev/null ${assignments} bash ./scripts/db-bootstrap-or-migrate.sh)`;
337
337
  });
338
- const primaryApi = plan.components.apis[0];
339
- if (plan.components.databases.length === 0) {
338
+ const primaryApi = plan.runtimes.apis[0];
339
+ if (plan.runtimes.databases.length === 0) {
340
340
  return renderEnvAwareShellScript([
341
- 'echo "No database components are configured; skipping DB bootstrap."'
341
+ 'echo "No database runtimes are configured; skipping DB bootstrap."'
342
342
  ]);
343
343
  }
344
344
  return renderEnvAwareShellScript([
@@ -367,9 +367,9 @@ function componentScriptOptions() {
367
367
  };
368
368
  }
369
369
 
370
- function renderEnvironmentServerDevScript(plan, component = plan.components.apis[0], options = {}) {
370
+ function renderEnvironmentServerDevScript(plan, component = plan.runtimes.apis[0], options = {}) {
371
371
  if (!component) {
372
- return renderEnvAwareShellScript(['echo "No API components are configured."']);
372
+ return renderEnvAwareShellScript(['echo "No API runtimes are configured."']);
373
373
  }
374
374
  const guardPortsScript = options.componentScript ? '"$ROOT_DIR/scripts/guard-ports.mjs"' : '"$SCRIPT_DIR/guard-ports.mjs"';
375
375
  return renderEnvAwareShellScript([
@@ -386,11 +386,11 @@ function renderEnvironmentServerDevScript(plan, component = plan.components.apis
386
386
  ], options.componentScript ? componentScriptOptions() : {});
387
387
  }
388
388
 
389
- function renderEnvironmentWebDevScript(plan, component = plan.components.webs[0], options = {}) {
389
+ function renderEnvironmentWebDevScript(plan, component = plan.runtimes.webs[0], options = {}) {
390
390
  if (!component) {
391
- return renderEnvAwareShellScript(['echo "No web components are configured."']);
391
+ return renderEnvAwareShellScript(['echo "No web runtimes are configured."']);
392
392
  }
393
- const apiComponent = plan.components.apis.find((entry) => entry.id === component.api) || plan.components.apis[0];
393
+ const apiComponent = plan.runtimes.apis.find((entry) => entry.id === component.api) || plan.runtimes.apis[0];
394
394
  const guardPortsScript = options.componentScript ? '"$ROOT_DIR/scripts/guard-ports.mjs"' : '"$SCRIPT_DIR/guard-ports.mjs"';
395
395
  return renderEnvAwareShellScript([
396
396
  `node ${guardPortsScript} web`,
@@ -406,8 +406,8 @@ function renderEnvironmentWebDevScript(plan, component = plan.components.webs[0]
406
406
 
407
407
  function renderEnvironmentStackDevScript(plan) {
408
408
  const startLines = [
409
- ...plan.components.apis.map((component) => `bash "$SCRIPT_DIR/services/${component.id}-dev.sh" &\nPIDS+=($!)`),
410
- ...plan.components.webs.map((component) => `bash "$SCRIPT_DIR/web/${component.id}-dev.sh" &\nPIDS+=($!)`)
409
+ ...plan.runtimes.apis.map((component) => `bash "$SCRIPT_DIR/services/${component.id}-dev.sh" &\nPIDS+=($!)`),
410
+ ...plan.runtimes.webs.map((component) => `bash "$SCRIPT_DIR/web/${component.id}-dev.sh" &\nPIDS+=($!)`)
411
411
  ];
412
412
  return `#!/usr/bin/env bash
413
413
  set -euo pipefail
@@ -447,8 +447,8 @@ ${startLines.length ? "wait" : ""}
447
447
 
448
448
  function renderEnvironmentGuardPortsScript(plan) {
449
449
  const ports = [
450
- ...plan.components.apis.map((component) => ({ id: component.id, type: "api", env: `${component.id.toUpperCase()}_PORT`, fallbackEnv: "SERVER_PORT", port: component.port })),
451
- ...plan.components.webs.map((component) => ({ id: component.id, type: "web", env: `${component.id.toUpperCase()}_PORT`, fallbackEnv: "WEB_PORT", port: component.port }))
450
+ ...plan.runtimes.apis.map((component) => ({ id: component.id, type: "api", env: `${component.id.toUpperCase()}_PORT`, fallbackEnv: "SERVER_PORT", port: component.port })),
451
+ ...plan.runtimes.webs.map((component) => ({ id: component.id, type: "web", env: `${component.id.toUpperCase()}_PORT`, fallbackEnv: "WEB_PORT", port: component.port }))
452
452
  ];
453
453
  return `#!/usr/bin/env node
454
454
  import net from "node:net";
@@ -596,10 +596,10 @@ export function generateEnvironmentBundle(graph, options = {}) {
596
596
  "scripts/guard-ports.mjs": renderEnvironmentGuardPortsScript(plan)
597
597
  };
598
598
 
599
- for (const component of plan.components.apis) {
599
+ for (const component of plan.runtimes.apis) {
600
600
  files[`scripts/services/${component.id}-dev.sh`] = renderEnvironmentServerDevScript(plan, component, { componentScript: true });
601
601
  }
602
- for (const component of plan.components.webs) {
602
+ for (const component of plan.runtimes.webs) {
603
603
  files[`scripts/web/${component.id}-dev.sh`] = renderEnvironmentWebDevScript(plan, component, { componentScript: true });
604
604
  }
605
605
 
@@ -38,11 +38,11 @@ function buildRuntimeCheckPlan(graph, options = {}) {
38
38
  db: dbProjection.id
39
39
  },
40
40
  topology: {
41
- components: topology.components.map((component) => ({
42
- id: component.id,
43
- type: component.type,
44
- projection: component.projection.id,
45
- generator: component.generator
41
+ runtimes: topology.runtimes.map((runtime) => ({
42
+ id: runtime.id,
43
+ kind: runtime.kind,
44
+ projection: runtime.projection.id,
45
+ generator: runtime.generator
46
46
  }))
47
47
  },
48
48
  ...(verification ? { verification } : {}),