@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.
@@ -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)}