@vibecheckai/cli 3.4.0 → 3.5.1

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 (228) hide show
  1. package/bin/registry.js +154 -338
  2. package/bin/runners/context/generators/mcp.js +13 -15
  3. package/bin/runners/context/proof-context.js +1 -248
  4. package/bin/runners/lib/analysis-core.js +180 -198
  5. package/bin/runners/lib/analyzers.js +223 -1669
  6. package/bin/runners/lib/cli-output.js +210 -242
  7. package/bin/runners/lib/detectors-v2.js +785 -547
  8. package/bin/runners/lib/entitlements-v2.js +458 -96
  9. package/bin/runners/lib/error-handler.js +9 -16
  10. package/bin/runners/lib/global-flags.js +0 -37
  11. package/bin/runners/lib/route-truth.js +322 -1167
  12. package/bin/runners/lib/scan-output.js +469 -448
  13. package/bin/runners/lib/ship-output.js +27 -280
  14. package/bin/runners/lib/terminal-ui.js +733 -231
  15. package/bin/runners/lib/truth.js +321 -1004
  16. package/bin/runners/lib/unified-output.js +158 -162
  17. package/bin/runners/lib/upsell.js +204 -104
  18. package/bin/runners/runAllowlist.js +324 -0
  19. package/bin/runners/runAuth.js +95 -324
  20. package/bin/runners/runCheckpoint.js +21 -39
  21. package/bin/runners/runContext.js +24 -136
  22. package/bin/runners/runDoctor.js +67 -115
  23. package/bin/runners/runEvidencePack.js +219 -0
  24. package/bin/runners/runFix.js +5 -6
  25. package/bin/runners/runGuard.js +118 -212
  26. package/bin/runners/runInit.js +2 -14
  27. package/bin/runners/runInstall.js +281 -0
  28. package/bin/runners/runLabs.js +341 -0
  29. package/bin/runners/runMcp.js +52 -130
  30. package/bin/runners/runPolish.js +20 -43
  31. package/bin/runners/runProve.js +3 -13
  32. package/bin/runners/runReality.js +0 -14
  33. package/bin/runners/runReport.js +2 -3
  34. package/bin/runners/runScan.js +44 -511
  35. package/bin/runners/runShip.js +14 -28
  36. package/bin/runners/runValidate.js +2 -19
  37. package/bin/runners/runWatch.js +54 -118
  38. package/bin/vibecheck.js +41 -148
  39. package/mcp-server/ARCHITECTURE.md +339 -0
  40. package/mcp-server/__tests__/cache.test.ts +313 -0
  41. package/mcp-server/__tests__/executor.test.ts +239 -0
  42. package/mcp-server/__tests__/fixtures/exclusion-test/.cache/webpack/cache.pack +1 -0
  43. package/mcp-server/__tests__/fixtures/exclusion-test/.next/server/chunk.js +3 -0
  44. package/mcp-server/__tests__/fixtures/exclusion-test/.turbo/cache.json +3 -0
  45. package/mcp-server/__tests__/fixtures/exclusion-test/.venv/lib/env.py +3 -0
  46. package/mcp-server/__tests__/fixtures/exclusion-test/dist/bundle.js +3 -0
  47. package/mcp-server/__tests__/fixtures/exclusion-test/package.json +5 -0
  48. package/mcp-server/__tests__/fixtures/exclusion-test/src/app.ts +5 -0
  49. package/mcp-server/__tests__/fixtures/exclusion-test/venv/lib/config.py +4 -0
  50. package/mcp-server/__tests__/ids.test.ts +345 -0
  51. package/mcp-server/__tests__/integration/tools.test.ts +410 -0
  52. package/mcp-server/__tests__/registry.test.ts +365 -0
  53. package/mcp-server/__tests__/sandbox.test.ts +323 -0
  54. package/mcp-server/__tests__/schemas.test.ts +372 -0
  55. package/mcp-server/benchmarks/run-benchmarks.ts +304 -0
  56. package/mcp-server/examples/doctor.request.json +14 -0
  57. package/mcp-server/examples/doctor.response.json +53 -0
  58. package/mcp-server/examples/error.response.json +15 -0
  59. package/mcp-server/examples/scan.request.json +14 -0
  60. package/mcp-server/examples/scan.response.json +108 -0
  61. package/mcp-server/handlers/tool-handler.ts +671 -0
  62. package/mcp-server/index-v3.ts +293 -0
  63. package/mcp-server/index.js +1072 -1573
  64. package/mcp-server/index.old.js +4137 -0
  65. package/mcp-server/lib/cache.ts +341 -0
  66. package/mcp-server/lib/errors.ts +346 -0
  67. package/mcp-server/lib/executor.ts +792 -0
  68. package/mcp-server/lib/ids.ts +238 -0
  69. package/mcp-server/lib/logger.ts +368 -0
  70. package/mcp-server/lib/metrics.ts +365 -0
  71. package/mcp-server/lib/sandbox.ts +337 -0
  72. package/mcp-server/lib/validator.ts +229 -0
  73. package/mcp-server/package-lock.json +165 -0
  74. package/mcp-server/package.json +32 -7
  75. package/mcp-server/premium-tools.js +2 -2
  76. package/mcp-server/registry/tools.json +476 -0
  77. package/mcp-server/schemas/error-envelope.schema.json +125 -0
  78. package/mcp-server/schemas/finding.schema.json +167 -0
  79. package/mcp-server/schemas/report-artifact.schema.json +88 -0
  80. package/mcp-server/schemas/run-request.schema.json +75 -0
  81. package/mcp-server/schemas/verdict.schema.json +168 -0
  82. package/mcp-server/tier-auth.d.ts +71 -0
  83. package/mcp-server/tier-auth.js +371 -183
  84. package/mcp-server/truth-context.js +90 -131
  85. package/mcp-server/truth-firewall-tools.js +1000 -1611
  86. package/mcp-server/tsconfig.json +34 -0
  87. package/mcp-server/vibecheck-tools.js +2 -2
  88. package/mcp-server/vitest.config.ts +16 -0
  89. package/package.json +3 -4
  90. package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +0 -474
  91. package/bin/runners/lib/agent-firewall/change-packet/builder.js +0 -488
  92. package/bin/runners/lib/agent-firewall/change-packet/schema.json +0 -228
  93. package/bin/runners/lib/agent-firewall/change-packet/store.js +0 -200
  94. package/bin/runners/lib/agent-firewall/claims/claim-types.js +0 -21
  95. package/bin/runners/lib/agent-firewall/claims/extractor.js +0 -303
  96. package/bin/runners/lib/agent-firewall/claims/patterns.js +0 -24
  97. package/bin/runners/lib/agent-firewall/critic/index.js +0 -151
  98. package/bin/runners/lib/agent-firewall/critic/judge.js +0 -432
  99. package/bin/runners/lib/agent-firewall/critic/prompts.js +0 -305
  100. package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +0 -88
  101. package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +0 -75
  102. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +0 -127
  103. package/bin/runners/lib/agent-firewall/evidence/resolver.js +0 -102
  104. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +0 -213
  105. package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +0 -145
  106. package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +0 -19
  107. package/bin/runners/lib/agent-firewall/fs-hook/installer.js +0 -87
  108. package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +0 -184
  109. package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +0 -163
  110. package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +0 -107
  111. package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +0 -68
  112. package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +0 -66
  113. package/bin/runners/lib/agent-firewall/interceptor/base.js +0 -304
  114. package/bin/runners/lib/agent-firewall/interceptor/cursor.js +0 -35
  115. package/bin/runners/lib/agent-firewall/interceptor/vscode.js +0 -35
  116. package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +0 -34
  117. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +0 -465
  118. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +0 -604
  119. package/bin/runners/lib/agent-firewall/lawbook/index.js +0 -304
  120. package/bin/runners/lib/agent-firewall/lawbook/registry.js +0 -514
  121. package/bin/runners/lib/agent-firewall/lawbook/schema.js +0 -420
  122. package/bin/runners/lib/agent-firewall/logger.js +0 -141
  123. package/bin/runners/lib/agent-firewall/policy/default-policy.json +0 -90
  124. package/bin/runners/lib/agent-firewall/policy/engine.js +0 -103
  125. package/bin/runners/lib/agent-firewall/policy/loader.js +0 -451
  126. package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +0 -50
  127. package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +0 -50
  128. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +0 -86
  129. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +0 -162
  130. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +0 -189
  131. package/bin/runners/lib/agent-firewall/policy/rules/scope.js +0 -93
  132. package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +0 -57
  133. package/bin/runners/lib/agent-firewall/policy/schema.json +0 -183
  134. package/bin/runners/lib/agent-firewall/policy/verdict.js +0 -54
  135. package/bin/runners/lib/agent-firewall/proposal/extractor.js +0 -394
  136. package/bin/runners/lib/agent-firewall/proposal/index.js +0 -212
  137. package/bin/runners/lib/agent-firewall/proposal/schema.js +0 -251
  138. package/bin/runners/lib/agent-firewall/proposal/validator.js +0 -386
  139. package/bin/runners/lib/agent-firewall/reality/index.js +0 -332
  140. package/bin/runners/lib/agent-firewall/reality/state.js +0 -625
  141. package/bin/runners/lib/agent-firewall/reality/watcher.js +0 -322
  142. package/bin/runners/lib/agent-firewall/risk/index.js +0 -173
  143. package/bin/runners/lib/agent-firewall/risk/scorer.js +0 -328
  144. package/bin/runners/lib/agent-firewall/risk/thresholds.js +0 -321
  145. package/bin/runners/lib/agent-firewall/risk/vectors.js +0 -421
  146. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +0 -472
  147. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +0 -346
  148. package/bin/runners/lib/agent-firewall/simulator/index.js +0 -181
  149. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +0 -380
  150. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +0 -661
  151. package/bin/runners/lib/agent-firewall/time-machine/index.js +0 -267
  152. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +0 -436
  153. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +0 -490
  154. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +0 -530
  155. package/bin/runners/lib/agent-firewall/truthpack/index.js +0 -67
  156. package/bin/runners/lib/agent-firewall/truthpack/loader.js +0 -137
  157. package/bin/runners/lib/agent-firewall/unblock/planner.js +0 -337
  158. package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +0 -118
  159. package/bin/runners/lib/api-client.js +0 -269
  160. package/bin/runners/lib/authority-badge.js +0 -425
  161. package/bin/runners/lib/engines/accessibility-engine.js +0 -190
  162. package/bin/runners/lib/engines/api-consistency-engine.js +0 -162
  163. package/bin/runners/lib/engines/ast-cache.js +0 -99
  164. package/bin/runners/lib/engines/code-quality-engine.js +0 -255
  165. package/bin/runners/lib/engines/console-logs-engine.js +0 -115
  166. package/bin/runners/lib/engines/cross-file-analysis-engine.js +0 -268
  167. package/bin/runners/lib/engines/dead-code-engine.js +0 -198
  168. package/bin/runners/lib/engines/deprecated-api-engine.js +0 -226
  169. package/bin/runners/lib/engines/empty-catch-engine.js +0 -150
  170. package/bin/runners/lib/engines/file-filter.js +0 -131
  171. package/bin/runners/lib/engines/hardcoded-secrets-engine.js +0 -251
  172. package/bin/runners/lib/engines/mock-data-engine.js +0 -272
  173. package/bin/runners/lib/engines/parallel-processor.js +0 -71
  174. package/bin/runners/lib/engines/performance-issues-engine.js +0 -265
  175. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +0 -243
  176. package/bin/runners/lib/engines/todo-fixme-engine.js +0 -115
  177. package/bin/runners/lib/engines/type-aware-engine.js +0 -152
  178. package/bin/runners/lib/engines/unsafe-regex-engine.js +0 -225
  179. package/bin/runners/lib/engines/vibecheck-engines/README.md +0 -53
  180. package/bin/runners/lib/engines/vibecheck-engines/index.js +0 -15
  181. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +0 -164
  182. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +0 -291
  183. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +0 -83
  184. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +0 -198
  185. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +0 -275
  186. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +0 -167
  187. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +0 -217
  188. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +0 -139
  189. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +0 -140
  190. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +0 -164
  191. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +0 -234
  192. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +0 -217
  193. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +0 -78
  194. package/bin/runners/lib/engines/vibecheck-engines/package.json +0 -13
  195. package/bin/runners/lib/exit-codes.js +0 -275
  196. package/bin/runners/lib/fingerprint.js +0 -377
  197. package/bin/runners/lib/help-formatter.js +0 -413
  198. package/bin/runners/lib/logger.js +0 -38
  199. package/bin/runners/lib/ship-output-enterprise.js +0 -239
  200. package/bin/runners/lib/unified-cli-output.js +0 -604
  201. package/bin/runners/runAgent.d.ts +0 -5
  202. package/bin/runners/runAgent.js +0 -161
  203. package/bin/runners/runApprove.js +0 -1200
  204. package/bin/runners/runClassify.js +0 -859
  205. package/bin/runners/runContext.d.ts +0 -4
  206. package/bin/runners/runFirewall.d.ts +0 -5
  207. package/bin/runners/runFirewall.js +0 -134
  208. package/bin/runners/runFirewallHook.d.ts +0 -5
  209. package/bin/runners/runFirewallHook.js +0 -56
  210. package/bin/runners/runPolish.d.ts +0 -4
  211. package/bin/runners/runProof.zip +0 -0
  212. package/bin/runners/runTruth.d.ts +0 -5
  213. package/bin/runners/runTruth.js +0 -101
  214. package/mcp-server/HARDENING_SUMMARY.md +0 -299
  215. package/mcp-server/agent-firewall-interceptor.js +0 -500
  216. package/mcp-server/authority-tools.js +0 -569
  217. package/mcp-server/conductor/conflict-resolver.js +0 -588
  218. package/mcp-server/conductor/execution-planner.js +0 -544
  219. package/mcp-server/conductor/index.js +0 -377
  220. package/mcp-server/conductor/lock-manager.js +0 -615
  221. package/mcp-server/conductor/request-queue.js +0 -550
  222. package/mcp-server/conductor/session-manager.js +0 -500
  223. package/mcp-server/conductor/tools.js +0 -510
  224. package/mcp-server/lib/api-client.cjs +0 -13
  225. package/mcp-server/lib/logger.cjs +0 -30
  226. package/mcp-server/logger.js +0 -173
  227. package/mcp-server/tools-v3.js +0 -706
  228. package/mcp-server/vibecheck-mcp-server-3.2.0.tgz +0 -0
@@ -1,1155 +1,322 @@
1
1
  /**
2
- * Route Truth v1 - JavaScript Runtime (hardened + more accurate)
3
- *
4
- * Upgrades vs your version:
5
- * - Next.js App/Pages routes: AST-based export detection + safer path derivation
6
- * - Fastify routes: AST-based extraction (get/post/route/register + inline plugins + relative import resolution)
7
- * - Better canonicalization + prefix joining + safer matching (no weird edge crashes)
8
- * - Gaps are real (unresolved plugins/modules) instead of silent “false”
2
+ * Route Truth v1 - JavaScript Runtime
3
+ *
4
+ * Generates a normalized route map with evidence from:
5
+ * - Next.js (App Router + Pages Router)
6
+ * - Fastify (shorthand + .route() + register prefixes)
7
+ *
8
+ * Then implements validate_claim(route_exists) on top of it.
9
9
  */
10
10
 
11
- const fs = require("fs");
12
- const path = require("path");
13
- const crypto = require("crypto");
14
-
15
- let fg = null;
16
- try {
17
- fg = require("fast-glob");
18
- } catch { /* optional */ }
19
-
20
- const parser = require("@babel/parser");
21
- const traverse = require("@babel/traverse").default;
22
- const t = require("@babel/types");
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const crypto = require('crypto');
23
14
 
24
15
  // ============================================================================
25
- // CANONICALIZATION + MATCHING
16
+ // CANONICALIZATION
26
17
  // ============================================================================
27
18
 
19
+ /**
20
+ * Canonicalize a path to standard format.
21
+ */
28
22
  function canonicalizePath(p) {
29
- let s = String(p || "").trim();
30
- if (!s) return "/";
31
- if (!s.startsWith("/")) s = "/" + s;
32
- s = s.replace(/\/+/g, "/");
33
-
34
- // Next.js dynamic segments
35
- s = s.replace(/\[\[\.{3}([^\]]+)\]\]/g, "*$1?"); // [[...slug]] -> *slug?
36
- s = s.replace(/\[\.{3}([^\]]+)\]/g, "*$1"); // [...slug] -> *slug
37
- s = s.replace(/\[([^\]]+)\]/g, ":$1"); // [id] -> :id
38
-
39
- if (s.length > 1) s = s.replace(/\/$/, "");
23
+ let s = p.trim();
24
+ if (!s.startsWith('/')) s = '/' + s;
25
+ s = s.replace(/\/+/g, '/');
26
+
27
+ // Convert Next.js dynamic segments
28
+ s = s.replace(/\[\[\.{3}([^\]]+)\]\]/g, '*$1?'); // [[...slug]] → *slug?
29
+ s = s.replace(/\[\.{3}([^\]]+)\]/g, '*$1'); // [...slug] *slug
30
+ s = s.replace(/\[([^\]]+)\]/g, ':$1'); // [id] → :id
31
+
32
+ if (s.length > 1) s = s.replace(/\/$/, '');
40
33
  return s;
41
34
  }
42
35
 
43
36
  function canonicalizeMethod(m) {
44
- const u = String(m || "").toUpperCase();
45
- if (u === "ALL" || u === "ANY" || u === "*" ) return "*";
46
- return u || "*";
37
+ const u = m.toUpperCase();
38
+ if (u === 'ALL' || u === 'ANY') return '*';
39
+ return u;
47
40
  }
48
41
 
49
42
  function joinPrefix(prefix, p) {
50
- const a = canonicalizePath(prefix || "/");
51
- const b = canonicalizePath(p || "/");
52
- if (a === "/") return b;
53
- if (b === "/") return a;
54
- return canonicalizePath(a + "/" + b);
43
+ const cleanPrefix = prefix.replace(/\/$/, '');
44
+ const cleanPath = p.startsWith('/') ? p : '/' + p;
45
+ return canonicalizePath(cleanPrefix + cleanPath);
55
46
  }
56
47
 
57
- function isParameterizedPath(p) {
58
- const s = canonicalizePath(p);
59
- return s.includes(":") || s.includes("*");
48
+ function isParameterizedPath(path) {
49
+ return path.includes(':') || path.includes('*');
60
50
  }
61
51
 
62
- /**
63
- * Match a pattern against a concrete path.
64
- * Supported:
65
- * - :id matches one segment
66
- * - *slug or *slug? matches the rest of the path (0+ segments)
67
- */
68
52
  function matchPath(pattern, concrete) {
69
- const pat = canonicalizePath(pattern);
70
- const con = canonicalizePath(concrete);
71
-
72
- if (pat === con) return true;
73
-
74
- const pParts = pat.split("/").filter(Boolean);
75
- const cParts = con.split("/").filter(Boolean);
76
-
53
+ const patternParts = pattern.split('/');
54
+ const concreteParts = concrete.split('/');
55
+
77
56
  let pIdx = 0, cIdx = 0;
78
-
79
- while (pIdx < pParts.length && cIdx < cParts.length) {
80
- const pSeg = pParts[pIdx];
81
- const cSeg = cParts[cIdx];
82
-
83
- if (pSeg.startsWith("*")) {
84
- // splat: match remainder (including empty remainder)
85
- return true;
86
- }
87
- if (pSeg.startsWith(":")) {
88
- pIdx++; cIdx++; continue;
89
- }
90
- if (pSeg !== cSeg) return false;
91
-
57
+
58
+ while (pIdx < patternParts.length && cIdx < concreteParts.length) {
59
+ const pPart = patternParts[pIdx];
60
+ const cPart = concreteParts[cIdx];
61
+
62
+ if (pPart.startsWith('*')) return true;
63
+ if (pPart.startsWith(':')) { pIdx++; cIdx++; continue; }
64
+ if (pPart !== cPart) return false;
92
65
  pIdx++; cIdx++;
93
66
  }
94
-
95
- // If pattern still has a trailing splat, it matches empty remainder
96
- if (pIdx === pParts.length - 1 && pParts[pIdx]?.startsWith("*")) return true;
97
-
98
- return pIdx === pParts.length && cIdx === cParts.length;
67
+
68
+ return pIdx === patternParts.length && cIdx === concreteParts.length;
99
69
  }
100
70
 
101
71
  function matchMethod(pattern, concrete) {
102
- const p = canonicalizeMethod(pattern);
103
- const c = canonicalizeMethod(concrete);
104
- if (p === "*") return true;
105
- return p === c;
72
+ if (pattern === '*') return true;
73
+ return pattern === concrete;
106
74
  }
107
75
 
108
76
  // ============================================================================
109
- // COMMON HELPERS
77
+ // NEXT.JS RESOLVER
110
78
  // ============================================================================
111
79
 
112
- const NEXT_HTTP_METHODS = new Set(["GET","POST","PUT","PATCH","DELETE","OPTIONS","HEAD"]);
80
+ const NEXT_HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
113
81
  let evidenceCounter = 0;
114
82
 
115
- function sha256Short(txt) {
116
- return crypto.createHash("sha256").update(String(txt || "")).digest("hex").slice(0, 16);
117
- }
118
-
119
83
  function createEvidence(file, lines, reason, snippet) {
120
84
  evidenceCounter++;
121
85
  return {
122
- id: `ev_${String(evidenceCounter).padStart(4, "0")}`,
86
+ id: `ev_${String(evidenceCounter).padStart(4, '0')}`,
123
87
  file,
124
88
  lines,
125
- snippetHash: `sha256:${sha256Short(snippet || "")}`,
89
+ snippetHash: `sha256:${crypto.createHash('sha256').update(snippet || '').digest('hex').slice(0, 16)}`,
126
90
  reason,
127
91
  };
128
92
  }
129
93
 
130
- function safeRead(fileAbs) {
131
- try { return fs.readFileSync(fileAbs, "utf8"); } catch { return null; }
132
- }
133
-
134
- function parseFile(code) {
135
- return parser.parse(code, {
136
- sourceType: "unambiguous",
137
- plugins: ["typescript", "jsx"],
138
- errorRecovery: true,
139
- ranges: false,
140
- });
141
- }
142
-
143
- function evidenceFromLoc({ fileAbs, fileRel, loc, reason }) {
144
- if (!loc || !loc.start) return [];
145
- const code = safeRead(fileAbs);
146
- if (!code) return [];
147
- const lines = code.split(/\r?\n/);
148
- const start = Math.max(1, loc.start.line || 1);
149
- const end = Math.max(start, loc.end?.line || start);
150
- const snippet = lines.slice(start - 1, end).join("\n");
151
- return [createEvidence(fileRel, `${start}-${end}`, reason, snippet)];
152
- }
153
-
154
- function findFilesFallback(dirAbs, includeRe, excludeRe) {
155
- const out = [];
94
+ function findFiles(dir, include, exclude) {
95
+ const files = [];
96
+
156
97
  function walk(d) {
157
- let entries;
158
- try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
159
- for (const ent of entries) {
160
- const full = path.join(d, ent.name);
161
- if (ent.isDirectory()) {
162
- if (
163
- ent.name === "node_modules" ||
164
- ent.name === ".next" ||
165
- ent.name === "dist" ||
166
- ent.name === "build" ||
167
- ent.name === "coverage" ||
168
- ent.name.startsWith(".")
169
- ) continue;
170
- walk(full);
171
- } else if (ent.isFile()) {
172
- if (excludeRe && excludeRe.test(ent.name)) continue;
173
- if (includeRe.test(ent.name)) out.push(full);
98
+ try {
99
+ const entries = fs.readdirSync(d, { withFileTypes: true });
100
+ for (const entry of entries) {
101
+ const fullPath = path.join(d, entry.name);
102
+ if (entry.isDirectory()) {
103
+ if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
104
+ walk(fullPath);
105
+ }
106
+ } else if (entry.isFile()) {
107
+ if (include.test(entry.name) && (!exclude || !exclude.test(entry.name))) {
108
+ files.push(fullPath);
109
+ }
110
+ }
174
111
  }
175
- }
112
+ } catch {}
176
113
  }
177
- walk(dirAbs);
178
- return out;
114
+
115
+ walk(dir);
116
+ return files;
179
117
  }
180
118
 
181
- async function globFiles(repoRoot, patterns, ignore) {
182
- if (fg) {
183
- return fg(patterns, {
184
- cwd: repoRoot,
185
- absolute: true,
186
- dot: false,
187
- ignore: ignore || [
188
- "**/node_modules/**",
189
- "**/.next/**",
190
- "**/dist/**",
191
- "**/build/**",
192
- "**/coverage/**",
193
- ],
194
- });
195
- }
196
-
197
- // fallback: only supports the specific Next patterns we use
198
- const out = [];
199
- for (const ptn of patterns) {
200
- // minimal handling: find root dirs from patterns
201
- if (ptn.includes("app/api") || ptn.includes("src/app/api")) {
202
- const dir1 = path.join(repoRoot, "app", "api");
203
- const dir2 = path.join(repoRoot, "src", "app", "api");
204
- if (fs.existsSync(dir1)) out.push(...findFilesFallback(dir1, /route\.(ts|js)$/));
205
- if (fs.existsSync(dir2)) out.push(...findFilesFallback(dir2, /route\.(ts|js)$/));
206
- }
207
- if (ptn.includes("pages/api") || ptn.includes("src/pages/api")) {
208
- const dir1 = path.join(repoRoot, "pages", "api");
209
- const dir2 = path.join(repoRoot, "src", "pages", "api");
210
- if (fs.existsSync(dir1)) out.push(...findFilesFallback(dir1, /\.(ts|js)$/, /\.d\.ts$/));
211
- if (fs.existsSync(dir2)) out.push(...findFilesFallback(dir2, /\.(ts|js)$/, /\.d\.ts$/));
212
- }
213
- }
214
- return Array.from(new Set(out));
215
- }
216
-
217
- // ============================================================================
218
- // NEXT.JS RESOLVER (AST)
219
- // ============================================================================
220
-
221
- function extractNextAppRouterMethodsAST(ast) {
119
+ function extractAppRouterMethods(code) {
222
120
  const methods = [];
223
- traverse(ast, {
224
- ExportNamedDeclaration(p) {
225
- const decl = p.node.declaration;
226
-
227
- // export function GET() {}
228
- if (t.isFunctionDeclaration(decl) && decl.id?.name) {
229
- const n = decl.id.name.toUpperCase();
230
- if (NEXT_HTTP_METHODS.has(n)) {
231
- methods.push({ name: n, loc: decl.loc });
232
- }
233
- }
234
-
235
- // export const GET = () => {}
236
- if (t.isVariableDeclaration(decl)) {
237
- for (const d of decl.declarations) {
238
- if (!t.isIdentifier(d.id)) continue;
239
- const n = d.id.name.toUpperCase();
240
- if (NEXT_HTTP_METHODS.has(n)) {
241
- methods.push({ name: n, loc: d.loc || decl.loc });
242
- }
243
- }
121
+ const lines = code.split('\n');
122
+
123
+ const patterns = [
124
+ /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(/,
125
+ /export\s+const\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*=/,
126
+ ];
127
+
128
+ for (let i = 0; i < lines.length; i++) {
129
+ for (const pattern of patterns) {
130
+ const match = lines[i].match(pattern);
131
+ if (match && NEXT_HTTP_METHODS.includes(match[1].toUpperCase())) {
132
+ methods.push({
133
+ name: match[1].toUpperCase(),
134
+ line: i + 1,
135
+ snippet: lines[i].trim(),
136
+ });
244
137
  }
245
- },
246
- });
138
+ }
139
+ }
140
+
247
141
  return methods;
248
142
  }
249
143
 
250
- function deriveNextAppRoutePath(fileRel) {
251
- // matches: app/api/**/route.ts|js OR src/app/api/**/route.ts|js
252
- const m = fileRel.match(/(?:^|\/)(?:src\/)?app\/api\/(.+)\/route\.(ts|js)$/);
253
- if (!m) return null;
254
- const sub = m[1];
255
- return canonicalizePath("/api/" + sub);
256
- }
257
-
258
- function deriveNextPagesRoutePath(fileRel) {
259
- // matches: pages/api/**.ts|js OR src/pages/api/**.ts|js
260
- const m = fileRel.match(/(?:^|\/)(?:src\/)?pages\/api\/(.+)\.(ts|js)$/);
261
- if (!m) return null;
262
- let sub = m[1];
263
- sub = sub.replace(/\/index$/, ""); // /foo/index -> /foo
264
- return canonicalizePath("/api/" + sub);
265
- }
266
-
267
144
  async function resolveNextRoutes(repoRoot) {
268
145
  const routes = [];
269
-
270
- // App Router
271
- const appFiles = await globFiles(repoRoot, [
272
- "**/app/api/**/route.@(ts|js)",
273
- "**/src/app/api/**/route.@(ts|js)",
274
- ]);
275
-
276
- for (const fileAbs of appFiles) {
277
- const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
278
- const routePath = deriveNextAppRoutePath(fileRel);
279
- if (!routePath) continue;
280
-
281
- const code = safeRead(fileAbs);
282
- if (!code) continue;
283
-
284
- let ast;
285
- try { ast = parseFile(code); } catch { continue; }
286
-
287
- const methods = extractNextAppRouterMethodsAST(ast);
288
-
289
- if (methods.length === 0) {
290
- routes.push({
291
- method: "*",
292
- path: routePath,
293
- handler: fileRel,
294
- framework: "next",
295
- routerType: "app",
296
- confidence: "low",
297
- evidence: [createEvidence(fileRel, "1", "route file with no method exports", code.slice(0, 140))],
298
- });
299
- continue;
146
+
147
+ // App Router: app/api/**/route.ts|js
148
+ const appDirs = ['app', 'src/app'];
149
+ for (const appDir of appDirs) {
150
+ const apiDir = path.join(repoRoot, appDir, 'api');
151
+ if (!fs.existsSync(apiDir)) continue;
152
+
153
+ const routeFiles = findFiles(apiDir, /route\.(ts|js)$/);
154
+
155
+ for (const file of routeFiles) {
156
+ const relPath = path.relative(repoRoot, file).replace(/\\/g, '/');
157
+ const apiIdx = relPath.indexOf('/api/');
158
+ const sub = relPath.slice(apiIdx + '/api/'.length).replace(/\/route\.(ts|js)$/, '');
159
+ const routePath = canonicalizePath('/api/' + sub);
160
+
161
+ const code = fs.readFileSync(file, 'utf8');
162
+ const methods = extractAppRouterMethods(code);
163
+
164
+ if (methods.length === 0) {
165
+ routes.push({
166
+ method: '*',
167
+ path: routePath,
168
+ handler: relPath,
169
+ framework: 'next',
170
+ routerType: 'app',
171
+ confidence: 'low',
172
+ evidence: [createEvidence(relPath, '1', 'route file with no exports', code.slice(0, 100))],
173
+ });
174
+ continue;
175
+ }
176
+
177
+ for (const m of methods) {
178
+ routes.push({
179
+ method: m.name,
180
+ path: routePath,
181
+ handler: `${relPath}:${m.line}`,
182
+ framework: 'next',
183
+ routerType: 'app',
184
+ confidence: 'high',
185
+ evidence: [createEvidence(relPath, String(m.line), `export ${m.name}`, m.snippet)],
186
+ });
187
+ }
300
188
  }
301
-
302
- for (const m of methods) {
189
+ }
190
+
191
+ // Pages Router: pages/api/**/*.ts|js
192
+ const pagesDirs = ['pages', 'src/pages'];
193
+ for (const pagesDir of pagesDirs) {
194
+ const apiDir = path.join(repoRoot, pagesDir, 'api');
195
+ if (!fs.existsSync(apiDir)) continue;
196
+
197
+ const apiFiles = findFiles(apiDir, /\.(ts|js)$/, /\.d\.ts$/);
198
+
199
+ for (const file of apiFiles) {
200
+ const relPath = path.relative(repoRoot, file).replace(/\\/g, '/');
201
+ const apiIdx = relPath.indexOf('/api/');
202
+ const sub = relPath
203
+ .slice(apiIdx + '/api/'.length)
204
+ .replace(/\.(ts|js)$/, '')
205
+ .replace(/\/index$/, '');
206
+
207
+ const routePath = canonicalizePath('/api/' + sub);
208
+ const code = fs.readFileSync(file, 'utf8');
209
+ const hasDefaultExport = /export\s+default/.test(code);
210
+
303
211
  routes.push({
304
- method: m.name,
212
+ method: '*',
305
213
  path: routePath,
306
- handler: fileRel,
307
- framework: "next",
308
- routerType: "app",
309
- confidence: "high",
310
- evidence: evidenceFromLoc({ fileAbs, fileRel, loc: m.loc, reason: `Next app router export ${m.name}` }),
214
+ handler: relPath,
215
+ framework: 'next',
216
+ routerType: 'pages',
217
+ confidence: hasDefaultExport ? 'med' : 'low',
218
+ evidence: [createEvidence(relPath, '1', 'Pages API route', code.slice(0, 100))],
311
219
  });
312
220
  }
313
221
  }
314
-
315
- // Pages Router
316
- const pagesFiles = await globFiles(repoRoot, [
317
- "**/pages/api/**/*.@(ts|js)",
318
- "**/src/pages/api/**/*.@(ts|js)",
319
- ]);
320
-
321
- for (const fileAbs of pagesFiles) {
322
- const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
323
- if (fileRel.endsWith(".d.ts")) continue;
324
-
325
- const routePath = deriveNextPagesRoutePath(fileRel);
326
- if (!routePath) continue;
327
-
328
- const code = safeRead(fileAbs);
329
- if (!code) continue;
330
-
331
- const hasDefaultExport = /\bexport\s+default\b/.test(code);
332
-
333
- routes.push({
334
- method: "*",
335
- path: routePath,
336
- handler: fileRel,
337
- framework: "next",
338
- routerType: "pages",
339
- confidence: hasDefaultExport ? "med" : "low",
340
- evidence: [createEvidence(fileRel, "1", "Next pages API route", code.slice(0, 140))],
341
- });
342
- }
343
-
222
+
344
223
  return routes;
345
224
  }
346
225
 
347
226
  // ============================================================================
348
- // FASTIFY RESOLVER (AST, follows register prefixes + relative plugin modules)
227
+ // FASTIFY RESOLVER (Simplified - regex based)
349
228
  // ============================================================================
350
229
 
351
- const FASTIFY_METHODS = new Set(["get","post","put","patch","delete","options","head","all"]);
352
-
353
- function existsFile(p) {
354
- try { return fs.statSync(p).isFile(); } catch { return false; }
355
- }
356
-
357
- function existsDir(p) {
358
- try { return fs.statSync(p).isDirectory(); } catch { return false; }
359
- }
360
-
361
- /**
362
- * Find project root by walking up from a file until we find package.json
363
- */
364
- function findProjectRoot(fromFileAbs) {
365
- let dir = path.dirname(fromFileAbs);
366
- const root = path.parse(dir).root;
367
- while (dir !== root) {
368
- if (existsFile(path.join(dir, "package.json"))) return dir;
369
- const parent = path.dirname(dir);
370
- if (parent === dir) break;
371
- dir = parent;
372
- }
373
- return null;
374
- }
375
-
376
- /**
377
- * Load and cache tsconfig.json paths for a project
378
- */
379
- const tsconfigCache = new Map();
380
-
381
- function loadTsConfigPaths(projectRoot) {
382
- if (!projectRoot) return null;
383
- if (tsconfigCache.has(projectRoot)) return tsconfigCache.get(projectRoot);
384
-
385
- const tsconfigPath = path.join(projectRoot, "tsconfig.json");
386
- if (!existsFile(tsconfigPath)) {
387
- tsconfigCache.set(projectRoot, null);
388
- return null;
389
- }
390
-
391
- try {
392
- const raw = fs.readFileSync(tsconfigPath, "utf8");
393
- // Remove comments (// and /* */) for JSON parsing
394
- const cleaned = raw
395
- .replace(/\/\/.*$/gm, "")
396
- .replace(/\/\*[\s\S]*?\*\//g, "");
397
- const tsconfig = JSON.parse(cleaned);
398
-
399
- const paths = tsconfig?.compilerOptions?.paths || {};
400
- const baseUrl = tsconfig?.compilerOptions?.baseUrl || ".";
401
- const baseDir = path.resolve(projectRoot, baseUrl);
402
-
403
- const result = { paths, baseDir, projectRoot };
404
- tsconfigCache.set(projectRoot, result);
405
- return result;
406
- } catch {
407
- tsconfigCache.set(projectRoot, null);
408
- return null;
409
- }
410
- }
411
-
412
- /**
413
- * Resolve a module specifier using TypeScript path mappings
414
- */
415
- function resolveWithTsConfigPaths(spec, tsConfig) {
416
- if (!tsConfig || !tsConfig.paths) return null;
417
-
418
- const { paths, baseDir } = tsConfig;
419
-
420
- for (const [pattern, targets] of Object.entries(paths)) {
421
- // Handle exact match: "@vibecheck/core" -> ["../../packages/core/dist"]
422
- if (pattern === spec) {
423
- for (const target of targets) {
424
- const resolved = path.resolve(baseDir, target.replace(/\/\*$/, ""));
425
- const candidates = [
426
- resolved,
427
- resolved + ".ts",
428
- resolved + ".js",
429
- path.join(resolved, "index.ts"),
430
- path.join(resolved, "index.js"),
431
- ];
432
- for (const c of candidates) if (existsFile(c)) return c;
433
- }
434
- }
435
-
436
- // Handle wildcard pattern: "@/*" -> ["./src/*"]
437
- if (pattern.endsWith("/*")) {
438
- const prefix = pattern.slice(0, -2);
439
- if (spec.startsWith(prefix + "/")) {
440
- const rest = spec.slice(prefix.length + 1);
441
- for (const target of targets) {
442
- const targetBase = target.replace(/\/\*$/, "");
443
- const resolved = path.resolve(baseDir, targetBase, rest);
444
- const candidates = [
445
- resolved,
446
- resolved + ".ts",
447
- resolved + ".js",
448
- path.join(resolved, "index.ts"),
449
- path.join(resolved, "index.js"),
450
- ];
451
- for (const c of candidates) if (existsFile(c)) return c;
452
- }
453
- }
454
- }
455
- }
456
-
457
- return null;
458
- }
459
-
460
- /**
461
- * Parse package.json and find the main entrypoint
462
- */
463
- function getPackageEntrypoint(pkgJsonPath) {
464
- try {
465
- const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
466
- const pkgDir = path.dirname(pkgJsonPath);
467
-
468
- // Priority: exports["."] > main > index.js
469
- // Handle exports field (modern packages)
470
- if (pkgJson.exports) {
471
- const exp = pkgJson.exports;
472
-
473
- // exports: "./lib/index.js" (string shorthand)
474
- if (typeof exp === "string") {
475
- const resolved = path.resolve(pkgDir, exp);
476
- if (existsFile(resolved)) return resolved;
477
- }
478
-
479
- // exports: { ".": "./lib/index.js" } or { ".": { "require": "...", "import": "..." } }
480
- if (typeof exp === "object" && exp["."]) {
481
- const dotExport = exp["."];
482
-
483
- if (typeof dotExport === "string") {
484
- const resolved = path.resolve(pkgDir, dotExport);
485
- if (existsFile(resolved)) return resolved;
486
- }
487
-
488
- // Conditional exports - prefer require for CommonJS, then import, then default
489
- if (typeof dotExport === "object") {
490
- const conditions = ["require", "node", "import", "default"];
491
- for (const cond of conditions) {
492
- if (dotExport[cond]) {
493
- const val = dotExport[cond];
494
- const target = typeof val === "string" ? val : val?.default;
495
- if (target) {
496
- const resolved = path.resolve(pkgDir, target);
497
- if (existsFile(resolved)) return resolved;
498
- }
499
- }
500
- }
501
- }
502
- }
503
- }
504
-
505
- // Fallback to main field
506
- if (pkgJson.main) {
507
- const resolved = path.resolve(pkgDir, pkgJson.main);
508
- if (existsFile(resolved)) return resolved;
509
- }
510
-
511
- // Final fallback: index.js
512
- const indexJs = path.join(pkgDir, "index.js");
513
- if (existsFile(indexJs)) return indexJs;
514
-
515
- return null;
516
- } catch {
517
- return null;
518
- }
519
- }
520
-
521
- /**
522
- * Resolve a package specifier from node_modules
523
- * Handles: "fastify", "@fastify/cors", "@vibecheck/core"
524
- */
525
- function resolveFromNodeModules(spec, projectRoot) {
526
- if (!projectRoot || !spec) return null;
527
-
528
- // Don't resolve built-in Node.js modules
529
- const builtins = new Set([
530
- "fs", "path", "http", "https", "url", "crypto", "os", "util", "stream",
531
- "events", "buffer", "querystring", "child_process", "cluster", "dgram",
532
- "dns", "net", "readline", "tls", "tty", "zlib", "assert", "async_hooks",
533
- "perf_hooks", "v8", "vm", "worker_threads", "module", "process"
534
- ]);
535
-
536
- const pkgName = spec.startsWith("@")
537
- ? spec.split("/").slice(0, 2).join("/") // @scope/name
538
- : spec.split("/")[0]; // name
539
-
540
- if (builtins.has(pkgName)) return null;
541
-
542
- // Walk up directory tree looking for node_modules
543
- let searchDir = projectRoot;
544
- const root = path.parse(searchDir).root;
545
-
546
- while (searchDir !== root) {
547
- const nodeModulesDir = path.join(searchDir, "node_modules");
548
-
549
- if (existsDir(nodeModulesDir)) {
550
- const pkgDir = path.join(nodeModulesDir, pkgName);
551
-
552
- if (existsDir(pkgDir)) {
553
- const pkgJsonPath = path.join(pkgDir, "package.json");
554
-
555
- if (existsFile(pkgJsonPath)) {
556
- // Check if spec has a subpath: "@fastify/cors/lib/foo"
557
- const subpath = spec.slice(pkgName.length);
558
-
559
- if (subpath && subpath !== "/") {
560
- // Resolve subpath within the package
561
- const subpathResolved = path.join(pkgDir, subpath);
562
- const candidates = [
563
- subpathResolved,
564
- subpathResolved + ".js",
565
- subpathResolved + ".ts",
566
- path.join(subpathResolved, "index.js"),
567
- path.join(subpathResolved, "index.ts"),
568
- ];
569
- for (const c of candidates) if (existsFile(c)) return c;
570
- }
571
-
572
- // Resolve main entrypoint
573
- const entry = getPackageEntrypoint(pkgJsonPath);
574
- if (entry) return entry;
575
- }
576
- }
577
- }
578
-
579
- const parent = path.dirname(searchDir);
580
- if (parent === searchDir) break;
581
- searchDir = parent;
582
- }
583
-
584
- return null;
585
- }
586
-
587
- /**
588
- * Check if a package is a "non-route" Fastify plugin
589
- * These plugins add functionality but don't define routes themselves
590
- */
591
- function isNonRoutePlugin(spec) {
592
- const nonRoutePlugins = new Set([
593
- // @fastify/* scoped plugins
594
- "@fastify/cors",
595
- "@fastify/helmet",
596
- "@fastify/compress",
597
- "@fastify/cookie",
598
- "@fastify/secure-session",
599
- "@fastify/session",
600
- "@fastify/rate-limit",
601
- "@fastify/jwt",
602
- "@fastify/auth",
603
- "@fastify/bearer-auth",
604
- "@fastify/basic-auth",
605
- "@fastify/multipart",
606
- "@fastify/formbody",
607
- "@fastify/static",
608
- "@fastify/view",
609
- "@fastify/sensible",
610
- "@fastify/env",
611
- "@fastify/accepts",
612
- "@fastify/caching",
613
- "@fastify/etag",
614
- "@fastify/circuit-breaker",
615
- "@fastify/response-validation",
616
- "@fastify/request-context",
617
- "@fastify/under-pressure",
618
- "@fastify/middie",
619
- "@fastify/express",
620
- "@fastify/http-proxy",
621
- "@fastify/reply-from",
622
- "@fastify/websocket",
623
- "@fastify/type-provider-json-schema-to-ts",
624
- "@fastify/type-provider-typebox",
625
- "@fastify/type-provider-zod",
626
- "@fastify/mongodb",
627
- "@fastify/postgres",
628
- "@fastify/mysql",
629
- "@fastify/redis",
630
- "@fastify/leveldb",
631
- "@fastify/elasticsearch",
632
- "@fastify/metrics",
633
- "@fastify/request-id",
634
- // Legacy fastify-* plugins
635
- "fastify-plugin",
636
- "fastify-cors",
637
- "fastify-helmet",
638
- "fastify-compress",
639
- "fastify-cookie",
640
- "fastify-session",
641
- "fastify-rate-limit",
642
- "fastify-jwt",
643
- "fastify-auth",
644
- "fastify-sensible",
645
- "fastify-multipart",
646
- "fastify-formbody",
647
- "fastify-static",
648
- "fastify-websocket",
649
- ]);
650
-
651
- const pkgName = spec.startsWith("@")
652
- ? spec.split("/").slice(0, 2).join("/")
653
- : spec.split("/")[0];
654
-
655
- return nonRoutePlugins.has(pkgName);
656
- }
657
-
658
- /**
659
- * Check if this is @fastify/autoload which needs special directory handling
660
- */
661
- function isAutoloadPlugin(spec) {
662
- return spec === "@fastify/autoload" || spec === "fastify-autoload";
663
- }
664
-
665
- function resolveRelativeModule(fromFileAbs, spec) {
666
- if (!spec || (!spec.startsWith("./") && !spec.startsWith("../"))) return null;
667
- const base = path.resolve(path.dirname(fromFileAbs), spec);
668
- const candidates = [
669
- base,
670
- base + ".ts",
671
- base + ".js",
672
- path.join(base, "index.ts"),
673
- path.join(base, "index.js"),
230
+ async function resolveFastifyRoutes(repoRoot) {
231
+ const routes = [];
232
+ const gaps = [];
233
+
234
+ const entryPoints = [
235
+ 'src/server.ts', 'src/server.js', 'src/index.ts', 'src/index.js',
236
+ 'server.ts', 'server.js', 'apps/api/src/server.ts', 'apps/api/src/index.ts',
674
237
  ];
675
- for (const c of candidates) if (existsFile(c)) return c;
676
- return null;
677
- }
678
-
679
- /**
680
- * Resolve any module specifier (relative, absolute, package, or TS paths)
681
- * Returns: { resolved: string | null, isNonRoute: boolean, reason: string }
682
- */
683
- function resolveModuleSpec(fromFileAbs, spec) {
684
- if (!spec) return { resolved: null, isNonRoute: false, reason: "empty spec" };
685
-
686
- // 1. Relative imports
687
- if (spec.startsWith("./") || spec.startsWith("../")) {
688
- const resolved = resolveRelativeModule(fromFileAbs, spec);
689
- return {
690
- resolved,
691
- isNonRoute: false,
692
- reason: resolved ? "relative import" : "relative module not found"
693
- };
694
- }
695
-
696
- // 2. Check if it's a non-route plugin (skip scanning)
697
- if (isNonRoutePlugin(spec)) {
698
- return {
699
- resolved: null,
700
- isNonRoute: true,
701
- reason: `non-route plugin: ${spec}`
702
- };
703
- }
704
-
705
- const projectRoot = findProjectRoot(fromFileAbs);
706
-
707
- // 3. TypeScript path mappings
708
- if (projectRoot) {
709
- const tsConfig = loadTsConfigPaths(projectRoot);
710
- if (tsConfig) {
711
- const resolved = resolveWithTsConfigPaths(spec, tsConfig);
712
- if (resolved) {
713
- return { resolved, isNonRoute: false, reason: "tsconfig paths" };
714
- }
715
- }
716
- }
717
-
718
- // 4. Node modules resolution
719
- if (projectRoot) {
720
- const resolved = resolveFromNodeModules(spec, projectRoot);
721
- if (resolved) {
722
- return { resolved, isNonRoute: false, reason: "node_modules" };
723
- }
724
- }
725
-
726
- return {
727
- resolved: null,
728
- isNonRoute: false,
729
- reason: `unresolved package: ${spec}`
730
- };
731
- }
732
-
733
- function extractStringLiteral(node) {
734
- return t.isStringLiteral(node) ? node.value : null;
735
- }
736
-
737
- function extractPrefixFromOpts(node) {
738
- if (!t.isObjectExpression(node)) return null;
739
- for (const p of node.properties) {
740
- if (!t.isObjectProperty(p)) continue;
741
- const key =
742
- t.isIdentifier(p.key) ? p.key.name :
743
- t.isStringLiteral(p.key) ? p.key.value :
744
- null;
745
- if (key === "prefix" && t.isStringLiteral(p.value)) return p.value.value;
746
- }
747
- return null;
748
- }
749
-
750
- function extractRouteObject(objExpr) {
751
- let url = null;
752
- let methods = [];
753
- let hasHandler = false;
754
-
755
- for (const p of objExpr.properties) {
756
- if (!t.isObjectProperty(p)) continue;
757
-
758
- const key =
759
- t.isIdentifier(p.key) ? p.key.name :
760
- t.isStringLiteral(p.key) ? p.key.value :
761
- null;
762
- if (!key) continue;
763
-
764
- if (key === "url" && t.isStringLiteral(p.value)) url = p.value.value;
765
-
766
- if (key === "method") {
767
- if (t.isStringLiteral(p.value)) methods = [p.value.value];
768
- if (t.isArrayExpression(p.value)) {
769
- methods = p.value.elements.filter(e => t.isStringLiteral(e)).map(e => e.value);
770
- }
238
+
239
+ const fastifyMethods = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'all'];
240
+
241
+ // Find source files
242
+ const srcDirs = ['src', 'apps/api/src', 'server'];
243
+ const files = [];
244
+
245
+ for (const srcDir of srcDirs) {
246
+ const fullDir = path.join(repoRoot, srcDir);
247
+ if (fs.existsSync(fullDir)) {
248
+ files.push(...findFiles(fullDir, /\.(ts|js)$/, /\.d\.ts$/));
771
249
  }
772
-
773
- if (key === "handler") hasHandler = true;
774
250
  }
775
-
776
- return { url, methods, hasHandler };
777
- }
778
-
779
- function detectFastifyEntry(repoRoot) {
780
- const candidates = [
781
- "src/server.ts","src/server.js",
782
- "server.ts","server.js",
783
- "src/index.ts","src/index.js",
784
- "index.ts","index.js",
785
- "apps/api/src/server.ts",
786
- "apps/api/src/index.ts",
251
+
252
+ // Patterns to detect routes
253
+ const patterns = [
254
+ // fastify.get('/path', handler)
255
+ /(?:fastify|app|server)\.(get|post|put|patch|delete|options|head|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
256
+ // router.get('/path', handler)
257
+ /router\.(get|post|put|patch|delete|options|head|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
258
+ // .route({ method: 'GET', url: '/path' })
259
+ /\.route\s*\(\s*\{[^}]*method:\s*['"`]([^'"`]+)['"`][^}]*url:\s*['"`]([^'"`]+)['"`]/gi,
260
+ /\.route\s*\(\s*\{[^}]*url:\s*['"`]([^'"`]+)['"`][^}]*method:\s*['"`]([^'"`]+)['"`]/gi,
787
261
  ];
788
- for (const rel of candidates) {
789
- const abs = path.join(repoRoot, rel);
790
- if (existsFile(abs)) return rel;
791
- }
792
- return null;
793
- }
794
-
795
- function resolveFastifyRoutesFromEntry(repoRoot, entryAbs) {
796
- const seen = new Set();
797
- const routes = [];
798
- const gaps = [];
799
-
800
- function scanFile(fileAbs, prefix) {
801
- if (!fileAbs || seen.has(fileAbs)) return;
802
- seen.add(fileAbs);
803
-
804
- const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
805
- const code = safeRead(fileAbs);
806
- if (!code) return;
807
-
808
- let ast;
809
- try { ast = parseFile(code); } catch { return; }
810
-
811
- // fastify instance identifiers
812
- const fastifyNames = new Set(["fastify", "app", "server"]);
813
-
814
- traverse(ast, {
815
- VariableDeclarator(p) {
816
- if (!t.isIdentifier(p.node.id)) return;
817
- const id = p.node.id.name;
818
- const init = p.node.init;
819
- if (!init) return;
820
- if (t.isCallExpression(init) && t.isIdentifier(init.callee)) {
821
- const cal = init.callee.name;
822
- if (cal === "Fastify" || cal === "fastify") fastifyNames.add(id);
823
- }
824
- },
825
- });
826
-
827
- function resolveImportSpecForLocal(localName) {
828
- let spec = null;
829
-
830
- traverse(ast, {
831
- ImportDeclaration(ip) {
832
- for (const s of ip.node.specifiers) {
833
- if (
834
- (t.isImportDefaultSpecifier(s) || t.isImportSpecifier(s)) &&
835
- s.local.name === localName
836
- ) {
837
- spec = ip.node.source.value;
262
+
263
+ // Track prefixes from register calls
264
+ const prefixMap = new Map(); // file → prefix
265
+
266
+ for (const file of files) {
267
+ try {
268
+ const code = fs.readFileSync(file, 'utf8');
269
+ const relPath = path.relative(repoRoot, file).replace(/\\/g, '/');
270
+ const lines = code.split('\n');
271
+
272
+ // Detect prefix from register calls
273
+ const registerPattern = /\.register\s*\([^,]+,\s*\{[^}]*prefix:\s*['"`]([^'"`]+)['"`]/g;
274
+ let match;
275
+ while ((match = registerPattern.exec(code)) !== null) {
276
+ prefixMap.set(relPath, match[1]);
277
+ }
278
+
279
+ // Extract routes
280
+ for (const pattern of patterns) {
281
+ pattern.lastIndex = 0;
282
+ while ((match = pattern.exec(code)) !== null) {
283
+ let method, routePath;
284
+
285
+ if (match[0].includes('.route')) {
286
+ // Handle .route() pattern - order varies
287
+ if (match[0].indexOf('method') < match[0].indexOf('url')) {
288
+ method = match[1];
289
+ routePath = match[2];
290
+ } else {
291
+ routePath = match[1];
292
+ method = match[2];
838
293
  }
294
+ } else {
295
+ method = match[1];
296
+ routePath = match[2];
839
297
  }
840
- },
841
- VariableDeclarator(vp) {
842
- if (!t.isIdentifier(vp.node.id) || vp.node.id.name !== localName) return;
843
- const init = vp.node.init;
844
- if (!t.isCallExpression(init)) return;
845
- if (!t.isIdentifier(init.callee) || init.callee.name !== "require") return;
846
- const a0 = init.arguments[0];
847
- if (t.isStringLiteral(a0)) spec = a0.value;
848
- },
849
- });
850
-
851
- return spec;
852
- }
853
-
854
- traverse(ast, {
855
- CallExpression(p) {
856
- const callee = p.node.callee;
857
- if (!t.isMemberExpression(callee)) return;
858
- if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property)) return;
859
-
860
- const obj = callee.object.name;
861
- const prop = callee.property.name;
862
-
863
- if (!fastifyNames.has(obj)) return;
864
-
865
- // fastify.get('/x', ...)
866
- if (FASTIFY_METHODS.has(prop)) {
867
- const routeStr = extractStringLiteral(p.node.arguments[0]);
868
- if (!routeStr) return;
869
-
298
+
299
+ const prefix = prefixMap.get(relPath) || '';
300
+ const fullPath = joinPrefix(prefix, routePath);
301
+ const lineNum = code.substring(0, match.index).split('\n').length;
302
+ const snippet = lines[lineNum - 1] || '';
303
+
870
304
  routes.push({
871
- method: canonicalizeMethod(prop),
872
- path: joinPrefix(prefix, routeStr),
873
- handler: fileRel,
874
- framework: "fastify",
875
- confidence: "med",
876
- evidence: evidenceFromLoc({
877
- fileAbs,
878
- fileRel,
879
- loc: p.node.loc,
880
- reason: `Fastify ${prop.toUpperCase()}("${routeStr}")`,
881
- }),
305
+ method: canonicalizeMethod(method),
306
+ path: fullPath,
307
+ handler: `${relPath}:${lineNum}`,
308
+ framework: 'fastify',
309
+ confidence: 'med',
310
+ evidence: [createEvidence(relPath, String(lineNum), `fastify.${method}('${routePath}')`, snippet)],
882
311
  });
883
- return;
884
- }
885
-
886
- // fastify.route({ method, url, handler })
887
- if (prop === "route") {
888
- const arg0 = p.node.arguments[0];
889
- if (!t.isObjectExpression(arg0)) return;
890
-
891
- const r = extractRouteObject(arg0);
892
- if (!r.url) return;
893
-
894
- const fullPath = joinPrefix(prefix, r.url);
895
- const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
896
-
897
- for (const m of ms) {
898
- routes.push({
899
- method: m,
900
- path: fullPath,
901
- handler: fileRel,
902
- framework: "fastify",
903
- confidence: r.hasHandler ? "med" : "low",
904
- evidence: evidenceFromLoc({
905
- fileAbs,
906
- fileRel,
907
- loc: p.node.loc,
908
- reason: `Fastify.route({ url: "${r.url}" })`,
909
- }),
910
- });
911
- }
912
- return;
913
- }
914
-
915
- // fastify.register(plugin, { prefix })
916
- if (prop === "register") {
917
- const pluginArg = p.node.arguments[0];
918
- const optsArg = p.node.arguments[1];
919
- const childPrefixRaw = extractPrefixFromOpts(optsArg);
920
- const childPrefix = childPrefixRaw ? joinPrefix(prefix, childPrefixRaw) : prefix;
921
-
922
- // inline plugin: fastify.register((f, opts) => { f.get(...) }, { prefix })
923
- if (t.isFunctionExpression(pluginArg) || t.isArrowFunctionExpression(pluginArg)) {
924
- const param0 = pluginArg.params[0];
925
- const innerName = t.isIdentifier(param0) ? param0.name : "fastify";
926
-
927
- traverse(
928
- pluginArg.body,
929
- {
930
- CallExpression(pp) {
931
- const c = pp.node.callee;
932
- if (!t.isMemberExpression(c)) return;
933
- if (!t.isIdentifier(c.object) || !t.isIdentifier(c.property)) return;
934
- if (c.object.name !== innerName) return;
935
-
936
- const pr = c.property.name;
937
-
938
- if (FASTIFY_METHODS.has(pr)) {
939
- const rs = extractStringLiteral(pp.node.arguments[0]);
940
- if (!rs) return;
941
-
942
- routes.push({
943
- method: canonicalizeMethod(pr),
944
- path: joinPrefix(childPrefix, rs),
945
- handler: fileRel,
946
- framework: "fastify",
947
- confidence: "med",
948
- evidence: evidenceFromLoc({
949
- fileAbs,
950
- fileRel,
951
- loc: pp.node.loc,
952
- reason: `Fastify plugin ${pr.toUpperCase()}("${rs}") prefix="${childPrefixRaw || ""}"`,
953
- }),
954
- });
955
- }
956
-
957
- if (pr === "route") {
958
- const a0 = pp.node.arguments[0];
959
- if (!t.isObjectExpression(a0)) return;
960
- const r = extractRouteObject(a0);
961
- if (!r.url) return;
962
-
963
- const fullPath = joinPrefix(childPrefix, r.url);
964
- const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
965
-
966
- for (const m of ms) {
967
- routes.push({
968
- method: m,
969
- path: fullPath,
970
- handler: fileRel,
971
- framework: "fastify",
972
- confidence: r.hasHandler ? "med" : "low",
973
- evidence: evidenceFromLoc({
974
- fileAbs,
975
- fileRel,
976
- loc: pp.node.loc,
977
- reason: `Fastify plugin route("${r.url}") prefix="${childPrefixRaw || ""}"`,
978
- }),
979
- });
980
- }
981
- }
982
- },
983
- },
984
- p.scope,
985
- p
986
- );
987
-
988
- return;
989
- }
990
-
991
- // imported plugin identifier: resolve module (relative, package, or TS paths)
992
- if (t.isIdentifier(pluginArg)) {
993
- const localName = pluginArg.name;
994
- const spec = resolveImportSpecForLocal(localName);
995
-
996
- if (!spec) {
997
- gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, name: localName });
998
- return;
999
- }
1000
-
1001
- // Handle @fastify/autoload: scan the specified directory
1002
- if (isAutoloadPlugin(spec)) {
1003
- const autoloadDir = extractAutoloadDir(optsArg, fileAbs);
1004
- if (autoloadDir && existsDir(autoloadDir)) {
1005
- scanAutoloadDir(autoloadDir, childPrefix);
1006
- }
1007
- return;
1008
- }
1009
-
1010
- const { resolved, isNonRoute, reason } = resolveModuleSpec(fileAbs, spec);
1011
-
1012
- // Skip non-route plugins silently (they don't add routes)
1013
- if (isNonRoute) {
1014
- return;
1015
- }
1016
-
1017
- if (!resolved) {
1018
- gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec, reason });
1019
- return;
1020
- }
1021
-
1022
- scanFile(resolved, childPrefix);
1023
- }
1024
-
1025
- // Direct require/import: fastify.register(require('./routes'))
1026
- if (t.isCallExpression(pluginArg)) {
1027
- const callee = pluginArg.callee;
1028
- if (t.isIdentifier(callee) && callee.name === "require") {
1029
- const reqArg = pluginArg.arguments[0];
1030
- if (t.isStringLiteral(reqArg)) {
1031
- const spec = reqArg.value;
1032
-
1033
- // Handle @fastify/autoload via require
1034
- if (isAutoloadPlugin(spec)) {
1035
- const autoloadDir = extractAutoloadDir(optsArg, fileAbs);
1036
- if (autoloadDir && existsDir(autoloadDir)) {
1037
- scanAutoloadDir(autoloadDir, childPrefix);
1038
- }
1039
- return;
1040
- }
1041
-
1042
- const { resolved, isNonRoute, reason } = resolveModuleSpec(fileAbs, spec);
1043
-
1044
- if (isNonRoute) return;
1045
-
1046
- if (!resolved) {
1047
- gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec, reason });
1048
- return;
1049
- }
1050
-
1051
- scanFile(resolved, childPrefix);
1052
- }
1053
- }
1054
- }
1055
- }
1056
- },
1057
- });
1058
- }
1059
-
1060
- /**
1061
- * Extract the 'dir' option from autoload config
1062
- * Handles: { dir: path.join(__dirname, 'routes') } or { dir: './routes' }
1063
- */
1064
- function extractAutoloadDir(optsNode, fromFileAbs) {
1065
- if (!t.isObjectExpression(optsNode)) return null;
1066
-
1067
- for (const prop of optsNode.properties) {
1068
- if (!t.isObjectProperty(prop)) continue;
1069
-
1070
- const key = t.isIdentifier(prop.key) ? prop.key.name :
1071
- t.isStringLiteral(prop.key) ? prop.key.value : null;
1072
-
1073
- if (key !== "dir") continue;
1074
-
1075
- // Simple string: { dir: './routes' }
1076
- if (t.isStringLiteral(prop.value)) {
1077
- return path.resolve(path.dirname(fromFileAbs), prop.value.value);
1078
- }
1079
-
1080
- // path.join(__dirname, 'routes')
1081
- if (t.isCallExpression(prop.value)) {
1082
- const callee = prop.value.callee;
1083
- if (t.isMemberExpression(callee) &&
1084
- t.isIdentifier(callee.object) && callee.object.name === "path" &&
1085
- t.isIdentifier(callee.property) && callee.property.name === "join") {
1086
- const args = prop.value.arguments;
1087
- // path.join(__dirname, 'subdir')
1088
- if (args.length >= 2 && t.isIdentifier(args[0]) && args[0].name === "__dirname") {
1089
- const parts = args.slice(1)
1090
- .filter(a => t.isStringLiteral(a))
1091
- .map(a => a.value);
1092
- if (parts.length > 0) {
1093
- return path.resolve(path.dirname(fromFileAbs), ...parts);
1094
- }
1095
- }
1096
312
  }
1097
313
  }
1098
-
1099
- // Template literal or identifier - can't resolve statically
1100
- return null;
1101
- }
1102
-
1103
- return null;
1104
- }
1105
-
1106
- /**
1107
- * Scan a directory for route files (used by @fastify/autoload)
1108
- */
1109
- function scanAutoloadDir(dirAbs, prefix) {
1110
- if (!existsDir(dirAbs)) return;
1111
-
1112
- let entries;
1113
- try {
1114
- entries = fs.readdirSync(dirAbs, { withFileTypes: true });
1115
- } catch {
1116
- return;
1117
- }
1118
-
1119
- for (const ent of entries) {
1120
- const fullPath = path.join(dirAbs, ent.name);
1121
-
1122
- if (ent.isDirectory()) {
1123
- // Subdirectory: recurse with directory name as prefix segment
1124
- // @fastify/autoload uses directory names as route prefixes
1125
- const subPrefix = joinPrefix(prefix, "/" + ent.name);
1126
- scanAutoloadDir(fullPath, subPrefix);
1127
- } else if (ent.isFile() && /\.(ts|js)$/.test(ent.name) && !ent.name.endsWith(".d.ts")) {
1128
- // Skip index files for prefix (they define routes at current prefix level)
1129
- const isIndex = /^index\.(ts|js)$/.test(ent.name);
1130
- const filePrefix = isIndex ? prefix : joinPrefix(prefix, "/" + ent.name.replace(/\.(ts|js)$/, ""));
1131
-
1132
- scanFile(fullPath, filePrefix);
1133
- }
1134
- }
314
+ } catch {}
1135
315
  }
1136
-
1137
- scanFile(entryAbs, "/");
316
+
1138
317
  return { routes, gaps };
1139
318
  }
1140
319
 
1141
- async function resolveFastifyRoutes(repoRoot, entryRel = null) {
1142
- const entry = entryRel || detectFastifyEntry(repoRoot);
1143
- if (!entry) return { routes: [], gaps: [{ kind: "fastify_entry_missing", file: null }] };
1144
-
1145
- const entryAbs = path.isAbsolute(entry) ? entry : path.join(repoRoot, entry);
1146
- if (!existsFile(entryAbs)) {
1147
- return { routes: [], gaps: [{ kind: "fastify_entry_missing", file: entry }] };
1148
- }
1149
-
1150
- return resolveFastifyRoutesFromEntry(repoRoot, entryAbs);
1151
- }
1152
-
1153
320
  // ============================================================================
1154
321
  // ROUTE INDEX
1155
322
  // ============================================================================
@@ -1162,88 +329,87 @@ class RouteIndex {
1162
329
  this.parameterized = [];
1163
330
  this.gaps = [];
1164
331
  }
1165
-
1166
- async build(repoRoot, options = {}) {
332
+
333
+ async build(repoRoot) {
334
+ // Resolve Next.js routes
1167
335
  const nextRoutes = await resolveNextRoutes(repoRoot);
1168
336
  this.routes.push(...nextRoutes);
1169
-
1170
- const { routes: fastifyRoutes, gaps } = await resolveFastifyRoutes(repoRoot, options.fastifyEntry || null);
337
+
338
+ // Resolve Fastify routes
339
+ const { routes: fastifyRoutes, gaps } = await resolveFastifyRoutes(repoRoot);
1171
340
  this.routes.push(...fastifyRoutes);
1172
- this.gaps.push(...(gaps || []));
1173
-
1174
- for (const r of this.routes) {
1175
- const m = canonicalizeMethod(r.method);
1176
- const p = canonicalizePath(r.path);
1177
-
1178
- r.method = m;
1179
- r.path = p;
1180
-
1181
- if (!this.byMethod.has(m)) this.byMethod.set(m, []);
1182
- this.byMethod.get(m).push(r);
1183
-
1184
- if (!this.byPath.has(p)) this.byPath.set(p, []);
1185
- this.byPath.get(p).push(r);
1186
-
1187
- if (isParameterizedPath(p)) this.parameterized.push(r);
341
+ this.gaps.push(...gaps);
342
+
343
+ // Build indexes
344
+ for (const route of this.routes) {
345
+ const methodKey = route.method;
346
+ if (!this.byMethod.has(methodKey)) this.byMethod.set(methodKey, []);
347
+ this.byMethod.get(methodKey).push(route);
348
+
349
+ const pathKey = route.path;
350
+ if (!this.byPath.has(pathKey)) this.byPath.set(pathKey, []);
351
+ this.byPath.get(pathKey).push(route);
352
+
353
+ if (isParameterizedPath(route.path)) {
354
+ this.parameterized.push(route);
355
+ }
1188
356
  }
1189
-
357
+
1190
358
  return this;
1191
359
  }
1192
-
1193
- findRoutes(method, p) {
1194
- const m = canonicalizeMethod(method);
1195
- const cp = canonicalizePath(p);
1196
-
360
+
361
+ findRoutes(method, path) {
362
+ const canonicalMethod = canonicalizeMethod(method);
363
+ const canonicalPath = canonicalizePath(path);
1197
364
  const matches = [];
1198
-
365
+
1199
366
  // Exact path match
1200
- for (const r of this.byPath.get(cp) || []) {
1201
- if (matchMethod(r.method, m)) matches.push(r);
367
+ const pathMatches = this.byPath.get(canonicalPath) || [];
368
+ for (const route of pathMatches) {
369
+ if (matchMethod(route.method, canonicalMethod)) {
370
+ matches.push(route);
371
+ }
1202
372
  }
1203
-
1204
- // Wildcard method match on exact path
1205
- for (const r of this.byMethod.get("*") || []) {
1206
- if (r.path === cp && !matches.includes(r)) matches.push(r);
373
+
374
+ // Wildcard method match
375
+ const wildcardMethods = this.byMethod.get('*') || [];
376
+ for (const route of wildcardMethods) {
377
+ if (route.path === canonicalPath && !matches.includes(route)) {
378
+ matches.push(route);
379
+ }
1207
380
  }
1208
-
381
+
1209
382
  // Parameterized route match
1210
- for (const r of this.parameterized) {
1211
- if (r.path === cp) continue;
1212
- if (matchPath(r.path, cp) && matchMethod(r.method, m)) {
1213
- if (!matches.includes(r)) matches.push(r);
383
+ for (const route of this.parameterized) {
384
+ if (matchPath(route.path, canonicalPath) && matchMethod(route.method, canonicalMethod)) {
385
+ if (!matches.includes(route)) matches.push(route);
1214
386
  }
1215
387
  }
1216
-
388
+
1217
389
  return matches;
1218
390
  }
1219
-
1220
- findClosestRoutes(p, limit = 3) {
1221
- const cp = canonicalizePath(p);
1222
- const pathParts = cp.split("/").filter(Boolean);
1223
-
1224
- const scored = this.routes.map((r) => {
1225
- const routeParts = r.path.split("/").filter(Boolean);
391
+
392
+ findClosestRoutes(path, limit = 3) {
393
+ const canonicalPath = canonicalizePath(path);
394
+ const pathParts = canonicalPath.split('/').filter(Boolean);
395
+
396
+ const scored = this.routes.map(route => {
397
+ const routeParts = route.path.split('/').filter(Boolean);
1226
398
  let score = 0;
1227
-
399
+
1228
400
  for (let i = 0; i < Math.min(pathParts.length, routeParts.length); i++) {
1229
- if (pathParts[i] === routeParts[i]) score += 1;
1230
- else if (routeParts[i].startsWith(":")) score += 0.8;
1231
- else if (routeParts[i].startsWith("*")) { score += 0.6; break; }
1232
- else break;
401
+ if (pathParts[i] === routeParts[i] || routeParts[i].startsWith(':')) {
402
+ score++;
403
+ } else break;
1233
404
  }
1234
-
1235
- if (pathParts.length === routeParts.length) score += 0.25;
1236
- if (isParameterizedPath(r.path)) score += 0.05;
1237
-
1238
- return { route: r, score };
405
+
406
+ if (pathParts.length === routeParts.length) score += 0.5;
407
+ return { route, score };
1239
408
  });
1240
-
1241
- return scored
1242
- .sort((a, b) => b.score - a.score)
1243
- .slice(0, Math.max(0, limit))
1244
- .map((s) => s.route);
409
+
410
+ return scored.sort((a, b) => b.score - a.score).slice(0, limit).map(s => s.route);
1245
411
  }
1246
-
412
+
1247
413
  getRouteMap() {
1248
414
  return {
1249
415
  server: this.routes,
@@ -1261,53 +427,43 @@ class RouteIndex {
1261
427
  async function validateRouteExists(claim, repoRoot, routeIndex) {
1262
428
  const index = routeIndex || new RouteIndex();
1263
429
  if (!routeIndex) await index.build(repoRoot);
1264
-
1265
- const method = claim?.method || "*";
1266
- const routePath = claim?.path;
1267
-
1268
- if (!routePath) {
1269
- return {
1270
- result: "unknown",
1271
- confidence: "low",
1272
- evidence: [],
1273
- nextSteps: ["Claim missing path"],
1274
- };
1275
- }
1276
-
430
+
431
+ const method = claim.method || '*';
432
+ const routePath = claim.path;
433
+
1277
434
  const matches = index.findRoutes(method, routePath);
1278
-
435
+
1279
436
  if (matches.length > 0) {
1280
- const best = matches[0];
1281
437
  return {
1282
- result: "true",
1283
- confidence: best.confidence || "med",
1284
- evidence: best.evidence || [],
1285
- matchedRoute: best,
438
+ result: 'true',
439
+ confidence: matches[0].confidence,
440
+ evidence: matches[0].evidence,
441
+ matchedRoute: matches[0],
1286
442
  };
1287
443
  }
1288
-
1289
- const closest = index.findClosestRoutes(routePath, 3);
1290
- const hasGaps = (index.gaps || []).length > 0;
1291
-
444
+
445
+ const closest = index.findClosestRoutes(routePath);
446
+ const hasGaps = index.gaps.length > 0;
447
+
1292
448
  if (hasGaps) {
1293
449
  return {
1294
- result: "unknown",
1295
- confidence: "low",
450
+ result: 'unknown',
451
+ confidence: 'low',
1296
452
  evidence: [],
1297
453
  closestRoutes: closest,
1298
454
  gaps: index.gaps,
1299
- nextSteps: ["Some routes may not be detected due to unresolved plugins/imports."],
455
+ nextSteps: ['Some routes may not be detected due to unresolved plugins'],
1300
456
  };
1301
457
  }
1302
-
458
+
1303
459
  return {
1304
- result: "false",
1305
- confidence: "high",
460
+ result: 'false',
461
+ confidence: 'high',
1306
462
  evidence: [],
1307
463
  closestRoutes: closest,
1308
- nextSteps: closest.length
1309
- ? [`Did you mean: ${closest.map(r => `${r.method} ${r.path}`).join(", ")}?`]
1310
- : ["No similar routes found"],
464
+ nextSteps: closest.length > 0
465
+ ? [`Did you mean: ${closest.map(r => `${r.method} ${r.path}`).join(', ')}?`]
466
+ : ['No similar routes found'],
1311
467
  };
1312
468
  }
1313
469
 
@@ -1316,7 +472,6 @@ module.exports = {
1316
472
  canonicalizeMethod,
1317
473
  resolveNextRoutes,
1318
474
  resolveFastifyRoutes,
1319
- detectFastifyEntry,
1320
475
  RouteIndex,
1321
476
  validateRouteExists,
1322
477
  };