@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.
- package/package.json +1 -1
- package/src/adoption/plan/index.js +12 -1
- package/src/cli/commands/import/workspace.js +1 -0
- package/src/cli.js +3 -3
- package/src/import/core/runner/candidates.js +2 -0
- package/src/import/core/runner/reports.js +12 -5
- package/src/import/core/runner/tracks.js +1 -1
- package/src/import/core/shared/files.js +76 -1
- package/src/import/core/shared/next-app.js +2 -2
- package/src/import/core/shared/ui-routes.js +106 -0
- package/src/import/core/shared.js +8 -1
- package/src/import/enrichers/django-rest.js +4 -4
- package/src/import/enrichers/rails-controllers.js +3 -3
- package/src/import/enrichers/rails-models.js +3 -3
- package/src/import/extractors/api/aspnet-core.js +5 -5
- package/src/import/extractors/api/django-routes.js +5 -5
- package/src/import/extractors/api/express.js +4 -4
- package/src/import/extractors/api/fastify.js +7 -7
- package/src/import/extractors/api/flutter-dio.js +4 -4
- package/src/import/extractors/api/generic-route-fallback.js +3 -2
- package/src/import/extractors/api/graphql-code-first.js +3 -3
- package/src/import/extractors/api/graphql-sdl.js +5 -5
- package/src/import/extractors/api/jaxrs.js +3 -3
- package/src/import/extractors/api/micronaut.js +3 -3
- package/src/import/extractors/api/openapi-code.js +4 -4
- package/src/import/extractors/api/openapi.js +3 -3
- package/src/import/extractors/api/rails-routes.js +3 -3
- package/src/import/extractors/api/react-native-repository.js +3 -3
- package/src/import/extractors/api/retrofit.js +3 -3
- package/src/import/extractors/api/spring-web.js +3 -3
- package/src/import/extractors/api/swift-webapi.js +3 -3
- package/src/import/extractors/api/trpc.js +4 -4
- package/src/import/extractors/cli/generic.js +5 -4
- package/src/import/extractors/db/django-models.js +4 -4
- package/src/import/extractors/db/dotnet-models.js +4 -4
- package/src/import/extractors/db/drizzle.js +21 -9
- package/src/import/extractors/db/ef-core.js +5 -5
- package/src/import/extractors/db/flutter-entities.js +3 -3
- package/src/import/extractors/db/jpa.js +3 -3
- package/src/import/extractors/db/liquibase.js +3 -3
- package/src/import/extractors/db/maintained-seams.js +27 -4
- package/src/import/extractors/db/mybatis-xml.js +4 -4
- package/src/import/extractors/db/prisma.js +10 -3
- package/src/import/extractors/db/rails-schema.js +3 -3
- package/src/import/extractors/db/react-native-entities.js +3 -3
- package/src/import/extractors/db/room.js +5 -5
- package/src/import/extractors/db/snapshot.js +3 -3
- package/src/import/extractors/db/sql.js +3 -3
- package/src/import/extractors/db/swiftdata.js +3 -3
- package/src/import/extractors/ui/android-compose.js +4 -4
- package/src/import/extractors/ui/backend-only.js +3 -3
- package/src/import/extractors/ui/blazor.js +3 -3
- package/src/import/extractors/ui/flutter-screens.js +3 -3
- package/src/import/extractors/ui/maui-xaml.js +4 -4
- package/src/import/extractors/ui/next-app-router.js +26 -5
- package/src/import/extractors/ui/next-pages-router.js +34 -10
- package/src/import/extractors/ui/razor-pages.js +3 -3
- package/src/import/extractors/ui/react-native-screens.js +4 -4
- package/src/import/extractors/ui/react-router.js +34 -6
- package/src/import/extractors/ui/sveltekit.js +34 -6
- package/src/import/extractors/ui/swiftui.js +4 -3
- package/src/import/extractors/ui/uikit.js +3 -3
- package/src/workflows/reconcile/bundle-core/index.js +20 -1
- package/src/workflows/reconcile/candidate-model.js +13 -1
- package/src/workflows/reconcile/impacts/adoption-plan.js +13 -0
- package/src/workflows/reconcile/renderers.js +33 -0
- 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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
133
|
+
const screenFiles = findPrimaryImportFiles(
|
|
134
134
|
context.paths,
|
|
135
135
|
(filePath) => /\/src\/.+\/presentation\/screens\/.+Screen\.tsx$/i.test(filePath)
|
|
136
136
|
);
|
|
137
|
-
const navigatorFile =
|
|
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
|
|
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:
|
|
91
|
+
provenance: routeProvenance,
|
|
86
92
|
track: "ui",
|
|
87
|
-
entity_id:
|
|
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:
|
|
105
|
+
provenance: routeProvenance,
|
|
99
106
|
track: "ui",
|
|
100
107
|
screen_id: screenId,
|
|
101
|
-
entity_id:
|
|
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
|
|
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:
|
|
90
|
+
provenance: routeProvenance,
|
|
85
91
|
track: "ui",
|
|
86
|
-
entity_id:
|
|
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:
|
|
104
|
+
provenance: routeProvenance,
|
|
98
105
|
track: "ui",
|
|
99
106
|
screen_id: screenId,
|
|
100
|
-
entity_id:
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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)}
|