@topogram/cli 0.3.75 → 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 +68 -6
- package/src/cli/commands/import/workspace.js +2 -0
- package/src/cli.js +3 -3
- package/src/import/core/runner/candidates.js +4 -1
- package/src/import/core/runner/reports.js +13 -3
- package/src/import/core/runner/tracks.js +2 -2
- 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 +45 -4
- package/src/import/extractors/db/maintained-seams.js +231 -0
- package/src/import/extractors/db/prisma.js +12 -4
- package/src/import/extractors/db/sql.js +6 -4
- 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 +21 -3
- package/src/workflows/reconcile/impacts/adoption-plan.js +13 -0
- package/src/workflows/reconcile/renderers.js +33 -0
- package/src/workflows/reconcile/workflow.js +3 -1
package/package.json
CHANGED
|
@@ -637,6 +637,7 @@ function mappingSuggestionsForItem(item) {
|
|
|
637
637
|
}
|
|
638
638
|
|
|
639
639
|
export function buildAgentAdoptionPlan(adoptionPlan, maintainedBoundaryArtifact = null) {
|
|
640
|
+
const importedMaintainedDbSeams = [...(adoptionPlan?.imported_maintained_db_seams || [])];
|
|
640
641
|
const items = (adoptionPlan?.items || []).map((item) => {
|
|
641
642
|
const currentState = inferCurrentAdoptionState(item);
|
|
642
643
|
const maintainedSeamCandidates = inferMaintainedSeamCandidates({
|
|
@@ -656,6 +657,9 @@ export function buildAgentAdoptionPlan(adoptionPlan, maintainedBoundaryArtifact
|
|
|
656
657
|
ui_impacts: [...(item.ui_impacts || [])],
|
|
657
658
|
workflow_impacts: [...(item.workflow_impacts || [])]
|
|
658
659
|
}, maintainedBoundaryArtifact);
|
|
660
|
+
const importedDbSeamCandidates = ["entity", "enum", "index", "relation", "db"].includes(item.kind) || item.track === "db"
|
|
661
|
+
? importedMaintainedDbSeams
|
|
662
|
+
: [];
|
|
659
663
|
return {
|
|
660
664
|
id: adoptionItemKey(item),
|
|
661
665
|
bundle: item.bundle,
|
|
@@ -691,11 +695,68 @@ export function buildAgentAdoptionPlan(adoptionPlan, maintainedBoundaryArtifact
|
|
|
691
695
|
projection_impacts: [...(item.projection_impacts || [])],
|
|
692
696
|
ui_impacts: [...(item.ui_impacts || [])],
|
|
693
697
|
workflow_impacts: [...(item.workflow_impacts || [])],
|
|
694
|
-
maintained_seam_candidates: maintainedSeamCandidates,
|
|
698
|
+
maintained_seam_candidates: [...maintainedSeamCandidates, ...importedDbSeamCandidates],
|
|
695
699
|
mapping_suggestions: mappingSuggestionsForItem(item),
|
|
696
700
|
available_actions: ADOPTION_STATE_VOCABULARY
|
|
697
701
|
};
|
|
698
702
|
});
|
|
703
|
+
const importedDbSeamSurfaces = importedMaintainedDbSeams.map((seam) => ({
|
|
704
|
+
id: seam.id_hint || seam.seam_id,
|
|
705
|
+
bundle: "database",
|
|
706
|
+
item: seam.id_hint || seam.seam_id,
|
|
707
|
+
kind: seam.kind || "maintained_db_migration_seam",
|
|
708
|
+
track: "db",
|
|
709
|
+
source_kind: seam.source_kind || "migration_strategy_inference",
|
|
710
|
+
source_path: "candidates/app/db/candidates.json",
|
|
711
|
+
canonical_rel_path: null,
|
|
712
|
+
related_shapes: [],
|
|
713
|
+
review_boundary: {
|
|
714
|
+
automation_class: "review_required",
|
|
715
|
+
reasons: [
|
|
716
|
+
"manual_project_config_review",
|
|
717
|
+
"proposal_only_maintained_db_migration_seam"
|
|
718
|
+
]
|
|
719
|
+
},
|
|
720
|
+
current_state: "stage",
|
|
721
|
+
recommended_state: "customize",
|
|
722
|
+
supported_states: ADOPTION_STATE_VOCABULARY,
|
|
723
|
+
human_review_required: true,
|
|
724
|
+
provenance: {
|
|
725
|
+
bundle: "database",
|
|
726
|
+
source_path: "candidates/app/db/candidates.json",
|
|
727
|
+
canonical_rel_path: null
|
|
728
|
+
},
|
|
729
|
+
requirements: {
|
|
730
|
+
related_docs: [],
|
|
731
|
+
related_capabilities: [],
|
|
732
|
+
related_shapes: [],
|
|
733
|
+
related_rules: [],
|
|
734
|
+
related_workflows: [],
|
|
735
|
+
blocking_dependencies: []
|
|
736
|
+
},
|
|
737
|
+
projection_impacts: [],
|
|
738
|
+
ui_impacts: [],
|
|
739
|
+
workflow_impacts: [],
|
|
740
|
+
maintained_seam_candidates: [seam],
|
|
741
|
+
mapping_suggestions: [{
|
|
742
|
+
type: "manual_project_config_review",
|
|
743
|
+
id: seam.id_hint || seam.seam_id,
|
|
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
|
+
]
|
|
756
|
+
}],
|
|
757
|
+
available_actions: ADOPTION_STATE_VOCABULARY
|
|
758
|
+
}));
|
|
759
|
+
const proposalSurfaces = [...items, ...importedDbSeamSurfaces];
|
|
699
760
|
|
|
700
761
|
return {
|
|
701
762
|
type: "agent_adoption_plan",
|
|
@@ -708,10 +769,11 @@ export function buildAgentAdoptionPlan(adoptionPlan, maintainedBoundaryArtifact
|
|
|
708
769
|
non_canonical_adoption_state: "stage"
|
|
709
770
|
},
|
|
710
771
|
approved_review_groups: [...new Set(adoptionPlan?.approved_review_groups || [])].sort(),
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
772
|
+
imported_maintained_db_seam_candidates: importedMaintainedDbSeams,
|
|
773
|
+
imported_proposal_surfaces: proposalSurfaces,
|
|
774
|
+
staged_items: proposalSurfaces.filter((item) => item.current_state === "stage").map((item) => item.id),
|
|
775
|
+
accepted_items: proposalSurfaces.filter((item) => item.current_state === "accept").map((item) => item.id),
|
|
776
|
+
rejected_items: proposalSurfaces.filter((item) => item.current_state === "reject").map((item) => item.id),
|
|
777
|
+
requires_human_review: proposalSurfaces.filter((item) => item.human_review_required).map((item) => item.id)
|
|
716
778
|
};
|
|
717
779
|
}
|
|
@@ -110,10 +110,12 @@ export function importCandidateCounts(summary) {
|
|
|
110
110
|
return {
|
|
111
111
|
dbEntities: candidates.db?.entities?.length || 0,
|
|
112
112
|
dbEnums: candidates.db?.enums?.length || 0,
|
|
113
|
+
dbMaintainedSeams: candidates.db?.maintained_seams?.length || 0,
|
|
113
114
|
apiCapabilities: candidates.api?.capabilities?.length || 0,
|
|
114
115
|
apiRoutes: candidates.api?.routes?.length || 0,
|
|
115
116
|
uiScreens: candidates.ui?.screens?.length || 0,
|
|
116
117
|
uiRoutes: candidates.ui?.routes?.length || 0,
|
|
118
|
+
uiFlows: candidates.ui?.flows?.length || 0,
|
|
117
119
|
uiWidgets: candidates.ui?.widgets?.length || candidates.ui?.components?.length || 0,
|
|
118
120
|
uiShapes: candidates.ui?.shapes?.length || 0,
|
|
119
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
|
}
|
|
@@ -261,7 +261,8 @@ export function normalizeCandidatesForTrack(track, candidates) {
|
|
|
261
261
|
entities: dedupeCandidateRecords(candidates.entities || [], idHint),
|
|
262
262
|
enums: dedupeCandidateRecords(candidates.enums || [], idHint),
|
|
263
263
|
relations: dedupeCandidateRecords(candidates.relations || [], idHint),
|
|
264
|
-
indexes: dedupeCandidateRecords(candidates.indexes || [], idHint)
|
|
264
|
+
indexes: dedupeCandidateRecords(candidates.indexes || [], idHint),
|
|
265
|
+
maintained_seams: dedupeCandidateRecords(candidates.maintained_seams || [], idHint)
|
|
265
266
|
};
|
|
266
267
|
}
|
|
267
268
|
if (track === "api") {
|
|
@@ -283,6 +284,7 @@ export function normalizeCandidatesForTrack(track, candidates) {
|
|
|
283
284
|
screens: dedupeCandidateRecords(candidates.screens || [], idHint),
|
|
284
285
|
routes: dedupeCandidateRecords(candidates.routes || [], idHint),
|
|
285
286
|
actions: dedupeCandidateRecords(candidates.actions || [], idHint),
|
|
287
|
+
flows: dedupeCandidateRecords(candidates.flows || [], idHint),
|
|
286
288
|
widgets,
|
|
287
289
|
shapes: dedupeCandidateRecords([...(candidates.shapes || []), ...eventShapes], idHint),
|
|
288
290
|
stacks: [...new Set(candidates.stacks || [])].sort()
|
|
@@ -329,6 +331,7 @@ export function enrichUiWidgetDataSources(uiCandidates, allCandidates) {
|
|
|
329
331
|
const { components, ...canonicalCandidates } = uiCandidates;
|
|
330
332
|
return {
|
|
331
333
|
...canonicalCandidates,
|
|
334
|
+
flows: dedupeCandidateRecords(canonicalCandidates.flows || [], (/** @type {any} */ flow) => flow.id_hint),
|
|
332
335
|
widgets: widgets.map((widget) => {
|
|
333
336
|
const dataSource = inferredDataSourceForWidget(widget, allCandidates);
|
|
334
337
|
const dataProp = widget.data_prop || "rows";
|
|
@@ -10,8 +10,14 @@ 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 || []).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
|
+
]);
|
|
13
19
|
return ensureTrailingNewline(
|
|
14
|
-
`# 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`
|
|
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`
|
|
15
21
|
);
|
|
16
22
|
}
|
|
17
23
|
if (track === "api") {
|
|
@@ -22,11 +28,15 @@ export function reportMarkdown(track, candidates) {
|
|
|
22
28
|
if (track === "ui") {
|
|
23
29
|
const widgets = uiWidgetCandidates(candidates);
|
|
24
30
|
const shapes = candidates.shapes || [];
|
|
31
|
+
const flows = candidates.flows || [];
|
|
25
32
|
const widgetLines = widgets.map((widget) =>
|
|
26
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}`
|
|
27
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
|
+
);
|
|
28
38
|
return ensureTrailingNewline(
|
|
29
|
-
`# 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`
|
|
30
40
|
);
|
|
31
41
|
}
|
|
32
42
|
if (track === "cli") {
|
|
@@ -54,6 +64,6 @@ export function reportMarkdown(track, candidates) {
|
|
|
54
64
|
*/
|
|
55
65
|
export function appReportMarkdown(candidates, tracks) {
|
|
56
66
|
return ensureTrailingNewline(
|
|
57
|
-
`# App Import Report\n\nTracks: ${tracks.join(", ")}\n\n## DB\n\n- Entities: ${candidates.db?.entities?.length || 0}\n- Enums: ${candidates.db?.enums?.length || 0}\n- Relations: ${candidates.db?.relations?.length || 0}\n\n## API\n\n- Capabilities: ${candidates.api?.capabilities?.length || 0}\n- Routes: ${candidates.api?.routes?.length || 0}\n- Stacks: ${candidates.api?.stacks?.length ? candidates.api.stacks.join(", ") : "none"}\n\n## UI\n\n- Screens: ${candidates.ui?.screens?.length || 0}\n- Routes: ${candidates.ui?.routes?.length || 0}\n- Actions: ${candidates.ui?.actions?.length || 0}\n- Widgets: ${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`
|
|
58
68
|
);
|
|
59
69
|
}
|
|
@@ -96,13 +96,13 @@ function selectDetectionsForTrack(track, detections) {
|
|
|
96
96
|
*/
|
|
97
97
|
function initialCandidatesForTrack(track) {
|
|
98
98
|
if (track === "db") {
|
|
99
|
-
return { entities: [], enums: [], relations: [], indexes: [] };
|
|
99
|
+
return { entities: [], enums: [], relations: [], indexes: [], maintained_seams: [] };
|
|
100
100
|
}
|
|
101
101
|
if (track === "api") {
|
|
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,11 +5,13 @@ import {
|
|
|
5
5
|
dedupeCandidateRecords,
|
|
6
6
|
findImportFiles,
|
|
7
7
|
idHintify,
|
|
8
|
+
isPrimaryImportSource,
|
|
8
9
|
makeCandidateRecord,
|
|
9
10
|
relativeTo,
|
|
10
11
|
slugify,
|
|
11
12
|
titleCase
|
|
12
13
|
} from "../../core/shared.js";
|
|
14
|
+
import { inferDrizzleMaintainedDbSeams } from "./maintained-seams.js";
|
|
13
15
|
|
|
14
16
|
function splitTopLevelEntries(block) {
|
|
15
17
|
const entries = [];
|
|
@@ -165,21 +167,59 @@ function parseDrizzleTables(schemaText) {
|
|
|
165
167
|
};
|
|
166
168
|
}
|
|
167
169
|
|
|
170
|
+
function drizzleConfigFiles(context) {
|
|
171
|
+
return findImportFiles(context.paths, (filePath) =>
|
|
172
|
+
/drizzle\.config\.(ts|js|mjs|cjs)$/i.test(path.basename(filePath)) &&
|
|
173
|
+
isPrimaryImportSource(context.paths, filePath)
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function configuredSchemaFiles(context, configFiles) {
|
|
178
|
+
const files = [];
|
|
179
|
+
for (const configFile of configFiles) {
|
|
180
|
+
const configText = context.helpers.readTextIfExists(configFile) || "";
|
|
181
|
+
for (const match of configText.matchAll(/\bschema\s*:\s*["'`]([^"'`*]+)["'`]/g)) {
|
|
182
|
+
const absoluteSchemaPath = path.resolve(path.dirname(configFile), match[1]);
|
|
183
|
+
if (context.helpers.readTextIfExists(absoluteSchemaPath) !== null) {
|
|
184
|
+
files.push(absoluteSchemaPath);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return files;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isDrizzleSchemaSource(context, filePath) {
|
|
192
|
+
const text = context.helpers.readTextIfExists(filePath) || "";
|
|
193
|
+
return /\b(?:pgTable|sqliteTable|mysqlTable)\s*\(/.test(text);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function findDrizzleSchemaFiles(context) {
|
|
197
|
+
const configFiles = drizzleConfigFiles(context);
|
|
198
|
+
const conventionalSchemaFiles = findImportFiles(context.paths, (filePath) =>
|
|
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)
|
|
201
|
+
);
|
|
202
|
+
return [...new Set([
|
|
203
|
+
...configuredSchemaFiles(context, configFiles),
|
|
204
|
+
...conventionalSchemaFiles
|
|
205
|
+
])].filter((filePath) => isDrizzleSchemaSource(context, filePath)).sort();
|
|
206
|
+
}
|
|
207
|
+
|
|
168
208
|
export const drizzleExtractor = {
|
|
169
209
|
id: "db.drizzle",
|
|
170
210
|
track: "db",
|
|
171
211
|
detect(context) {
|
|
172
|
-
const hasConfig =
|
|
173
|
-
const hasSchema =
|
|
212
|
+
const hasConfig = drizzleConfigFiles(context).length > 0;
|
|
213
|
+
const hasSchema = findDrizzleSchemaFiles(context).length > 0;
|
|
174
214
|
return {
|
|
175
215
|
score: hasConfig || hasSchema ? 95 : 0,
|
|
176
216
|
reasons: hasConfig || hasSchema ? ["Found Drizzle config/schema source"] : []
|
|
177
217
|
};
|
|
178
218
|
},
|
|
179
219
|
extract(context) {
|
|
180
|
-
const schemaFiles =
|
|
220
|
+
const schemaFiles = findDrizzleSchemaFiles(context);
|
|
181
221
|
const findings = [];
|
|
182
|
-
const candidates = { entities: [], enums: [], relations: [], indexes: [] };
|
|
222
|
+
const candidates = { entities: [], enums: [], relations: [], indexes: [], maintained_seams: [] };
|
|
183
223
|
for (const filePath of schemaFiles) {
|
|
184
224
|
const parsed = parseDrizzleTables(context.helpers.readTextIfExists(filePath) || "");
|
|
185
225
|
const provenance = relativeTo(context.paths.repoRoot, filePath);
|
|
@@ -237,6 +277,7 @@ export const drizzleExtractor = {
|
|
|
237
277
|
candidates.enums = dedupeCandidateRecords(candidates.enums, (record) => record.id_hint);
|
|
238
278
|
candidates.relations = dedupeCandidateRecords(candidates.relations, (record) => record.id_hint);
|
|
239
279
|
candidates.indexes = dedupeCandidateRecords(candidates.indexes, (record) => record.id_hint);
|
|
280
|
+
candidates.maintained_seams = inferDrizzleMaintainedDbSeams(context, schemaFiles);
|
|
240
281
|
return { findings, candidates };
|
|
241
282
|
}
|
|
242
283
|
};
|