@topogram/cli 0.3.76 → 0.3.78

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 (67) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +12 -1
  3. package/src/cli/commands/import/workspace.js +1 -0
  4. package/src/cli.js +3 -3
  5. package/src/import/core/runner/candidates.js +2 -0
  6. package/src/import/core/runner/reports.js +12 -5
  7. package/src/import/core/runner/tracks.js +1 -1
  8. package/src/import/core/shared/files.js +76 -1
  9. package/src/import/core/shared/next-app.js +2 -2
  10. package/src/import/core/shared/ui-routes.js +106 -0
  11. package/src/import/core/shared.js +8 -1
  12. package/src/import/enrichers/django-rest.js +4 -4
  13. package/src/import/enrichers/rails-controllers.js +3 -3
  14. package/src/import/enrichers/rails-models.js +3 -3
  15. package/src/import/extractors/api/aspnet-core.js +5 -5
  16. package/src/import/extractors/api/django-routes.js +5 -5
  17. package/src/import/extractors/api/express.js +4 -4
  18. package/src/import/extractors/api/fastify.js +7 -7
  19. package/src/import/extractors/api/flutter-dio.js +4 -4
  20. package/src/import/extractors/api/generic-route-fallback.js +3 -2
  21. package/src/import/extractors/api/graphql-code-first.js +3 -3
  22. package/src/import/extractors/api/graphql-sdl.js +5 -5
  23. package/src/import/extractors/api/jaxrs.js +3 -3
  24. package/src/import/extractors/api/micronaut.js +3 -3
  25. package/src/import/extractors/api/openapi-code.js +4 -4
  26. package/src/import/extractors/api/openapi.js +3 -3
  27. package/src/import/extractors/api/rails-routes.js +3 -3
  28. package/src/import/extractors/api/react-native-repository.js +3 -3
  29. package/src/import/extractors/api/retrofit.js +3 -3
  30. package/src/import/extractors/api/spring-web.js +3 -3
  31. package/src/import/extractors/api/swift-webapi.js +3 -3
  32. package/src/import/extractors/api/trpc.js +4 -4
  33. package/src/import/extractors/cli/generic.js +5 -4
  34. package/src/import/extractors/db/django-models.js +4 -4
  35. package/src/import/extractors/db/dotnet-models.js +4 -4
  36. package/src/import/extractors/db/drizzle.js +21 -9
  37. package/src/import/extractors/db/ef-core.js +5 -5
  38. package/src/import/extractors/db/flutter-entities.js +3 -3
  39. package/src/import/extractors/db/jpa.js +3 -3
  40. package/src/import/extractors/db/liquibase.js +3 -3
  41. package/src/import/extractors/db/maintained-seams.js +27 -4
  42. package/src/import/extractors/db/mybatis-xml.js +4 -4
  43. package/src/import/extractors/db/prisma.js +10 -3
  44. package/src/import/extractors/db/rails-schema.js +3 -3
  45. package/src/import/extractors/db/react-native-entities.js +3 -3
  46. package/src/import/extractors/db/room.js +5 -5
  47. package/src/import/extractors/db/snapshot.js +3 -3
  48. package/src/import/extractors/db/sql.js +3 -3
  49. package/src/import/extractors/db/swiftdata.js +3 -3
  50. package/src/import/extractors/ui/android-compose.js +4 -4
  51. package/src/import/extractors/ui/backend-only.js +3 -3
  52. package/src/import/extractors/ui/blazor.js +3 -3
  53. package/src/import/extractors/ui/flutter-screens.js +3 -3
  54. package/src/import/extractors/ui/maui-xaml.js +4 -4
  55. package/src/import/extractors/ui/next-app-router.js +26 -5
  56. package/src/import/extractors/ui/next-pages-router.js +34 -10
  57. package/src/import/extractors/ui/razor-pages.js +3 -3
  58. package/src/import/extractors/ui/react-native-screens.js +4 -4
  59. package/src/import/extractors/ui/react-router.js +34 -6
  60. package/src/import/extractors/ui/sveltekit.js +34 -6
  61. package/src/import/extractors/ui/swiftui.js +4 -3
  62. package/src/import/extractors/ui/uikit.js +3 -3
  63. package/src/workflows/reconcile/bundle-core/index.js +20 -1
  64. package/src/workflows/reconcile/candidate-model.js +13 -1
  65. package/src/workflows/reconcile/impacts/adoption-plan.js +13 -0
  66. package/src/workflows/reconcile/renderers.js +33 -0
  67. package/src/workflows/reconcile/workflow.js +2 -1
@@ -2,10 +2,13 @@ import path from "node:path";
2
2
 
3
3
  import {
4
4
  dedupeCandidateRecords,
5
- findImportFiles,
5
+ findPrimaryImportFiles,
6
+ inferNonResourceUiFlow,
6
7
  makeCandidateRecord,
8
+ proposedUiContractAdditionsForFlow,
7
9
  relativeTo,
8
10
  titleCase,
11
+ uiFlowIdForRoute,
9
12
  idHintify
10
13
  } from "../../core/shared.js";
11
14
 
@@ -58,7 +61,7 @@ export const nextPagesRouterUiExtractor = {
58
61
  id: "ui.next-pages-router",
59
62
  track: "ui",
60
63
  detect(context) {
61
- const pageFiles = findImportFiles(context.paths, (filePath) => /src\/pages\/.+\.(tsx|ts|jsx|js|mdx)$/i.test(filePath))
64
+ const pageFiles = findPrimaryImportFiles(context.paths, (filePath) => /src\/pages\/.+\.(tsx|ts|jsx|js|mdx)$/i.test(filePath))
62
65
  .filter((filePath) => !/\/api\//.test(filePath) && !/\/_(app|document|error)\./.test(filePath) && !/\/404\./.test(filePath));
63
66
  return {
64
67
  score: pageFiles.length > 0 ? 85 : 0,
@@ -67,10 +70,10 @@ export const nextPagesRouterUiExtractor = {
67
70
  },
68
71
  extract(context) {
69
72
  const pagesRoot = path.join(context.paths.workspaceRoot, "src", "pages");
70
- const pageFiles = findImportFiles(context.paths, (filePath) => /src\/pages\/.+\.(tsx|ts|jsx|js|mdx)$/i.test(filePath))
73
+ const pageFiles = findPrimaryImportFiles(context.paths, (filePath) => /src\/pages\/.+\.(tsx|ts|jsx|js|mdx)$/i.test(filePath))
71
74
  .filter((filePath) => !/\/api\//.test(filePath) && !/\/_(app|document|error)\./.test(filePath) && !/\/404\./.test(filePath));
72
75
  const findings = [];
73
- const candidates = { screens: [], routes: [], actions: [], stacks: [] };
76
+ const candidates = { screens: [], routes: [], actions: [], flows: [], stacks: [] };
74
77
  if (pageFiles.length > 0) {
75
78
  const routes = [];
76
79
  for (const filePath of pageFiles) {
@@ -78,6 +81,7 @@ export const nextPagesRouterUiExtractor = {
78
81
  const text = context.helpers.readTextIfExists(filePath) || "";
79
82
  const hooks = inferTrpcHooks(text);
80
83
  const screen = screenFromRouteAndHooks(routePath, hooks);
84
+ const flow = inferNonResourceUiFlow(routePath);
81
85
  const provenance = `${relativeTo(context.paths.repoRoot, filePath)}#${routePath}`;
82
86
  routes.push(routePath);
83
87
  candidates.screens.push(makeCandidateRecord({
@@ -88,11 +92,11 @@ export const nextPagesRouterUiExtractor = {
88
92
  sourceKind: "route_code",
89
93
  provenance,
90
94
  track: "ui",
91
- entity_id: screen.entity,
92
- concept_id: screen.concept,
93
- screen_kind: screen.kind,
95
+ entity_id: flow ? null : screen.entity,
96
+ concept_id: flow?.concept_id || screen.concept,
97
+ screen_kind: flow ? "flow" : screen.kind,
94
98
  route_path: routePath,
95
- capability_hints: hooks.map((hook) => capabilityHint(hook.router, hook.procedure))
99
+ capability_hints: flow ? [] : hooks.map((hook) => capabilityHint(hook.router, hook.procedure))
96
100
  }));
97
101
  candidates.routes.push(makeCandidateRecord({
98
102
  kind: "ui_route",
@@ -103,10 +107,29 @@ export const nextPagesRouterUiExtractor = {
103
107
  provenance,
104
108
  track: "ui",
105
109
  screen_id: screen.id,
106
- entity_id: screen.entity,
107
- concept_id: screen.concept,
110
+ entity_id: flow ? null : screen.entity,
111
+ concept_id: flow?.concept_id || screen.concept,
108
112
  path: routePath
109
113
  }));
114
+ if (flow) {
115
+ candidates.flows.push(makeCandidateRecord({
116
+ kind: "ui_flow",
117
+ idHint: uiFlowIdForRoute(routePath, screen.id),
118
+ label: `${titleCase(flow.flow_type)} Flow`,
119
+ confidence: flow.confidence,
120
+ sourceKind: "route_code",
121
+ sourceOfTruth: "candidate",
122
+ provenance,
123
+ track: "ui",
124
+ flow_type: flow.flow_type,
125
+ concept_id: flow.concept_id,
126
+ screen_ids: [screen.id],
127
+ route_paths: [routePath],
128
+ evidence: [provenance],
129
+ missing_decisions: flow.missing_decisions,
130
+ proposed_ui_contract_additions: proposedUiContractAdditionsForFlow(routePath, screen.id, "flow")
131
+ }));
132
+ }
110
133
  for (const hook of hooks.filter((hook) => hook.hook === "Mutation")) {
111
134
  const capability = capabilityHint(hook.router, hook.procedure);
112
135
  candidates.actions.push(makeCandidateRecord({
@@ -135,6 +158,7 @@ export const nextPagesRouterUiExtractor = {
135
158
  candidates.screens = dedupeCandidateRecords(candidates.screens, (record) => record.id_hint);
136
159
  candidates.routes = dedupeCandidateRecords(candidates.routes, (record) => record.id_hint);
137
160
  candidates.actions = dedupeCandidateRecords(candidates.actions, (record) => record.id_hint);
161
+ candidates.flows = dedupeCandidateRecords(candidates.flows, (record) => record.id_hint);
138
162
  candidates.stacks = [...new Set(candidates.stacks)].sort();
139
163
  return { findings, candidates };
140
164
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  canonicalCandidateTerm,
3
3
  dedupeCandidateRecords,
4
- findImportFiles,
4
+ findPrimaryImportFiles,
5
5
  idHintify,
6
6
  makeCandidateRecord,
7
7
  relativeTo,
@@ -81,7 +81,7 @@ export const razorPagesUiExtractor = {
81
81
  id: "ui.razor-pages",
82
82
  track: "ui",
83
83
  detect(context) {
84
- const files = findImportFiles(context.paths, (filePath) => /\.cshtml$/i.test(filePath))
84
+ const files = findPrimaryImportFiles(context.paths, (filePath) => /\.cshtml$/i.test(filePath))
85
85
  .filter((filePath) => !shouldIgnoreFile(filePath));
86
86
  const score = files.length > 0 ? 84 : 0;
87
87
  return {
@@ -90,7 +90,7 @@ export const razorPagesUiExtractor = {
90
90
  };
91
91
  },
92
92
  extract(context) {
93
- const files = findImportFiles(context.paths, (filePath) => /\.cshtml$/i.test(filePath))
93
+ const files = findPrimaryImportFiles(context.paths, (filePath) => /\.cshtml$/i.test(filePath))
94
94
  .filter((filePath) => !shouldIgnoreFile(filePath));
95
95
  const findings = [];
96
96
  const candidates = { screens: [], routes: [], actions: [], stacks: [] };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  canonicalCandidateTerm,
3
3
  dedupeCandidateRecords,
4
- findImportFiles,
4
+ findPrimaryImportFiles,
5
5
  idHintify,
6
6
  makeCandidateRecord,
7
7
  relativeTo,
@@ -119,7 +119,7 @@ export const reactNativeScreensExtractor = {
119
119
  id: "ui.react-native-screens",
120
120
  track: "ui",
121
121
  detect(context) {
122
- const files = findImportFiles(
122
+ const files = findPrimaryImportFiles(
123
123
  context.paths,
124
124
  (filePath) => /\/src\/.+\/presentation\/screens\/.+Screen\.tsx$/i.test(filePath)
125
125
  );
@@ -130,11 +130,11 @@ export const reactNativeScreensExtractor = {
130
130
  };
131
131
  },
132
132
  extract(context) {
133
- const screenFiles = findImportFiles(
133
+ const screenFiles = findPrimaryImportFiles(
134
134
  context.paths,
135
135
  (filePath) => /\/src\/.+\/presentation\/screens\/.+Screen\.tsx$/i.test(filePath)
136
136
  );
137
- const navigatorFile = findImportFiles(
137
+ const navigatorFile = findPrimaryImportFiles(
138
138
  context.paths,
139
139
  (filePath) => /\/src\/core\/presentation\/navigation\/RootNavigator\.tsx$/i.test(filePath)
140
140
  )[0];
@@ -2,14 +2,17 @@ import {
2
2
  dedupeCandidateRecords,
3
3
  detectUiPresentationFeatures,
4
4
  entityIdForRoute,
5
+ inferNonResourceUiFlow,
5
6
  inferNavigationStructure,
6
7
  inferReactRoutes,
7
8
  makeCandidateRecord,
8
9
  navigationPatternsFromStructure,
10
+ proposedUiContractAdditionsForFlow,
9
11
  relativeTo,
10
12
  shellKindFromNavigation,
11
13
  screenIdForRoute,
12
14
  screenKindForRoute,
15
+ uiFlowIdForRoute,
13
16
  uiCapabilityHintsForRoute,
14
17
  titleCase,
15
18
  idHintify
@@ -29,7 +32,7 @@ export const reactRouterUiExtractor = {
29
32
  },
30
33
  extract(context) {
31
34
  const findings = [];
32
- const candidates = { screens: [], routes: [], actions: [], stacks: [] };
35
+ const candidates = { screens: [], routes: [], actions: [], flows: [], stacks: [] };
33
36
  const roots = [
34
37
  path.join(context.paths.workspaceRoot, "apps", "web"),
35
38
  path.join(context.paths.workspaceRoot, "examples", "maintained", "proof-app")
@@ -75,16 +78,20 @@ export const reactRouterUiExtractor = {
75
78
  for (const routePath of routes) {
76
79
  const screenId = screenIdForRoute(routePath);
77
80
  const screenKind = screenKindForRoute(routePath);
78
- const capabilityHints = uiCapabilityHintsForRoute(routePath);
81
+ const flow = inferNonResourceUiFlow(routePath);
82
+ const capabilityHints = flow ? { load: null, submit: null, primary_action: null } : uiCapabilityHintsForRoute(routePath);
83
+ const entityId = flow ? null : entityIdForRoute(routePath);
84
+ const routeProvenance = `${provenance}#${routePath}`;
79
85
  candidates.screens.push(makeCandidateRecord({
80
86
  kind: "screen",
81
87
  idHint: screenId,
82
88
  label: titleCase(screenId),
83
89
  confidence: "medium",
84
90
  sourceKind: "route_code",
85
- provenance: `${provenance}#${routePath}`,
91
+ provenance: routeProvenance,
86
92
  track: "ui",
87
- entity_id: entityIdForRoute(routePath),
93
+ entity_id: entityId,
94
+ concept_id: flow?.concept_id || entityId,
88
95
  screen_kind: screenKind,
89
96
  route_path: routePath,
90
97
  capability_hints: capabilityHints
@@ -95,12 +102,32 @@ export const reactRouterUiExtractor = {
95
102
  label: routePath,
96
103
  confidence: "medium",
97
104
  sourceKind: "route_code",
98
- provenance: `${provenance}#${routePath}`,
105
+ provenance: routeProvenance,
99
106
  track: "ui",
100
107
  screen_id: screenId,
101
- entity_id: entityIdForRoute(routePath),
108
+ entity_id: entityId,
109
+ concept_id: flow?.concept_id || entityId,
102
110
  path: routePath
103
111
  }));
112
+ if (flow) {
113
+ candidates.flows.push(makeCandidateRecord({
114
+ kind: "ui_flow",
115
+ idHint: uiFlowIdForRoute(routePath, screenId),
116
+ label: `${titleCase(flow.flow_type)} Flow`,
117
+ confidence: flow.confidence,
118
+ sourceKind: "route_code",
119
+ sourceOfTruth: "candidate",
120
+ provenance: routeProvenance,
121
+ track: "ui",
122
+ flow_type: flow.flow_type,
123
+ concept_id: flow.concept_id,
124
+ screen_ids: [screenId],
125
+ route_paths: [routePath],
126
+ evidence: [routeProvenance],
127
+ missing_decisions: flow.missing_decisions,
128
+ proposed_ui_contract_additions: proposedUiContractAdditionsForFlow(routePath, screenId, screenKind)
129
+ }));
130
+ }
104
131
  if (capabilityHints.primary_action) {
105
132
  candidates.actions.push(makeCandidateRecord({
106
133
  kind: "ui_action",
@@ -133,6 +160,7 @@ export const reactRouterUiExtractor = {
133
160
  candidates.screens = dedupeCandidateRecords(candidates.screens, (record) => record.id_hint);
134
161
  candidates.routes = dedupeCandidateRecords(candidates.routes, (record) => record.id_hint);
135
162
  candidates.actions = dedupeCandidateRecords(candidates.actions, (record) => record.id_hint);
163
+ candidates.flows = dedupeCandidateRecords(candidates.flows, (record) => record.id_hint);
136
164
  candidates.stacks = [...new Set(candidates.stacks)].sort();
137
165
  return { findings, candidates };
138
166
  }
@@ -2,14 +2,17 @@ import {
2
2
  dedupeCandidateRecords,
3
3
  detectUiPresentationFeatures,
4
4
  entityIdForRoute,
5
+ inferNonResourceUiFlow,
5
6
  inferNavigationStructure,
6
7
  inferSvelteRoutes,
7
8
  makeCandidateRecord,
8
9
  navigationPatternsFromStructure,
10
+ proposedUiContractAdditionsForFlow,
9
11
  relativeTo,
10
12
  shellKindFromNavigation,
11
13
  screenIdForRoute,
12
14
  screenKindForRoute,
15
+ uiFlowIdForRoute,
13
16
  uiCapabilityHintsForRoute,
14
17
  titleCase
15
18
  } from "../../core/shared.js";
@@ -28,7 +31,7 @@ export const svelteKitUiExtractor = {
28
31
  },
29
32
  extract(context) {
30
33
  const findings = [];
31
- const candidates = { screens: [], routes: [], actions: [], stacks: [] };
34
+ const candidates = { screens: [], routes: [], actions: [], flows: [], stacks: [] };
32
35
  const roots = [
33
36
  path.join(context.paths.workspaceRoot, "apps", "web-sveltekit"),
34
37
  path.join(context.paths.workspaceRoot, "apps", "local-stack", "web")
@@ -74,16 +77,20 @@ export const svelteKitUiExtractor = {
74
77
  for (const routePath of routes) {
75
78
  const screenId = screenIdForRoute(routePath);
76
79
  const screenKind = screenKindForRoute(routePath);
77
- const capabilityHints = uiCapabilityHintsForRoute(routePath);
80
+ const flow = inferNonResourceUiFlow(routePath);
81
+ const capabilityHints = flow ? { load: null, submit: null, primary_action: null } : uiCapabilityHintsForRoute(routePath);
82
+ const entityId = flow ? null : entityIdForRoute(routePath);
83
+ const routeProvenance = `${provenance}#${routePath}`;
78
84
  candidates.screens.push(makeCandidateRecord({
79
85
  kind: "screen",
80
86
  idHint: screenId,
81
87
  label: titleCase(screenId),
82
88
  confidence: "medium",
83
89
  sourceKind: "route_code",
84
- provenance: `${provenance}#${routePath}`,
90
+ provenance: routeProvenance,
85
91
  track: "ui",
86
- entity_id: entityIdForRoute(routePath),
92
+ entity_id: entityId,
93
+ concept_id: flow?.concept_id || entityId,
87
94
  screen_kind: screenKind,
88
95
  route_path: routePath,
89
96
  capability_hints: capabilityHints
@@ -94,12 +101,32 @@ export const svelteKitUiExtractor = {
94
101
  label: routePath,
95
102
  confidence: "medium",
96
103
  sourceKind: "route_code",
97
- provenance: `${provenance}#${routePath}`,
104
+ provenance: routeProvenance,
98
105
  track: "ui",
99
106
  screen_id: screenId,
100
- entity_id: entityIdForRoute(routePath),
107
+ entity_id: entityId,
108
+ concept_id: flow?.concept_id || entityId,
101
109
  path: routePath
102
110
  }));
111
+ if (flow) {
112
+ candidates.flows.push(makeCandidateRecord({
113
+ kind: "ui_flow",
114
+ idHint: uiFlowIdForRoute(routePath, screenId),
115
+ label: `${titleCase(flow.flow_type)} Flow`,
116
+ confidence: flow.confidence,
117
+ sourceKind: "route_code",
118
+ sourceOfTruth: "candidate",
119
+ provenance: routeProvenance,
120
+ track: "ui",
121
+ flow_type: flow.flow_type,
122
+ concept_id: flow.concept_id,
123
+ screen_ids: [screenId],
124
+ route_paths: [routePath],
125
+ evidence: [routeProvenance],
126
+ missing_decisions: flow.missing_decisions,
127
+ proposed_ui_contract_additions: proposedUiContractAdditionsForFlow(routePath, screenId, screenKind)
128
+ }));
129
+ }
103
130
  }
104
131
  for (const feature of features) {
105
132
  candidates.actions.push(makeCandidateRecord({
@@ -117,6 +144,7 @@ export const svelteKitUiExtractor = {
117
144
  candidates.screens = dedupeCandidateRecords(candidates.screens, (record) => record.id_hint);
118
145
  candidates.routes = dedupeCandidateRecords(candidates.routes, (record) => record.id_hint);
119
146
  candidates.actions = dedupeCandidateRecords(candidates.actions, (record) => record.id_hint);
147
+ candidates.flows = dedupeCandidateRecords(candidates.flows, (record) => record.id_hint);
120
148
  candidates.stacks = [...new Set(candidates.stacks)].sort();
121
149
  return { findings, candidates };
122
150
  }
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  canonicalCandidateTerm,
3
3
  dedupeCandidateRecords,
4
- findImportFiles,
4
+ findPrimaryImportFiles,
5
5
  idHintify,
6
+ isPrimaryImportSource,
6
7
  makeCandidateRecord,
7
8
  relativeTo,
8
9
  titleCase
@@ -117,7 +118,7 @@ export const swiftUiExtractor = {
117
118
  id: "ui.swiftui",
118
119
  track: "ui",
119
120
  detect(context) {
120
- const files = findImportFiles(context.paths, (filePath) => /\.swift$/i.test(filePath))
121
+ const files = findPrimaryImportFiles(context.paths, (filePath) => /\.swift$/i.test(filePath) && isPrimaryImportSource(context.paths, filePath))
121
122
  .filter((filePath) => /:\s*View\b/.test(context.helpers.readTextIfExists(filePath) || ""));
122
123
  return {
123
124
  score: files.length > 0 ? 84 : 0,
@@ -125,7 +126,7 @@ export const swiftUiExtractor = {
125
126
  };
126
127
  },
127
128
  extract(context) {
128
- const files = findImportFiles(context.paths, (filePath) => /\.swift$/i.test(filePath))
129
+ const files = findPrimaryImportFiles(context.paths, (filePath) => /\.swift$/i.test(filePath) && isPrimaryImportSource(context.paths, filePath))
129
130
  .filter((filePath) => /:\s*View\b/.test(context.helpers.readTextIfExists(filePath) || ""));
130
131
  const findings = [];
131
132
  const candidates = { screens: [], routes: [], actions: [], stacks: [] };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  canonicalCandidateTerm,
3
3
  dedupeCandidateRecords,
4
- findImportFiles,
4
+ findPrimaryImportFiles,
5
5
  idHintify,
6
6
  makeCandidateRecord,
7
7
  relativeTo,
@@ -64,7 +64,7 @@ export const uiKitExtractor = {
64
64
  id: "ui.uikit",
65
65
  track: "ui",
66
66
  detect(context) {
67
- const controllerFiles = findImportFiles(context.paths, (filePath) => /\.swift$/i.test(filePath))
67
+ const controllerFiles = findPrimaryImportFiles(context.paths, (filePath) => /\.swift$/i.test(filePath))
68
68
  .filter((filePath) => !shouldIgnoreFile(filePath))
69
69
  .filter((filePath) => /(UIViewController|UITableViewController|UICollectionViewController)/.test(context.helpers.readTextIfExists(filePath) || ""));
70
70
  return {
@@ -73,7 +73,7 @@ export const uiKitExtractor = {
73
73
  };
74
74
  },
75
75
  extract(context) {
76
- const controllerFiles = findImportFiles(context.paths, (filePath) => /\.swift$/i.test(filePath))
76
+ const controllerFiles = findPrimaryImportFiles(context.paths, (filePath) => /\.swift$/i.test(filePath))
77
77
  .filter((filePath) => !shouldIgnoreFile(filePath))
78
78
  .filter((filePath) => /(UIViewController|UITableViewController|UICollectionViewController)/.test(context.helpers.readTextIfExists(filePath) || ""));
79
79
  const findings = [];
@@ -39,6 +39,7 @@ export function getOrCreateCandidateBundle(bundles, conceptId, label) {
39
39
  screens: [],
40
40
  uiRoutes: [],
41
41
  uiActions: [],
42
+ uiFlows: [],
42
43
  workflows: [],
43
44
  verifications: [],
44
45
  workflowStates: [],
@@ -337,6 +338,7 @@ export function renderCandidateBundleReadme(bundle, proposalSurfaces = []) {
337
338
  `Screens: ${bundle.screens.length}`,
338
339
  `UI routes: ${bundle.uiRoutes.length}`,
339
340
  `UI actions: ${bundle.uiActions.length}`,
341
+ `UI flows: ${(bundle.uiFlows || []).length}`,
340
342
  `Workflows: ${bundle.workflows.length}`,
341
343
  `Verifications: ${bundle.verifications.length}`,
342
344
  `Workflow states: ${bundle.workflowStates.length}`,
@@ -455,6 +457,16 @@ export function renderCandidateBundleReadme(bundle, proposalSurfaces = []) {
455
457
  lines.push(` - label ${candidate.label}`);
456
458
  lines.push(` - kind ${candidate.kind}`);
457
459
  lines.push(` - why matched ${candidate.match_reasons.length ? candidate.match_reasons.join("; ") : "dependency overlap with maintained seam evidence"}`);
460
+ lines.push(` - missing decisions ${(candidate.missing_decisions || []).length ? candidate.missing_decisions.join("; ") : "none"}`);
461
+ lines.push(` - project config target \`${candidate.project_config_target?.path || "topology.runtimes[].migration"}\``);
462
+ lines.push(` - evidence ${(candidate.evidence || []).slice(0, 3).map((/** @type {string} */ item) => `\`${item}\``).join(", ") || "_none_"}`);
463
+ lines.push(" - proposed runtime migration");
464
+ lines.push(" ```json");
465
+ lines.push(` ${JSON.stringify(candidate.proposed_runtime_migration || {}, null, 2).replace(/\n/g, "\n ")}`);
466
+ lines.push(" ```");
467
+ for (const step of candidate.manual_next_steps || []) {
468
+ lines.push(` - manual next: ${step}`);
469
+ }
458
470
  }
459
471
  }
460
472
  }
@@ -554,6 +566,13 @@ export function renderCandidateBundleReadme(bundle, proposalSurfaces = []) {
554
566
  lines.push(`- \`${entry.id_hint}\` ${entry.screen_kind} at \`${entry.route_path}\``);
555
567
  }
556
568
  }
569
+ if ((bundle.uiFlows || []).length > 0) {
570
+ lines.push("", "## UI Flow Evidence", "");
571
+ for (const entry of bundle.uiFlows) {
572
+ lines.push(`- \`${entry.id_hint}\` ${entry.flow_type || "flow"} routes ${(entry.route_paths || []).map((/** @type {string} */ item) => `\`${item}\``).join(", ") || "_none_"}`);
573
+ lines.push(` - missing decisions ${(entry.missing_decisions || []).length ? entry.missing_decisions.join("; ") : "none"}`);
574
+ }
575
+ }
557
576
  if (bundle.workflows.length > 0) {
558
577
  lines.push("", "## Workflow Evidence", "");
559
578
  for (const entry of bundle.workflows) {
@@ -578,7 +597,7 @@ export function renderMaintainedSeamCandidatesInline(bundle) {
578
597
  return entries
579
598
  .map((/** @type {any} */ surface) => {
580
599
  const seams = (surface.maintained_seam_candidates || [])
581
- .map((/** @type {any} */ candidate) => `\`${candidate.seam_id}\` (${candidate.status}, ${candidate.ownership_class}, confidence=${candidate.confidence})`)
600
+ .map((/** @type {any} */ candidate) => `\`${candidate.seam_id}\` (${candidate.status}, ${candidate.ownership_class}, confidence=${candidate.confidence}, missing decisions=${(candidate.missing_decisions || []).length})`)
582
601
  .join(", ");
583
602
  return `${surface.id}: ${seams}`;
584
603
  })
@@ -22,6 +22,7 @@ import {
22
22
  renderCandidateEntity,
23
23
  renderCandidateEnum,
24
24
  renderCandidateShape,
25
+ renderCandidateUiFlowDoc,
25
26
  renderCandidateUiReportDoc,
26
27
  renderCandidateVerification,
27
28
  renderCandidateWidget,
@@ -83,8 +84,9 @@ export function bestContextBundleForCandidate(bundles, candidate) {
83
84
  export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
84
85
  const dbCandidates = appImport.candidates.db || { entities: [], enums: [], maintained_seams: [] };
85
86
  const apiCandidates = appImport.candidates.api || { capabilities: [] };
86
- const uiCandidates = appImport.candidates.ui || { screens: [], routes: [], actions: [], widgets: [], shapes: [] };
87
+ const uiCandidates = appImport.candidates.ui || { screens: [], routes: [], actions: [], flows: [], widgets: [], shapes: [] };
87
88
  const uiWidgetCandidates = uiCandidates.widgets || uiCandidates.components || [];
89
+ const uiFlowCandidates = uiCandidates.flows || [];
88
90
  const uiShapeCandidates = uiCandidates.shapes || [];
89
91
  const uiShapeCandidatesById = new Map(uiShapeCandidates.map((/** @type {any} */ shape) => [shape.id || shape.id_hint, shape]));
90
92
  const workflowCandidates = appImport.candidates.workflows || { workflows: [], workflow_states: [], workflow_transitions: [] };
@@ -209,6 +211,11 @@ export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
209
211
  const bundle = getOrCreateCandidateBundle(bundles, conceptId, bundleLabelFromConceptId(conceptId || entry.screen_id || entry.id_hint));
210
212
  bundle.uiActions.push(entry);
211
213
  }
214
+ for (const entry of uiFlowCandidates) {
215
+ const conceptId = entry.concept_id || `flow_${canonicalCandidateTerm(entry.flow_type || entry.id_hint)}`;
216
+ const bundle = getOrCreateCandidateBundle(bundles, conceptId, bundleLabelFromConceptId(conceptId || entry.id_hint));
217
+ bundle.uiFlows.push(entry);
218
+ }
212
219
  /** @param {WorkflowRecord} entry @returns {any} */
213
220
  function widgetConceptId(entry) {
214
221
  if (entry.entity_id || entry.concept_id) {
@@ -334,6 +341,7 @@ export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
334
341
  bundle.screens.length > 0 ||
335
342
  bundle.uiRoutes.length > 0 ||
336
343
  bundle.uiActions.length > 0 ||
344
+ bundle.uiFlows.length > 0 ||
337
345
  bundle.workflows.length > 0 ||
338
346
  bundle.verifications.length > 0 ||
339
347
  bundle.workflowStates.length > 0 ||
@@ -354,6 +362,7 @@ export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
354
362
  screens: bundle.screens.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
355
363
  uiRoutes: bundle.uiRoutes.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
356
364
  uiActions: bundle.uiActions.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
365
+ uiFlows: bundle.uiFlows.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
357
366
  workflows: bundle.workflows.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
358
367
  verifications: bundle.verifications.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
359
368
  workflowStates: bundle.workflowStates.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
@@ -499,6 +508,9 @@ export function buildCandidateModelFiles(graph, appImport, topogramRoot) {
499
508
  const actions = bundle.uiActions.filter((/** @type {any} */ action) => action.screen_id === screen.id_hint);
500
509
  files[`${bundleRoot}/docs/reports/ui-${screen.id_hint}.md`] = renderCandidateUiReportDoc(screen, routes, actions);
501
510
  }
511
+ for (const flow of bundle.uiFlows || []) {
512
+ files[`${bundleRoot}/docs/reports/ui-flow-${flow.id_hint}.md`] = renderCandidateUiFlowDoc(flow);
513
+ }
502
514
  for (const patch of bundle.projectionPatches || []) {
503
515
  files[`${bundleRoot}/${patch.patch_rel_path}`] = renderProjectionPatchDoc(patch);
504
516
  }
@@ -156,6 +156,19 @@ export function buildBundleAdoptionPlan(bundle, canonicalShapeIndex) {
156
156
  canonical_rel_path: `docs/reports/ui-${screen.id_hint}.md`
157
157
  });
158
158
  }
159
+ for (const flow of bundle.uiFlows || []) {
160
+ steps.push({
161
+ action: "promote_ui_flow_report",
162
+ item: `ui_flow_${flow.id_hint}`,
163
+ target: null,
164
+ confidence: flow.confidence || "medium",
165
+ inference_summary: `Review non-resource UI flow candidate ${flow.id_hint}.`,
166
+ source_kind: flow.source_kind || "route_code",
167
+ source_path: `candidates/reconcile/model/bundles/${bundle.slug}/docs/reports/ui-flow-${flow.id_hint}.md`,
168
+ canonical_rel_path: `docs/reports/ui-flow-${flow.id_hint}.md`,
169
+ track: "ui"
170
+ });
171
+ }
159
172
  for (const patch of bundle.projectionPatches || []) {
160
173
  for (const hint of patch.missing_auth_permissions || []) {
161
174
  steps.push({
@@ -283,6 +283,39 @@ export function renderCandidateUiReportDoc(screen, routes, actions) {
283
283
  return renderMarkdownDoc(metadata, body);
284
284
  }
285
285
 
286
+ /** @param {WorkflowRecord} flow @returns {any} */
287
+ export function renderCandidateUiFlowDoc(flow) {
288
+ /** @type {WorkflowRecord} */
289
+ const metadata = {
290
+ id: `ui_flow_${flow.id_hint}`,
291
+ kind: "report",
292
+ title: `${flow.label || flow.id_hint} Review`,
293
+ status: "inferred",
294
+ source_of_truth: "imported",
295
+ confidence: flow.confidence || "medium",
296
+ review_required: true,
297
+ provenance: flow.provenance || flow.evidence || [],
298
+ tags: ["import", "ui", "flow"]
299
+ };
300
+ const body = [
301
+ "Candidate non-resource UI flow imported from brownfield route evidence.",
302
+ "",
303
+ `Flow: \`${flow.id_hint}\` (${flow.flow_type || "unknown"})`,
304
+ `Screens: ${(flow.screen_ids || []).map((/** @type {string} */ item) => `\`${item}\``).join(", ") || "_none_"}`,
305
+ `Routes: ${(flow.route_paths || []).map((/** @type {string} */ item) => `\`${item}\``).join(", ") || "_none_"}`,
306
+ `Missing decisions: ${(flow.missing_decisions || []).length ? flow.missing_decisions.join("; ") : "none"}`,
307
+ "",
308
+ "Proposed UI contract additions:",
309
+ "",
310
+ "```json",
311
+ JSON.stringify(flow.proposed_ui_contract_additions || {}, null, 2),
312
+ "```",
313
+ "",
314
+ "Review this flow before promoting it into shared UI contract behavior."
315
+ ].join("\n");
316
+ return renderMarkdownDoc(metadata, body);
317
+ }
318
+
286
319
  /** @param {WorkflowRecord} widget @returns {any} */
287
320
  export function renderCandidateWidget(widget) {
288
321
  const propName = widget.data_prop || "rows";
@@ -262,6 +262,7 @@ export function reconcileWorkflow(inputPath, options = {}) {
262
262
  widgets: bundle.widgets.map((/** @type {any} */ entry) => entry.id_hint),
263
263
  cli_surfaces: (bundle.cliSurfaces || []).map((/** @type {any} */ entry) => entry.id_hint),
264
264
  screens: bundle.screens.map((/** @type {any} */ entry) => entry.id_hint),
265
+ ui_flows: (bundle.uiFlows || []).map((/** @type {any} */ entry) => entry.id_hint),
265
266
  workflows: bundle.workflows.map((/** @type {any} */ entry) => entry.id_hint),
266
267
  docs: bundle.docs.map((/** @type {any} */ entry) => entry.id),
267
268
  maintained_seam_candidates: (agentAdoptionPlan.imported_proposal_surfaces || [])
@@ -284,7 +285,7 @@ export function reconcileWorkflow(inputPath, options = {}) {
284
285
  : "## Promoted Canonical Items";
285
286
  files["candidates/reconcile/report.json"] = `${stableStringify(report)}\n`;
286
287
  const candidateModelBundlesMarkdown = report.candidate_model_bundles.length
287
- ? report.candidate_model_bundles.map((/** @type {any} */ bundle) => `- \`${bundle.slug}\` (${bundle.actors.length} actors, ${bundle.roles.length} roles, ${bundle.entities.length} entities, ${bundle.enums.length} enums, ${bundle.capabilities.length} capabilities, ${bundle.shapes.length} shapes, ${bundle.widgets.length} widgets, ${bundle.cli_surfaces.length} CLI surfaces, ${bundle.screens.length} screens, ${bundle.workflows.length} workflows, ${bundle.docs.length} docs)
288
+ ? report.candidate_model_bundles.map((/** @type {any} */ bundle) => `- \`${bundle.slug}\` (${bundle.actors.length} actors, ${bundle.roles.length} roles, ${bundle.entities.length} entities, ${bundle.enums.length} enums, ${bundle.capabilities.length} capabilities, ${bundle.shapes.length} shapes, ${bundle.widgets.length} widgets, ${bundle.cli_surfaces.length} CLI surfaces, ${bundle.screens.length} screens, ${(bundle.ui_flows || []).length} UI flows, ${bundle.workflows.length} workflows, ${bundle.docs.length} docs)
288
289
  - primary concept \`${bundle.operator_summary.primaryConcept}\`${bundle.operator_summary.primaryEntityId ? `, primary entity \`${bundle.operator_summary.primaryEntityId}\`` : ""}
289
290
  - participants ${bundle.operator_summary.participants.label}
290
291
  - main capabilities ${summarizeBundleSurface(bundle, bundle.operator_summary.capabilityIds)}