@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
@@ -0,0 +1,40 @@
1
+ function stableSortedWidgets(graph) {
2
+ return [...(graph?.byKind?.widget || [])].sort((a, b) => a.id.localeCompare(b.id));
3
+ }
4
+
5
+ function widgetContract(widget) {
6
+ return widget.widgetContract || {
7
+ type: "ui_widget_contract",
8
+ id: widget.id,
9
+ name: widget.name || widget.id,
10
+ description: widget.description || null,
11
+ category: widget.category || null,
12
+ version: widget.version || null,
13
+ status: widget.status || null,
14
+ props: widget.props || [],
15
+ events: widget.events || [],
16
+ slots: widget.slots || [],
17
+ behavior: widget.behavior || [],
18
+ behaviors: widget.behaviors || [],
19
+ patterns: widget.patterns || [],
20
+ regions: widget.regions || [],
21
+ approvals: widget.approvals || [],
22
+ lookups: widget.lookups || [],
23
+ dependencies: widget.dependencies || []
24
+ };
25
+ }
26
+
27
+ export function generateUiWidgetContract(graph, options = {}) {
28
+ const widgetId = options.widgetId || options.componentId;
29
+ if (widgetId) {
30
+ const widget = (graph?.byKind?.widget || graph?.byKind?.component || []).find((entry) => entry.id === widgetId);
31
+ if (!widget) {
32
+ throw new Error(`No widget found with id '${widgetId}'`);
33
+ }
34
+ return widgetContract(widget);
35
+ }
36
+
37
+ return Object.fromEntries(
38
+ stableSortedWidgets(graph).map((widget) => [widget.id, widgetContract(widget)])
39
+ );
40
+ }
@@ -175,18 +175,16 @@ export function generatorPackageAllowed(policy, packageName) {
175
175
  * @returns {PackageGeneratorBinding[]}
176
176
  */
177
177
  export function packageBackedGeneratorBindings(projectConfig) {
178
- const components = Array.isArray(projectConfig?.topology?.components)
179
- ? projectConfig.topology.components
180
- : [];
181
- return components
182
- .filter((component) => typeof component?.generator?.package === "string" && component.generator.package.length > 0)
183
- .map((component) => ({
184
- componentId: String(component.id || "unknown"),
185
- componentType: String(component.type || "unknown"),
186
- projection: String(component.projection || "unknown"),
187
- generatorId: String(component.generator.id || "unknown"),
188
- version: String(component.generator.version || "unknown"),
189
- packageName: String(component.generator.package)
178
+ const runtimes = Array.isArray(projectConfig?.topology?.runtimes) ? projectConfig.topology.runtimes : [];
179
+ return runtimes
180
+ .filter((runtime) => typeof runtime?.generator?.package === "string" && runtime.generator.package.length > 0)
181
+ .map((runtime) => ({
182
+ componentId: String(runtime.id || "unknown"),
183
+ componentType: String(runtime.kind || runtime.type || "unknown"),
184
+ projection: String(runtime.projection || "unknown"),
185
+ generatorId: String(runtime.generator.id || "unknown"),
186
+ version: String(runtime.generator.version || "unknown"),
187
+ packageName: String(runtime.generator.package)
190
188
  }));
191
189
  }
192
190
 
@@ -153,8 +153,8 @@ function deriveUiComponentCandidates(candidates) {
153
153
  const componentStem = idHintify(`${screen.id_hint}_results`);
154
154
  const loadCapability = loadCapabilityForScreen(screen);
155
155
  return makeCandidateRecord({
156
- kind: "component",
157
- idHint: `component_ui_${componentStem}`,
156
+ kind: "widget",
157
+ idHint: `widget_${componentStem}`,
158
158
  label: `${screen.label || screen.id_hint} results`,
159
159
  confidence: presentations.length > 0 ? "medium" : "low",
160
160
  sourceKind: "ui_projection_inference",
@@ -171,13 +171,13 @@ function deriveUiComponentCandidates(candidates) {
171
171
  inferred_pattern: pattern,
172
172
  evidence: screen.provenance || [],
173
173
  missing_decisions: [
174
- "confirm component reuse boundary",
174
+ "confirm widget reuse boundary",
175
175
  "confirm prop names and data source",
176
176
  "confirm events and behavior",
177
177
  "confirm supported regions and patterns"
178
178
  ],
179
179
  notes: [
180
- "Imported component candidates are review-only.",
180
+ "Imported widget candidates are review-only.",
181
181
  "Confirm props, behavior, events, and reuse before adoption."
182
182
  ]
183
183
  });
@@ -293,7 +293,7 @@ function reportMarkdown(track, candidates) {
293
293
  `- \`${component.id_hint}\` confidence ${component.confidence || "unknown"} pattern \`${component.pattern || component.inferred_pattern || "unknown"}\` region \`${component.region || component.inferred_region || "unknown"}\` evidence ${(component.evidence || component.provenance || []).length} missing decisions ${(component.missing_decisions || []).length}`
294
294
  );
295
295
  return ensureTrailingNewline(
296
- `# UI Import Report\n\n- Screens: ${candidates.screens.length}\n- Routes: ${candidates.routes.length}\n- Actions: ${candidates.actions.length}\n- Components: ${candidates.components.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n\n## Component Candidates\n\n${componentLines.length ? componentLines.join("\n") : "- none"}\n\n## Next Validation\n\n- Review candidates under \`topogram/candidates/app/ui/drafts/components/**\`.\n- Run \`topogram import plan <path>\` before adoption.\n- After adoption, run \`topogram check <path>\`, \`topogram component check <path>\`, and \`topogram component behavior <path>\`.\n`
296
+ `# UI Import Report\n\n- Screens: ${candidates.screens.length}\n- Routes: ${candidates.routes.length}\n- Actions: ${candidates.actions.length}\n- Widgets: ${candidates.components.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n\n## Widget Candidates\n\n${componentLines.length ? componentLines.join("\n") : "- none"}\n\n## Next Validation\n\n- Review candidates under \`topogram/candidates/app/ui/drafts/widgets/**\`.\n- Run \`topogram import plan <path>\` before adoption.\n- After adoption, run \`topogram check <path>\`, \`topogram widget check <path>\`, and \`topogram widget behavior <path>\`.\n`
297
297
  );
298
298
  }
299
299
  if (track === "verification") {
@@ -319,7 +319,7 @@ function projectionIdStem(workspaceRoot) {
319
319
  }
320
320
 
321
321
  function componentCandidateFileName(component) {
322
- return `${String(component.id_hint || "component")
322
+ return `${String(component.id_hint || "widget")
323
323
  .replace(/^component_/, "")
324
324
  .replace(/_/g, "-")}.tg`;
325
325
  }
@@ -327,15 +327,15 @@ function componentCandidateFileName(component) {
327
327
  function renderComponentCandidate(component) {
328
328
  const evidenceCount = (component.evidence || component.provenance || []).length;
329
329
  const missingDecisions = component.missing_decisions || [
330
- "confirm component reuse boundary",
330
+ "confirm widget reuse boundary",
331
331
  "confirm prop names and data source",
332
332
  "confirm events and behavior"
333
333
  ];
334
- return `component ${component.id_hint} {
334
+ return `widget ${component.id_hint} {
335
335
  # Import metadata: confidence ${component.confidence || "unknown"}; evidence ${evidenceCount}; inferred pattern ${component.pattern || component.inferred_pattern || "search_results"}; inferred region ${component.region || component.inferred_region || "results"}.
336
336
  # Missing decisions: ${missingDecisions.join("; ")}.
337
337
  name "${component.label || component.id_hint}"
338
- description "Candidate reusable component inferred from imported UI evidence. Review props, behavior, events, and reuse before adoption."
338
+ description "Candidate reusable widget inferred from imported UI evidence. Review props, behavior, events, and reuse before adoption."
339
339
  category collection
340
340
  props {
341
341
  ${component.data_prop || "rows"} array required
@@ -355,7 +355,7 @@ function uiComponentLinesForCandidates(componentCandidates, allCandidates) {
355
355
  const dataBinding = dataSource
356
356
  ? ` data ${component.data_prop || "rows"} from ${dataSource}`
357
357
  : "";
358
- return ` screen ${component.screen_id} region ${component.region} component ${component.id_hint}${dataBinding}`;
358
+ return ` screen ${component.screen_id} region ${component.region} widget ${component.id_hint}${dataBinding}`;
359
359
  });
360
360
  }
361
361
 
@@ -386,7 +386,7 @@ function draftUiProjectionFiles(context, candidates, allCandidates = {}) {
386
386
  const actions = ui.actions || [];
387
387
  const componentCandidates = [...(ui.components || [])].sort((a, b) => a.id_hint.localeCompare(b.id_hint));
388
388
  const shell = actions.find((entry) => entry.kind === "ui_shell")?.shell_kind || "topbar";
389
- const navigationPatterns = uniqueSorted(actions.filter((entry) => entry.kind === "ui_navigation").map((entry) => entry.navigation_pattern));
389
+ const navigationPatterns = uniqueSorted(actions.filter((entry) => entry.kind === "navigation").map((entry) => entry.navigation_pattern));
390
390
  const presentations = uniqueSorted(actions.filter((entry) => entry.kind === "ui_presentation").map((entry) => entry.presentation));
391
391
  const capabilityHints = uniqueSorted([
392
392
  ...screens.flatMap((screen) => capabilityHintsForScreen(screen)),
@@ -488,22 +488,22 @@ function draftUiProjectionFiles(context, candidates, allCandidates = {}) {
488
488
  }
489
489
  const uiComponentLines = uiComponentLinesForCandidates(componentCandidates, allCandidates);
490
490
 
491
- const uiSharedDraft = `projection proj_ui_shared_imported_${stem} {
492
- name "Imported Shared UI Draft"
491
+ const uiSharedDraft = `projection proj_ui_contract_imported_${stem} {
492
+ name "Imported UI Contract Draft"
493
493
  description "Drafted from imported UI candidates. Review and adapt before adoption."
494
494
 
495
- platform ui_shared
495
+ type ui_contract
496
496
  realizes [
497
497
  ${capabilityHints.length > 0 ? capabilityHints.map((hint) => ` ${hint}`).join(",\n") : " // add capability ids"}
498
498
  ]
499
499
  outputs [ui_contract]
500
500
 
501
- ui_app_shell {
501
+ app_shell {
502
502
  brand "Imported ${stem.replace(/_/g, " ")}"
503
503
  shell ${shell}
504
504
  ${presentations.includes("search") ? " global_search true\n" : ""}${presentations.includes("multi_window") ? " windowing multi_window\n" : ""} }
505
505
 
506
- ui_design {
506
+ design_tokens {
507
507
  density comfortable
508
508
  tone operational
509
509
  radius_scale medium
@@ -517,15 +517,15 @@ ${presentations.includes("search") ? " global_search true\n" : ""}${presentat
517
517
  accessibility focus visible
518
518
  }
519
519
 
520
- ui_screens {
520
+ screens {
521
521
  ${uiScreensBlock}
522
522
  }
523
523
 
524
- ${uiCollectionsLines.length > 0 ? ` ui_collections {\n${uiCollectionsLines.join("\n")}\n }\n\n` : ""}${uiActionsLines.length > 0 ? ` ui_actions {\n${uiActionsLines.join("\n")}\n }\n\n` : ""} ui_navigation {
524
+ ${uiCollectionsLines.length > 0 ? ` collection_views {\n${uiCollectionsLines.join("\n")}\n }\n\n` : ""}${uiActionsLines.length > 0 ? ` screen_actions {\n${uiActionsLines.join("\n")}\n }\n\n` : ""} navigation {
525
525
  ${uiNavigationLines.join("\n")}
526
526
  }
527
527
 
528
- ${uiScreenRegionLines.length > 0 ? ` ui_screen_regions {\n${uiScreenRegionLines.join("\n")}\n }\n\n` : ""}${uiComponentLines.length > 0 ? ` ui_components {\n${uiComponentLines.join("\n")}\n }\n\n` : ""} status proposed
528
+ ${uiScreenRegionLines.length > 0 ? ` screen_regions {\n${uiScreenRegionLines.join("\n")}\n }\n\n` : ""}${uiComponentLines.length > 0 ? ` widget_bindings {\n${uiComponentLines.join("\n")}\n }\n\n` : ""} status proposed
529
529
  }
530
530
  `;
531
531
 
@@ -565,22 +565,22 @@ ${uiScreenRegionLines.length > 0 ? ` ui_screen_regions {\n${uiScreenRegionLines
565
565
  uiWebLines.push(` action ${entry.capability_hint} present ${actionPresent}`);
566
566
  }
567
567
 
568
- const uiWebDraft = `projection proj_ui_web_imported_${stem} {
569
- name "Imported Web UI Draft"
568
+ const uiWebDraft = `projection proj_web_surface_imported_${stem} {
569
+ name "Imported Web Surface Draft"
570
570
  description "Drafted from imported UI candidates. Review and adapt before adoption."
571
571
 
572
- platform ui_web
572
+ type web_surface
573
573
  realizes [
574
- proj_ui_shared_imported_${stem},
574
+ proj_ui_contract_imported_${stem},
575
575
  ${webCapHints}
576
576
  ]
577
577
  outputs [ui_contract, web_app]
578
578
 
579
- ui_routes {
579
+ screen_routes {
580
580
  ${uiRouteLines.length > 0 ? uiRouteLines.join("\n") : " // add routes"}
581
581
  }
582
582
 
583
- ${uiWebLines.length > 0 ? ` ui_web {\n${uiWebLines.join("\n")}\n }\n\n` : ""} generator_defaults {
583
+ ${uiWebLines.length > 0 ? ` web_hints {\n${uiWebLines.join("\n")}\n }\n\n` : ""} generator_defaults {
584
584
  profile react
585
585
  language typescript
586
586
  styling css
@@ -592,9 +592,9 @@ ${uiWebLines.length > 0 ? ` ui_web {\n${uiWebLines.join("\n")}\n }\n\n` : ""}
592
592
 
593
593
  const coverage = `# Imported UI Projection Drafts
594
594
 
595
- - Draft shared projection: \`candidates/app/ui/drafts/proj-ui-shared.tg\`
596
- - Draft web projection: \`candidates/app/ui/drafts/proj-ui-web.tg\`
597
- - Draft component candidates: ${componentCandidates.length}
595
+ - Draft UI contract projection: \`candidates/app/ui/drafts/proj-ui-contract.tg\`
596
+ - Draft web surface projection: \`candidates/app/ui/drafts/proj-web-surface.tg\`
597
+ - Draft widget candidates: ${componentCandidates.length}
598
598
  - Imported screens: ${screens.length}
599
599
  - Imported routes: ${(ui.routes || []).length}
600
600
  - Imported UI actions/presentations: ${actions.length}
@@ -605,19 +605,19 @@ ${uiWebLines.length > 0 ? ` ui_web {\n${uiWebLines.join("\n")}\n }\n\n` : ""}
605
605
 
606
606
  - These files are drafts, not adopted canonical projections.
607
607
  - Capability ids come from imported hints and may need renaming or pruning.
608
- - Component candidates are suggested reusable contracts, not canonical ownership.
609
- - Review component props, events, behavior, regions, and patterns before adopting.
608
+ - Widget candidates are suggested reusable contracts, not canonical ownership.
609
+ - Review widget props, events, behavior, regions, and patterns before adopting.
610
610
  - Search and refresh directives are inferred heuristically.
611
611
  - Navigation groups currently default to a single \`workspace\` group unless stronger grouping evidence exists.
612
612
  `;
613
613
 
614
614
  const files = {
615
- "candidates/app/ui/drafts/proj-ui-shared.tg": ensureTrailingNewline(uiSharedDraft),
616
- "candidates/app/ui/drafts/proj-ui-web.tg": ensureTrailingNewline(uiWebDraft),
615
+ "candidates/app/ui/drafts/proj-ui-contract.tg": ensureTrailingNewline(uiSharedDraft),
616
+ "candidates/app/ui/drafts/proj-web-surface.tg": ensureTrailingNewline(uiWebDraft),
617
617
  "candidates/app/ui/drafts/README.md": ensureTrailingNewline(coverage)
618
618
  };
619
619
  for (const component of componentCandidates) {
620
- files[`candidates/app/ui/drafts/components/${componentCandidateFileName(component)}`] = ensureTrailingNewline(renderComponentCandidate(component));
620
+ files[`candidates/app/ui/drafts/widgets/${componentCandidateFileName(component)}`] = ensureTrailingNewline(renderComponentCandidate(component));
621
621
  }
622
622
  return files;
623
623
  }
@@ -676,7 +676,7 @@ export function runImportApp(inputPath, options = {}) {
676
676
  files["candidates/app/findings.json"] = `${JSON.stringify(findings, null, 2)}\n`;
677
677
  files["candidates/app/candidates.json"] = `${JSON.stringify(candidates, null, 2)}\n`;
678
678
  files["candidates/app/report.md"] = ensureTrailingNewline(
679
- `# App Import Report\n\nTracks: ${tracks.join(", ")}\n\n## DB\n\n- Entities: ${candidates.db?.entities.length || 0}\n- Enums: ${candidates.db?.enums.length || 0}\n- Relations: ${candidates.db?.relations.length || 0}\n\n## API\n\n- Capabilities: ${candidates.api?.capabilities.length || 0}\n- Routes: ${candidates.api?.routes.length || 0}\n- Stacks: ${candidates.api?.stacks?.length ? candidates.api.stacks.join(", ") : "none"}\n\n## UI\n\n- Screens: ${candidates.ui?.screens.length || 0}\n- Routes: ${candidates.ui?.routes.length || 0}\n- Actions: ${candidates.ui?.actions.length || 0}\n- Components: ${candidates.ui?.components.length || 0}\n- Stacks: ${candidates.ui?.stacks?.length ? candidates.ui.stacks.join(", ") : "none"}\n\n## Workflows\n\n- Workflows: ${candidates.workflows?.workflows.length || 0}\n- States: ${candidates.workflows?.workflow_states.length || 0}\n- Transitions: ${candidates.workflows?.workflow_transitions.length || 0}\n\n## Verification\n\n- Verifications: ${candidates.verification?.verifications.length || 0}\n- Scenarios: ${candidates.verification?.scenarios.length || 0}\n- Frameworks: ${candidates.verification?.frameworks?.length ? candidates.verification.frameworks.join(", ") : "none"}\n- Scripts: ${candidates.verification?.scripts.length || 0}\n`
679
+ `# App Import Report\n\nTracks: ${tracks.join(", ")}\n\n## DB\n\n- Entities: ${candidates.db?.entities?.length || 0}\n- Enums: ${candidates.db?.enums?.length || 0}\n- Relations: ${candidates.db?.relations?.length || 0}\n\n## API\n\n- Capabilities: ${candidates.api?.capabilities?.length || 0}\n- Routes: ${candidates.api?.routes?.length || 0}\n- Stacks: ${candidates.api?.stacks?.length ? candidates.api.stacks.join(", ") : "none"}\n\n## UI\n\n- Screens: ${candidates.ui?.screens?.length || 0}\n- Routes: ${candidates.ui?.routes?.length || 0}\n- Actions: ${candidates.ui?.actions?.length || 0}\n- Widgets: ${candidates.ui?.components?.length || 0}\n- Stacks: ${candidates.ui?.stacks?.length ? candidates.ui.stacks.join(", ") : "none"}\n\n## Workflows\n\n- Workflows: ${candidates.workflows?.workflows?.length || 0}\n- States: ${candidates.workflows?.workflow_states?.length || 0}\n- Transitions: ${candidates.workflows?.workflow_transitions?.length || 0}\n\n## Verification\n\n- Verifications: ${candidates.verification?.verifications?.length || 0}\n- Scenarios: ${candidates.verification?.scenarios?.length || 0}\n- Frameworks: ${candidates.verification?.frameworks?.length ? candidates.verification.frameworks.join(", ") : "none"}\n- Scripts: ${candidates.verification?.scripts?.length || 0}\n`
680
680
  );
681
681
 
682
682
  return {
@@ -213,7 +213,7 @@ export function makeCandidateRecord({
213
213
  ? "db"
214
214
  : kind === "capability"
215
215
  ? "api"
216
- : kind === "component"
216
+ : kind === "widget"
217
217
  ? "ui"
218
218
  : null);
219
219
  return {
@@ -131,7 +131,7 @@ export const androidComposeUiExtractor = {
131
131
  }
132
132
  for (const pattern of aggregateSummary.patterns) {
133
133
  candidates.actions.push(makeCandidateRecord({
134
- kind: "ui_navigation",
134
+ kind: "navigation",
135
135
  idHint: `compose_${idHintify(pattern)}`,
136
136
  label: pattern,
137
137
  confidence: "medium",
@@ -138,7 +138,7 @@ export const blazorUiExtractor = {
138
138
  }
139
139
  for (const pattern of summary.patterns) {
140
140
  candidates.actions.push(makeCandidateRecord({
141
- kind: "ui_navigation",
141
+ kind: "navigation",
142
142
  idHint: `blazor_${idHintify(pattern)}`,
143
143
  label: pattern,
144
144
  confidence: "medium",
@@ -150,7 +150,7 @@ export const razorPagesUiExtractor = {
150
150
 
151
151
  for (const pattern of shellPatterns(text)) {
152
152
  candidates.actions.push(makeCandidateRecord({
153
- kind: "ui_navigation",
153
+ kind: "navigation",
154
154
  idHint: `razor_${idHintify(pattern)}`,
155
155
  label: pattern,
156
156
  confidence: "low",
@@ -42,9 +42,9 @@ export const reactRouterUiExtractor = {
42
42
  const features = detectUiPresentationFeatures(rootDir);
43
43
  const shellKind = shellKindFromNavigation(navigation);
44
44
  const navigationPatterns = navigationPatternsFromStructure(navigation);
45
- findings.push({ kind: "react_ui_routes", file: provenance, routes });
46
- findings.push({ kind: "react_ui_navigation", file: provenance, navigation });
47
- findings.push({ kind: "react_ui_features", file: provenance, features });
45
+ findings.push({ kind: "react_screen_routes", file: provenance, routes });
46
+ findings.push({ kind: "react_navigation", file: provenance, navigation });
47
+ findings.push({ kind: "react_surface_features", file: provenance, features });
48
48
  candidates.stacks.push("react_web");
49
49
  if (shellKind) {
50
50
  candidates.actions.push(makeCandidateRecord({
@@ -62,7 +62,7 @@ export const reactRouterUiExtractor = {
62
62
  }
63
63
  for (const pattern of navigationPatterns) {
64
64
  candidates.actions.push(makeCandidateRecord({
65
- kind: "ui_navigation",
65
+ kind: "navigation",
66
66
  idHint: `${path.basename(rootDir)}_${idHintify(pattern)}`,
67
67
  label: pattern,
68
68
  confidence: "medium",
@@ -41,9 +41,9 @@ export const svelteKitUiExtractor = {
41
41
  const features = detectUiPresentationFeatures(rootDir);
42
42
  const shellKind = shellKindFromNavigation(navigation);
43
43
  const navigationPatterns = navigationPatternsFromStructure(navigation);
44
- findings.push({ kind: "sveltekit_ui_routes", file: provenance, routes });
45
- findings.push({ kind: "sveltekit_ui_navigation", file: provenance, navigation });
46
- findings.push({ kind: "sveltekit_ui_features", file: provenance, features });
44
+ findings.push({ kind: "sveltekit_screen_routes", file: provenance, routes });
45
+ findings.push({ kind: "sveltekit_navigation", file: provenance, navigation });
46
+ findings.push({ kind: "sveltekit_surface_features", file: provenance, features });
47
47
  candidates.stacks.push("sveltekit_web");
48
48
  if (shellKind) {
49
49
  candidates.actions.push(makeCandidateRecord({
@@ -61,7 +61,7 @@ export const svelteKitUiExtractor = {
61
61
  }
62
62
  for (const pattern of navigationPatterns) {
63
63
  candidates.actions.push(makeCandidateRecord({
64
- kind: "ui_navigation",
64
+ kind: "navigation",
65
65
  idHint: `${path.basename(rootDir)}_${pattern}`,
66
66
  label: pattern,
67
67
  confidence: "medium",
@@ -153,7 +153,7 @@ export const swiftUiExtractor = {
153
153
  }
154
154
  for (const pattern of summary.patterns) {
155
155
  candidates.actions.push(makeCandidateRecord({
156
- kind: "ui_navigation",
156
+ kind: "navigation",
157
157
  idHint: `swiftui_${idHintify(pattern)}`,
158
158
  label: pattern,
159
159
  confidence: "medium",
@@ -144,7 +144,7 @@ export const uiKitExtractor = {
144
144
 
145
145
  if (candidates.screens.length > 0) {
146
146
  candidates.actions.push(makeCandidateRecord({
147
- kind: "ui_navigation",
147
+ kind: "navigation",
148
148
  idHint: "uikit_stack_navigation",
149
149
  label: "stack_navigation",
150
150
  confidence: "medium",
@@ -27,10 +27,11 @@ const GENERATOR_LABELS = new Map([
27
27
  ]);
28
28
 
29
29
  const SURFACE_ORDER = new Map([
30
- ["web", 10],
31
- ["api", 20],
30
+ ["web_surface", 10],
31
+ ["api_service", 20],
32
32
  ["database", 30],
33
- ["native", 40]
33
+ ["ios_surface", 40],
34
+ ["android_surface", 50]
34
35
  ]);
35
36
 
36
37
  /**
@@ -427,26 +428,26 @@ function generatorLabel(generatorId) {
427
428
  function summarizeTemplateTopology(templateRoot) {
428
429
  const projectConfigPath = path.join(templateRoot, "topogram.project.json");
429
430
  const projectConfig = JSON.parse(fs.readFileSync(projectConfigPath, "utf8"));
430
- const rawComponents = /** @type {any[]} */ (
431
- Array.isArray(projectConfig.topology?.components) ? projectConfig.topology.components : []
431
+ const rawRuntimes = /** @type {any[]} */ (
432
+ Array.isArray(projectConfig.topology?.runtimes) ? projectConfig.topology.runtimes : []
432
433
  );
433
434
  /** @type {Array<Record<string, any>>} */
434
- const components = [];
435
- for (const component of rawComponents) {
436
- if (component && typeof component === "object" && typeof component.type === "string") {
437
- components.push(/** @type {Record<string, any>} */ (component));
435
+ const runtimes = [];
436
+ for (const runtime of rawRuntimes) {
437
+ if (runtime && typeof runtime === "object" && typeof runtime.kind === "string") {
438
+ runtimes.push(/** @type {Record<string, any>} */ (runtime));
438
439
  }
439
440
  }
440
- const sortedComponents = [...components].sort((a, b) => {
441
- const aOrder = SURFACE_ORDER.get(a.type) ?? 100;
442
- const bOrder = SURFACE_ORDER.get(b.type) ?? 100;
441
+ const sortedRuntimes = [...runtimes].sort((a, b) => {
442
+ const aOrder = SURFACE_ORDER.get(a.kind) ?? 100;
443
+ const bOrder = SURFACE_ORDER.get(b.kind) ?? 100;
443
444
  return aOrder - bOrder;
444
445
  });
445
- const surfaces = [...new Set(sortedComponents.map((component) => String(component.type)))];
446
+ const surfaces = [...new Set(sortedRuntimes.map((runtime) => String(runtime.kind)))];
446
447
  const generators = [
447
448
  ...new Set(
448
- sortedComponents
449
- .map((component) => component.generator?.id)
449
+ sortedRuntimes
450
+ .map((runtime) => runtime.generator?.id)
450
451
  .filter((generatorId) => typeof generatorId === "string")
451
452
  .map((generatorId) => String(generatorId))
452
453
  )
@@ -704,7 +705,7 @@ function writeProjectTemplateMetadata(projectRoot, template, templateProvenance
704
705
  const projectConfigPath = path.join(projectRoot, "topogram.project.json");
705
706
  const projectConfig = JSON.parse(fs.readFileSync(projectConfigPath, "utf8"));
706
707
  projectConfig.template = projectTemplateMetadata(template, templateProvenance);
707
- fs.writeFileSync(projectConfigPath, `${JSON.stringify(projectConfig, null, 2)}\n`, "utf8");
708
+ fs.writeFileSync(projectConfigPath, `${stableJsonStringify(projectConfig)}\n`, "utf8");
708
709
  return projectConfig;
709
710
  }
710
711
 
@@ -1323,8 +1324,8 @@ function currentTemplateOwnedFiles(projectRoot, includeImplementation, projectCo
1323
1324
  if (fs.existsSync(projectConfigPath)) {
1324
1325
  files.set("topogram.project.json", {
1325
1326
  path: "topogram.project.json",
1326
- absolutePath: null,
1327
- content: `${stableJsonStringify(projectConfig)}\n`
1327
+ absolutePath: projectConfigPath,
1328
+ content: null
1328
1329
  });
1329
1330
  }
1330
1331
  return files;