@topogram/cli 0.3.77 → 0.3.79

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.
Files changed (129) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/package.json +2 -2
  3. package/src/agent-brief.js +29 -23
  4. package/src/agent-ops/query-builders/change-risk/{import-plan.js → extract-plan.js} +1 -1
  5. package/src/agent-ops/query-builders/change-risk/review-packets.js +5 -5
  6. package/src/agent-ops/query-builders/change-risk.js +1 -1
  7. package/src/agent-ops/query-builders/common.js +2 -2
  8. package/src/agent-ops/query-builders/multi-agent.js +1 -1
  9. package/src/agent-ops/query-builders/workflow-context-shared.js +4 -4
  10. package/src/catalog/provenance.js +1 -1
  11. package/src/cli/catalog-alias.d.ts +2 -0
  12. package/src/cli/catalog-alias.js +2 -2
  13. package/src/cli/command-parsers/core.js +9 -5
  14. package/src/cli/command-parsers/import.js +11 -17
  15. package/src/cli/command-parsers/project.js +0 -3
  16. package/src/cli/commands/catalog/copy.js +3 -3
  17. package/src/cli/commands/catalog/help.js +1 -2
  18. package/src/cli/commands/catalog/list.js +7 -4
  19. package/src/cli/commands/catalog/show.js +4 -4
  20. package/src/cli/commands/copy.js +356 -0
  21. package/src/cli/commands/doctor.js +1 -1
  22. package/src/cli/commands/import/adopt.js +9 -9
  23. package/src/cli/commands/import/check.js +15 -15
  24. package/src/cli/commands/import/diff.js +6 -6
  25. package/src/cli/commands/import/help.js +43 -34
  26. package/src/cli/commands/import/paths.js +3 -3
  27. package/src/cli/commands/import/plan.js +8 -8
  28. package/src/cli/commands/import/refresh.js +25 -24
  29. package/src/cli/commands/import/status-history.js +4 -4
  30. package/src/cli/commands/import/workspace.js +16 -16
  31. package/src/cli/commands/import-runner.js +6 -5
  32. package/src/cli/commands/import.js +4 -1
  33. package/src/cli/commands/init.js +67 -0
  34. package/src/cli/commands/query/{import-adopt.js → extract-adopt.js} +2 -2
  35. package/src/cli/commands/query/runner/change.js +2 -2
  36. package/src/cli/commands/query/runner/{import-adopt.js → extract-adopt.js} +9 -9
  37. package/src/cli/commands/query/runner/index.js +1 -1
  38. package/src/cli/commands/query/runner/workflow.js +7 -7
  39. package/src/cli/commands/query/workspace.js +4 -4
  40. package/src/cli/commands/release-status.js +2 -2
  41. package/src/cli/commands/source.js +2 -2
  42. package/src/cli/commands/template/check.js +2 -2
  43. package/src/cli/commands/template/list-show.js +4 -4
  44. package/src/cli/dispatcher.js +18 -3
  45. package/src/cli/help-dispatch.js +22 -8
  46. package/src/cli/help.js +68 -52
  47. package/src/cli/migration-guidance.js +9 -0
  48. package/src/generator/context/bundle.js +14 -7
  49. package/src/generator/context/diff.js +8 -1
  50. package/src/generator/context/digest.js +10 -1
  51. package/src/generator/context/shared/domain-sdlc.js +5 -1
  52. package/src/generator/context/shared/relationships.js +20 -5
  53. package/src/generator/context/shared/summaries.js +26 -0
  54. package/src/generator/context/shared.d.ts +1 -0
  55. package/src/generator/context/shared.js +1 -0
  56. package/src/generator/context/slice/core.js +9 -5
  57. package/src/generator/context/slice/sdlc.js +31 -2
  58. package/src/generator/context/task-mode.js +3 -3
  59. package/src/import/core/runner/reports.js +4 -4
  60. package/src/import/core/shared/files.js +21 -2
  61. package/src/import/core/shared.js +2 -1
  62. package/src/import/enrichers/django-rest.js +4 -4
  63. package/src/import/enrichers/rails-controllers.js +3 -3
  64. package/src/import/enrichers/rails-models.js +3 -3
  65. package/src/import/extractors/api/aspnet-core.js +5 -5
  66. package/src/import/extractors/api/django-routes.js +5 -5
  67. package/src/import/extractors/api/express.js +4 -4
  68. package/src/import/extractors/api/fastify.js +7 -7
  69. package/src/import/extractors/api/flutter-dio.js +4 -4
  70. package/src/import/extractors/api/generic-route-fallback.js +2 -2
  71. package/src/import/extractors/api/graphql-code-first.js +3 -3
  72. package/src/import/extractors/api/graphql-sdl.js +5 -5
  73. package/src/import/extractors/api/jaxrs.js +3 -3
  74. package/src/import/extractors/api/micronaut.js +3 -3
  75. package/src/import/extractors/api/openapi-code.js +4 -4
  76. package/src/import/extractors/api/openapi.js +3 -3
  77. package/src/import/extractors/api/rails-routes.js +3 -3
  78. package/src/import/extractors/api/react-native-repository.js +3 -3
  79. package/src/import/extractors/api/retrofit.js +3 -3
  80. package/src/import/extractors/api/spring-web.js +3 -3
  81. package/src/import/extractors/api/swift-webapi.js +3 -3
  82. package/src/import/extractors/api/trpc.js +4 -4
  83. package/src/import/extractors/cli/generic.js +3 -3
  84. package/src/import/extractors/db/django-models.js +4 -4
  85. package/src/import/extractors/db/dotnet-models.js +4 -4
  86. package/src/import/extractors/db/drizzle.js +9 -7
  87. package/src/import/extractors/db/ef-core.js +5 -5
  88. package/src/import/extractors/db/flutter-entities.js +3 -3
  89. package/src/import/extractors/db/jpa.js +3 -3
  90. package/src/import/extractors/db/liquibase.js +3 -3
  91. package/src/import/extractors/db/maintained-seams.js +4 -4
  92. package/src/import/extractors/db/mybatis-xml.js +4 -4
  93. package/src/import/extractors/db/prisma.js +3 -3
  94. package/src/import/extractors/db/rails-schema.js +3 -3
  95. package/src/import/extractors/db/react-native-entities.js +3 -3
  96. package/src/import/extractors/db/room.js +5 -5
  97. package/src/import/extractors/db/snapshot.js +3 -3
  98. package/src/import/extractors/db/sql.js +3 -3
  99. package/src/import/extractors/db/swiftdata.js +3 -3
  100. package/src/import/extractors/ui/android-compose.js +4 -4
  101. package/src/import/extractors/ui/backend-only.js +3 -3
  102. package/src/import/extractors/ui/blazor.js +3 -3
  103. package/src/import/extractors/ui/flutter-screens.js +3 -3
  104. package/src/import/extractors/ui/maui-xaml.js +4 -4
  105. package/src/import/extractors/ui/next-pages-router.js +3 -3
  106. package/src/import/extractors/ui/razor-pages.js +3 -3
  107. package/src/import/extractors/ui/react-native-screens.js +4 -4
  108. package/src/import/extractors/ui/swiftui.js +3 -3
  109. package/src/import/extractors/ui/uikit.js +3 -3
  110. package/src/import/provenance.js +16 -16
  111. package/src/init-project.js +215 -0
  112. package/src/new-project/constants.js +1 -1
  113. package/src/new-project/create.js +2 -2
  114. package/src/new-project/project-files.js +7 -7
  115. package/src/reconcile/journeys.js +8 -3
  116. package/src/record-blocks.js +125 -0
  117. package/src/resolver/index.js +3 -0
  118. package/src/resolver/journeys.js +74 -0
  119. package/src/resolver/normalize.js +25 -0
  120. package/src/sdlc/adopt.js +1 -1
  121. package/src/validator/common.js +34 -1
  122. package/src/validator/index.js +4 -0
  123. package/src/validator/kinds.d.ts +2 -0
  124. package/src/validator/kinds.js +34 -1
  125. package/src/validator/per-kind/journey.js +233 -0
  126. package/src/workflows/docs-generate.js +4 -1
  127. package/src/workflows/reconcile/bundle-core/index.js +4 -2
  128. package/src/workflows/reconcile/canonical-surface.js +4 -1
  129. package/src/cli/commands/new.js +0 -94
@@ -97,6 +97,30 @@ export function summarizeJourneyDoc(doc) {
97
97
  };
98
98
  }
99
99
 
100
+ /**
101
+ * @param {import("./types.d.ts").ContextStatement} journey
102
+ * @returns {any}
103
+ */
104
+ export function summarizeJourney(journey) {
105
+ return {
106
+ id: journey.id,
107
+ kind: journey.kind,
108
+ name: journey.name || journey.id,
109
+ description: journey.description || null,
110
+ status: journey.status || null,
111
+ goal: journey.goal || null,
112
+ actors: stableSortedStrings(journey.actors || []),
113
+ relatedCapabilities: stableSortedStrings(journey.relatedCapabilities || []),
114
+ relatedWorkflows: stableSortedStrings(journey.relatedWorkflows || []),
115
+ relatedProjections: stableSortedStrings(journey.relatedProjections || []),
116
+ relatedWidgets: stableSortedStrings(journey.relatedWidgets || []),
117
+ stepCount: (journey.steps || []).length,
118
+ alternateCount: (journey.alternates || []).length,
119
+ reviewBoundary: reviewBoundaryForJourneyDocPolicy(journey),
120
+ ownership_boundary: defaultOwnershipBoundary()
121
+ };
122
+ }
123
+
100
124
  /**
101
125
  * @param {import("./types.d.ts").ContextStatement} statement
102
126
  * @returns {any}
@@ -163,6 +187,8 @@ export function summarizeStatement(statement) {
163
187
  };
164
188
  case "widget":
165
189
  return summarizeComponent(statement);
190
+ case "journey":
191
+ return summarizeJourney(statement);
166
192
  case "shape":
167
193
  return {
168
194
  id: statement.id,
@@ -34,6 +34,7 @@ export function summarizeById(...args: any[]): any;
34
34
  export function summarizeDocsByIds(...args: any[]): any;
35
35
  export function summarizeDocument(...args: any[]): any;
36
36
  export function summarizeDomain(...args: any[]): any;
37
+ export function summarizeJourneyLikeByIds(...args: any[]): any;
37
38
  export function summarizePitch(...args: any[]): any;
38
39
  export function summarizePlan(...args: any[]): any;
39
40
  export function summarizeProjection(...args: any[]): any;
@@ -20,6 +20,7 @@ export {
20
20
  summarizeById,
21
21
  summarizeStatementsByIds,
22
22
  summarizeDocsByIds,
23
+ summarizeJourneyLikeByIds,
23
24
  workspaceInventory
24
25
  } from "./shared/relationships.js";
25
26
  export {
@@ -20,6 +20,7 @@ import {
20
20
  relatedWorkflowDocsForCapability,
21
21
  summarizeById,
22
22
  summarizeDocsByIds,
23
+ summarizeJourneyLikeByIds,
23
24
  summarizeProjection,
24
25
  summarizeStatementsByIds,
25
26
  verificationIdsForTarget,
@@ -87,7 +88,7 @@ function capabilitySlice(graph, capabilityId) {
87
88
  entities: summarizeStatementsByIds(graph, entities),
88
89
  rules: summarizeStatementsByIds(graph, rules),
89
90
  workflows: summarizeDocsByIds(graph, workflows),
90
- journeys: summarizeDocsByIds(graph, journeys),
91
+ journeys: summarizeJourneyLikeByIds(graph, journeys),
91
92
  projections: summarizeStatementsByIds(graph, projections)
92
93
  },
93
94
  verification: summarizeStatementsByIds(graph, verifications),
@@ -118,9 +119,12 @@ function workflowSlice(graph, workflowId) {
118
119
  ];
119
120
  }))].sort();
120
121
  const rules = [...new Set(capabilities.flatMap(/** @param {string} capabilityId */ (capabilityId) => relatedRulesForTarget(graph, capabilityId)))].sort();
121
- const journeys = (graph.docs || [])
122
- .filter(/** @param {any} doc */ (doc) => doc.kind === "journey" && (doc.relatedWorkflows || []).includes(workflowId))
123
- .map(/** @param {any} doc */ (doc) => doc.id)
122
+ const journeys = [
123
+ ...(graph.byKind?.journey || []),
124
+ ...(graph.docs || []).filter(/** @param {any} doc */ (doc) => doc.kind === "journey")
125
+ ]
126
+ .filter(/** @param {any} journey */ (journey) => (journey.relatedWorkflows || []).includes(workflowId))
127
+ .map(/** @param {any} journey */ (journey) => journey.id)
124
128
  .sort();
125
129
  const verifications = verificationIdsForTarget(graph, [...capabilities, ...entities, workflowId]);
126
130
 
@@ -143,7 +147,7 @@ function workflowSlice(graph, workflowId) {
143
147
  capabilities: summarizeStatementsByIds(graph, capabilities),
144
148
  entities: summarizeStatementsByIds(graph, entities),
145
149
  rules: summarizeStatementsByIds(graph, rules),
146
- journeys: summarizeDocsByIds(graph, journeys)
150
+ journeys: summarizeJourneyLikeByIds(graph, journeys)
147
151
  },
148
152
  verification: summarizeStatementsByIds(graph, verifications),
149
153
  verification_targets: recommendedVerificationTargets(graph, [...capabilities, ...entities, workflowId], {
@@ -49,9 +49,16 @@ import {
49
49
  export function journeySlice(graph, journeyId) {
50
50
  const journey = getJourneyDoc(graph, journeyId);
51
51
  const capabilities = [...(journey.relatedCapabilities || [])].sort();
52
+ const entities = [...(journey.relatedEntities || [])].sort();
53
+ const rules = [...(journey.relatedRules || [])].sort();
52
54
  const workflows = [...(journey.relatedWorkflows || [])].sort();
53
55
  const projections = [...(journey.relatedProjections || [])].sort();
54
- const verifications = verificationIdsForTarget(graph, [...capabilities, ...workflows, ...projections, journeyId]);
56
+ const widgets = [...(journey.relatedWidgets || [])].sort();
57
+ const declaredVerifications = [...(journey.relatedVerifications || [])].sort();
58
+ const verifications = [...new Set([
59
+ ...declaredVerifications,
60
+ ...verificationIdsForTarget(graph, [...capabilities, ...workflows, ...projections, ...widgets, journeyId])
61
+ ])].sort();
55
62
 
56
63
  return {
57
64
  type: "context_slice",
@@ -63,14 +70,36 @@ export function journeySlice(graph, journeyId) {
63
70
  summary: summarizeById(graph, journeyId),
64
71
  depends_on: {
65
72
  capabilities,
73
+ entities,
74
+ rules,
66
75
  workflows,
67
76
  projections,
77
+ widgets,
68
78
  verifications
69
79
  },
80
+ steps: (journey.steps || []).map(/** @param {any} step */ (step) => ({
81
+ id: step.id,
82
+ intent: step.intent,
83
+ commands: [...(step.commands || [])],
84
+ expects: [...(step.expects || [])],
85
+ after: [...(step.after || [])],
86
+ notes: step.notes || null
87
+ })),
88
+ alternates: (journey.alternates || []).map(/** @param {any} alternate */ (alternate) => ({
89
+ id: alternate.id,
90
+ from: alternate.from,
91
+ condition: alternate.condition,
92
+ commands: [...(alternate.commands || [])],
93
+ expects: [...(alternate.expects || [])],
94
+ notes: alternate.notes || null
95
+ })),
70
96
  related: {
71
97
  capabilities: summarizeStatementsByIds(graph, capabilities),
98
+ entities: summarizeStatementsByIds(graph, entities),
99
+ rules: summarizeStatementsByIds(graph, rules),
72
100
  workflows: summarizeDocsByIds(graph, workflows),
73
- projections: summarizeStatementsByIds(graph, projections)
101
+ projections: summarizeStatementsByIds(graph, projections),
102
+ widgets: summarizeStatementsByIds(graph, widgets)
74
103
  },
75
104
  verification: summarizeStatementsByIds(graph, verifications),
76
105
  verification_targets: recommendedVerificationTargets(graph, [...capabilities, ...workflows, ...projections, journeyId], {
@@ -325,7 +325,7 @@ function importAdoptMode(graph, options = {}) {
325
325
  return {
326
326
  type: "context_task_mode",
327
327
  version: 1,
328
- mode: "import-adopt",
328
+ mode: "extract-adopt",
329
329
  summary: {
330
330
  focus: "Proposal review and adoption planning",
331
331
  preferred_start: "adoption-plan.agent.json",
@@ -450,7 +450,7 @@ function verificationMode(graph, options = {}) {
450
450
  export function generateContextTaskMode(graph, options = {}) {
451
451
  const mode = String(options.modeId || "").trim();
452
452
  if (!mode) {
453
- throw new Error("context-task-mode requires --mode <modeling|maintained-app-edit|import-adopt|diff-review|verification>");
453
+ throw new Error("context-task-mode requires --mode <modeling|maintained-app-edit|extract-adopt|diff-review|verification>");
454
454
  }
455
455
 
456
456
  if (mode === "modeling") {
@@ -459,7 +459,7 @@ export function generateContextTaskMode(graph, options = {}) {
459
459
  if (mode === "maintained-app-edit") {
460
460
  return maintainedAppEditMode(graph, options);
461
461
  }
462
- if (mode === "import-adopt") {
462
+ if (mode === "extract-adopt") {
463
463
  return importAdoptMode(graph, options);
464
464
  }
465
465
  if (mode === "diff-review") {
@@ -17,12 +17,12 @@ export function reportMarkdown(track, candidates) {
17
17
  ` - manual next: ${(seam.manual_next_steps || []).slice(0, 2).join(" ")}`
18
18
  ]);
19
19
  return ensureTrailingNewline(
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`
20
+ `# DB Extract 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`
21
21
  );
22
22
  }
23
23
  if (track === "api") {
24
24
  return ensureTrailingNewline(
25
- `# API Import Report\n\n- Capabilities: ${candidates.capabilities.length}\n- Routes: ${candidates.routes.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n`
25
+ `# API Extract Report\n\n- Capabilities: ${candidates.capabilities.length}\n- Routes: ${candidates.routes.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n`
26
26
  );
27
27
  }
28
28
  if (track === "ui") {
@@ -36,7 +36,7 @@ export function reportMarkdown(track, candidates) {
36
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
37
  );
38
38
  return ensureTrailingNewline(
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`
39
+ `# UI Extract 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 extract plan <path>\` before adoption.\n- After adoption, run \`topogram check <path>\`, \`topogram widget check <path>\`, and \`topogram widget behavior <path>\`.\n`
40
40
  );
41
41
  }
42
42
  if (track === "cli") {
@@ -44,7 +44,7 @@ export function reportMarkdown(track, candidates) {
44
44
  `- \`${command.command_id || command.id_hint}\` usage \`${command.usage || "unknown"}\` effects ${(command.effects || []).map((/** @type {any} */ effect) => `\`${effect}\``).join(", ") || "_none_"}`
45
45
  );
46
46
  return ensureTrailingNewline(
47
- `# CLI Import Report\n\n- Commands: ${candidates.commands.length}\n- Capabilities: ${candidates.capabilities.length}\n- CLI surfaces: ${candidates.surfaces.length}\n\n## Command Candidates\n\n${commandLines.length ? commandLines.join("\n") : "- none"}\n`
47
+ `# CLI Extract Report\n\n- Commands: ${candidates.commands.length}\n- Capabilities: ${candidates.capabilities.length}\n- CLI surfaces: ${candidates.surfaces.length}\n\n## Command Candidates\n\n${commandLines.length ? commandLines.join("\n") : "- none"}\n`
48
48
  );
49
49
  }
50
50
  if (track === "verification") {
@@ -107,7 +107,7 @@ export function classifyImportSourcePath(paths, filePath) {
107
107
  if (/(^|\/)(\.tmp|tmp|temp|dist|build|coverage|out|generated|docs-generated|snapshots?)(\/|$)/i.test(relativePath)) {
108
108
  return "generated_output";
109
109
  }
110
- if (/(^|\/)[^/]*templates?[^/]*(\/|$)/i.test(relativePath)) {
110
+ if (/^(template|templates|[^/]+-templates|[^/]+_templates)(\/|$)/i.test(relativePath)) {
111
111
  return "fixtures";
112
112
  }
113
113
  if (/(^|\/)(test|tests|__tests__|spec|specs|mocks?)(\/|$)|\.(test|spec)\.(js|jsx|ts|tsx|mjs|cjs)$/i.test(relativePath)) {
@@ -213,9 +213,10 @@ export function selectPreferredImportFiles(paths, files, kind) {
213
213
  /**
214
214
  * @param {import("./types.d.ts").ImportPaths} paths
215
215
  * @param {any} predicate
216
+ * @param {{ primaryOnly?: boolean }} [options]
216
217
  * @returns {any}
217
218
  */
218
- export function findImportFiles(paths, predicate) {
219
+ export function findImportFiles(paths, predicate, options = {}) {
219
220
  const files = new Set();
220
221
  for (const rootDir of importSearchRoots(paths)) {
221
222
  for (const filePath of listFilesRecursive(rootDir, predicate)) {
@@ -226,8 +227,26 @@ export function findImportFiles(paths, predicate) {
226
227
  ) {
227
228
  continue;
228
229
  }
230
+ if (options.primaryOnly && !isPrimaryImportSource(paths, filePath)) {
231
+ continue;
232
+ }
229
233
  files.add(filePath);
230
234
  }
231
235
  }
232
236
  return [...files].sort();
233
237
  }
238
+
239
+ /**
240
+ * Find files that are eligible to create primary import candidates.
241
+ *
242
+ * Docs, tests, fixture/template roots, generated output, and cache output may
243
+ * still support evidence elsewhere, but candidate-producing extractors should
244
+ * start from this helper so the primary-source rule is one API boundary.
245
+ *
246
+ * @param {import("./types.d.ts").ImportPaths} paths
247
+ * @param {any} predicate
248
+ * @returns {any}
249
+ */
250
+ export function findPrimaryImportFiles(paths, predicate) {
251
+ return findImportFiles(paths, predicate, { primaryOnly: true });
252
+ }
@@ -24,7 +24,8 @@ export {
24
24
  isPrimaryImportSource,
25
25
  canonicalSourceRank,
26
26
  selectPreferredImportFiles,
27
- findImportFiles
27
+ findImportFiles,
28
+ findPrimaryImportFiles
28
29
  } from "./shared/files.js";
29
30
  export {
30
31
  normalizeOpenApiPath,
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
 
3
- import { findImportFiles, readTextIfExists } from "../core/shared.js";
3
+ import { findPrimaryImportFiles, readTextIfExists } from "../core/shared.js";
4
4
 
5
5
  function splitClassBlocks(text) {
6
6
  const lines = String(text || "").split(/\r?\n/);
@@ -51,7 +51,7 @@ function splitClassBlocks(text) {
51
51
  }
52
52
 
53
53
  function buildSerializerIndex(paths) {
54
- const files = findImportFiles(paths, (filePath) => /\/serializers\.py$/i.test(filePath));
54
+ const files = findPrimaryImportFiles(paths, (filePath) => /\/serializers\.py$/i.test(filePath));
55
55
  const index = new Map();
56
56
 
57
57
  for (const filePath of files) {
@@ -89,7 +89,7 @@ function buildSerializerIndex(paths) {
89
89
  }
90
90
 
91
91
  function buildViewIndex(paths) {
92
- const files = findImportFiles(paths, (filePath) => /\/views\.py$/i.test(filePath));
92
+ const files = findPrimaryImportFiles(paths, (filePath) => /\/views\.py$/i.test(filePath));
93
93
  const index = new Map();
94
94
 
95
95
  for (const filePath of files) {
@@ -182,7 +182,7 @@ export const djangoRestEnricher = {
182
182
  applies(context, candidates) {
183
183
  if ((candidates.capabilities || []).length === 0) return false;
184
184
  return (candidates.stacks || []).includes("django") ||
185
- findImportFiles(context.paths, (filePath) => /\/serializers\.py$/i.test(filePath)).length > 0;
185
+ findPrimaryImportFiles(context.paths, (filePath) => /\/serializers\.py$/i.test(filePath)).length > 0;
186
186
  },
187
187
  enrich(context, candidates) {
188
188
  const serializerIndex = buildSerializerIndex(context.paths);
@@ -1,9 +1,9 @@
1
1
  import path from "node:path";
2
2
 
3
- import { findImportFiles, readTextIfExists } from "../core/shared.js";
3
+ import { findPrimaryImportFiles, readTextIfExists } from "../core/shared.js";
4
4
 
5
5
  function buildControllerIndex(paths) {
6
- const files = findImportFiles(paths, (filePath) => /app\/controllers\/.+_controller\.rb$/i.test(filePath));
6
+ const files = findPrimaryImportFiles(paths, (filePath) => /app\/controllers\/.+_controller\.rb$/i.test(filePath));
7
7
  const index = new Map();
8
8
  for (const filePath of files) {
9
9
  const text = readTextIfExists(filePath) || "";
@@ -186,7 +186,7 @@ export const railsControllerEnricher = {
186
186
  applies(context, candidates) {
187
187
  if ((candidates.capabilities || []).length === 0) return false;
188
188
  return (candidates.stacks || []).includes("rails") ||
189
- findImportFiles(context.paths, (filePath) => /app\/controllers\/.+_controller\.rb$/i.test(filePath)).length > 0;
189
+ findPrimaryImportFiles(context.paths, (filePath) => /app\/controllers\/.+_controller\.rb$/i.test(filePath)).length > 0;
190
190
  },
191
191
  enrich(context, candidates) {
192
192
  const controllers = buildControllerIndex(context.paths);
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
 
3
- import { findImportFiles, readTextIfExists } from "../core/shared.js";
3
+ import { findPrimaryImportFiles, readTextIfExists } from "../core/shared.js";
4
4
 
5
5
  function modelClassNameForEntity(entityId) {
6
6
  const stem = String(entityId || "")
@@ -13,7 +13,7 @@ function modelClassNameForEntity(entityId) {
13
13
  }
14
14
 
15
15
  function buildModelIndex(paths) {
16
- const modelFiles = findImportFiles(paths, (filePath) => /app\/models\/.+\.rb$/i.test(filePath));
16
+ const modelFiles = findPrimaryImportFiles(paths, (filePath) => /app\/models\/.+\.rb$/i.test(filePath));
17
17
  const index = new Map();
18
18
  for (const filePath of modelFiles) {
19
19
  const text = readTextIfExists(filePath) || "";
@@ -93,7 +93,7 @@ export const railsModelEnricher = {
93
93
  track: "db",
94
94
  applies(context, candidates) {
95
95
  if ((candidates.entities || []).length === 0) return false;
96
- return findImportFiles(context.paths, (filePath) => /app\/models\/.+\.rb$/i.test(filePath)).length > 0;
96
+ return findPrimaryImportFiles(context.paths, (filePath) => /app\/models\/.+\.rb$/i.test(filePath)).length > 0;
97
97
  },
98
98
  enrich(context, candidates) {
99
99
  const modelIndex = buildModelIndex(context.paths);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  dedupeCandidateRecords,
3
- findImportFiles,
3
+ findPrimaryImportFiles,
4
4
  inferApiEntityIdFromPath,
5
5
  inferRouteCapabilityId,
6
6
  makeCandidateRecord,
@@ -22,7 +22,7 @@ function parsePublicProperties(text) {
22
22
  }
23
23
 
24
24
  function buildDotnetFileIndex(paths) {
25
- const files = findImportFiles(paths, (filePath) => /\.cs$/i.test(filePath));
25
+ const files = findPrimaryImportFiles(paths, (filePath) => /\.cs$/i.test(filePath));
26
26
  const index = new Map();
27
27
  for (const filePath of files) {
28
28
  index.set(relativeTo(paths.repoRoot, filePath), readTextIfExists(filePath) || "");
@@ -239,8 +239,8 @@ export const aspNetCoreExtractor = {
239
239
  id: "api.aspnet-core",
240
240
  track: "api",
241
241
  detect(context) {
242
- const controllerFiles = findImportFiles(context.paths, (filePath) => /Controller\.cs$/i.test(filePath));
243
- const programFiles = findImportFiles(context.paths, (filePath) => /Program\.cs$/i.test(filePath));
242
+ const controllerFiles = findPrimaryImportFiles(context.paths, (filePath) => /Controller\.cs$/i.test(filePath));
243
+ const programFiles = findPrimaryImportFiles(context.paths, (filePath) => /Program\.cs$/i.test(filePath));
244
244
  const score = controllerFiles.length > 0 && programFiles.some((filePath) => /WebApplication\.CreateBuilder|AddSwaggerGen|AddMvc/.test(readTextIfExists(filePath) || "")) ? 88 : 0;
245
245
  return {
246
246
  score,
@@ -248,7 +248,7 @@ export const aspNetCoreExtractor = {
248
248
  };
249
249
  },
250
250
  extract(context) {
251
- const controllerFiles = findImportFiles(context.paths, (filePath) => /Controller\.cs$/i.test(filePath));
251
+ const controllerFiles = findPrimaryImportFiles(context.paths, (filePath) => /Controller\.cs$/i.test(filePath));
252
252
  const featureFiles = buildDotnetFileIndex(context.paths).files.map((filePath) => ({ filePath, text: readTextIfExists(filePath) || "" }));
253
253
  const findings = [];
254
254
  const candidates = { capabilities: [], routes: [], stacks: [] };
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
 
3
3
  import {
4
4
  dedupeCandidateRecords,
5
- findImportFiles,
5
+ findPrimaryImportFiles,
6
6
  inferApiEntityIdFromPath,
7
7
  inferRouteCapabilityId,
8
8
  makeCandidateRecord,
@@ -90,7 +90,7 @@ function permissionAuthHint(permissionText, method) {
90
90
  }
91
91
 
92
92
  function buildViewIndex(paths) {
93
- const viewFiles = findImportFiles(paths, (filePath) => /\/views\.py$/i.test(filePath));
93
+ const viewFiles = findPrimaryImportFiles(paths, (filePath) => /\/views\.py$/i.test(filePath));
94
94
  const index = new Map();
95
95
 
96
96
  for (const filePath of viewFiles) {
@@ -250,8 +250,8 @@ export const djangoRoutesExtractor = {
250
250
  id: "api.django-routes",
251
251
  track: "api",
252
252
  detect(context) {
253
- const manageFiles = findImportFiles(context.paths, (filePath) => /\/manage\.py$/i.test(filePath));
254
- const urlFiles = findImportFiles(context.paths, (filePath) => /\/urls\.py$/i.test(filePath));
253
+ const manageFiles = findPrimaryImportFiles(context.paths, (filePath) => /\/manage\.py$/i.test(filePath));
254
+ const urlFiles = findPrimaryImportFiles(context.paths, (filePath) => /\/urls\.py$/i.test(filePath));
255
255
  const score = manageFiles.length > 0 && urlFiles.length > 0 ? 90 : 0;
256
256
  return {
257
257
  score,
@@ -259,7 +259,7 @@ export const djangoRoutesExtractor = {
259
259
  };
260
260
  },
261
261
  extract(context) {
262
- const urlFiles = findImportFiles(context.paths, (filePath) => /\/urls\.py$/i.test(filePath));
262
+ const urlFiles = findPrimaryImportFiles(context.paths, (filePath) => /\/urls\.py$/i.test(filePath));
263
263
  const moduleMap = new Map(urlFiles.map((filePath) => [moduleNameForFile(context.paths.repoRoot, filePath), filePath]));
264
264
  const viewIndex = buildViewIndex(context.paths);
265
265
  const rootFiles = urlFiles.filter((filePath) => !/\/apps\//.test(filePath));
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
 
3
3
  import {
4
4
  dedupeCandidateRecords,
5
- findImportFiles,
5
+ findPrimaryImportFiles,
6
6
  inferApiEntityIdFromPath,
7
7
  inferRouteAuthHint,
8
8
  inferRouteCapabilityId,
@@ -94,18 +94,18 @@ export const expressExtractor = {
94
94
  id: "api.express",
95
95
  track: "api",
96
96
  detect(context) {
97
- const routeFiles = findImportFiles(context.paths, (filePath) => /src\/routes\/.+\.(ts|js|mjs|cjs)$/i.test(filePath));
97
+ const routeFiles = findPrimaryImportFiles(context.paths, (filePath) => /src\/routes\/.+\.(ts|js|mjs|cjs)$/i.test(filePath));
98
98
  return {
99
99
  score: routeFiles.length > 0 ? 85 : 0,
100
100
  reasons: routeFiles.length > 0 ? ["Found Express route modules"] : []
101
101
  };
102
102
  },
103
103
  extract(context) {
104
- const permissionsFile = findImportFiles(context.paths, (filePath) => /src\/helpers\/permissions\.(ts|js|mjs|cjs)$/i.test(filePath))[0];
104
+ const permissionsFile = findPrimaryImportFiles(context.paths, (filePath) => /src\/helpers\/permissions\.(ts|js|mjs|cjs)$/i.test(filePath))[0];
105
105
  const permissionsText = permissionsFile ? context.helpers.readTextIfExists(permissionsFile) || "" : "";
106
106
  const apiRoutes = parseApiRoutesMap(permissionsText);
107
107
  const permissionMeta = parsePermissionsMetadata(permissionsText);
108
- const routeFiles = findImportFiles(context.paths, (filePath) => /src\/routes\/.+\.(ts|js|mjs|cjs)$/i.test(filePath));
108
+ const routeFiles = findPrimaryImportFiles(context.paths, (filePath) => /src\/routes\/.+\.(ts|js|mjs|cjs)$/i.test(filePath));
109
109
  const routes = routeFiles.flatMap((filePath) =>
110
110
  parseExpressRouteCalls(filePath, context.helpers.readTextIfExists(filePath) || "", apiRoutes, permissionMeta)
111
111
  );
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
 
3
3
  import {
4
4
  dedupeCandidateRecords,
5
- findImportFiles,
5
+ findPrimaryImportFiles,
6
6
  inferApiCapabilityIdFromOperation,
7
7
  inferApiEntityIdFromPath,
8
8
  makeCandidateRecord,
@@ -263,8 +263,8 @@ export const fastifyExtractor = {
263
263
  id: "api.fastify",
264
264
  track: "api",
265
265
  detect(context) {
266
- const routeFiles = findImportFiles(context.paths, (filePath) => /src\/routes\/api\/.+\.(ts|js|mjs|cjs)$/i.test(filePath));
267
- const packageJsonFiles = findImportFiles(context.paths, (filePath) => /package\.json$/i.test(filePath));
266
+ const routeFiles = findPrimaryImportFiles(context.paths, (filePath) => /src\/routes\/api\/.+\.(ts|js|mjs|cjs)$/i.test(filePath));
267
+ const packageJsonFiles = findPrimaryImportFiles(context.paths, (filePath) => /package\.json$/i.test(filePath));
268
268
  const hasFastifyDependency = packageJsonFiles.some((filePath) => /"fastify"\s*:/.test(context.helpers.readTextIfExists(filePath) || ""));
269
269
  return {
270
270
  score: routeFiles.length > 0 && hasFastifyDependency ? 86 : 0,
@@ -272,16 +272,16 @@ export const fastifyExtractor = {
272
272
  };
273
273
  },
274
274
  extract(context) {
275
- const apiRoutesRoot = findImportFiles(context.paths, (filePath) => /src\/routes\/api\/index\.(ts|js|mjs|cjs)$/i.test(filePath))[0]
276
- ? path.join(path.dirname(findImportFiles(context.paths, (filePath) => /src\/routes\/api\/index\.(ts|js|mjs|cjs)$/i.test(filePath))[0]))
275
+ const apiRoutesRoot = findPrimaryImportFiles(context.paths, (filePath) => /src\/routes\/api\/index\.(ts|js|mjs|cjs)$/i.test(filePath))[0]
276
+ ? path.join(path.dirname(findPrimaryImportFiles(context.paths, (filePath) => /src\/routes\/api\/index\.(ts|js|mjs|cjs)$/i.test(filePath))[0]))
277
277
  : null;
278
278
  if (!apiRoutesRoot) {
279
279
  return { findings: [], candidates: { capabilities: [], routes: [], stacks: [] } };
280
280
  }
281
281
 
282
- const routeFiles = findImportFiles(context.paths, (filePath) => /src\/routes\/api\/.+\.(ts|js|mjs|cjs)$/i.test(filePath))
282
+ const routeFiles = findPrimaryImportFiles(context.paths, (filePath) => /src\/routes\/api\/.+\.(ts|js|mjs|cjs)$/i.test(filePath))
283
283
  .filter((filePath) => !/\/autohooks\.(ts|js|mjs|cjs)$/i.test(filePath));
284
- const schemaFiles = findImportFiles(context.paths, (filePath) => /src\/schemas\/.+\.(ts|js|mjs|cjs)$/i.test(filePath));
284
+ const schemaFiles = findPrimaryImportFiles(context.paths, (filePath) => /src\/schemas\/.+\.(ts|js|mjs|cjs)$/i.test(filePath));
285
285
  const namedSchemas = parseNamedTypeboxSchemas(schemaFiles, context.helpers.readTextIfExists);
286
286
 
287
287
  const routes = [];
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  canonicalCandidateTerm,
3
3
  dedupeCandidateRecords,
4
- findImportFiles,
4
+ findPrimaryImportFiles,
5
5
  makeCandidateRecord,
6
6
  pluralizeCandidateTerm,
7
7
  relativeTo,
@@ -31,7 +31,7 @@ function capabilityIdFor(featureStem, methodName, httpMethod) {
31
31
  }
32
32
 
33
33
  function extractApiConfigPaths(context) {
34
- const configFile = findImportFiles(context.paths, (filePath) => /\/lib\/common\/network\/api_config\.dart$/i.test(filePath))[0];
34
+ const configFile = findPrimaryImportFiles(context.paths, (filePath) => /\/lib\/common\/network\/api_config\.dart$/i.test(filePath))[0];
35
35
  const mapping = new Map();
36
36
  if (!configFile) return mapping;
37
37
  const text = context.helpers.readTextIfExists(configFile) || "";
@@ -87,7 +87,7 @@ export const flutterDioExtractor = {
87
87
  id: "api.flutter-dio",
88
88
  track: "api",
89
89
  detect(context) {
90
- const files = findImportFiles(
90
+ const files = findPrimaryImportFiles(
91
91
  context.paths,
92
92
  (filePath) => /\/lib\/features\/.+\/data\/datasources\/.+_remote_data_source\.dart$/i.test(filePath)
93
93
  );
@@ -98,7 +98,7 @@ export const flutterDioExtractor = {
98
98
  };
99
99
  },
100
100
  extract(context) {
101
- const files = findImportFiles(
101
+ const files = findPrimaryImportFiles(
102
102
  context.paths,
103
103
  (filePath) => /\/lib\/features\/.+\/data\/datasources\/.+_remote_data_source\.dart$/i.test(filePath)
104
104
  );
@@ -1,4 +1,4 @@
1
- import { findImportFiles, inferRouteAuthHint, inferRouteCapabilityId, inferRouteQueryParams, isPrimaryImportSource, makeCandidateRecord, normalizeOpenApiPath, relativeTo } from "../../core/shared.js";
1
+ import { findPrimaryImportFiles, inferRouteAuthHint, inferRouteCapabilityId, inferRouteQueryParams, isPrimaryImportSource, makeCandidateRecord, normalizeOpenApiPath, relativeTo } from "../../core/shared.js";
2
2
 
3
3
  function extractHandlerContext(text, handlerName) {
4
4
  if (!handlerName) return "";
@@ -15,7 +15,7 @@ function extractHandlerContext(text, handlerName) {
15
15
 
16
16
  function inferServerRoutes(context) {
17
17
  const routes = [];
18
- const routeFiles = findImportFiles(
18
+ const routeFiles = findPrimaryImportFiles(
19
19
  context.paths,
20
20
  (filePath) =>
21
21
  /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath) &&
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  canonicalCandidateTerm,
3
3
  dedupeCandidateRecords,
4
- findImportFiles,
4
+ findPrimaryImportFiles,
5
5
  idHintify,
6
6
  makeCandidateRecord,
7
7
  pluralizeCandidateTerm,
@@ -464,7 +464,7 @@ export const graphQlCodeFirstExtractor = {
464
464
  id: "api.graphql-code-first",
465
465
  track: "api",
466
466
  detect(context) {
467
- const files = findImportFiles(context.paths, (filePath) => /(\/src\/.+|\/pages\/api\/.+)\.(ts|tsx|js|jsx)$/i.test(filePath));
467
+ const files = findPrimaryImportFiles(context.paths, (filePath) => /(\/src\/.+|\/pages\/api\/.+)\.(ts|tsx|js|jsx)$/i.test(filePath));
468
468
  const hasNestResolvers = files.some((filePath) => {
469
469
  const text = readTextIfExists(filePath) || "";
470
470
  return /@nestjs\/graphql/.test(text) && /@(Query|Mutation)\s*\(/.test(text);
@@ -489,7 +489,7 @@ export const graphQlCodeFirstExtractor = {
489
489
  };
490
490
  },
491
491
  extract(context) {
492
- const files = findImportFiles(context.paths, (filePath) => /(\/src\/.+|\/pages\/api\/.+)\.(ts|tsx|js|jsx)$/i.test(filePath)).filter((filePath) => !/\.test\./i.test(filePath));
492
+ const files = findPrimaryImportFiles(context.paths, (filePath) => /(\/src\/.+|\/pages\/api\/.+)\.(ts|tsx|js|jsx)$/i.test(filePath)).filter((filePath) => !/\.test\./i.test(filePath));
493
493
  const nestTypes = parseNestTypes(files);
494
494
  const pothosTypes = parsePothosTypes(files);
495
495
  const nexusTypes = parseNexusTypes(files);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  canonicalCandidateTerm,
3
3
  dedupeCandidateRecords,
4
- findImportFiles,
4
+ findPrimaryImportFiles,
5
5
  idHintify,
6
6
  makeCandidateRecord,
7
7
  pluralizeCandidateTerm,
@@ -174,7 +174,7 @@ function inferEndpointPath(context, graphqlFiles) {
174
174
  return endpointMatch[1];
175
175
  }
176
176
  }
177
- const packageJsonPath = findImportFiles(context.paths, (filePath) => /package\.json$/i.test(filePath))[0];
177
+ const packageJsonPath = findPrimaryImportFiles(context.paths, (filePath) => /package\.json$/i.test(filePath))[0];
178
178
  const packageText = packageJsonPath ? readTextIfExists(packageJsonPath) : null;
179
179
  if (packageText && /graphql-yoga|apollo-server|@apollo\/server/.test(packageText)) {
180
180
  return "/graphql";
@@ -183,7 +183,7 @@ function inferEndpointPath(context, graphqlFiles) {
183
183
  }
184
184
 
185
185
  function extractGraphqlSchemaSources(context) {
186
- const files = findImportFiles(
186
+ const files = findPrimaryImportFiles(
187
187
  context.paths,
188
188
  (filePath) =>
189
189
  /\/src\/.+\.(ts|tsx|js|jsx)$/i.test(filePath) ||
@@ -210,7 +210,7 @@ export const graphQlSdlExtractor = {
210
210
  detect(context) {
211
211
  const schemaSources = extractGraphqlSchemaSources(context);
212
212
  const hasOperations = schemaSources.some(({ schema }) => /\btype\s+Query\b|\btype\s+Mutation\b/.test(schema));
213
- const packageJsonPath = findImportFiles(context.paths, (filePath) => /package\.json$/i.test(filePath))[0];
213
+ const packageJsonPath = findPrimaryImportFiles(context.paths, (filePath) => /package\.json$/i.test(filePath))[0];
214
214
  const packageText = packageJsonPath ? readTextIfExists(packageJsonPath) : "";
215
215
  const hasGraphqlRuntime = /graphql-yoga|graphql|apollo-server|@apollo\/server/.test(packageText || "");
216
216
  return {
@@ -220,7 +220,7 @@ export const graphQlSdlExtractor = {
220
220
  },
221
221
  extract(context) {
222
222
  const schemaSources = extractGraphqlSchemaSources(context);
223
- const endpointPath = inferEndpointPath(context, findImportFiles(context.paths, (filePath) => /\/src\/.+\.(ts|tsx|js|jsx)$/i.test(filePath)));
223
+ const endpointPath = inferEndpointPath(context, findPrimaryImportFiles(context.paths, (filePath) => /\/src\/.+\.(ts|tsx|js|jsx)$/i.test(filePath)));
224
224
  const mergedSchema = schemaSources.map(({ schema }) => schema).join("\n\n");
225
225
  const blocks = parseGraphqlSchemaBlocks(mergedSchema);
226
226
  const inputTypes = new Map([...blocks.entries()].filter(([, block]) => block.kind === "input"));