@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
package/package.json
CHANGED
|
@@ -741,7 +741,18 @@ export function buildAgentAdoptionPlan(adoptionPlan, maintainedBoundaryArtifact
|
|
|
741
741
|
mapping_suggestions: [{
|
|
742
742
|
type: "manual_project_config_review",
|
|
743
743
|
id: seam.id_hint || seam.seam_id,
|
|
744
|
-
reason: "Review the inferred database migration strategy before editing topogram.project.json."
|
|
744
|
+
reason: "Review the inferred database migration strategy before editing topogram.project.json.",
|
|
745
|
+
project_config_target: seam.project_config_target || {
|
|
746
|
+
file: "topogram.project.json",
|
|
747
|
+
path: "topology.runtimes[].migration"
|
|
748
|
+
},
|
|
749
|
+
proposed_runtime_migration: seam.proposed_runtime_migration || null,
|
|
750
|
+
missing_decisions: seam.missing_decisions || [],
|
|
751
|
+
evidence: seam.evidence || seam.provenance || [],
|
|
752
|
+
manual_next_steps: seam.manual_next_steps || [
|
|
753
|
+
"Review evidence and missing decisions.",
|
|
754
|
+
"Manually copy the accepted migration strategy into topogram.project.json."
|
|
755
|
+
]
|
|
745
756
|
}],
|
|
746
757
|
available_actions: ADOPTION_STATE_VOCABULARY
|
|
747
758
|
}));
|
|
@@ -115,6 +115,7 @@ export function importCandidateCounts(summary) {
|
|
|
115
115
|
apiRoutes: candidates.api?.routes?.length || 0,
|
|
116
116
|
uiScreens: candidates.ui?.screens?.length || 0,
|
|
117
117
|
uiRoutes: candidates.ui?.routes?.length || 0,
|
|
118
|
+
uiFlows: candidates.ui?.flows?.length || 0,
|
|
118
119
|
uiWidgets: candidates.ui?.widgets?.length || candidates.ui?.components?.length || 0,
|
|
119
120
|
uiShapes: candidates.ui?.shapes?.length || 0,
|
|
120
121
|
cliCommands: candidates.cli?.commands?.length || 0,
|
package/src/cli.js
CHANGED
|
@@ -63,18 +63,18 @@ const inputPath = commandArgs && Object.prototype.hasOwnProperty.call(commandArg
|
|
|
63
63
|
const cliOptions = parseCliOptions(args, commandArgs);
|
|
64
64
|
|
|
65
65
|
try {
|
|
66
|
-
process.
|
|
66
|
+
process.exitCode = await runCliDispatch({
|
|
67
67
|
args,
|
|
68
68
|
commandArgs,
|
|
69
69
|
inputPath,
|
|
70
70
|
cliOptions,
|
|
71
71
|
executablePath: path.resolve(process.argv[1] || fileURLToPath(import.meta.url))
|
|
72
|
-
})
|
|
72
|
+
});
|
|
73
73
|
} catch (error) {
|
|
74
74
|
if (error.validation) {
|
|
75
75
|
console.error(formatValidationErrors(error.validation));
|
|
76
76
|
} else {
|
|
77
77
|
console.error(error.message);
|
|
78
78
|
}
|
|
79
|
-
process.
|
|
79
|
+
process.exitCode = 1;
|
|
80
80
|
}
|
|
@@ -284,6 +284,7 @@ export function normalizeCandidatesForTrack(track, candidates) {
|
|
|
284
284
|
screens: dedupeCandidateRecords(candidates.screens || [], idHint),
|
|
285
285
|
routes: dedupeCandidateRecords(candidates.routes || [], idHint),
|
|
286
286
|
actions: dedupeCandidateRecords(candidates.actions || [], idHint),
|
|
287
|
+
flows: dedupeCandidateRecords(candidates.flows || [], idHint),
|
|
287
288
|
widgets,
|
|
288
289
|
shapes: dedupeCandidateRecords([...(candidates.shapes || []), ...eventShapes], idHint),
|
|
289
290
|
stacks: [...new Set(candidates.stacks || [])].sort()
|
|
@@ -330,6 +331,7 @@ export function enrichUiWidgetDataSources(uiCandidates, allCandidates) {
|
|
|
330
331
|
const { components, ...canonicalCandidates } = uiCandidates;
|
|
331
332
|
return {
|
|
332
333
|
...canonicalCandidates,
|
|
334
|
+
flows: dedupeCandidateRecords(canonicalCandidates.flows || [], (/** @type {any} */ flow) => flow.id_hint),
|
|
333
335
|
widgets: widgets.map((widget) => {
|
|
334
336
|
const dataSource = inferredDataSourceForWidget(widget, allCandidates);
|
|
335
337
|
const dataProp = widget.data_prop || "rows";
|
|
@@ -10,9 +10,12 @@ import { uiWidgetCandidates } from "./candidates.js";
|
|
|
10
10
|
*/
|
|
11
11
|
export function reportMarkdown(track, candidates) {
|
|
12
12
|
if (track === "db") {
|
|
13
|
-
const seamLines = (candidates.maintained_seams || []).
|
|
14
|
-
`- \`${seam.id_hint}\` tool \`${seam.tool}\` confidence ${seam.confidence || "unknown"} schema \`${seam.schemaPath || "none"}\` migrations \`${seam.migrationsPath || "review-required"}\` missing decisions ${(seam.missing_decisions || []).length}
|
|
15
|
-
|
|
13
|
+
const seamLines = (candidates.maintained_seams || []).flatMap((/** @type {any} */ seam) => [
|
|
14
|
+
`- \`${seam.id_hint}\` tool \`${seam.tool}\` confidence ${seam.confidence || "unknown"} schema \`${seam.schemaPath || "none"}\` migrations \`${seam.migrationsPath || "review-required"}\` missing decisions ${(seam.missing_decisions || []).length}`,
|
|
15
|
+
` - project config target: \`${seam.project_config_target?.path || "topology.runtimes[].migration"}\``,
|
|
16
|
+
` - evidence: ${(seam.evidence || []).slice(0, 3).map((/** @type {string} */ item) => `\`${item}\``).join(", ") || "_none_"}`,
|
|
17
|
+
` - manual next: ${(seam.manual_next_steps || []).slice(0, 2).join(" ")}`
|
|
18
|
+
]);
|
|
16
19
|
return ensureTrailingNewline(
|
|
17
20
|
`# DB Import Report\n\n- Entities: ${candidates.entities.length}\n- Enums: ${candidates.enums.length}\n- Relations: ${candidates.relations.length}\n- Indexes: ${candidates.indexes.length}\n- Maintained DB migration seams: ${(candidates.maintained_seams || []).length}\n\n## Maintained DB Migration Seam Candidates\n\n${seamLines.length ? seamLines.join("\n") : "- none"}\n`
|
|
18
21
|
);
|
|
@@ -25,11 +28,15 @@ export function reportMarkdown(track, candidates) {
|
|
|
25
28
|
if (track === "ui") {
|
|
26
29
|
const widgets = uiWidgetCandidates(candidates);
|
|
27
30
|
const shapes = candidates.shapes || [];
|
|
31
|
+
const flows = candidates.flows || [];
|
|
28
32
|
const widgetLines = widgets.map((widget) =>
|
|
29
33
|
`- \`${widget.id_hint}\` confidence ${widget.confidence || "unknown"} pattern \`${widget.pattern || widget.inferred_pattern || "unknown"}\` region \`${widget.region || widget.inferred_region || "unknown"}\` events ${(widget.inferred_events || []).length} evidence ${(widget.evidence || widget.provenance || []).length} missing decisions ${(widget.missing_decisions || []).length}`
|
|
30
34
|
);
|
|
35
|
+
const flowLines = flows.map((/** @type {any} */ flow) =>
|
|
36
|
+
`- \`${flow.id_hint}\` type \`${flow.flow_type}\` confidence ${flow.confidence || "unknown"} routes ${(flow.route_paths || []).map((/** @type {string} */ route) => `\`${route}\``).join(", ") || "_none_"} missing decisions ${(flow.missing_decisions || []).length}`
|
|
37
|
+
);
|
|
31
38
|
return ensureTrailingNewline(
|
|
32
|
-
`# UI Import Report\n\n- Screens: ${candidates.screens.length}\n- Routes: ${candidates.routes.length}\n- Actions: ${candidates.actions.length}\n- Widgets: ${widgets.length}\n- Event payload shapes: ${shapes.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n\n## Widget Candidates\n\n${widgetLines.length ? widgetLines.join("\n") : "- none"}\n\n## Next Validation\n\n- Review candidates under \`topo/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`
|
|
39
|
+
`# UI Import Report\n\n- Screens: ${candidates.screens.length}\n- Routes: ${candidates.routes.length}\n- Actions: ${candidates.actions.length}\n- Flow candidates: ${flows.length}\n- Widgets: ${widgets.length}\n- Event payload shapes: ${shapes.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n\n## Flow Candidates\n\n${flowLines.length ? flowLines.join("\n") : "- none"}\n\n## Widget Candidates\n\n${widgetLines.length ? widgetLines.join("\n") : "- none"}\n\n## Next Validation\n\n- Review flow candidates in \`topo/candidates/app/ui/candidates.json\` before adding shared UI contract behavior.\n- Review candidates under \`topo/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`
|
|
33
40
|
);
|
|
34
41
|
}
|
|
35
42
|
if (track === "cli") {
|
|
@@ -57,6 +64,6 @@ export function reportMarkdown(track, candidates) {
|
|
|
57
64
|
*/
|
|
58
65
|
export function appReportMarkdown(candidates, tracks) {
|
|
59
66
|
return ensureTrailingNewline(
|
|
60
|
-
`# 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- Maintained DB migration seams: ${candidates.db?.maintained_seams?.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: ${uiWidgetCandidates(candidates.ui).length}\n- Event payload shapes: ${candidates.ui?.shapes?.length || 0}\n- Stacks: ${candidates.ui?.stacks?.length ? candidates.ui.stacks.join(", ") : "none"}\n\n## CLI\n\n- Commands: ${candidates.cli?.commands?.length || 0}\n- Capabilities: ${candidates.cli?.capabilities?.length || 0}\n- CLI surfaces: ${candidates.cli?.surfaces?.length || 0}\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`
|
|
67
|
+
`# 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- Maintained DB migration seams: ${candidates.db?.maintained_seams?.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- Flow candidates: ${candidates.ui?.flows?.length || 0}\n- Widgets: ${uiWidgetCandidates(candidates.ui).length}\n- Event payload shapes: ${candidates.ui?.shapes?.length || 0}\n- Stacks: ${candidates.ui?.stacks?.length ? candidates.ui.stacks.join(", ") : "none"}\n\n## CLI\n\n- Commands: ${candidates.cli?.commands?.length || 0}\n- Capabilities: ${candidates.cli?.capabilities?.length || 0}\n- CLI surfaces: ${candidates.cli?.surfaces?.length || 0}\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`
|
|
61
68
|
);
|
|
62
69
|
}
|
|
@@ -102,7 +102,7 @@ function initialCandidatesForTrack(track) {
|
|
|
102
102
|
return { capabilities: [], routes: [], stacks: [] };
|
|
103
103
|
}
|
|
104
104
|
if (track === "ui") {
|
|
105
|
-
return { screens: [], routes: [], actions: [], stacks: [] };
|
|
105
|
+
return { screens: [], routes: [], actions: [], flows: [], stacks: [] };
|
|
106
106
|
}
|
|
107
107
|
if (track === "cli") {
|
|
108
108
|
return { commands: [], capabilities: [], surfaces: [] };
|
|
@@ -6,12 +6,17 @@ import { relativeTo } from "../../../path-helpers.js";
|
|
|
6
6
|
export const DEFAULT_IGNORED_DIRS = new Set([
|
|
7
7
|
".git",
|
|
8
8
|
".next",
|
|
9
|
+
".tmp",
|
|
9
10
|
".turbo",
|
|
10
11
|
".yarn",
|
|
12
|
+
".cache",
|
|
13
|
+
".parcel-cache",
|
|
14
|
+
".svelte-kit",
|
|
11
15
|
"build",
|
|
12
16
|
"coverage",
|
|
13
17
|
"dist",
|
|
14
18
|
"node_modules",
|
|
19
|
+
"out",
|
|
15
20
|
"tmp"
|
|
16
21
|
]);
|
|
17
22
|
|
|
@@ -82,6 +87,54 @@ export function normalizeImportRelativePath(paths, filePath) {
|
|
|
82
87
|
return relativeTo(paths.repoRoot, filePath);
|
|
83
88
|
}
|
|
84
89
|
|
|
90
|
+
/**
|
|
91
|
+
* @param {import("./types.d.ts").ImportPaths} paths
|
|
92
|
+
* @param {string} filePath
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
export function importSourcePathRelativeToWorkspace(paths, filePath) {
|
|
96
|
+
return relativeTo(paths.workspaceRoot, filePath).replaceAll(path.sep, "/");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {import("./types.d.ts").ImportPaths} paths
|
|
101
|
+
* @param {string} filePath
|
|
102
|
+
* @returns {"runtime_source"|"parser_config"|"docs"|"tests"|"fixtures"|"generated_output"}
|
|
103
|
+
*/
|
|
104
|
+
export function classifyImportSourcePath(paths, filePath) {
|
|
105
|
+
const relativePath = importSourcePathRelativeToWorkspace(paths, filePath);
|
|
106
|
+
const basename = path.basename(relativePath).toLowerCase();
|
|
107
|
+
if (/(^|\/)(\.tmp|tmp|temp|dist|build|coverage|out|generated|docs-generated|snapshots?)(\/|$)/i.test(relativePath)) {
|
|
108
|
+
return "generated_output";
|
|
109
|
+
}
|
|
110
|
+
if (/^(template|templates|[^/]+-templates|[^/]+_templates)(\/|$)/i.test(relativePath)) {
|
|
111
|
+
return "fixtures";
|
|
112
|
+
}
|
|
113
|
+
if (/(^|\/)(test|tests|__tests__|spec|specs|mocks?)(\/|$)|\.(test|spec)\.(js|jsx|ts|tsx|mjs|cjs)$/i.test(relativePath)) {
|
|
114
|
+
return "tests";
|
|
115
|
+
}
|
|
116
|
+
if (/(^|\/)(fixture|fixtures|examples?)(\/|$)/i.test(relativePath)) {
|
|
117
|
+
return "fixtures";
|
|
118
|
+
}
|
|
119
|
+
if (/(^|\/)(doc|docs|documentation|guides?)(\/|$)|^(readme|changelog|contributing|license)(\.[a-z0-9]+)?$/i.test(relativePath) || /^(readme|changelog|contributing|license)(\.[a-z0-9]+)?$/i.test(basename)) {
|
|
120
|
+
return "docs";
|
|
121
|
+
}
|
|
122
|
+
if (/(^|\/)(package\.json|openapi\.(json|ya?ml)|swagger\.(json|ya?ml)|drizzle\.config\.(ts|js|mjs|cjs)|tsconfig\.json)$/i.test(relativePath)) {
|
|
123
|
+
return "parser_config";
|
|
124
|
+
}
|
|
125
|
+
return "runtime_source";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {import("./types.d.ts").ImportPaths} paths
|
|
130
|
+
* @param {string} filePath
|
|
131
|
+
* @returns {boolean}
|
|
132
|
+
*/
|
|
133
|
+
export function isPrimaryImportSource(paths, filePath) {
|
|
134
|
+
const sourceType = classifyImportSourcePath(paths, filePath);
|
|
135
|
+
return sourceType === "runtime_source" || sourceType === "parser_config";
|
|
136
|
+
}
|
|
137
|
+
|
|
85
138
|
/**
|
|
86
139
|
* @param {import("./types.d.ts").ImportPaths} paths
|
|
87
140
|
* @param {string} filePath
|
|
@@ -130,6 +183,9 @@ export function canonicalSourceRank(paths, filePath, kind) {
|
|
|
130
183
|
rank += penalty.weight;
|
|
131
184
|
}
|
|
132
185
|
}
|
|
186
|
+
if (!isPrimaryImportSource(paths, filePath)) {
|
|
187
|
+
rank += 1000;
|
|
188
|
+
}
|
|
133
189
|
return rank;
|
|
134
190
|
}
|
|
135
191
|
|
|
@@ -157,9 +213,10 @@ export function selectPreferredImportFiles(paths, files, kind) {
|
|
|
157
213
|
/**
|
|
158
214
|
* @param {import("./types.d.ts").ImportPaths} paths
|
|
159
215
|
* @param {any} predicate
|
|
216
|
+
* @param {{ primaryOnly?: boolean }} [options]
|
|
160
217
|
* @returns {any}
|
|
161
218
|
*/
|
|
162
|
-
export function findImportFiles(paths, predicate) {
|
|
219
|
+
export function findImportFiles(paths, predicate, options = {}) {
|
|
163
220
|
const files = new Set();
|
|
164
221
|
for (const rootDir of importSearchRoots(paths)) {
|
|
165
222
|
for (const filePath of listFilesRecursive(rootDir, predicate)) {
|
|
@@ -170,8 +227,26 @@ export function findImportFiles(paths, predicate) {
|
|
|
170
227
|
) {
|
|
171
228
|
continue;
|
|
172
229
|
}
|
|
230
|
+
if (options.primaryOnly && !isPrimaryImportSource(paths, filePath)) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
173
233
|
files.add(filePath);
|
|
174
234
|
}
|
|
175
235
|
}
|
|
176
236
|
return [...files].sort();
|
|
177
237
|
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Find files that are eligible to create primary import candidates.
|
|
241
|
+
*
|
|
242
|
+
* Docs, tests, fixture/template roots, generated output, and cache output may
|
|
243
|
+
* still support evidence elsewhere, but candidate-producing extractors should
|
|
244
|
+
* start from this helper so the primary-source rule is one API boundary.
|
|
245
|
+
*
|
|
246
|
+
* @param {import("./types.d.ts").ImportPaths} paths
|
|
247
|
+
* @param {any} predicate
|
|
248
|
+
* @returns {any}
|
|
249
|
+
*/
|
|
250
|
+
export function findPrimaryImportFiles(paths, predicate) {
|
|
251
|
+
return findImportFiles(paths, predicate, { primaryOnly: true });
|
|
252
|
+
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
normalizeEndpointPathForMatch,
|
|
11
11
|
normalizeOpenApiPath
|
|
12
12
|
} from "./api-routes.js";
|
|
13
|
-
import { entityIdForRoute, screenIdForRoute, screenKindForRoute, uiCapabilityHintsForRoute } from "./ui-routes.js";
|
|
13
|
+
import { entityIdForRoute, inferNonResourceUiFlow, screenIdForRoute, screenKindForRoute, uiCapabilityHintsForRoute } from "./ui-routes.js";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* @param {string} rootDir
|
|
@@ -48,7 +48,7 @@ export function inferNextAppRoutes(rootDir) {
|
|
|
48
48
|
*/
|
|
49
49
|
export function nextScreenKindForRoute(routePath) {
|
|
50
50
|
const normalized = String(routePath || "");
|
|
51
|
-
if (/\/(login|register|setup)$/.test(normalized)) return "flow";
|
|
51
|
+
if (inferNonResourceUiFlow(normalized) || /\/(login|register|setup)$/.test(normalized)) return "flow";
|
|
52
52
|
return screenKindForRoute(routePath);
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -5,6 +5,51 @@ import { relativeTo } from "../../../path-helpers.js";
|
|
|
5
5
|
import { canonicalCandidateTerm } from "./candidates.js";
|
|
6
6
|
import { listFilesRecursive, readTextIfExists } from "./files.js";
|
|
7
7
|
|
|
8
|
+
const NON_RESOURCE_FLOW_DEFINITIONS = [
|
|
9
|
+
{
|
|
10
|
+
flow_type: "auth",
|
|
11
|
+
concept_id: "flow_auth",
|
|
12
|
+
pattern: /\/(login|log-in|signin|sign-in|sign_in|logout|log-out|register|signup|sign-up|forgot-password|reset-password|password-reset)(\/|$)/i,
|
|
13
|
+
confidence: "medium",
|
|
14
|
+
missing_decisions: ["confirm auth provider and session lifecycle", "confirm success and failure destinations"]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
flow_type: "onboarding_wizard",
|
|
18
|
+
concept_id: "flow_onboarding",
|
|
19
|
+
pattern: /\/(onboarding|setup|welcome|get-started|getting-started|wizard|stepper)(\/|$)/i,
|
|
20
|
+
confidence: "medium",
|
|
21
|
+
missing_decisions: ["confirm step order and completion criteria", "confirm resumability and exit behavior"]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
flow_type: "settings_preferences",
|
|
25
|
+
concept_id: "flow_settings",
|
|
26
|
+
pattern: /\/(settings|preferences|profile|account|billing|security)(\/|$)/i,
|
|
27
|
+
confidence: "medium",
|
|
28
|
+
missing_decisions: ["confirm settings ownership boundary", "confirm save, validation, and permission behavior"]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
flow_type: "dashboard_reporting",
|
|
32
|
+
concept_id: "flow_dashboard",
|
|
33
|
+
pattern: /\/(dashboard|reports|reporting|analytics|insights|metrics)(\/|$)/i,
|
|
34
|
+
confidence: "medium",
|
|
35
|
+
missing_decisions: ["confirm reporting data sources", "confirm refresh cadence and empty states"]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
flow_type: "search_filter",
|
|
39
|
+
concept_id: "flow_search",
|
|
40
|
+
pattern: /\/(search|results|explore|browse|discover)(\/|$)/i,
|
|
41
|
+
confidence: "medium",
|
|
42
|
+
missing_decisions: ["confirm searchable resources", "confirm filter, sorting, and pagination behavior"]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
flow_type: "bulk_review",
|
|
46
|
+
concept_id: "flow_bulk_review",
|
|
47
|
+
pattern: /\/(review|reviews|approvals|approval|moderation|queue|bulk)(\/|$)/i,
|
|
48
|
+
confidence: "medium",
|
|
49
|
+
missing_decisions: ["confirm bulk action eligibility", "confirm review outcomes and audit requirements"]
|
|
50
|
+
}
|
|
51
|
+
];
|
|
52
|
+
|
|
8
53
|
/**
|
|
9
54
|
* @param {string} routePath
|
|
10
55
|
* @returns {any}
|
|
@@ -23,12 +68,72 @@ export function routeSegments(routePath) {
|
|
|
23
68
|
export function screenKindForRoute(routePath) {
|
|
24
69
|
const normalized = String(routePath || "");
|
|
25
70
|
const segments = routeSegments(normalized);
|
|
71
|
+
if (inferNonResourceUiFlow(normalized)) return "flow";
|
|
26
72
|
if (/\/new$/.test(normalized)) return "form";
|
|
27
73
|
if (/\/:?[A-Za-z0-9_]+\/edit$/.test(normalized)) return "form";
|
|
28
74
|
if (segments.length >= 2 && !/\/new$/.test(normalized) && !/\/edit$/.test(normalized)) return "detail";
|
|
29
75
|
return "list";
|
|
30
76
|
}
|
|
31
77
|
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} routePath
|
|
80
|
+
* @returns {any}
|
|
81
|
+
*/
|
|
82
|
+
export function inferNonResourceUiFlow(routePath) {
|
|
83
|
+
const normalized = String(routePath || "");
|
|
84
|
+
for (const definition of NON_RESOURCE_FLOW_DEFINITIONS) {
|
|
85
|
+
if (definition.pattern.test(normalized)) {
|
|
86
|
+
return {
|
|
87
|
+
flow_type: definition.flow_type,
|
|
88
|
+
concept_id: definition.concept_id,
|
|
89
|
+
confidence: definition.confidence,
|
|
90
|
+
missing_decisions: definition.missing_decisions
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {string} routePath
|
|
99
|
+
* @param {string} screenId
|
|
100
|
+
* @returns {any}
|
|
101
|
+
*/
|
|
102
|
+
export function uiFlowIdForRoute(routePath, screenId) {
|
|
103
|
+
const flow = inferNonResourceUiFlow(routePath);
|
|
104
|
+
if (!flow) return null;
|
|
105
|
+
return `flow_${canonicalCandidateTerm(screenId || flow.flow_type).replace(/-/g, "_")}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {string} routePath
|
|
110
|
+
* @param {string} screenId
|
|
111
|
+
* @param {string} screenKind
|
|
112
|
+
* @returns {any}
|
|
113
|
+
*/
|
|
114
|
+
export function proposedUiContractAdditionsForFlow(routePath, screenId, screenKind) {
|
|
115
|
+
return {
|
|
116
|
+
projection_type: "ui_contract",
|
|
117
|
+
screens: [
|
|
118
|
+
{
|
|
119
|
+
id: screenId,
|
|
120
|
+
kind: screenKind || "flow",
|
|
121
|
+
title: screenId
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
screen_routes: [
|
|
125
|
+
{
|
|
126
|
+
screen: screenId,
|
|
127
|
+
path: routePath
|
|
128
|
+
}
|
|
129
|
+
],
|
|
130
|
+
notes: [
|
|
131
|
+
"Review-only flow recovery proposal.",
|
|
132
|
+
"Promote into the shared UI contract only after confirming behavior, state, and ownership decisions."
|
|
133
|
+
]
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
32
137
|
/**
|
|
33
138
|
* @param {string} routePath
|
|
34
139
|
* @returns {any}
|
|
@@ -37,6 +142,7 @@ export function screenIdForRoute(routePath) {
|
|
|
37
142
|
const segments = routeSegments(routePath);
|
|
38
143
|
const resource = canonicalCandidateTerm(segments[0] || "home");
|
|
39
144
|
const kind = screenKindForRoute(routePath);
|
|
145
|
+
if (kind === "flow") return canonicalCandidateTerm(segments.join("_") || "home").replace(/-/g, "_");
|
|
40
146
|
if (kind === "form" && /\/new$/.test(routePath)) return `${resource}_create`;
|
|
41
147
|
if (kind === "form" && /\/edit$/.test(routePath)) return `${resource}_edit`;
|
|
42
148
|
if (kind === "detail") return `${resource}_detail`;
|
|
@@ -19,9 +19,13 @@ export {
|
|
|
19
19
|
listFilesRecursive,
|
|
20
20
|
importSearchRoots,
|
|
21
21
|
normalizeImportRelativePath,
|
|
22
|
+
importSourcePathRelativeToWorkspace,
|
|
23
|
+
classifyImportSourcePath,
|
|
24
|
+
isPrimaryImportSource,
|
|
22
25
|
canonicalSourceRank,
|
|
23
26
|
selectPreferredImportFiles,
|
|
24
|
-
findImportFiles
|
|
27
|
+
findImportFiles,
|
|
28
|
+
findPrimaryImportFiles
|
|
25
29
|
} from "./shared/files.js";
|
|
26
30
|
export {
|
|
27
31
|
normalizeOpenApiPath,
|
|
@@ -38,6 +42,9 @@ export {
|
|
|
38
42
|
screenIdForRoute,
|
|
39
43
|
uiCapabilityHintsForRoute,
|
|
40
44
|
entityIdForRoute,
|
|
45
|
+
inferNonResourceUiFlow,
|
|
46
|
+
uiFlowIdForRoute,
|
|
47
|
+
proposedUiContractAdditionsForFlow,
|
|
41
48
|
inferReactRoutes,
|
|
42
49
|
inferSvelteRoutes,
|
|
43
50
|
inferNavigationStructure,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { findPrimaryImportFiles, readTextIfExists } from "../core/shared.js";
|
|
4
4
|
|
|
5
5
|
function splitClassBlocks(text) {
|
|
6
6
|
const lines = String(text || "").split(/\r?\n/);
|
|
@@ -51,7 +51,7 @@ function splitClassBlocks(text) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function buildSerializerIndex(paths) {
|
|
54
|
-
const files =
|
|
54
|
+
const files = findPrimaryImportFiles(paths, (filePath) => /\/serializers\.py$/i.test(filePath));
|
|
55
55
|
const index = new Map();
|
|
56
56
|
|
|
57
57
|
for (const filePath of files) {
|
|
@@ -89,7 +89,7 @@ function buildSerializerIndex(paths) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
function buildViewIndex(paths) {
|
|
92
|
-
const files =
|
|
92
|
+
const files = findPrimaryImportFiles(paths, (filePath) => /\/views\.py$/i.test(filePath));
|
|
93
93
|
const index = new Map();
|
|
94
94
|
|
|
95
95
|
for (const filePath of files) {
|
|
@@ -182,7 +182,7 @@ export const djangoRestEnricher = {
|
|
|
182
182
|
applies(context, candidates) {
|
|
183
183
|
if ((candidates.capabilities || []).length === 0) return false;
|
|
184
184
|
return (candidates.stacks || []).includes("django") ||
|
|
185
|
-
|
|
185
|
+
findPrimaryImportFiles(context.paths, (filePath) => /\/serializers\.py$/i.test(filePath)).length > 0;
|
|
186
186
|
},
|
|
187
187
|
enrich(context, candidates) {
|
|
188
188
|
const serializerIndex = buildSerializerIndex(context.paths);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { findPrimaryImportFiles, readTextIfExists } from "../core/shared.js";
|
|
4
4
|
|
|
5
5
|
function buildControllerIndex(paths) {
|
|
6
|
-
const files =
|
|
6
|
+
const files = findPrimaryImportFiles(paths, (filePath) => /app\/controllers\/.+_controller\.rb$/i.test(filePath));
|
|
7
7
|
const index = new Map();
|
|
8
8
|
for (const filePath of files) {
|
|
9
9
|
const text = readTextIfExists(filePath) || "";
|
|
@@ -186,7 +186,7 @@ export const railsControllerEnricher = {
|
|
|
186
186
|
applies(context, candidates) {
|
|
187
187
|
if ((candidates.capabilities || []).length === 0) return false;
|
|
188
188
|
return (candidates.stacks || []).includes("rails") ||
|
|
189
|
-
|
|
189
|
+
findPrimaryImportFiles(context.paths, (filePath) => /app\/controllers\/.+_controller\.rb$/i.test(filePath)).length > 0;
|
|
190
190
|
},
|
|
191
191
|
enrich(context, candidates) {
|
|
192
192
|
const controllers = buildControllerIndex(context.paths);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { findPrimaryImportFiles, readTextIfExists } from "../core/shared.js";
|
|
4
4
|
|
|
5
5
|
function modelClassNameForEntity(entityId) {
|
|
6
6
|
const stem = String(entityId || "")
|
|
@@ -13,7 +13,7 @@ function modelClassNameForEntity(entityId) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function buildModelIndex(paths) {
|
|
16
|
-
const modelFiles =
|
|
16
|
+
const modelFiles = findPrimaryImportFiles(paths, (filePath) => /app\/models\/.+\.rb$/i.test(filePath));
|
|
17
17
|
const index = new Map();
|
|
18
18
|
for (const filePath of modelFiles) {
|
|
19
19
|
const text = readTextIfExists(filePath) || "";
|
|
@@ -93,7 +93,7 @@ export const railsModelEnricher = {
|
|
|
93
93
|
track: "db",
|
|
94
94
|
applies(context, candidates) {
|
|
95
95
|
if ((candidates.entities || []).length === 0) return false;
|
|
96
|
-
return
|
|
96
|
+
return findPrimaryImportFiles(context.paths, (filePath) => /app\/models\/.+\.rb$/i.test(filePath)).length > 0;
|
|
97
97
|
},
|
|
98
98
|
enrich(context, candidates) {
|
|
99
99
|
const modelIndex = buildModelIndex(context.paths);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
dedupeCandidateRecords,
|
|
3
|
-
|
|
3
|
+
findPrimaryImportFiles,
|
|
4
4
|
inferApiEntityIdFromPath,
|
|
5
5
|
inferRouteCapabilityId,
|
|
6
6
|
makeCandidateRecord,
|
|
@@ -22,7 +22,7 @@ function parsePublicProperties(text) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function buildDotnetFileIndex(paths) {
|
|
25
|
-
const files =
|
|
25
|
+
const files = findPrimaryImportFiles(paths, (filePath) => /\.cs$/i.test(filePath));
|
|
26
26
|
const index = new Map();
|
|
27
27
|
for (const filePath of files) {
|
|
28
28
|
index.set(relativeTo(paths.repoRoot, filePath), readTextIfExists(filePath) || "");
|
|
@@ -239,8 +239,8 @@ export const aspNetCoreExtractor = {
|
|
|
239
239
|
id: "api.aspnet-core",
|
|
240
240
|
track: "api",
|
|
241
241
|
detect(context) {
|
|
242
|
-
const controllerFiles =
|
|
243
|
-
const programFiles =
|
|
242
|
+
const controllerFiles = findPrimaryImportFiles(context.paths, (filePath) => /Controller\.cs$/i.test(filePath));
|
|
243
|
+
const programFiles = findPrimaryImportFiles(context.paths, (filePath) => /Program\.cs$/i.test(filePath));
|
|
244
244
|
const score = controllerFiles.length > 0 && programFiles.some((filePath) => /WebApplication\.CreateBuilder|AddSwaggerGen|AddMvc/.test(readTextIfExists(filePath) || "")) ? 88 : 0;
|
|
245
245
|
return {
|
|
246
246
|
score,
|
|
@@ -248,7 +248,7 @@ export const aspNetCoreExtractor = {
|
|
|
248
248
|
};
|
|
249
249
|
},
|
|
250
250
|
extract(context) {
|
|
251
|
-
const controllerFiles =
|
|
251
|
+
const controllerFiles = findPrimaryImportFiles(context.paths, (filePath) => /Controller\.cs$/i.test(filePath));
|
|
252
252
|
const featureFiles = buildDotnetFileIndex(context.paths).files.map((filePath) => ({ filePath, text: readTextIfExists(filePath) || "" }));
|
|
253
253
|
const findings = [];
|
|
254
254
|
const candidates = { capabilities: [], routes: [], stacks: [] };
|
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
dedupeCandidateRecords,
|
|
5
|
-
|
|
5
|
+
findPrimaryImportFiles,
|
|
6
6
|
inferApiEntityIdFromPath,
|
|
7
7
|
inferRouteCapabilityId,
|
|
8
8
|
makeCandidateRecord,
|
|
@@ -90,7 +90,7 @@ function permissionAuthHint(permissionText, method) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
function buildViewIndex(paths) {
|
|
93
|
-
const viewFiles =
|
|
93
|
+
const viewFiles = findPrimaryImportFiles(paths, (filePath) => /\/views\.py$/i.test(filePath));
|
|
94
94
|
const index = new Map();
|
|
95
95
|
|
|
96
96
|
for (const filePath of viewFiles) {
|
|
@@ -250,8 +250,8 @@ export const djangoRoutesExtractor = {
|
|
|
250
250
|
id: "api.django-routes",
|
|
251
251
|
track: "api",
|
|
252
252
|
detect(context) {
|
|
253
|
-
const manageFiles =
|
|
254
|
-
const urlFiles =
|
|
253
|
+
const manageFiles = findPrimaryImportFiles(context.paths, (filePath) => /\/manage\.py$/i.test(filePath));
|
|
254
|
+
const urlFiles = findPrimaryImportFiles(context.paths, (filePath) => /\/urls\.py$/i.test(filePath));
|
|
255
255
|
const score = manageFiles.length > 0 && urlFiles.length > 0 ? 90 : 0;
|
|
256
256
|
return {
|
|
257
257
|
score,
|
|
@@ -259,7 +259,7 @@ export const djangoRoutesExtractor = {
|
|
|
259
259
|
};
|
|
260
260
|
},
|
|
261
261
|
extract(context) {
|
|
262
|
-
const urlFiles =
|
|
262
|
+
const urlFiles = findPrimaryImportFiles(context.paths, (filePath) => /\/urls\.py$/i.test(filePath));
|
|
263
263
|
const moduleMap = new Map(urlFiles.map((filePath) => [moduleNameForFile(context.paths.repoRoot, filePath), filePath]));
|
|
264
264
|
const viewIndex = buildViewIndex(context.paths);
|
|
265
265
|
const rootFiles = urlFiles.filter((filePath) => !/\/apps\//.test(filePath));
|
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
dedupeCandidateRecords,
|
|
5
|
-
|
|
5
|
+
findPrimaryImportFiles,
|
|
6
6
|
inferApiEntityIdFromPath,
|
|
7
7
|
inferRouteAuthHint,
|
|
8
8
|
inferRouteCapabilityId,
|
|
@@ -94,18 +94,18 @@ export const expressExtractor = {
|
|
|
94
94
|
id: "api.express",
|
|
95
95
|
track: "api",
|
|
96
96
|
detect(context) {
|
|
97
|
-
const routeFiles =
|
|
97
|
+
const routeFiles = findPrimaryImportFiles(context.paths, (filePath) => /src\/routes\/.+\.(ts|js|mjs|cjs)$/i.test(filePath));
|
|
98
98
|
return {
|
|
99
99
|
score: routeFiles.length > 0 ? 85 : 0,
|
|
100
100
|
reasons: routeFiles.length > 0 ? ["Found Express route modules"] : []
|
|
101
101
|
};
|
|
102
102
|
},
|
|
103
103
|
extract(context) {
|
|
104
|
-
const permissionsFile =
|
|
104
|
+
const permissionsFile = findPrimaryImportFiles(context.paths, (filePath) => /src\/helpers\/permissions\.(ts|js|mjs|cjs)$/i.test(filePath))[0];
|
|
105
105
|
const permissionsText = permissionsFile ? context.helpers.readTextIfExists(permissionsFile) || "" : "";
|
|
106
106
|
const apiRoutes = parseApiRoutesMap(permissionsText);
|
|
107
107
|
const permissionMeta = parsePermissionsMetadata(permissionsText);
|
|
108
|
-
const routeFiles =
|
|
108
|
+
const routeFiles = findPrimaryImportFiles(context.paths, (filePath) => /src\/routes\/.+\.(ts|js|mjs|cjs)$/i.test(filePath));
|
|
109
109
|
const routes = routeFiles.flatMap((filePath) =>
|
|
110
110
|
parseExpressRouteCalls(filePath, context.helpers.readTextIfExists(filePath) || "", apiRoutes, permissionMeta)
|
|
111
111
|
);
|