@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
|
@@ -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,
|
|
@@ -81,10 +82,11 @@ export function bestContextBundleForCandidate(bundles, candidate) {
|
|
|
81
82
|
|
|
82
83
|
/** @param {ResolvedGraph} graph @param {ImportArtifacts} appImport @param {any} topogramRoot @returns {any} */
|
|
83
84
|
export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
|
|
84
|
-
const dbCandidates = appImport.candidates.db || { entities: [], enums: [] };
|
|
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: [] };
|
|
@@ -151,6 +153,10 @@ export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
|
|
|
151
153
|
bundles.delete(`enum_${enumId}`);
|
|
152
154
|
}
|
|
153
155
|
}
|
|
156
|
+
for (const seam of dbCandidates.maintained_seams || []) {
|
|
157
|
+
const bundle = getOrCreateCandidateBundle(bundles, "database", "Database");
|
|
158
|
+
bundle.maintainedSeams = [...(bundle.maintainedSeams || []), seam];
|
|
159
|
+
}
|
|
154
160
|
for (const entry of apiCandidates.capabilities || []) {
|
|
155
161
|
const matchedCapability = graph ? matchImportedApiCapability(entry, topogramApiCapabilities) : null;
|
|
156
162
|
if (matchedCapability) {
|
|
@@ -205,6 +211,11 @@ export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
|
|
|
205
211
|
const bundle = getOrCreateCandidateBundle(bundles, conceptId, bundleLabelFromConceptId(conceptId || entry.screen_id || entry.id_hint));
|
|
206
212
|
bundle.uiActions.push(entry);
|
|
207
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
|
+
}
|
|
208
219
|
/** @param {WorkflowRecord} entry @returns {any} */
|
|
209
220
|
function widgetConceptId(entry) {
|
|
210
221
|
if (entry.entity_id || entry.concept_id) {
|
|
@@ -330,10 +341,12 @@ export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
|
|
|
330
341
|
bundle.screens.length > 0 ||
|
|
331
342
|
bundle.uiRoutes.length > 0 ||
|
|
332
343
|
bundle.uiActions.length > 0 ||
|
|
344
|
+
bundle.uiFlows.length > 0 ||
|
|
333
345
|
bundle.workflows.length > 0 ||
|
|
334
346
|
bundle.verifications.length > 0 ||
|
|
335
347
|
bundle.workflowStates.length > 0 ||
|
|
336
|
-
bundle.workflowTransitions.length > 0
|
|
348
|
+
bundle.workflowTransitions.length > 0 ||
|
|
349
|
+
(bundle.maintainedSeams || []).length > 0
|
|
337
350
|
)
|
|
338
351
|
.map((/** @type {any} */ bundle) => {
|
|
339
352
|
const sortedBundle = {
|
|
@@ -349,10 +362,12 @@ export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
|
|
|
349
362
|
screens: bundle.screens.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
|
|
350
363
|
uiRoutes: bundle.uiRoutes.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
|
|
351
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)),
|
|
352
366
|
workflows: bundle.workflows.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
|
|
353
367
|
verifications: bundle.verifications.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
|
|
354
368
|
workflowStates: bundle.workflowStates.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
|
|
355
369
|
workflowTransitions: bundle.workflowTransitions.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
|
|
370
|
+
maintainedSeams: (bundle.maintainedSeams || []).sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
|
|
356
371
|
docs: bundle.docs.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id.localeCompare(b.id))
|
|
357
372
|
};
|
|
358
373
|
const mergeHints = buildBundleMergeHints(sortedBundle, canonicalEntityIds);
|
|
@@ -493,6 +508,9 @@ export function buildCandidateModelFiles(graph, appImport, topogramRoot) {
|
|
|
493
508
|
const actions = bundle.uiActions.filter((/** @type {any} */ action) => action.screen_id === screen.id_hint);
|
|
494
509
|
files[`${bundleRoot}/docs/reports/ui-${screen.id_hint}.md`] = renderCandidateUiReportDoc(screen, routes, actions);
|
|
495
510
|
}
|
|
511
|
+
for (const flow of bundle.uiFlows || []) {
|
|
512
|
+
files[`${bundleRoot}/docs/reports/ui-flow-${flow.id_hint}.md`] = renderCandidateUiFlowDoc(flow);
|
|
513
|
+
}
|
|
496
514
|
for (const patch of bundle.projectionPatches || []) {
|
|
497
515
|
files[`${bundleRoot}/${patch.patch_rel_path}`] = renderProjectionPatchDoc(patch);
|
|
498
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";
|
|
@@ -91,6 +91,7 @@ export function reconcileWorkflow(inputPath, options = {}) {
|
|
|
91
91
|
type: "reconcile_adoption_plan",
|
|
92
92
|
workspace: paths.topogramRoot,
|
|
93
93
|
approved_review_groups: [...new Set(existingPlan?.approved_review_groups || [])],
|
|
94
|
+
imported_maintained_db_seams: appImport.candidates?.db?.maintained_seams || [],
|
|
94
95
|
items: mergedPlanItems,
|
|
95
96
|
projection_review_groups: buildProjectionReviewGroups(mergedPlanItems),
|
|
96
97
|
ui_review_groups: buildUiReviewGroups(mergedPlanItems),
|
|
@@ -261,6 +262,7 @@ export function reconcileWorkflow(inputPath, options = {}) {
|
|
|
261
262
|
widgets: bundle.widgets.map((/** @type {any} */ entry) => entry.id_hint),
|
|
262
263
|
cli_surfaces: (bundle.cliSurfaces || []).map((/** @type {any} */ entry) => entry.id_hint),
|
|
263
264
|
screens: bundle.screens.map((/** @type {any} */ entry) => entry.id_hint),
|
|
265
|
+
ui_flows: (bundle.uiFlows || []).map((/** @type {any} */ entry) => entry.id_hint),
|
|
264
266
|
workflows: bundle.workflows.map((/** @type {any} */ entry) => entry.id_hint),
|
|
265
267
|
docs: bundle.docs.map((/** @type {any} */ entry) => entry.id),
|
|
266
268
|
maintained_seam_candidates: (agentAdoptionPlan.imported_proposal_surfaces || [])
|
|
@@ -283,7 +285,7 @@ export function reconcileWorkflow(inputPath, options = {}) {
|
|
|
283
285
|
: "## Promoted Canonical Items";
|
|
284
286
|
files["candidates/reconcile/report.json"] = `${stableStringify(report)}\n`;
|
|
285
287
|
const candidateModelBundlesMarkdown = report.candidate_model_bundles.length
|
|
286
|
-
? 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)
|
|
287
289
|
- primary concept \`${bundle.operator_summary.primaryConcept}\`${bundle.operator_summary.primaryEntityId ? `, primary entity \`${bundle.operator_summary.primaryEntityId}\`` : ""}
|
|
288
290
|
- participants ${bundle.operator_summary.participants.label}
|
|
289
291
|
- main capabilities ${summarizeBundleSurface(bundle, bundle.operator_summary.capabilityIds)}
|