@topogram/cli 0.3.63 → 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 (344) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +703 -0
  3. package/src/adoption/plan.d.ts +6 -0
  4. package/src/adoption/plan.js +12 -703
  5. package/src/adoption/reporting.d.ts +10 -0
  6. package/src/adoption/review-groups.d.ts +6 -0
  7. package/src/agent-brief.d.ts +3 -0
  8. package/src/agent-brief.js +495 -0
  9. package/src/agent-ops/query-builders/auth.js +375 -0
  10. package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
  11. package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
  12. package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
  13. package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
  14. package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
  15. package/src/agent-ops/query-builders/change-risk.js +25 -0
  16. package/src/agent-ops/query-builders/common.js +149 -0
  17. package/src/agent-ops/query-builders/maintained-risk.js +539 -0
  18. package/src/agent-ops/query-builders/maintained-shared.js +120 -0
  19. package/src/agent-ops/query-builders/multi-agent.js +547 -0
  20. package/src/agent-ops/query-builders/projection-impacts.js +514 -0
  21. package/src/agent-ops/query-builders/work-packets.js +417 -0
  22. package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
  23. package/src/agent-ops/query-builders/workflow-context.js +398 -0
  24. package/src/agent-ops/query-builders/workflow-presets-core.js +676 -0
  25. package/src/agent-ops/query-builders/workflow-presets.js +341 -0
  26. package/src/agent-ops/query-builders.d.ts +26 -0
  27. package/src/agent-ops/query-builders.js +42 -5021
  28. package/src/archive/archive.d.ts +2 -0
  29. package/src/archive/compact.d.ts +1 -0
  30. package/src/archive/unarchive.d.ts +1 -0
  31. package/src/catalog/constants.js +10 -0
  32. package/src/catalog/copy.js +60 -0
  33. package/src/catalog/diagnostics.js +15 -0
  34. package/src/catalog/entries.js +42 -0
  35. package/src/catalog/files.js +67 -0
  36. package/src/catalog/provenance.js +122 -0
  37. package/src/catalog/source.js +150 -0
  38. package/src/catalog/validation.js +252 -0
  39. package/src/catalog.d.ts +12 -0
  40. package/src/catalog.js +18 -750
  41. package/src/cli/catalog-alias.d.ts +1 -0
  42. package/src/cli/command-parser.js +38 -0
  43. package/src/cli/command-parsers/core.js +102 -0
  44. package/src/cli/command-parsers/generator.js +39 -0
  45. package/src/cli/command-parsers/import.js +44 -0
  46. package/src/cli/command-parsers/legacy-workflow.js +21 -0
  47. package/src/cli/command-parsers/project.js +47 -0
  48. package/src/cli/command-parsers/sdlc.js +47 -0
  49. package/src/cli/command-parsers/shared.js +51 -0
  50. package/src/cli/command-parsers/template.js +48 -0
  51. package/src/cli/commands/agent.js +47 -0
  52. package/src/cli/commands/catalog/check.js +31 -0
  53. package/src/cli/commands/catalog/copy.js +59 -0
  54. package/src/cli/commands/catalog/doctor.js +248 -0
  55. package/src/cli/commands/catalog/help.js +21 -0
  56. package/src/cli/commands/catalog/list.js +52 -0
  57. package/src/cli/commands/catalog/runner.js +92 -0
  58. package/src/cli/commands/catalog/shared.js +17 -0
  59. package/src/cli/commands/catalog/show.js +134 -0
  60. package/src/cli/commands/catalog.js +32 -0
  61. package/src/cli/commands/check.js +268 -0
  62. package/src/cli/commands/doctor.js +268 -0
  63. package/src/cli/commands/emit.js +149 -0
  64. package/src/cli/commands/generate.js +96 -0
  65. package/src/cli/commands/generator-policy/package-info.js +162 -0
  66. package/src/cli/commands/generator-policy/payloads.js +372 -0
  67. package/src/cli/commands/generator-policy/printers.js +159 -0
  68. package/src/cli/commands/generator-policy/runner.js +81 -0
  69. package/src/cli/commands/generator-policy/shared.js +39 -0
  70. package/src/cli/commands/generator-policy.js +17 -0
  71. package/src/cli/commands/generator.js +443 -0
  72. package/src/cli/commands/import/adopt.js +170 -0
  73. package/src/cli/commands/import/check.js +91 -0
  74. package/src/cli/commands/import/diff.js +84 -0
  75. package/src/cli/commands/import/help.js +47 -0
  76. package/src/cli/commands/import/paths.js +277 -0
  77. package/src/cli/commands/import/plan.js +284 -0
  78. package/src/cli/commands/import/refresh.js +470 -0
  79. package/src/cli/commands/import/status-history.js +196 -0
  80. package/src/cli/commands/import/workspace.js +230 -0
  81. package/src/cli/commands/import-runner.js +157 -0
  82. package/src/cli/commands/import.js +35 -0
  83. package/src/cli/commands/inspect.js +55 -0
  84. package/src/cli/commands/new.js +94 -0
  85. package/src/cli/commands/package/constants.js +17 -0
  86. package/src/cli/commands/package/doctor.js +240 -0
  87. package/src/cli/commands/package/help.js +27 -0
  88. package/src/cli/commands/package/lockfile.js +135 -0
  89. package/src/cli/commands/package/npm.js +97 -0
  90. package/src/cli/commands/package/reporting.js +35 -0
  91. package/src/cli/commands/package/runner.js +33 -0
  92. package/src/cli/commands/package/shared.js +9 -0
  93. package/src/cli/commands/package/update-cli.js +252 -0
  94. package/src/cli/commands/package/versions.js +35 -0
  95. package/src/cli/commands/package.js +31 -0
  96. package/src/cli/commands/query/change-plan.js +68 -0
  97. package/src/cli/commands/query/definitions.js +202 -0
  98. package/src/cli/commands/query/import-adopt.js +121 -0
  99. package/src/cli/commands/query/runner/artifacts.js +102 -0
  100. package/src/cli/commands/query/runner/boundaries.js +211 -0
  101. package/src/cli/commands/query/runner/change.js +182 -0
  102. package/src/cli/commands/query/runner/import-adopt.js +111 -0
  103. package/src/cli/commands/query/runner/index.js +31 -0
  104. package/src/cli/commands/query/runner/output.js +12 -0
  105. package/src/cli/commands/query/runner/workflow.js +241 -0
  106. package/src/cli/commands/query/runner.js +3 -0
  107. package/src/cli/commands/query/workflow-context.js +5 -0
  108. package/src/cli/commands/query/workspace.js +274 -0
  109. package/src/cli/commands/query.js +11 -0
  110. package/src/cli/commands/release-rollout.js +257 -0
  111. package/src/cli/commands/release-shared.js +528 -0
  112. package/src/cli/commands/release-status.js +429 -0
  113. package/src/cli/commands/release.js +107 -0
  114. package/src/cli/commands/sdlc.js +168 -0
  115. package/src/cli/commands/setup.js +76 -0
  116. package/src/cli/commands/source.js +291 -0
  117. package/src/cli/commands/template/baseline.js +100 -0
  118. package/src/cli/commands/template/check.js +466 -0
  119. package/src/cli/commands/template/constants.js +8 -0
  120. package/src/cli/commands/template/diagnostics.js +26 -0
  121. package/src/cli/commands/template/help.js +28 -0
  122. package/src/cli/commands/template/lifecycle.js +404 -0
  123. package/src/cli/commands/template/list-show.js +287 -0
  124. package/src/cli/commands/template/policy.js +422 -0
  125. package/src/cli/commands/template/shared.js +127 -0
  126. package/src/cli/commands/template/updates.js +352 -0
  127. package/src/cli/commands/template-runner.js +198 -0
  128. package/src/cli/commands/template.js +43 -0
  129. package/src/cli/commands/trust.js +219 -0
  130. package/src/cli/commands/version.js +40 -0
  131. package/src/cli/commands/widget.js +168 -0
  132. package/src/cli/commands/workflow.js +63 -0
  133. package/src/cli/dispatcher.js +392 -0
  134. package/src/cli/help-dispatch.js +188 -0
  135. package/src/cli/help.js +296 -0
  136. package/src/cli/migration-guidance.js +59 -0
  137. package/src/cli/options.js +96 -0
  138. package/src/cli/output-safety.js +107 -0
  139. package/src/cli/path-normalization.js +29 -0
  140. package/src/cli.js +47 -11711
  141. package/src/example-implementation.d.ts +2 -0
  142. package/src/format.d.ts +1 -0
  143. package/src/generator/api/contracts.js +497 -0
  144. package/src/generator/api/metadata.js +221 -0
  145. package/src/generator/api/openapi.js +559 -0
  146. package/src/generator/api/schema.js +124 -0
  147. package/src/generator/api/types.d.ts +98 -0
  148. package/src/generator/api.js +3 -1195
  149. package/src/generator/check.d.ts +1 -0
  150. package/src/generator/context/bundle.d.ts +1 -0
  151. package/src/generator/context/shared/domain-sdlc.js +282 -0
  152. package/src/generator/context/shared/maintained-boundary.js +665 -0
  153. package/src/generator/context/shared/metrics.js +85 -0
  154. package/src/generator/context/shared/primitives.js +64 -0
  155. package/src/generator/context/shared/relationships.js +453 -0
  156. package/src/generator/context/shared/summaries.js +263 -0
  157. package/src/generator/context/shared/types.d.ts +207 -0
  158. package/src/generator/context/shared.d.ts +44 -0
  159. package/src/generator/context/shared.js +80 -1390
  160. package/src/generator/context/slice/core.js +397 -0
  161. package/src/generator/context/slice/sdlc.js +417 -0
  162. package/src/generator/context/slice/ui-packets.js +183 -0
  163. package/src/generator/context/slice.js +2 -859
  164. package/src/generator/native/parity-bundle.js +2 -1
  165. package/src/generator/registry/index.js +507 -0
  166. package/src/generator/registry.js +18 -504
  167. package/src/generator/runtime/environment/index.js +666 -0
  168. package/src/generator/runtime/environment.js +4 -666
  169. package/src/generator/runtime/runtime-check/index.js +554 -0
  170. package/src/generator/runtime/runtime-check.js +4 -554
  171. package/src/generator/runtime/shared/index.js +572 -0
  172. package/src/generator/runtime/shared.js +19 -570
  173. package/src/generator/shared.d.ts +2 -0
  174. package/src/generator/surfaces/shared.d.ts +3 -0
  175. package/src/generator/surfaces/web/html-escape.js +22 -0
  176. package/src/generator/surfaces/web/react.js +10 -8
  177. package/src/generator/surfaces/web/sveltekit.js +7 -5
  178. package/src/generator/surfaces/web/vanilla.js +8 -4
  179. package/src/generator/widget-conformance/behavior-report.js +258 -0
  180. package/src/generator/widget-conformance/checks.js +371 -0
  181. package/src/generator/widget-conformance/projection-context.js +200 -0
  182. package/src/generator/widget-conformance/report.js +166 -0
  183. package/src/generator/widget-conformance/types.d.ts +121 -0
  184. package/src/generator/widget-conformance.js +3 -824
  185. package/src/generator.d.ts +2 -0
  186. package/src/github-client.js +520 -0
  187. package/src/import/core/context.d.ts +3 -0
  188. package/src/import/core/contracts.d.ts +1 -0
  189. package/src/import/core/registry.d.ts +4 -0
  190. package/src/import/core/runner/candidates.js +217 -0
  191. package/src/import/core/runner/options.js +22 -0
  192. package/src/import/core/runner/reports.js +50 -0
  193. package/src/import/core/runner/run.js +79 -0
  194. package/src/import/core/runner/tracks.js +150 -0
  195. package/src/import/core/runner/ui-drafts.js +337 -0
  196. package/src/import/core/runner.js +3 -698
  197. package/src/import/core/shared/api-routes.js +221 -0
  198. package/src/import/core/shared/candidates.js +97 -0
  199. package/src/import/core/shared/files.js +177 -0
  200. package/src/import/core/shared/next-app.js +389 -0
  201. package/src/import/core/shared/types.d.ts +51 -0
  202. package/src/import/core/shared/ui-routes.js +230 -0
  203. package/src/import/core/shared.js +67 -910
  204. package/src/import/extractors/api/flutter-dio.js +4 -8
  205. package/src/import/extractors/api/react-native-repository.js +4 -8
  206. package/src/import/index.d.ts +4 -0
  207. package/src/import/provenance.d.ts +4 -0
  208. package/src/new-project/constants.js +128 -0
  209. package/src/new-project/create.js +83 -0
  210. package/src/new-project/json.js +28 -0
  211. package/src/new-project/metadata.js +96 -0
  212. package/src/new-project/package-spec.js +161 -0
  213. package/src/new-project/project-files.js +348 -0
  214. package/src/new-project/template-policy.js +269 -0
  215. package/src/new-project/template-resolution.js +368 -0
  216. package/src/new-project/template-snapshots.js +430 -0
  217. package/src/new-project/template-updates.js +512 -0
  218. package/src/new-project/types.d.ts +83 -0
  219. package/src/new-project.js +6 -2188
  220. package/src/npm-safety.js +79 -0
  221. package/src/parser.d.ts +87 -0
  222. package/src/parser.js +118 -0
  223. package/src/path-helpers.d.ts +1 -0
  224. package/src/path-helpers.js +20 -0
  225. package/src/policy/review-boundaries.d.ts +15 -0
  226. package/src/project-config/index.js +564 -0
  227. package/src/project-config.js +19 -560
  228. package/src/reconcile/docs.d.ts +8 -0
  229. package/src/reconcile/journeys.d.ts +1 -0
  230. package/src/resolver/enrich/acceptance-criterion.js +2 -0
  231. package/src/resolver/enrich/bug.js +2 -0
  232. package/src/resolver/enrich/pitch.js +2 -0
  233. package/src/resolver/enrich/requirement.js +2 -0
  234. package/src/resolver/enrich/task.js +2 -0
  235. package/src/resolver/index.js +19 -2089
  236. package/src/resolver/normalize.js +384 -1
  237. package/src/resolver/plans.js +168 -0
  238. package/src/resolver/projections-api.js +494 -0
  239. package/src/resolver/projections-db.js +133 -0
  240. package/src/resolver/projections-ui.js +317 -0
  241. package/src/resolver/shapes.js +251 -0
  242. package/src/resolver/shared.js +278 -0
  243. package/src/resolver/widgets.js +132 -0
  244. package/src/resolver.d.ts +1 -0
  245. package/src/runtime-support.js +29 -0
  246. package/src/sdlc/adopt.d.ts +1 -0
  247. package/src/sdlc/check.d.ts +1 -0
  248. package/src/sdlc/explain.d.ts +1 -0
  249. package/src/sdlc/release.d.ts +1 -0
  250. package/src/sdlc/scaffold.d.ts +1 -0
  251. package/src/sdlc/transition.d.ts +1 -0
  252. package/src/template-trust/constants.js +62 -0
  253. package/src/template-trust/content.js +258 -0
  254. package/src/template-trust/diff.js +92 -0
  255. package/src/template-trust/policy.js +61 -0
  256. package/src/template-trust/record.js +90 -0
  257. package/src/template-trust/status.js +182 -0
  258. package/src/template-trust.js +24 -687
  259. package/src/text-helpers.d.ts +7 -0
  260. package/src/text-helpers.js +245 -0
  261. package/src/topogram-config.js +306 -0
  262. package/src/topogram-types.d.ts +69 -0
  263. package/src/validator/common.js +488 -0
  264. package/src/validator/data-model.js +237 -0
  265. package/src/validator/docs.js +167 -0
  266. package/src/validator/expressions.js +146 -1
  267. package/src/validator/index.d.ts +23 -0
  268. package/src/validator/index.js +32 -3585
  269. package/src/validator/kinds.d.ts +41 -0
  270. package/src/validator/kinds.js +2 -0
  271. package/src/validator/model-helpers.js +46 -0
  272. package/src/validator/per-kind/acceptance-criterion.js +5 -0
  273. package/src/validator/per-kind/bug.js +6 -0
  274. package/src/validator/per-kind/domain.js +15 -2
  275. package/src/validator/per-kind/pitch.js +7 -0
  276. package/src/validator/per-kind/requirement.js +5 -0
  277. package/src/validator/per-kind/task.js +7 -0
  278. package/src/validator/per-kind/widget.js +14 -0
  279. package/src/validator/projections/api-http-async.js +410 -0
  280. package/src/validator/projections/api-http-authz.js +88 -0
  281. package/src/validator/projections/api-http-core.js +205 -0
  282. package/src/validator/projections/api-http-policies.js +339 -0
  283. package/src/validator/projections/api-http-responses.js +233 -0
  284. package/src/validator/projections/api-http.js +44 -0
  285. package/src/validator/projections/db.js +353 -0
  286. package/src/validator/projections/generator-defaults.js +45 -0
  287. package/src/validator/projections/helpers.js +87 -0
  288. package/src/validator/projections/ui-helpers.js +214 -0
  289. package/src/validator/projections/ui-navigation.js +344 -0
  290. package/src/validator/projections/ui-structure.js +364 -0
  291. package/src/validator/projections/ui-widgets.js +493 -0
  292. package/src/validator/projections/ui.js +46 -0
  293. package/src/validator/registry.js +48 -1
  294. package/src/validator/utils.d.ts +20 -0
  295. package/src/validator/utils.js +115 -12
  296. package/src/validator.d.ts +2 -0
  297. package/src/widget-behavior.d.ts +1 -0
  298. package/src/workflows/adoption/index.js +26 -0
  299. package/src/workflows/docs-generate.js +262 -0
  300. package/src/workflows/docs-scan.js +703 -0
  301. package/src/workflows/docs.js +15 -0
  302. package/src/workflows/import-app/api/collect.js +221 -0
  303. package/src/workflows/import-app/api/openapi.js +257 -0
  304. package/src/workflows/import-app/api/routes.js +327 -0
  305. package/src/workflows/import-app/api/sources.js +22 -0
  306. package/src/workflows/import-app/api.js +4 -0
  307. package/src/workflows/import-app/db.js +538 -0
  308. package/src/workflows/import-app/index.js +30 -0
  309. package/src/workflows/import-app/shared.js +218 -0
  310. package/src/workflows/import-app/ui.js +443 -0
  311. package/src/workflows/import-app/workflow.js +159 -0
  312. package/src/workflows/reconcile/adoption-plan/build.js +208 -0
  313. package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
  314. package/src/workflows/reconcile/adoption-plan/outputs.js +143 -0
  315. package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
  316. package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
  317. package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
  318. package/src/workflows/reconcile/adoption-plan.js +32 -0
  319. package/src/workflows/reconcile/auth/closures.js +115 -0
  320. package/src/workflows/reconcile/auth/formatters.js +142 -0
  321. package/src/workflows/reconcile/auth/inference.js +330 -0
  322. package/src/workflows/reconcile/auth/roles.js +122 -0
  323. package/src/workflows/reconcile/auth.js +37 -0
  324. package/src/workflows/reconcile/bundle-core/index.js +600 -0
  325. package/src/workflows/reconcile/bundle-core.js +14 -0
  326. package/src/workflows/reconcile/bundle-shared.js +75 -0
  327. package/src/workflows/reconcile/candidate-model.js +477 -0
  328. package/src/workflows/reconcile/canonical-surface.js +264 -0
  329. package/src/workflows/reconcile/gap-report.js +333 -0
  330. package/src/workflows/reconcile/ids.js +6 -0
  331. package/src/workflows/reconcile/impacts/adoption-plan.js +192 -0
  332. package/src/workflows/reconcile/impacts/indexes.js +101 -0
  333. package/src/workflows/reconcile/impacts/patches.js +252 -0
  334. package/src/workflows/reconcile/impacts/reports.js +80 -0
  335. package/src/workflows/reconcile/impacts.js +16 -0
  336. package/src/workflows/reconcile/index.js +7 -0
  337. package/src/workflows/reconcile/renderers.js +461 -0
  338. package/src/workflows/reconcile/summary.js +90 -0
  339. package/src/workflows/reconcile/workflow.js +309 -0
  340. package/src/workflows/shared.js +189 -0
  341. package/src/workflows/types.d.ts +93 -0
  342. package/src/workflows.d.ts +1 -0
  343. package/src/workflows.js +10 -7652
  344. package/src/workspace-docs.d.ts +29 -0
@@ -1,910 +1,67 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
-
4
- export const DEFAULT_IGNORED_DIRS = new Set([
5
- ".git",
6
- ".next",
7
- ".turbo",
8
- ".yarn",
9
- "build",
10
- "coverage",
11
- "dist",
12
- "node_modules",
13
- "tmp"
14
- ]);
15
-
16
- export function ensureTrailingNewline(value) {
17
- return value.endsWith("\n") ? value : `${value}\n`;
18
- }
19
-
20
- export function slugify(value) {
21
- return String(value || "")
22
- .trim()
23
- .toLowerCase()
24
- .replace(/[^a-z0-9]+/g, "-")
25
- .replace(/^-+|-+$/g, "") || "untitled";
26
- }
27
-
28
- export function idHintify(value) {
29
- return String(value || "")
30
- .trim()
31
- .toLowerCase()
32
- .replace(/[^a-z0-9]+/g, "_")
33
- .replace(/^_+|_+$/g, "") || "untitled";
34
- }
35
-
36
- export function canonicalCandidateTerm(value) {
37
- const normalized = slugify(value);
38
- if (normalized.endsWith("ies")) {
39
- return `${normalized.slice(0, -3)}y`;
40
- }
41
- if (normalized === "status" || normalized === "stats") {
42
- return normalized;
43
- }
44
- if (normalized.endsWith("s") && !normalized.endsWith("ss") && !normalized.endsWith("us") && !normalized.endsWith("is")) {
45
- return normalized.slice(0, -1);
46
- }
47
- return normalized;
48
- }
49
-
50
- export function pluralizeCandidateTerm(value) {
51
- const normalized = String(value || "");
52
- if (!normalized) return "items";
53
- const parts = normalized.split("_");
54
- const last = parts.pop() || "item";
55
- let plural = last;
56
- if (last.endsWith("y") && !/[aeiou]y$/.test(last)) {
57
- plural = `${last.slice(0, -1)}ies`;
58
- } else if (/(s|x|z|ch|sh)$/.test(last)) {
59
- plural = `${last}es`;
60
- } else {
61
- plural = `${last}s`;
62
- }
63
- return [...parts, plural].join("_");
64
- }
65
-
66
- export function titleCase(value) {
67
- return String(value || "")
68
- .split(/[_\-\s]+/)
69
- .filter(Boolean)
70
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
71
- .join(" ");
72
- }
73
-
74
- export function readTextIfExists(filePath) {
75
- return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : null;
76
- }
77
-
78
- export function readJsonIfExists(filePath) {
79
- if (!fs.existsSync(filePath)) {
80
- return null;
81
- }
82
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
83
- }
84
-
85
- export function listFilesRecursive(rootDir, predicate = () => true, options = {}) {
86
- if (!fs.existsSync(rootDir)) {
87
- return [];
88
- }
89
- const ignoredDirs = options.ignoredDirs || DEFAULT_IGNORED_DIRS;
90
- const files = [];
91
- const walk = (currentDir) => {
92
- for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
93
- const childPath = path.join(currentDir, entry.name);
94
- if (entry.isDirectory()) {
95
- if (ignoredDirs.has(entry.name)) {
96
- continue;
97
- }
98
- walk(childPath);
99
- continue;
100
- }
101
- if (entry.isFile() && predicate(childPath)) {
102
- files.push(childPath);
103
- }
104
- }
105
- };
106
- walk(rootDir);
107
- return files.sort();
108
- }
109
-
110
- export function relativeTo(base, filePath) {
111
- return path.relative(base, filePath).replaceAll(path.sep, "/");
112
- }
113
-
114
- export function importSearchRoots(paths) {
115
- return [...new Set([paths.workspaceRoot, paths.topogramRoot].filter(Boolean))];
116
- }
117
-
118
- export function normalizeImportRelativePath(paths, filePath) {
119
- return relativeTo(paths.repoRoot, filePath);
120
- }
121
-
122
- export function canonicalSourceRank(paths, filePath, kind) {
123
- const relativePath = normalizeImportRelativePath(paths, filePath);
124
- const normalizedPath = relativePath.replaceAll(path.sep, "/");
125
- const penalties = [
126
- { pattern: /\/apps\/local-stack\//, weight: 80 },
127
- { pattern: /\/artifacts\/environment\//, weight: 60 },
128
- { pattern: /\/artifacts\/deploy\//, weight: 60 },
129
- { pattern: /\/artifacts\/compile-check\//, weight: 50 },
130
- { pattern: /\/artifacts\/db-lifecycle\//, weight: 50 },
131
- { pattern: /\/artifacts\/migrations\//, weight: 40 }
132
- ];
133
-
134
- let rank = 100;
135
- if (kind === "prisma") {
136
- if (/\/prisma\/schema\.prisma$/i.test(normalizedPath) && !normalizedPath.includes("/artifacts/")) {
137
- rank = 0;
138
- } else if (/\/apps\/backend\/prisma\/schema\.prisma$/i.test(normalizedPath)) {
139
- rank = 0;
140
- } else if (/\/artifacts\/prisma\/schema\.prisma$/i.test(normalizedPath)) {
141
- rank = 10;
142
- }
143
- } else if (kind === "sql") {
144
- if (/\/db\/schema\.sql$/i.test(normalizedPath) || /\/schema\.sql$/i.test(normalizedPath)) {
145
- rank = 0;
146
- } else if (/\/artifacts\/db\/.+\.sql$/i.test(normalizedPath)) {
147
- rank = 10;
148
- } else if (/migration/i.test(path.basename(normalizedPath))) {
149
- rank = 30;
150
- }
151
- } else if (kind === "openapi") {
152
- if (/\/artifacts\/openapi\/openapi\.(json|ya?ml)$/i.test(normalizedPath)) {
153
- rank = 0;
154
- } else if (/\/openapi\.(json|ya?ml)$/i.test(normalizedPath) || /\/swagger\.(json|ya?ml)$/i.test(normalizedPath)) {
155
- rank = 10;
156
- }
157
- }
158
-
159
- for (const penalty of penalties) {
160
- if (penalty.pattern.test(normalizedPath)) {
161
- rank += penalty.weight;
162
- }
163
- }
164
- return rank;
165
- }
166
-
167
- export function selectPreferredImportFiles(paths, files, kind) {
168
- if (files.length === 0) {
169
- return [];
170
- }
171
- const rankedFiles = files.map((filePath) => ({
172
- filePath,
173
- rank: canonicalSourceRank(paths, filePath, kind)
174
- }));
175
- const bestRank = Math.min(...rankedFiles.map((entry) => entry.rank));
176
- return rankedFiles
177
- .filter((entry) => entry.rank === bestRank)
178
- .map((entry) => entry.filePath)
179
- .sort();
180
- }
181
-
182
- export function findImportFiles(paths, predicate) {
183
- const files = new Set();
184
- for (const rootDir of importSearchRoots(paths)) {
185
- for (const filePath of listFilesRecursive(rootDir, predicate)) {
186
- if (
187
- filePath.includes(`${path.sep}candidates${path.sep}`) ||
188
- filePath.includes(`${path.sep}docs-generated${path.sep}`) ||
189
- filePath.includes(`${path.sep}topogram${path.sep}tests${path.sep}fixtures${path.sep}expected${path.sep}`)
190
- ) {
191
- continue;
192
- }
193
- files.add(filePath);
194
- }
195
- }
196
- return [...files].sort();
197
- }
198
-
199
- export function makeCandidateRecord({
200
- kind,
201
- idHint,
202
- label,
203
- confidence = "medium",
204
- sourceKind,
205
- sourceOfTruth = "imported",
206
- provenance,
207
- track = null,
208
- ...payload
209
- }) {
210
- const inferredTrack =
211
- track ||
212
- (["entity", "enum", "relation", "index"].includes(kind)
213
- ? "db"
214
- : kind === "capability"
215
- ? "api"
216
- : kind === "widget"
217
- ? "ui"
218
- : null);
219
- return {
220
- kind,
221
- id_hint: idHint,
222
- label,
223
- confidence,
224
- source_kind: sourceKind,
225
- source_of_truth: sourceOfTruth,
226
- provenance: Array.isArray(provenance) ? provenance : [provenance].filter(Boolean),
227
- track: inferredTrack,
228
- ...payload
229
- };
230
- }
231
-
232
- export function dedupeCandidateRecords(records, keyFn) {
233
- const seen = new Map();
234
- for (const record of records) {
235
- const key = keyFn(record);
236
- const recordProvenance = Array.isArray(record.provenance) ? record.provenance : [record.provenance].filter(Boolean);
237
- if (!seen.has(key)) {
238
- seen.set(key, { ...record, provenance: recordProvenance });
239
- continue;
240
- }
241
- const current = seen.get(key);
242
- const currentProvenance = Array.isArray(current.provenance) ? current.provenance : [current.provenance].filter(Boolean);
243
- current.provenance = [...new Set([...currentProvenance, ...recordProvenance])];
244
- }
245
- return [...seen.values()];
246
- }
247
-
248
- export function normalizePrismaType(typeName) {
249
- const normalized = String(typeName || "").toLowerCase();
250
- switch (normalized) {
251
- case "string": return "string";
252
- case "int": return "int";
253
- case "bigint": return "bigint";
254
- case "float": return "float";
255
- case "decimal": return "decimal";
256
- case "boolean":
257
- case "bool": return "boolean";
258
- case "datetime": return "datetime";
259
- case "bytes": return "bytes";
260
- case "json": return "json";
261
- default: return typeName;
262
- }
263
- }
264
-
265
- export function normalizeOpenApiPath(pathValue) {
266
- return String(pathValue || "")
267
- .replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "{$1}")
268
- .replace(/\/+$/, "") || "/";
269
- }
270
-
271
- export function normalizeEndpointPathForMatch(pathValue) {
272
- const normalizedPath = normalizeOpenApiPath(pathValue);
273
- const segments = normalizedPath
274
- .split("/")
275
- .filter(Boolean)
276
- .map((segment) => {
277
- if (/^\{[^}]+\}$/.test(segment)) {
278
- return "{}";
279
- }
280
- return segment
281
- .split("-")
282
- .map((part) => canonicalCandidateTerm(part))
283
- .join("-");
284
- });
285
- return `/${segments.join("/")}`.replace(/\/+$/, "") || "/";
286
- }
287
-
288
- export function nonParamEndpointSegments(pathValue) {
289
- return normalizeOpenApiPath(pathValue)
290
- .split("/")
291
- .filter(Boolean)
292
- .filter((segment) => segment !== "{}" && !/^\{[^}]+\}$/.test(segment))
293
- .map((segment) => canonicalCandidateTerm(segment));
294
- }
295
-
296
- function trimmedApiSegments(pathValue) {
297
- const segments = nonParamEndpointSegments(pathValue);
298
- if (segments[0] === "api" && segments.length > 1) {
299
- return segments.slice(1);
300
- }
301
- if (segments[0] === "admin" && segments.length > 1) {
302
- return segments.slice(1);
303
- }
304
- return segments;
305
- }
306
-
307
- export function inferApiEntityIdFromPath(pathValue, options = {}) {
308
- const tags = (options.tags || []).map((tag) => canonicalCandidateTerm(tag));
309
- const summary = String(options.summary || "").toLowerCase();
310
- const normalizedPath = normalizeOpenApiPath(pathValue);
311
- const segments = trimmedApiSegments(normalizedPath);
312
-
313
- if (/\/(login|signin|sign-in|signup|register)$/.test(normalizedPath) || tags.includes("authentication")) {
314
- return "entity_account";
315
- }
316
- if (normalizedPath === "/me" || segments.includes("profile") || /profile/.test(summary)) {
317
- return "entity_profile";
318
- }
319
- if (tags.includes("member") || tags.includes("members") || segments.includes("membership") || segments.includes("memberships") || segments.includes("member") || segments.includes("members")) {
320
- return "entity_workspace-membership";
321
- }
322
- if (segments.includes("audit-log") || segments.includes("audit-logs")) {
323
- return "entity_audit-log";
324
- }
325
- if (segments.includes("account") || segments.includes("accounts")) {
326
- return "entity_account";
327
- }
328
- if (segments.includes("workspace") || segments.includes("workspaces")) {
329
- return "entity_workspace";
330
- }
331
- const nestedResourceActions = new Set(["favorite", "follow", "feed", "stats", "role", "status", "login", "signin", "sign-in", "signup", "register", "search", "payment", "delivery"]);
332
- const lastSegment = segments[segments.length - 1];
333
- const resource = segments.length > 1 && lastSegment && !nestedResourceActions.has(lastSegment)
334
- ? lastSegment
335
- : (segments[0] || "item");
336
- return `entity_${canonicalCandidateTerm(resource)}`;
337
- }
338
-
339
- export function inferApiCapabilityIdFromOperation(operation) {
340
- const method = String(operation.method || "").toUpperCase();
341
- const pathValue = normalizeOpenApiPath(operation.path || "");
342
- const summary = String(operation.summary || "").toLowerCase();
343
- const tags = operation.tags || [];
344
- const segments = trimmedApiSegments(pathValue);
345
- const rawSegments = normalizeOpenApiPath(pathValue)
346
- .split("/")
347
- .filter(Boolean)
348
- .filter((segment) => !/^\{[^}]+\}$/.test(segment));
349
- const trimmedRawSegments = rawSegments[0] === "api" && rawSegments.length > 1
350
- ? rawSegments.slice(1)
351
- : rawSegments[0] === "admin" && rawSegments.length > 1
352
- ? rawSegments.slice(1)
353
- : rawSegments;
354
- const hasPathParams = /\{[^}]+\}/.test(pathValue);
355
- const entityStem = inferApiEntityIdFromPath(pathValue, { tags, summary }).replace(/^entity_/, "").replace(/-/g, "_");
356
- const last = segments[segments.length - 1] || entityStem;
357
- const rawLast = trimmedRawSegments[trimmedRawSegments.length - 1] || "";
358
-
359
- if (/(^|\/)(login|signin|sign-in)$/.test(pathValue) || /(sign in|login)/.test(summary)) {
360
- return `cap_sign_in_${entityStem}`;
361
- }
362
- if (/(^|\/)(signup|register)$/.test(pathValue) || /(sign up|signup|registration|register)/.test(summary)) {
363
- return `cap_register_${entityStem}`;
364
- }
365
- if (/(^|\/)search\/\{[^}]+\}$/.test(pathValue) || /(search)/.test(summary)) {
366
- return `cap_search_${pluralizeCandidateTerm(entityStem)}`;
367
- }
368
- if (pathValue === "/me") {
369
- return `cap_get_${entityStem}`;
370
- }
371
- if (method === "GET" && /\/feed$/.test(pathValue)) {
372
- return `cap_feed_${entityStem}`;
373
- }
374
- if (method === "POST" && /\/favorite$/.test(pathValue)) {
375
- return `cap_favorite_${entityStem}`;
376
- }
377
- if (method === "DELETE" && /\/favorite$/.test(pathValue)) {
378
- return `cap_unfavorite_${entityStem}`;
379
- }
380
- if (method === "POST" && /\/follow$/.test(pathValue)) {
381
- return `cap_follow_${entityStem}`;
382
- }
383
- if (method === "DELETE" && /\/follow$/.test(pathValue)) {
384
- return `cap_unfollow_${entityStem}`;
385
- }
386
- if (method === "POST" && /\/payment$/.test(pathValue)) {
387
- return `cap_pay_${entityStem}`;
388
- }
389
- if (method === "POST" && /\/delivery$/.test(pathValue)) {
390
- return `cap_delivery_${entityStem}`;
391
- }
392
-
393
- if ((method === "PATCH" || method === "PUT") && ["role", "status"].includes(last)) {
394
- return `cap_update_${entityStem}_${last}`;
395
- }
396
- if (method === "GET" && last === "stats") {
397
- return `cap_get_${entityStem}_stats`;
398
- }
399
-
400
- if (method === "GET" && segments.length <= 1 && !hasPathParams) {
401
- const singularPath = rawLast && canonicalCandidateTerm(rawLast) === rawLast;
402
- return singularPath ? `cap_get_${entityStem}` : `cap_list_${pluralizeCandidateTerm(entityStem)}`;
403
- }
404
- if (method === "GET" && segments.length <= 1 && hasPathParams) return `cap_get_${entityStem}`;
405
- if (method === "GET" && segments.length > 1 && !["role", "status", "stats"].includes(last)) {
406
- if (!/\{[^}]+\}$/.test(pathValue) && rawLast && canonicalCandidateTerm(rawLast) !== rawLast) {
407
- return `cap_list_${pluralizeCandidateTerm(entityStem)}`;
408
- }
409
- if (segments.includes("member") || segments.includes("membership") || segments.includes("memberships")) {
410
- return `cap_list_${pluralizeCandidateTerm(entityStem)}`;
411
- }
412
- return `cap_get_${entityStem}`;
413
- }
414
- if (method === "POST") return `cap_create_${entityStem}`;
415
- if (method === "PATCH" || method === "PUT") return `cap_update_${entityStem}`;
416
- if (method === "DELETE") return `cap_delete_${entityStem}`;
417
- return `candidate_${String(operation.method || "unknown").toLowerCase()}_${slugify(pathValue)}`;
418
- }
419
-
420
- export function routeSegments(routePath) {
421
- return String(routePath || "")
422
- .split("/")
423
- .filter(Boolean)
424
- .map((segment) => segment.replace(/^:/, ""));
425
- }
426
-
427
- export function screenKindForRoute(routePath) {
428
- const normalized = String(routePath || "");
429
- const segments = routeSegments(normalized);
430
- if (/\/new$/.test(normalized)) return "form";
431
- if (/\/:?[A-Za-z0-9_]+\/edit$/.test(normalized)) return "form";
432
- if (segments.length >= 2 && !/\/new$/.test(normalized) && !/\/edit$/.test(normalized)) return "detail";
433
- return "list";
434
- }
435
-
436
- export function screenIdForRoute(routePath) {
437
- const segments = routeSegments(routePath);
438
- const resource = canonicalCandidateTerm(segments[0] || "home");
439
- const kind = screenKindForRoute(routePath);
440
- if (kind === "form" && /\/new$/.test(routePath)) return `${resource}_create`;
441
- if (kind === "form" && /\/edit$/.test(routePath)) return `${resource}_edit`;
442
- if (kind === "detail") return `${resource}_detail`;
443
- return `${resource}_list`;
444
- }
445
-
446
- export function uiCapabilityHintsForRoute(routePath) {
447
- const segments = routeSegments(routePath);
448
- const resource = canonicalCandidateTerm(segments[0] || "item");
449
- const idSegment = segments[1] || null;
450
- if (/\/new$/.test(routePath)) {
451
- return { load: null, submit: `cap_create_${resource}`, primary_action: `cap_create_${resource}` };
452
- }
453
- if (/\/edit$/.test(routePath)) {
454
- return { load: `cap_get_${resource}`, submit: `cap_update_${resource}`, primary_action: `cap_update_${resource}` };
455
- }
456
- if (idSegment && !/new|edit/.test(idSegment)) {
457
- return { load: `cap_get_${resource}`, submit: null, primary_action: `cap_update_${resource}` };
458
- }
459
- return { load: `cap_list_${resource}s`, submit: null, primary_action: `cap_create_${resource}` };
460
- }
461
-
462
- export function entityIdForRoute(routePath) {
463
- const segments = routeSegments(routePath);
464
- return `entity_${canonicalCandidateTerm(segments[0] || "item")}`;
465
- }
466
-
467
- export function inferReactRoutes(rootDir) {
468
- const appPath = path.join(rootDir, "src", "App.tsx");
469
- const text = readTextIfExists(appPath);
470
- if (!text) return [];
471
- const routes = new Set();
472
- for (const match of text.matchAll(/path:\s*"([^"]+)"/g)) routes.add(match[1]);
473
- for (const match of text.matchAll(/path="([^"]+)"/g)) routes.add(match[1]);
474
- return [...routes].sort();
475
- }
476
-
477
- export function inferSvelteRoutes(rootDir) {
478
- const routesRoot = path.join(rootDir, "src", "routes");
479
- if (!fs.existsSync(routesRoot)) return [];
480
- const files = listFilesRecursive(routesRoot, (child) => child.endsWith("+page.svelte") || child.endsWith("+page.ts") || child.endsWith("+page.server.ts"));
481
- const routes = new Set();
482
- for (const filePath of files) {
483
- const relative = relativeTo(routesRoot, filePath)
484
- .replace(/(^|\/)\+page(\.server|)\.(svelte|ts)$/, "")
485
- .replace(/\[(.+?)\]/g, ":$1")
486
- .replace(/^$/, "/");
487
- routes.add(relative.startsWith("/") ? relative : `/${relative}`);
488
- }
489
- return [...routes].sort();
490
- }
491
-
492
- export function inferNavigationStructure(rootDir, options = {}) {
493
- const filePatterns = options.filePatterns || [/(^|\/)App\.(tsx|jsx)$/i, /(^|\/)\+layout\.svelte$/i];
494
- const files = listFilesRecursive(rootDir, (filePath) => filePatterns.some((pattern) => pattern.test(filePath)));
495
- const result = {
496
- hasHeader: false,
497
- hasSidebar: false,
498
- hasTopbar: false,
499
- hasBottomTabs: false,
500
- hasTabs: false,
501
- hasBreadcrumbs: false,
502
- hasCommandPalette: false,
503
- hasSegmentedControl: false,
504
- navLinks: []
505
- };
506
-
507
- for (const filePath of files) {
508
- const text = readTextIfExists(filePath) || "";
509
- if (!text) continue;
510
- if (/<header\b|class(Name)?=["'][^"']*(topbar|app-nav|navbar)/i.test(text)) {
511
- result.hasHeader = true;
512
- result.hasTopbar = true;
513
- }
514
- if (/<aside\b|class(Name)?=["'][^"']*(sidebar|side-nav|sidenav)/i.test(text)) {
515
- result.hasSidebar = true;
516
- }
517
- if (/\bbottom[-_ ]tabs\b|\bTabBar\b|\bBottomNavigation\b/i.test(text)) {
518
- result.hasBottomTabs = true;
519
- }
520
- if (/breadcrumb/i.test(text)) {
521
- result.hasBreadcrumbs = true;
522
- }
523
- if (/\bcommand palette\b|\bCommandPalette\b/i.test(text)) {
524
- result.hasCommandPalette = true;
525
- }
526
- if (/\bsegmented\b|\bSegmentedControl\b/i.test(text)) {
527
- result.hasSegmentedControl = true;
528
- }
529
- if (/<nav\b|role=["']tablist["']|class(Name)?=["'][^"']*\btabs?\b/i.test(text)) {
530
- result.hasTabs = result.hasTabs || /role=["']tablist["']|class(Name)?=["'][^"']*\btabs?\b/i.test(text);
531
- for (const match of text.matchAll(/(?:href|to)=["'`]([^"'`]+)["'`]/g)) {
532
- result.navLinks.push(match[1]);
533
- }
534
- }
535
- }
536
-
537
- result.navLinks = [...new Set(result.navLinks)].sort();
538
- return result;
539
- }
540
-
541
- export function shellKindFromNavigation(navigation) {
542
- if (!navigation) return null;
543
- if (navigation.hasBottomTabs) return "bottom_tabs";
544
- if (navigation.hasSidebar && navigation.hasHeader) return "split_view";
545
- if (navigation.hasSidebar) return "sidebar";
546
- if (navigation.hasTopbar || navigation.hasHeader) return "topbar";
547
- return null;
548
- }
549
-
550
- export function navigationPatternsFromStructure(navigation) {
551
- const patterns = new Set();
552
- if (!navigation) return [];
553
- if (navigation.hasTabs) patterns.add("tabs");
554
- if (navigation.hasBreadcrumbs) patterns.add("breadcrumbs");
555
- if (navigation.hasBottomTabs) patterns.add("bottom_tabs");
556
- if (navigation.hasRail) patterns.add("navigation_rail");
557
- if (navigation.hasCommandPalette) patterns.add("command_palette");
558
- if (navigation.hasSegmentedControl) patterns.add("segmented_control");
559
- if (navigation.hasSidebar && navigation.hasHeader) patterns.add("split_view");
560
- return [...patterns].sort();
561
- }
562
-
563
- export function detectUiPresentationFeatures(rootDir) {
564
- const files = listFilesRecursive(rootDir, (filePath) => /\.(tsx|ts|jsx|js|svelte|vue|html)$/i.test(filePath));
565
- const features = new Set();
566
-
567
- for (const filePath of files) {
568
- const text = readTextIfExists(filePath) || "";
569
- if (!text) continue;
570
- if (/\bDataGrid\b|\bag-grid\b|\bAGGrid\b|\bTanStackTable\b|\breact-data-grid\b|\bmui[-_ ]?datagrid\b/i.test(text)) {
571
- features.add("data_grid");
572
- }
573
- if (/<table\b|react-table/i.test(text)) features.add("table");
574
- if (/\b(card|cards|Card)\b/.test(text)) features.add("cards");
575
- if (/\bkanban|board\b/i.test(text)) features.add("board");
576
- if (/\bcalendar\b/i.test(text)) features.add("calendar");
577
- if (/\bgallery\b/i.test(text)) features.add("gallery");
578
- if (/\bmodal\b|Dialog|AlertDialog/i.test(text)) features.add("modal");
579
- if (/\bdrawer\b|Sheet/i.test(text)) features.add("drawer");
580
- if (/\bsheet\b|BottomSheet|ModalBottomSheet/i.test(text)) features.add("sheet");
581
- if (/\bBottomSheet\b|\bModalBottomSheet\b/i.test(text)) features.add("bottom_sheet");
582
- if (/\bFloatingActionButton\b|\bExtendedFloatingActionButton\b|\bfloating action button\b/i.test(text)) features.add("fab");
583
- if (/\bempty state\b|empty-state|No results|No items/i.test(text)) features.add("empty_state");
584
- if (/\berror state\b|Something went wrong|error/i.test(text)) features.add("error_state");
585
- if (/\bloading\b|skeleton|spinner/i.test(text)) features.add("loading_state");
586
- if (/\bbreadcrumb/i.test(text)) features.add("breadcrumbs");
587
- if (/\bactivity\b|\btimeline\b|\bcomment/i.test(text)) features.add("activity");
588
- if (/\bsettings\b|\bpreferences\b|\bbilling\b|\bsecurity\b/i.test(text)) features.add("settings");
589
- if (/\bonboarding\b|\bwizard\b|\bstepper\b/i.test(text)) features.add("wizard");
590
- if (/\bpull[-_ ]to[-_ ]refresh\b|SwipeRefresh|refreshable\b/i.test(text)) features.add("pull_to_refresh");
591
- if (/\bcommand palette\b|\bCommandPalette\b/i.test(text)) features.add("command_palette");
592
- if (/\binspector\b|\bDetailsPane\b|\bproperties pane\b/i.test(text)) features.add("inspector_pane");
593
- if (/\bWindowGroup\b|\bBrowserWindow\b|\bmulti[-_ ]window\b/i.test(text)) features.add("multi_window");
594
- }
595
-
596
- return [...features].sort();
597
- }
598
-
599
- export function inferNextAppRoutes(rootDir) {
600
- const appDir = path.join(rootDir, "app");
601
- if (!fs.existsSync(appDir)) return [];
602
- const routeFiles = listFilesRecursive(
603
- appDir,
604
- (child) => /\/page\.(tsx|ts|jsx|js|mdx)$/.test(child) || /\/route\.(tsx|ts|jsx|js)$/.test(child)
605
- );
606
- const routes = [];
607
- for (const filePath of routeFiles) {
608
- const relative = relativeTo(appDir, filePath);
609
- const isPage = /\/page\.(tsx|ts|jsx|js|mdx)$/.test(`/${relative}`) || /^page\.(tsx|ts|jsx|js|mdx)$/.test(relative);
610
- const normalizedPath = `/${relative}`
611
- .replace(/\/page\.(tsx|ts|jsx|js|mdx)$/, "")
612
- .replace(/\/route\.(tsx|ts|jsx|js)$/, "")
613
- .replace(/\[(\.\.\.)?([^\]]+)\]/g, (_m, catchAll, name) => catchAll ? `:${name}*` : `:${name}`)
614
- .replace(/\/index$/, "")
615
- .replace(/^\/$/, "/");
616
- routes.push({
617
- path: normalizedPath === "" ? "/" : normalizedPath,
618
- kind: isPage ? "page" : "route",
619
- file: filePath
620
- });
621
- }
622
- return routes.sort((a, b) => a.path.localeCompare(b.path) || a.kind.localeCompare(b.kind));
623
- }
624
-
625
- export function nextScreenKindForRoute(routePath) {
626
- const normalized = String(routePath || "");
627
- if (/\/(login|register|setup)$/.test(normalized)) return "flow";
628
- return screenKindForRoute(routePath);
629
- }
630
-
631
- export function nextScreenIdForRoute(routePath) {
632
- const normalized = String(routePath || "");
633
- if (normalized === "/") return "home";
634
- if (/\/login$/.test(normalized)) return "login";
635
- if (/\/register$/.test(normalized)) return "register";
636
- if (/\/setup$/.test(normalized)) return "setup";
637
- return screenIdForRoute(routePath);
638
- }
639
-
640
- export function entityIdForNextRoute(routePath) {
641
- const normalized = String(routePath || "");
642
- if (/^\/posts(\/|$)/.test(normalized)) return "entity_post";
643
- if (/^\/users(\/|$)/.test(normalized)) return "entity_user";
644
- return null;
645
- }
646
-
647
- export function conceptIdForNextRoute(routePath) {
648
- const normalized = String(routePath || "");
649
- if (normalized === "/") return "surface_home";
650
- if (/\/login$/.test(normalized)) return "flow_login";
651
- if (/\/register$/.test(normalized)) return "flow_register";
652
- if (/\/setup$/.test(normalized)) return "flow_setup";
653
- return entityIdForNextRoute(routePath) || entityIdForRoute(routePath);
654
- }
655
-
656
- export function uiCapabilityHintsForNextRoute(routePath) {
657
- const normalized = String(routePath || "");
658
- if (normalized === "/") return { load: null, submit: null, primary_action: null };
659
- if (/\/login$/.test(normalized)) return { load: null, submit: "cap_sign_in_user", primary_action: "cap_sign_in_user" };
660
- if (/\/register$/.test(normalized)) return { load: null, submit: "cap_register_user", primary_action: "cap_register_user" };
661
- if (/\/setup$/.test(normalized)) return { load: null, submit: null, primary_action: null };
662
- if (/^\/posts\/new$/.test(normalized)) return { load: null, submit: "cap_create_post", primary_action: "cap_create_post" };
663
- if (/^\/posts\/:id$/.test(normalized) || /^\/posts\/:[^/]+$/.test(normalized)) return { load: "cap_get_post", submit: null, primary_action: "cap_update_post" };
664
- if (/^\/posts$/.test(normalized)) return { load: "cap_list_posts", submit: null, primary_action: "cap_create_post" };
665
- if (/^\/users\/new$/.test(normalized)) return { load: null, submit: "cap_create_user", primary_action: "cap_create_user" };
666
- return uiCapabilityHintsForRoute(routePath);
667
- }
668
-
669
- export function inferRouteQueryParams(text) {
670
- const params = new Set();
671
- for (const match of String(text || "").matchAll(/\bquery\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g)) params.add(match[1]);
672
- for (const match of String(text || "").matchAll(/\bquery\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) params.add(match[1]);
673
- return [...params].sort();
674
- }
675
-
676
- export function inferRouteAuthHint(routeArguments, handlerContext) {
677
- const combined = `${routeArguments || ""}\n${handlerContext || ""}`.toLowerCase();
678
- if (/\b(signin|sign_in|login|register|credentialsprovider)\b/.test(combined)) {
679
- return "public";
680
- }
681
- return /\b(auth|session|permission|guard|protected|require_auth|requireauth|ensureauth)\b/.test(combined)
682
- ? "secured"
683
- : "unknown";
684
- }
685
-
686
- export function extractNamedExportBlock(text, exportName) {
687
- const escapedName = exportName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
688
- const match = text.match(new RegExp(`export\\s+async\\s+function\\s+${escapedName}\\s*\\([^)]*\\)\\s*\\{([\\s\\S]{0,2000}?)\\n\\}`, "m"));
689
- return match ? match[1] : "";
690
- }
691
-
692
- export function inferNextRequestSearchParams(text) {
693
- const params = new Set();
694
- for (const match of String(text || "").matchAll(/searchParams\.get\(\s*["'`]([^"'`]+)["'`]\s*\)/g)) {
695
- params.add(match[1]);
696
- }
697
- return [...params].sort();
698
- }
699
-
700
- export function inferNextJsonFields(text) {
701
- const fields = new Set();
702
- for (const match of String(text || "").matchAll(/NextResponse\.json\(\s*\{([\s\S]{0,400}?)\}\s*\)/g)) {
703
- for (const fieldMatch of match[1].matchAll(/\b([A-Za-z_][A-Za-z0-9_]*)\b\s*[:,]/g)) {
704
- fields.add(fieldMatch[1]);
705
- }
706
- }
707
- return [...fields].sort();
708
- }
709
-
710
- export function extractHandlerContext(text, handlerName) {
711
- if (!handlerName) return "";
712
- const escapedName = handlerName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
713
- const patterns = [
714
- new RegExp(`function\\s+${escapedName}\\s*\\([^)]*\\)\\s*\\{([\\s\\S]{0,1200}?)\\n\\}`, "m"),
715
- new RegExp(`const\\s+${escapedName}\\s*=\\s*(?:async\\s*)?\\([^)]*\\)\\s*=>\\s*\\{([\\s\\S]{0,1200}?)\\n\\}`, "m")
716
- ];
717
- for (const pattern of patterns) {
718
- const match = text.match(pattern);
719
- if (match) return match[1];
720
- }
721
- return "";
722
- }
723
-
724
- export function inferRouteCapabilityId(route) {
725
- if (route.handler_hint) {
726
- const genericHttpHandler = /^(get|post|put|patch|delete)$/i.test(route.handler_hint);
727
- if (!genericHttpHandler) {
728
- const normalizedHandler = route.handler_hint
729
- .replace(/^(handle|on)/i, "")
730
- .replace(/(handler|route|controller|action)$/i, "");
731
- const handlerTokens = normalizedHandler
732
- .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
733
- .split(/[^A-Za-z0-9]+/)
734
- .filter(Boolean)
735
- .map((token) => token.toLowerCase());
736
- if (handlerTokens.length > 0) {
737
- return `cap_${handlerTokens.join("_")}`;
738
- }
739
- }
740
- }
741
- return inferApiCapabilityIdFromOperation(route);
742
- }
743
-
744
- export function nextAppRoutePathFromFile(appRoot, filePath) {
745
- const relative = relativeTo(appRoot, filePath);
746
- return `/${relative}`
747
- .replace(/\/actions\.(tsx|ts|jsx|js)$/, "")
748
- .replace(/\/page\.(tsx|ts|jsx|js|mdx)$/, "")
749
- .replace(/\[(\.\.\.)?([^\]]+)\]/g, (_match, catchAll, name) => catchAll ? `:${name}*` : `:${name}`)
750
- .replace(/\/index$/, "")
751
- .replace(/^\/$/, "/") || "/";
752
- }
753
-
754
- export function inferFormDataFields(text) {
755
- const fields = new Set();
756
- for (const match of String(text || "").matchAll(/formData\.get\(\s*["'`]([^"'`]+)["'`]\s*\)/g)) {
757
- fields.add(match[1]);
758
- }
759
- return [...fields].sort();
760
- }
761
-
762
- export function inferInputNames(text) {
763
- const fields = new Set();
764
- for (const match of String(text || "").matchAll(/\bname=["'`]([^"'`]+)["'`]/g)) {
765
- fields.add(match[1]);
766
- }
767
- return [...fields].sort();
768
- }
769
-
770
- export function inferNextApiRoutes(workspaceRoot, helpers = { readTextIfExists }) {
771
- const apiRoot = path.join(workspaceRoot, "app", "api");
772
- if (!fs.existsSync(apiRoot)) return [];
773
- const routeFiles = listFilesRecursive(
774
- apiRoot,
775
- (child) => /\/route\.(tsx|ts|jsx|js)$/.test(child) || /^route\.(tsx|ts|jsx|js)$/.test(path.basename(child))
776
- );
777
- const routes = [];
778
- for (const filePath of routeFiles) {
779
- const text = helpers.readTextIfExists(filePath) || "";
780
- const relative = relativeTo(apiRoot, filePath);
781
- const routePath = `/${relative}`
782
- .replace(/\/route\.(tsx|ts|jsx|js)$/, "")
783
- .replace(/\[(\.\.\.)?([^\]]+)\]/g, (_match, catchAll, name) => catchAll ? `:${name}*` : `:${name}`);
784
- for (const match of text.matchAll(/export\s+async\s+function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(([^)]*)\)/g)) {
785
- const method = match[1].toUpperCase();
786
- const handlerContext = extractNamedExportBlock(text, match[1]) || "";
787
- routes.push({
788
- file: filePath,
789
- method,
790
- path: routePath === "" ? "/" : routePath,
791
- handler_hint: match[1].toLowerCase(),
792
- path_params: [...normalizeOpenApiPath(routePath).matchAll(/\{([^}]+)\}/g)].map((entry) => entry[1]),
793
- query_params: inferNextRequestSearchParams(handlerContext),
794
- output_fields: inferNextJsonFields(handlerContext),
795
- auth_hint: inferRouteAuthHint(match[0], handlerContext),
796
- source_kind: "route_code"
797
- });
798
- }
799
- }
800
- return routes;
801
- }
802
-
803
- export function inferCapabilityEntityId(record) {
804
- if (record.entity_id) {
805
- return record.entity_id;
806
- }
807
- const pathSegments = normalizeEndpointPathForMatch(record.endpoint?.path || "")
808
- .split("/")
809
- .filter(Boolean)
810
- .filter((segment) => segment !== "{}");
811
- const resourceSegment = pathSegments[0] || String(record.id_hint || "").replace(/^cap_(create|update|delete|get|list)_/, "");
812
- return `entity_${idHintify(canonicalCandidateTerm(resourceSegment))}`;
813
- }
814
-
815
- export function inferNextServerActionCapabilities(workspaceRoot, helpers = { readTextIfExists }) {
816
- const appRoot = path.join(workspaceRoot, "app");
817
- if (!fs.existsSync(appRoot)) return [];
818
- const actionFiles = listFilesRecursive(
819
- appRoot,
820
- (child) =>
821
- /\/actions\.(tsx|ts|jsx|js)$/.test(child) ||
822
- /\/page\.(tsx|ts|jsx|js|mdx)$/.test(child) ||
823
- /^page\.(tsx|ts|jsx|js|mdx)$/.test(path.basename(child))
824
- );
825
- const capabilities = [];
826
- for (const filePath of actionFiles) {
827
- const text = helpers.readTextIfExists(filePath) || "";
828
- const routePath = nextAppRoutePathFromFile(appRoot, filePath);
829
- 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)) {
830
- const functionName = match[1];
831
- const body = match[3] || "";
832
- const trimmedBody = body.trimStart();
833
- const isServerAction =
834
- /\/actions\.(tsx|ts|jsx|js)$/.test(filePath) ||
835
- trimmedBody.startsWith('"use server"') ||
836
- trimmedBody.startsWith("'use server'");
837
- if (!isServerAction) continue;
838
- const routeLike = {
839
- file: filePath,
840
- method: "POST",
841
- path: routePath,
842
- handler_hint: functionName,
843
- auth_hint: inferRouteAuthHint(functionName, body)
844
- };
845
- const idHint = inferRouteCapabilityId(routeLike);
846
- capabilities.push({
847
- file: filePath,
848
- function_name: functionName,
849
- method: "POST",
850
- path: routePath,
851
- id_hint: idHint,
852
- input_fields: inferFormDataFields(body),
853
- output_fields: [],
854
- path_params: [...normalizeOpenApiPath(routePath).matchAll(/\{([^}]+)\}/g)].map((entry) => entry[1]),
855
- auth_hint: routeLike.auth_hint,
856
- entity_id: inferCapabilityEntityId({ endpoint: { path: routePath }, id_hint: idHint }),
857
- source_kind: "route_code"
858
- });
859
- }
860
- }
861
- return capabilities.sort((a, b) => a.id_hint.localeCompare(b.id_hint) || a.path.localeCompare(b.path));
862
- }
863
-
864
- export function inferNextAuthCapabilities(paths, helpers = { readTextIfExists }) {
865
- const authConfigPath = path.join(paths.workspaceRoot, "auth.ts");
866
- const authConfigText = helpers.readTextIfExists(authConfigPath) || "";
867
- const hasCredentialsProvider = /CredentialsProvider\s*\(/.test(authConfigText);
868
- const createsUserOnAuthorize = /prisma\.user\.create\s*\(/.test(authConfigText);
869
- const pages = [
870
- {
871
- file: path.join(paths.workspaceRoot, "app", "login", "page.tsx"),
872
- path: "/login",
873
- id_hint: "cap_sign_in_user",
874
- label: "Sign In User",
875
- target_state: "authenticated"
876
- },
877
- {
878
- file: path.join(paths.workspaceRoot, "app", "register", "page.tsx"),
879
- path: "/register",
880
- id_hint: "cap_register_user",
881
- label: "Register User",
882
- target_state: createsUserOnAuthorize ? "registered" : "created"
883
- }
884
- ];
885
- const capabilities = [];
886
- for (const page of pages) {
887
- const text = helpers.readTextIfExists(page.file) || "";
888
- if (!text || !/signIn\(\s*["'`]credentials["'`]/.test(text)) continue;
889
- capabilities.push({
890
- file: page.file,
891
- function_name: page.id_hint.replace(/^cap_/, ""),
892
- method: "POST",
893
- path: page.path,
894
- id_hint: page.id_hint,
895
- label: page.label,
896
- input_fields: inferInputNames(text),
897
- output_fields: [],
898
- path_params: [],
899
- auth_hint: "public",
900
- entity_id: "entity_user",
901
- target_state: page.target_state,
902
- provenance: [
903
- relativeTo(paths.repoRoot, page.file),
904
- ...(hasCredentialsProvider ? [relativeTo(paths.repoRoot, authConfigPath)] : [])
905
- ],
906
- source_kind: "route_code"
907
- });
908
- }
909
- return capabilities.sort((a, b) => a.id_hint.localeCompare(b.id_hint));
910
- }
1
+ // @ts-check
2
+
3
+ export { relativeTo } from "../../path-helpers.js";
4
+ export {
5
+ canonicalCandidateTerm,
6
+ ensureTrailingNewline,
7
+ idHintify,
8
+ pluralizeCandidateTerm,
9
+ slugify,
10
+ titleCase,
11
+ makeCandidateRecord,
12
+ dedupeCandidateRecords,
13
+ normalizePrismaType
14
+ } from "./shared/candidates.js";
15
+ export {
16
+ DEFAULT_IGNORED_DIRS,
17
+ readTextIfExists,
18
+ readJsonIfExists,
19
+ listFilesRecursive,
20
+ importSearchRoots,
21
+ normalizeImportRelativePath,
22
+ canonicalSourceRank,
23
+ selectPreferredImportFiles,
24
+ findImportFiles
25
+ } from "./shared/files.js";
26
+ export {
27
+ normalizeOpenApiPath,
28
+ normalizeEndpointPathForMatch,
29
+ nonParamEndpointSegments,
30
+ inferApiEntityIdFromPath,
31
+ inferApiCapabilityIdFromOperation,
32
+ inferRouteCapabilityId,
33
+ inferCapabilityEntityId
34
+ } from "./shared/api-routes.js";
35
+ export {
36
+ routeSegments,
37
+ screenKindForRoute,
38
+ screenIdForRoute,
39
+ uiCapabilityHintsForRoute,
40
+ entityIdForRoute,
41
+ inferReactRoutes,
42
+ inferSvelteRoutes,
43
+ inferNavigationStructure,
44
+ shellKindFromNavigation,
45
+ navigationPatternsFromStructure,
46
+ detectUiPresentationFeatures
47
+ } from "./shared/ui-routes.js";
48
+ export {
49
+ inferNextAppRoutes,
50
+ nextScreenKindForRoute,
51
+ nextScreenIdForRoute,
52
+ entityIdForNextRoute,
53
+ conceptIdForNextRoute,
54
+ uiCapabilityHintsForNextRoute,
55
+ inferRouteQueryParams,
56
+ inferRouteAuthHint,
57
+ extractNamedExportBlock,
58
+ inferNextRequestSearchParams,
59
+ inferNextJsonFields,
60
+ extractHandlerContext,
61
+ nextAppRoutePathFromFile,
62
+ inferFormDataFields,
63
+ inferInputNames,
64
+ inferNextApiRoutes,
65
+ inferNextServerActionCapabilities,
66
+ inferNextAuthCapabilities
67
+ } from "./shared/next-app.js";