@vibecheckai/cli 3.5.1 → 3.5.2

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 (272) hide show
  1. package/bin/registry.js +406 -154
  2. package/bin/runners/context/analyzer.js +52 -1
  3. package/bin/runners/context/generators/mcp.js +15 -13
  4. package/bin/runners/context/git-context.js +3 -1
  5. package/bin/runners/context/proof-context.js +248 -1
  6. package/bin/runners/context/team-conventions.js +33 -7
  7. package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
  8. package/bin/runners/lib/agent-firewall/change-packet/builder.js +488 -0
  9. package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
  10. package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
  11. package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
  12. package/bin/runners/lib/agent-firewall/claims/extractor.js +303 -0
  13. package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
  14. package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
  15. package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
  16. package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
  17. package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
  18. package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
  19. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +127 -0
  20. package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
  21. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +213 -0
  22. package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
  23. package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
  24. package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
  25. package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
  26. package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
  27. package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
  28. package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
  29. package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
  30. package/bin/runners/lib/agent-firewall/interceptor/base.js +304 -0
  31. package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
  32. package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
  33. package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
  34. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
  35. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
  36. package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
  37. package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
  38. package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
  39. package/bin/runners/lib/agent-firewall/logger.js +141 -0
  40. package/bin/runners/lib/agent-firewall/policy/default-policy.json +90 -0
  41. package/bin/runners/lib/agent-firewall/policy/engine.js +103 -0
  42. package/bin/runners/lib/agent-firewall/policy/loader.js +451 -0
  43. package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
  44. package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
  45. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +86 -0
  46. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +162 -0
  47. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +189 -0
  48. package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
  49. package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
  50. package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
  51. package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
  52. package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
  53. package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
  54. package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
  55. package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
  56. package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
  57. package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
  58. package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
  59. package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
  60. package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
  61. package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
  62. package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
  63. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
  64. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
  65. package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
  66. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
  67. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
  68. package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
  69. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
  70. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
  71. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
  72. package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
  73. package/bin/runners/lib/agent-firewall/truthpack/loader.js +137 -0
  74. package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
  75. package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
  76. package/bin/runners/lib/analysis-core.js +220 -182
  77. package/bin/runners/lib/analyzers.js +2145 -224
  78. package/bin/runners/lib/api-client.js +269 -0
  79. package/bin/runners/lib/authority-badge.js +425 -0
  80. package/bin/runners/lib/cli-output.js +242 -210
  81. package/bin/runners/lib/default-config.js +127 -0
  82. package/bin/runners/lib/detectors-v2.js +547 -785
  83. package/bin/runners/lib/doctor/modules/security.js +3 -1
  84. package/bin/runners/lib/engine/ast-cache.js +210 -0
  85. package/bin/runners/lib/engine/auth-extractor.js +211 -0
  86. package/bin/runners/lib/engine/billing-extractor.js +112 -0
  87. package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
  88. package/bin/runners/lib/engine/env-extractor.js +207 -0
  89. package/bin/runners/lib/engine/express-extractor.js +208 -0
  90. package/bin/runners/lib/engine/extractors.js +849 -0
  91. package/bin/runners/lib/engine/index.js +207 -0
  92. package/bin/runners/lib/engine/repo-index.js +514 -0
  93. package/bin/runners/lib/engine/types.js +124 -0
  94. package/bin/runners/lib/engines/accessibility-engine.js +190 -0
  95. package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
  96. package/bin/runners/lib/engines/ast-cache.js +99 -0
  97. package/bin/runners/lib/engines/code-quality-engine.js +255 -0
  98. package/bin/runners/lib/engines/console-logs-engine.js +115 -0
  99. package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
  100. package/bin/runners/lib/engines/dead-code-engine.js +198 -0
  101. package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
  102. package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
  103. package/bin/runners/lib/engines/file-filter.js +131 -0
  104. package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
  105. package/bin/runners/lib/engines/mock-data-engine.js +272 -0
  106. package/bin/runners/lib/engines/parallel-processor.js +71 -0
  107. package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
  108. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
  109. package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
  110. package/bin/runners/lib/engines/type-aware-engine.js +152 -0
  111. package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
  112. package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
  113. package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
  114. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
  115. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
  116. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
  117. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
  118. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
  119. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
  120. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
  121. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
  122. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
  123. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
  124. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
  125. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
  126. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
  127. package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
  128. package/bin/runners/lib/entitlements-v2.js +152 -446
  129. package/bin/runners/lib/error-handler.js +60 -12
  130. package/bin/runners/lib/error-messages.js +289 -0
  131. package/bin/runners/lib/evidence-pack.js +7 -1
  132. package/bin/runners/lib/exit-codes.js +275 -0
  133. package/bin/runners/lib/finding-id.js +69 -0
  134. package/bin/runners/lib/finding-sorter.js +89 -0
  135. package/bin/runners/lib/fingerprint.js +377 -0
  136. package/bin/runners/lib/global-flags.js +37 -0
  137. package/bin/runners/lib/help-formatter.js +413 -0
  138. package/bin/runners/lib/logger.js +38 -0
  139. package/bin/runners/lib/next-action.js +560 -0
  140. package/bin/runners/lib/prerequisites.js +149 -0
  141. package/bin/runners/lib/route-detection.js +137 -68
  142. package/bin/runners/lib/route-truth.js +1167 -322
  143. package/bin/runners/lib/scan-output.js +504 -463
  144. package/bin/runners/lib/scan-runner.js +135 -0
  145. package/bin/runners/lib/schemas/ajv-validator.js +464 -0
  146. package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
  147. package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
  148. package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
  149. package/bin/runners/lib/schemas/run-request.schema.json +108 -0
  150. package/bin/runners/lib/schemas/validator.js +27 -0
  151. package/bin/runners/lib/schemas/verdict.schema.json +140 -0
  152. package/bin/runners/lib/ship-output-enterprise.js +239 -0
  153. package/bin/runners/lib/ship-output.js +328 -31
  154. package/bin/runners/lib/terminal-ui.js +234 -731
  155. package/bin/runners/lib/truth.js +1332 -308
  156. package/bin/runners/lib/unified-cli-output.js +604 -0
  157. package/bin/runners/lib/unified-output.js +163 -155
  158. package/bin/runners/lib/upsell.js +104 -204
  159. package/bin/runners/runAgent.d.ts +5 -0
  160. package/bin/runners/runAgent.js +161 -0
  161. package/bin/runners/runAllowlist.js +166 -101
  162. package/bin/runners/runApprove.js +1200 -0
  163. package/bin/runners/runAuth.js +373 -95
  164. package/bin/runners/runCheckpoint.js +59 -21
  165. package/bin/runners/runClassify.js +926 -0
  166. package/bin/runners/runContext.d.ts +4 -0
  167. package/bin/runners/runContext.js +136 -24
  168. package/bin/runners/runDoctor.js +115 -67
  169. package/bin/runners/runEvidencePack.js +239 -96
  170. package/bin/runners/runFirewall.d.ts +5 -0
  171. package/bin/runners/runFirewall.js +134 -0
  172. package/bin/runners/runFirewallHook.d.ts +5 -0
  173. package/bin/runners/runFirewallHook.js +56 -0
  174. package/bin/runners/runFix.js +6 -5
  175. package/bin/runners/runGuard.js +212 -118
  176. package/bin/runners/runInit.js +66 -21
  177. package/bin/runners/runLabs.js +204 -121
  178. package/bin/runners/runMcp.js +131 -60
  179. package/bin/runners/runPolish.d.ts +4 -0
  180. package/bin/runners/runPolish.js +43 -20
  181. package/bin/runners/runProof.zip +0 -0
  182. package/bin/runners/runProve.js +15 -5
  183. package/bin/runners/runQuickstart.js +531 -0
  184. package/bin/runners/runReality.js +14 -0
  185. package/bin/runners/runReport.js +36 -4
  186. package/bin/runners/runScan.js +689 -91
  187. package/bin/runners/runShip.js +96 -40
  188. package/bin/runners/runTruth.d.ts +5 -0
  189. package/bin/runners/runTruth.js +101 -0
  190. package/bin/runners/runValidate.js +21 -4
  191. package/bin/runners/runWatch.js +118 -54
  192. package/bin/scan.js +6 -1
  193. package/bin/vibecheck.js +297 -52
  194. package/mcp-server/HARDENING_SUMMARY.md +299 -0
  195. package/mcp-server/agent-firewall-interceptor.js +500 -0
  196. package/mcp-server/authority-tools.js +569 -0
  197. package/mcp-server/conductor/conflict-resolver.js +588 -0
  198. package/mcp-server/conductor/execution-planner.js +544 -0
  199. package/mcp-server/conductor/index.js +377 -0
  200. package/mcp-server/conductor/lock-manager.js +615 -0
  201. package/mcp-server/conductor/request-queue.js +550 -0
  202. package/mcp-server/conductor/session-manager.js +500 -0
  203. package/mcp-server/conductor/tools.js +510 -0
  204. package/mcp-server/deprecation-middleware.js +282 -0
  205. package/mcp-server/handlers/index.ts +15 -0
  206. package/mcp-server/handlers/tool-handler.ts +474 -591
  207. package/mcp-server/index.js +1748 -1099
  208. package/mcp-server/lib/api-client.cjs +13 -0
  209. package/mcp-server/lib/cache-wrapper.cjs +383 -0
  210. package/mcp-server/lib/error-envelope.js +138 -0
  211. package/mcp-server/lib/executor.ts +428 -721
  212. package/mcp-server/lib/index.ts +19 -0
  213. package/mcp-server/lib/logger.cjs +30 -0
  214. package/mcp-server/lib/rate-limiter.js +166 -0
  215. package/mcp-server/lib/sandbox.test.ts +519 -0
  216. package/mcp-server/lib/sandbox.ts +342 -284
  217. package/mcp-server/lib/types.ts +267 -0
  218. package/mcp-server/logger.js +173 -0
  219. package/mcp-server/package.json +11 -27
  220. package/mcp-server/premium-tools.js +2 -2
  221. package/mcp-server/registry/tool-registry.js +794 -0
  222. package/mcp-server/registry/tools.json +507 -378
  223. package/mcp-server/registry.test.ts +334 -0
  224. package/mcp-server/tests/tier-gating.test.js +297 -0
  225. package/mcp-server/tier-auth.js +492 -347
  226. package/mcp-server/tools-v3.js +950 -0
  227. package/mcp-server/truth-context.js +131 -90
  228. package/mcp-server/truth-firewall-tools.js +1612 -1001
  229. package/mcp-server/tsconfig.json +8 -5
  230. package/mcp-server/vibecheck-2.0-tools.js +14 -1
  231. package/mcp-server/vibecheck-mcp-server-3.2.0.tgz +0 -0
  232. package/mcp-server/vibecheck-tools.js +2 -2
  233. package/package.json +4 -3
  234. package/bin/runners/runInstall.js +0 -281
  235. package/mcp-server/ARCHITECTURE.md +0 -339
  236. package/mcp-server/__tests__/cache.test.ts +0 -313
  237. package/mcp-server/__tests__/executor.test.ts +0 -239
  238. package/mcp-server/__tests__/fixtures/exclusion-test/.cache/webpack/cache.pack +0 -1
  239. package/mcp-server/__tests__/fixtures/exclusion-test/.next/server/chunk.js +0 -3
  240. package/mcp-server/__tests__/fixtures/exclusion-test/.turbo/cache.json +0 -3
  241. package/mcp-server/__tests__/fixtures/exclusion-test/.venv/lib/env.py +0 -3
  242. package/mcp-server/__tests__/fixtures/exclusion-test/dist/bundle.js +0 -3
  243. package/mcp-server/__tests__/fixtures/exclusion-test/package.json +0 -5
  244. package/mcp-server/__tests__/fixtures/exclusion-test/src/app.ts +0 -5
  245. package/mcp-server/__tests__/fixtures/exclusion-test/venv/lib/config.py +0 -4
  246. package/mcp-server/__tests__/ids.test.ts +0 -345
  247. package/mcp-server/__tests__/integration/tools.test.ts +0 -410
  248. package/mcp-server/__tests__/registry.test.ts +0 -365
  249. package/mcp-server/__tests__/sandbox.test.ts +0 -323
  250. package/mcp-server/__tests__/schemas.test.ts +0 -372
  251. package/mcp-server/benchmarks/run-benchmarks.ts +0 -304
  252. package/mcp-server/examples/doctor.request.json +0 -14
  253. package/mcp-server/examples/doctor.response.json +0 -53
  254. package/mcp-server/examples/error.response.json +0 -15
  255. package/mcp-server/examples/scan.request.json +0 -14
  256. package/mcp-server/examples/scan.response.json +0 -108
  257. package/mcp-server/index-v3.ts +0 -293
  258. package/mcp-server/index.old.js +0 -4137
  259. package/mcp-server/lib/cache.ts +0 -341
  260. package/mcp-server/lib/errors.ts +0 -346
  261. package/mcp-server/lib/ids.ts +0 -238
  262. package/mcp-server/lib/logger.ts +0 -368
  263. package/mcp-server/lib/metrics.ts +0 -365
  264. package/mcp-server/lib/validator.ts +0 -229
  265. package/mcp-server/package-lock.json +0 -165
  266. package/mcp-server/schemas/error-envelope.schema.json +0 -125
  267. package/mcp-server/schemas/finding.schema.json +0 -167
  268. package/mcp-server/schemas/report-artifact.schema.json +0 -88
  269. package/mcp-server/schemas/run-request.schema.json +0 -75
  270. package/mcp-server/schemas/verdict.schema.json +0 -168
  271. package/mcp-server/tier-auth.d.ts +0 -71
  272. package/mcp-server/vitest.config.ts +0 -16
@@ -0,0 +1,849 @@
1
+ // bin/runners/lib/engine/extractors.js
2
+ // Optimized route extractors that use RepoIndex and globalASTCache
3
+
4
+ const path = require("path");
5
+ const crypto = require("crypto");
6
+ const traverse = require("@babel/traverse").default;
7
+ const t = require("@babel/types");
8
+ const { globalASTCache } = require("./ast-cache");
9
+
10
+ // ---------- helpers ----------
11
+
12
+ function sha256(text) {
13
+ return "sha256:" + crypto.createHash("sha256").update(text).digest("hex");
14
+ }
15
+
16
+ function canonicalizeMethod(m) {
17
+ const u = String(m || "").toUpperCase();
18
+ if (u === "ALL" || u === "ANY" || u === "*") return "*";
19
+ return u;
20
+ }
21
+
22
+ function stripQueryHash(s) {
23
+ const v = String(s || "");
24
+ const q = v.indexOf("?");
25
+ const h = v.indexOf("#");
26
+ const cut = q === -1 ? h : h === -1 ? q : Math.min(q, h);
27
+ return cut === -1 ? v : v.slice(0, cut);
28
+ }
29
+
30
+ function canonicalizePath(p) {
31
+ let s = stripQueryHash(String(p || "").trim());
32
+ const protoIdx = s.indexOf("://");
33
+ if (protoIdx !== -1) {
34
+ const slashAfterHost = s.indexOf("/", protoIdx + 3);
35
+ s = slashAfterHost === -1 ? "/" : s.slice(slashAfterHost);
36
+ }
37
+ if (!s.startsWith("/")) s = "/" + s;
38
+ s = s.replace(/\/+/g, "/");
39
+ s = s.replace(/\[\[\.{3}([^\]]+)\]\]/g, "*$1?");
40
+ s = s.replace(/\[\.{3}([^\]]+)\]/g, "*$1");
41
+ s = s.replace(/\[([^\]]+)\]/g, ":$1");
42
+ if (s.length > 1) s = s.replace(/\/$/, "");
43
+ return s;
44
+ }
45
+
46
+ function joinPaths(prefix, p) {
47
+ const a = canonicalizePath(prefix || "/");
48
+ const b = canonicalizePath(p || "/");
49
+ if (a === "/") return b;
50
+ if (b === "/") return a;
51
+ return canonicalizePath(a + "/" + b);
52
+ }
53
+
54
+ function evidenceFromContent(content, fileRel, loc, reason) {
55
+ if (!loc || !content) return null;
56
+ const lines = content.split(/\r?\n/);
57
+ const start = Math.max(1, loc.start?.line || 1);
58
+ const end = Math.max(start, loc.end?.line || start);
59
+ const snippet = lines.slice(start - 1, end).join("\n");
60
+ return {
61
+ id: `ev_${crypto.randomBytes(4).toString("hex")}`,
62
+ file: fileRel,
63
+ lines: `${start}-${end}`,
64
+ snippetHash: sha256(snippet),
65
+ reason,
66
+ };
67
+ }
68
+
69
+ function isRouteGroupSegment(seg) {
70
+ return seg.startsWith("(") && seg.endsWith(")");
71
+ }
72
+
73
+ function isParallelSegment(seg) {
74
+ return seg.startsWith("@");
75
+ }
76
+
77
+ // ---------- Next.js App Router ----------
78
+
79
+ const HTTP_EXPORTS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]);
80
+
81
+ function nextAppApiPathFromRel(fileRel) {
82
+ const idx = fileRel.indexOf("app/api/");
83
+ if (idx === -1) return null;
84
+ let sub = fileRel.slice(idx + "app/api/".length);
85
+ sub = sub.replace(/\/route\.(ts|tsx|js|jsx)$/, "");
86
+ const parts = sub.split("/").filter(Boolean).filter((seg) => !isRouteGroupSegment(seg) && !isParallelSegment(seg));
87
+ sub = parts.join("/");
88
+ return canonicalizePath("/api/" + sub);
89
+ }
90
+
91
+ /**
92
+ * Extract Next.js App Router API routes using RepoIndex
93
+ * @param {import('./repo-index').RepoIndex} index
94
+ * @param {Object} stats
95
+ * @returns {Array}
96
+ */
97
+ function extractNextAppRoutes(index, stats) {
98
+ // Get route files from index (already filtered)
99
+ const routeFiles = index.files.filter(f =>
100
+ f.rel.includes("/app/") &&
101
+ f.rel.includes("/api/") &&
102
+ /route\.(ts|tsx|js|jsx)$/.test(f.rel)
103
+ );
104
+
105
+ const out = [];
106
+
107
+ for (const file of routeFiles) {
108
+ const routePath = nextAppApiPathFromRel(file.rel);
109
+ if (!routePath) continue;
110
+
111
+ const content = index.getContent(file.abs);
112
+ if (!content) continue;
113
+
114
+ // Use shared AST cache
115
+ const { ast, error } = globalASTCache.parse(content, file.abs);
116
+ if (error || !ast) {
117
+ stats.parseErrors++;
118
+ continue;
119
+ }
120
+
121
+ const methods = [];
122
+
123
+ try {
124
+ traverse(ast, {
125
+ ExportNamedDeclaration(p) {
126
+ const decl = p.node.declaration;
127
+
128
+ if (t.isFunctionDeclaration(decl) && decl.id?.name) {
129
+ const n = decl.id.name.toUpperCase();
130
+ if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: decl.loc, why: "export function" });
131
+ }
132
+
133
+ if (t.isVariableDeclaration(decl)) {
134
+ for (const d of decl.declarations) {
135
+ if (!t.isVariableDeclarator(d)) continue;
136
+ if (!t.isIdentifier(d.id)) continue;
137
+ const n = d.id.name.toUpperCase();
138
+ if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: d.loc || decl.loc, why: "export const" });
139
+ }
140
+ }
141
+
142
+ for (const s of p.node.specifiers || []) {
143
+ if (!t.isExportSpecifier(s)) continue;
144
+ if (!t.isIdentifier(s.exported)) continue;
145
+ const n = s.exported.name.toUpperCase();
146
+ if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: s.loc || p.node.loc, why: "export re-export" });
147
+ }
148
+ },
149
+ });
150
+ } catch {
151
+ stats.parseErrors++;
152
+ continue;
153
+ }
154
+
155
+ if (methods.length === 0) {
156
+ out.push({
157
+ method: "*",
158
+ path: routePath,
159
+ handler: file.rel,
160
+ confidence: "low",
161
+ framework: "next",
162
+ evidence: [],
163
+ });
164
+ continue;
165
+ }
166
+
167
+ for (const m of methods) {
168
+ const ev = evidenceFromContent(content, file.rel, m.loc, `Next app router ${m.method} (${m.why})`);
169
+ out.push({
170
+ method: m.method,
171
+ path: routePath,
172
+ handler: file.rel,
173
+ confidence: m.why === "export re-export" ? "med" : "high",
174
+ framework: "next",
175
+ evidence: ev ? [ev] : [],
176
+ });
177
+ }
178
+ }
179
+
180
+ return out;
181
+ }
182
+
183
+ // ---------- Next.js Pages Router ----------
184
+
185
+ function nextPagesApiPathFromRel(fileRel) {
186
+ const idx = fileRel.indexOf("pages/api/");
187
+ if (idx === -1) return null;
188
+ let sub = fileRel.slice(idx + "pages/api/".length);
189
+ sub = sub.replace(/\.(ts|tsx|js|jsx)$/, "");
190
+ if (sub === "index") sub = "";
191
+ sub = sub.replace(/\/index$/, "");
192
+ return canonicalizePath("/api/" + sub);
193
+ }
194
+
195
+ /**
196
+ * Extract Next.js Pages Router API routes using RepoIndex
197
+ * @param {import('./repo-index').RepoIndex} index
198
+ * @param {Object} stats
199
+ * @returns {Array}
200
+ */
201
+ function extractNextPagesRoutes(index, stats) {
202
+ const pagesFiles = index.files.filter(f =>
203
+ f.rel.includes("/pages/api/") &&
204
+ /\.(ts|tsx|js|jsx)$/.test(f.rel) &&
205
+ !f.rel.includes("/_")
206
+ );
207
+
208
+ const out = [];
209
+
210
+ for (const file of pagesFiles) {
211
+ const routePath = nextPagesApiPathFromRel(file.rel);
212
+ if (!routePath) continue;
213
+
214
+ const content = index.getContent(file.abs);
215
+ if (!content) continue;
216
+
217
+ const { ast, error } = globalASTCache.parse(content, file.abs);
218
+ if (error || !ast) {
219
+ stats.parseErrors++;
220
+ continue;
221
+ }
222
+
223
+ // Check for export default
224
+ let hasDefaultExport = false;
225
+ try {
226
+ traverse(ast, {
227
+ ExportDefaultDeclaration(p) {
228
+ hasDefaultExport = true;
229
+ p.stop();
230
+ },
231
+ });
232
+ } catch {
233
+ stats.parseErrors++;
234
+ continue;
235
+ }
236
+
237
+ if (!hasDefaultExport) continue;
238
+
239
+ out.push({
240
+ method: "*",
241
+ path: routePath,
242
+ handler: file.rel,
243
+ confidence: "med",
244
+ framework: "next",
245
+ evidence: [],
246
+ });
247
+ }
248
+
249
+ return out;
250
+ }
251
+
252
+ // ---------- Client Refs (fetch/axios/useSWR/useQuery) ----------
253
+
254
+ function isAxiosMember(node) {
255
+ return (
256
+ t.isMemberExpression(node) &&
257
+ t.isIdentifier(node.object) &&
258
+ t.isIdentifier(node.property) &&
259
+ ["get", "post", "put", "patch", "delete"].includes(node.property.name)
260
+ );
261
+ }
262
+
263
+ function extractUrlLike(node) {
264
+ if (t.isStringLiteral(node)) return { url: node.value, confidence: "high", note: "string" };
265
+ if (t.isTemplateLiteral(node) && node.expressions.length === 0) {
266
+ return { url: node.quasis.map((q) => q.value.cooked || "").join(""), confidence: "high", note: "template_static" };
267
+ }
268
+ if (t.isTemplateLiteral(node) && node.quasis.length >= 1) {
269
+ const start = node.quasis[0]?.value?.cooked || "";
270
+ if (!start.startsWith("/")) return null;
271
+ let built = "";
272
+ for (let i = 0; i < node.quasis.length; i++) {
273
+ built += node.quasis[i].value.cooked || "";
274
+ if (i < node.expressions.length) {
275
+ const expr = node.expressions[i];
276
+ if (t.isIdentifier(expr)) built += `:${expr.name}`;
277
+ else built += "*";
278
+ }
279
+ }
280
+ return { url: built, confidence: "med", note: "template_dynamic" };
281
+ }
282
+ if (t.isBinaryExpression(node, { operator: "+" })) {
283
+ if (t.isStringLiteral(node.left) && node.left.value.startsWith("/")) {
284
+ const left = node.left.value;
285
+ let right = "";
286
+ if (t.isStringLiteral(node.right)) right = node.right.value;
287
+ else if (t.isIdentifier(node.right)) right = `:${node.right.name}`;
288
+ else right = "*";
289
+ return { url: left + right, confidence: "low", note: "concat" };
290
+ }
291
+ }
292
+ return null;
293
+ }
294
+
295
+ function extractFetchMethodFromOptions(node) {
296
+ if (!t.isObjectExpression(node)) return "*";
297
+ for (const prop of node.properties) {
298
+ if (!t.isObjectProperty(prop)) continue;
299
+ const key = t.isIdentifier(prop.key) ? prop.key.name : t.isStringLiteral(prop.key) ? prop.key.value : null;
300
+ if (key === "method" && t.isStringLiteral(prop.value)) return canonicalizeMethod(prop.value.value);
301
+ }
302
+ return "*";
303
+ }
304
+
305
+ function extractAxiosConfig(node) {
306
+ if (!t.isObjectExpression(node)) return null;
307
+ let urlNode = null;
308
+ let methodNode = null;
309
+ for (const prop of node.properties) {
310
+ if (!t.isObjectProperty(prop)) continue;
311
+ const key = t.isIdentifier(prop.key) ? prop.key.name : t.isStringLiteral(prop.key) ? prop.key.value : null;
312
+ if (key === "url") urlNode = prop.value;
313
+ if (key === "method") methodNode = prop.value;
314
+ }
315
+ const urlInfo = urlNode ? extractUrlLike(urlNode) : null;
316
+ if (!urlInfo) return null;
317
+ const method = methodNode && t.isStringLiteral(methodNode) ? canonicalizeMethod(methodNode.value) : "*";
318
+ return { method, urlInfo };
319
+ }
320
+
321
+ /**
322
+ * Extract client route refs using RepoIndex (prefiltered)
323
+ * @param {import('./repo-index').RepoIndex} index
324
+ * @param {Object} stats
325
+ * @returns {Array}
326
+ */
327
+ function extractClientRefs(index, stats) {
328
+ // Use token prefilter - only scan files that contain fetch/axios/useSWR/useQuery
329
+ const candidateAbs = index.getByAnyToken(["fetch(", "axios", "useSWR", "useQuery"]);
330
+
331
+ // Filter to JS/TS files only
332
+ const jsExtensions = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
333
+ const candidates = candidateAbs.filter(abs => {
334
+ const ext = path.extname(abs).toLowerCase();
335
+ return jsExtensions.has(ext);
336
+ });
337
+
338
+ // Filter out test files
339
+ const files = candidates.filter(abs => {
340
+ const rel = index.relPath(abs);
341
+ return !rel.includes("/test/") &&
342
+ !rel.includes("/tests/") &&
343
+ !rel.includes("/__tests__/") &&
344
+ !rel.includes(".test.") &&
345
+ !rel.includes(".spec.") &&
346
+ !rel.includes("/mocks/") &&
347
+ !rel.includes("/fixtures/");
348
+ });
349
+
350
+ const out = [];
351
+
352
+ for (const fileAbs of files) {
353
+ const fileRel = index.relPath(fileAbs);
354
+ const content = index.getContent(fileAbs);
355
+ if (!content) continue;
356
+
357
+ const { ast, error } = globalASTCache.parse(content, fileAbs);
358
+ if (error || !ast) {
359
+ stats.parseErrors++;
360
+ continue;
361
+ }
362
+
363
+ try {
364
+ traverse(ast, {
365
+ CallExpression(p) {
366
+ const callee = p.node.callee;
367
+
368
+ // fetch(url, opts)
369
+ if (t.isIdentifier(callee, { name: "fetch" })) {
370
+ const a0 = p.node.arguments[0];
371
+ const a1 = p.node.arguments[1];
372
+ const urlInfo = extractUrlLike(a0);
373
+ if (!urlInfo) return;
374
+ const url = urlInfo.url;
375
+ if (!url.startsWith("/")) return;
376
+ const method = extractFetchMethodFromOptions(a1);
377
+ const ev = evidenceFromContent(content, fileRel, p.node.loc, `Client fetch(${urlInfo.note}) "${stripQueryHash(url)}"`);
378
+ out.push({
379
+ method,
380
+ path: canonicalizePath(url),
381
+ source: fileRel,
382
+ confidence: urlInfo.confidence,
383
+ kind: "fetch",
384
+ evidence: ev ? [ev] : [],
385
+ });
386
+ return;
387
+ }
388
+
389
+ // axios.get("/api/x") etc
390
+ if (isAxiosMember(callee)) {
391
+ const verb = callee.property.name.toUpperCase();
392
+ const a0 = p.node.arguments[0];
393
+ const urlInfo = extractUrlLike(a0);
394
+ if (!urlInfo) return;
395
+ const url = urlInfo.url;
396
+ if (!url.startsWith("/")) return;
397
+ const ev = evidenceFromContent(content, fileRel, p.node.loc, `Client axios.${verb.toLowerCase()}(${urlInfo.note}) "${stripQueryHash(url)}"`);
398
+ out.push({
399
+ method: canonicalizeMethod(verb),
400
+ path: canonicalizePath(url),
401
+ source: fileRel,
402
+ confidence: urlInfo.confidence,
403
+ kind: "axios_member",
404
+ evidence: ev ? [ev] : [],
405
+ });
406
+ return;
407
+ }
408
+
409
+ // axios({ url, method })
410
+ if (t.isIdentifier(callee, { name: "axios" })) {
411
+ const a0 = p.node.arguments[0];
412
+ const cfg = extractAxiosConfig(a0);
413
+ if (!cfg) return;
414
+ const url = cfg.urlInfo.url;
415
+ if (!url.startsWith("/")) return;
416
+ const ev = evidenceFromContent(content, fileRel, p.node.loc, `Client axios(config:${cfg.urlInfo.note}) "${stripQueryHash(url)}"`);
417
+ out.push({
418
+ method: cfg.method,
419
+ path: canonicalizePath(url),
420
+ source: fileRel,
421
+ confidence: cfg.urlInfo.confidence === "high" ? "high" : "med",
422
+ kind: "axios_config",
423
+ evidence: ev ? [ev] : [],
424
+ });
425
+ return;
426
+ }
427
+
428
+ // useSWR("/api/user", fetcher)
429
+ if (t.isIdentifier(callee, { name: "useSWR" })) {
430
+ const a0 = p.node.arguments[0];
431
+ const urlInfo = extractUrlLike(a0);
432
+ if (!urlInfo) return;
433
+ const url = urlInfo.url;
434
+ if (!url.startsWith("/")) return;
435
+ const ev = evidenceFromContent(content, fileRel, p.node.loc, `Client useSWR(${urlInfo.note}) "${stripQueryHash(url)}"`);
436
+ out.push({
437
+ method: "GET",
438
+ path: canonicalizePath(url),
439
+ source: fileRel,
440
+ confidence: urlInfo.confidence,
441
+ kind: "useSWR",
442
+ evidence: ev ? [ev] : [],
443
+ });
444
+ return;
445
+ }
446
+
447
+ // useQuery
448
+ if (t.isIdentifier(callee, { name: "useQuery" })) {
449
+ const a0 = p.node.arguments[0];
450
+ if (t.isObjectExpression(a0)) {
451
+ for (const prop of a0.properties) {
452
+ if (!t.isObjectProperty(prop)) continue;
453
+ if (!t.isIdentifier(prop.key, { name: "queryFn" })) continue;
454
+ const fn = prop.value;
455
+ if (t.isArrowFunctionExpression(fn) || t.isFunctionExpression(fn)) {
456
+ const fnCode = content.slice(fn.start, fn.end);
457
+ const urlMatch = fnCode.match(/["'`](\/api\/[^"'`]+)["'`]/);
458
+ if (urlMatch) {
459
+ const url = urlMatch[1].split("?")[0].split("#")[0];
460
+ const ev = evidenceFromContent(content, fileRel, p.node.loc, `Client useQuery(queryFn) "${url}"`);
461
+ out.push({
462
+ method: "GET",
463
+ path: canonicalizePath(url),
464
+ source: fileRel,
465
+ confidence: "low",
466
+ kind: "useQuery",
467
+ evidence: ev ? [ev] : [],
468
+ });
469
+ }
470
+ }
471
+ }
472
+ }
473
+ }
474
+ },
475
+ });
476
+ } catch {
477
+ stats.parseErrors++;
478
+ }
479
+ }
480
+
481
+ return out;
482
+ }
483
+
484
+ // ---------- Fastify Routes ----------
485
+
486
+ const FASTIFY_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "all"]);
487
+
488
+ function isFastifyMethod(name) {
489
+ return FASTIFY_METHODS.has(name);
490
+ }
491
+
492
+ /**
493
+ * Extract Fastify routes using RepoIndex
494
+ * @param {import('./repo-index').RepoIndex} index
495
+ * @param {string} entryAbs - Entry file absolute path
496
+ * @param {Object} stats
497
+ * @returns {{ routes: Array, gaps: Array }}
498
+ */
499
+ function extractFastifyRoutes(index, entryAbs, stats) {
500
+ const seen = new Set();
501
+ const routes = [];
502
+ const gaps = [];
503
+
504
+ function scanFile(fileAbs, prefix) {
505
+ if (!fileAbs || seen.has(fileAbs)) return;
506
+ seen.add(fileAbs);
507
+
508
+ const fileRel = index.relPath(fileAbs);
509
+ const content = index.getContent(fileAbs);
510
+ if (!content) return;
511
+
512
+ const { ast, error } = globalASTCache.parse(content, fileAbs);
513
+ if (error || !ast) {
514
+ stats.parseErrors++;
515
+ return;
516
+ }
517
+
518
+ const fastifyNames = new Set(["fastify"]);
519
+ const _rawConstInits = new Map();
520
+ const _constStrings = new Map();
521
+ const _localPlugins = new Map();
522
+
523
+ function evalStaticString(node, depth = 0) {
524
+ if (!node || depth > 6) return null;
525
+ if (t.isStringLiteral(node)) return node.value;
526
+ if (t.isTemplateLiteral(node) && node.expressions.length === 0) {
527
+ return node.quasis.map((q) => q.value.cooked || "").join("");
528
+ }
529
+ if (t.isIdentifier(node)) {
530
+ const name = node.name;
531
+ if (_constStrings.has(name)) return _constStrings.get(name);
532
+ const init = _rawConstInits.get(name);
533
+ if (!init) return null;
534
+ const v = evalStaticString(init, depth + 1);
535
+ if (typeof v === "string" && v.length <= 4096) {
536
+ _constStrings.set(name, v);
537
+ return v;
538
+ }
539
+ return null;
540
+ }
541
+ if (t.isBinaryExpression(node, { operator: "+" })) {
542
+ const l = evalStaticString(node.left, depth + 1);
543
+ const r = evalStaticString(node.right, depth + 1);
544
+ if (typeof l !== "string" || typeof r !== "string") return null;
545
+ return (l + r).length <= 4096 ? l + r : null;
546
+ }
547
+ return null;
548
+ }
549
+
550
+ function extractPrefixFromOpts(node) {
551
+ if (!t.isObjectExpression(node)) return null;
552
+ for (const p of node.properties) {
553
+ if (!t.isObjectProperty(p)) continue;
554
+ const key = t.isIdentifier(p.key) ? p.key.name : t.isStringLiteral(p.key) ? p.key.value : null;
555
+ if (key !== "prefix") continue;
556
+ return evalStaticString(p.value);
557
+ }
558
+ return null;
559
+ }
560
+
561
+ function extractRouteObject(objExpr) {
562
+ let url = null;
563
+ let methods = [];
564
+ let hasHandler = false;
565
+ const hooks = [];
566
+ for (const p of objExpr.properties) {
567
+ if (!t.isObjectProperty(p)) continue;
568
+ const key = t.isIdentifier(p.key) ? p.key.name : t.isStringLiteral(p.key) ? p.key.value : null;
569
+ if (!key) continue;
570
+ if (key === "url") {
571
+ const u = evalStaticString(p.value);
572
+ if (typeof u === "string") url = u;
573
+ }
574
+ if (key === "method") {
575
+ if (t.isStringLiteral(p.value)) methods = [p.value.value];
576
+ if (t.isArrayExpression(p.value)) {
577
+ methods = p.value.elements.filter((e) => t.isStringLiteral(e)).map((e) => e.value);
578
+ }
579
+ }
580
+ if (key === "handler") hasHandler = true;
581
+ if (["preHandler", "onRequest", "preValidation", "preSerialization"].includes(key)) hooks.push(key);
582
+ }
583
+ return { url, methods, hasHandler, hooks };
584
+ }
585
+
586
+ function unwrapPluginArg(node) {
587
+ if (!node || !t.isCallExpression(node)) return node;
588
+ const calleeName = t.isIdentifier(node.callee)
589
+ ? node.callee.name
590
+ : t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.property)
591
+ ? node.callee.property.name
592
+ : null;
593
+ if (!calleeName || !/^(fp|fastifyPlugin|plugin|fastifyPluginify)$/i.test(calleeName)) return node;
594
+ return node.arguments?.[0] || node;
595
+ }
596
+
597
+ function resolveRelativeModule(fromFileAbs, spec) {
598
+ if (!spec || (!spec.startsWith("./") && !spec.startsWith("../"))) return null;
599
+ const base = path.resolve(path.dirname(fromFileAbs), spec);
600
+ const candidates = [base, base + ".ts", base + ".tsx", base + ".js", base + ".jsx",
601
+ path.join(base, "index.ts"), path.join(base, "index.tsx"), path.join(base, "index.js"), path.join(base, "index.jsx")];
602
+ for (const c of candidates) {
603
+ if (index.files.some(f => f.abs === c)) return c;
604
+ // Fall back to fs check for files not in index
605
+ try { require("fs").statSync(c).isFile() && candidates.push(c); } catch {}
606
+ }
607
+ for (const c of candidates) {
608
+ try { if (require("fs").statSync(c).isFile()) return c; } catch {}
609
+ }
610
+ return null;
611
+ }
612
+
613
+ function extractRequireOrImportSpec(node) {
614
+ if (t.isCallExpression(node) && t.isIdentifier(node.callee, { name: "require" })) {
615
+ const a0 = node.arguments?.[0];
616
+ if (t.isStringLiteral(a0)) return a0.value;
617
+ }
618
+ if (t.isCallExpression(node) && node.callee?.type === "Import") {
619
+ const a0 = node.arguments?.[0];
620
+ if (t.isStringLiteral(a0)) return a0.value;
621
+ }
622
+ if (t.isAwaitExpression(node)) return extractRequireOrImportSpec(node.argument);
623
+ return null;
624
+ }
625
+
626
+ try {
627
+ traverse(ast, {
628
+ FunctionDeclaration(p) {
629
+ if (t.isIdentifier(p.node.id)) _localPlugins.set(p.node.id.name, p.node);
630
+ },
631
+ VariableDeclarator(p) {
632
+ if (!t.isIdentifier(p.node.id)) return;
633
+ const id = p.node.id.name;
634
+ const init = p.node.init;
635
+ if (!init) return;
636
+ _rawConstInits.set(id, init);
637
+ if (t.isFunctionExpression(init) || t.isArrowFunctionExpression(init)) _localPlugins.set(id, init);
638
+ if (t.isCallExpression(init) && t.isIdentifier(init.callee)) {
639
+ if (init.callee.name === "Fastify" || init.callee.name === "fastify") fastifyNames.add(id);
640
+ }
641
+ if (t.isCallExpression(init) && t.isCallExpression(init.callee)) {
642
+ const inner = init.callee;
643
+ if (t.isIdentifier(inner.callee, { name: "require" }) && t.isStringLiteral(inner.arguments?.[0], { value: "fastify" })) {
644
+ fastifyNames.add(id);
645
+ }
646
+ }
647
+ },
648
+ });
649
+ } catch {}
650
+
651
+ function resolveImportSpecForLocal(localName) {
652
+ let spec = null;
653
+ try {
654
+ traverse(ast, {
655
+ ImportDeclaration(ip) {
656
+ for (const s of ip.node.specifiers) {
657
+ if ((t.isImportDefaultSpecifier(s) || t.isImportSpecifier(s)) && s.local.name === localName) {
658
+ spec = ip.node.source.value;
659
+ }
660
+ }
661
+ },
662
+ VariableDeclarator(vp) {
663
+ if (!t.isIdentifier(vp.node.id) || vp.node.id.name !== localName) return;
664
+ const init = vp.node.init;
665
+ if (!t.isCallExpression(init) || !t.isIdentifier(init.callee, { name: "require" })) return;
666
+ const a0 = init.arguments[0];
667
+ if (t.isStringLiteral(a0)) spec = a0.value;
668
+ },
669
+ });
670
+ } catch {}
671
+ return spec;
672
+ }
673
+
674
+ try {
675
+ traverse(ast, {
676
+ CallExpression(p) {
677
+ const callee = p.node.callee;
678
+ if (!t.isMemberExpression(callee)) return;
679
+ if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property)) return;
680
+ const obj = callee.object.name;
681
+ const prop = callee.property.name;
682
+ if (!fastifyNames.has(obj)) return;
683
+
684
+ if (isFastifyMethod(prop)) {
685
+ const routeStr = evalStaticString(p.node.arguments[0]);
686
+ if (!routeStr) return;
687
+ const fullPath = joinPaths(prefix, routeStr);
688
+ const ev = evidenceFromContent(content, fileRel, p.node.loc, `Fastify ${prop.toUpperCase()}("${routeStr}")`);
689
+ routes.push({
690
+ method: canonicalizeMethod(prop),
691
+ path: fullPath,
692
+ handler: fileRel,
693
+ confidence: "med",
694
+ framework: "fastify",
695
+ evidence: ev ? [ev] : [],
696
+ });
697
+ return;
698
+ }
699
+
700
+ if (prop === "route") {
701
+ const arg0 = p.node.arguments[0];
702
+ if (!t.isObjectExpression(arg0)) return;
703
+ const r = extractRouteObject(arg0);
704
+ if (!r.url) return;
705
+ const fullPath = joinPaths(prefix, r.url);
706
+ const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
707
+ const ev = evidenceFromContent(content, fileRel, p.node.loc, `Fastify.route({ url: "${r.url}" })`);
708
+ for (const m of ms) {
709
+ routes.push({
710
+ method: m,
711
+ path: fullPath,
712
+ handler: fileRel,
713
+ hooks: r.hooks,
714
+ confidence: r.hasHandler ? "med" : "low",
715
+ framework: "fastify",
716
+ evidence: ev ? [ev] : [],
717
+ });
718
+ }
719
+ return;
720
+ }
721
+
722
+ if (prop === "register") {
723
+ const pluginArgRaw = unwrapPluginArg(p.node.arguments[0]);
724
+ const optsArg = p.node.arguments[1];
725
+ const childPrefixRaw = extractPrefixFromOpts(optsArg);
726
+ const childPrefix = childPrefixRaw ? joinPaths(prefix, childPrefixRaw) : prefix;
727
+
728
+ let pluginFn = null;
729
+ const localIdentName = t.isIdentifier(pluginArgRaw) ? pluginArgRaw.name : null;
730
+ if (t.isFunctionExpression(pluginArgRaw) || t.isArrowFunctionExpression(pluginArgRaw)) pluginFn = pluginArgRaw;
731
+ if (!pluginFn && localIdentName) pluginFn = _localPlugins.get(localIdentName) || null;
732
+
733
+ if (pluginFn) {
734
+ const param0 = pluginFn.params?.[0];
735
+ const innerName = t.isIdentifier(param0) ? param0.name : "fastify";
736
+ const bodyNode = pluginFn.body;
737
+ if (t.isBlockStatement(bodyNode)) {
738
+ try {
739
+ traverse(bodyNode, {
740
+ CallExpression(pp) {
741
+ const c = pp.node.callee;
742
+ if (!t.isMemberExpression(c) || !t.isIdentifier(c.object) || !t.isIdentifier(c.property)) return;
743
+ if (c.object.name !== innerName) return;
744
+ const pr = c.property.name;
745
+ if (isFastifyMethod(pr)) {
746
+ const rs = evalStaticString(pp.node.arguments[0]);
747
+ if (!rs) return;
748
+ const fp = joinPaths(childPrefix, rs);
749
+ const ev = evidenceFromContent(content, fileRel, pp.node.loc, `Fastify plugin ${pr.toUpperCase()}("${rs}")`);
750
+ routes.push({ method: canonicalizeMethod(pr), path: fp, handler: fileRel, confidence: "med", framework: "fastify", evidence: ev ? [ev] : [] });
751
+ }
752
+ if (pr === "route") {
753
+ const a0 = pp.node.arguments[0];
754
+ if (!t.isObjectExpression(a0)) return;
755
+ const r = extractRouteObject(a0);
756
+ if (!r.url) return;
757
+ const fp = joinPaths(childPrefix, r.url);
758
+ const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
759
+ const ev = evidenceFromContent(content, fileRel, pp.node.loc, `Fastify plugin route("${r.url}")`);
760
+ for (const m of ms) {
761
+ routes.push({ method: m, path: fp, handler: fileRel, confidence: "med", framework: "fastify", evidence: ev ? [ev] : [] });
762
+ }
763
+ }
764
+ },
765
+ }, p.scope, p);
766
+ } catch {}
767
+ }
768
+ if (localIdentName) return;
769
+ return;
770
+ }
771
+
772
+ const dynSpec = extractRequireOrImportSpec(pluginArgRaw);
773
+ if (dynSpec) {
774
+ const resolved = resolveRelativeModule(fileAbs, dynSpec);
775
+ if (!resolved) {
776
+ gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec: dynSpec });
777
+ return;
778
+ }
779
+ scanFile(resolved, childPrefix);
780
+ return;
781
+ }
782
+
783
+ if (t.isIdentifier(pluginArgRaw)) {
784
+ const localName = pluginArgRaw.name;
785
+ const spec = resolveImportSpecForLocal(localName);
786
+ if (!spec) {
787
+ gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, name: localName });
788
+ return;
789
+ }
790
+ const resolved = resolveRelativeModule(fileAbs, spec);
791
+ if (!resolved) {
792
+ gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec });
793
+ return;
794
+ }
795
+ scanFile(resolved, childPrefix);
796
+ return;
797
+ }
798
+
799
+ gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, note: "register() plugin not statically resolvable" });
800
+ }
801
+ },
802
+ });
803
+ } catch {
804
+ stats.parseErrors++;
805
+ }
806
+ }
807
+
808
+ scanFile(entryAbs, "/");
809
+ return { routes, gaps };
810
+ }
811
+
812
+ /**
813
+ * Detect Fastify entry files using RepoIndex
814
+ * @param {import('./repo-index').RepoIndex} index
815
+ * @returns {string[]} - Array of absolute paths
816
+ */
817
+ function detectFastifyEntries(index) {
818
+ if (!index.hasFramework("fastify")) return [];
819
+
820
+ // Use token prefilter
821
+ const fastifyFiles = index.getByAnyToken(["fastify", "Fastify"]);
822
+
823
+ const fastifySignal = /\b(Fastify\s*\(|fastify\s*\(|require\(['"]fastify['"]\)|from\s+['"]fastify['"])\b/;
824
+ const listenSignal = /\.\s*(listen|ready)\s*\(/;
825
+
826
+ const entries = [];
827
+
828
+ for (const fileAbs of fastifyFiles) {
829
+ const content = index.getContent(fileAbs);
830
+ if (!content) continue;
831
+ if (fastifySignal.test(content) && listenSignal.test(content)) {
832
+ entries.push(fileAbs);
833
+ }
834
+ }
835
+
836
+ return Array.from(new Set(entries));
837
+ }
838
+
839
+ module.exports = {
840
+ extractNextAppRoutes,
841
+ extractNextPagesRoutes,
842
+ extractClientRefs,
843
+ extractFastifyRoutes,
844
+ detectFastifyEntries,
845
+ // Helpers exported for testing
846
+ canonicalizeMethod,
847
+ canonicalizePath,
848
+ joinPaths,
849
+ };