@topogram/cli 0.3.34

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 (257) hide show
  1. package/ARCHITECTURE.md +67 -0
  2. package/CHANGELOG.md +240 -0
  3. package/README.md +223 -0
  4. package/package.json +51 -0
  5. package/src/adoption/index.js +3 -0
  6. package/src/adoption/plan.js +702 -0
  7. package/src/adoption/reporting.js +464 -0
  8. package/src/adoption/review-groups.js +313 -0
  9. package/src/agent-ops/query-builders.js +5012 -0
  10. package/src/archive/archive.js +141 -0
  11. package/src/archive/compact.js +26 -0
  12. package/src/archive/jsonl.js +70 -0
  13. package/src/archive/resolver-bridge.js +82 -0
  14. package/src/archive/schema.js +87 -0
  15. package/src/archive/unarchive.js +108 -0
  16. package/src/catalog.js +752 -0
  17. package/src/cli/catalog-alias.js +166 -0
  18. package/src/cli.js +9738 -0
  19. package/src/component-behavior.js +173 -0
  20. package/src/example-implementation.js +91 -0
  21. package/src/format.js +19 -0
  22. package/src/generator/adapters.d.ts +4 -0
  23. package/src/generator/adapters.js +325 -0
  24. package/src/generator/api.d.ts +1 -0
  25. package/src/generator/api.js +1196 -0
  26. package/src/generator/check.js +355 -0
  27. package/src/generator/component-conformance.js +767 -0
  28. package/src/generator/components.js +39 -0
  29. package/src/generator/context/bundle.js +291 -0
  30. package/src/generator/context/diff.js +256 -0
  31. package/src/generator/context/digest.js +182 -0
  32. package/src/generator/context/domain-coverage.js +94 -0
  33. package/src/generator/context/domain-page.js +137 -0
  34. package/src/generator/context/index.js +42 -0
  35. package/src/generator/context/report.js +121 -0
  36. package/src/generator/context/shared.js +1397 -0
  37. package/src/generator/context/slice.js +703 -0
  38. package/src/generator/context/task-mode.js +466 -0
  39. package/src/generator/docs.js +327 -0
  40. package/src/generator/index.js +161 -0
  41. package/src/generator/native/parity-bundle.js +311 -0
  42. package/src/generator/output.js +300 -0
  43. package/src/generator/registry.js +482 -0
  44. package/src/generator/runtime/app-bundle.js +456 -0
  45. package/src/generator/runtime/bundle-shared.js +166 -0
  46. package/src/generator/runtime/compile-check.js +163 -0
  47. package/src/generator/runtime/deployment.js +287 -0
  48. package/src/generator/runtime/environment.js +635 -0
  49. package/src/generator/runtime/index.js +32 -0
  50. package/src/generator/runtime/runtime-check.js +554 -0
  51. package/src/generator/runtime/shared.js +515 -0
  52. package/src/generator/runtime/smoke.js +219 -0
  53. package/src/generator/schema.js +204 -0
  54. package/src/generator/sdlc/board.js +66 -0
  55. package/src/generator/sdlc/doc-page.js +53 -0
  56. package/src/generator/sdlc/index.js +23 -0
  57. package/src/generator/sdlc/release-notes.js +62 -0
  58. package/src/generator/sdlc/traceability-matrix.js +65 -0
  59. package/src/generator/shared.js +29 -0
  60. package/src/generator/surfaces/contracts.js +146 -0
  61. package/src/generator/surfaces/databases/contract.js +40 -0
  62. package/src/generator/surfaces/databases/index.js +84 -0
  63. package/src/generator/surfaces/databases/lifecycle-shared.d.ts +1 -0
  64. package/src/generator/surfaces/databases/lifecycle-shared.js +612 -0
  65. package/src/generator/surfaces/databases/migration-plan.js +281 -0
  66. package/src/generator/surfaces/databases/postgres/capabilities.js +14 -0
  67. package/src/generator/surfaces/databases/postgres/drizzle.js +99 -0
  68. package/src/generator/surfaces/databases/postgres/index.js +9 -0
  69. package/src/generator/surfaces/databases/postgres/lifecycle.js +16 -0
  70. package/src/generator/surfaces/databases/postgres/prisma.js +159 -0
  71. package/src/generator/surfaces/databases/postgres/sql-migration.js +102 -0
  72. package/src/generator/surfaces/databases/postgres/sql-schema.js +34 -0
  73. package/src/generator/surfaces/databases/shared.d.ts +1 -0
  74. package/src/generator/surfaces/databases/shared.js +350 -0
  75. package/src/generator/surfaces/databases/snapshot.js +96 -0
  76. package/src/generator/surfaces/databases/sqlite/capabilities.js +14 -0
  77. package/src/generator/surfaces/databases/sqlite/index.js +8 -0
  78. package/src/generator/surfaces/databases/sqlite/lifecycle.js +16 -0
  79. package/src/generator/surfaces/databases/sqlite/prisma.js +143 -0
  80. package/src/generator/surfaces/databases/sqlite/sql-migration.js +65 -0
  81. package/src/generator/surfaces/databases/sqlite/sql-schema.js +27 -0
  82. package/src/generator/surfaces/index.js +25 -0
  83. package/src/generator/surfaces/native/swiftui-app.js +38 -0
  84. package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +20 -0
  85. package/src/generator/surfaces/native/swiftui-templates/README.generated.md +26 -0
  86. package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +682 -0
  87. package/src/generator/surfaces/native/swiftui-templates/runtime/TodoAPIClient.swift +156 -0
  88. package/src/generator/surfaces/native/swiftui-templates/runtime/TodoSwiftUIApp.swift +44 -0
  89. package/src/generator/surfaces/native/swiftui-templates/runtime/Visibility.swift +183 -0
  90. package/src/generator/surfaces/services/express.d.ts +1 -0
  91. package/src/generator/surfaces/services/express.js +766 -0
  92. package/src/generator/surfaces/services/hono.d.ts +1 -0
  93. package/src/generator/surfaces/services/hono.js +204 -0
  94. package/src/generator/surfaces/services/index.js +42 -0
  95. package/src/generator/surfaces/services/persistence-wiring.js +240 -0
  96. package/src/generator/surfaces/services/runtime-helpers.js +631 -0
  97. package/src/generator/surfaces/services/server-contract.js +80 -0
  98. package/src/generator/surfaces/services/stateless.d.ts +1 -0
  99. package/src/generator/surfaces/services/stateless.js +97 -0
  100. package/src/generator/surfaces/shared.js +64 -0
  101. package/src/generator/surfaces/web/api-client.js +1 -0
  102. package/src/generator/surfaces/web/forms.js +1 -0
  103. package/src/generator/surfaces/web/index.d.ts +2 -0
  104. package/src/generator/surfaces/web/index.js +53 -0
  105. package/src/generator/surfaces/web/react-components.js +248 -0
  106. package/src/generator/surfaces/web/react.js +538 -0
  107. package/src/generator/surfaces/web/routes.js +1 -0
  108. package/src/generator/surfaces/web/screens.js +1 -0
  109. package/src/generator/surfaces/web/shared.js +369 -0
  110. package/src/generator/surfaces/web/sveltekit-actions.js +28 -0
  111. package/src/generator/surfaces/web/sveltekit-components.js +234 -0
  112. package/src/generator/surfaces/web/sveltekit.js +426 -0
  113. package/src/generator/surfaces/web/ui-web-contract.js +65 -0
  114. package/src/generator/surfaces/web/vanilla.js +239 -0
  115. package/src/generator/verification.js +84 -0
  116. package/src/generator.js +1 -0
  117. package/src/import/core/context.js +52 -0
  118. package/src/import/core/contracts.js +23 -0
  119. package/src/import/core/registry.js +81 -0
  120. package/src/import/core/runner.js +646 -0
  121. package/src/import/core/shared.js +910 -0
  122. package/src/import/enrichers/auth-session.js +18 -0
  123. package/src/import/enrichers/django-rest.js +226 -0
  124. package/src/import/enrichers/doc-linking.js +20 -0
  125. package/src/import/enrichers/rails-controllers.js +246 -0
  126. package/src/import/enrichers/rails-models.js +130 -0
  127. package/src/import/enrichers/workflow-target-state.js +10 -0
  128. package/src/import/extractors/api/aspnet-core.js +304 -0
  129. package/src/import/extractors/api/django-routes.js +318 -0
  130. package/src/import/extractors/api/express.js +154 -0
  131. package/src/import/extractors/api/fastify.js +371 -0
  132. package/src/import/extractors/api/flutter-dio.js +135 -0
  133. package/src/import/extractors/api/generic-route-fallback.js +90 -0
  134. package/src/import/extractors/api/graphql-code-first.js +565 -0
  135. package/src/import/extractors/api/graphql-sdl.js +309 -0
  136. package/src/import/extractors/api/jaxrs.js +303 -0
  137. package/src/import/extractors/api/micronaut.js +213 -0
  138. package/src/import/extractors/api/next-route.js +50 -0
  139. package/src/import/extractors/api/next-server-action.js +51 -0
  140. package/src/import/extractors/api/nextauth.js +52 -0
  141. package/src/import/extractors/api/openapi-code.js +242 -0
  142. package/src/import/extractors/api/openapi.js +232 -0
  143. package/src/import/extractors/api/rails-routes.js +230 -0
  144. package/src/import/extractors/api/react-native-repository.js +128 -0
  145. package/src/import/extractors/api/retrofit.js +103 -0
  146. package/src/import/extractors/api/spring-web.js +372 -0
  147. package/src/import/extractors/api/swift-webapi.js +116 -0
  148. package/src/import/extractors/api/trpc.js +212 -0
  149. package/src/import/extractors/db/django-models.js +232 -0
  150. package/src/import/extractors/db/dotnet-models.js +93 -0
  151. package/src/import/extractors/db/drizzle.js +242 -0
  152. package/src/import/extractors/db/ef-core.js +221 -0
  153. package/src/import/extractors/db/flutter-entities.js +120 -0
  154. package/src/import/extractors/db/jpa.js +120 -0
  155. package/src/import/extractors/db/liquibase.js +180 -0
  156. package/src/import/extractors/db/mybatis-xml.js +145 -0
  157. package/src/import/extractors/db/prisma.js +185 -0
  158. package/src/import/extractors/db/rails-schema.js +175 -0
  159. package/src/import/extractors/db/react-native-entities.js +95 -0
  160. package/src/import/extractors/db/room.js +193 -0
  161. package/src/import/extractors/db/snapshot.js +112 -0
  162. package/src/import/extractors/db/sql.js +180 -0
  163. package/src/import/extractors/db/swiftdata.js +137 -0
  164. package/src/import/extractors/ui/android-compose.js +230 -0
  165. package/src/import/extractors/ui/backend-only.js +70 -0
  166. package/src/import/extractors/ui/blazor.js +227 -0
  167. package/src/import/extractors/ui/flutter-screens.js +152 -0
  168. package/src/import/extractors/ui/maui-xaml.js +135 -0
  169. package/src/import/extractors/ui/next-app-router.js +83 -0
  170. package/src/import/extractors/ui/next-pages-router.js +141 -0
  171. package/src/import/extractors/ui/razor-pages.js +181 -0
  172. package/src/import/extractors/ui/react-native-screens.js +166 -0
  173. package/src/import/extractors/ui/react-router.js +139 -0
  174. package/src/import/extractors/ui/sveltekit.js +123 -0
  175. package/src/import/extractors/ui/swiftui.js +193 -0
  176. package/src/import/extractors/ui/uikit.js +175 -0
  177. package/src/import/extractors/verification/generic.js +290 -0
  178. package/src/import/extractors/workflows/generic.js +137 -0
  179. package/src/import/index.js +7 -0
  180. package/src/import/provenance.js +158 -0
  181. package/src/new-project.js +2107 -0
  182. package/src/parser.js +439 -0
  183. package/src/policy/review-boundaries.js +165 -0
  184. package/src/project-config.js +535 -0
  185. package/src/proofs/backend-parity.js +19 -0
  186. package/src/proofs/contract-audit.js +220 -0
  187. package/src/proofs/ios-parity.js +7 -0
  188. package/src/proofs/issues-parity.js +10 -0
  189. package/src/proofs/web-parity.js +50 -0
  190. package/src/realization/api/build-api-realization.js +5 -0
  191. package/src/realization/api/index.js +1 -0
  192. package/src/realization/backend/build-backend-runtime-realization.js +82 -0
  193. package/src/realization/backend/index.d.ts +1 -0
  194. package/src/realization/backend/index.js +4 -0
  195. package/src/realization/db/build-db-realization.js +17 -0
  196. package/src/realization/db/index.js +3 -0
  197. package/src/realization/db/migration-plan.js +5 -0
  198. package/src/realization/db/snapshot.js +5 -0
  199. package/src/realization/ui/build-ui-shared-realization.js +305 -0
  200. package/src/realization/ui/build-web-realization.js +189 -0
  201. package/src/realization/ui/index.js +2 -0
  202. package/src/reconcile/docs.js +280 -0
  203. package/src/reconcile/index.js +3 -0
  204. package/src/reconcile/journeys.js +441 -0
  205. package/src/resolver/docs.js +1 -0
  206. package/src/resolver/enrich/acceptance-criterion.js +14 -0
  207. package/src/resolver/enrich/bug.js +12 -0
  208. package/src/resolver/enrich/component.js +2 -0
  209. package/src/resolver/enrich/index.js +1 -0
  210. package/src/resolver/enrich/pitch.js +18 -0
  211. package/src/resolver/enrich/requirement.js +20 -0
  212. package/src/resolver/enrich/task.js +16 -0
  213. package/src/resolver/expressions.js +1 -0
  214. package/src/resolver/index.js +2422 -0
  215. package/src/resolver/normalize.js +1 -0
  216. package/src/resolver.js +1 -0
  217. package/src/sdlc/adopt.js +65 -0
  218. package/src/sdlc/check.js +86 -0
  219. package/src/sdlc/dod/acceptance-criterion.js +22 -0
  220. package/src/sdlc/dod/bug.js +26 -0
  221. package/src/sdlc/dod/document.js +23 -0
  222. package/src/sdlc/dod/index.js +25 -0
  223. package/src/sdlc/dod/pitch.js +23 -0
  224. package/src/sdlc/dod/requirement.js +34 -0
  225. package/src/sdlc/dod/task.js +39 -0
  226. package/src/sdlc/explain.js +116 -0
  227. package/src/sdlc/history.js +80 -0
  228. package/src/sdlc/paths.js +11 -0
  229. package/src/sdlc/release.js +106 -0
  230. package/src/sdlc/scaffold.js +89 -0
  231. package/src/sdlc/status-filter.js +54 -0
  232. package/src/sdlc/transition.js +112 -0
  233. package/src/sdlc/transitions/acceptance-criterion.js +28 -0
  234. package/src/sdlc/transitions/bug.js +31 -0
  235. package/src/sdlc/transitions/document.js +29 -0
  236. package/src/sdlc/transitions/index.js +56 -0
  237. package/src/sdlc/transitions/pitch.js +34 -0
  238. package/src/sdlc/transitions/requirement.js +31 -0
  239. package/src/sdlc/transitions/task.js +34 -0
  240. package/src/template-trust.js +597 -0
  241. package/src/validator/expressions.js +1 -0
  242. package/src/validator/index.js +3424 -0
  243. package/src/validator/kinds.js +346 -0
  244. package/src/validator/per-kind/acceptance-criterion.js +91 -0
  245. package/src/validator/per-kind/bug.js +77 -0
  246. package/src/validator/per-kind/component.js +274 -0
  247. package/src/validator/per-kind/domain.js +205 -0
  248. package/src/validator/per-kind/pitch.js +101 -0
  249. package/src/validator/per-kind/requirement.js +75 -0
  250. package/src/validator/per-kind/task.js +96 -0
  251. package/src/validator/registry.js +1 -0
  252. package/src/validator/utils.js +12 -0
  253. package/src/validator.js +1 -0
  254. package/src/workflows.js +7597 -0
  255. package/src/workspace-docs.js +265 -0
  256. package/template-helpers/react.js +5 -0
  257. package/template-helpers/sveltekit.js +5 -0
@@ -0,0 +1,154 @@
1
+ import path from "node:path";
2
+
3
+ import {
4
+ dedupeCandidateRecords,
5
+ findImportFiles,
6
+ inferApiEntityIdFromPath,
7
+ inferRouteAuthHint,
8
+ inferRouteCapabilityId,
9
+ inferRouteQueryParams,
10
+ makeCandidateRecord,
11
+ normalizeOpenApiPath,
12
+ relativeTo,
13
+ titleCase
14
+ } from "../../core/shared.js";
15
+
16
+ function parseApiRoutesMap(text) {
17
+ const match = text.match(/export\s+const\s+API_ROUTES\s*=\s*\{([\s\S]*?)\}\s*as\s+const/);
18
+ if (!match) return {};
19
+ const entries = {};
20
+ for (const entry of match[1].split(/\r?\n/)) {
21
+ const line = entry.trim().replace(/,$/, "");
22
+ const routeMatch = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*["'`]([^"'`]+)["'`]$/);
23
+ if (routeMatch) {
24
+ entries[routeMatch[1]] = routeMatch[2];
25
+ }
26
+ }
27
+ return entries;
28
+ }
29
+
30
+ function parsePermissionsMetadata(text) {
31
+ const entries = new Map();
32
+ for (const match of text.matchAll(/permissions\.set\(\s*API_ROUTES\.([A-Za-z_][A-Za-z0-9_]*)\s*,\s*\{([\s\S]*?)\}\s*\)/g)) {
33
+ const routeKey = match[1];
34
+ const body = match[2];
35
+ entries.set(routeKey, {
36
+ authenticated: /authenticated:\s*true/.test(body),
37
+ super: /super:\s*true/.test(body)
38
+ });
39
+ }
40
+ return entries;
41
+ }
42
+
43
+ function extractHandlerContext(text, handlerName) {
44
+ if (!handlerName) return "";
45
+ const escaped = handlerName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
46
+ for (const pattern of [
47
+ new RegExp(`function\\s+${escaped}\\s*\\([^)]*\\)\\s*\\{([\\s\\S]{0,1200}?)\\n\\}`, "m"),
48
+ new RegExp(`const\\s+${escaped}\\s*=\\s*(?:async\\s*)?\\([^)]*\\)\\s*=>\\s*\\{([\\s\\S]{0,1200}?)\\n\\}`, "m")
49
+ ]) {
50
+ const match = text.match(pattern);
51
+ if (match) return match[1];
52
+ }
53
+ return "";
54
+ }
55
+
56
+ function parseExpressRouteCalls(filePath, text, apiRoutes, permissionMeta) {
57
+ const routes = [];
58
+ for (const match of text.matchAll(/app\.(get|post|put|patch|delete)\(\s*([^,]+?)\s*,([\s\S]*?)\)\s*;?/g)) {
59
+ const method = match[1].toUpperCase();
60
+ const rawPath = match[2].trim();
61
+ const args = match[3];
62
+ let routePath = null;
63
+ let routeKey = null;
64
+ const apiRouteRef = rawPath.match(/^API_ROUTES\.([A-Za-z_][A-Za-z0-9_]*)$/);
65
+ if (apiRouteRef) {
66
+ routeKey = apiRouteRef[1];
67
+ routePath = apiRoutes[routeKey] || null;
68
+ } else {
69
+ routePath = rawPath.replace(/^["'`]|["'`]$/g, "");
70
+ }
71
+ if (!routePath) continue;
72
+ const handlerNames = [...args.matchAll(/\b([A-Za-z_][A-Za-z0-9_]*)\b/g)].map((entry) => entry[1]);
73
+ const filtered = handlerNames.filter((name) => !["app", "get", "post", "put", "patch", "delete"].includes(name));
74
+ const handlerHint = filtered.length > 0 ? filtered[filtered.length - 1] : null;
75
+ const handlerContext = extractHandlerContext(text, handlerHint);
76
+ const meta = routeKey ? permissionMeta.get(routeKey) : null;
77
+ routes.push({
78
+ file: filePath,
79
+ method,
80
+ path: routePath,
81
+ handler_hint: handlerHint,
82
+ path_params: [...routePath.matchAll(/:([A-Za-z_][A-Za-z0-9_]*)/g)].map((entry) => entry[1]),
83
+ query_params: inferRouteQueryParams(handlerContext),
84
+ auth_hint:
85
+ meta?.super || meta?.authenticated
86
+ ? "secured"
87
+ : inferRouteAuthHint(args, handlerContext)
88
+ });
89
+ }
90
+ return routes;
91
+ }
92
+
93
+ export const expressExtractor = {
94
+ id: "api.express",
95
+ track: "api",
96
+ detect(context) {
97
+ const routeFiles = findImportFiles(context.paths, (filePath) => /src\/routes\/.+\.(ts|js|mjs|cjs)$/i.test(filePath));
98
+ return {
99
+ score: routeFiles.length > 0 ? 85 : 0,
100
+ reasons: routeFiles.length > 0 ? ["Found Express route modules"] : []
101
+ };
102
+ },
103
+ extract(context) {
104
+ const permissionsFile = findImportFiles(context.paths, (filePath) => /src\/helpers\/permissions\.(ts|js|mjs|cjs)$/i.test(filePath))[0];
105
+ const permissionsText = permissionsFile ? context.helpers.readTextIfExists(permissionsFile) || "" : "";
106
+ const apiRoutes = parseApiRoutesMap(permissionsText);
107
+ const permissionMeta = parsePermissionsMetadata(permissionsText);
108
+ const routeFiles = findImportFiles(context.paths, (filePath) => /src\/routes\/.+\.(ts|js|mjs|cjs)$/i.test(filePath));
109
+ const routes = routeFiles.flatMap((filePath) =>
110
+ parseExpressRouteCalls(filePath, context.helpers.readTextIfExists(filePath) || "", apiRoutes, permissionMeta)
111
+ );
112
+ const findings = [];
113
+ const candidates = { capabilities: [], routes: [], stacks: [] };
114
+ if (routes.length > 0) {
115
+ findings.push({
116
+ kind: "express_routes",
117
+ files: [...new Set(routes.map((route) => relativeTo(context.paths.repoRoot, route.file)))],
118
+ route_count: routes.length
119
+ });
120
+ candidates.capabilities.push(...routes.map((route) => makeCandidateRecord({
121
+ kind: "capability",
122
+ idHint: inferRouteCapabilityId(route),
123
+ label: route.handler_hint ? titleCase(route.handler_hint) : `${route.method} ${route.path}`,
124
+ confidence: "high",
125
+ sourceKind: "route_code",
126
+ provenance: `${relativeTo(context.paths.repoRoot, route.file)}#${route.method} ${route.path}`,
127
+ endpoint: { method: route.method, path: normalizeOpenApiPath(route.path) },
128
+ path_params: (route.path_params || []).map((name) => ({ name, required: true, type: null })),
129
+ query_params: (route.query_params || []).map((name) => ({ name, required: false, type: null })),
130
+ header_params: [],
131
+ input_fields: [],
132
+ output_fields: [],
133
+ auth_hint: route.auth_hint || "unknown",
134
+ entity_id: inferApiEntityIdFromPath(route.path),
135
+ track: "api"
136
+ })));
137
+ candidates.routes.push(...routes.map((route) => ({
138
+ path: normalizeOpenApiPath(route.path),
139
+ method: route.method,
140
+ confidence: "high",
141
+ source_kind: "route_code",
142
+ provenance: `${relativeTo(context.paths.repoRoot, route.file)}#${route.method} ${route.path}`
143
+ })));
144
+ candidates.stacks.push("express");
145
+ }
146
+ candidates.capabilities = dedupeCandidateRecords(candidates.capabilities, (record) => record.id_hint);
147
+ candidates.routes = dedupeCandidateRecords(
148
+ candidates.routes.map((route) => ({ ...route, id_hint: `${route.method}_${route.path}` })),
149
+ (record) => `${record.method}:${record.path}:${record.source_kind}`
150
+ ).map(({ id_hint, ...route }) => route);
151
+ candidates.stacks = [...new Set(candidates.stacks)].sort();
152
+ return { findings, candidates };
153
+ }
154
+ };
@@ -0,0 +1,371 @@
1
+ import path from "node:path";
2
+
3
+ import {
4
+ dedupeCandidateRecords,
5
+ findImportFiles,
6
+ inferApiCapabilityIdFromOperation,
7
+ inferApiEntityIdFromPath,
8
+ makeCandidateRecord,
9
+ normalizeOpenApiPath,
10
+ relativeTo,
11
+ titleCase
12
+ } from "../../core/shared.js";
13
+
14
+ function splitTopLevelProperties(block) {
15
+ const props = [];
16
+ let current = "";
17
+ let depth = 0;
18
+ let inString = false;
19
+ let quote = null;
20
+ for (let i = 0; i < block.length; i += 1) {
21
+ const ch = block[i];
22
+ const prev = block[i - 1];
23
+ if (inString) {
24
+ current += ch;
25
+ if (ch === quote && prev !== "\\") {
26
+ inString = false;
27
+ quote = null;
28
+ }
29
+ continue;
30
+ }
31
+ if (ch === "'" || ch === '"' || ch === "`") {
32
+ inString = true;
33
+ quote = ch;
34
+ current += ch;
35
+ continue;
36
+ }
37
+ if (ch === "{" || ch === "[" || ch === "(") {
38
+ depth += 1;
39
+ current += ch;
40
+ continue;
41
+ }
42
+ if (ch === "}" || ch === "]" || ch === ")") {
43
+ depth -= 1;
44
+ current += ch;
45
+ continue;
46
+ }
47
+ if (ch === "," && depth === 0) {
48
+ if (current.trim()) props.push(current.trim());
49
+ current = "";
50
+ continue;
51
+ }
52
+ current += ch;
53
+ }
54
+ if (current.trim()) props.push(current.trim());
55
+ return props;
56
+ }
57
+
58
+ function extractBalanced(text, openIndex, openChar, closeChar) {
59
+ let depth = 0;
60
+ let inString = false;
61
+ let quote = null;
62
+ for (let i = openIndex; i < text.length; i += 1) {
63
+ const ch = text[i];
64
+ const prev = text[i - 1];
65
+ if (inString) {
66
+ if (ch === quote && prev !== "\\") {
67
+ inString = false;
68
+ quote = null;
69
+ }
70
+ continue;
71
+ }
72
+ if (ch === "'" || ch === '"' || ch === "`") {
73
+ inString = true;
74
+ quote = ch;
75
+ continue;
76
+ }
77
+ if (ch === openChar) {
78
+ depth += 1;
79
+ } else if (ch === closeChar) {
80
+ depth -= 1;
81
+ if (depth === 0) {
82
+ return text.slice(openIndex, i + 1);
83
+ }
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function findCallArguments(text, calleePattern) {
90
+ const calls = [];
91
+ for (const match of text.matchAll(calleePattern)) {
92
+ const openIndex = text.indexOf("(", match.index);
93
+ if (openIndex < 0) continue;
94
+ const callText = extractBalanced(text, openIndex, "(", ")");
95
+ if (!callText) continue;
96
+ const inner = callText.slice(1, -1);
97
+ calls.push({
98
+ method: match[1].toUpperCase(),
99
+ args: splitTopLevelProperties(inner)
100
+ });
101
+ }
102
+ return calls;
103
+ }
104
+
105
+ function parseNamedTypeboxSchemas(schemaFiles, readText) {
106
+ const schemas = new Map();
107
+ for (const filePath of schemaFiles) {
108
+ const text = readText(filePath) || "";
109
+ for (const match of text.matchAll(/export\s+const\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*Type\.Object\s*\(/g)) {
110
+ const openParenIndex = text.indexOf("(", match.index);
111
+ const objectCall = extractBalanced(text, openParenIndex, "(", ")");
112
+ if (!objectCall) continue;
113
+ const firstBraceIndex = objectCall.indexOf("{");
114
+ if (firstBraceIndex < 0) continue;
115
+ const objectBlock = extractBalanced(objectCall, firstBraceIndex, "{", "}");
116
+ if (!objectBlock) continue;
117
+ schemas.set(match[1], objectBlock);
118
+ }
119
+ }
120
+ return schemas;
121
+ }
122
+
123
+ function parseSchemaExpressionFields(expression, namedSchemas, seen = new Set()) {
124
+ const value = String(expression || "").trim();
125
+ if (!value) return [];
126
+
127
+ const identifierMatch = value.match(/^([A-Za-z_][A-Za-z0-9_]*)$/);
128
+ if (identifierMatch) {
129
+ const schemaName = identifierMatch[1];
130
+ if (seen.has(schemaName)) return [];
131
+ if (namedSchemas.has(schemaName)) {
132
+ return parseSchemaExpressionFields(namedSchemas.get(schemaName), namedSchemas, new Set([...seen, schemaName]));
133
+ }
134
+ return [];
135
+ }
136
+
137
+ if ((value.startsWith("{") && value.endsWith("}"))) {
138
+ const fields = [];
139
+ for (const prop of splitTopLevelProperties(value.slice(1, -1))) {
140
+ const propMatch = prop.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:/);
141
+ if (propMatch) fields.push(propMatch[1]);
142
+ }
143
+ return [...new Set(fields)].sort();
144
+ }
145
+
146
+ const typeObjectMatch = value.match(/^Type\.Object\s*\(/);
147
+ if (typeObjectMatch) {
148
+ const firstBraceIndex = value.indexOf("{");
149
+ if (firstBraceIndex < 0) return [];
150
+ const objectBlock = extractBalanced(value, firstBraceIndex, "{", "}");
151
+ if (!objectBlock) return [];
152
+ return parseSchemaExpressionFields(objectBlock, namedSchemas, seen);
153
+ }
154
+
155
+ const arrayMatch = value.match(/^Type\.(Array|Optional)\s*\(([\s\S]*)\)$/);
156
+ if (arrayMatch) {
157
+ return parseSchemaExpressionFields(arrayMatch[2], namedSchemas, seen);
158
+ }
159
+
160
+ return [];
161
+ }
162
+
163
+ function parseSchemaBlock(optionsArg) {
164
+ const schemaKeyIndex = optionsArg.indexOf("schema:");
165
+ if (schemaKeyIndex < 0) return null;
166
+ const braceIndex = optionsArg.indexOf("{", schemaKeyIndex);
167
+ if (braceIndex < 0) return null;
168
+ return extractBalanced(optionsArg, braceIndex, "{", "}");
169
+ }
170
+
171
+ function parseSchemaEntryExpression(schemaBlock, entryName) {
172
+ const match = schemaBlock.match(new RegExp(`${entryName}\\s*:\\s*`, "m"));
173
+ if (!match || typeof match.index !== "number") return null;
174
+ const start = match.index + match[0].length;
175
+ let i = start;
176
+ while (i < schemaBlock.length && /\s/.test(schemaBlock[i])) i += 1;
177
+ if (i >= schemaBlock.length) return null;
178
+ const ch = schemaBlock[i];
179
+ if (ch === "{") {
180
+ return extractBalanced(schemaBlock, i, "{", "}");
181
+ }
182
+ if (/[A-Za-z_]/.test(ch)) {
183
+ let j = i;
184
+ let depth = 0;
185
+ let inString = false;
186
+ let quote = null;
187
+ for (; j < schemaBlock.length; j += 1) {
188
+ const current = schemaBlock[j];
189
+ const prev = schemaBlock[j - 1];
190
+ if (inString) {
191
+ if (current === quote && prev !== "\\") {
192
+ inString = false;
193
+ quote = null;
194
+ }
195
+ continue;
196
+ }
197
+ if (current === "'" || current === '"' || current === "`") {
198
+ inString = true;
199
+ quote = current;
200
+ continue;
201
+ }
202
+ if (current === "(" || current === "{" || current === "[") {
203
+ depth += 1;
204
+ } else if (current === ")" || current === "}" || current === "]") {
205
+ depth -= 1;
206
+ } else if ((current === "," || current === "\n") && depth <= 0) {
207
+ break;
208
+ }
209
+ }
210
+ return schemaBlock.slice(i, j).trim().replace(/,$/, "");
211
+ }
212
+ return null;
213
+ }
214
+
215
+ function parseResponseExpression(schemaBlock) {
216
+ const responseExpression = parseSchemaEntryExpression(schemaBlock, "response");
217
+ if (!responseExpression) return null;
218
+ if (!(responseExpression.startsWith("{") && responseExpression.endsWith("}"))) {
219
+ return responseExpression;
220
+ }
221
+ const preferredCodes = ["200", "201", "202", "204"];
222
+ const entries = splitTopLevelProperties(responseExpression.slice(1, -1));
223
+ for (const code of preferredCodes) {
224
+ const entry = entries.find((prop) => prop.trim().startsWith(`${code}:`));
225
+ if (!entry) continue;
226
+ const value = entry.slice(entry.indexOf(":") + 1).trim();
227
+ if (!value) continue;
228
+ if (value.startsWith("{")) {
229
+ return value;
230
+ }
231
+ return value;
232
+ }
233
+ return null;
234
+ }
235
+
236
+ function parseTags(schemaBlock) {
237
+ const tagsMatch = schemaBlock.match(/tags\s*:\s*\[([^\]]*)\]/m);
238
+ if (!tagsMatch) return [];
239
+ return tagsMatch[1]
240
+ .split(",")
241
+ .map((tag) => tag.trim().replace(/^["'`]|["'`]$/g, ""))
242
+ .filter(Boolean);
243
+ }
244
+
245
+ function joinRoutePath(basePath, localPath) {
246
+ const full = `${basePath === "/" ? "" : basePath}${localPath === "/" ? "" : localPath}` || "/";
247
+ return normalizeOpenApiPath(full.startsWith("/") ? full : `/${full}`);
248
+ }
249
+
250
+ function inferBasePath(apiRoutesRoot, filePath) {
251
+ const relativeDir = path.relative(apiRoutesRoot, path.dirname(filePath)).replaceAll(path.sep, "/");
252
+ if (!relativeDir || relativeDir === ".") return "/";
253
+ return `/${relativeDir}`;
254
+ }
255
+
256
+ function inferFastifyAuthHint(routePath, optionsArg) {
257
+ if (routePath === "/auth/login" || routePath === "/auth/register") return "public";
258
+ if (/preHandler\s*:/.test(optionsArg)) return "secured";
259
+ return "secured";
260
+ }
261
+
262
+ export const fastifyExtractor = {
263
+ id: "api.fastify",
264
+ track: "api",
265
+ detect(context) {
266
+ const routeFiles = findImportFiles(context.paths, (filePath) => /src\/routes\/api\/.+\.(ts|js|mjs|cjs)$/i.test(filePath));
267
+ const packageJsonFiles = findImportFiles(context.paths, (filePath) => /package\.json$/i.test(filePath));
268
+ const hasFastifyDependency = packageJsonFiles.some((filePath) => /"fastify"\s*:/.test(context.helpers.readTextIfExists(filePath) || ""));
269
+ return {
270
+ score: routeFiles.length > 0 && hasFastifyDependency ? 86 : 0,
271
+ reasons: routeFiles.length > 0 && hasFastifyDependency ? ["Found Fastify route plugins and Fastify package dependency"] : []
272
+ };
273
+ },
274
+ extract(context) {
275
+ const apiRoutesRoot = findImportFiles(context.paths, (filePath) => /src\/routes\/api\/index\.(ts|js|mjs|cjs)$/i.test(filePath))[0]
276
+ ? path.join(path.dirname(findImportFiles(context.paths, (filePath) => /src\/routes\/api\/index\.(ts|js|mjs|cjs)$/i.test(filePath))[0]))
277
+ : null;
278
+ if (!apiRoutesRoot) {
279
+ return { findings: [], candidates: { capabilities: [], routes: [], stacks: [] } };
280
+ }
281
+
282
+ const routeFiles = findImportFiles(context.paths, (filePath) => /src\/routes\/api\/.+\.(ts|js|mjs|cjs)$/i.test(filePath))
283
+ .filter((filePath) => !/\/autohooks\.(ts|js|mjs|cjs)$/i.test(filePath));
284
+ const schemaFiles = findImportFiles(context.paths, (filePath) => /src\/schemas\/.+\.(ts|js|mjs|cjs)$/i.test(filePath));
285
+ const namedSchemas = parseNamedTypeboxSchemas(schemaFiles, context.helpers.readTextIfExists);
286
+
287
+ const routes = [];
288
+ for (const filePath of routeFiles) {
289
+ const text = context.helpers.readTextIfExists(filePath) || "";
290
+ const basePath = inferBasePath(apiRoutesRoot, filePath);
291
+ const routeCalls = findCallArguments(text, /fastify\.(get|post|put|patch|delete)\s*\(/g);
292
+ for (const routeCall of routeCalls) {
293
+ const [pathArg, optionsArg = "", handlerArg = ""] = routeCall.args;
294
+ if (!pathArg) continue;
295
+ const localPath = pathArg.trim().replace(/^["'`]|["'`]$/g, "") || "/";
296
+ const fullPath = joinRoutePath(basePath, localPath);
297
+ const schemaBlock = parseSchemaBlock(optionsArg);
298
+ const tags = schemaBlock ? parseTags(schemaBlock) : [];
299
+ const queryExpression = schemaBlock ? parseSchemaEntryExpression(schemaBlock, "querystring") : null;
300
+ const bodyExpression = schemaBlock ? parseSchemaEntryExpression(schemaBlock, "body") : null;
301
+ const paramsExpression = schemaBlock ? parseSchemaEntryExpression(schemaBlock, "params") : null;
302
+ const responseExpression = schemaBlock ? parseResponseExpression(schemaBlock) : null;
303
+ const operation = {
304
+ method: routeCall.method,
305
+ path: fullPath,
306
+ tags
307
+ };
308
+ routes.push({
309
+ file: filePath,
310
+ method: routeCall.method,
311
+ path: fullPath,
312
+ id_hint: inferApiCapabilityIdFromOperation(operation),
313
+ label: titleCase(inferApiCapabilityIdFromOperation(operation).replace(/^cap_/, "")),
314
+ path_params: parseSchemaExpressionFields(paramsExpression, namedSchemas).map((name) => ({ name, required: true, type: null })),
315
+ query_params: parseSchemaExpressionFields(queryExpression, namedSchemas).map((name) => ({ name, required: false, type: null })),
316
+ input_fields: parseSchemaExpressionFields(bodyExpression, namedSchemas),
317
+ output_fields: parseSchemaExpressionFields(responseExpression, namedSchemas),
318
+ auth_hint: inferFastifyAuthHint(fullPath, optionsArg),
319
+ entity_id: inferApiEntityIdFromPath(fullPath, { tags }),
320
+ tags,
321
+ provenance: `${relativeTo(context.paths.repoRoot, filePath)}#${routeCall.method} ${fullPath}`
322
+ });
323
+ }
324
+ }
325
+
326
+ const findings = [];
327
+ const candidates = { capabilities: [], routes: [], stacks: [] };
328
+ if (routes.length > 0) {
329
+ findings.push({
330
+ kind: "fastify_routes",
331
+ files: [...new Set(routes.map((route) => relativeTo(context.paths.repoRoot, route.file)))],
332
+ route_count: routes.length
333
+ });
334
+ candidates.capabilities.push(...routes.map((route) => makeCandidateRecord({
335
+ kind: "capability",
336
+ idHint: route.id_hint,
337
+ label: route.label,
338
+ confidence: "high",
339
+ sourceKind: "route_code",
340
+ provenance: route.provenance,
341
+ endpoint: { method: route.method, path: route.path },
342
+ path_params: route.path_params,
343
+ query_params: route.query_params,
344
+ header_params: [],
345
+ input_fields: route.input_fields,
346
+ output_fields: route.output_fields,
347
+ auth_hint: route.auth_hint,
348
+ entity_id: route.entity_id,
349
+ tags: route.tags,
350
+ track: "api"
351
+ })));
352
+ candidates.routes.push(...routes.map((route) => ({
353
+ path: route.path,
354
+ method: route.method,
355
+ confidence: "high",
356
+ source_kind: "route_code",
357
+ provenance: route.provenance
358
+ })));
359
+ candidates.stacks.push("fastify");
360
+ }
361
+
362
+ candidates.capabilities = dedupeCandidateRecords(candidates.capabilities, (record) => record.id_hint);
363
+ candidates.routes = dedupeCandidateRecords(
364
+ candidates.routes.map((route) => ({ ...route, id_hint: `${route.method}_${route.path}` })),
365
+ (record) => `${record.method}:${record.path}:${record.source_kind}`
366
+ ).map(({ id_hint, ...route }) => route);
367
+ candidates.stacks = [...new Set(candidates.stacks)].sort();
368
+
369
+ return { findings, candidates };
370
+ }
371
+ };
@@ -0,0 +1,135 @@
1
+ import {
2
+ canonicalCandidateTerm,
3
+ dedupeCandidateRecords,
4
+ findImportFiles,
5
+ makeCandidateRecord,
6
+ relativeTo,
7
+ titleCase
8
+ } from "../../core/shared.js";
9
+
10
+ function featureStemFromPath(filePath) {
11
+ return canonicalCandidateTerm(filePath.match(/\/features\/([^/]+)\//)?.[1] || "item");
12
+ }
13
+
14
+ function pluralizeStem(stem) {
15
+ if (stem.endsWith("s")) return stem;
16
+ return `${stem}s`;
17
+ }
18
+
19
+ function capabilityIdFor(featureStem, methodName, httpMethod) {
20
+ const stem = canonicalCandidateTerm(featureStem);
21
+ const normalizedMethod = String(methodName || "").toLowerCase();
22
+ if (httpMethod === "GET") {
23
+ return `cap_list_${pluralizeStem(stem)}`;
24
+ }
25
+ if (httpMethod === "POST") {
26
+ return `cap_create_${stem}`;
27
+ }
28
+ if (httpMethod === "PUT" || httpMethod === "PATCH") {
29
+ return `cap_update_${stem}`;
30
+ }
31
+ if (httpMethod === "DELETE") {
32
+ return `cap_delete_${stem}`;
33
+ }
34
+ return `cap_${normalizedMethod}_${stem}`;
35
+ }
36
+
37
+ function extractApiConfigPaths(context) {
38
+ const configFile = findImportFiles(context.paths, (filePath) => /\/lib\/common\/network\/api_config\.dart$/i.test(filePath))[0];
39
+ const mapping = new Map();
40
+ if (!configFile) return mapping;
41
+ const text = context.helpers.readTextIfExists(configFile) || "";
42
+ for (const match of text.matchAll(/static\s+const\s+String\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*'([^']+)'/g)) {
43
+ mapping.set(match[1], match[2]);
44
+ }
45
+ return mapping;
46
+ }
47
+
48
+ function parseDatasourceFile(text, provenance, filePath, apiConfigPaths) {
49
+ const featureStem = featureStemFromPath(filePath);
50
+ const capabilities = [];
51
+ const methods = [...String(text || "").matchAll(/Future<[\s\S]*?>\s+([A-Za-z_][A-Za-z0-9_]*)\([^)]*\)\s+async\s*\{/g)];
52
+ for (let index = 0; index < methods.length; index += 1) {
53
+ const methodName = methods[index][1];
54
+ const bodyStart = methods[index].index + methods[index][0].length;
55
+ const bodyEnd = index + 1 < methods.length ? methods[index + 1].index : String(text || "").length;
56
+ const body = String(text || "").slice(bodyStart, bodyEnd);
57
+ const dioCall = body.match(/dioClient\.dio\.(get|post|put|patch|delete)\(([\s\S]*?)\)/i);
58
+ if (!dioCall) continue;
59
+ const httpMethod = dioCall[1].toUpperCase();
60
+ const callArgs = dioCall[2];
61
+ const apiRef = callArgs.match(/ApiConfig\.([A-Za-z_][A-Za-z0-9_]*)/);
62
+ const basePath = apiConfigPaths.get(apiRef?.[1] || "") || `/${pluralizeStem(featureStem)}`;
63
+ const dynamicPath = callArgs.match(/"\$\{ApiConfig\.[A-Za-z_][A-Za-z0-9_]*\}\/\$\{[^}]+\}"/);
64
+ const path = dynamicPath ? `${basePath}/{id}` : basePath;
65
+ const queryParams = [...body.matchAll(/'([^']+)'\s*:\s*[^,}]+/g)]
66
+ .map((entry) => ({ name: entry[1], required: false, type: null }));
67
+ const outputFields = /^GET$/.test(httpMethod) ? [pluralizeStem(featureStem)] : [];
68
+ const inputFields = [...new Set([...callArgs.matchAll(/data:\s*([A-Za-z_][A-Za-z0-9_]*)/g)].map((entry) => entry[1]))];
69
+ capabilities.push(makeCandidateRecord({
70
+ kind: "capability",
71
+ idHint: capabilityIdFor(featureStem, methodName, httpMethod),
72
+ label: titleCase(capabilityIdFor(featureStem, methodName, httpMethod).replace(/^cap_/, "")),
73
+ confidence: "high",
74
+ sourceKind: "route_code",
75
+ provenance: `${provenance}#${methodName}`,
76
+ endpoint: { method: httpMethod, path },
77
+ path_params: /\{id\}/.test(path) ? [{ name: "id", required: true, type: null }] : [],
78
+ query_params: queryParams,
79
+ header_params: [],
80
+ input_fields: inputFields,
81
+ output_fields: outputFields,
82
+ auth_hint: "public",
83
+ entity_id: `entity_${featureStem}`,
84
+ track: "api"
85
+ }));
86
+ }
87
+ return capabilities;
88
+ }
89
+
90
+ export const flutterDioExtractor = {
91
+ id: "api.flutter-dio",
92
+ track: "api",
93
+ detect(context) {
94
+ const files = findImportFiles(
95
+ context.paths,
96
+ (filePath) => /\/lib\/features\/.+\/data\/datasources\/.+_remote_data_source\.dart$/i.test(filePath)
97
+ );
98
+ const score = files.some((filePath) => /dioClient\.dio\./.test(context.helpers.readTextIfExists(filePath) || "")) ? 85 : 0;
99
+ return {
100
+ score,
101
+ reasons: score > 0 ? ["Found Flutter remote data sources using Dio"] : []
102
+ };
103
+ },
104
+ extract(context) {
105
+ const files = findImportFiles(
106
+ context.paths,
107
+ (filePath) => /\/lib\/features\/.+\/data\/datasources\/.+_remote_data_source\.dart$/i.test(filePath)
108
+ );
109
+ const apiConfigPaths = extractApiConfigPaths(context);
110
+ const capabilities = [];
111
+ for (const filePath of files) {
112
+ const provenance = relativeTo(context.paths.repoRoot, filePath);
113
+ capabilities.push(...parseDatasourceFile(context.helpers.readTextIfExists(filePath) || "", provenance, filePath, apiConfigPaths));
114
+ }
115
+ const findings = capabilities.length > 0 ? [{
116
+ kind: "flutter_dio",
117
+ files: [...new Set(capabilities.flatMap((entry) => entry.provenance || []).map((entry) => String(entry).split("#")[0]))],
118
+ capability_count: capabilities.length
119
+ }] : [];
120
+ return {
121
+ findings,
122
+ candidates: {
123
+ capabilities: dedupeCandidateRecords(capabilities, (record) => `${record.id_hint}:${record.endpoint?.method}:${record.endpoint?.path}`),
124
+ routes: capabilities.map((entry) => ({
125
+ path: entry.endpoint.path,
126
+ method: entry.endpoint.method,
127
+ confidence: entry.confidence,
128
+ source_kind: entry.source_kind,
129
+ provenance: entry.provenance?.[0] || null
130
+ })),
131
+ stacks: capabilities.length > 0 ? ["flutter_dio"] : []
132
+ }
133
+ };
134
+ }
135
+ };