@topogram/cli 0.3.64 → 0.3.66

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 (278) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +716 -0
  3. package/src/adoption/plan.js +12 -703
  4. package/src/adoption/reporting.js +1 -1
  5. package/src/agent-brief.js +7 -21
  6. package/src/agent-ops/query-builders/auth.js +375 -0
  7. package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
  8. package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
  9. package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
  10. package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
  11. package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
  12. package/src/agent-ops/query-builders/change-risk.js +25 -0
  13. package/src/agent-ops/query-builders/common.js +149 -0
  14. package/src/agent-ops/query-builders/maintained-risk.js +539 -0
  15. package/src/agent-ops/query-builders/maintained-shared.js +120 -0
  16. package/src/agent-ops/query-builders/multi-agent.js +547 -0
  17. package/src/agent-ops/query-builders/projection-impacts.js +514 -0
  18. package/src/agent-ops/query-builders/work-packets.js +417 -0
  19. package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
  20. package/src/agent-ops/query-builders/workflow-context.js +398 -0
  21. package/src/agent-ops/query-builders/workflow-presets-core.js +677 -0
  22. package/src/agent-ops/query-builders/workflow-presets.js +341 -0
  23. package/src/agent-ops/query-builders.d.ts +26 -26
  24. package/src/agent-ops/query-builders.js +42 -5021
  25. package/src/archive/jsonl.js +2 -2
  26. package/src/archive/resolver-bridge.js +1 -1
  27. package/src/archive/unarchive.js +2 -1
  28. package/src/catalog/constants.js +10 -0
  29. package/src/catalog/copy.js +65 -0
  30. package/src/catalog/diagnostics.js +15 -0
  31. package/src/catalog/entries.js +42 -0
  32. package/src/catalog/files.js +67 -0
  33. package/src/catalog/provenance.js +123 -0
  34. package/src/catalog/source.js +150 -0
  35. package/src/catalog/validation.js +252 -0
  36. package/src/catalog.d.ts +2 -0
  37. package/src/catalog.js +18 -746
  38. package/src/cli/command-parsers/project.js +3 -0
  39. package/src/cli/command-parsers/shared.js +1 -1
  40. package/src/cli/commands/agent.js +2 -2
  41. package/src/cli/commands/catalog/check.js +31 -0
  42. package/src/cli/commands/catalog/copy.js +59 -0
  43. package/src/cli/commands/catalog/doctor.js +248 -0
  44. package/src/cli/commands/catalog/help.js +21 -0
  45. package/src/cli/commands/catalog/list.js +52 -0
  46. package/src/cli/commands/catalog/runner.js +92 -0
  47. package/src/cli/commands/catalog/shared.js +17 -0
  48. package/src/cli/commands/catalog/show.js +134 -0
  49. package/src/cli/commands/catalog.js +30 -615
  50. package/src/cli/commands/check.js +3 -3
  51. package/src/cli/commands/doctor.js +2 -9
  52. package/src/cli/commands/generator-policy/package-info.js +162 -0
  53. package/src/cli/commands/generator-policy/payloads.js +372 -0
  54. package/src/cli/commands/generator-policy/printers.js +159 -0
  55. package/src/cli/commands/generator-policy/runner.js +81 -0
  56. package/src/cli/commands/generator-policy/shared.js +39 -0
  57. package/src/cli/commands/generator-policy.js +15 -783
  58. package/src/cli/commands/import/adopt.js +170 -0
  59. package/src/cli/commands/import/check.js +91 -0
  60. package/src/cli/commands/import/diff.js +84 -0
  61. package/src/cli/commands/import/help.js +47 -0
  62. package/src/cli/commands/import/paths.js +269 -0
  63. package/src/cli/commands/import/plan.js +292 -0
  64. package/src/cli/commands/import/refresh.js +471 -0
  65. package/src/cli/commands/import/status-history.js +196 -0
  66. package/src/cli/commands/import/workspace.js +233 -0
  67. package/src/cli/commands/import.js +33 -1732
  68. package/src/cli/commands/migrate.js +153 -0
  69. package/src/cli/commands/package/constants.js +17 -0
  70. package/src/cli/commands/package/doctor.js +240 -0
  71. package/src/cli/commands/package/help.js +27 -0
  72. package/src/cli/commands/package/lockfile.js +135 -0
  73. package/src/cli/commands/package/npm.js +97 -0
  74. package/src/cli/commands/package/reporting.js +35 -0
  75. package/src/cli/commands/package/runner.js +33 -0
  76. package/src/cli/commands/package/shared.js +9 -0
  77. package/src/cli/commands/package/update-cli.js +252 -0
  78. package/src/cli/commands/package/versions.js +35 -0
  79. package/src/cli/commands/package.js +29 -813
  80. package/src/cli/commands/query/change-plan.js +68 -0
  81. package/src/cli/commands/query/definitions.js +202 -0
  82. package/src/cli/commands/query/import-adopt.js +121 -0
  83. package/src/cli/commands/query/runner/artifacts.js +102 -0
  84. package/src/cli/commands/query/runner/boundaries.js +211 -0
  85. package/src/cli/commands/query/runner/change.js +182 -0
  86. package/src/cli/commands/query/runner/import-adopt.js +111 -0
  87. package/src/cli/commands/query/runner/index.js +31 -0
  88. package/src/cli/commands/query/runner/output.js +12 -0
  89. package/src/cli/commands/query/runner/workflow.js +241 -0
  90. package/src/cli/commands/query/runner.js +3 -0
  91. package/src/cli/commands/query/workflow-context.js +5 -0
  92. package/src/cli/commands/query/workspace.js +270 -0
  93. package/src/cli/commands/query.js +9 -1300
  94. package/src/cli/commands/source.js +3 -12
  95. package/src/cli/commands/template/baseline.js +100 -0
  96. package/src/cli/commands/template/check.js +467 -0
  97. package/src/cli/commands/template/constants.js +8 -0
  98. package/src/cli/commands/template/diagnostics.js +26 -0
  99. package/src/cli/commands/template/help.js +28 -0
  100. package/src/cli/commands/template/lifecycle.js +404 -0
  101. package/src/cli/commands/template/list-show.js +287 -0
  102. package/src/cli/commands/template/policy.js +422 -0
  103. package/src/cli/commands/template/shared.js +127 -0
  104. package/src/cli/commands/template/updates.js +352 -0
  105. package/src/cli/commands/template-runner.js +6 -6
  106. package/src/cli/commands/template.js +41 -2143
  107. package/src/cli/commands/trust.js +1 -1
  108. package/src/cli/commands/workflow.js +6 -1
  109. package/src/cli/dispatcher.js +6 -1
  110. package/src/cli/help.js +15 -14
  111. package/src/cli/migration-guidance.js +1 -1
  112. package/src/cli/output-safety.js +2 -1
  113. package/src/cli/path-normalization.js +3 -13
  114. package/src/generator/api/contracts.js +497 -0
  115. package/src/generator/api/metadata.js +221 -0
  116. package/src/generator/api/openapi.js +559 -0
  117. package/src/generator/api/schema.js +124 -0
  118. package/src/generator/api/types.d.ts +98 -0
  119. package/src/generator/api.js +3 -1195
  120. package/src/generator/context/domain-page.js +1 -1
  121. package/src/generator/context/shared/domain-sdlc.js +282 -0
  122. package/src/generator/context/shared/maintained-boundary.js +665 -0
  123. package/src/generator/context/shared/metrics.js +85 -0
  124. package/src/generator/context/shared/primitives.js +64 -0
  125. package/src/generator/context/shared/relationships.js +453 -0
  126. package/src/generator/context/shared/summaries.js +263 -0
  127. package/src/generator/context/shared/types.d.ts +207 -0
  128. package/src/generator/context/shared.d.ts +42 -0
  129. package/src/generator/context/shared.js +80 -1390
  130. package/src/generator/context/slice/core.js +397 -0
  131. package/src/generator/context/slice/sdlc.js +417 -0
  132. package/src/generator/context/slice/ui-packets.js +183 -0
  133. package/src/generator/context/slice.js +2 -859
  134. package/src/generator/context/task-mode.js +2 -2
  135. package/src/generator/registry/index.js +507 -0
  136. package/src/generator/registry.js +18 -504
  137. package/src/generator/runtime/environment/index.js +666 -0
  138. package/src/generator/runtime/environment.js +4 -666
  139. package/src/generator/runtime/runtime-check/index.js +554 -0
  140. package/src/generator/runtime/runtime-check.js +4 -554
  141. package/src/generator/runtime/shared/index.js +572 -0
  142. package/src/generator/runtime/shared.js +19 -570
  143. package/src/generator/sdlc/doc-page.js +1 -1
  144. package/src/generator/shared.d.ts +2 -0
  145. package/src/generator/surfaces/databases/lifecycle-shared.js +1 -1
  146. package/src/generator/surfaces/native/swiftui-templates/README.generated.md +1 -1
  147. package/src/generator/surfaces/shared.d.ts +3 -0
  148. package/src/generator/widget-conformance/behavior-report.js +258 -0
  149. package/src/generator/widget-conformance/checks.js +371 -0
  150. package/src/generator/widget-conformance/projection-context.js +200 -0
  151. package/src/generator/widget-conformance/report.js +166 -0
  152. package/src/generator/widget-conformance/types.d.ts +121 -0
  153. package/src/generator/widget-conformance.js +3 -824
  154. package/src/import/core/context.d.ts +3 -0
  155. package/src/import/core/context.js +5 -7
  156. package/src/import/core/contracts.d.ts +1 -0
  157. package/src/import/core/registry.d.ts +4 -0
  158. package/src/import/core/runner/candidates.js +337 -0
  159. package/src/import/core/runner/options.js +22 -0
  160. package/src/import/core/runner/reports.js +51 -0
  161. package/src/import/core/runner/run.js +79 -0
  162. package/src/import/core/runner/tracks.js +150 -0
  163. package/src/import/core/runner/ui-drafts.js +393 -0
  164. package/src/import/core/runner.js +3 -698
  165. package/src/import/core/shared/api-routes.js +221 -0
  166. package/src/import/core/shared/candidates.js +97 -0
  167. package/src/import/core/shared/files.js +177 -0
  168. package/src/import/core/shared/next-app.js +389 -0
  169. package/src/import/core/shared/types.d.ts +51 -0
  170. package/src/import/core/shared/ui-routes.js +230 -0
  171. package/src/import/core/shared.js +60 -861
  172. package/src/new-project/constants.js +128 -0
  173. package/src/new-project/create.js +90 -0
  174. package/src/new-project/json.js +28 -0
  175. package/src/new-project/metadata.js +96 -0
  176. package/src/new-project/package-spec.js +161 -0
  177. package/src/new-project/project-files.js +351 -0
  178. package/src/new-project/template-policy.js +269 -0
  179. package/src/new-project/template-resolution.js +370 -0
  180. package/src/new-project/template-snapshots.js +442 -0
  181. package/src/new-project/template-updates.js +512 -0
  182. package/src/new-project/types.d.ts +83 -0
  183. package/src/new-project.js +6 -2277
  184. package/src/parser.d.ts +87 -1
  185. package/src/parser.js +118 -0
  186. package/src/policy/review-boundaries.d.ts +15 -0
  187. package/src/project-config/index.js +591 -0
  188. package/src/project-config.js +19 -561
  189. package/src/resolver/enrich/acceptance-criterion.js +2 -0
  190. package/src/resolver/enrich/bug.js +2 -0
  191. package/src/resolver/enrich/pitch.js +2 -0
  192. package/src/resolver/enrich/requirement.js +2 -0
  193. package/src/resolver/enrich/task.js +2 -0
  194. package/src/resolver/index.js +19 -2089
  195. package/src/resolver/normalize.js +384 -1
  196. package/src/resolver/plans.js +168 -0
  197. package/src/resolver/projections-api.js +494 -0
  198. package/src/resolver/projections-db.js +133 -0
  199. package/src/resolver/projections-ui.js +317 -0
  200. package/src/resolver/shapes.js +251 -0
  201. package/src/resolver/shared.js +278 -0
  202. package/src/resolver/widgets.js +132 -0
  203. package/src/sdlc/adopt.js +6 -5
  204. package/src/sdlc/paths.js +3 -5
  205. package/src/sdlc/scaffold.js +2 -1
  206. package/src/template-trust/constants.js +62 -0
  207. package/src/template-trust/content.js +258 -0
  208. package/src/template-trust/diff.js +92 -0
  209. package/src/template-trust/policy.js +61 -0
  210. package/src/template-trust/record.js +90 -0
  211. package/src/template-trust/status.js +182 -0
  212. package/src/template-trust.js +24 -687
  213. package/src/text-helpers.d.ts +1 -0
  214. package/src/topogram-types.d.ts +69 -0
  215. package/src/validator/common.js +488 -0
  216. package/src/validator/data-model.js +237 -0
  217. package/src/validator/docs.js +167 -0
  218. package/src/validator/expressions.js +146 -1
  219. package/src/validator/index.d.ts +23 -0
  220. package/src/validator/index.js +32 -3585
  221. package/src/validator/kinds.d.ts +41 -0
  222. package/src/validator/kinds.js +2 -0
  223. package/src/validator/model-helpers.js +46 -0
  224. package/src/validator/per-kind/acceptance-criterion.js +5 -0
  225. package/src/validator/per-kind/bug.js +6 -0
  226. package/src/validator/per-kind/domain.js +15 -2
  227. package/src/validator/per-kind/pitch.js +7 -0
  228. package/src/validator/per-kind/requirement.js +5 -0
  229. package/src/validator/per-kind/task.js +7 -0
  230. package/src/validator/per-kind/widget.js +14 -0
  231. package/src/validator/projections/api-http-async.js +410 -0
  232. package/src/validator/projections/api-http-authz.js +88 -0
  233. package/src/validator/projections/api-http-core.js +205 -0
  234. package/src/validator/projections/api-http-policies.js +339 -0
  235. package/src/validator/projections/api-http-responses.js +233 -0
  236. package/src/validator/projections/api-http.js +44 -0
  237. package/src/validator/projections/db.js +353 -0
  238. package/src/validator/projections/generator-defaults.js +45 -0
  239. package/src/validator/projections/helpers.js +87 -0
  240. package/src/validator/projections/ui-helpers.js +214 -0
  241. package/src/validator/projections/ui-navigation.js +344 -0
  242. package/src/validator/projections/ui-structure.js +364 -0
  243. package/src/validator/projections/ui-widgets.js +493 -0
  244. package/src/validator/projections/ui.js +46 -0
  245. package/src/validator/registry.js +48 -1
  246. package/src/validator/utils.d.ts +20 -0
  247. package/src/validator/utils.js +115 -12
  248. package/src/widget-behavior.d.ts +1 -0
  249. package/src/workflows/import-app/api/collect.js +221 -0
  250. package/src/workflows/import-app/api/openapi.js +257 -0
  251. package/src/workflows/import-app/api/routes.js +327 -0
  252. package/src/workflows/import-app/api/sources.js +22 -0
  253. package/src/workflows/import-app/api.js +2 -797
  254. package/src/workflows/reconcile/adoption-plan/build.js +212 -0
  255. package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
  256. package/src/workflows/reconcile/adoption-plan/outputs.js +153 -0
  257. package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
  258. package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
  259. package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
  260. package/src/workflows/reconcile/adoption-plan.js +30 -740
  261. package/src/workflows/reconcile/auth/closures.js +115 -0
  262. package/src/workflows/reconcile/auth/formatters.js +142 -0
  263. package/src/workflows/reconcile/auth/inference.js +330 -0
  264. package/src/workflows/reconcile/auth/roles.js +122 -0
  265. package/src/workflows/reconcile/auth.js +35 -690
  266. package/src/workflows/reconcile/bundle-core/index.js +600 -0
  267. package/src/workflows/reconcile/bundle-core.js +12 -598
  268. package/src/workflows/reconcile/candidate-model.js +18 -2
  269. package/src/workflows/reconcile/canonical-surface.js +1 -1
  270. package/src/workflows/reconcile/impacts/adoption-plan.js +196 -0
  271. package/src/workflows/reconcile/impacts/indexes.js +105 -0
  272. package/src/workflows/reconcile/impacts/patches.js +252 -0
  273. package/src/workflows/reconcile/impacts/reports.js +80 -0
  274. package/src/workflows/reconcile/impacts.js +14 -623
  275. package/src/workflows/reconcile/renderers.js +41 -6
  276. package/src/workflows/shared.js +5 -11
  277. package/src/workspace-docs.d.ts +29 -0
  278. package/src/workspace-paths.js +328 -0
@@ -1,799 +1,4 @@
1
1
  // @ts-check
2
- import fs from "node:fs";
3
- import path from "node:path";
4
2
 
5
- import { relativeTo } from "../../path-helpers.js";
6
- import { canonicalCandidateTerm, idHintify, slugify, titleCase } from "../../text-helpers.js";
7
- import { listFilesRecursive, readTextIfExists } from "../shared.js";
8
- import { inferReactRoutes, inferSvelteRoutes, routeSegments } from "./ui.js";
9
- import { dedupeCandidateRecords, findImportFiles, inferCapabilityEntityId, makeCandidateRecord, normalizeOpenApiPath, selectPreferredImportFiles } from "./shared.js";
10
-
11
- /** @param {WorkspacePaths} paths @returns {any} */
12
- export function discoverApiSources(paths) {
13
- const allOpenApiFiles = findImportFiles(
14
- paths,
15
- (/** @type {any} */ filePath) =>
16
- /(openapi|swagger)\.(json|ya?ml)$/i.test(path.basename(filePath))
17
- );
18
- const openApiFiles = selectPreferredImportFiles(paths, allOpenApiFiles, "openapi");
19
- const routeFiles = findImportFiles(
20
- paths,
21
- (/** @type {any} */ filePath) =>
22
- /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath) &&
23
- /(server|api|routes|src)/i.test(filePath)
24
- );
25
- return { openApiFiles, routeFiles };
26
- }
27
-
28
- /** @param {WorkflowRecord} document @param {any} provenance @param {string} sourceKind @returns {any} */
29
- function parseOpenApiDocument(document, provenance, sourceKind = "openapi") {
30
- /** @type {any[]} */
31
- const capabilities = [];
32
- /** @type {any[]} */
33
- const routes = [];
34
- const pathsObject = document.paths || {};
35
- for (const [endpointPath, operations] of Object.entries(pathsObject)) {
36
- for (const [method, operation] of Object.entries(operations || {})) {
37
- const normalizedMethod = method.toUpperCase();
38
- const operationId = operation.operationId || `candidate_${normalizedMethod.toLowerCase()}_${slugify(endpointPath)}`;
39
- const requestSchema =
40
- operation.requestBody?.content?.["application/json"]?.schema?.$ref ||
41
- operation.requestBody?.content?.["application/json"]?.schema?.type ||
42
- null;
43
- const successResponse = Object.entries(operation.responses || {}).find((/** @type {any} */ [status]) => /^2/.test(status));
44
- const responseSchema =
45
- successResponse?.[1]?.content?.["application/json"]?.schema?.$ref ||
46
- successResponse?.[1]?.content?.["application/json"]?.schema?.type ||
47
- null;
48
- const parameterHints = extractOpenApiParameterHints(document, endpointPath, operation);
49
- const requestFieldHints = extractOpenApiSchemaFieldHints(document, operation.requestBody?.content?.["application/json"]?.schema);
50
- const responseFieldHints = extractOpenApiSchemaFieldHints(document, successResponse?.[1]?.content?.["application/json"]?.schema);
51
- const securitySchemes = extractOpenApiSecuritySchemes(document, operation);
52
- capabilities.push(
53
- makeCandidateRecord({
54
- kind: "capability",
55
- idHint: operationId,
56
- label: operation.summary || titleCase(operationId.replace(/^cap_/, "")),
57
- confidence: "high",
58
- sourceKind,
59
- provenance: `${provenance}#${normalizedMethod} ${endpointPath}`,
60
- endpoint: {
61
- method: normalizedMethod,
62
- path: endpointPath
63
- },
64
- input_hint: requestSchema,
65
- output_hint: responseSchema,
66
- input_fields: requestFieldHints.body_fields,
67
- output_fields: responseFieldHints.body_fields,
68
- path_params: parameterHints.path,
69
- query_params: parameterHints.query,
70
- header_params: parameterHints.header,
71
- security_schemes: securitySchemes,
72
- auth_hint: securitySchemes.length > 0 ? "secured" : "unknown"
73
- })
74
- );
75
- routes.push({
76
- path: endpointPath,
77
- method: normalizedMethod,
78
- source_kind: sourceKind,
79
- provenance: `${provenance}#${normalizedMethod} ${endpointPath}`
80
- });
81
- }
82
- }
83
- return { capabilities, routes };
84
- }
85
-
86
- /** @param {string} ref @returns {any} */
87
- function openApiRefName(ref) {
88
- if (!ref || typeof ref !== "string") {
89
- return null;
90
- }
91
- return ref.split("/").pop() || null;
92
- }
93
-
94
- /** @param {WorkflowRecord} document @param {WorkflowRecord} schema @param {Set<any>} seen @returns {any} */
95
- function resolveOpenApiSchema(document, schema, seen = new Set()) {
96
- if (!schema || typeof schema !== "object") {
97
- return null;
98
- }
99
- if (schema.$ref) {
100
- if (seen.has(schema.$ref)) {
101
- return null;
102
- }
103
- seen.add(schema.$ref);
104
- if (!schema.$ref.startsWith("#/")) {
105
- return null;
106
- }
107
- const segments = schema.$ref.slice(2).split("/");
108
- let current = document;
109
- for (const segment of segments) {
110
- current = current?.[segment];
111
- if (current == null) {
112
- return null;
113
- }
114
- }
115
- return resolveOpenApiSchema(document, current, seen) || current;
116
- }
117
- return schema;
118
- }
119
-
120
- /** @param {WorkflowRecord} document @param {WorkflowRecord} schema @param {Set<any>} fields @param {Set<any>} seen @returns {any} */
121
- function collectOpenApiObjectFields(document, schema, fields = new Set(), seen = new Set()) {
122
- const resolved = resolveOpenApiSchema(document, schema, seen);
123
- if (!resolved || typeof resolved !== "object") {
124
- return fields;
125
- }
126
- if (resolved.type === "array" && resolved.items) {
127
- collectOpenApiObjectFields(document, resolved.items, fields, seen);
128
- return fields;
129
- }
130
- for (const propertyName of Object.keys(resolved.properties || {})) {
131
- fields.add(propertyName);
132
- }
133
- for (const entry of resolved.allOf || []) {
134
- collectOpenApiObjectFields(document, entry, fields, seen);
135
- }
136
- for (const entry of resolved.oneOf || []) {
137
- collectOpenApiObjectFields(document, entry, fields, seen);
138
- }
139
- for (const entry of resolved.anyOf || []) {
140
- collectOpenApiObjectFields(document, entry, fields, seen);
141
- }
142
- return fields;
143
- }
144
-
145
- /** @param {WorkflowRecord} document @param {WorkflowRecord} schema @returns {any} */
146
- function extractOpenApiSchemaFieldHints(document, schema) {
147
- const fieldNames = [...collectOpenApiObjectFields(document, schema)].sort();
148
- return {
149
- schema_ref: openApiRefName(schema?.$ref || null),
150
- body_fields: fieldNames
151
- };
152
- }
153
-
154
- /** @param {string} endpointPath @param {WorkflowRecord} operation @returns {any} */
155
- function collectOpenApiParameters(endpointPath, operation) {
156
- const pathParams = [...String(endpointPath || "").matchAll(/\{([^}]+)\}/g)].map((/** @type {any} */ match) => ({
157
- name: match[1],
158
- in: "path",
159
- required: true
160
- }));
161
- return [...pathParams, ...((operation.parameters || []).filter(Boolean))];
162
- }
163
-
164
- /** @param {WorkflowRecord} document @param {string} endpointPath @param {WorkflowRecord} operation @returns {any} */
165
- function extractOpenApiParameterHints(document, endpointPath, operation) {
166
- /** @type {WorkflowRecord} */
167
- const grouped = {
168
- path: [],
169
- query: [],
170
- header: []
171
- };
172
- for (const parameter of collectOpenApiParameters(endpointPath, operation)) {
173
- const schema = resolveOpenApiSchema(document, parameter.schema || null);
174
- const target = parameter.in === "query" ? "query" : parameter.in === "header" ? "header" : "path";
175
- grouped[target].push({
176
- name: parameter.name,
177
- required: Boolean(parameter.required),
178
- type: schema?.type || null
179
- });
180
- }
181
- for (const key of Object.keys(grouped)) {
182
- grouped[key] = grouped[key].sort((/** @type {any} */ a, /** @type {any} */ b) => a.name.localeCompare(b.name));
183
- }
184
- return grouped;
185
- }
186
-
187
- /** @param {WorkflowRecord} document @param {WorkflowRecord} operation @returns {any} */
188
- function extractOpenApiSecuritySchemes(document, operation) {
189
- const securityEntries = [...(operation.security || []), ...(document.security || [])];
190
- const schemes = new Set();
191
- for (const entry of securityEntries) {
192
- for (const key of Object.keys(entry || {})) {
193
- schemes.add(key);
194
- }
195
- }
196
- return [...schemes].sort();
197
- }
198
-
199
- /** @param {string} text @returns {any} */
200
- function parseOpenApiYaml(text) {
201
- /** @type {WorkflowRecord} */
202
- const doc = { paths: {} };
203
- let currentPath = null;
204
- let currentMethod = null;
205
- let inRequestBody = false;
206
- let inResponses = false;
207
- let currentResponseStatus = null;
208
-
209
- for (const rawLine of text.split(/\r?\n/)) {
210
- const line = rawLine.replace(/#.*$/, "");
211
- if (!line.trim()) {
212
- continue;
213
- }
214
- const pathMatch = line.match(/^\s{2}(\/[^:]+):\s*$/);
215
- if (pathMatch) {
216
- currentPath = pathMatch[1];
217
- currentMethod = null;
218
- doc.paths[currentPath] = doc.paths[currentPath] || {};
219
- inRequestBody = false;
220
- inResponses = false;
221
- currentResponseStatus = null;
222
- continue;
223
- }
224
- const methodMatch = line.match(/^\s{4}(get|post|put|patch|delete):\s*$/i);
225
- if (methodMatch && currentPath) {
226
- currentMethod = methodMatch[1].toLowerCase();
227
- doc.paths[currentPath][currentMethod] = { responses: {} };
228
- inRequestBody = false;
229
- inResponses = false;
230
- currentResponseStatus = null;
231
- continue;
232
- }
233
- if (!currentPath || !currentMethod) {
234
- continue;
235
- }
236
- const operation = doc.paths[currentPath][currentMethod];
237
- const operationIdMatch = line.match(/^\s{6}operationId:\s*(.+)$/);
238
- if (operationIdMatch) {
239
- operation.operationId = operationIdMatch[1].trim().replace(/^["']|["']$/g, "");
240
- continue;
241
- }
242
- const summaryMatch = line.match(/^\s{6}summary:\s*(.+)$/);
243
- if (summaryMatch) {
244
- operation.summary = summaryMatch[1].trim().replace(/^["']|["']$/g, "");
245
- continue;
246
- }
247
- if (/^\s{6}requestBody:\s*$/.test(line)) {
248
- inRequestBody = true;
249
- inResponses = false;
250
- currentResponseStatus = null;
251
- continue;
252
- }
253
- if (/^\s{6}responses:\s*$/.test(line)) {
254
- inResponses = true;
255
- inRequestBody = false;
256
- currentResponseStatus = null;
257
- continue;
258
- }
259
- const responseStatusMatch = line.match(/^\s{8}['"]?([0-9Xx]{3})['"]?:\s*$/);
260
- if (inResponses && responseStatusMatch) {
261
- currentResponseStatus = responseStatusMatch[1];
262
- operation.responses[currentResponseStatus] = operation.responses[currentResponseStatus] || {};
263
- continue;
264
- }
265
- const refMatch = line.match(/^\s+\$ref:\s*(.+)$/);
266
- if (refMatch) {
267
- const ref = refMatch[1].trim().replace(/^["']|["']$/g, "");
268
- if (inRequestBody) {
269
- operation.requestBody = operation.requestBody || { content: { "application/json": { schema: {} } } };
270
- operation.requestBody.content["application/json"].schema.$ref = ref;
271
- } else if (inResponses && currentResponseStatus) {
272
- operation.responses[currentResponseStatus].content = operation.responses[currentResponseStatus].content || { "application/json": { schema: {} } };
273
- operation.responses[currentResponseStatus].content["application/json"].schema.$ref = ref;
274
- }
275
- }
276
- }
277
-
278
- return doc;
279
- }
280
-
281
- /** @param {WorkspacePaths} paths @returns {any} */
282
- function inferServerRoutes(paths) {
283
- /** @type {any[]} */
284
- const routes = [];
285
- const routeFiles = findImportFiles(
286
- paths,
287
- (/** @type {any} */ filePath) =>
288
- /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath) &&
289
- /(server|api|routes|src)/i.test(filePath)
290
- );
291
- for (const filePath of routeFiles) {
292
- const text = readTextIfExists(filePath) || "";
293
- for (const match of text.matchAll(/\.(get|post|put|patch|delete)\(\s*["'`]([^"'`]+)["'`]\s*,([\s\S]*?)\)\s*;?/gi)) {
294
- const handlerTokens = [...match[3].matchAll(/\b([A-Za-z_][A-Za-z0-9_]*)\b/g)].map((/** @type {any} */ entry) => entry[1]);
295
- const handlerHint = handlerTokens.length > 0 ? handlerTokens[handlerTokens.length - 1] : null;
296
- const pathParams = [...normalizeOpenApiPath(match[2]).matchAll(/\{([^}]+)\}/g)].map((/** @type {any} */ entry) => entry[1]);
297
- const handlerContext = handlerHint ? extractHandlerContext(text, handlerHint) : "";
298
- const queryParams = inferRouteQueryParams(handlerContext);
299
- const authHint = inferRouteAuthHint(match[3], handlerContext);
300
- routes.push({
301
- file: filePath,
302
- method: match[1].toUpperCase(),
303
- path: match[2],
304
- handler_hint: handlerHint,
305
- path_params: pathParams,
306
- query_params: queryParams,
307
- auth_hint: authHint
308
- });
309
- }
310
- }
311
- return routes;
312
- }
313
-
314
- /** @param {WorkspacePaths} paths @returns {any} */
315
- function inferNextApiRoutes(paths) {
316
- const apiRoot = path.join(paths.workspaceRoot, "app", "api");
317
- if (!fs.existsSync(apiRoot)) {
318
- return [];
319
- }
320
- const routeFiles = listFilesRecursive(
321
- apiRoot,
322
- (/** @type {any} */ child) => /\/route\.(tsx|ts|jsx|js)$/.test(child) || /^route\.(tsx|ts|jsx|js)$/.test(path.basename(child))
323
- );
324
- /** @type {any[]} */
325
- const routes = [];
326
- for (const filePath of routeFiles) {
327
- const text = readTextIfExists(filePath) || "";
328
- const relative = relativeTo(apiRoot, filePath);
329
- const routePath = `/${relative}`
330
- .replace(/\/route\.(tsx|ts|jsx|js)$/, "")
331
- .replace(/\[(\.\.\.)?([^\]]+)\]/g, (/** @type {any} */ _match, /** @type {any} */ catchAll, /** @type {any} */ name) => catchAll ? `:${name}*` : `:${name}`);
332
- for (const match of text.matchAll(/export\s+async\s+function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(([^)]*)\)/g)) {
333
- const method = match[1].toUpperCase();
334
- const handlerContext = extractNamedExportBlock(text, match[1]) || "";
335
- const queryParams = inferNextRequestSearchParams(handlerContext);
336
- const outputFields = inferNextJsonFields(handlerContext);
337
- const authHint = inferRouteAuthHint(match[0], handlerContext);
338
- routes.push({
339
- file: filePath,
340
- method,
341
- path: routePath === "" ? "/" : routePath,
342
- handler_hint: match[1].toLowerCase(),
343
- path_params: [...normalizeOpenApiPath(routePath).matchAll(/\{([^}]+)\}/g)].map((/** @type {any} */ entry) => entry[1]),
344
- query_params: queryParams,
345
- output_fields: outputFields,
346
- auth_hint: authHint,
347
- source_kind: "route_code"
348
- });
349
- }
350
- }
351
- return routes;
352
- }
353
-
354
- /** @param {any} appRoot @param {string} filePath @returns {any} */
355
- function nextAppRoutePathFromFile(appRoot, filePath) {
356
- const relative = relativeTo(appRoot, filePath);
357
- return `/${relative}`
358
- .replace(/\/actions\.(tsx|ts|jsx|js)$/, "")
359
- .replace(/\/page\.(tsx|ts|jsx|js|mdx)$/, "")
360
- .replace(/\[(\.\.\.)?([^\]]+)\]/g, (/** @type {any} */ _match, /** @type {any} */ catchAll, /** @type {any} */ name) => catchAll ? `:${name}*` : `:${name}`)
361
- .replace(/\/index$/, "")
362
- .replace(/^\/$/, "/") || "/";
363
- }
364
-
365
- /** @param {string} text @returns {any} */
366
- function inferFormDataFields(text) {
367
- const fields = new Set();
368
- for (const match of String(text || "").matchAll(/formData\.get\(\s*["'`]([^"'`]+)["'`]\s*\)/g)) {
369
- fields.add(match[1]);
370
- }
371
- return [...fields].sort();
372
- }
373
-
374
- /** @param {string} text @returns {any} */
375
- function inferInputNames(text) {
376
- const fields = new Set();
377
- for (const match of String(text || "").matchAll(/\bname=["'`]([^"'`]+)["'`]/g)) {
378
- fields.add(match[1]);
379
- }
380
- return [...fields].sort();
381
- }
382
-
383
- /** @param {WorkspacePaths} paths @returns {any} */
384
- function inferNextAuthCapabilities(paths) {
385
- const authConfigPath = path.join(paths.workspaceRoot, "auth.ts");
386
- const authConfigText = readTextIfExists(authConfigPath) || "";
387
- const hasCredentialsProvider = /CredentialsProvider\s*\(/.test(authConfigText);
388
- const createsUserOnAuthorize = /prisma\.user\.create\s*\(/.test(authConfigText);
389
- const loginPagePath = path.join(paths.workspaceRoot, "app", "login", "page.tsx");
390
- const registerPagePath = path.join(paths.workspaceRoot, "app", "register", "page.tsx");
391
- const pages = [
392
- {
393
- file: loginPagePath,
394
- path: "/login",
395
- id_hint: "cap_sign_in_user",
396
- label: "Sign In User",
397
- target_state: "authenticated"
398
- },
399
- {
400
- file: registerPagePath,
401
- path: "/register",
402
- id_hint: "cap_register_user",
403
- label: "Register User",
404
- target_state: createsUserOnAuthorize ? "registered" : "created"
405
- }
406
- ];
407
- /** @type {any[]} */
408
- const capabilities = [];
409
- for (const page of pages) {
410
- const text = readTextIfExists(page.file) || "";
411
- if (!text || !/signIn\(\s*["'`]credentials["'`]/.test(text)) {
412
- continue;
413
- }
414
- const inputFields = inferInputNames(text);
415
- capabilities.push({
416
- file: page.file,
417
- function_name: page.id_hint.replace(/^cap_/, ""),
418
- method: "POST",
419
- path: page.path,
420
- id_hint: page.id_hint,
421
- label: page.label,
422
- input_fields: inputFields,
423
- output_fields: [],
424
- path_params: [],
425
- auth_hint: "public",
426
- entity_id: "entity_user",
427
- target_state: page.target_state,
428
- provenance: [
429
- relativeTo(paths.repoRoot, page.file),
430
- ...(hasCredentialsProvider ? [relativeTo(paths.repoRoot, authConfigPath)] : [])
431
- ],
432
- source_kind: "route_code"
433
- });
434
- }
435
- return capabilities.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint));
436
- }
437
-
438
- /** @param {WorkspacePaths} paths @returns {any} */
439
- function inferNextServerActionCapabilities(paths) {
440
- const appRoot = path.join(paths.workspaceRoot, "app");
441
- if (!fs.existsSync(appRoot)) {
442
- return [];
443
- }
444
- const actionFiles = listFilesRecursive(
445
- appRoot,
446
- (/** @type {any} */ child) =>
447
- /\/actions\.(tsx|ts|jsx|js)$/.test(child) ||
448
- /\/page\.(tsx|ts|jsx|js|mdx)$/.test(child) ||
449
- /^page\.(tsx|ts|jsx|js|mdx)$/.test(path.basename(child))
450
- );
451
- /** @type {any[]} */
452
- const capabilities = [];
453
- for (const filePath of actionFiles) {
454
- const text = readTextIfExists(filePath) || "";
455
- const routePath = nextAppRoutePathFromFile(appRoot, filePath);
456
- for (const match of text.matchAll(/(?:export\s+)?async\s+function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)\s*\{([\s\S]{0,2400}?)\n\}/g)) {
457
- const functionName = match[1];
458
- const body = match[3] || "";
459
- const trimmedBody = body.trimStart();
460
- const isServerAction =
461
- /\/actions\.(tsx|ts|jsx|js)$/.test(filePath) ||
462
- trimmedBody.startsWith('"use server"') ||
463
- trimmedBody.startsWith("'use server'");
464
- if (!isServerAction) {
465
- continue;
466
- }
467
- /** @type {WorkflowRecord} */
468
- const routeLike = {
469
- file: filePath,
470
- method: "POST",
471
- path: routePath,
472
- handler_hint: functionName,
473
- auth_hint: inferRouteAuthHint(functionName, body)
474
- };
475
- capabilities.push({
476
- file: filePath,
477
- function_name: functionName,
478
- method: "POST",
479
- path: routePath,
480
- id_hint: inferRouteCapabilityId(routeLike),
481
- input_fields: inferFormDataFields(body),
482
- output_fields: [],
483
- path_params: [...normalizeOpenApiPath(routePath).matchAll(/\{([^}]+)\}/g)].map((/** @type {any} */ entry) => entry[1]),
484
- auth_hint: routeLike.auth_hint,
485
- entity_id: inferCapabilityEntityId({ endpoint: { path: routePath }, id_hint: inferRouteCapabilityId(routeLike) }),
486
- source_kind: "route_code"
487
- });
488
- }
489
- }
490
- return capabilities.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint) || a.path.localeCompare(b.path));
491
- }
492
-
493
- /** @param {string} text @param {string} exportName @returns {any} */
494
- function extractNamedExportBlock(text, exportName) {
495
- const escapedName = exportName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
496
- const match = text.match(new RegExp(`export\\s+async\\s+function\\s+${escapedName}\\s*\\([^)]*\\)\\s*\\{([\\s\\S]{0,2000}?)\\n\\}`, "m"));
497
- return match ? match[1] : "";
498
- }
499
-
500
- /** @param {string} text @returns {any} */
501
- function inferNextRequestSearchParams(text) {
502
- const params = new Set();
503
- for (const match of String(text || "").matchAll(/searchParams\.get\(\s*["'`]([^"'`]+)["'`]\s*\)/g)) {
504
- params.add(match[1]);
505
- }
506
- return [...params].sort();
507
- }
508
-
509
- /** @param {string} text @returns {any} */
510
- function inferNextJsonFields(text) {
511
- const fields = new Set();
512
- for (const match of String(text || "").matchAll(/NextResponse\.json\(\s*\{([\s\S]{0,400}?)\}\s*\)/g)) {
513
- for (const fieldMatch of match[1].matchAll(/\b([A-Za-z_][A-Za-z0-9_]*)\b\s*[:,]/g)) {
514
- fields.add(fieldMatch[1]);
515
- }
516
- }
517
- return [...fields].sort();
518
- }
519
-
520
- /** @param {string} text @param {any} handlerName @returns {any} */
521
- function extractHandlerContext(text, handlerName) {
522
- if (!handlerName) {
523
- return "";
524
- }
525
- const escapedName = handlerName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
526
- const patterns = [
527
- new RegExp(`function\\s+${escapedName}\\s*\\([^)]*\\)\\s*\\{([\\s\\S]{0,1200}?)\\n\\}`, "m"),
528
- new RegExp(`const\\s+${escapedName}\\s*=\\s*(?:async\\s*)?\\([^)]*\\)\\s*=>\\s*\\{([\\s\\S]{0,1200}?)\\n\\}`, "m")
529
- ];
530
- for (const pattern of patterns) {
531
- const match = text.match(pattern);
532
- if (match) {
533
- return match[1];
534
- }
535
- }
536
- return "";
537
- }
538
-
539
- /** @param {string} text @returns {any} */
540
- function inferRouteQueryParams(text) {
541
- const params = new Set();
542
- for (const match of String(text || "").matchAll(/\bquery\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g)) {
543
- params.add(match[1]);
544
- }
545
- for (const match of String(text || "").matchAll(/\bquery\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
546
- params.add(match[1]);
547
- }
548
- return [...params].sort();
549
- }
550
-
551
- /** @param {any[]} routeArguments @param {any} handlerContext @returns {any} */
552
- function inferRouteAuthHint(routeArguments, handlerContext) {
553
- const combined = `${routeArguments || ""}\n${handlerContext || ""}`.toLowerCase();
554
- return /\b(auth|session|permission|guard|protected|require_auth|requireauth|ensureauth)\b/.test(combined)
555
- ? "secured"
556
- : "unknown";
557
- }
558
-
559
- /** @param {WorkflowRecord} route @returns {any} */
560
- function inferRouteCapabilityId(route) {
561
- if (route.handler_hint) {
562
- const genericHttpHandler = /^(get|post|put|patch|delete)$/i.test(route.handler_hint);
563
- if (!genericHttpHandler) {
564
- const normalizedHandler = route.handler_hint
565
- .replace(/^(handle|on)/i, "")
566
- .replace(/(handler|route|controller|action)$/i, "");
567
- const handlerTokens = normalizedHandler
568
- .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
569
- .split(/[^A-Za-z0-9]+/)
570
- .filter(Boolean)
571
- .map((/** @type {any} */ token) => token.toLowerCase());
572
- if (handlerTokens.length > 0) {
573
- return `cap_${handlerTokens.join("_")}`;
574
- }
575
- }
576
- }
577
- const method = String(route.method || "").toUpperCase();
578
- const segments = routeSegments(normalizeOpenApiPath(route.path));
579
- const resource = canonicalCandidateTerm(segments[0] || "item");
580
- if (method === "GET" && segments.length <= 1) {
581
- return `cap_list_${resource}s`;
582
- }
583
- if (method === "GET" && segments.length > 1) {
584
- return `cap_get_${resource}`;
585
- }
586
- if (method === "POST") {
587
- return `cap_create_${resource}`;
588
- }
589
- if (method === "PATCH" || method === "PUT") {
590
- return `cap_update_${resource}`;
591
- }
592
- if (method === "DELETE") {
593
- return `cap_delete_${resource}`;
594
- }
595
- return `candidate_${route.method.toLowerCase()}_${slugify(route.path)}`;
596
- }
597
-
598
- /** @param {WorkspacePaths} paths @returns {any} */
599
- export function collectApiImport(paths) {
600
- /** @type {any[]} */
601
- const findings = [];
602
- /** @type {WorkflowRecord} */
603
- const candidates = {
604
- capabilities: [],
605
- routes: [],
606
- stacks: []
607
- };
608
- const { openApiFiles } = discoverApiSources(paths);
609
- let usedOpenApi = false;
610
- for (const filePath of openApiFiles) {
611
- const provenance = relativeTo(paths.repoRoot, filePath);
612
- const text = readTextIfExists(filePath) || "";
613
- const document = filePath.endsWith(".json") ? JSON.parse(text) : parseOpenApiYaml(text);
614
- const parsed = parseOpenApiDocument(document, provenance, "openapi");
615
- usedOpenApi = true;
616
- findings.push({
617
- kind: "openapi",
618
- file: provenance,
619
- capability_count: parsed.capabilities.length
620
- });
621
- candidates.capabilities.push(...parsed.capabilities);
622
- candidates.routes.push(...parsed.routes.map((/** @type {any} */ route) => ({
623
- path: route.path,
624
- method: route.method,
625
- confidence: "high",
626
- source_kind: route.source_kind,
627
- provenance: route.provenance
628
- })));
629
- }
630
- if (!usedOpenApi) {
631
- const inferredRoutes = [
632
- ...inferNextApiRoutes(paths),
633
- ...inferServerRoutes(paths)
634
- ];
635
- const inferredServerActions = inferNextServerActionCapabilities(paths);
636
- const inferredAuthCapabilities = inferNextAuthCapabilities(paths);
637
- if (inferredRoutes.length > 0) {
638
- findings.push({
639
- kind: "route_inventory",
640
- files: [...new Set(inferredRoutes.map((/** @type {any} */ route) => relativeTo(paths.repoRoot, route.file)))],
641
- route_count: inferredRoutes.length
642
- });
643
- candidates.capabilities.push(
644
- ...inferredRoutes.map((/** @type {any} */ route) =>
645
- makeCandidateRecord({
646
- kind: "capability",
647
- idHint: inferRouteCapabilityId(route),
648
- label: `${route.method} ${route.path}`,
649
- confidence: "medium",
650
- sourceKind: "route_code",
651
- provenance: `${relativeTo(paths.repoRoot, route.file)}#${route.method} ${route.path}`,
652
- endpoint: {
653
- method: route.method,
654
- path: normalizeOpenApiPath(route.path)
655
- },
656
- path_params: (route.path_params || []).map((/** @type {any} */ name) => ({ name, required: true, type: null })),
657
- query_params: (route.query_params || []).map((/** @type {any} */ name) => ({ name, required: false, type: null })),
658
- header_params: [],
659
- input_fields: [],
660
- output_fields: route.output_fields || [],
661
- auth_hint: route.auth_hint || "unknown"
662
- })
663
- )
664
- );
665
- candidates.routes.push(
666
- ...inferredRoutes.map((/** @type {any} */ route) => ({
667
- path: normalizeOpenApiPath(route.path),
668
- method: route.method,
669
- confidence: "medium",
670
- source_kind: "route_code",
671
- provenance: `${relativeTo(paths.repoRoot, route.file)}#${route.method} ${route.path}`
672
- }))
673
- );
674
- }
675
- if (inferredServerActions.length > 0) {
676
- findings.push({
677
- kind: "next_server_actions",
678
- files: [...new Set(inferredServerActions.map((/** @type {any} */ action) => relativeTo(paths.repoRoot, action.file)))],
679
- action_count: inferredServerActions.length
680
- });
681
- candidates.capabilities.push(
682
- ...inferredServerActions.map((/** @type {any} */ action) =>
683
- makeCandidateRecord({
684
- kind: "capability",
685
- idHint: action.id_hint,
686
- label: titleCase(action.id_hint.replace(/^cap_/, "")),
687
- confidence: "medium",
688
- sourceKind: action.source_kind,
689
- provenance: `${relativeTo(paths.repoRoot, action.file)}#${action.function_name}`,
690
- endpoint: {
691
- method: action.method,
692
- path: normalizeOpenApiPath(action.path)
693
- },
694
- path_params: (action.path_params || []).map((/** @type {any} */ name) => ({ name, required: true, type: null })),
695
- query_params: [],
696
- header_params: [],
697
- input_fields: action.input_fields || [],
698
- output_fields: action.output_fields || [],
699
- auth_hint: action.auth_hint || "unknown"
700
- })
701
- )
702
- );
703
- candidates.routes.push(
704
- ...inferredServerActions.map((/** @type {any} */ action) => ({
705
- path: normalizeOpenApiPath(action.path),
706
- method: action.method,
707
- confidence: "medium",
708
- source_kind: action.source_kind,
709
- provenance: `${relativeTo(paths.repoRoot, action.file)}#${action.function_name}`
710
- }))
711
- );
712
- }
713
- if (inferredAuthCapabilities.length > 0) {
714
- findings.push({
715
- kind: "next_auth_flows",
716
- files: [...new Set(inferredAuthCapabilities.flatMap((/** @type {any} */ capability) => capability.provenance || []))],
717
- capability_count: inferredAuthCapabilities.length
718
- });
719
- candidates.capabilities.push(
720
- ...inferredAuthCapabilities.map((/** @type {any} */ capability) =>
721
- makeCandidateRecord({
722
- kind: "capability",
723
- idHint: capability.id_hint,
724
- label: capability.label,
725
- confidence: "medium",
726
- sourceKind: capability.source_kind,
727
- provenance: capability.provenance,
728
- endpoint: {
729
- method: capability.method,
730
- path: normalizeOpenApiPath(capability.path)
731
- },
732
- path_params: [],
733
- query_params: [],
734
- header_params: [],
735
- input_fields: capability.input_fields || [],
736
- output_fields: capability.output_fields || [],
737
- auth_hint: capability.auth_hint || "unknown",
738
- entity_id: capability.entity_id,
739
- target_state: capability.target_state || null
740
- })
741
- )
742
- );
743
- candidates.routes.push(
744
- ...inferredAuthCapabilities.map((/** @type {any} */ capability) => ({
745
- path: normalizeOpenApiPath(capability.path),
746
- method: capability.method,
747
- confidence: "medium",
748
- source_kind: capability.source_kind,
749
- provenance: capability.provenance
750
- }))
751
- );
752
- }
753
- }
754
- const reactRoutes = inferReactRoutes(path.join(paths.workspaceRoot, "apps", "web"));
755
- if (reactRoutes.length > 0) {
756
- findings.push({
757
- kind: "react_routes",
758
- file: relativeTo(paths.repoRoot, path.join(paths.workspaceRoot, "apps", "web", "src", "App.tsx")),
759
- routes: reactRoutes
760
- });
761
- candidates.routes.push(...reactRoutes.map((/** @type {any} */ route) => ({
762
- path: route,
763
- method: "GET",
764
- confidence: "medium",
765
- source_kind: "generated_artifact",
766
- provenance: relativeTo(paths.repoRoot, path.join(paths.workspaceRoot, "apps", "web", "src", "App.tsx"))
767
- })));
768
- candidates.stacks.push("react_web");
769
- }
770
- const svelteRoutes = inferSvelteRoutes(path.join(paths.workspaceRoot, "apps", "web-sveltekit"));
771
- if (svelteRoutes.length > 0) {
772
- findings.push({
773
- kind: "sveltekit_routes",
774
- file: relativeTo(paths.repoRoot, path.join(paths.workspaceRoot, "apps", "web-sveltekit", "src", "routes")),
775
- routes: svelteRoutes
776
- });
777
- candidates.routes.push(...svelteRoutes.map((/** @type {any} */ route) => ({
778
- path: route,
779
- method: "GET",
780
- confidence: "medium",
781
- source_kind: "generated_artifact",
782
- provenance: relativeTo(paths.repoRoot, path.join(paths.workspaceRoot, "apps", "web-sveltekit", "src", "routes"))
783
- })));
784
- candidates.stacks.push("sveltekit_web");
785
- }
786
- candidates.capabilities = dedupeCandidateRecords(candidates.capabilities, (/** @type {any} */ record) => record.id_hint);
787
- candidates.routes = dedupeCandidateRecords(
788
- candidates.routes.map((/** @type {any} */ route) => ({
789
- ...route,
790
- id_hint: route.id_hint || `${route.method}_${route.path}`
791
- })),
792
- (/** @type {any} */ record) => `${record.method}:${record.path}:${record.source_kind}`
793
- ).map((/** @type {any} */ record) => {
794
- const { id_hint, ...route } = record;
795
- return route;
796
- });
797
-
798
- return { findings, candidates };
799
- }
3
+ export { collectApiImport } from "./api/collect.js";
4
+ export { discoverApiSources } from "./api/sources.js";