@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
@@ -18,6 +18,19 @@ const { buildEnforcementTruth } = require("./enforcement");
18
18
  // Multi-framework route detection v2
19
19
  const { resolveAllRoutes, detectFrameworks } = require("./route-detection");
20
20
 
21
+ // ---------- constants ----------
22
+ const IGNORE_GLOBS = [
23
+ "**/node_modules/**",
24
+ "**/.next/**",
25
+ "**/dist/**",
26
+ "**/build/**",
27
+ "**/.turbo/**",
28
+ "**/.git/**",
29
+ "**/.vibecheck/**",
30
+ ];
31
+
32
+ const CODE_FILE_GLOBS = ["**/*.{ts,tsx,js,jsx}"];
33
+
21
34
  // ---------- helpers ----------
22
35
  function sha256(text) {
23
36
  return "sha256:" + crypto.createHash("sha256").update(text).digest("hex");
@@ -29,15 +42,43 @@ function canonicalizeMethod(m) {
29
42
  return u;
30
43
  }
31
44
 
45
+ function stripQueryHash(s) {
46
+ const v = String(s || "");
47
+ const q = v.indexOf("?");
48
+ const h = v.indexOf("#");
49
+ const cut = (q === -1 ? h : (h === -1 ? q : Math.min(q, h)));
50
+ return cut === -1 ? v : v.slice(0, cut);
51
+ }
52
+
53
+ /**
54
+ * Canonical path rules:
55
+ * - ensure leading slash
56
+ * - collapse multiple slashes
57
+ * - strip query/hash
58
+ * - normalize Next dynamic segments:
59
+ * [[...slug]] -> *slug?
60
+ * [...slug] -> *slug
61
+ * [id] -> :id
62
+ */
32
63
  function canonicalizePath(p) {
33
- let s = String(p || "").trim();
64
+ let s = stripQueryHash(String(p || "").trim());
65
+
66
+ // If someone passed a full URL, only keep pathname-like portion if possible.
67
+ // (We still require local routes to start with "/" for client refs.)
68
+ const protoIdx = s.indexOf("://");
69
+ if (protoIdx !== -1) {
70
+ // attempt to strip scheme+host
71
+ const slashAfterHost = s.indexOf("/", protoIdx + 3);
72
+ s = slashAfterHost === -1 ? "/" : s.slice(slashAfterHost);
73
+ }
74
+
34
75
  if (!s.startsWith("/")) s = "/" + s;
35
76
  s = s.replace(/\/+/g, "/");
36
77
 
37
- // Next dynamic segments
78
+ // Next dynamic segments (filesystem style)
38
79
  s = s.replace(/\[\[\.{3}([^\]]+)\]\]/g, "*$1?"); // [[...slug]] -> *slug?
39
- s = s.replace(/\[\.{3}([^\]]+)\]/g, "*$1"); // [...slug] -> *slug
40
- s = s.replace(/\[([^\]]+)\]/g, ":$1"); // [id] -> :id
80
+ s = s.replace(/\[\.{3}([^\]]+)\]/g, "*$1"); // [...slug] -> *slug
81
+ s = s.replace(/\[([^\]]+)\]/g, ":$1"); // [id] -> :id
41
82
 
42
83
  if (s.length > 1) s = s.replace(/\/$/, "");
43
84
  return s;
@@ -51,12 +92,46 @@ function joinPaths(prefix, p) {
51
92
  return canonicalizePath(a + "/" + b);
52
93
  }
53
94
 
54
- function parseFile(code) {
55
- return parser.parse(code, { sourceType: "unambiguous", plugins: ["typescript", "jsx"] });
95
+ function parseFile(code, fileAbsForErrors) {
96
+ // Be permissive: production repos contain decorators, top-level await, etc.
97
+ return parser.parse(code, {
98
+ sourceType: "unambiguous",
99
+ errorRecovery: true,
100
+ allowImportExportEverywhere: true,
101
+ plugins: [
102
+ "typescript",
103
+ "jsx",
104
+ "dynamicImport",
105
+ "importMeta",
106
+ "topLevelAwait",
107
+ "classProperties",
108
+ "classPrivateProperties",
109
+ "classPrivateMethods",
110
+ "optionalChaining",
111
+ "nullishCoalescingOperator",
112
+ "decorators-legacy",
113
+ ],
114
+ sourceFilename: fileAbsForErrors || undefined,
115
+ });
56
116
  }
57
117
 
118
+ // File cache for performance (avoids reading the same file multiple times)
119
+ const _FILE_CACHE = new Map();
120
+
58
121
  function safeRead(fileAbs) {
59
- return fs.readFileSync(fileAbs, "utf8");
122
+ if (_FILE_CACHE.has(fileAbs)) return _FILE_CACHE.get(fileAbs);
123
+ try {
124
+ const content = fs.readFileSync(fileAbs, "utf8");
125
+ _FILE_CACHE.set(fileAbs, content);
126
+ return content;
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ // Clear cache to free memory after a scan (important for long-running processes)
133
+ function clearCache() {
134
+ _FILE_CACHE.clear();
60
135
  }
61
136
 
62
137
  function ensureDir(p) {
@@ -65,68 +140,154 @@ function ensureDir(p) {
65
140
 
66
141
  function evidenceFromLoc({ fileAbs, fileRel, loc, reason }) {
67
142
  if (!loc) return null;
68
- const lines = fs.readFileSync(fileAbs, "utf8").split(/\r?\n/);
143
+ const code = safeRead(fileAbs);
144
+ if (!code) return null;
145
+
146
+ const lines = code.split(/\r?\n/);
69
147
  const start = Math.max(1, loc.start?.line || 1);
70
148
  const end = Math.max(start, loc.end?.line || start);
71
149
  const snippet = lines.slice(start - 1, end).join("\n");
150
+
72
151
  return {
73
152
  id: `ev_${crypto.randomBytes(4).toString("hex")}`,
74
153
  file: fileRel,
75
154
  lines: `${start}-${end}`,
76
155
  snippetHash: sha256(snippet),
77
- reason
156
+ reason,
78
157
  };
79
158
  }
80
159
 
160
+ function normalizeRel(repoRoot, fileAbs) {
161
+ return path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
162
+ }
163
+
164
+ function scoreConfidence(c) {
165
+ if (c === "high") return 3;
166
+ if (c === "med") return 2;
167
+ if (c === "low") return 1;
168
+ return 0;
169
+ }
170
+
171
+ function isRouteGroupSegment(seg) {
172
+ // Next route group: (group)
173
+ return seg.startsWith("(") && seg.endsWith(")");
174
+ }
175
+
176
+ function isParallelSegment(seg) {
177
+ // Next parallel routes: @slot
178
+ return seg.startsWith("@");
179
+ }
180
+
81
181
  // ---------- Next: app router API ----------
82
- const HTTP_EXPORTS = new Set(["GET","POST","PUT","PATCH","DELETE","OPTIONS","HEAD"]);
182
+ const HTTP_EXPORTS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]);
183
+
184
+ function nextAppApiPathFromRel(fileRel) {
185
+ const idx = fileRel.indexOf("app/api/");
186
+ if (idx === -1) return null;
187
+
188
+ let sub = fileRel.slice(idx + "app/api/".length);
189
+
190
+ // route.ts / route.js / route.tsx / route.jsx
191
+ sub = sub.replace(/\/route\.(ts|tsx|js|jsx)$/, "");
83
192
 
84
- async function resolveNextAppApiRoutes(repoRoot) {
85
- const files = await fg(["**/app/api/**/route.@(ts|js)"], {
193
+ // remove route groups + parallel segments from the filesystem path
194
+ const parts = sub.split("/").filter(Boolean).filter((seg) => !isRouteGroupSegment(seg) && !isParallelSegment(seg));
195
+ sub = parts.join("/");
196
+
197
+ return canonicalizePath("/api/" + sub);
198
+ }
199
+
200
+ async function resolveNextAppApiRoutes(repoRoot, stats) {
201
+ const files = await fg(["**/app/api/**/route.@(ts|tsx|js|jsx)"], {
86
202
  cwd: repoRoot,
87
203
  absolute: true,
88
- ignore: ["**/node_modules/**","**/.next/**","**/dist/**","**/build/**"]
204
+ ignore: IGNORE_GLOBS,
89
205
  });
90
206
 
91
207
  const out = [];
92
208
 
93
209
  for (const fileAbs of files) {
94
- const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
95
- const idx = fileRel.indexOf("app/api/");
96
- const sub = fileRel.slice(idx + "app/api/".length).replace(/\/route\.(ts|js)$/, "");
97
- const routePath = canonicalizePath("/api/" + sub);
210
+ const fileRel = normalizeRel(repoRoot, fileAbs);
211
+ const routePath = nextAppApiPathFromRel(fileRel);
212
+ if (!routePath) continue;
98
213
 
99
214
  const code = safeRead(fileAbs);
215
+ if (!code) continue;
216
+
100
217
  let ast;
101
- try { ast = parseFile(code); } catch { continue; }
218
+ try {
219
+ ast = parseFile(code, fileAbs);
220
+ } catch {
221
+ stats.parseErrors++;
222
+ continue;
223
+ }
102
224
 
103
225
  const methods = [];
104
- traverse(ast, {
105
- ExportNamedDeclaration(p) {
106
- const decl = p.node.declaration;
107
- if (t.isFunctionDeclaration(decl) && decl.id?.name) {
108
- const n = decl.id.name.toUpperCase();
109
- if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: decl.loc });
110
- }
111
- }
112
- });
226
+
227
+ try {
228
+ traverse(ast, {
229
+ // export async function GET() {}
230
+ ExportNamedDeclaration(p) {
231
+ const decl = p.node.declaration;
232
+
233
+ if (t.isFunctionDeclaration(decl) && decl.id?.name) {
234
+ const n = decl.id.name.toUpperCase();
235
+ if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: decl.loc, why: "export function" });
236
+ }
237
+
238
+ // export const GET = async () => {}
239
+ if (t.isVariableDeclaration(decl)) {
240
+ for (const d of decl.declarations) {
241
+ if (!t.isVariableDeclarator(d)) continue;
242
+ if (!t.isIdentifier(d.id)) continue;
243
+ const n = d.id.name.toUpperCase();
244
+ if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: d.loc || decl.loc, why: "export const" });
245
+ }
246
+ }
247
+
248
+ // export { GET } from "./handler"
249
+ for (const s of p.node.specifiers || []) {
250
+ if (!t.isExportSpecifier(s)) continue;
251
+ if (!t.isIdentifier(s.exported)) continue;
252
+ const n = s.exported.name.toUpperCase();
253
+ if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: s.loc || p.node.loc, why: "export re-export" });
254
+ }
255
+ },
256
+ });
257
+ } catch {
258
+ // Babel traverse can fail on some edge-case files; skip them
259
+ stats.parseErrors++;
260
+ continue;
261
+ }
113
262
 
114
263
  if (methods.length === 0) {
115
- out.push({ method: "*", path: routePath, handler: fileRel, confidence: "low", evidence: [] });
264
+ // Still include route.ts, but with "*" and low confidence to avoid missing-route spam.
265
+ out.push({
266
+ method: "*",
267
+ path: routePath,
268
+ handler: fileRel,
269
+ confidence: "low",
270
+ framework: "next",
271
+ evidence: [],
272
+ });
116
273
  continue;
117
274
  }
118
275
 
119
276
  for (const m of methods) {
120
277
  const ev = evidenceFromLoc({
121
- fileAbs, fileRel, loc: m.loc,
122
- reason: `Next app router export ${m.method}`
278
+ fileAbs,
279
+ fileRel,
280
+ loc: m.loc,
281
+ reason: `Next app router ${m.method} (${m.why})`,
123
282
  });
283
+
124
284
  out.push({
125
285
  method: m.method,
126
286
  path: routePath,
127
287
  handler: fileRel,
128
- confidence: "high",
129
- evidence: ev ? [ev] : []
288
+ confidence: m.why === "export re-export" ? "med" : "high",
289
+ framework: "next",
290
+ evidence: ev ? [ev] : [],
130
291
  });
131
292
  }
132
293
  }
@@ -135,51 +296,133 @@ async function resolveNextAppApiRoutes(repoRoot) {
135
296
  }
136
297
 
137
298
  // ---------- Next: pages router API ----------
138
- async function resolveNextPagesApiRoutes(repoRoot) {
139
- const files = await fg(["**/pages/api/**/*.@(ts|js)"], {
299
+ function nextPagesApiPathFromRel(fileRel) {
300
+ const idx = fileRel.indexOf("pages/api/");
301
+ if (idx === -1) return null;
302
+
303
+ let sub = fileRel.slice(idx + "pages/api/".length);
304
+ sub = sub.replace(/\.(ts|tsx|js|jsx)$/, "");
305
+
306
+ // pages/api/foo/index.ts -> /api/foo
307
+ if (sub === "index") sub = "";
308
+ sub = sub.replace(/\/index$/, "");
309
+
310
+ return canonicalizePath("/api/" + sub);
311
+ }
312
+
313
+ async function resolveNextPagesApiRoutes(repoRoot, stats) {
314
+ const files = await fg(["**/pages/api/**/*.@(ts|tsx|js|jsx)"], {
140
315
  cwd: repoRoot,
141
316
  absolute: true,
142
- ignore: ["**/node_modules/**","**/.next/**","**/dist/**","**/build/**"]
317
+ ignore: IGNORE_GLOBS,
143
318
  });
144
319
 
145
320
  const out = [];
321
+
146
322
  for (const fileAbs of files) {
147
- const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
148
- const idx = fileRel.indexOf("pages/api/");
149
- const sub = fileRel.slice(idx + "pages/api/".length).replace(/\.(ts|js)$/, "");
150
- const routePath = canonicalizePath("/api/" + sub);
323
+ const fileRel = normalizeRel(repoRoot, fileAbs);
324
+
325
+ // Skip Next.js special files that aren't API routes (_app, _document, _utils, etc.)
326
+ if (fileRel.includes("/_") && !fileRel.includes("/_next")) continue;
327
+
328
+ const routePath = nextPagesApiPathFromRel(fileRel);
329
+ if (!routePath) continue;
330
+
331
+ const code = safeRead(fileAbs);
332
+ if (!code) continue;
333
+
334
+ // Parse to verify it's actually a route (has export default)
335
+ let ast;
336
+ try {
337
+ ast = parseFile(code, fileAbs);
338
+ } catch {
339
+ stats.parseErrors++;
340
+ continue;
341
+ }
342
+
343
+ // Check for 'export default' (Required for Pages Router API routes)
344
+ // Files without default export are helper files (db.ts, types.ts, utils.ts)
345
+ let hasDefaultExport = false;
346
+ try {
347
+ traverse(ast, {
348
+ ExportDefaultDeclaration(p) {
349
+ hasDefaultExport = true;
350
+ p.stop(); // Found it, stop traversing
351
+ },
352
+ });
353
+ } catch {
354
+ // Traverse failed, skip this file
355
+ stats.parseErrors++;
356
+ continue;
357
+ }
358
+
359
+ if (!hasDefaultExport) continue; // It's a helper file, not an API route
151
360
 
152
361
  out.push({
153
- method: "*",
362
+ method: "*", // Pages router handles all methods in one function
154
363
  path: routePath,
155
364
  handler: fileRel,
156
365
  confidence: "med",
157
- evidence: []
366
+ framework: "next",
367
+ evidence: [],
158
368
  });
159
369
  }
370
+
160
371
  return out;
161
372
  }
162
373
 
163
374
  // ---------- minimal relative module resolver ----------
164
375
  function exists(p) {
165
- try { return fs.statSync(p).isFile(); } catch { return false; }
376
+ try {
377
+ return fs.statSync(p).isFile();
378
+ } catch {
379
+ return false;
380
+ }
166
381
  }
382
+
167
383
  function resolveRelativeModule(fromFileAbs, spec) {
168
384
  if (!spec || (!spec.startsWith("./") && !spec.startsWith("../"))) return null;
385
+
169
386
  const base = path.resolve(path.dirname(fromFileAbs), spec);
170
387
  const candidates = [
171
388
  base,
172
389
  base + ".ts",
390
+ base + ".tsx",
173
391
  base + ".js",
392
+ base + ".jsx",
174
393
  path.join(base, "index.ts"),
175
- path.join(base, "index.js")
394
+ path.join(base, "index.tsx"),
395
+ path.join(base, "index.js"),
396
+ path.join(base, "index.jsx"),
176
397
  ];
398
+
177
399
  for (const c of candidates) if (exists(c)) return c;
178
400
  return null;
179
401
  }
180
402
 
403
+ function extractRequireOrImportSpec(node) {
404
+ // require("./x")
405
+ if (t.isCallExpression(node) && t.isIdentifier(node.callee, { name: "require" })) {
406
+ const a0 = node.arguments && node.arguments[0];
407
+ if (t.isStringLiteral(a0)) return a0.value;
408
+ }
409
+
410
+ // import("./x")
411
+ if (t.isCallExpression(node) && node.callee && node.callee.type === "Import") {
412
+ const a0 = node.arguments && node.arguments[0];
413
+ if (t.isStringLiteral(a0)) return a0.value;
414
+ }
415
+
416
+ // await import("./x")
417
+ if (t.isAwaitExpression(node)) {
418
+ return extractRequireOrImportSpec(node.argument);
419
+ }
420
+
421
+ return null;
422
+ }
423
+
181
424
  // ---------- Fastify route extraction ----------
182
- const FASTIFY_METHODS = new Set(["get","post","put","patch","delete","options","head","all"]);
425
+ const FASTIFY_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "all"]);
183
426
 
184
427
  function isFastifyMethod(name) {
185
428
  return FASTIFY_METHODS.has(name);
@@ -222,18 +465,18 @@ function extractRouteObject(objExpr) {
222
465
  if (key === "method") {
223
466
  if (t.isStringLiteral(p.value)) methods = [p.value.value];
224
467
  if (t.isArrayExpression(p.value)) {
225
- methods = p.value.elements.filter(e => t.isStringLiteral(e)).map(e => e.value);
468
+ methods = p.value.elements.filter((e) => t.isStringLiteral(e)).map((e) => e.value);
226
469
  }
227
470
  }
228
471
 
229
472
  if (key === "handler") hasHandler = true;
230
- if (["preHandler","onRequest","preValidation","preSerialization"].includes(key)) hooks.push(key);
473
+ if (["preHandler", "onRequest", "preValidation", "preSerialization"].includes(key)) hooks.push(key);
231
474
  }
232
475
 
233
476
  return { url, methods, hasHandler, hooks };
234
477
  }
235
478
 
236
- function resolveFastifyRoutes(repoRoot, entryAbs) {
479
+ function resolveFastifyRoutes(repoRoot, entryAbs, stats) {
237
480
  const seen = new Set();
238
481
  const routes = [];
239
482
  const gaps = [];
@@ -242,217 +485,521 @@ function resolveFastifyRoutes(repoRoot, entryAbs) {
242
485
  if (!fileAbs || seen.has(fileAbs)) return;
243
486
  seen.add(fileAbs);
244
487
 
245
- const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
488
+ const fileRel = normalizeRel(repoRoot, fileAbs);
246
489
  const code = safeRead(fileAbs);
490
+ if (!code) return;
247
491
 
248
492
  let ast;
249
- try { ast = parseFile(code); } catch { return; }
493
+ try {
494
+ ast = parseFile(code, fileAbs);
495
+ } catch {
496
+ stats.parseErrors++;
497
+ return;
498
+ }
250
499
 
251
500
  // best-effort: fastify instance identifiers
252
501
  const fastifyNames = new Set(["fastify"]);
253
502
 
254
- traverse(ast, {
255
- VariableDeclarator(p) {
256
- if (!t.isIdentifier(p.node.id)) return;
257
- const id = p.node.id.name;
258
- const init = p.node.init;
259
- if (!init) return;
260
- if (t.isCallExpression(init) && t.isIdentifier(init.callee)) {
261
- const cal = init.callee.name;
262
- if (cal === "Fastify" || cal === "fastify") fastifyNames.add(id);
503
+ // Static string + local-plugin harvesting (cheap, big impact on false positives)
504
+ const _rawConstInits = new Map(); // name -> init node (evaluated lazily)
505
+ const _constStrings = new Map(); // name -> resolved string
506
+ const _localPlugins = new Map(); // name -> Function node
507
+
508
+ function evalStaticString(node, depth = 0) {
509
+ if (!node || depth > 6) return null;
510
+
511
+ if (t.isStringLiteral(node)) return node.value;
512
+
513
+ if (t.isTemplateLiteral(node) && node.expressions.length === 0) {
514
+ return node.quasis.map((q) => q.value.cooked || "").join("");
515
+ }
516
+
517
+ if (t.isIdentifier(node)) {
518
+ const name = node.name;
519
+ if (_constStrings.has(name)) return _constStrings.get(name);
520
+ const init = _rawConstInits.get(name);
521
+ if (!init) return null;
522
+ const v = evalStaticString(init, depth + 1);
523
+ if (typeof v === "string") {
524
+ // cap to avoid pathological blowups
525
+ if (v.length <= 4096) _constStrings.set(name, v);
526
+ return v;
263
527
  }
528
+ return null;
264
529
  }
265
- });
266
530
 
267
- // helper: resolve imports for register(pluginIdent,...)
268
- function resolveImportSpecForLocal(localName) {
269
- let spec = null;
531
+ if (t.isBinaryExpression(node, { operator: "+" })) {
532
+ const l = evalStaticString(node.left, depth + 1);
533
+ const r = evalStaticString(node.right, depth + 1);
534
+ if (typeof l !== "string" || typeof r !== "string") return null;
535
+ const out = l + r;
536
+ return out.length <= 4096 ? out : null;
537
+ }
270
538
 
271
- traverse(ast, {
272
- ImportDeclaration(ip) {
273
- for (const s of ip.node.specifiers) {
274
- if ((t.isImportDefaultSpecifier(s) || t.isImportSpecifier(s)) && s.local.name === localName) {
275
- spec = ip.node.source.value;
276
- }
539
+ return null;
540
+ }
541
+
542
+ function extractPrefixFromOptsV3(node) {
543
+ if (!t.isObjectExpression(node)) return null;
544
+ for (const p of node.properties) {
545
+ if (!t.isObjectProperty(p)) continue;
546
+ const key =
547
+ t.isIdentifier(p.key) ? p.key.name :
548
+ t.isStringLiteral(p.key) ? p.key.value :
549
+ null;
550
+ if (key !== "prefix") continue;
551
+ return evalStaticString(p.value);
552
+ }
553
+ return null;
554
+ }
555
+
556
+ function extractRouteObjectV3(objExpr) {
557
+ let url = null;
558
+ let methods = [];
559
+ let hasHandler = false;
560
+ const hooks = [];
561
+
562
+ for (const p of objExpr.properties) {
563
+ if (!t.isObjectProperty(p)) continue;
564
+
565
+ const key =
566
+ t.isIdentifier(p.key) ? p.key.name :
567
+ t.isStringLiteral(p.key) ? p.key.value :
568
+ null;
569
+ if (!key) continue;
570
+
571
+ if (key === "url") {
572
+ const u = evalStaticString(p.value);
573
+ if (typeof u === "string") url = u;
574
+ }
575
+
576
+ if (key === "method") {
577
+ if (t.isStringLiteral(p.value)) methods = [p.value.value];
578
+ if (t.isArrayExpression(p.value)) {
579
+ methods = p.value.elements.filter((e) => t.isStringLiteral(e)).map((e) => e.value);
277
580
  }
278
- },
279
- VariableDeclarator(vp) {
280
- if (!t.isIdentifier(vp.node.id) || vp.node.id.name !== localName) return;
281
- const init = vp.node.init;
282
- if (!t.isCallExpression(init)) return;
283
- if (!t.isIdentifier(init.callee) || init.callee.name !== "require") return;
284
- const a0 = init.arguments[0];
285
- if (t.isStringLiteral(a0)) spec = a0.value;
286
581
  }
287
- });
288
582
 
289
- return spec;
583
+ if (key === "handler") hasHandler = true;
584
+ if (["preHandler", "onRequest", "preValidation", "preSerialization"].includes(key)) hooks.push(key);
585
+ }
586
+
587
+ return { url, methods, hasHandler, hooks };
290
588
  }
291
589
 
292
- traverse(ast, {
293
- CallExpression(p) {
294
- const callee = p.node.callee;
295
- if (!t.isMemberExpression(callee)) return;
296
- if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property)) return;
590
+ function unwrapPluginArg(node) {
591
+ // fastify-plugin wrappers are common: fastify.register(fp(plugin))
592
+ // We unwrap 1 layer, best-effort.
593
+ if (!node) return node;
594
+ if (!t.isCallExpression(node)) return node;
297
595
 
298
- const obj = callee.object.name;
299
- const prop = callee.property.name;
596
+ const calleeName =
597
+ t.isIdentifier(node.callee) ? node.callee.name :
598
+ t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.property) ? node.callee.property.name :
599
+ null;
300
600
 
301
- if (!fastifyNames.has(obj)) return;
601
+ if (!calleeName) return node;
602
+ if (!/^(fp|fastifyPlugin|plugin|fastifyPluginify)$/i.test(calleeName)) return node;
302
603
 
303
- // fastify.get('/x', ...)
304
- if (isFastifyMethod(prop)) {
305
- const routeStr = extractStringLiteral(p.node.arguments[0]);
306
- if (!routeStr) return;
604
+ const a0 = node.arguments && node.arguments[0];
605
+ return a0 || node;
606
+ }
307
607
 
308
- const fullPath = joinPaths(prefix, routeStr);
309
- const method = canonicalizeMethod(prop);
608
+ try {
609
+ traverse(ast, {
610
+ FunctionDeclaration(p) {
611
+ if (t.isIdentifier(p.node.id)) {
612
+ _localPlugins.set(p.node.id.name, p.node);
613
+ }
614
+ },
615
+ VariableDeclarator(p) {
616
+ if (!t.isIdentifier(p.node.id)) return;
617
+ const id = p.node.id.name;
618
+ const init = p.node.init;
619
+ if (!init) return;
310
620
 
311
- const ev = evidenceFromLoc({
312
- fileAbs, fileRel, loc: p.node.loc,
313
- reason: `Fastify ${prop.toUpperCase()}("${routeStr}")`
314
- });
621
+ _rawConstInits.set(id, init);
315
622
 
316
- routes.push({
317
- method,
318
- path: fullPath,
319
- handler: fileRel,
320
- confidence: "med",
321
- evidence: ev ? [ev] : []
322
- });
323
- return;
324
- }
623
+ // local plugin: const routes = async (fastify) => { ... }
624
+ if (t.isFunctionExpression(init) || t.isArrowFunctionExpression(init)) {
625
+ _localPlugins.set(id, init);
626
+ }
325
627
 
326
- // fastify.route({ method, url, handler })
327
- if (prop === "route") {
328
- const arg0 = p.node.arguments[0];
329
- if (!t.isObjectExpression(arg0)) return;
628
+ // const app = Fastify()
629
+ if (t.isCallExpression(init) && t.isIdentifier(init.callee)) {
630
+ const cal = init.callee.name;
631
+ if (cal === "Fastify" || cal === "fastify") fastifyNames.add(id);
632
+ }
633
+
634
+ // const app = require("fastify")()
635
+ if (t.isCallExpression(init) && t.isCallExpression(init.callee)) {
636
+ const inner = init.callee;
637
+ if (t.isIdentifier(inner.callee, { name: "require" }) && t.isStringLiteral(inner.arguments?.[0], { value: "fastify" })) {
638
+ fastifyNames.add(id);
639
+ }
640
+ }
641
+ },
642
+ });
643
+ } catch {
644
+ // Babel traverse can fail on some edge-case files; skip this step
645
+ }
330
646
 
331
- const r = extractRouteObject(arg0);
332
- if (!r.url) return;
647
+ // helper: resolve imports for register(pluginIdent,...)
648
+ function resolveImportSpecForLocal(localName) {
649
+ let spec = null;
333
650
 
334
- const fullPath = joinPaths(prefix, r.url);
335
- const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
651
+ try {
652
+ traverse(ast, {
653
+ ImportDeclaration(ip) {
654
+ for (const s of ip.node.specifiers) {
655
+ if ((t.isImportDefaultSpecifier(s) || t.isImportSpecifier(s)) && s.local.name === localName) {
656
+ spec = ip.node.source.value;
657
+ }
658
+ }
659
+ },
660
+ VariableDeclarator(vp) {
661
+ if (!t.isIdentifier(vp.node.id) || vp.node.id.name !== localName) return;
662
+ const init = vp.node.init;
663
+ if (!t.isCallExpression(init)) return;
664
+ if (!t.isIdentifier(init.callee) || init.callee.name !== "require") return;
665
+ const a0 = init.arguments[0];
666
+ if (t.isStringLiteral(a0)) spec = a0.value;
667
+ },
668
+ });
669
+ } catch {
670
+ // Babel traverse can fail; ignore
671
+ }
336
672
 
337
- const ev = evidenceFromLoc({
338
- fileAbs, fileRel, loc: p.node.loc,
339
- reason: `Fastify.route({ url: "${r.url}" })`
340
- });
673
+ return spec;
674
+ }
675
+
676
+ try {
677
+ traverse(ast, {
678
+ CallExpression(p) {
679
+ const callee = p.node.callee;
680
+ if (!t.isMemberExpression(callee)) return;
681
+ if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property)) return;
682
+
683
+ const obj = callee.object.name;
684
+ const prop = callee.property.name;
685
+
686
+ if (!fastifyNames.has(obj)) return;
687
+
688
+ // fastify.get('/x', ...)
689
+ if (isFastifyMethod(prop)) {
690
+ const routeStr = evalStaticString(p.node.arguments[0]);
691
+ if (!routeStr) return;
692
+
693
+ const fullPath = joinPaths(prefix, routeStr);
694
+ const method = canonicalizeMethod(prop);
695
+
696
+ const ev = evidenceFromLoc({
697
+ fileAbs,
698
+ fileRel,
699
+ loc: p.node.loc,
700
+ reason: `Fastify ${prop.toUpperCase()}("${routeStr}")`,
701
+ });
341
702
 
342
- for (const m of ms) {
343
703
  routes.push({
344
- method: m,
704
+ method,
345
705
  path: fullPath,
346
706
  handler: fileRel,
347
- hooks: r.hooks,
348
- confidence: r.hasHandler ? "med" : "low",
349
- evidence: ev ? [ev] : []
707
+ confidence: "med",
708
+ framework: "fastify",
709
+ evidence: ev ? [ev] : [],
350
710
  });
711
+ return;
351
712
  }
352
- return;
353
- }
354
713
 
355
- // fastify.register(plugin, { prefix })
356
- if (prop === "register") {
357
- const pluginArg = p.node.arguments[0];
358
- const optsArg = p.node.arguments[1];
359
- const childPrefixRaw = extractPrefixFromOpts(optsArg);
360
- const childPrefix = childPrefixRaw ? joinPaths(prefix, childPrefixRaw) : prefix;
361
-
362
- // inline plugin
363
- if (t.isFunctionExpression(pluginArg) || t.isArrowFunctionExpression(pluginArg)) {
364
- const param0 = pluginArg.params[0];
365
- const innerName = t.isIdentifier(param0) ? param0.name : "fastify";
366
-
367
- // traverse just the plugin body (best effort)
368
- traverse(pluginArg.body, {
369
- CallExpression(pp) {
370
- const c = pp.node.callee;
371
- if (!t.isMemberExpression(c)) return;
372
- if (!t.isIdentifier(c.object) || !t.isIdentifier(c.property)) return;
373
- if (c.object.name !== innerName) return;
374
-
375
- const pr = c.property.name;
376
-
377
- if (isFastifyMethod(pr)) {
378
- const rs = extractStringLiteral(pp.node.arguments[0]);
379
- if (!rs) return;
380
- const fullPath = joinPaths(childPrefix, rs);
381
- const method = canonicalizeMethod(pr);
382
-
383
- const ev = evidenceFromLoc({
384
- fileAbs, fileRel, loc: pp.node.loc,
385
- reason: `Fastify plugin ${pr.toUpperCase()}("${rs}") prefix="${childPrefixRaw || ""}"`
386
- });
387
-
388
- routes.push({ method, path: fullPath, handler: fileRel, confidence: "med", evidence: ev ? [ev] : [] });
389
- }
714
+ // fastify.route({ method, url, handler })
715
+ if (prop === "route") {
716
+ const arg0 = p.node.arguments[0];
717
+ if (!t.isObjectExpression(arg0)) return;
390
718
 
391
- if (pr === "route") {
392
- const a0 = pp.node.arguments[0];
393
- if (!t.isObjectExpression(a0)) return;
394
- const r = extractRouteObject(a0);
395
- if (!r.url) return;
396
- const fullPath = joinPaths(childPrefix, r.url);
397
- const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
719
+ const r = extractRouteObjectV3(arg0);
720
+ if (!r.url) return;
398
721
 
399
- const ev = evidenceFromLoc({
400
- fileAbs, fileRel, loc: pp.node.loc,
401
- reason: `Fastify plugin route("${r.url}") prefix="${childPrefixRaw || ""}"`
402
- });
722
+ const fullPath = joinPaths(prefix, r.url);
723
+ const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
403
724
 
404
- for (const m of ms) routes.push({ method: m, path: fullPath, handler: fileRel, confidence: "med", evidence: ev ? [ev] : [] });
405
- }
406
- }
407
- }, p.scope, p);
725
+ const ev = evidenceFromLoc({
726
+ fileAbs,
727
+ fileRel,
728
+ loc: p.node.loc,
729
+ reason: `Fastify.route({ url: "${r.url}" })`,
730
+ });
408
731
 
732
+ for (const m of ms) {
733
+ routes.push({
734
+ method: m,
735
+ path: fullPath,
736
+ handler: fileRel,
737
+ hooks: r.hooks,
738
+ confidence: r.hasHandler ? "med" : "low",
739
+ framework: "fastify",
740
+ evidence: ev ? [ev] : [],
741
+ });
742
+ }
409
743
  return;
410
744
  }
411
745
 
412
- // imported plugin identifier
413
- if (t.isIdentifier(pluginArg)) {
414
- const localName = pluginArg.name;
415
- const spec = resolveImportSpecForLocal(localName);
746
+ // fastify.register(plugin, { prefix })
747
+ if (prop === "register") {
748
+ const pluginArgRaw = unwrapPluginArg(p.node.arguments[0]);
749
+ const optsArg = p.node.arguments[1];
750
+
751
+ const childPrefixRaw = extractPrefixFromOptsV3(optsArg);
752
+ const childPrefix = childPrefixRaw ? joinPaths(prefix, childPrefixRaw) : prefix;
753
+
754
+ // inline plugin OR local plugin identifier (common in real Fastify codebases)
755
+ let pluginFn = null;
756
+ const localIdentName = t.isIdentifier(pluginArgRaw) ? pluginArgRaw.name : null;
757
+ if (t.isFunctionExpression(pluginArgRaw) || t.isArrowFunctionExpression(pluginArgRaw)) pluginFn = pluginArgRaw;
758
+ if (!pluginFn && localIdentName) pluginFn = _localPlugins.get(localIdentName) || null;
759
+
760
+ if (pluginFn) {
761
+ const param0 = pluginFn.params && pluginFn.params[0];
762
+ const innerName = t.isIdentifier(param0) ? param0.name : "fastify";
763
+ const bodyNode = pluginFn.body;
764
+ if (!t.isBlockStatement(bodyNode)) {
765
+ // We only handle block bodies. Expression-bodied arrows are rare for route plugins.
766
+ if (localIdentName) return;
767
+ }
768
+
769
+ // traverse just the plugin body (best effort)
770
+ try {
771
+ traverse(
772
+ bodyNode,
773
+ {
774
+ CallExpression(pp) {
775
+ const c = pp.node.callee;
776
+ if (!t.isMemberExpression(c)) return;
777
+ if (!t.isIdentifier(c.object) || !t.isIdentifier(c.property)) return;
778
+ if (c.object.name !== innerName) return;
779
+
780
+ const pr = c.property.name;
781
+
782
+ if (isFastifyMethod(pr)) {
783
+ const rs = evalStaticString(pp.node.arguments[0]);
784
+ if (!rs) return;
785
+
786
+ const fullPath = joinPaths(childPrefix, rs);
787
+ const method = canonicalizeMethod(pr);
788
+
789
+ const ev = evidenceFromLoc({
790
+ fileAbs,
791
+ fileRel,
792
+ loc: pp.node.loc,
793
+ reason: `Fastify plugin ${pr.toUpperCase()}("${rs}") prefix="${childPrefixRaw || ""}"`,
794
+ });
795
+
796
+ routes.push({
797
+ method,
798
+ path: fullPath,
799
+ handler: fileRel,
800
+ confidence: "med",
801
+ framework: "fastify",
802
+ evidence: ev ? [ev] : [],
803
+ });
804
+ }
805
+
806
+ if (pr === "route") {
807
+ const a0 = pp.node.arguments[0];
808
+ if (!t.isObjectExpression(a0)) return;
809
+
810
+ const r = extractRouteObjectV3(a0);
811
+ if (!r.url) return;
812
+
813
+ const fullPath = joinPaths(childPrefix, r.url);
814
+ const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
815
+
816
+ const ev = evidenceFromLoc({
817
+ fileAbs,
818
+ fileRel,
819
+ loc: pp.node.loc,
820
+ reason: `Fastify plugin route("${r.url}") prefix="${childPrefixRaw || ""}"`,
821
+ });
822
+
823
+ for (const m of ms) {
824
+ routes.push({
825
+ method: m,
826
+ path: fullPath,
827
+ handler: fileRel,
828
+ confidence: "med",
829
+ framework: "fastify",
830
+ evidence: ev ? [ev] : [],
831
+ });
832
+ }
833
+ }
834
+ },
835
+ },
836
+ p.scope,
837
+ p
838
+ );
839
+ } catch {
840
+ // Inner traverse can fail; skip this plugin body
841
+ }
416
842
 
417
- if (!spec) {
418
- gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, name: localName });
843
+ // If this was a local plugin identifier, we've already extracted its routes.
844
+ if (localIdentName) return;
419
845
  return;
420
846
  }
421
847
 
422
- const resolved = resolveRelativeModule(fileAbs, spec);
423
- if (!resolved) {
424
- gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec });
848
+ // Resolve dynamic require/import spec directly (fastify.register(require("./x")) / import("./x"))
849
+ const dynSpec = extractRequireOrImportSpec(pluginArgRaw);
850
+ if (dynSpec) {
851
+ const resolved = resolveRelativeModule(fileAbs, dynSpec);
852
+ if (!resolved) {
853
+ gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec: dynSpec });
854
+ return;
855
+ }
856
+ scanFile(resolved, childPrefix);
857
+ return;
858
+ }
859
+
860
+ // imported plugin identifier
861
+ if (t.isIdentifier(pluginArgRaw)) {
862
+ const localName = pluginArgRaw.name;
863
+ const spec = resolveImportSpecForLocal(localName);
864
+
865
+ if (!spec) {
866
+ gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, name: localName });
867
+ return;
868
+ }
869
+
870
+ const resolved = resolveRelativeModule(fileAbs, spec);
871
+ if (!resolved) {
872
+ gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec });
873
+ return;
874
+ }
875
+
876
+ scanFile(resolved, childPrefix);
425
877
  return;
426
878
  }
427
879
 
428
- scanFile(resolved, childPrefix);
880
+ // Anything else: unknown plugin shape. Mark a gap so analyzers can be lenient.
881
+ gaps.push({
882
+ kind: "fastify_plugin_unresolved",
883
+ file: fileRel,
884
+ note: "register() plugin not statically resolvable",
885
+ });
429
886
  }
430
- }
431
- }
432
- });
887
+ },
888
+ });
889
+ } catch {
890
+ // Babel traverse can fail on some edge-case files; skip
891
+ stats.parseErrors++;
892
+ }
433
893
  }
434
894
 
435
895
  scanFile(entryAbs, "/");
436
896
  return { routes, gaps };
437
897
  }
438
898
 
439
- // ---------- client refs (fetch + axios string literal only) ----------
899
+ // ---------- client refs (fetch + axios + template literals best-effort) ----------
440
900
  function isAxiosMember(node) {
441
- return t.isMemberExpression(node) &&
901
+ return (
902
+ t.isMemberExpression(node) &&
442
903
  t.isIdentifier(node.object) &&
443
904
  t.isIdentifier(node.property) &&
444
- ["get","post","put","patch","delete"].includes(node.property.name);
905
+ ["get", "post", "put", "patch", "delete"].includes(node.property.name)
906
+ );
907
+ }
908
+
909
+ function isAxiosCallee(node) {
910
+ return t.isIdentifier(node, { name: "axios" }) || isAxiosMember(node);
911
+ }
912
+
913
+ function extractUrlLike(node) {
914
+ // "literal"
915
+ if (t.isStringLiteral(node)) return { url: node.value, confidence: "high", note: "string" };
916
+
917
+ // `literal`
918
+ if (t.isTemplateLiteral(node) && node.expressions.length === 0) {
919
+ return { url: node.quasis.map((q) => q.value.cooked || "").join(""), confidence: "high", note: "template_static" };
920
+ }
921
+
922
+ // `/api/x/${id}` -> "/api/x/:id" (med confidence)
923
+ if (t.isTemplateLiteral(node) && node.quasis.length >= 1) {
924
+ const start = node.quasis[0]?.value?.cooked || "";
925
+ if (!start.startsWith("/")) return null;
926
+
927
+ let built = "";
928
+ for (let i = 0; i < node.quasis.length; i++) {
929
+ built += node.quasis[i].value.cooked || "";
930
+ if (i < node.expressions.length) {
931
+ const expr = node.expressions[i];
932
+ if (t.isIdentifier(expr)) built += `:${expr.name}`;
933
+ else built += "*";
934
+ }
935
+ }
936
+ return { url: built, confidence: "med", note: "template_dynamic" };
937
+ }
938
+
939
+ // "/api/x" + "/y" or "/api/x" + id (low confidence)
940
+ if (t.isBinaryExpression(node, { operator: "+" })) {
941
+ if (t.isStringLiteral(node.left) && node.left.value.startsWith("/")) {
942
+ const left = node.left.value;
943
+ let right = "";
944
+ if (t.isStringLiteral(node.right)) right = node.right.value;
945
+ else if (t.isIdentifier(node.right)) right = `:${node.right.name}`;
946
+ else right = "*";
947
+ return { url: left + right, confidence: "low", note: "concat" };
948
+ }
949
+ }
950
+
951
+ return null;
952
+ }
953
+
954
+ function extractFetchMethodFromOptions(node) {
955
+ if (!t.isObjectExpression(node)) return "*";
956
+ for (const prop of node.properties) {
957
+ if (!t.isObjectProperty(prop)) continue;
958
+ const key =
959
+ t.isIdentifier(prop.key) ? prop.key.name :
960
+ t.isStringLiteral(prop.key) ? prop.key.value :
961
+ null;
962
+ if (key === "method" && t.isStringLiteral(prop.value)) return canonicalizeMethod(prop.value.value);
963
+ }
964
+ return "*";
445
965
  }
446
966
 
447
- async function resolveClientRouteRefs(repoRoot) {
448
- const files = await fg(["**/*.{ts,tsx,js,jsx}"], {
967
+ function extractAxiosConfig(node) {
968
+ // axios({ url: "/api/x", method: "post" })
969
+ if (!t.isObjectExpression(node)) return null;
970
+
971
+ let urlNode = null;
972
+ let methodNode = null;
973
+
974
+ for (const prop of node.properties) {
975
+ if (!t.isObjectProperty(prop)) continue;
976
+ const key =
977
+ t.isIdentifier(prop.key) ? prop.key.name :
978
+ t.isStringLiteral(prop.key) ? prop.key.value :
979
+ null;
980
+ if (!key) continue;
981
+
982
+ if (key === "url") urlNode = prop.value;
983
+ if (key === "method") methodNode = prop.value;
984
+ }
985
+
986
+ const urlInfo = urlNode ? extractUrlLike(urlNode) : null;
987
+ if (!urlInfo) return null;
988
+
989
+ const method =
990
+ methodNode && t.isStringLiteral(methodNode)
991
+ ? canonicalizeMethod(methodNode.value)
992
+ : "*";
993
+
994
+ return { method, urlInfo };
995
+ }
996
+
997
+ async function resolveClientRouteRefs(repoRoot, stats) {
998
+ const files = await fg(CODE_FILE_GLOBS, {
449
999
  cwd: repoRoot,
450
1000
  absolute: true,
451
1001
  ignore: [
452
- "**/node_modules/**",
453
- "**/.next/**",
454
- "**/dist/**",
455
- "**/build/**",
1002
+ ...IGNORE_GLOBS,
456
1003
  "**/test/**",
457
1004
  "**/tests/**",
458
1005
  "**/__tests__/**",
@@ -462,141 +1009,372 @@ async function resolveClientRouteRefs(repoRoot) {
462
1009
  "**/jest.config.*",
463
1010
  "**/*.mock.*",
464
1011
  "**/mocks/**",
465
- "**/fixtures/**"
466
- ]
1012
+ "**/fixtures/**",
1013
+ ],
467
1014
  });
468
1015
 
469
1016
  const out = [];
470
1017
 
471
1018
  for (const fileAbs of files) {
472
- const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
1019
+ const fileRel = normalizeRel(repoRoot, fileAbs);
473
1020
  const code = safeRead(fileAbs);
1021
+ if (!code) continue;
474
1022
 
475
1023
  let ast;
476
- try { ast = parseFile(code); } catch { continue; }
477
-
478
- traverse(ast, {
479
- CallExpression(p) {
480
- const callee = p.node.callee;
481
-
482
- // fetch("/api/x", { method: "POST" })
483
- if (t.isIdentifier(callee) && callee.name === "fetch") {
484
- const a0 = p.node.arguments[0];
485
- if (!t.isStringLiteral(a0)) return;
486
-
487
- const url = a0.value;
488
- if (!url.startsWith("/")) return;
489
-
490
- let method = "*";
491
- const a1 = p.node.arguments[1];
492
- if (t.isObjectExpression(a1)) {
493
- for (const prop of a1.properties) {
494
- if (!t.isObjectProperty(prop)) continue;
495
- const key =
496
- t.isIdentifier(prop.key) ? prop.key.name :
497
- t.isStringLiteral(prop.key) ? prop.key.value :
498
- null;
499
- if (key === "method" && t.isStringLiteral(prop.value)) {
500
- method = canonicalizeMethod(prop.value.value);
1024
+ try {
1025
+ ast = parseFile(code, fileAbs);
1026
+ } catch {
1027
+ stats.parseErrors++;
1028
+ continue;
1029
+ }
1030
+
1031
+ try {
1032
+ traverse(ast, {
1033
+ CallExpression(p) {
1034
+ const callee = p.node.callee;
1035
+
1036
+ // fetch(url, opts)
1037
+ if (t.isIdentifier(callee, { name: "fetch" })) {
1038
+ const a0 = p.node.arguments[0];
1039
+ const a1 = p.node.arguments[1];
1040
+
1041
+ const urlInfo = extractUrlLike(a0);
1042
+ if (!urlInfo) return;
1043
+
1044
+ const url = urlInfo.url;
1045
+ if (!url.startsWith("/")) return;
1046
+
1047
+ const method = extractFetchMethodFromOptions(a1);
1048
+
1049
+ const ev = evidenceFromLoc({
1050
+ fileAbs,
1051
+ fileRel,
1052
+ loc: p.node.loc,
1053
+ reason: `Client fetch(${urlInfo.note}) "${stripQueryHash(url)}"`,
1054
+ });
1055
+
1056
+ out.push({
1057
+ method,
1058
+ path: canonicalizePath(url),
1059
+ source: fileRel,
1060
+ confidence: urlInfo.confidence,
1061
+ kind: "fetch",
1062
+ evidence: ev ? [ev] : [],
1063
+ });
1064
+ return;
1065
+ }
1066
+
1067
+ // axios.get("/api/x") etc
1068
+ if (isAxiosMember(callee)) {
1069
+ const verb = callee.property.name.toUpperCase();
1070
+ const a0 = p.node.arguments[0];
1071
+
1072
+ const urlInfo = extractUrlLike(a0);
1073
+ if (!urlInfo) return;
1074
+
1075
+ const url = urlInfo.url;
1076
+ if (!url.startsWith("/")) return;
1077
+
1078
+ const ev = evidenceFromLoc({
1079
+ fileAbs,
1080
+ fileRel,
1081
+ loc: p.node.loc,
1082
+ reason: `Client axios.${verb.toLowerCase()}(${urlInfo.note}) "${stripQueryHash(url)}"`,
1083
+ });
1084
+
1085
+ out.push({
1086
+ method: canonicalizeMethod(verb),
1087
+ path: canonicalizePath(url),
1088
+ source: fileRel,
1089
+ confidence: urlInfo.confidence,
1090
+ kind: "axios_member",
1091
+ evidence: ev ? [ev] : [],
1092
+ });
1093
+ return;
1094
+ }
1095
+
1096
+ // axios({ url, method })
1097
+ if (t.isIdentifier(callee, { name: "axios" })) {
1098
+ const a0 = p.node.arguments[0];
1099
+ const cfg = extractAxiosConfig(a0);
1100
+ if (!cfg) return;
1101
+
1102
+ const url = cfg.urlInfo.url;
1103
+ if (!url.startsWith("/")) return;
1104
+
1105
+ const ev = evidenceFromLoc({
1106
+ fileAbs,
1107
+ fileRel,
1108
+ loc: p.node.loc,
1109
+ reason: `Client axios(config:${cfg.urlInfo.note}) "${stripQueryHash(url)}"`,
1110
+ });
1111
+
1112
+ out.push({
1113
+ method: cfg.method,
1114
+ path: canonicalizePath(url),
1115
+ source: fileRel,
1116
+ confidence: cfg.urlInfo.confidence === "high" ? "high" : "med",
1117
+ kind: "axios_config",
1118
+ evidence: ev ? [ev] : [],
1119
+ });
1120
+ return;
1121
+ }
1122
+
1123
+ // useSWR("/api/user", fetcher) - Modern React data fetching
1124
+ if (t.isIdentifier(callee, { name: "useSWR" })) {
1125
+ const a0 = p.node.arguments[0];
1126
+
1127
+ const urlInfo = extractUrlLike(a0);
1128
+ if (!urlInfo) return;
1129
+
1130
+ const url = urlInfo.url;
1131
+ if (!url.startsWith("/")) return;
1132
+
1133
+ const ev = evidenceFromLoc({
1134
+ fileAbs,
1135
+ fileRel,
1136
+ loc: p.node.loc,
1137
+ reason: `Client useSWR(${urlInfo.note}) "${stripQueryHash(url)}"`,
1138
+ });
1139
+
1140
+ out.push({
1141
+ method: "GET", // SWR is almost always GET
1142
+ path: canonicalizePath(url),
1143
+ source: fileRel,
1144
+ confidence: urlInfo.confidence,
1145
+ kind: "useSWR",
1146
+ evidence: ev ? [ev] : [],
1147
+ });
1148
+ return;
1149
+ }
1150
+
1151
+ // useQuery (React Query / TanStack Query) - Another popular data fetching library
1152
+ if (t.isIdentifier(callee, { name: "useQuery" })) {
1153
+ // useQuery({ queryKey: [...], queryFn: () => fetch("/api/x") })
1154
+ // or useQuery(["key"], () => fetch("/api/x"))
1155
+ const a0 = p.node.arguments[0];
1156
+
1157
+ // Try to extract URL from the arguments (often in queryFn)
1158
+ if (t.isObjectExpression(a0)) {
1159
+ for (const prop of a0.properties) {
1160
+ if (!t.isObjectProperty(prop)) continue;
1161
+ if (!t.isIdentifier(prop.key, { name: "queryFn" })) continue;
1162
+
1163
+ // queryFn is often an arrow function with fetch inside
1164
+ const fn = prop.value;
1165
+ if (t.isArrowFunctionExpression(fn) || t.isFunctionExpression(fn)) {
1166
+ // Best effort: look for string literals that look like API paths
1167
+ const fnCode = code.slice(fn.start, fn.end);
1168
+ const urlMatch = fnCode.match(/["'`](\/api\/[^"'`]+)["'`]/);
1169
+ if (urlMatch) {
1170
+ const url = urlMatch[1].split("?")[0].split("#")[0];
1171
+ const ev = evidenceFromLoc({
1172
+ fileAbs,
1173
+ fileRel,
1174
+ loc: p.node.loc,
1175
+ reason: `Client useQuery(queryFn) "${url}"`,
1176
+ });
1177
+
1178
+ out.push({
1179
+ method: "GET",
1180
+ path: canonicalizePath(url),
1181
+ source: fileRel,
1182
+ confidence: "low", // Less certain extraction
1183
+ kind: "useQuery",
1184
+ evidence: ev ? [ev] : [],
1185
+ });
1186
+ }
1187
+ }
501
1188
  }
502
1189
  }
503
1190
  }
1191
+ },
1192
+ });
1193
+ } catch {
1194
+ // Babel traverse can fail on some edge-case files; skip
1195
+ stats.parseErrors++;
1196
+ }
1197
+ }
504
1198
 
505
- const ev = evidenceFromLoc({
506
- fileAbs, fileRel, loc: p.node.loc,
507
- reason: `Client fetch("${url}")`
508
- });
509
-
510
- out.push({
511
- method,
512
- path: canonicalizePath(url),
513
- source: fileRel,
514
- confidence: "high",
515
- evidence: ev ? [ev] : []
516
- });
517
- return;
518
- }
1199
+ return out;
1200
+ }
519
1201
 
520
- // axios.get("/api/x")
521
- if (isAxiosMember(callee)) {
522
- const verb = callee.property.name.toUpperCase();
523
- const a0 = p.node.arguments[0];
524
- if (!t.isStringLiteral(a0)) return;
525
-
526
- const url = a0.value;
527
- if (!url.startsWith("/")) return;
528
-
529
- const ev = evidenceFromLoc({
530
- fileAbs, fileRel, loc: p.node.loc,
531
- reason: `Client axios.${verb.toLowerCase()}("${url}")`
532
- });
533
-
534
- out.push({
535
- method: canonicalizeMethod(verb),
536
- path: canonicalizePath(url),
537
- source: fileRel,
538
- confidence: "high",
539
- evidence: ev ? [ev] : []
540
- });
541
- }
1202
+ // ---------- workspace detection (best-effort, no new deps) ----------
1203
+ function readJsonIfExists(abs) {
1204
+ try {
1205
+ return JSON.parse(fs.readFileSync(abs, "utf8"));
1206
+ } catch {
1207
+ return null;
1208
+ }
1209
+ }
1210
+
1211
+ function detectWorkspaces(repoRoot) {
1212
+ const roots = [];
1213
+
1214
+ const pkg = readJsonIfExists(path.join(repoRoot, "package.json"));
1215
+ if (pkg && pkg.workspaces) {
1216
+ const ws = pkg.workspaces;
1217
+ const patterns = Array.isArray(ws) ? ws : Array.isArray(ws.packages) ? ws.packages : [];
1218
+ for (const pat of patterns) {
1219
+ if (typeof pat === "string") roots.push(pat);
1220
+ }
1221
+ }
1222
+
1223
+ // pnpm-workspace.yaml minimal parser (just handles `packages:` list)
1224
+ const pnpmWs = path.join(repoRoot, "pnpm-workspace.yaml");
1225
+ if (fs.existsSync(pnpmWs)) {
1226
+ const raw = safeRead(pnpmWs) || "";
1227
+ const lines = raw.split(/\r?\n/);
1228
+ let inPackages = false;
1229
+ for (const line of lines) {
1230
+ const l = line.trim();
1231
+ if (!l) continue;
1232
+ if (l.startsWith("packages:")) {
1233
+ inPackages = true;
1234
+ continue;
542
1235
  }
543
- });
1236
+ if (inPackages) {
1237
+ const m = l.match(/^-+\s*['"]?([^'"]+)['"]?\s*$/);
1238
+ if (m && m[1]) roots.push(m[1]);
1239
+ else if (!l.startsWith("-")) inPackages = false;
1240
+ }
1241
+ }
544
1242
  }
545
1243
 
546
- return out;
1244
+ // Expand to actual package.json roots
1245
+ const uniq = Array.from(new Set(roots)).filter(Boolean);
1246
+ const pkgJsonGlobs = uniq.map((p) => (p.endsWith("/") ? p : p + "/") + "package.json");
1247
+
1248
+ const found = pkgJsonGlobs.length
1249
+ ? fg.sync(pkgJsonGlobs, { cwd: repoRoot, absolute: true, ignore: IGNORE_GLOBS })
1250
+ : [];
1251
+
1252
+ const workspaces = found
1253
+ .map((abs) => path.dirname(abs))
1254
+ .map((abs) => normalizeRel(repoRoot, abs))
1255
+ .sort();
1256
+
1257
+ return workspaces;
547
1258
  }
548
1259
 
549
- // ---------- fastify entry detection ----------
550
- function detectFastifyEntry(repoRoot) {
551
- const candidates = [
552
- "src/server.ts","src/server.js",
553
- "server.ts","server.js",
554
- "src/index.ts","src/index.js",
555
- "index.ts","index.js"
556
- ];
557
- for (const rel of candidates) {
558
- const abs = path.join(repoRoot, rel);
559
- if (exists(abs)) return rel;
1260
+ // ---------- fastify entry detection (monorepo-friendly) ----------
1261
+ async function detectFastifyEntries(repoRoot) {
1262
+ // Keep it targeted (fast), but broad enough for monorepos.
1263
+ const candidates = await fg(
1264
+ [
1265
+ "**/{server,app,main,index}.{ts,tsx,js,jsx}",
1266
+ "**/src/{server,app,main,index}.{ts,tsx,js,jsx}",
1267
+ "**/*fastify*.{ts,tsx,js,jsx}",
1268
+ "**/*api*.{ts,tsx,js,jsx}",
1269
+ ],
1270
+ {
1271
+ cwd: repoRoot,
1272
+ absolute: true,
1273
+ ignore: IGNORE_GLOBS,
1274
+ }
1275
+ );
1276
+
1277
+ const entries = [];
1278
+ const fastifySignal = /\b(Fastify\s*\(|fastify\s*\(|require\(['"]fastify['"]\)|from\s+['"]fastify['"])\b/;
1279
+ const listenSignal = /\.\s*(listen|ready)\s*\(/;
1280
+
1281
+ for (const fileAbs of candidates) {
1282
+ const code = safeRead(fileAbs);
1283
+ if (!code) continue;
1284
+ // Must look like fastify + server start-ish signal (reduces noise)
1285
+ if (fastifySignal.test(code) && listenSignal.test(code)) {
1286
+ entries.push(fileAbs);
1287
+ }
560
1288
  }
561
- return null;
1289
+
1290
+ return Array.from(new Set(entries));
562
1291
  }
563
1292
 
564
1293
  // ---------- truthpack build/write ----------
565
1294
  async function buildTruthpack({ repoRoot, fastifyEntry }) {
1295
+ const stats = {
1296
+ parseErrors: 0,
1297
+ fastifyEntries: 0,
1298
+ fastifyRoutes: 0,
1299
+ nextAppRoutes: 0,
1300
+ nextPagesRoutes: 0,
1301
+ clientRefs: 0,
1302
+ serverRoutes: 0,
1303
+ gaps: 0,
1304
+ };
1305
+
1306
+ // Workspaces (for metadata + future use)
1307
+ const workspaces = detectWorkspaces(repoRoot);
1308
+
566
1309
  // Next.js routes (App Router + Pages Router)
567
- const nextApp = await resolveNextAppApiRoutes(repoRoot);
568
- const nextPages = await resolveNextPagesApiRoutes(repoRoot);
1310
+ const nextApp = await resolveNextAppApiRoutes(repoRoot, stats);
1311
+ const nextPages = await resolveNextPagesApiRoutes(repoRoot, stats);
1312
+
1313
+ stats.nextAppRoutes = nextApp.length;
1314
+ stats.nextPagesRoutes = nextPages.length;
569
1315
 
570
- // Fastify routes (legacy detection)
571
- const entryRel = fastifyEntry || detectFastifyEntry(repoRoot);
1316
+ // Fastify routes (monorepo-friendly)
572
1317
  let fastify = { routes: [], gaps: [] };
573
- if (entryRel) {
574
- const entryAbs = path.isAbsolute(entryRel) ? entryRel : path.join(repoRoot, entryRel);
575
- if (exists(entryAbs)) fastify = resolveFastifyRoutes(repoRoot, entryAbs);
1318
+
1319
+ if (fastifyEntry) {
1320
+ const entryAbs = path.isAbsolute(fastifyEntry) ? fastifyEntry : path.join(repoRoot, fastifyEntry);
1321
+ if (exists(entryAbs)) {
1322
+ const resolved = resolveFastifyRoutes(repoRoot, entryAbs, stats);
1323
+ fastify.routes.push(...resolved.routes);
1324
+ fastify.gaps.push(...resolved.gaps);
1325
+ stats.fastifyEntries = 1;
1326
+ }
1327
+ } else {
1328
+ const entries = await detectFastifyEntries(repoRoot);
1329
+ stats.fastifyEntries = entries.length;
1330
+
1331
+ for (const entryAbs of entries) {
1332
+ const resolved = resolveFastifyRoutes(repoRoot, entryAbs, stats);
1333
+ fastify.routes.push(...resolved.routes);
1334
+ fastify.gaps.push(...resolved.gaps);
1335
+ }
576
1336
  }
577
1337
 
1338
+ stats.fastifyRoutes = fastify.routes.length;
1339
+
578
1340
  // Multi-framework route detection v2 (Express, Flask, FastAPI, Django, Hono, Koa, etc.)
579
1341
  const multiFramework = await resolveAllRoutes(repoRoot);
580
1342
  const detectedFrameworks = await detectFrameworks(repoRoot);
581
1343
 
582
1344
  // Client refs (JS/TS fetch/axios + Python requests/httpx)
583
- const clientRefs = await resolveClientRouteRefs(repoRoot);
584
- const allClientRefs = [...clientRefs, ...multiFramework.clientRefs];
1345
+ const clientRefs = await resolveClientRouteRefs(repoRoot, stats);
1346
+ const allClientRefs = [...clientRefs, ...(multiFramework.clientRefs || [])];
1347
+
1348
+ stats.clientRefs = allClientRefs.length;
1349
+
1350
+ // Merge all server routes (dedupe with priority)
1351
+ const serverRoutesRaw = [...nextApp, ...nextPages, ...(fastify.routes || []), ...(multiFramework.routes || [])];
585
1352
 
586
- // Merge all server routes (dedupe by method+path)
587
- const serverRoutesRaw = [...nextApp, ...nextPages, ...fastify.routes, ...multiFramework.routes];
588
- const seenRoutes = new Set();
589
- const server = [];
1353
+ const bestByKey = new Map(); // key = method:path
590
1354
  for (const r of serverRoutesRaw) {
591
- const key = `${r.method}:${r.path}`;
592
- if (!seenRoutes.has(key)) {
593
- seenRoutes.add(key);
594
- server.push(r);
1355
+ const key = `${canonicalizeMethod(r.method)}:${canonicalizePath(r.path)}`;
1356
+
1357
+ const prev = bestByKey.get(key);
1358
+ if (!prev) {
1359
+ bestByKey.set(key, { ...r, method: canonicalizeMethod(r.method), path: canonicalizePath(r.path) });
1360
+ continue;
1361
+ }
1362
+
1363
+ // Prefer higher confidence, and prefer specific method over "*"
1364
+ const prevScore = scoreConfidence(prev.confidence) + (prev.method === "*" ? 0 : 1);
1365
+ const curScore = scoreConfidence(r.confidence) + (r.method === "*" ? 0 : 1);
1366
+
1367
+ if (curScore > prevScore) {
1368
+ bestByKey.set(key, { ...r, method: canonicalizeMethod(r.method), path: canonicalizePath(r.path) });
595
1369
  }
596
1370
  }
597
1371
 
1372
+ const server = Array.from(bestByKey.values());
1373
+ stats.serverRoutes = server.length;
1374
+
598
1375
  // Merge gaps
599
1376
  const allGaps = [...(fastify.gaps || []), ...(multiFramework.gaps || [])];
1377
+ stats.gaps = allGaps.length;
600
1378
 
601
1379
  // Env Truth v1
602
1380
  const env = await buildEnvTruth(repoRoot);
@@ -611,23 +1389,32 @@ async function buildTruthpack({ repoRoot, fastifyEntry }) {
611
1389
  const enforcement = buildEnforcementTruth(repoRoot, server);
612
1390
 
613
1391
  // Determine frameworks
614
- const frameworks = new Set(["next", "fastify"]);
615
- detectedFrameworks.forEach(f => frameworks.add(f));
616
- server.forEach(r => r.framework && frameworks.add(r.framework));
1392
+ const frameworks = new Set();
1393
+ detectedFrameworks.forEach((f) => frameworks.add(f));
1394
+ server.forEach((r) => r.framework && frameworks.add(r.framework));
1395
+ if (nextApp.length || nextPages.length) frameworks.add("next");
1396
+ if (fastify.routes.length) frameworks.add("fastify");
617
1397
 
618
1398
  const truthpack = {
619
1399
  meta: {
620
- version: "2.0.0",
1400
+ version: "2.1.0",
621
1401
  generatedAt: new Date().toISOString(),
622
1402
  repoRoot,
623
- commit: { sha: process.env.VIBECHECK_COMMIT_SHA || "unknown" }
1403
+ commit: { sha: process.env.VIBECHECK_COMMIT_SHA || "unknown" },
1404
+ stats,
1405
+ },
1406
+ project: {
1407
+ frameworks: Array.from(frameworks),
1408
+ workspaces,
1409
+ entrypoints: {
1410
+ fastify: fastifyEntry ? [fastifyEntry] : [], // entries auto-detected are not stored as rel here by default
1411
+ },
624
1412
  },
625
- project: { frameworks: Array.from(frameworks), workspaces: [], entrypoints: [] },
626
1413
  routes: { server, clientRefs: allClientRefs, gaps: allGaps },
627
1414
  env,
628
1415
  auth,
629
1416
  billing,
630
- enforcement
1417
+ enforcement,
631
1418
  };
632
1419
 
633
1420
  const hash = sha256(JSON.stringify(truthpack));
@@ -639,29 +1426,266 @@ async function buildTruthpack({ repoRoot, fastifyEntry }) {
639
1426
  function writeTruthpack(repoRoot, truthpack) {
640
1427
  const dir = path.join(repoRoot, ".vibecheck");
641
1428
  ensureDir(dir);
642
- // Spec: .vibecheck/truthpack.json (not .vibecheck/truth/truthpack.json)
643
- fs.writeFileSync(path.join(dir, "truthpack.json"), JSON.stringify(truthpack, null, 2));
1429
+
1430
+ const target = path.join(dir, "truthpack.json");
1431
+ const tmp = path.join(dir, `truthpack.${process.pid}.${Date.now()}.tmp.json`);
1432
+
1433
+ // atomic-ish write: write tmp then rename
1434
+ fs.writeFileSync(tmp, JSON.stringify(truthpack, null, 2));
1435
+ fs.renameSync(tmp, target);
644
1436
  }
645
1437
 
646
1438
  function loadTruthpack(repoRoot) {
647
- // Spec path: .vibecheck/truthpack.json
648
1439
  const specPath = path.join(repoRoot, ".vibecheck", "truthpack.json");
649
- // Legacy path: .vibecheck/truth/truthpack.json (backward compat)
650
1440
  const legacyPath = path.join(repoRoot, ".vibecheck", "truth", "truthpack.json");
1441
+
1442
+ try {
1443
+ return JSON.parse(fs.readFileSync(specPath, "utf8"));
1444
+ } catch {
1445
+ try {
1446
+ return JSON.parse(fs.readFileSync(legacyPath, "utf8"));
1447
+ } catch {
1448
+ return null;
1449
+ }
1450
+ }
1451
+ }
1452
+
1453
+ // ---------- RepoIndex-powered build (vNext) ----------
1454
+
1455
+ /**
1456
+ * Build truthpack using RepoIndex for single-pass file indexing
1457
+ * This is the optimized path that shares file reads across all extractors.
1458
+ *
1459
+ * Enable with: VIBECHECK_ENGINE_V2=1
1460
+ *
1461
+ * @param {Object} options
1462
+ * @param {string} options.repoRoot
1463
+ * @param {string} [options.fastifyEntry]
1464
+ * @param {boolean} [options.verbose]
1465
+ * @returns {Promise<Object>}
1466
+ */
1467
+ async function buildTruthpackV2({ repoRoot, fastifyEntry, verbose }) {
1468
+ const {
1469
+ createIndex,
1470
+ globalASTCache,
1471
+ logIndexSummary,
1472
+ extractNextAppRoutes,
1473
+ extractNextPagesRoutes,
1474
+ extractClientRefs,
1475
+ extractFastifyRoutes,
1476
+ detectFastifyEntries: detectFastifyEntriesV2,
1477
+ buildEnvTruthV2,
1478
+ buildBillingTruthV2,
1479
+ buildAuthTruthV2,
1480
+ buildEnforcementTruthV2,
1481
+ extractExpressRoutes,
1482
+ } = require("./engine");
1483
+
1484
+ const startTime = Date.now();
1485
+
1486
+ // Phase 0: Build RepoIndex (single glob pass)
1487
+ const index = await createIndex(repoRoot);
1488
+
1489
+ if (verbose || process.env.VIBECHECK_VERBOSE) {
1490
+ logIndexSummary(index);
1491
+ console.log(` AST Cache: ${globalASTCache.getSummary().hitRate} hit rate`);
1492
+ }
1493
+
1494
+ const stats = {
1495
+ parseErrors: 0,
1496
+ fastifyEntries: 0,
1497
+ fastifyRoutes: 0,
1498
+ nextAppRoutes: 0,
1499
+ nextPagesRoutes: 0,
1500
+ clientRefs: 0,
1501
+ serverRoutes: 0,
1502
+ gaps: 0,
1503
+ indexTimeMs: index.stats.indexTimeMs,
1504
+ };
1505
+
1506
+ // Use signals from index instead of re-detecting
1507
+ const detectedFrameworks = Array.from(index.signals.detectedFrameworks);
1508
+
1509
+ // Workspaces
1510
+ const workspaces = detectWorkspaces(repoRoot);
1511
+
1512
+ // Phase 1: Extract routes using optimized extractors (use RepoIndex + AST cache)
651
1513
 
652
- try {
653
- return JSON.parse(fs.readFileSync(specPath, "utf8"));
654
- } catch {
655
- // Try legacy path
656
- try { return JSON.parse(fs.readFileSync(legacyPath, "utf8")); } catch { return null; }
1514
+ // Next.js routes - use optimized extractors
1515
+ const nextApp = extractNextAppRoutes(index, stats);
1516
+ const nextPages = extractNextPagesRoutes(index, stats);
1517
+
1518
+ stats.nextAppRoutes = nextApp.length;
1519
+ stats.nextPagesRoutes = nextPages.length;
1520
+
1521
+ // Fastify routes - use optimized extractor
1522
+ let fastify = { routes: [], gaps: [] };
1523
+
1524
+ if (index.hasFramework("fastify") || fastifyEntry) {
1525
+ if (fastifyEntry) {
1526
+ const entryAbs = path.isAbsolute(fastifyEntry) ? fastifyEntry : path.join(repoRoot, fastifyEntry);
1527
+ if (exists(entryAbs)) {
1528
+ const resolved = extractFastifyRoutes(index, entryAbs, stats);
1529
+ fastify.routes.push(...resolved.routes);
1530
+ fastify.gaps.push(...resolved.gaps);
1531
+ stats.fastifyEntries = 1;
1532
+ }
1533
+ } else {
1534
+ const entries = detectFastifyEntriesV2(index);
1535
+ stats.fastifyEntries = entries.length;
1536
+
1537
+ for (const entryAbs of entries) {
1538
+ const resolved = extractFastifyRoutes(index, entryAbs, stats);
1539
+ fastify.routes.push(...resolved.routes);
1540
+ fastify.gaps.push(...resolved.gaps);
1541
+ }
1542
+ }
657
1543
  }
1544
+
1545
+ stats.fastifyRoutes = fastify.routes.length;
1546
+
1547
+ // Express routes - use optimized extractor
1548
+ const expressRoutes = extractExpressRoutes(index, stats);
1549
+
1550
+ // Multi-framework routes (Flask, Django, etc. - still uses old resolvers for non-JS frameworks)
1551
+ // Express is handled above via optimized extractor
1552
+ const multiFramework = await resolveAllRoutes(repoRoot, {
1553
+ mode: process.env.VIBECHECK_ROUTE_SCAN_MODE || "smart",
1554
+ verbose: verbose || !!process.env.VIBECHECK_VERBOSE_ROUTES,
1555
+ skipExpress: true, // Skip Express since we handle it above with optimized extractor
1556
+ });
1557
+
1558
+ // Client refs - use optimized extractor
1559
+ const clientRefsOptimized = extractClientRefs(index, stats);
1560
+ const allClientRefs = [...clientRefsOptimized, ...(multiFramework.clientRefs || [])];
1561
+
1562
+ stats.clientRefs = allClientRefs.length;
1563
+
1564
+ // Merge server routes (including optimized Express routes)
1565
+ const serverRoutesRaw = [...nextApp, ...nextPages, ...(fastify.routes || []), ...expressRoutes, ...(multiFramework.routes || [])];
1566
+
1567
+ const bestByKey = new Map();
1568
+ for (const r of serverRoutesRaw) {
1569
+ const key = `${canonicalizeMethod(r.method)}:${canonicalizePath(r.path)}`;
1570
+ const prev = bestByKey.get(key);
1571
+ if (!prev) {
1572
+ bestByKey.set(key, { ...r, method: canonicalizeMethod(r.method), path: canonicalizePath(r.path) });
1573
+ continue;
1574
+ }
1575
+ const prevScore = scoreConfidence(prev.confidence) + (prev.method === "*" ? 0 : 1);
1576
+ const curScore = scoreConfidence(r.confidence) + (r.method === "*" ? 0 : 1);
1577
+ if (curScore > prevScore) {
1578
+ bestByKey.set(key, { ...r, method: canonicalizeMethod(r.method), path: canonicalizePath(r.path) });
1579
+ }
1580
+ }
1581
+
1582
+ const server = Array.from(bestByKey.values());
1583
+ stats.serverRoutes = server.length;
1584
+
1585
+ // Merge gaps
1586
+ const allGaps = [...(fastify.gaps || []), ...(multiFramework.gaps || [])];
1587
+ stats.gaps = allGaps.length;
1588
+
1589
+ // Phase 2: Build other truths (env, auth, billing, enforcement)
1590
+ // All use optimized extractors with RepoIndex
1591
+ const env = buildEnvTruthV2(index, stats);
1592
+ const auth = buildAuthTruthV2(index, server);
1593
+ const billing = buildBillingTruthV2(index, stats);
1594
+ const enforcement = buildEnforcementTruthV2(index, server);
1595
+
1596
+ // Determine frameworks
1597
+ const frameworks = new Set(detectedFrameworks);
1598
+ server.forEach((r) => r.framework && frameworks.add(r.framework));
1599
+ if (nextApp.length || nextPages.length) frameworks.add("next");
1600
+ if (fastify.routes.length) frameworks.add("fastify");
1601
+
1602
+ stats.totalTimeMs = Date.now() - startTime;
1603
+
1604
+ const truthpack = {
1605
+ meta: {
1606
+ version: "2.2.0", // Bump version for v2 engine
1607
+ generatedAt: new Date().toISOString(),
1608
+ repoRoot,
1609
+ commit: { sha: process.env.VIBECHECK_COMMIT_SHA || "unknown" },
1610
+ stats,
1611
+ engine: "v2", // Mark as v2 engine
1612
+ },
1613
+ project: {
1614
+ frameworks: Array.from(frameworks),
1615
+ workspaces,
1616
+ entrypoints: {
1617
+ fastify: fastifyEntry ? [fastifyEntry] : [],
1618
+ },
1619
+ },
1620
+ routes: { server, clientRefs: allClientRefs, gaps: allGaps },
1621
+ env,
1622
+ auth,
1623
+ billing,
1624
+ enforcement,
1625
+ };
1626
+
1627
+ const hash = sha256(JSON.stringify(truthpack));
1628
+ truthpack.index = {
1629
+ hashes: { truthpackHash: hash },
1630
+ evidenceRefs: [],
1631
+ repoIndex: {
1632
+ totalFiles: index.stats.totalFiles,
1633
+ totalSize: index.stats.totalSize,
1634
+ indexTimeMs: index.stats.indexTimeMs,
1635
+ },
1636
+ };
1637
+
1638
+ // Clear caches to free memory
1639
+ index.clearContentCache();
1640
+ clearCache();
1641
+
1642
+ return truthpack;
1643
+ }
1644
+
1645
+ /**
1646
+ * Smart buildTruthpack - uses v2 engine by default for better performance.
1647
+ * Set VIBECHECK_ENGINE_V1=1 to fall back to v1 for backward compatibility.
1648
+ *
1649
+ * V2 Engine Benefits:
1650
+ * - Single-pass file indexing (RepoIndex)
1651
+ * - Shared AST cache (globalASTCache)
1652
+ * - Token prefiltering for faster file selection
1653
+ * - ~30% faster on typical projects
1654
+ */
1655
+ async function buildTruthpackSmart(options) {
1656
+ if (process.env.VIBECHECK_ENGINE_V1 === "1") {
1657
+ return buildTruthpack(options);
1658
+ }
1659
+ // V2 is now the default - uses RepoIndex + AST cache
1660
+ return buildTruthpackV2(options);
658
1661
  }
659
1662
 
660
1663
  module.exports = {
661
1664
  canonicalizeMethod,
662
1665
  canonicalizePath,
663
1666
  buildTruthpack,
1667
+ buildTruthpackV2,
1668
+ buildTruthpackSmart,
664
1669
  writeTruthpack,
665
1670
  loadTruthpack,
666
- detectFastifyEntry
1671
+ clearCache, // Clear file cache to free memory (important for long-running processes)
1672
+ // kept for backward compatibility if other code imports it,
1673
+ // but fastifyEntry is now optional and auto-detected.
1674
+ detectFastifyEntry: function detectFastifyEntry(repoRoot) {
1675
+ const candidates = [
1676
+ "src/server.ts",
1677
+ "src/server.js",
1678
+ "server.ts",
1679
+ "server.js",
1680
+ "src/index.ts",
1681
+ "src/index.js",
1682
+ "index.ts",
1683
+ "index.js",
1684
+ ];
1685
+ for (const rel of candidates) {
1686
+ const abs = path.join(repoRoot, rel);
1687
+ if (exists(abs)) return rel;
1688
+ }
1689
+ return null;
1690
+ },
667
1691
  };