@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
@@ -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
 
@@ -103,6 +106,16 @@ function readJson(filePath) {
103
106
  return JSON.parse(fs.readFileSync(filePath, "utf8"));
104
107
  }
105
108
 
109
+ /**
110
+ * @param {string} oldName
111
+ * @param {string} newName
112
+ * @param {string} example
113
+ * @returns {string}
114
+ */
115
+ function renameDiagnostic(oldName, newName, example) {
116
+ return `${oldName} was renamed to ${newName}. Example fix: ${example}`;
117
+ }
118
+
106
119
  /**
107
120
  * @param {string} root
108
121
  * @param {string} fileName
@@ -149,46 +162,46 @@ export function defaultProjectConfigForGraph(graph, implementation = null) {
149
162
  const runtimeReference = implementation?.runtime?.reference || {};
150
163
  /** @type {Array<Record<string, any>>} */
151
164
  const projections = graph.byKind.projection || [];
152
- const apiProjection = projections.find((projection) => (projection.http || []).length > 0);
165
+ const apiProjection = projections.find((projection) => (projection.http || []).length > 0 || projection.type === "api_contract");
153
166
  const webProjection =
154
- projections.find((projection) => projection.id === "proj_ui_web") ||
155
- projections.find((projection) => projection.platform === "ui_web");
167
+ projections.find((projection) => projection.id === "proj_web") ||
168
+ projections.find((projection) => projection.type === "web_surface");
156
169
  const dbProjection =
157
170
  projections.find((projection) => projection.id === runtimeReference.localDbProjectionId) ||
158
- projections.find((projection) => projection.platform === "db_postgres") ||
159
- projections.find((projection) => projection.platform === "db_sqlite");
171
+ projections.find((projection) => projection.type === "db_contract");
160
172
  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 = [
173
+ const dbProfile = (dbProjection?.generatorDefaults || []).find((/** @type {Record<string, any>} */ entry) => entry.key === "profile")?.value;
174
+ const dbGenerator = dbProfile === "sqlite_sql" ? "topogram/sqlite" : "topogram/postgres";
175
+ const dbRuntimeId = dbProfile === "sqlite_sql" ? "app_sqlite" : "app_postgres";
176
+ /** @type {RuntimeTopologyRuntime[]} */
177
+ const runtimes = [
165
178
  ...(apiProjection
166
179
  ? [{
167
180
  id: "app_api",
168
- type: /** @type {"api"} */ ("api"),
181
+ kind: /** @type {"api_service"} */ ("api_service"),
169
182
  projection: apiProjection.id,
170
183
  generator: { id: "topogram/hono", version: "1" },
171
184
  port: ports.server || 3000,
172
- ...(dbProjection ? { database: dbComponentId } : {})
185
+ ...(dbProjection ? { uses_database: dbRuntimeId } : {})
173
186
  }]
174
187
  : []),
175
188
  ...(webProjection
176
189
  ? [{
177
190
  id: "app_sveltekit",
178
- type: /** @type {"web"} */ ("web"),
191
+ kind: /** @type {"web_surface"} */ ("web_surface"),
179
192
  projection: webProjection.id,
180
193
  generator: { id: "topogram/sveltekit", version: "1" },
181
194
  port: ports.web || 5173,
182
- ...(apiProjection ? { api: "app_api" } : {})
195
+ ...(apiProjection ? { uses_api: "app_api" } : {})
183
196
  }]
184
197
  : []),
185
198
  ...(dbProjection
186
199
  ? [{
187
- id: dbComponentId,
188
- type: /** @type {"database"} */ ("database"),
200
+ id: dbRuntimeId,
201
+ kind: /** @type {"database"} */ ("database"),
189
202
  projection: dbProjection.id,
190
203
  generator: { id: dbGenerator, version: "1" },
191
- port: dbProjection.platform === "db_sqlite" ? null : 5432
204
+ port: dbProfile === "sqlite_sql" ? null : 5432
192
205
  }]
193
206
  : [])
194
207
  ];
@@ -207,7 +220,41 @@ export function defaultProjectConfigForGraph(graph, implementation = null) {
207
220
  }
208
221
  },
209
222
  topology: {
210
- components
223
+ runtimes
224
+ }
225
+ };
226
+ }
227
+
228
+ /**
229
+ * @param {string} kind
230
+ * @returns {string}
231
+ */
232
+ function legacyRuntimeType(kind) {
233
+ if (kind === "api_service") return "api";
234
+ if (kind === "web_surface") return "web";
235
+ if (kind === "ios_surface" || kind === "android_surface") return "native";
236
+ return kind;
237
+ }
238
+
239
+ /**
240
+ * @param {any} config
241
+ * @returns {any}
242
+ */
243
+ function normalizeProjectConfigRuntimeAliases(config) {
244
+ if (!config?.topology || !Array.isArray(config.topology.runtimes)) {
245
+ return config;
246
+ }
247
+ return {
248
+ ...config,
249
+ topology: {
250
+ ...config.topology,
251
+ __normalizedRuntimeAliases: true,
252
+ components: config.topology.runtimes.map((/** @type {RuntimeTopologyRuntime} */ runtime) => ({
253
+ ...runtime,
254
+ type: legacyRuntimeType(runtime.kind),
255
+ api: runtime.uses_api,
256
+ database: runtime.uses_database
257
+ }))
211
258
  }
212
259
  };
213
260
  }
@@ -223,6 +270,7 @@ export function loadProjectConfig(root) {
223
270
  }
224
271
  return {
225
272
  ...found,
273
+ config: normalizeProjectConfigRuntimeAliases(found.config),
226
274
  compatibility: false
227
275
  };
228
276
  }
@@ -242,7 +290,7 @@ export function projectConfigOrDefault(root, graph = null, implementation = null
242
290
  return null;
243
291
  }
244
292
  return {
245
- config: defaultProjectConfigForGraph(graph, implementation),
293
+ config: normalizeProjectConfigRuntimeAliases(defaultProjectConfigForGraph(graph, implementation)),
246
294
  configPath: null,
247
295
  configDir: path.dirname(path.resolve(root)),
248
296
  compatibility: true
@@ -298,7 +346,7 @@ function validateOutputConfig(errors, config) {
298
346
  * @returns {string}
299
347
  */
300
348
  function componentLabel(component) {
301
- return component?.id ? `Component '${component.id}'` : "Topology component";
349
+ return component?.id ? `Runtime '${component.id}'` : "Topology runtime";
302
350
  }
303
351
 
304
352
  /**
@@ -309,18 +357,27 @@ function componentLabel(component) {
309
357
  */
310
358
  function validateComponentShape(errors, component, seenIds) {
311
359
  if (!component || typeof component !== "object" || Array.isArray(component)) {
312
- pushError(errors, "Topology component must be an object");
360
+ pushError(errors, "Topology runtime must be an object");
313
361
  return false;
314
362
  }
315
363
  if (typeof component.id !== "string" || !IDENTIFIER_PATTERN.test(component.id)) {
316
364
  pushError(errors, `${componentLabel(component)} id must match ${IDENTIFIER_PATTERN}`);
317
365
  } else if (seenIds.has(component.id)) {
318
- pushError(errors, `Duplicate topology component id '${component.id}'`);
366
+ pushError(errors, `Duplicate topology runtime id '${component.id}'`);
319
367
  } else {
320
368
  seenIds.add(component.id);
321
369
  }
322
- if (!["api", "web", "database", "native"].includes(component.type)) {
323
- pushError(errors, `${componentLabel(component)} type must be api, web, database, or native`);
370
+ if (component.type != null) {
371
+ pushError(errors, `${componentLabel(component)} ${renameDiagnostic("'type'", "'kind'", `"kind": "api_service"`)}`);
372
+ }
373
+ if (component.database != null) {
374
+ pushError(errors, `${componentLabel(component)} ${renameDiagnostic("'database'", "'uses_database'", `"uses_database": "app_db"`)}`);
375
+ }
376
+ if (component.api != null) {
377
+ pushError(errors, `${componentLabel(component)} ${renameDiagnostic("'api'", "'uses_api'", `"uses_api": "app_api"`)}`);
378
+ }
379
+ if (!["api_service", "web_surface", "ios_surface", "android_surface", "database"].includes(component.kind)) {
380
+ pushError(errors, `${componentLabel(component)} kind must be api_service, web_surface, ios_surface, android_surface, or database`);
324
381
  }
325
382
  if (typeof component.projection !== "string" || component.projection.length === 0) {
326
383
  pushError(errors, `${componentLabel(component)} projection must be a non-empty string`);
@@ -346,7 +403,7 @@ function validateComponentShape(errors, component, seenIds) {
346
403
 
347
404
  /**
348
405
  * @param {ValidationError[]} errors
349
- * @param {RuntimeTopologyComponent} component
406
+ * @param {RuntimeTopologyRuntime} component
350
407
  * @param {Map<string, Record<string, any>>} projections
351
408
  * @param {{ configDir?: string|null, rootDir?: string|null }} [options]
352
409
  * @returns {void}
@@ -377,14 +434,14 @@ function validateComponentCompatibility(errors, component, projections, options
377
434
  if (manifest.version !== component.generator.version) {
378
435
  pushError(errors, `${componentLabel(component)} for projection '${projection.id}' generator '${manifest.id}' version '${component.generator.version}' is unsupported; expected '${manifest.version}'`);
379
436
  }
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"}'`);
437
+ if (!isGeneratorCompatible(manifest, component.kind, projection)) {
438
+ 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
439
  }
383
440
  }
384
441
 
385
442
  /**
386
443
  * @param {ValidationError[]} errors
387
- * @param {RuntimeTopologyComponent[]} components
444
+ * @param {RuntimeTopologyRuntime[]} components
388
445
  * @returns {void}
389
446
  */
390
447
  function validateTopologyReferences(errors, components) {
@@ -399,14 +456,14 @@ function validateTopologyReferences(errors, components) {
399
456
  usedPorts.set(component.port, component.id);
400
457
  }
401
458
  }
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}'`);
459
+ if (component.kind === "api_service") {
460
+ if (component.uses_database && byId.get(component.uses_database)?.kind !== "database") {
461
+ pushError(errors, `${componentLabel(component)} references missing database runtime '${component.uses_database}'`);
405
462
  }
406
463
  }
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}'`);
464
+ if (["web_surface", "ios_surface", "android_surface"].includes(component.kind)) {
465
+ if (component.uses_api && byId.get(component.uses_api)?.kind !== "api_service") {
466
+ pushError(errors, `${componentLabel(component)} references missing api runtime '${component.uses_api}'`);
410
467
  }
411
468
  }
412
469
  }
@@ -428,23 +485,26 @@ export function validateProjectConfig(config, graph = null, options = {}) {
428
485
  pushError(errors, "topogram.project.json version must be a non-empty string");
429
486
  }
430
487
  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");
488
+ if (config.topology?.components != null && config.topology.__normalizedRuntimeAliases !== true) {
489
+ pushError(errors, `topogram.project.json ${renameDiagnostic("'topology.components'", "'topology.runtimes'", `"topology": { "runtimes": [] }`)}`);
490
+ }
491
+ if (!config.topology || typeof config.topology !== "object" || !Array.isArray(config.topology.runtimes)) {
492
+ pushError(errors, "topogram.project.json topology.runtimes must be an array");
433
493
  } else {
434
494
  const seenIds = new Set();
435
- for (const component of config.topology.components) {
495
+ for (const component of config.topology.runtimes) {
436
496
  validateComponentShape(errors, component, seenIds);
437
497
  }
438
- const generatorPolicy = validateProjectGeneratorPolicy(config, options);
498
+ const generatorPolicy = validateProjectGeneratorPolicy(normalizeProjectConfigRuntimeAliases(config), options);
439
499
  for (const error of generatorPolicy.errors) {
440
500
  pushError(errors, error.message, error.loc);
441
501
  }
442
502
  if (graph) {
443
503
  const projections = projectionById(graph);
444
- for (const component of config.topology.components) {
504
+ for (const component of config.topology.runtimes) {
445
505
  validateComponentCompatibility(errors, component, projections, options);
446
506
  }
447
- validateTopologyReferences(errors, config.topology.components);
507
+ validateTopologyReferences(errors, config.topology.runtimes);
448
508
  }
449
509
  }
450
510
  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 || []));