@topogram/cli 0.3.76 → 0.3.77
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 +56 -0
- 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 +6 -0
- package/src/import/extractors/api/generic-route-fallback.js +2 -1
- package/src/import/extractors/api/openapi.js +3 -3
- package/src/import/extractors/cli/generic.js +2 -1
- package/src/import/extractors/db/drizzle.js +13 -3
- package/src/import/extractors/db/maintained-seams.js +27 -4
- package/src/import/extractors/db/prisma.js +9 -2
- package/src/import/extractors/db/sql.js +3 -3
- package/src/import/extractors/ui/next-app-router.js +26 -5
- package/src/import/extractors/ui/next-pages-router.js +31 -7
- 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 +3 -2
- 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 (/(^|\/)[^/]*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
|
|
|
@@ -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,6 +19,9 @@ export {
|
|
|
19
19
|
listFilesRecursive,
|
|
20
20
|
importSearchRoots,
|
|
21
21
|
normalizeImportRelativePath,
|
|
22
|
+
importSourcePathRelativeToWorkspace,
|
|
23
|
+
classifyImportSourcePath,
|
|
24
|
+
isPrimaryImportSource,
|
|
22
25
|
canonicalSourceRank,
|
|
23
26
|
selectPreferredImportFiles,
|
|
24
27
|
findImportFiles
|
|
@@ -38,6 +41,9 @@ export {
|
|
|
38
41
|
screenIdForRoute,
|
|
39
42
|
uiCapabilityHintsForRoute,
|
|
40
43
|
entityIdForRoute,
|
|
44
|
+
inferNonResourceUiFlow,
|
|
45
|
+
uiFlowIdForRoute,
|
|
46
|
+
proposedUiContractAdditionsForFlow,
|
|
41
47
|
inferReactRoutes,
|
|
42
48
|
inferSvelteRoutes,
|
|
43
49
|
inferNavigationStructure,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { findImportFiles, inferRouteAuthHint, inferRouteCapabilityId, inferRouteQueryParams, makeCandidateRecord, normalizeOpenApiPath, relativeTo } from "../../core/shared.js";
|
|
1
|
+
import { findImportFiles, inferRouteAuthHint, inferRouteCapabilityId, inferRouteQueryParams, isPrimaryImportSource, makeCandidateRecord, normalizeOpenApiPath, relativeTo } from "../../core/shared.js";
|
|
2
2
|
|
|
3
3
|
function extractHandlerContext(text, handlerName) {
|
|
4
4
|
if (!handlerName) return "";
|
|
@@ -19,6 +19,7 @@ function inferServerRoutes(context) {
|
|
|
19
19
|
context.paths,
|
|
20
20
|
(filePath) =>
|
|
21
21
|
/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath) &&
|
|
22
|
+
isPrimaryImportSource(context.paths, filePath) &&
|
|
22
23
|
/(server|api|routes|src)/i.test(filePath)
|
|
23
24
|
);
|
|
24
25
|
for (const filePath of routeFiles) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { findImportFiles, makeCandidateRecord, normalizeOpenApiPath, relativeTo, selectPreferredImportFiles, slugify, titleCase } from "../../core/shared.js";
|
|
1
|
+
import { findImportFiles, isPrimaryImportSource, makeCandidateRecord, normalizeOpenApiPath, relativeTo, selectPreferredImportFiles, slugify, titleCase } from "../../core/shared.js";
|
|
2
2
|
|
|
3
3
|
function openApiRefName(ref) {
|
|
4
4
|
return typeof ref === "string" ? ref.split("/").pop() || null : null;
|
|
@@ -196,7 +196,7 @@ export const openApiExtractor = {
|
|
|
196
196
|
detect(context) {
|
|
197
197
|
const files = selectPreferredImportFiles(
|
|
198
198
|
context.paths,
|
|
199
|
-
findImportFiles(context.paths, (filePath) => /(openapi|swagger)\.(json|ya?ml)$/i.test(filePath)),
|
|
199
|
+
findImportFiles(context.paths, (filePath) => /(openapi|swagger)\.(json|ya?ml)$/i.test(filePath) && isPrimaryImportSource(context.paths, filePath)),
|
|
200
200
|
"openapi"
|
|
201
201
|
);
|
|
202
202
|
return {
|
|
@@ -207,7 +207,7 @@ export const openApiExtractor = {
|
|
|
207
207
|
extract(context) {
|
|
208
208
|
const openApiFiles = selectPreferredImportFiles(
|
|
209
209
|
context.paths,
|
|
210
|
-
findImportFiles(context.paths, (filePath) => /(openapi|swagger)\.(json|ya?ml)$/i.test(filePath)),
|
|
210
|
+
findImportFiles(context.paths, (filePath) => /(openapi|swagger)\.(json|ya?ml)$/i.test(filePath) && isPrimaryImportSource(context.paths, filePath)),
|
|
211
211
|
"openapi"
|
|
212
212
|
);
|
|
213
213
|
const findings = [];
|
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import {
|
|
6
6
|
findImportFiles,
|
|
7
7
|
idHintify,
|
|
8
|
+
isPrimaryImportSource,
|
|
8
9
|
makeCandidateRecord,
|
|
9
10
|
normalizeImportRelativePath,
|
|
10
11
|
readJsonIfExists,
|
|
@@ -33,7 +34,7 @@ function normalizePath(value) {
|
|
|
33
34
|
*/
|
|
34
35
|
function isAuthoritativeCliSource(paths, filePath) {
|
|
35
36
|
const normalized = normalizePath(normalizeImportRelativePath(paths, filePath));
|
|
36
|
-
return !NON_AUTHORITATIVE_CLI_PATH_PATTERN.test(normalized);
|
|
37
|
+
return isPrimaryImportSource(paths, filePath) && !NON_AUTHORITATIVE_CLI_PATH_PATTERN.test(normalized);
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
dedupeCandidateRecords,
|
|
6
6
|
findImportFiles,
|
|
7
7
|
idHintify,
|
|
8
|
+
isPrimaryImportSource,
|
|
8
9
|
makeCandidateRecord,
|
|
9
10
|
relativeTo,
|
|
10
11
|
slugify,
|
|
@@ -167,7 +168,10 @@ function parseDrizzleTables(schemaText) {
|
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
function drizzleConfigFiles(context) {
|
|
170
|
-
return findImportFiles(context.paths, (filePath) =>
|
|
171
|
+
return findImportFiles(context.paths, (filePath) =>
|
|
172
|
+
/drizzle\.config\.(ts|js|mjs|cjs)$/i.test(path.basename(filePath)) &&
|
|
173
|
+
isPrimaryImportSource(context.paths, filePath)
|
|
174
|
+
);
|
|
171
175
|
}
|
|
172
176
|
|
|
173
177
|
function configuredSchemaFiles(context, configFiles) {
|
|
@@ -184,15 +188,21 @@ function configuredSchemaFiles(context, configFiles) {
|
|
|
184
188
|
return files;
|
|
185
189
|
}
|
|
186
190
|
|
|
191
|
+
function isDrizzleSchemaSource(context, filePath) {
|
|
192
|
+
const text = context.helpers.readTextIfExists(filePath) || "";
|
|
193
|
+
return /\b(?:pgTable|sqliteTable|mysqlTable)\s*\(/.test(text);
|
|
194
|
+
}
|
|
195
|
+
|
|
187
196
|
function findDrizzleSchemaFiles(context) {
|
|
188
197
|
const configFiles = drizzleConfigFiles(context);
|
|
189
198
|
const conventionalSchemaFiles = findImportFiles(context.paths, (filePath) =>
|
|
190
|
-
/(?:^|\/)(?:src\/db\/schema|src\/schema|db\/schema|schema)\.(ts|js|mjs|cjs)$/i.test(relativeTo(context.paths.workspaceRoot, filePath).replaceAll(path.sep, "/"))
|
|
199
|
+
/(?:^|\/)(?:src\/db\/schema|src\/schema|db\/schema|schema)\.(ts|js|mjs|cjs)$/i.test(relativeTo(context.paths.workspaceRoot, filePath).replaceAll(path.sep, "/")) &&
|
|
200
|
+
isPrimaryImportSource(context.paths, filePath)
|
|
191
201
|
);
|
|
192
202
|
return [...new Set([
|
|
193
203
|
...configuredSchemaFiles(context, configFiles),
|
|
194
204
|
...conventionalSchemaFiles
|
|
195
|
-
])].sort();
|
|
205
|
+
])].filter((filePath) => isDrizzleSchemaSource(context, filePath)).sort();
|
|
196
206
|
}
|
|
197
207
|
|
|
198
208
|
export const drizzleExtractor = {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
import { findImportFiles, makeCandidateRecord, relativeTo } from "../../core/shared.js";
|
|
5
|
+
import { findImportFiles, isPrimaryImportSource, makeCandidateRecord, relativeTo } from "../../core/shared.js";
|
|
6
6
|
|
|
7
7
|
/** @param {string} value @returns {string} */
|
|
8
8
|
function toPosix(value) {
|
|
@@ -86,6 +86,13 @@ function maintainedDbSeamCandidate(context, options) {
|
|
|
86
86
|
...(options.migrationsPath ? { migrationsPath: options.migrationsPath } : {})
|
|
87
87
|
};
|
|
88
88
|
const idHint = `seam_${options.tool}_db_migrations`;
|
|
89
|
+
const manualNextSteps = [
|
|
90
|
+
"Review evidence, match_reasons, and missing_decisions before accepting this seam.",
|
|
91
|
+
`Confirm database runtime '${runtimeId}' and projection '${projectionId}' are the right topology targets for the maintained app.`,
|
|
92
|
+
"If accepted, copy proposed_runtime_migration into the matching database runtime's migration block in topogram.project.json.",
|
|
93
|
+
"Keep ownership 'maintained' and apply 'never'; import must not apply migrations or patch maintained app files.",
|
|
94
|
+
"After editing topogram.project.json, run topogram check . --json and the maintained app's migration verification."
|
|
95
|
+
];
|
|
89
96
|
|
|
90
97
|
return makeCandidateRecord({
|
|
91
98
|
kind: "maintained_db_migration_seam",
|
|
@@ -112,6 +119,13 @@ function maintainedDbSeamCandidate(context, options) {
|
|
|
112
119
|
match_reasons: options.matchReasons,
|
|
113
120
|
missing_decisions: options.missingDecisions,
|
|
114
121
|
proposed_runtime_migration: proposedRuntimeMigration,
|
|
122
|
+
manual_next_steps: manualNextSteps,
|
|
123
|
+
project_config_target: {
|
|
124
|
+
file: "topogram.project.json",
|
|
125
|
+
path: `topology.runtimes[id=${runtimeId}].migration`,
|
|
126
|
+
runtime_id: runtimeId,
|
|
127
|
+
projection_id: projectionId
|
|
128
|
+
},
|
|
115
129
|
maintained_modules: [options.schemaPath, options.migrationsPath].filter(Boolean),
|
|
116
130
|
emitted_dependencies: [snapshotPath, projectionId],
|
|
117
131
|
allowed_change_classes: ["proposal_only"],
|
|
@@ -125,7 +139,10 @@ export function inferPrismaMaintainedDbSeams(context, prismaFiles) {
|
|
|
125
139
|
return [];
|
|
126
140
|
}
|
|
127
141
|
const schemaPath = appRelativePath(context, prismaFiles[0]);
|
|
128
|
-
const migrationFiles = /** @type {string[]} */ (findImportFiles(context.paths, /** @param {string} filePath */ (filePath) =>
|
|
142
|
+
const migrationFiles = /** @type {string[]} */ (findImportFiles(context.paths, /** @param {string} filePath */ (filePath) =>
|
|
143
|
+
toPosix(filePath).includes("/prisma/migrations/") &&
|
|
144
|
+
isPrimaryImportSource(context.paths, filePath)
|
|
145
|
+
));
|
|
129
146
|
const migrationsPath = firstMarkedDirectory(context, migrationFiles, [["prisma", "migrations"]]);
|
|
130
147
|
return [
|
|
131
148
|
maintainedDbSeamCandidate(context, {
|
|
@@ -150,8 +167,14 @@ export function inferDrizzleMaintainedDbSeams(context, schemaFiles) {
|
|
|
150
167
|
if (!schemaFiles.length) {
|
|
151
168
|
return [];
|
|
152
169
|
}
|
|
153
|
-
const configFiles = /** @type {string[]} */ (findImportFiles(context.paths, /** @param {string} filePath */ (filePath) =>
|
|
154
|
-
|
|
170
|
+
const configFiles = /** @type {string[]} */ (findImportFiles(context.paths, /** @param {string} filePath */ (filePath) =>
|
|
171
|
+
/drizzle\.config\.(ts|js|mjs|cjs)$/i.test(path.basename(filePath)) &&
|
|
172
|
+
isPrimaryImportSource(context.paths, filePath)
|
|
173
|
+
));
|
|
174
|
+
const drizzleFiles = /** @type {string[]} */ (findImportFiles(context.paths, /** @param {string} filePath */ (filePath) =>
|
|
175
|
+
appRelativePath(context, filePath).startsWith("drizzle/") &&
|
|
176
|
+
isPrimaryImportSource(context.paths, filePath)
|
|
177
|
+
));
|
|
155
178
|
const configuredOutPath = drizzleOutPathFromConfig(context, configFiles);
|
|
156
179
|
const migrationsPath = configuredOutPath ||
|
|
157
180
|
firstMarkedDirectory(context, drizzleFiles, [["drizzle"]]);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
findImportFiles,
|
|
3
|
+
isPrimaryImportSource,
|
|
3
4
|
makeCandidateRecord,
|
|
4
5
|
normalizePrismaType,
|
|
5
6
|
relativeTo,
|
|
@@ -113,7 +114,10 @@ export const prismaExtractor = {
|
|
|
113
114
|
id: "db.prisma",
|
|
114
115
|
track: "db",
|
|
115
116
|
detect(context) {
|
|
116
|
-
const files = findImportFiles(context.paths, (filePath) =>
|
|
117
|
+
const files = findImportFiles(context.paths, (filePath) =>
|
|
118
|
+
(filePath.endsWith("/prisma/schema.prisma") || filePath.endsWith("prisma/schema.prisma")) &&
|
|
119
|
+
isPrimaryImportSource(context.paths, filePath)
|
|
120
|
+
);
|
|
117
121
|
return {
|
|
118
122
|
score: files.length > 0 ? 100 : 0,
|
|
119
123
|
reasons: files.length > 0 ? ["Found Prisma schema"] : []
|
|
@@ -122,7 +126,10 @@ export const prismaExtractor = {
|
|
|
122
126
|
extract(context) {
|
|
123
127
|
const prismaFiles = selectPreferredImportFiles(
|
|
124
128
|
context.paths,
|
|
125
|
-
findImportFiles(context.paths, (filePath) =>
|
|
129
|
+
findImportFiles(context.paths, (filePath) =>
|
|
130
|
+
(filePath.endsWith("/prisma/schema.prisma") || filePath.endsWith("prisma/schema.prisma")) &&
|
|
131
|
+
isPrimaryImportSource(context.paths, filePath)
|
|
132
|
+
),
|
|
126
133
|
"prisma"
|
|
127
134
|
);
|
|
128
135
|
const findings = [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { canonicalCandidateTerm, findImportFiles, makeCandidateRecord, relativeTo, selectPreferredImportFiles, slugify, titleCase, idHintify } from "../../core/shared.js";
|
|
1
|
+
import { canonicalCandidateTerm, findImportFiles, isPrimaryImportSource, makeCandidateRecord, relativeTo, selectPreferredImportFiles, slugify, titleCase, idHintify } from "../../core/shared.js";
|
|
2
2
|
import { inferSqlMaintainedDbSeams } from "./maintained-seams.js";
|
|
3
3
|
|
|
4
4
|
function parseTableConstraint(line, tableName) {
|
|
@@ -105,14 +105,14 @@ export const sqlExtractor = {
|
|
|
105
105
|
id: "db.sql",
|
|
106
106
|
track: "db",
|
|
107
107
|
detect(context) {
|
|
108
|
-
const files = findImportFiles(context.paths, (filePath) => filePath.endsWith(".sql"));
|
|
108
|
+
const files = findImportFiles(context.paths, (filePath) => filePath.endsWith(".sql") && isPrimaryImportSource(context.paths, filePath));
|
|
109
109
|
return {
|
|
110
110
|
score: files.length > 0 ? 80 : 0,
|
|
111
111
|
reasons: files.length > 0 ? ["Found SQL schema or migration files"] : []
|
|
112
112
|
};
|
|
113
113
|
},
|
|
114
114
|
extract(context) {
|
|
115
|
-
const allSqlFiles = findImportFiles(context.paths, (filePath) => filePath.endsWith(".sql"));
|
|
115
|
+
const allSqlFiles = findImportFiles(context.paths, (filePath) => filePath.endsWith(".sql") && isPrimaryImportSource(context.paths, filePath));
|
|
116
116
|
const schemaSqlFiles = allSqlFiles.filter((filePath) => !/migration/i.test(filePath) && !/\/src\/test\//i.test(filePath));
|
|
117
117
|
const migrationSqlFiles = allSqlFiles.filter((filePath) => /migration/i.test(filePath));
|
|
118
118
|
const sqlFiles =
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { dedupeCandidateRecords, inferNextAppRoutes, makeCandidateRecord, nextScreenIdForRoute, nextScreenKindForRoute, uiCapabilityHintsForNextRoute, entityIdForNextRoute, conceptIdForNextRoute, relativeTo, titleCase, idHintify } from "../../core/shared.js";
|
|
1
|
+
import { dedupeCandidateRecords, inferNextAppRoutes, inferNonResourceUiFlow, makeCandidateRecord, nextScreenIdForRoute, nextScreenKindForRoute, proposedUiContractAdditionsForFlow, uiCapabilityHintsForNextRoute, entityIdForNextRoute, conceptIdForNextRoute, relativeTo, titleCase, idHintify, uiFlowIdForRoute } from "../../core/shared.js";
|
|
2
2
|
|
|
3
3
|
export const nextAppRouterUiExtractor = {
|
|
4
4
|
id: "ui.next-app-router",
|
|
@@ -13,7 +13,7 @@ export const nextAppRouterUiExtractor = {
|
|
|
13
13
|
extract(context) {
|
|
14
14
|
const routes = inferNextAppRoutes(context.paths.workspaceRoot, context.helpers);
|
|
15
15
|
const findings = [];
|
|
16
|
-
const candidates = { screens: [], routes: [], actions: [], stacks: [] };
|
|
16
|
+
const candidates = { screens: [], routes: [], actions: [], flows: [], stacks: [] };
|
|
17
17
|
if (routes.length > 0) {
|
|
18
18
|
findings.push({
|
|
19
19
|
kind: "next_app_routes",
|
|
@@ -26,9 +26,10 @@ export const nextAppRouterUiExtractor = {
|
|
|
26
26
|
const provenance = `${relativeTo(context.paths.repoRoot, route.file)}#${route.path}`;
|
|
27
27
|
const screenId = nextScreenIdForRoute(route.path);
|
|
28
28
|
const screenKind = nextScreenKindForRoute(route.path);
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
29
|
+
const flow = inferNonResourceUiFlow(route.path);
|
|
30
|
+
const capabilityHints = flow ? { load: null, submit: null, primary_action: null } : uiCapabilityHintsForNextRoute(route.path);
|
|
31
|
+
const entityId = flow ? null : entityIdForNextRoute(route.path);
|
|
32
|
+
const conceptId = flow?.concept_id || conceptIdForNextRoute(route.path);
|
|
32
33
|
candidates.screens.push(makeCandidateRecord({
|
|
33
34
|
kind: "screen",
|
|
34
35
|
idHint: screenId,
|
|
@@ -56,6 +57,25 @@ export const nextAppRouterUiExtractor = {
|
|
|
56
57
|
concept_id: conceptId,
|
|
57
58
|
path: route.path
|
|
58
59
|
}));
|
|
60
|
+
if (flow) {
|
|
61
|
+
candidates.flows.push(makeCandidateRecord({
|
|
62
|
+
kind: "ui_flow",
|
|
63
|
+
idHint: uiFlowIdForRoute(route.path, screenId),
|
|
64
|
+
label: `${titleCase(flow.flow_type)} Flow`,
|
|
65
|
+
confidence: flow.confidence,
|
|
66
|
+
sourceKind: "route_code",
|
|
67
|
+
sourceOfTruth: "candidate",
|
|
68
|
+
provenance,
|
|
69
|
+
track: "ui",
|
|
70
|
+
flow_type: flow.flow_type,
|
|
71
|
+
concept_id: flow.concept_id,
|
|
72
|
+
screen_ids: [screenId],
|
|
73
|
+
route_paths: [route.path],
|
|
74
|
+
evidence: [provenance],
|
|
75
|
+
missing_decisions: flow.missing_decisions,
|
|
76
|
+
proposed_ui_contract_additions: proposedUiContractAdditionsForFlow(route.path, screenId, screenKind)
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
59
79
|
if (capabilityHints.primary_action) {
|
|
60
80
|
candidates.actions.push(makeCandidateRecord({
|
|
61
81
|
kind: "ui_action",
|
|
@@ -77,6 +97,7 @@ export const nextAppRouterUiExtractor = {
|
|
|
77
97
|
candidates.screens = dedupeCandidateRecords(candidates.screens, (record) => record.id_hint);
|
|
78
98
|
candidates.routes = dedupeCandidateRecords(candidates.routes, (record) => record.id_hint);
|
|
79
99
|
candidates.actions = dedupeCandidateRecords(candidates.actions, (record) => record.id_hint);
|
|
100
|
+
candidates.flows = dedupeCandidateRecords(candidates.flows, (record) => record.id_hint);
|
|
80
101
|
candidates.stacks = [...new Set(candidates.stacks)].sort();
|
|
81
102
|
return { findings, candidates };
|
|
82
103
|
}
|
|
@@ -3,9 +3,12 @@ import path from "node:path";
|
|
|
3
3
|
import {
|
|
4
4
|
dedupeCandidateRecords,
|
|
5
5
|
findImportFiles,
|
|
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
|
|
|
@@ -70,7 +73,7 @@ export const nextPagesRouterUiExtractor = {
|
|
|
70
73
|
const pageFiles = findImportFiles(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
|
}
|
|
@@ -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
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
dedupeCandidateRecords,
|
|
4
4
|
findImportFiles,
|
|
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 = findImportFiles(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 = findImportFiles(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: [] };
|
|
@@ -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)}
|