@topogram/cli 0.3.51 → 0.3.52

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 +61 -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 +92 -42
  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 +473 -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
@@ -18,14 +18,17 @@ import { validateProjectGeneratorPolicy } from "./generator-policy.js";
18
18
  */
19
19
 
20
20
  /**
21
- * @typedef {Object} RuntimeTopologyComponent
21
+ * @typedef {Object} RuntimeTopologyRuntime
22
22
  * @property {string} id
23
- * @property {"api"|"web"|"database"|"native"} type
23
+ * @property {"api_service"|"web_surface"|"ios_surface"|"android_surface"|"database"} kind
24
24
  * @property {string} projection
25
25
  * @property {GeneratorBinding} generator
26
26
  * @property {number|null} [port]
27
- * @property {string} [api]
28
- * @property {string} [database]
27
+ * @property {string} [uses_api]
28
+ * @property {string} [uses_database]
29
+ * @property {string} [type] Migration diagnostic only.
30
+ * @property {string} [api] Migration diagnostic only.
31
+ * @property {string} [database] Migration diagnostic only.
29
32
  * @property {Record<string, string>} [env]
30
33
  */
31
34
 
@@ -33,7 +36,7 @@ import { validateProjectGeneratorPolicy } from "./generator-policy.js";
33
36
  * @typedef {Object} ProjectConfig
34
37
  * @property {string} version
35
38
  * @property {Record<string, { path: string, ownership: "generated"|"maintained" }>} outputs
36
- * @property {{ components: RuntimeTopologyComponent[] }} topology
39
+ * @property {{ runtimes: RuntimeTopologyRuntime[], components?: any[] }} topology
37
40
  * @property {{ id?: string, module?: string, export?: string, implementation_module?: string, implementation_export?: string }} [implementation]
38
41
  */
39
42
 
@@ -149,46 +152,46 @@ export function defaultProjectConfigForGraph(graph, implementation = null) {
149
152
  const runtimeReference = implementation?.runtime?.reference || {};
150
153
  /** @type {Array<Record<string, any>>} */
151
154
  const projections = graph.byKind.projection || [];
152
- const apiProjection = projections.find((projection) => (projection.http || []).length > 0);
155
+ const apiProjection = projections.find((projection) => (projection.http || []).length > 0 || projection.type === "api_contract");
153
156
  const webProjection =
154
- projections.find((projection) => projection.id === "proj_ui_web") ||
155
- projections.find((projection) => projection.platform === "ui_web");
157
+ projections.find((projection) => projection.id === "proj_web") ||
158
+ projections.find((projection) => projection.type === "web_surface");
156
159
  const dbProjection =
157
160
  projections.find((projection) => projection.id === runtimeReference.localDbProjectionId) ||
158
- projections.find((projection) => projection.platform === "db_postgres") ||
159
- projections.find((projection) => projection.platform === "db_sqlite");
161
+ projections.find((projection) => projection.type === "db_contract");
160
162
  const ports = runtimeReference.ports || {};
161
- const dbGenerator = dbProjection?.platform === "db_sqlite" ? "topogram/sqlite" : "topogram/postgres";
162
- const dbComponentId = dbProjection?.platform === "db_sqlite" ? "app_sqlite" : "app_postgres";
163
- /** @type {RuntimeTopologyComponent[]} */
164
- const components = [
163
+ const dbProfile = (dbProjection?.generatorDefaults || []).find((/** @type {Record<string, any>} */ entry) => entry.key === "profile")?.value;
164
+ const dbGenerator = dbProfile === "sqlite_sql" ? "topogram/sqlite" : "topogram/postgres";
165
+ const dbRuntimeId = dbProfile === "sqlite_sql" ? "app_sqlite" : "app_postgres";
166
+ /** @type {RuntimeTopologyRuntime[]} */
167
+ const runtimes = [
165
168
  ...(apiProjection
166
169
  ? [{
167
170
  id: "app_api",
168
- type: /** @type {"api"} */ ("api"),
171
+ kind: /** @type {"api_service"} */ ("api_service"),
169
172
  projection: apiProjection.id,
170
173
  generator: { id: "topogram/hono", version: "1" },
171
174
  port: ports.server || 3000,
172
- ...(dbProjection ? { database: dbComponentId } : {})
175
+ ...(dbProjection ? { uses_database: dbRuntimeId } : {})
173
176
  }]
174
177
  : []),
175
178
  ...(webProjection
176
179
  ? [{
177
180
  id: "app_sveltekit",
178
- type: /** @type {"web"} */ ("web"),
181
+ kind: /** @type {"web_surface"} */ ("web_surface"),
179
182
  projection: webProjection.id,
180
183
  generator: { id: "topogram/sveltekit", version: "1" },
181
184
  port: ports.web || 5173,
182
- ...(apiProjection ? { api: "app_api" } : {})
185
+ ...(apiProjection ? { uses_api: "app_api" } : {})
183
186
  }]
184
187
  : []),
185
188
  ...(dbProjection
186
189
  ? [{
187
- id: dbComponentId,
188
- type: /** @type {"database"} */ ("database"),
190
+ id: dbRuntimeId,
191
+ kind: /** @type {"database"} */ ("database"),
189
192
  projection: dbProjection.id,
190
193
  generator: { id: dbGenerator, version: "1" },
191
- port: dbProjection.platform === "db_sqlite" ? null : 5432
194
+ port: dbProfile === "sqlite_sql" ? null : 5432
192
195
  }]
193
196
  : [])
194
197
  ];
@@ -207,7 +210,41 @@ export function defaultProjectConfigForGraph(graph, implementation = null) {
207
210
  }
208
211
  },
209
212
  topology: {
210
- components
213
+ runtimes
214
+ }
215
+ };
216
+ }
217
+
218
+ /**
219
+ * @param {string} kind
220
+ * @returns {string}
221
+ */
222
+ function legacyRuntimeType(kind) {
223
+ if (kind === "api_service") return "api";
224
+ if (kind === "web_surface") return "web";
225
+ if (kind === "ios_surface" || kind === "android_surface") return "native";
226
+ return kind;
227
+ }
228
+
229
+ /**
230
+ * @param {any} config
231
+ * @returns {any}
232
+ */
233
+ function normalizeProjectConfigRuntimeAliases(config) {
234
+ if (!config?.topology || !Array.isArray(config.topology.runtimes)) {
235
+ return config;
236
+ }
237
+ return {
238
+ ...config,
239
+ topology: {
240
+ ...config.topology,
241
+ __normalizedRuntimeAliases: true,
242
+ components: config.topology.runtimes.map((/** @type {RuntimeTopologyRuntime} */ runtime) => ({
243
+ ...runtime,
244
+ type: legacyRuntimeType(runtime.kind),
245
+ api: runtime.uses_api,
246
+ database: runtime.uses_database
247
+ }))
211
248
  }
212
249
  };
213
250
  }
@@ -223,6 +260,7 @@ export function loadProjectConfig(root) {
223
260
  }
224
261
  return {
225
262
  ...found,
263
+ config: normalizeProjectConfigRuntimeAliases(found.config),
226
264
  compatibility: false
227
265
  };
228
266
  }
@@ -242,7 +280,7 @@ export function projectConfigOrDefault(root, graph = null, implementation = null
242
280
  return null;
243
281
  }
244
282
  return {
245
- config: defaultProjectConfigForGraph(graph, implementation),
283
+ config: normalizeProjectConfigRuntimeAliases(defaultProjectConfigForGraph(graph, implementation)),
246
284
  configPath: null,
247
285
  configDir: path.dirname(path.resolve(root)),
248
286
  compatibility: true
@@ -298,7 +336,7 @@ function validateOutputConfig(errors, config) {
298
336
  * @returns {string}
299
337
  */
300
338
  function componentLabel(component) {
301
- return component?.id ? `Component '${component.id}'` : "Topology component";
339
+ return component?.id ? `Runtime '${component.id}'` : "Topology runtime";
302
340
  }
303
341
 
304
342
  /**
@@ -319,8 +357,11 @@ function validateComponentShape(errors, component, seenIds) {
319
357
  } else {
320
358
  seenIds.add(component.id);
321
359
  }
322
- if (!["api", "web", "database", "native"].includes(component.type)) {
323
- pushError(errors, `${componentLabel(component)} type must be api, web, database, or native`);
360
+ if (component.type != null) {
361
+ pushError(errors, `${componentLabel(component)} type was renamed to kind`);
362
+ }
363
+ if (!["api_service", "web_surface", "ios_surface", "android_surface", "database"].includes(component.kind)) {
364
+ pushError(errors, `${componentLabel(component)} kind must be api_service, web_surface, ios_surface, android_surface, or database`);
324
365
  }
325
366
  if (typeof component.projection !== "string" || component.projection.length === 0) {
326
367
  pushError(errors, `${componentLabel(component)} projection must be a non-empty string`);
@@ -346,7 +387,7 @@ function validateComponentShape(errors, component, seenIds) {
346
387
 
347
388
  /**
348
389
  * @param {ValidationError[]} errors
349
- * @param {RuntimeTopologyComponent} component
390
+ * @param {RuntimeTopologyRuntime} component
350
391
  * @param {Map<string, Record<string, any>>} projections
351
392
  * @param {{ configDir?: string|null, rootDir?: string|null }} [options]
352
393
  * @returns {void}
@@ -377,14 +418,14 @@ function validateComponentCompatibility(errors, component, projections, options
377
418
  if (manifest.version !== component.generator.version) {
378
419
  pushError(errors, `${componentLabel(component)} for projection '${projection.id}' generator '${manifest.id}' version '${component.generator.version}' is unsupported; expected '${manifest.version}'`);
379
420
  }
380
- if (!isGeneratorCompatible(manifest, component.type, projection)) {
381
- 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"}'`);
421
+ if (!isGeneratorCompatible(manifest, component.kind, projection)) {
422
+ pushError(errors, `${componentLabel(component)} for projection '${projection.id}' generator '${manifest.id}@${manifest.version}' is incompatible with runtime kind '${component.kind}' and projection type '${projection.type || "api_contract"}'`);
382
423
  }
383
424
  }
384
425
 
385
426
  /**
386
427
  * @param {ValidationError[]} errors
387
- * @param {RuntimeTopologyComponent[]} components
428
+ * @param {RuntimeTopologyRuntime[]} components
388
429
  * @returns {void}
389
430
  */
390
431
  function validateTopologyReferences(errors, components) {
@@ -399,14 +440,20 @@ function validateTopologyReferences(errors, components) {
399
440
  usedPorts.set(component.port, component.id);
400
441
  }
401
442
  }
402
- if (component.type === "api") {
403
- if (component.database && byId.get(component.database)?.type !== "database") {
404
- pushError(errors, `${componentLabel(component)} references missing database component '${component.database}'`);
443
+ if (component.database != null) {
444
+ pushError(errors, `${componentLabel(component)} database was renamed to uses_database`);
445
+ }
446
+ if (component.api != null) {
447
+ pushError(errors, `${componentLabel(component)} api was renamed to uses_api`);
448
+ }
449
+ if (component.kind === "api_service") {
450
+ if (component.uses_database && byId.get(component.uses_database)?.kind !== "database") {
451
+ pushError(errors, `${componentLabel(component)} references missing database runtime '${component.uses_database}'`);
405
452
  }
406
453
  }
407
- if (component.type === "web") {
408
- if (component.api && byId.get(component.api)?.type !== "api") {
409
- pushError(errors, `${componentLabel(component)} references missing api component '${component.api}'`);
454
+ if (["web_surface", "ios_surface", "android_surface"].includes(component.kind)) {
455
+ if (component.uses_api && byId.get(component.uses_api)?.kind !== "api_service") {
456
+ pushError(errors, `${componentLabel(component)} references missing api runtime '${component.uses_api}'`);
410
457
  }
411
458
  }
412
459
  }
@@ -428,23 +475,26 @@ export function validateProjectConfig(config, graph = null, options = {}) {
428
475
  pushError(errors, "topogram.project.json version must be a non-empty string");
429
476
  }
430
477
  validateOutputConfig(errors, config);
431
- if (!config.topology || typeof config.topology !== "object" || !Array.isArray(config.topology.components)) {
432
- pushError(errors, "topogram.project.json topology.components must be an array");
478
+ if (config.topology?.components != null && config.topology.__normalizedRuntimeAliases !== true) {
479
+ pushError(errors, "topogram.project.json topology.components was renamed to topology.runtimes");
480
+ }
481
+ if (!config.topology || typeof config.topology !== "object" || !Array.isArray(config.topology.runtimes)) {
482
+ pushError(errors, "topogram.project.json topology.runtimes must be an array");
433
483
  } else {
434
484
  const seenIds = new Set();
435
- for (const component of config.topology.components) {
485
+ for (const component of config.topology.runtimes) {
436
486
  validateComponentShape(errors, component, seenIds);
437
487
  }
438
- const generatorPolicy = validateProjectGeneratorPolicy(config, options);
488
+ const generatorPolicy = validateProjectGeneratorPolicy(normalizeProjectConfigRuntimeAliases(config), options);
439
489
  for (const error of generatorPolicy.errors) {
440
490
  pushError(errors, error.message, error.loc);
441
491
  }
442
492
  if (graph) {
443
493
  const projections = projectionById(graph);
444
- for (const component of config.topology.components) {
494
+ for (const component of config.topology.runtimes) {
445
495
  validateComponentCompatibility(errors, component, projections, options);
446
496
  }
447
- validateTopologyReferences(errors, config.topology.components);
497
+ validateTopologyReferences(errors, config.topology.runtimes);
448
498
  }
449
499
  }
450
500
  return {
@@ -187,7 +187,7 @@ export function auditUiContractPair(leftContract, rightContract) {
187
187
  const appShellParity = stableStringify(left.appShell) === stableStringify(right.appShell);
188
188
 
189
189
  return {
190
- seam: "ui_web_contract",
190
+ seam: "ui_surface_contract",
191
191
  semanticParity: screenDiffs.length === 0 && navigationParity && appShellParity,
192
192
  summary: {
193
193
  screenCount: left.screens.length,
@@ -1,7 +1,7 @@
1
1
  import { stableStringify } from "../format.js";
2
2
  import { normalizeScreens } from "./web-parity.js";
3
3
 
4
- /** Semantic fingerprint of screens slice embedded in Swift ui-web-contract JSON (matches web normalization). */
4
+ /** Semantic fingerprint of screens slice embedded in Swift ui-surface-contract JSON (matches web normalization). */
5
5
  export function fingerprintIosEmbeddedUiContract(contract) {
6
6
  return stableStringify(normalizeScreens(contract));
7
7
  }
@@ -2,7 +2,7 @@ import { buildBackendParityEvidence } from "./backend-parity.js";
2
2
  import { buildWebParityEvidence } from "./web-parity.js";
3
3
 
4
4
  export function buildIssuesParityEvidence(graph) {
5
- const web = buildWebParityEvidence(graph, "proj_ui_web__react", "proj_ui_web__sveltekit");
5
+ const web = buildWebParityEvidence(graph, "proj_web_surface__react", "proj_web_surface__sveltekit");
6
6
  return {
7
7
  web,
8
8
  runtime: buildBackendParityEvidence(graph, "proj_api")
@@ -28,8 +28,8 @@ export function getDefaultBackendDbProjection(graph, options = {}) {
28
28
  return (
29
29
  explicit ||
30
30
  preferred ||
31
- candidates.find((projection) => projection.platform === "db_postgres") ||
32
- candidates.find((projection) => projection.platform === "db_sqlite") ||
31
+ candidates.find((projection) => projection.platform === "db_contract") ||
32
+ candidates.find((projection) => projection.platform === "db_contract") ||
33
33
  candidates[0] ||
34
34
  null
35
35
  );
@@ -1,5 +1,5 @@
1
1
  import { getProjection, uiProjectionCandidates } from "../../generator/surfaces/shared.js";
2
- import { buildComponentBehaviorRealizations } from "../../component-behavior.js";
2
+ import { buildWidgetBehaviorRealizations } from "../../widget-behavior.js";
3
3
  import { defaultPatternForScreen } from "../../ui/taxonomy.js";
4
4
 
5
5
  function toBooleanFlag(value, fallback = false) {
@@ -48,25 +48,25 @@ function ownershipFieldByCapability(graph) {
48
48
  return output;
49
49
  }
50
50
 
51
- function componentById(graph, componentId) {
52
- return (graph.byKind.component || []).find((component) => component.id === componentId) || null;
51
+ function widgetById(graph, widgetId) {
52
+ return (graph.byKind.widget || graph.byKind.component || []).find((widget) => widget.id === widgetId) || null;
53
53
  }
54
54
 
55
- function componentContractFor(graph, componentId) {
56
- const component = componentById(graph, componentId);
57
- return component?.componentContract || null;
55
+ function widgetContractFor(graph, widgetId) {
56
+ const widget = widgetById(graph, widgetId);
57
+ return widget?.widgetContract || widget?.componentContract || null;
58
58
  }
59
59
 
60
- function summarizeComponentRef(graph, componentId) {
61
- const component = componentById(graph, componentId);
62
- if (!component) {
63
- return { id: componentId, name: componentId, category: null, version: null };
60
+ function summarizeWidgetRef(graph, widgetId) {
61
+ const widget = widgetById(graph, widgetId);
62
+ if (!widget) {
63
+ return { id: widgetId, name: widgetId, category: null, version: null };
64
64
  }
65
65
  return {
66
- id: component.id,
67
- name: component.name || component.id,
68
- category: component.category || null,
69
- version: component.version || null
66
+ id: widget.id,
67
+ name: widget.name || widget.id,
68
+ category: widget.category || null,
69
+ version: widget.version || null
70
70
  };
71
71
  }
72
72
 
@@ -118,16 +118,16 @@ function buildDesignIntentContract(projection) {
118
118
  return design;
119
119
  }
120
120
 
121
- export function buildComponentUsageContract(graph, entry, options = {}) {
122
- const componentId = entry.component?.id || null;
123
- const contract = componentId ? componentContractFor(graph, componentId) : null;
121
+ export function buildWidgetUsageContract(graph, entry, options = {}) {
122
+ const widgetId = entry.widget?.id || entry.component?.id || null;
123
+ const contract = widgetId ? widgetContractFor(graph, widgetId) : null;
124
124
  const region = options.region || null;
125
125
  return {
126
- type: "ui_component_usage",
126
+ type: "ui_widget_usage",
127
127
  region: entry.region || null,
128
128
  pattern: region?.pattern || null,
129
129
  placement: region?.placement || null,
130
- component: componentId ? summarizeComponentRef(graph, componentId) : null,
130
+ widget: widgetId ? summarizeWidgetRef(graph, widgetId) : null,
131
131
  dataBindings: (entry.dataBindings || []).map((binding) => ({
132
132
  prop: binding.prop || null,
133
133
  source: binding.source || null
@@ -137,15 +137,15 @@ export function buildComponentUsageContract(graph, entry, options = {}) {
137
137
  action: binding.action || null,
138
138
  target: binding.target || null
139
139
  })),
140
- behaviorRealizations: buildComponentBehaviorRealizations(contract, entry)
140
+ behaviorRealizations: buildWidgetBehaviorRealizations(contract, entry)
141
141
  };
142
142
  }
143
143
 
144
- export function buildComponentContractMap(graph, componentUsages) {
144
+ export function buildWidgetContractMap(graph, widgetUsages) {
145
145
  return Object.fromEntries(
146
- [...new Set(componentUsages.map((entry) => entry.component?.id).filter(Boolean))]
146
+ [...new Set(widgetUsages.map((entry) => entry.widget?.id || entry.component?.id).filter(Boolean))]
147
147
  .sort()
148
- .map((componentId) => [componentId, componentContractFor(graph, componentId)])
148
+ .map((widgetId) => [widgetId, widgetContractFor(graph, widgetId)])
149
149
  .filter(([, contract]) => contract)
150
150
  );
151
151
  }
@@ -202,7 +202,7 @@ function buildUiScreenContract(graph, projection, screen, ownershipFields) {
202
202
  const actionEntries = (projection.uiActions || []).filter((entry) => entry.screenId === screen.id);
203
203
  const lookupEntries = (projection.uiLookups || []).filter((entry) => entry.screenId === screen.id);
204
204
  const regionEntries = (projection.uiScreenRegions || []).filter((entry) => entry.screenId === screen.id);
205
- const componentEntries = (projection.uiComponents || []).filter((entry) => entry.screenId === screen.id);
205
+ const widgetEntries = (projection.uiComponents || []).filter((entry) => entry.screenId === screen.id);
206
206
  const screenActionIds = new Set(
207
207
  [
208
208
  screen.primaryAction?.id,
@@ -292,7 +292,7 @@ function buildUiScreenContract(graph, projection, screen, ownershipFields) {
292
292
  state: entry.state || null,
293
293
  variant: entry.variant || null
294
294
  })),
295
- components: componentEntries.map((entry) => buildComponentUsageContract(graph, entry, {
295
+ widgets: widgetEntries.map((entry) => buildWidgetUsageContract(graph, entry, {
296
296
  region: regionContractFor(regionEntries, entry.region)
297
297
  })),
298
298
  patterns: [...patterns]
@@ -305,18 +305,18 @@ export function buildUiSharedRealization(graph, options = {}) {
305
305
 
306
306
  if (options.projectionId) {
307
307
  const projection = projections[0];
308
- const componentUsages = projection.uiComponents || [];
308
+ const widgetUsages = projection.uiComponents || [];
309
309
  const screens = (projection.uiScreens || []).map((screen) => buildUiScreenContract(graph, projection, screen, ownershipFields));
310
310
  return {
311
311
  projection: {
312
312
  id: projection.id,
313
313
  name: projection.name || projection.id,
314
- platform: projection.platform
314
+ type: projection.type || projection.platform
315
315
  },
316
316
  realizes: projection.realizes,
317
317
  outputs: projection.outputs,
318
- components: buildComponentContractMap(graph, componentUsages),
319
- design: buildDesignIntentContract(projection),
318
+ widgets: buildWidgetContractMap(graph, widgetUsages),
319
+ designTokens: buildDesignIntentContract(projection),
320
320
  appShell: buildAppShellContract(projection),
321
321
  navigation: buildNavigationContract(projection, screens),
322
322
  screens
@@ -325,18 +325,18 @@ export function buildUiSharedRealization(graph, options = {}) {
325
325
 
326
326
  const output = {};
327
327
  for (const projection of projections) {
328
- const componentUsages = projection.uiComponents || [];
328
+ const widgetUsages = projection.uiComponents || [];
329
329
  const screens = (projection.uiScreens || []).map((screen) => buildUiScreenContract(graph, projection, screen, ownershipFields));
330
330
  output[projection.id] = {
331
331
  projection: {
332
332
  id: projection.id,
333
333
  name: projection.name || projection.id,
334
- platform: projection.platform
334
+ type: projection.type || projection.platform
335
335
  },
336
336
  realizes: projection.realizes,
337
337
  outputs: projection.outputs,
338
- components: buildComponentContractMap(graph, componentUsages),
339
- design: buildDesignIntentContract(projection),
338
+ widgets: buildWidgetContractMap(graph, widgetUsages),
339
+ designTokens: buildDesignIntentContract(projection),
340
340
  appShell: buildAppShellContract(projection),
341
341
  navigation: buildNavigationContract(projection, screens),
342
342
  screens
@@ -26,19 +26,20 @@ export function buildWebRealization(graph, options = {}) {
26
26
  }
27
27
 
28
28
  const projection = getProjection(graph, options.projectionId);
29
+ const projectionType = projection.type || projection.platform;
29
30
  const surfaceHints =
30
- projection.platform === "ui_ios" ? projection.uiIos || [] : projection.uiWeb || [];
31
+ projectionType === "ios_surface" ? projection.uiIos || [] : projection.uiWeb || [];
31
32
  const sharedProjection = sharedUiProjectionForWeb(graph, projection);
32
33
  const sharedContract = sharedProjection
33
34
  ? buildUiSharedRealization(graph, { projectionId: sharedProjection.id })
34
35
  : {
35
- projection: null,
36
- realizes: [],
37
- outputs: [],
38
- components: {},
39
- design: null,
40
- screens: []
41
- };
36
+ projection: null,
37
+ realizes: [],
38
+ outputs: [],
39
+ widgets: {},
40
+ designTokens: null,
41
+ screens: []
42
+ };
42
43
  const concreteContract = buildUiSharedRealization(graph, { projectionId: projection.id });
43
44
 
44
45
  const routeMap = new Map((projection.uiRoutes || []).map((entry) => [entry.screenId, entry]));
@@ -59,17 +60,17 @@ export function buildWebRealization(graph, options = {}) {
59
60
  }
60
61
  }
61
62
 
62
- const screenMap = new Map((sharedContract.screens || []).map((screen) => [screen.id, { ...screen, components: [...(screen.components || [])] }]));
63
+ const screenMap = new Map((sharedContract.screens || []).map((screen) => [screen.id, { ...screen, widgets: [...(screen.widgets || [])] }]));
63
64
  for (const screen of concreteContract.screens || []) {
64
65
  if (!screenMap.has(screen.id)) {
65
- screenMap.set(screen.id, { ...screen, components: [...(screen.components || [])] });
66
+ screenMap.set(screen.id, { ...screen, widgets: [...(screen.widgets || [])] });
66
67
  continue;
67
68
  }
68
69
  const existing = screenMap.get(screen.id);
69
70
  screenMap.set(screen.id, {
70
71
  ...existing,
71
72
  ...screen,
72
- components: [...(existing.components || [])],
73
+ widgets: [...(existing.widgets || [])],
73
74
  regions: mergeByKey(existing.regions || [], screen.regions || [], (entry) => entry.region),
74
75
  patterns: [...new Set([...(existing.patterns || []), ...(screen.patterns || [])])]
75
76
  });
@@ -77,24 +78,26 @@ export function buildWebRealization(graph, options = {}) {
77
78
 
78
79
  const appShell = projection.uiAppShell?.length || !sharedProjection ? concreteContract.appShell : sharedContract.appShell;
79
80
  const navigation = projection.uiNavigation?.length || !sharedProjection ? concreteContract.navigation : sharedContract.navigation;
80
- const design = projection.uiDesign?.length || !sharedProjection ? concreteContract.design : sharedContract.design;
81
+ const designTokens = projection.uiDesign?.length || !sharedProjection ? concreteContract.designTokens : sharedContract.designTokens;
81
82
 
82
83
  const contract = {
84
+ type: "ui_surface_contract",
83
85
  projection: {
84
86
  id: projection.id,
85
87
  name: projection.name || projection.id,
86
- platform: projection.platform
88
+ type: projection.type || projection.platform
87
89
  },
88
- sharedProjection: sharedProjection
90
+ uiContract: sharedProjection
89
91
  ? {
90
92
  id: sharedProjection.id,
91
- name: sharedProjection.name || sharedProjection.id
93
+ name: sharedProjection.name || sharedProjection.id,
94
+ type: sharedProjection.type || sharedProjection.platform
92
95
  }
93
96
  : null,
94
97
  generatorDefaults: generatorDefaultsMap(projection),
95
98
  outputs: projection.outputs,
96
- components: sharedProjection ? (sharedContract.components || {}) : (concreteContract.components || {}),
97
- design: design || null,
99
+ widgets: sharedProjection ? (sharedContract.widgets || {}) : (concreteContract.widgets || {}),
100
+ designTokens: designTokens || null,
98
101
  appShell: appShell || null,
99
102
  navigation: {
100
103
  groups: navigation?.groups || [],
@@ -108,8 +111,8 @@ export function buildWebRealization(graph, options = {}) {
108
111
  screens: [...screenMap.values()].map((screen) => ({
109
112
  ...screen,
110
113
  route: routeMap.get(screen.id)?.path || null,
111
- web: Object.fromEntries((uiWebByScreen.get(screen.id) || []).map((entry) => [entry.directive, entry.value])),
112
- actionWeb: Object.fromEntries(
114
+ surfaceHints: Object.fromEntries((uiWebByScreen.get(screen.id) || []).map((entry) => [entry.directive, entry.value])),
115
+ actionSurfaceHints: Object.fromEntries(
113
116
  [...screen.actions.screen, screen.actions.primary, screen.actions.secondary, screen.actions.destructive, screen.actions.terminal]
114
117
  .filter(Boolean)
115
118
  .map((action) => {
@@ -134,7 +137,7 @@ export function buildWebRealization(graph, options = {}) {
134
137
  apiContracts[capabilityId] = buildApiRealization(graph, { capabilityId });
135
138
  }
136
139
 
137
- const isNativeUi = projection.platform === "ui_ios";
140
+ const isNativeUi = projectionType === "ios_surface";
138
141
 
139
142
  return {
140
143
  type: isNativeUi ? "native_ui_realization" : "web_app_realization",
@@ -40,7 +40,7 @@ function collectJourneyGenerationContext(graph) {
40
40
  const rules = graph.byKind.rule || [];
41
41
  const projections = graph.byKind.projection || [];
42
42
  const uiSharedScreens = projections
43
- .filter((projection) => projection.platform === "ui_shared")
43
+ .filter((projection) => projection.platform === "ui_contract")
44
44
  .flatMap((projection) => (projection.uiScreens || []).map((screen) => ({ ...screen, projectionId: projection.id })));
45
45
  const canonicalJourneys = (graph.docs || []).filter((doc) => doc.kind === "journey");
46
46
  const coveredEntityIds = new Set(canonicalJourneys.flatMap((doc) => doc.relatedEntities || []));