@topogram/cli 0.3.64 → 0.3.65

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 (245) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +703 -0
  3. package/src/adoption/plan.js +12 -703
  4. package/src/agent-ops/query-builders/auth.js +375 -0
  5. package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
  6. package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
  7. package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
  8. package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
  9. package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
  10. package/src/agent-ops/query-builders/change-risk.js +25 -0
  11. package/src/agent-ops/query-builders/common.js +149 -0
  12. package/src/agent-ops/query-builders/maintained-risk.js +539 -0
  13. package/src/agent-ops/query-builders/maintained-shared.js +120 -0
  14. package/src/agent-ops/query-builders/multi-agent.js +547 -0
  15. package/src/agent-ops/query-builders/projection-impacts.js +514 -0
  16. package/src/agent-ops/query-builders/work-packets.js +417 -0
  17. package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
  18. package/src/agent-ops/query-builders/workflow-context.js +398 -0
  19. package/src/agent-ops/query-builders/workflow-presets-core.js +676 -0
  20. package/src/agent-ops/query-builders/workflow-presets.js +341 -0
  21. package/src/agent-ops/query-builders.d.ts +26 -26
  22. package/src/agent-ops/query-builders.js +42 -5021
  23. package/src/catalog/constants.js +10 -0
  24. package/src/catalog/copy.js +60 -0
  25. package/src/catalog/diagnostics.js +15 -0
  26. package/src/catalog/entries.js +42 -0
  27. package/src/catalog/files.js +67 -0
  28. package/src/catalog/provenance.js +122 -0
  29. package/src/catalog/source.js +150 -0
  30. package/src/catalog/validation.js +252 -0
  31. package/src/catalog.d.ts +2 -0
  32. package/src/catalog.js +18 -746
  33. package/src/cli/commands/catalog/check.js +31 -0
  34. package/src/cli/commands/catalog/copy.js +59 -0
  35. package/src/cli/commands/catalog/doctor.js +248 -0
  36. package/src/cli/commands/catalog/help.js +21 -0
  37. package/src/cli/commands/catalog/list.js +52 -0
  38. package/src/cli/commands/catalog/runner.js +92 -0
  39. package/src/cli/commands/catalog/shared.js +17 -0
  40. package/src/cli/commands/catalog/show.js +134 -0
  41. package/src/cli/commands/catalog.js +30 -615
  42. package/src/cli/commands/generator-policy/package-info.js +162 -0
  43. package/src/cli/commands/generator-policy/payloads.js +372 -0
  44. package/src/cli/commands/generator-policy/printers.js +159 -0
  45. package/src/cli/commands/generator-policy/runner.js +81 -0
  46. package/src/cli/commands/generator-policy/shared.js +39 -0
  47. package/src/cli/commands/generator-policy.js +15 -783
  48. package/src/cli/commands/import/adopt.js +170 -0
  49. package/src/cli/commands/import/check.js +91 -0
  50. package/src/cli/commands/import/diff.js +84 -0
  51. package/src/cli/commands/import/help.js +47 -0
  52. package/src/cli/commands/import/paths.js +277 -0
  53. package/src/cli/commands/import/plan.js +284 -0
  54. package/src/cli/commands/import/refresh.js +470 -0
  55. package/src/cli/commands/import/status-history.js +196 -0
  56. package/src/cli/commands/import/workspace.js +230 -0
  57. package/src/cli/commands/import.js +33 -1732
  58. package/src/cli/commands/package/constants.js +17 -0
  59. package/src/cli/commands/package/doctor.js +240 -0
  60. package/src/cli/commands/package/help.js +27 -0
  61. package/src/cli/commands/package/lockfile.js +135 -0
  62. package/src/cli/commands/package/npm.js +97 -0
  63. package/src/cli/commands/package/reporting.js +35 -0
  64. package/src/cli/commands/package/runner.js +33 -0
  65. package/src/cli/commands/package/shared.js +9 -0
  66. package/src/cli/commands/package/update-cli.js +252 -0
  67. package/src/cli/commands/package/versions.js +35 -0
  68. package/src/cli/commands/package.js +29 -813
  69. package/src/cli/commands/query/change-plan.js +68 -0
  70. package/src/cli/commands/query/definitions.js +202 -0
  71. package/src/cli/commands/query/import-adopt.js +121 -0
  72. package/src/cli/commands/query/runner/artifacts.js +102 -0
  73. package/src/cli/commands/query/runner/boundaries.js +211 -0
  74. package/src/cli/commands/query/runner/change.js +182 -0
  75. package/src/cli/commands/query/runner/import-adopt.js +111 -0
  76. package/src/cli/commands/query/runner/index.js +31 -0
  77. package/src/cli/commands/query/runner/output.js +12 -0
  78. package/src/cli/commands/query/runner/workflow.js +241 -0
  79. package/src/cli/commands/query/runner.js +3 -0
  80. package/src/cli/commands/query/workflow-context.js +5 -0
  81. package/src/cli/commands/query/workspace.js +274 -0
  82. package/src/cli/commands/query.js +9 -1300
  83. package/src/cli/commands/template/baseline.js +100 -0
  84. package/src/cli/commands/template/check.js +466 -0
  85. package/src/cli/commands/template/constants.js +8 -0
  86. package/src/cli/commands/template/diagnostics.js +26 -0
  87. package/src/cli/commands/template/help.js +28 -0
  88. package/src/cli/commands/template/lifecycle.js +404 -0
  89. package/src/cli/commands/template/list-show.js +287 -0
  90. package/src/cli/commands/template/policy.js +422 -0
  91. package/src/cli/commands/template/shared.js +127 -0
  92. package/src/cli/commands/template/updates.js +352 -0
  93. package/src/cli/commands/template.js +41 -2143
  94. package/src/generator/api/contracts.js +497 -0
  95. package/src/generator/api/metadata.js +221 -0
  96. package/src/generator/api/openapi.js +559 -0
  97. package/src/generator/api/schema.js +124 -0
  98. package/src/generator/api/types.d.ts +98 -0
  99. package/src/generator/api.js +3 -1195
  100. package/src/generator/context/shared/domain-sdlc.js +282 -0
  101. package/src/generator/context/shared/maintained-boundary.js +665 -0
  102. package/src/generator/context/shared/metrics.js +85 -0
  103. package/src/generator/context/shared/primitives.js +64 -0
  104. package/src/generator/context/shared/relationships.js +453 -0
  105. package/src/generator/context/shared/summaries.js +263 -0
  106. package/src/generator/context/shared/types.d.ts +207 -0
  107. package/src/generator/context/shared.d.ts +42 -0
  108. package/src/generator/context/shared.js +80 -1390
  109. package/src/generator/context/slice/core.js +397 -0
  110. package/src/generator/context/slice/sdlc.js +417 -0
  111. package/src/generator/context/slice/ui-packets.js +183 -0
  112. package/src/generator/context/slice.js +2 -859
  113. package/src/generator/registry/index.js +507 -0
  114. package/src/generator/registry.js +18 -504
  115. package/src/generator/runtime/environment/index.js +666 -0
  116. package/src/generator/runtime/environment.js +4 -666
  117. package/src/generator/runtime/runtime-check/index.js +554 -0
  118. package/src/generator/runtime/runtime-check.js +4 -554
  119. package/src/generator/runtime/shared/index.js +572 -0
  120. package/src/generator/runtime/shared.js +19 -570
  121. package/src/generator/shared.d.ts +2 -0
  122. package/src/generator/surfaces/shared.d.ts +3 -0
  123. package/src/generator/widget-conformance/behavior-report.js +258 -0
  124. package/src/generator/widget-conformance/checks.js +371 -0
  125. package/src/generator/widget-conformance/projection-context.js +200 -0
  126. package/src/generator/widget-conformance/report.js +166 -0
  127. package/src/generator/widget-conformance/types.d.ts +121 -0
  128. package/src/generator/widget-conformance.js +3 -824
  129. package/src/import/core/context.d.ts +3 -0
  130. package/src/import/core/contracts.d.ts +1 -0
  131. package/src/import/core/registry.d.ts +4 -0
  132. package/src/import/core/runner/candidates.js +217 -0
  133. package/src/import/core/runner/options.js +22 -0
  134. package/src/import/core/runner/reports.js +50 -0
  135. package/src/import/core/runner/run.js +79 -0
  136. package/src/import/core/runner/tracks.js +150 -0
  137. package/src/import/core/runner/ui-drafts.js +337 -0
  138. package/src/import/core/runner.js +3 -698
  139. package/src/import/core/shared/api-routes.js +221 -0
  140. package/src/import/core/shared/candidates.js +97 -0
  141. package/src/import/core/shared/files.js +177 -0
  142. package/src/import/core/shared/next-app.js +389 -0
  143. package/src/import/core/shared/types.d.ts +51 -0
  144. package/src/import/core/shared/ui-routes.js +230 -0
  145. package/src/import/core/shared.js +60 -861
  146. package/src/new-project/constants.js +128 -0
  147. package/src/new-project/create.js +83 -0
  148. package/src/new-project/json.js +28 -0
  149. package/src/new-project/metadata.js +96 -0
  150. package/src/new-project/package-spec.js +161 -0
  151. package/src/new-project/project-files.js +348 -0
  152. package/src/new-project/template-policy.js +269 -0
  153. package/src/new-project/template-resolution.js +368 -0
  154. package/src/new-project/template-snapshots.js +430 -0
  155. package/src/new-project/template-updates.js +512 -0
  156. package/src/new-project/types.d.ts +83 -0
  157. package/src/new-project.js +6 -2277
  158. package/src/parser.d.ts +87 -1
  159. package/src/parser.js +118 -0
  160. package/src/policy/review-boundaries.d.ts +15 -0
  161. package/src/project-config/index.js +564 -0
  162. package/src/project-config.js +19 -561
  163. package/src/resolver/enrich/acceptance-criterion.js +2 -0
  164. package/src/resolver/enrich/bug.js +2 -0
  165. package/src/resolver/enrich/pitch.js +2 -0
  166. package/src/resolver/enrich/requirement.js +2 -0
  167. package/src/resolver/enrich/task.js +2 -0
  168. package/src/resolver/index.js +19 -2089
  169. package/src/resolver/normalize.js +384 -1
  170. package/src/resolver/plans.js +168 -0
  171. package/src/resolver/projections-api.js +494 -0
  172. package/src/resolver/projections-db.js +133 -0
  173. package/src/resolver/projections-ui.js +317 -0
  174. package/src/resolver/shapes.js +251 -0
  175. package/src/resolver/shared.js +278 -0
  176. package/src/resolver/widgets.js +132 -0
  177. package/src/template-trust/constants.js +62 -0
  178. package/src/template-trust/content.js +258 -0
  179. package/src/template-trust/diff.js +92 -0
  180. package/src/template-trust/policy.js +61 -0
  181. package/src/template-trust/record.js +90 -0
  182. package/src/template-trust/status.js +182 -0
  183. package/src/template-trust.js +24 -687
  184. package/src/text-helpers.d.ts +1 -0
  185. package/src/topogram-types.d.ts +69 -0
  186. package/src/validator/common.js +488 -0
  187. package/src/validator/data-model.js +237 -0
  188. package/src/validator/docs.js +167 -0
  189. package/src/validator/expressions.js +146 -1
  190. package/src/validator/index.d.ts +23 -0
  191. package/src/validator/index.js +32 -3585
  192. package/src/validator/kinds.d.ts +41 -0
  193. package/src/validator/kinds.js +2 -0
  194. package/src/validator/model-helpers.js +46 -0
  195. package/src/validator/per-kind/acceptance-criterion.js +5 -0
  196. package/src/validator/per-kind/bug.js +6 -0
  197. package/src/validator/per-kind/domain.js +15 -2
  198. package/src/validator/per-kind/pitch.js +7 -0
  199. package/src/validator/per-kind/requirement.js +5 -0
  200. package/src/validator/per-kind/task.js +7 -0
  201. package/src/validator/per-kind/widget.js +14 -0
  202. package/src/validator/projections/api-http-async.js +410 -0
  203. package/src/validator/projections/api-http-authz.js +88 -0
  204. package/src/validator/projections/api-http-core.js +205 -0
  205. package/src/validator/projections/api-http-policies.js +339 -0
  206. package/src/validator/projections/api-http-responses.js +233 -0
  207. package/src/validator/projections/api-http.js +44 -0
  208. package/src/validator/projections/db.js +353 -0
  209. package/src/validator/projections/generator-defaults.js +45 -0
  210. package/src/validator/projections/helpers.js +87 -0
  211. package/src/validator/projections/ui-helpers.js +214 -0
  212. package/src/validator/projections/ui-navigation.js +344 -0
  213. package/src/validator/projections/ui-structure.js +364 -0
  214. package/src/validator/projections/ui-widgets.js +493 -0
  215. package/src/validator/projections/ui.js +46 -0
  216. package/src/validator/registry.js +48 -1
  217. package/src/validator/utils.d.ts +20 -0
  218. package/src/validator/utils.js +115 -12
  219. package/src/widget-behavior.d.ts +1 -0
  220. package/src/workflows/import-app/api/collect.js +221 -0
  221. package/src/workflows/import-app/api/openapi.js +257 -0
  222. package/src/workflows/import-app/api/routes.js +327 -0
  223. package/src/workflows/import-app/api/sources.js +22 -0
  224. package/src/workflows/import-app/api.js +2 -797
  225. package/src/workflows/reconcile/adoption-plan/build.js +208 -0
  226. package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
  227. package/src/workflows/reconcile/adoption-plan/outputs.js +143 -0
  228. package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
  229. package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
  230. package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
  231. package/src/workflows/reconcile/adoption-plan.js +30 -740
  232. package/src/workflows/reconcile/auth/closures.js +115 -0
  233. package/src/workflows/reconcile/auth/formatters.js +142 -0
  234. package/src/workflows/reconcile/auth/inference.js +330 -0
  235. package/src/workflows/reconcile/auth/roles.js +122 -0
  236. package/src/workflows/reconcile/auth.js +35 -690
  237. package/src/workflows/reconcile/bundle-core/index.js +600 -0
  238. package/src/workflows/reconcile/bundle-core.js +12 -598
  239. package/src/workflows/reconcile/canonical-surface.js +1 -1
  240. package/src/workflows/reconcile/impacts/adoption-plan.js +192 -0
  241. package/src/workflows/reconcile/impacts/indexes.js +101 -0
  242. package/src/workflows/reconcile/impacts/patches.js +252 -0
  243. package/src/workflows/reconcile/impacts/reports.js +80 -0
  244. package/src/workflows/reconcile/impacts.js +14 -623
  245. package/src/workspace-docs.d.ts +29 -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";