@topogram/cli 0.3.64 → 0.3.66

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