@vibecheckai/cli 3.5.0 → 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 (224) hide show
  1. package/bin/registry.js +214 -237
  2. package/bin/runners/cli-utils.js +33 -2
  3. package/bin/runners/context/analyzer.js +52 -1
  4. package/bin/runners/context/generators/cursor.js +2 -49
  5. package/bin/runners/context/git-context.js +3 -1
  6. package/bin/runners/context/team-conventions.js +33 -7
  7. package/bin/runners/lib/analysis-core.js +25 -5
  8. package/bin/runners/lib/analyzers.js +431 -481
  9. package/bin/runners/lib/default-config.js +127 -0
  10. package/bin/runners/lib/doctor/modules/security.js +3 -1
  11. package/bin/runners/lib/engine/ast-cache.js +210 -0
  12. package/bin/runners/lib/engine/auth-extractor.js +211 -0
  13. package/bin/runners/lib/engine/billing-extractor.js +112 -0
  14. package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
  15. package/bin/runners/lib/engine/env-extractor.js +207 -0
  16. package/bin/runners/lib/engine/express-extractor.js +208 -0
  17. package/bin/runners/lib/engine/extractors.js +849 -0
  18. package/bin/runners/lib/engine/index.js +207 -0
  19. package/bin/runners/lib/engine/repo-index.js +514 -0
  20. package/bin/runners/lib/engine/types.js +124 -0
  21. package/bin/runners/lib/engines/accessibility-engine.js +18 -218
  22. package/bin/runners/lib/engines/api-consistency-engine.js +30 -335
  23. package/bin/runners/lib/engines/cross-file-analysis-engine.js +27 -292
  24. package/bin/runners/lib/engines/empty-catch-engine.js +17 -127
  25. package/bin/runners/lib/engines/mock-data-engine.js +10 -53
  26. package/bin/runners/lib/engines/performance-issues-engine.js +36 -176
  27. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +54 -382
  28. package/bin/runners/lib/engines/type-aware-engine.js +39 -263
  29. package/bin/runners/lib/engines/vibecheck-engines/index.js +13 -122
  30. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
  31. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
  32. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
  33. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
  34. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
  35. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
  36. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
  37. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +73 -373
  38. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
  39. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
  40. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
  41. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
  42. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
  43. package/bin/runners/lib/entitlements-v2.js +73 -97
  44. package/bin/runners/lib/error-handler.js +44 -3
  45. package/bin/runners/lib/error-messages.js +289 -0
  46. package/bin/runners/lib/evidence-pack.js +7 -1
  47. package/bin/runners/lib/finding-id.js +69 -0
  48. package/bin/runners/lib/finding-sorter.js +89 -0
  49. package/bin/runners/lib/html-proof-report.js +700 -350
  50. package/bin/runners/lib/missions/plan.js +6 -46
  51. package/bin/runners/lib/missions/templates.js +0 -232
  52. package/bin/runners/lib/next-action.js +560 -0
  53. package/bin/runners/lib/prerequisites.js +149 -0
  54. package/bin/runners/lib/route-detection.js +137 -68
  55. package/bin/runners/lib/scan-output.js +91 -76
  56. package/bin/runners/lib/scan-runner.js +135 -0
  57. package/bin/runners/lib/schemas/ajv-validator.js +464 -0
  58. package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
  59. package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
  60. package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
  61. package/bin/runners/lib/schemas/run-request.schema.json +108 -0
  62. package/bin/runners/lib/schemas/validator.js +27 -0
  63. package/bin/runners/lib/schemas/verdict.schema.json +140 -0
  64. package/bin/runners/lib/ship-output-enterprise.js +23 -23
  65. package/bin/runners/lib/ship-output.js +75 -31
  66. package/bin/runners/lib/terminal-ui.js +6 -113
  67. package/bin/runners/lib/truth.js +351 -10
  68. package/bin/runners/lib/unified-cli-output.js +430 -603
  69. package/bin/runners/lib/unified-output.js +13 -9
  70. package/bin/runners/runAIAgent.js +10 -5
  71. package/bin/runners/runAgent.js +0 -3
  72. package/bin/runners/runAllowlist.js +389 -0
  73. package/bin/runners/runApprove.js +0 -33
  74. package/bin/runners/runAuth.js +73 -45
  75. package/bin/runners/runCheckpoint.js +51 -11
  76. package/bin/runners/runClassify.js +85 -21
  77. package/bin/runners/runContext.js +0 -3
  78. package/bin/runners/runDoctor.js +41 -28
  79. package/bin/runners/runEvidencePack.js +362 -0
  80. package/bin/runners/runFirewall.js +0 -3
  81. package/bin/runners/runFirewallHook.js +0 -3
  82. package/bin/runners/runFix.js +66 -76
  83. package/bin/runners/runGuard.js +18 -411
  84. package/bin/runners/runInit.js +113 -30
  85. package/bin/runners/runLabs.js +424 -0
  86. package/bin/runners/runMcp.js +19 -25
  87. package/bin/runners/runPolish.js +64 -240
  88. package/bin/runners/runPromptFirewall.js +12 -5
  89. package/bin/runners/runProve.js +57 -22
  90. package/bin/runners/runQuickstart.js +531 -0
  91. package/bin/runners/runReality.js +59 -68
  92. package/bin/runners/runReport.js +38 -33
  93. package/bin/runners/runRuntime.js +8 -5
  94. package/bin/runners/runScan.js +1413 -190
  95. package/bin/runners/runShip.js +113 -719
  96. package/bin/runners/runTruth.js +0 -3
  97. package/bin/runners/runValidate.js +13 -9
  98. package/bin/runners/runWatch.js +23 -14
  99. package/bin/scan.js +6 -1
  100. package/bin/vibecheck.js +204 -185
  101. package/mcp-server/deprecation-middleware.js +282 -0
  102. package/mcp-server/handlers/index.ts +15 -0
  103. package/mcp-server/handlers/tool-handler.ts +554 -0
  104. package/mcp-server/index-v1.js +698 -0
  105. package/mcp-server/index.js +210 -238
  106. package/mcp-server/lib/cache-wrapper.cjs +383 -0
  107. package/mcp-server/lib/error-envelope.js +138 -0
  108. package/mcp-server/lib/executor.ts +499 -0
  109. package/mcp-server/lib/index.ts +19 -0
  110. package/mcp-server/lib/rate-limiter.js +166 -0
  111. package/mcp-server/lib/sandbox.test.ts +519 -0
  112. package/mcp-server/lib/sandbox.ts +395 -0
  113. package/mcp-server/lib/types.ts +267 -0
  114. package/mcp-server/package.json +12 -3
  115. package/mcp-server/registry/tool-registry.js +794 -0
  116. package/mcp-server/registry/tools.json +605 -0
  117. package/mcp-server/registry.test.ts +334 -0
  118. package/mcp-server/tests/tier-gating.test.js +297 -0
  119. package/mcp-server/tier-auth.js +378 -45
  120. package/mcp-server/tools-v3.js +353 -442
  121. package/mcp-server/tsconfig.json +37 -0
  122. package/mcp-server/vibecheck-2.0-tools.js +14 -1
  123. package/package.json +1 -1
  124. package/bin/runners/lib/agent-firewall/learning/learning-engine.js +0 -849
  125. package/bin/runners/lib/audit-logger.js +0 -532
  126. package/bin/runners/lib/authority/authorities/architecture.js +0 -364
  127. package/bin/runners/lib/authority/authorities/compliance.js +0 -341
  128. package/bin/runners/lib/authority/authorities/human.js +0 -343
  129. package/bin/runners/lib/authority/authorities/quality.js +0 -420
  130. package/bin/runners/lib/authority/authorities/security.js +0 -228
  131. package/bin/runners/lib/authority/index.js +0 -293
  132. package/bin/runners/lib/bundle/bundle-intelligence.js +0 -846
  133. package/bin/runners/lib/cli-charts.js +0 -368
  134. package/bin/runners/lib/cli-config-display.js +0 -405
  135. package/bin/runners/lib/cli-demo.js +0 -275
  136. package/bin/runners/lib/cli-errors.js +0 -438
  137. package/bin/runners/lib/cli-help-formatter.js +0 -439
  138. package/bin/runners/lib/cli-interactive-menu.js +0 -509
  139. package/bin/runners/lib/cli-prompts.js +0 -441
  140. package/bin/runners/lib/cli-scan-cards.js +0 -362
  141. package/bin/runners/lib/compliance-reporter.js +0 -710
  142. package/bin/runners/lib/conductor/index.js +0 -671
  143. package/bin/runners/lib/easy/README.md +0 -123
  144. package/bin/runners/lib/easy/index.js +0 -140
  145. package/bin/runners/lib/easy/interactive-wizard.js +0 -788
  146. package/bin/runners/lib/easy/one-click-firewall.js +0 -564
  147. package/bin/runners/lib/easy/zero-config-reality.js +0 -714
  148. package/bin/runners/lib/engines/async-patterns-engine.js +0 -444
  149. package/bin/runners/lib/engines/bundle-size-engine.js +0 -433
  150. package/bin/runners/lib/engines/confidence-scoring.js +0 -276
  151. package/bin/runners/lib/engines/context-detection.js +0 -264
  152. package/bin/runners/lib/engines/database-patterns-engine.js +0 -429
  153. package/bin/runners/lib/engines/duplicate-code-engine.js +0 -354
  154. package/bin/runners/lib/engines/env-variables-engine.js +0 -458
  155. package/bin/runners/lib/engines/error-handling-engine.js +0 -437
  156. package/bin/runners/lib/engines/false-positive-prevention.js +0 -630
  157. package/bin/runners/lib/engines/framework-adapters/index.js +0 -607
  158. package/bin/runners/lib/engines/framework-detection.js +0 -508
  159. package/bin/runners/lib/engines/import-order-engine.js +0 -429
  160. package/bin/runners/lib/engines/naming-conventions-engine.js +0 -544
  161. package/bin/runners/lib/engines/noise-reduction-engine.js +0 -452
  162. package/bin/runners/lib/engines/orchestrator.js +0 -334
  163. package/bin/runners/lib/engines/react-patterns-engine.js +0 -457
  164. package/bin/runners/lib/engines/vibecheck-engines/lib/ai-hallucination-engine.js +0 -806
  165. package/bin/runners/lib/engines/vibecheck-engines/lib/smart-fix-engine.js +0 -577
  166. package/bin/runners/lib/engines/vibecheck-engines/lib/vibe-score-engine.js +0 -543
  167. package/bin/runners/lib/engines/vibecheck-engines.js +0 -514
  168. package/bin/runners/lib/enhanced-features/index.js +0 -305
  169. package/bin/runners/lib/enhanced-output.js +0 -631
  170. package/bin/runners/lib/enterprise.js +0 -300
  171. package/bin/runners/lib/firewall/command-validator.js +0 -351
  172. package/bin/runners/lib/firewall/config.js +0 -341
  173. package/bin/runners/lib/firewall/content-validator.js +0 -519
  174. package/bin/runners/lib/firewall/index.js +0 -101
  175. package/bin/runners/lib/firewall/path-validator.js +0 -256
  176. package/bin/runners/lib/intelligence/cross-repo-intelligence.js +0 -817
  177. package/bin/runners/lib/mcp-utils.js +0 -425
  178. package/bin/runners/lib/output/index.js +0 -1022
  179. package/bin/runners/lib/policy-engine.js +0 -652
  180. package/bin/runners/lib/polish/autofix/accessibility-fixes.js +0 -333
  181. package/bin/runners/lib/polish/autofix/async-handlers.js +0 -273
  182. package/bin/runners/lib/polish/autofix/dead-code.js +0 -280
  183. package/bin/runners/lib/polish/autofix/imports-optimizer.js +0 -344
  184. package/bin/runners/lib/polish/autofix/index.js +0 -200
  185. package/bin/runners/lib/polish/autofix/remove-consoles.js +0 -209
  186. package/bin/runners/lib/polish/autofix/strengthen-types.js +0 -245
  187. package/bin/runners/lib/polish/backend-checks.js +0 -148
  188. package/bin/runners/lib/polish/documentation-checks.js +0 -111
  189. package/bin/runners/lib/polish/frontend-checks.js +0 -168
  190. package/bin/runners/lib/polish/index.js +0 -71
  191. package/bin/runners/lib/polish/infrastructure-checks.js +0 -131
  192. package/bin/runners/lib/polish/library-detection.js +0 -175
  193. package/bin/runners/lib/polish/performance-checks.js +0 -100
  194. package/bin/runners/lib/polish/security-checks.js +0 -148
  195. package/bin/runners/lib/polish/utils.js +0 -203
  196. package/bin/runners/lib/prompt-builder.js +0 -540
  197. package/bin/runners/lib/proof-certificate.js +0 -634
  198. package/bin/runners/lib/reality/accessibility-audit.js +0 -946
  199. package/bin/runners/lib/reality/api-contract-validator.js +0 -1012
  200. package/bin/runners/lib/reality/chaos-engineering.js +0 -1084
  201. package/bin/runners/lib/reality/performance-tracker.js +0 -1077
  202. package/bin/runners/lib/reality/scenario-generator.js +0 -1404
  203. package/bin/runners/lib/reality/visual-regression.js +0 -852
  204. package/bin/runners/lib/reality-profiler.js +0 -717
  205. package/bin/runners/lib/replay/flight-recorder-viewer.js +0 -1160
  206. package/bin/runners/lib/review/ai-code-review.js +0 -832
  207. package/bin/runners/lib/rules/custom-rule-engine.js +0 -985
  208. package/bin/runners/lib/sbom-generator.js +0 -641
  209. package/bin/runners/lib/scan-output-enhanced.js +0 -512
  210. package/bin/runners/lib/security/owasp-scanner.js +0 -939
  211. package/bin/runners/lib/validators/contract-validator.js +0 -283
  212. package/bin/runners/lib/validators/dead-export-detector.js +0 -279
  213. package/bin/runners/lib/validators/dep-audit.js +0 -245
  214. package/bin/runners/lib/validators/env-validator.js +0 -319
  215. package/bin/runners/lib/validators/index.js +0 -120
  216. package/bin/runners/lib/validators/license-checker.js +0 -252
  217. package/bin/runners/lib/validators/route-validator.js +0 -290
  218. package/bin/runners/runAuthority.js +0 -528
  219. package/bin/runners/runConductor.js +0 -772
  220. package/bin/runners/runContainer.js +0 -366
  221. package/bin/runners/runEasy.js +0 -410
  222. package/bin/runners/runIaC.js +0 -372
  223. package/bin/runners/runVibe.js +0 -791
  224. package/mcp-server/tools.js +0 -495
@@ -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
+ };