vibecheck-ai 2.0.1 → 5.0.0

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 (456) hide show
  1. package/bin/.generated +25 -0
  2. package/bin/_deprecations.js +463 -0
  3. package/bin/_router.js +46 -0
  4. package/bin/cli-hygiene.js +241 -0
  5. package/bin/dev/run-v2-torture.js +30 -0
  6. package/bin/registry.js +656 -0
  7. package/bin/runners/CLI_REFACTOR_SUMMARY.md +229 -0
  8. package/bin/runners/ENHANCEMENT_GUIDE.md +121 -0
  9. package/bin/runners/REPORT_AUDIT.md +64 -0
  10. package/bin/runners/cli-utils.js +1070 -0
  11. package/bin/runners/context/ai-task-decomposer.js +337 -0
  12. package/bin/runners/context/analyzer.js +513 -0
  13. package/bin/runners/context/api-contracts.js +427 -0
  14. package/bin/runners/context/context-diff.js +342 -0
  15. package/bin/runners/context/context-pruner.js +291 -0
  16. package/bin/runners/context/dependency-graph.js +414 -0
  17. package/bin/runners/context/generators/claude.js +107 -0
  18. package/bin/runners/context/generators/codex.js +108 -0
  19. package/bin/runners/context/generators/copilot.js +119 -0
  20. package/bin/runners/context/generators/cursor-enhanced.js +2525 -0
  21. package/bin/runners/context/generators/cursor.js +514 -0
  22. package/bin/runners/context/generators/mcp.js +169 -0
  23. package/bin/runners/context/generators/windsurf.js +180 -0
  24. package/bin/runners/context/git-context.js +304 -0
  25. package/bin/runners/context/index.js +1110 -0
  26. package/bin/runners/context/insights.js +173 -0
  27. package/bin/runners/context/mcp-server/generate-rules.js +337 -0
  28. package/bin/runners/context/mcp-server/index.js +1176 -0
  29. package/bin/runners/context/mcp-server/package.json +24 -0
  30. package/bin/runners/context/memory.js +200 -0
  31. package/bin/runners/context/monorepo.js +215 -0
  32. package/bin/runners/context/multi-repo-federation.js +404 -0
  33. package/bin/runners/context/patterns.js +253 -0
  34. package/bin/runners/context/proof-context.js +1264 -0
  35. package/bin/runners/context/security-scanner.js +541 -0
  36. package/bin/runners/context/semantic-search.js +350 -0
  37. package/bin/runners/context/shared.js +264 -0
  38. package/bin/runners/context/team-conventions.js +336 -0
  39. package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -0
  40. package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
  41. package/bin/runners/lib/agent-firewall/change-packet/builder.js +488 -0
  42. package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
  43. package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
  44. package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
  45. package/bin/runners/lib/agent-firewall/claims/extractor.js +303 -0
  46. package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
  47. package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
  48. package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
  49. package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
  50. package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
  51. package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
  52. package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
  53. package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
  54. package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
  55. package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
  56. package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
  57. package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
  58. package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
  59. package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
  60. package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
  61. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +127 -0
  62. package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
  63. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +213 -0
  64. package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
  65. package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
  66. package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
  67. package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
  68. package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
  69. package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
  70. package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
  71. package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
  72. package/bin/runners/lib/agent-firewall/index.js +200 -0
  73. package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
  74. package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
  75. package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +634 -0
  76. package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
  77. package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
  78. package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
  79. package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
  80. package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
  81. package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
  82. package/bin/runners/lib/agent-firewall/interceptor/base.js +308 -0
  83. package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
  84. package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
  85. package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
  86. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
  87. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
  88. package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
  89. package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
  90. package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
  91. package/bin/runners/lib/agent-firewall/logger.js +141 -0
  92. package/bin/runners/lib/agent-firewall/policy/default-policy.json +90 -0
  93. package/bin/runners/lib/agent-firewall/policy/engine.js +103 -0
  94. package/bin/runners/lib/agent-firewall/policy/loader.js +451 -0
  95. package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
  96. package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
  97. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +79 -0
  98. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +227 -0
  99. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +191 -0
  100. package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
  101. package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
  102. package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
  103. package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
  104. package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
  105. package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
  106. package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
  107. package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
  108. package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
  109. package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
  110. package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
  111. package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
  112. package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
  113. package/bin/runners/lib/agent-firewall/risk/thresholds.js +322 -0
  114. package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
  115. package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
  116. package/bin/runners/lib/agent-firewall/session/index.js +26 -0
  117. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
  118. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
  119. package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
  120. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
  121. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
  122. package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
  123. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
  124. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
  125. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
  126. package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
  127. package/bin/runners/lib/agent-firewall/truthpack/loader.js +137 -0
  128. package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
  129. package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
  130. package/bin/runners/lib/ai-bridge.js +416 -0
  131. package/bin/runners/lib/analysis-core.js +309 -0
  132. package/bin/runners/lib/analyzers.js +2500 -0
  133. package/bin/runners/lib/api-client.js +269 -0
  134. package/bin/runners/lib/approve-output.js +235 -0
  135. package/bin/runners/lib/artifact-envelope.js +540 -0
  136. package/bin/runners/lib/assets/vibecheck-logo.png +0 -0
  137. package/bin/runners/lib/audit-bridge.js +391 -0
  138. package/bin/runners/lib/auth-shared.js +977 -0
  139. package/bin/runners/lib/auth-truth.js +193 -0
  140. package/bin/runners/lib/auth.js +215 -0
  141. package/bin/runners/lib/authority-badge.js +425 -0
  142. package/bin/runners/lib/backup.js +62 -0
  143. package/bin/runners/lib/billing.js +107 -0
  144. package/bin/runners/lib/checkpoint.js +941 -0
  145. package/bin/runners/lib/claims.js +118 -0
  146. package/bin/runners/lib/classify-output.js +204 -0
  147. package/bin/runners/lib/cleanup/engine.js +571 -0
  148. package/bin/runners/lib/cleanup/index.js +53 -0
  149. package/bin/runners/lib/cleanup/output.js +375 -0
  150. package/bin/runners/lib/cleanup/rules.js +1060 -0
  151. package/bin/runners/lib/cli-output.js +400 -0
  152. package/bin/runners/lib/cli-ui.js +540 -0
  153. package/bin/runners/lib/compliance-bridge-new.js +0 -0
  154. package/bin/runners/lib/compliance-bridge.js +165 -0
  155. package/bin/runners/lib/contracts/auth-contract.js +202 -0
  156. package/bin/runners/lib/contracts/env-contract.js +181 -0
  157. package/bin/runners/lib/contracts/external-contract.js +206 -0
  158. package/bin/runners/lib/contracts/guard.js +168 -0
  159. package/bin/runners/lib/contracts/index.js +89 -0
  160. package/bin/runners/lib/contracts/plan-validator.js +311 -0
  161. package/bin/runners/lib/contracts/route-contract.js +199 -0
  162. package/bin/runners/lib/contracts.js +804 -0
  163. package/bin/runners/lib/default-config.js +127 -0
  164. package/bin/runners/lib/detect.js +89 -0
  165. package/bin/runners/lib/detectors-v2.js +622 -0
  166. package/bin/runners/lib/doctor/autofix.js +254 -0
  167. package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
  168. package/bin/runners/lib/doctor/failure-signatures.js +526 -0
  169. package/bin/runners/lib/doctor/fix-script.js +336 -0
  170. package/bin/runners/lib/doctor/index.js +37 -0
  171. package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
  172. package/bin/runners/lib/doctor/modules/dependencies.js +325 -0
  173. package/bin/runners/lib/doctor/modules/index.js +105 -0
  174. package/bin/runners/lib/doctor/modules/network.js +250 -0
  175. package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
  176. package/bin/runners/lib/doctor/modules/project.js +312 -0
  177. package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
  178. package/bin/runners/lib/doctor/modules/runtime.js +224 -0
  179. package/bin/runners/lib/doctor/modules/security.js +350 -0
  180. package/bin/runners/lib/doctor/modules/system.js +213 -0
  181. package/bin/runners/lib/doctor/modules/vibecheck.js +394 -0
  182. package/bin/runners/lib/doctor/reporter.js +262 -0
  183. package/bin/runners/lib/doctor/safe-repair.js +384 -0
  184. package/bin/runners/lib/doctor/service.js +262 -0
  185. package/bin/runners/lib/doctor/types.js +113 -0
  186. package/bin/runners/lib/doctor/ui.js +263 -0
  187. package/bin/runners/lib/doctor-enhanced.js +233 -0
  188. package/bin/runners/lib/doctor-output.js +226 -0
  189. package/bin/runners/lib/doctor-v2.js +608 -0
  190. package/bin/runners/lib/drift.js +425 -0
  191. package/bin/runners/lib/enforcement.js +72 -0
  192. package/bin/runners/lib/engine/ast-cache.js +210 -0
  193. package/bin/runners/lib/engine/auth-extractor.js +211 -0
  194. package/bin/runners/lib/engine/billing-extractor.js +112 -0
  195. package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
  196. package/bin/runners/lib/engine/env-extractor.js +207 -0
  197. package/bin/runners/lib/engine/express-extractor.js +208 -0
  198. package/bin/runners/lib/engine/extractors.js +849 -0
  199. package/bin/runners/lib/engine/index.js +207 -0
  200. package/bin/runners/lib/engine/repo-index.js +514 -0
  201. package/bin/runners/lib/engine/types.js +124 -0
  202. package/bin/runners/lib/engines/accessibility-engine.js +190 -0
  203. package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
  204. package/bin/runners/lib/engines/ast-cache.js +99 -0
  205. package/bin/runners/lib/engines/attack-detector.js +1192 -0
  206. package/bin/runners/lib/engines/code-quality-engine.js +255 -0
  207. package/bin/runners/lib/engines/console-logs-engine.js +115 -0
  208. package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
  209. package/bin/runners/lib/engines/dead-code-engine.js +198 -0
  210. package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
  211. package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
  212. package/bin/runners/lib/engines/file-filter.js +131 -0
  213. package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
  214. package/bin/runners/lib/engines/mock-data-engine.js +272 -0
  215. package/bin/runners/lib/engines/parallel-processor.js +71 -0
  216. package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
  217. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
  218. package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
  219. package/bin/runners/lib/engines/type-aware-engine.js +152 -0
  220. package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
  221. package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
  222. package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
  223. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
  224. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
  225. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
  226. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
  227. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
  228. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
  229. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
  230. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
  231. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
  232. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
  233. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
  234. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
  235. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
  236. package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
  237. package/bin/runners/lib/enterprise-detect.js +603 -0
  238. package/bin/runners/lib/enterprise-init.js +942 -0
  239. package/bin/runners/lib/entitlements-v2.js +265 -0
  240. package/bin/runners/lib/entitlements.generated.js +0 -0
  241. package/bin/runners/lib/entitlements.js +340 -0
  242. package/bin/runners/lib/env-resolver.js +417 -0
  243. package/bin/runners/lib/env-template.js +66 -0
  244. package/bin/runners/lib/env.js +189 -0
  245. package/bin/runners/lib/error-handler.js +368 -0
  246. package/bin/runners/lib/error-messages.js +289 -0
  247. package/bin/runners/lib/evidence-pack.js +684 -0
  248. package/bin/runners/lib/exit-codes.js +275 -0
  249. package/bin/runners/lib/extractors/client-calls.js +990 -0
  250. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -0
  251. package/bin/runners/lib/extractors/fastify-routes.js +426 -0
  252. package/bin/runners/lib/extractors/index.js +363 -0
  253. package/bin/runners/lib/extractors/next-routes.js +524 -0
  254. package/bin/runners/lib/extractors/proof-graph.js +431 -0
  255. package/bin/runners/lib/extractors/route-matcher.js +451 -0
  256. package/bin/runners/lib/extractors/truthpack-v2.js +377 -0
  257. package/bin/runners/lib/extractors/ui-bindings.js +547 -0
  258. package/bin/runners/lib/finding-id.js +69 -0
  259. package/bin/runners/lib/finding-sorter.js +89 -0
  260. package/bin/runners/lib/findings-schema.js +281 -0
  261. package/bin/runners/lib/fingerprint.js +377 -0
  262. package/bin/runners/lib/firewall-prompt.js +50 -0
  263. package/bin/runners/lib/fix-output.js +228 -0
  264. package/bin/runners/lib/global-flags.js +250 -0
  265. package/bin/runners/lib/graph/graph-builder.js +265 -0
  266. package/bin/runners/lib/graph/html-renderer.js +413 -0
  267. package/bin/runners/lib/graph/index.js +32 -0
  268. package/bin/runners/lib/graph/runtime-collector.js +215 -0
  269. package/bin/runners/lib/graph/static-extractor.js +518 -0
  270. package/bin/runners/lib/help-formatter.js +413 -0
  271. package/bin/runners/lib/html-proof-report.js +913 -0
  272. package/bin/runners/lib/html-report.js +650 -0
  273. package/bin/runners/lib/init-wizard.js +601 -0
  274. package/bin/runners/lib/interactive-menu.js +1496 -0
  275. package/bin/runners/lib/json-output.js +76 -0
  276. package/bin/runners/lib/llm.js +75 -0
  277. package/bin/runners/lib/logger.js +38 -0
  278. package/bin/runners/lib/meter.js +61 -0
  279. package/bin/runners/lib/missions/briefing.js +427 -0
  280. package/bin/runners/lib/missions/checkpoint.js +753 -0
  281. package/bin/runners/lib/missions/evidence.js +126 -0
  282. package/bin/runners/lib/missions/hardening.js +851 -0
  283. package/bin/runners/lib/missions/plan.js +648 -0
  284. package/bin/runners/lib/missions/safety-gates.js +645 -0
  285. package/bin/runners/lib/missions/schema.js +478 -0
  286. package/bin/runners/lib/missions/templates.js +317 -0
  287. package/bin/runners/lib/next-action.js +560 -0
  288. package/bin/runners/lib/packs/bundle.js +675 -0
  289. package/bin/runners/lib/packs/evidence-pack.js +671 -0
  290. package/bin/runners/lib/packs/pack-factory.js +837 -0
  291. package/bin/runners/lib/packs/permissions-pack.js +686 -0
  292. package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
  293. package/bin/runners/lib/patch.js +40 -0
  294. package/bin/runners/lib/permissions/auth-model.js +213 -0
  295. package/bin/runners/lib/permissions/idor-prover.js +205 -0
  296. package/bin/runners/lib/permissions/index.js +45 -0
  297. package/bin/runners/lib/permissions/matrix-builder.js +198 -0
  298. package/bin/runners/lib/pkgjson.js +28 -0
  299. package/bin/runners/lib/policy.js +295 -0
  300. package/bin/runners/lib/polish/accessibility.js +62 -0
  301. package/bin/runners/lib/polish/analyzer.js +93 -0
  302. package/bin/runners/lib/polish/backend.js +87 -0
  303. package/bin/runners/lib/polish/configuration.js +83 -0
  304. package/bin/runners/lib/polish/documentation.js +83 -0
  305. package/bin/runners/lib/polish/frontend.js +817 -0
  306. package/bin/runners/lib/polish/index.js +27 -0
  307. package/bin/runners/lib/polish/infrastructure.js +80 -0
  308. package/bin/runners/lib/polish/internationalization.js +85 -0
  309. package/bin/runners/lib/polish/libraries.js +180 -0
  310. package/bin/runners/lib/polish/observability.js +75 -0
  311. package/bin/runners/lib/polish/performance.js +64 -0
  312. package/bin/runners/lib/polish/privacy.js +110 -0
  313. package/bin/runners/lib/polish/resilience.js +92 -0
  314. package/bin/runners/lib/polish/security.js +78 -0
  315. package/bin/runners/lib/polish/seo.js +71 -0
  316. package/bin/runners/lib/polish/styles.js +62 -0
  317. package/bin/runners/lib/polish/utils.js +104 -0
  318. package/bin/runners/lib/preflight.js +142 -0
  319. package/bin/runners/lib/prerequisites.js +149 -0
  320. package/bin/runners/lib/prove-output.js +220 -0
  321. package/bin/runners/lib/reality/correlation-detectors.js +359 -0
  322. package/bin/runners/lib/reality/index.js +318 -0
  323. package/bin/runners/lib/reality/request-hashing.js +416 -0
  324. package/bin/runners/lib/reality/request-mapper.js +453 -0
  325. package/bin/runners/lib/reality/safety-rails.js +463 -0
  326. package/bin/runners/lib/reality/semantic-snapshot.js +408 -0
  327. package/bin/runners/lib/reality/toast-detector.js +393 -0
  328. package/bin/runners/lib/reality-findings.js +84 -0
  329. package/bin/runners/lib/reality-output.js +231 -0
  330. package/bin/runners/lib/receipts.js +179 -0
  331. package/bin/runners/lib/redact.js +29 -0
  332. package/bin/runners/lib/replay/capsule-manager.js +154 -0
  333. package/bin/runners/lib/replay/index.js +263 -0
  334. package/bin/runners/lib/replay/player.js +348 -0
  335. package/bin/runners/lib/replay/recorder.js +331 -0
  336. package/bin/runners/lib/report-engine.js +626 -0
  337. package/bin/runners/lib/report-html.js +1233 -0
  338. package/bin/runners/lib/report-output.js +366 -0
  339. package/bin/runners/lib/report-templates.js +967 -0
  340. package/bin/runners/lib/report.js +135 -0
  341. package/bin/runners/lib/route-detection.js +1209 -0
  342. package/bin/runners/lib/route-truth.js +1322 -0
  343. package/bin/runners/lib/safelist/index.js +96 -0
  344. package/bin/runners/lib/safelist/integration.js +334 -0
  345. package/bin/runners/lib/safelist/matcher.js +696 -0
  346. package/bin/runners/lib/safelist/schema.js +948 -0
  347. package/bin/runners/lib/safelist/store.js +438 -0
  348. package/bin/runners/lib/sandbox/index.js +59 -0
  349. package/bin/runners/lib/sandbox/proof-chain.js +399 -0
  350. package/bin/runners/lib/sandbox/sandbox-runner.js +205 -0
  351. package/bin/runners/lib/sandbox/worktree.js +174 -0
  352. package/bin/runners/lib/scan-cache.js +330 -0
  353. package/bin/runners/lib/scan-output-schema.js +344 -0
  354. package/bin/runners/lib/scan-output.js +631 -0
  355. package/bin/runners/lib/scan-runner.js +135 -0
  356. package/bin/runners/lib/schema-validator.js +350 -0
  357. package/bin/runners/lib/schemas/ajv-validator.js +464 -0
  358. package/bin/runners/lib/schemas/contracts.schema.json +160 -0
  359. package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
  360. package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
  361. package/bin/runners/lib/schemas/finding.schema.json +100 -0
  362. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -0
  363. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -0
  364. package/bin/runners/lib/schemas/reality-report.schema.json +162 -0
  365. package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
  366. package/bin/runners/lib/schemas/run-request.schema.json +108 -0
  367. package/bin/runners/lib/schemas/share-pack.schema.json +180 -0
  368. package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
  369. package/bin/runners/lib/schemas/ship-report.schema.json +117 -0
  370. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -0
  371. package/bin/runners/lib/schemas/validator.js +465 -0
  372. package/bin/runners/lib/schemas/verdict.schema.json +140 -0
  373. package/bin/runners/lib/score-history.js +282 -0
  374. package/bin/runners/lib/security-bridge.js +249 -0
  375. package/bin/runners/lib/server-usage.js +513 -0
  376. package/bin/runners/lib/share-pack.js +239 -0
  377. package/bin/runners/lib/ship-gate.js +832 -0
  378. package/bin/runners/lib/ship-manifest.js +1153 -0
  379. package/bin/runners/lib/ship-output-enterprise.js +239 -0
  380. package/bin/runners/lib/ship-output.js +1128 -0
  381. package/bin/runners/lib/snippets.js +67 -0
  382. package/bin/runners/lib/status-output.js +340 -0
  383. package/bin/runners/lib/terminal-ui.js +356 -0
  384. package/bin/runners/lib/truth.js +1691 -0
  385. package/bin/runners/lib/ui.js +562 -0
  386. package/bin/runners/lib/unified-cli-output.js +947 -0
  387. package/bin/runners/lib/unified-output.js +197 -0
  388. package/bin/runners/lib/upsell.js +410 -0
  389. package/bin/runners/lib/usage.js +153 -0
  390. package/bin/runners/lib/validate-patch.js +156 -0
  391. package/bin/runners/lib/verdict-engine.js +628 -0
  392. package/bin/runners/lib/verification.js +345 -0
  393. package/bin/runners/lib/why-tree.js +650 -0
  394. package/bin/runners/reality/engine.js +917 -0
  395. package/bin/runners/reality/flows.js +122 -0
  396. package/bin/runners/reality/report.js +378 -0
  397. package/bin/runners/reality/session.js +193 -0
  398. package/bin/runners/runAIAgent.js +229 -0
  399. package/bin/runners/runAgent.d.ts +5 -0
  400. package/bin/runners/runAgent.js +161 -0
  401. package/bin/runners/runAllowlist.js +418 -0
  402. package/bin/runners/runApprove.js +320 -0
  403. package/bin/runners/runAudit.js +692 -0
  404. package/bin/runners/runAuth.js +731 -0
  405. package/bin/runners/runCI.js +353 -0
  406. package/bin/runners/runCheckpoint.js +530 -0
  407. package/bin/runners/runClassify.js +928 -0
  408. package/bin/runners/runCleanup.js +343 -0
  409. package/bin/runners/runContext.d.ts +4 -0
  410. package/bin/runners/runContext.js +175 -0
  411. package/bin/runners/runDoctor.js +877 -0
  412. package/bin/runners/runEvidencePack.js +362 -0
  413. package/bin/runners/runFirewall.d.ts +5 -0
  414. package/bin/runners/runFirewall.js +134 -0
  415. package/bin/runners/runFirewallHook.d.ts +5 -0
  416. package/bin/runners/runFirewallHook.js +56 -0
  417. package/bin/runners/runFix.js +1355 -0
  418. package/bin/runners/runForge.js +451 -0
  419. package/bin/runners/runGuard.js +262 -0
  420. package/bin/runners/runInit.js +1927 -0
  421. package/bin/runners/runIntent.js +906 -0
  422. package/bin/runners/runKickoff.js +878 -0
  423. package/bin/runners/runLabs.js +424 -0
  424. package/bin/runners/runLaunch.js +2000 -0
  425. package/bin/runners/runLink.js +785 -0
  426. package/bin/runners/runMcp.js +1875 -0
  427. package/bin/runners/runPacks.js +2089 -0
  428. package/bin/runners/runPolish.d.ts +4 -0
  429. package/bin/runners/runPolish.js +390 -0
  430. package/bin/runners/runPromptFirewall.js +211 -0
  431. package/bin/runners/runProve.js +1411 -0
  432. package/bin/runners/runQuickstart.js +531 -0
  433. package/bin/runners/runReality.js +2260 -0
  434. package/bin/runners/runReport.js +726 -0
  435. package/bin/runners/runRuntime.js +110 -0
  436. package/bin/runners/runSafelist.js +1190 -0
  437. package/bin/runners/runScan.js +688 -0
  438. package/bin/runners/runShield.js +1282 -0
  439. package/bin/runners/runShip.js +1660 -0
  440. package/bin/runners/runTruth.d.ts +5 -0
  441. package/bin/runners/runTruth.js +101 -0
  442. package/bin/runners/runValidate.js +179 -0
  443. package/bin/runners/runWatch.js +478 -0
  444. package/bin/runners/utils.js +360 -0
  445. package/bin/scan.js +617 -0
  446. package/bin/vibecheck.js +1617 -0
  447. package/dist/guardrail/index.d.ts +2405 -0
  448. package/dist/guardrail/index.js +9747 -0
  449. package/dist/guardrail/index.js.map +1 -0
  450. package/dist/scanner/index.d.ts +282 -0
  451. package/dist/scanner/index.js +3395 -0
  452. package/dist/scanner/index.js.map +1 -0
  453. package/package.json +123 -104
  454. package/README.md +0 -491
  455. package/dist/index.js +0 -99711
  456. package/dist/index.js.map +0 -1
@@ -0,0 +1,2500 @@
1
+ // bin/runners/lib/analyzers.js
2
+ "use strict";
3
+
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const fg = require("fast-glob");
7
+ const crypto = require("crypto");
8
+ const { URL } = require("url");
9
+ const parser = require("@babel/parser");
10
+ const traverse = require("@babel/traverse").default;
11
+ const t = require("@babel/types");
12
+
13
+ const { routeMatches } = require("./claims");
14
+ const { matcherCoversPath } = require("./auth-truth");
15
+
16
+ /* ============================================================================
17
+ * STANDARD IGNORE PATTERNS
18
+ * Used by all analyzers to exclude non-production code
19
+ * ========================================================================== */
20
+ const STANDARD_IGNORE_PATTERNS = [
21
+ // Core excludes
22
+ "**/node_modules/**",
23
+ "**/.next/**",
24
+ "**/dist/**",
25
+ "**/build/**",
26
+ "**/*.d.ts",
27
+ "**/*.d.ts.map",
28
+ // Test files
29
+ "**/__tests__/**",
30
+ "**/tests/**",
31
+ "**/*.test.ts",
32
+ "**/*.test.tsx",
33
+ "**/*.test.js",
34
+ "**/*.spec.ts",
35
+ "**/*.spec.tsx",
36
+ "**/*.spec.js",
37
+ "**/fixtures/**",
38
+ // Internal tooling
39
+ "**/mcp-server/**",
40
+ "**/bin/**",
41
+ "**/packages/cli/**",
42
+ // Examples and templates
43
+ "**/examples/**",
44
+ "**/templates/**",
45
+ "**/docs/**",
46
+ // Cache and generated
47
+ "**/.guardrail/**",
48
+ "**/.cursor/**",
49
+ "**/.vibecheck/**",
50
+ "**/coverage/**",
51
+ "**/_archive/**",
52
+ ];
53
+
54
+ /* ============================================================================
55
+ * WORLD-CLASS INFRA HELPERS
56
+ * - file caching (speed + consistent evidence)
57
+ * - stable IDs (diff-friendly)
58
+ * - safe regex usage (fixes /g + .test() state bugs)
59
+ * - memory management (clearFileCache for monorepos)
60
+ * ========================================================================== */
61
+
62
+ const _FILE_TEXT = new Map();
63
+ const _FILE_LINES = new Map();
64
+
65
+ function readFileCached(fileAbs) {
66
+ if (_FILE_TEXT.has(fileAbs)) return _FILE_TEXT.get(fileAbs);
67
+ const txt = fs.readFileSync(fileAbs, "utf8");
68
+ _FILE_TEXT.set(fileAbs, txt);
69
+ return txt;
70
+ }
71
+
72
+ function readLinesCached(fileAbs) {
73
+ if (_FILE_LINES.has(fileAbs)) return _FILE_LINES.get(fileAbs);
74
+ const lines = readFileCached(fileAbs).split(/\r?\n/);
75
+ _FILE_LINES.set(fileAbs, lines);
76
+ return lines;
77
+ }
78
+
79
+ /**
80
+ * V3: Clear file cache to prevent memory leaks in large monorepos.
81
+ * Call this after a scan completes or between major steps.
82
+ */
83
+ function clearFileCache() {
84
+ _FILE_TEXT.clear();
85
+ _FILE_LINES.clear();
86
+ }
87
+
88
+ /**
89
+ * V3: Shannon Entropy calculator for detecting high-randomness strings (likely secrets).
90
+ * Entropy > 4.5 typically indicates a random/secret string vs structured data.
91
+ * Git SHAs (hex only) have lower effective entropy due to limited charset.
92
+ */
93
+ function getShannonEntropy(str) {
94
+ if (!str || str.length === 0) return 0;
95
+ const len = str.length;
96
+ const frequencies = {};
97
+ for (let i = 0; i < len; i++) {
98
+ const char = str[i];
99
+ frequencies[char] = (frequencies[char] || 0) + 1;
100
+ }
101
+
102
+ let entropy = 0;
103
+ for (const char in frequencies) {
104
+ const p = frequencies[char] / len;
105
+ entropy -= p * Math.log2(p);
106
+ }
107
+ return entropy;
108
+ }
109
+
110
+ function sha256(text) {
111
+ return "sha256:" + crypto.createHash("sha256").update(String(text || "")).digest("hex");
112
+ }
113
+
114
+ function stableId(prefix, key) {
115
+ const h = crypto.createHash("sha256").update(String(key || "")).digest("hex").slice(0, 10);
116
+ return `${prefix}_${h}`;
117
+ }
118
+
119
+ // IMPORTANT: /g + .test() is stateful. This helper makes it deterministic.
120
+ function rxTest(rx, s) {
121
+ if (!rx) return false;
122
+ rx.lastIndex = 0;
123
+ return rx.test(s);
124
+ }
125
+
126
+ // Try to use the engine's globalASTCache if available for better performance
127
+ let _globalASTCache = null;
128
+ try {
129
+ _globalASTCache = require("./engine/ast-cache").globalASTCache;
130
+ } catch {
131
+ // Engine not available, will use direct parsing
132
+ }
133
+
134
+ function parseFile(code, fileAbsForErrors = "") {
135
+ // Use globalASTCache if available (engine v2 integration)
136
+ if (_globalASTCache) {
137
+ const result = _globalASTCache.parse(code, fileAbsForErrors);
138
+ if (result.ast) return result.ast;
139
+ // Fall through to direct parse if cache failed
140
+ }
141
+
142
+ // Error recovery avoids hard-failing on mixed TS/JS/JSX edge cases.
143
+ return parser.parse(code, {
144
+ sourceType: "unambiguous",
145
+ errorRecovery: true,
146
+ allowReturnOutsideFunction: true,
147
+ plugins: [
148
+ "typescript",
149
+ "jsx",
150
+ "dynamicImport",
151
+ "topLevelAwait",
152
+ "classProperties",
153
+ "classPrivateProperties",
154
+ "classPrivateMethods",
155
+ "decorators-legacy",
156
+ "optionalChaining",
157
+ "nullishCoalescingOperator",
158
+ ],
159
+ });
160
+ }
161
+
162
+ function evidenceFromLoc(fileAbs, repoRoot, loc, reason) {
163
+ if (!loc) return null;
164
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
165
+ const lines = readLinesCached(fileAbs);
166
+ const start = Math.max(1, loc.start?.line || 1);
167
+ const end = Math.max(start, loc.end?.line || start);
168
+ const snippet = lines.slice(start - 1, end).join("\n");
169
+ return {
170
+ id: stableId("ev", `${fileRel}:${start}-${end}:${reason || ""}:${sha256(snippet)}`),
171
+ file: fileRel,
172
+ lines: `${start}-${end}`,
173
+ snippetHash: sha256(snippet),
174
+ reason,
175
+ };
176
+ }
177
+
178
+ /* ============================================================================
179
+ * ROUTE GAP ENGINE (world-class missing route logic)
180
+ * ========================================================================== */
181
+
182
+ function safeUrlParse(maybeUrl) {
183
+ try {
184
+ // URL() needs protocol; allow //host/path too
185
+ if (typeof maybeUrl !== "string") return null;
186
+ if (/^https?:\/\//i.test(maybeUrl)) return new URL(maybeUrl);
187
+ if (/^\/\//.test(maybeUrl)) return new URL("https:" + maybeUrl);
188
+ return null;
189
+ } catch {
190
+ return null;
191
+ }
192
+ }
193
+
194
+ function normalizePath(raw) {
195
+ if (!raw) return "/";
196
+ let p = String(raw).trim();
197
+
198
+ // If full URL, strip to pathname.
199
+ const u = safeUrlParse(p);
200
+ if (u) p = u.pathname || "/";
201
+
202
+ // Strip query/hash if present
203
+ p = p.split("?")[0].split("#")[0];
204
+
205
+ // Decode safely
206
+ try {
207
+ p = decodeURIComponent(p);
208
+ } catch {
209
+ // keep original
210
+ }
211
+
212
+ // Ensure leading slash
213
+ if (!p.startsWith("/")) p = "/" + p;
214
+
215
+ // Collapse duplicate slashes
216
+ p = p.replace(/\/{2,}/g, "/");
217
+
218
+ // Remove trailing slash (except root)
219
+ if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
220
+
221
+ return p;
222
+ }
223
+
224
+ function pathLooksLikeAsset(p) {
225
+ const s = String(p || "");
226
+ // Common Next/static + file extensions that are not API routes
227
+ if (/^\/(_next|static|assets)\b/i.test(s)) return true;
228
+ if (/\.(png|jpg|jpeg|gif|webp|svg|ico|css|js|map|txt|xml|woff2?|ttf|eot)$/i.test(s)) return true;
229
+ return false;
230
+ }
231
+
232
+ function isInternalUtilityRoute(p) {
233
+ // Common internal/utility routes that should NOT be flagged as missing
234
+ // These are typically framework-specific, monitoring, or debugging endpoints
235
+ const internalPatterns = [
236
+ // Health/monitoring
237
+ /^\/(health|healthz|healthcheck|ready|readyz|live|livez|liveness|readiness|metrics|status|ping|version)/i,
238
+ // Internal/debug
239
+ /^\/(debug|internal|_internal|__internal|security|\.well-known)/i,
240
+ // WebSockets
241
+ /^\/(websocket|ws|socket\.io|sockjs)/i,
242
+ // Admin/dashboard
243
+ /^\/(admin|dashboard|_admin|__admin)/i,
244
+ // Next.js internals
245
+ /^\/_next\//i,
246
+ // Vite/dev tools
247
+ /^\/@vite|^\/@fs|^\/__vite/i,
248
+ // GraphQL
249
+ /^\/(graphql|graphiql|playground)/i,
250
+ // Swagger/API docs
251
+ /^\/(swagger|api-docs|openapi|docs|redoc)/i,
252
+ // Auth callbacks
253
+ /^\/(auth|oauth|callback|login|logout|signin|signout)\/?(callback|redirect)?$/i,
254
+ // Common framework routes
255
+ /^\/(favicon\.ico|robots\.txt|sitemap\.xml|manifest\.json)$/i,
256
+ // Vercel/serverless
257
+ /^\/api\/_/i,
258
+ // Hidden paths
259
+ /^\/[._]/i,
260
+ ];
261
+ return internalPatterns.some(rx => rxTest(rx, p));
262
+ }
263
+
264
+ function looksInventedRoute(p) {
265
+ // Stuff AI loves to hallucinate - but be VERY precise to avoid false positives
266
+ // Only flag patterns that are CLEARLY fake/placeholder
267
+
268
+ // Require routes to start with these patterns (not just contain them)
269
+ // This avoids flagging legitimate routes like /users/foo-bar-123
270
+ const clearlyFakeStarts = [
271
+ /^\/(fake|dummy|placeholder|asdf|qwerty|lorem|ipsum)\b/i,
272
+ /^\/(foo|bar|baz|xxx|yyy)$/i, // Only if it's the ENTIRE route segment
273
+ ];
274
+
275
+ for (const rx of clearlyFakeStarts) {
276
+ if (rxTest(rx, p)) return true;
277
+ }
278
+
279
+ // Obvious "ai generated" patterns - must be at route start
280
+ if (rxTest(/^\/(generated|auto[-_]?gen|ai[-_]?gen)\b/i, p)) return true;
281
+
282
+ // Obvious placeholder test data patterns (test123, abc123, demo123)
283
+ // Only if the ENTIRE segment is clearly placeholder
284
+ if (rxTest(/\/(test123|abc123|demo123|sample123|example123)$/i, p)) return true;
285
+
286
+ // Very long hex strings in route (32+ chars) that aren't IDs
287
+ // Skip if it looks like a valid UUID pattern or session token
288
+ const segments = p.split('/').filter(Boolean);
289
+ for (const seg of segments) {
290
+ // Long hex that's NOT a UUID format and NOT a reasonable ID length
291
+ if (/^[a-f0-9]{40,}$/i.test(seg) && !/^[a-f0-9]{8}-/.test(seg)) {
292
+ return true;
293
+ }
294
+ }
295
+
296
+ return false;
297
+ }
298
+
299
+ function canonicalizeDynamicSegments(p) {
300
+ // Convert common dynamic segments to a stable token so "/users/123" can match "/users/:id"
301
+ // NOTE: This function returns a string, not a boolean - name is for canonicalization, not validation
302
+ const segs = normalizePath(p).split("/").filter(Boolean);
303
+ const canon = segs.map((seg) => {
304
+ if (!seg) return seg;
305
+ // UUID
306
+ if (rxTest(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, seg)) return ":id";
307
+ // Numeric IDs
308
+ if (rxTest(/^\d{1,18}$/i, seg)) return ":id";
309
+ // Long hex
310
+ if (rxTest(/^(0x)?[0-9a-f]{16,}$/i, seg)) return ":id";
311
+ // Next-ish catchalls
312
+ if (seg === "[...slug]" || seg === "[[...slug]]") return ":slug";
313
+ return seg;
314
+ });
315
+ return "/" + canon.join("/");
316
+ }
317
+
318
+ function firstSegment(p) {
319
+ const seg = normalizePath(p).split("/").filter(Boolean)[0];
320
+ return seg || "";
321
+ }
322
+
323
+ function inferDominantPrefix(paths, minShare = 0.7) {
324
+ // Find a dominant first segment like "api" across a set of paths
325
+ const counts = new Map();
326
+ for (const p of paths) {
327
+ const seg = firstSegment(p);
328
+ if (!seg) continue;
329
+ counts.set(seg, (counts.get(seg) || 0) + 1);
330
+ }
331
+ let best = { seg: "", n: 0 };
332
+ for (const [seg, n] of counts.entries()) {
333
+ if (n > best.n) best = { seg, n };
334
+ }
335
+ if (!best.seg) return null;
336
+ const share = best.n / Math.max(1, paths.length);
337
+ return share >= minShare ? "/" + best.seg : null;
338
+ }
339
+
340
+ function buildServerRouteIndex(serverRoutes) {
341
+ // Index by method + first segment for fast shortlist
342
+ const byMethod = new Map(); // method -> seg -> routes[]
343
+ const all = [];
344
+
345
+ for (const r of serverRoutes) {
346
+ const method = String(r.method || "*").toUpperCase();
347
+ const pNorm = normalizePath(r.path);
348
+ const seg = firstSegment(pNorm);
349
+
350
+ const rec = { ...r, _method: method, _pathNorm: pNorm, _seg: seg, _canon: canonicalizeDynamicSegments(pNorm) };
351
+ all.push(rec);
352
+
353
+ if (!byMethod.has(method)) byMethod.set(method, new Map());
354
+ const segMap = byMethod.get(method);
355
+ if (!segMap.has(seg)) segMap.set(seg, []);
356
+ segMap.get(seg).push(rec);
357
+
358
+ // Also index wildcard bucket
359
+ if (!byMethod.has("*")) byMethod.set("*", new Map());
360
+ const w = byMethod.get("*");
361
+ if (!w.has(seg)) w.set(seg, []);
362
+ w.get(seg).push(rec);
363
+ }
364
+
365
+ return { byMethod, all };
366
+ }
367
+
368
+ function shortlistServerRoutes(index, method, pNorm) {
369
+ const m = String(method || "*").toUpperCase();
370
+ const seg = firstSegment(pNorm);
371
+
372
+ const pick = (meth) => {
373
+ const segMap = index.byMethod.get(meth);
374
+ if (!segMap) return [];
375
+ const bucket = segMap.get(seg) || [];
376
+ // If seg is empty or dynamic roots exist, include a fallback bucket
377
+ const rootBucket = segMap.get("") || [];
378
+ return bucket.concat(rootBucket);
379
+ };
380
+
381
+ // prioritize exact method, then wildcard
382
+ const a = pick(m);
383
+ const b = pick("*");
384
+ // de-dupe by path+method
385
+ const seen = new Set();
386
+ const out = [];
387
+ for (const r of a.concat(b)) {
388
+ const k = `${r._method}:${r._pathNorm}`;
389
+ if (seen.has(k)) continue;
390
+ seen.add(k);
391
+ out.push(r);
392
+ }
393
+ return out.length ? out : index.all;
394
+ }
395
+
396
+ function routeSimilarityScore(refPath, serverPathPattern) {
397
+ // Score 0..1 based on static segment overlap + prefix alignment
398
+ const a = canonicalizeDynamicSegments(refPath).split("/").filter(Boolean);
399
+ const b = canonicalizeDynamicSegments(serverPathPattern).split("/").filter(Boolean);
400
+
401
+ if (!a.length || !b.length) return 0;
402
+
403
+ const aStatic = a.filter((s) => !s.startsWith(":") && !s.startsWith("["));
404
+ const bStatic = b.filter((s) => !s.startsWith(":") && !s.startsWith("["));
405
+
406
+ const setA = new Set(aStatic);
407
+ const setB = new Set(bStatic);
408
+
409
+ let inter = 0;
410
+ for (const s of setA) if (setB.has(s)) inter++;
411
+
412
+ const union = new Set([...setA, ...setB]).size || 1;
413
+
414
+ const jaccard = inter / union;
415
+
416
+ // prefix bonus if first 1-2 segments align
417
+ const prefix1 = a[0] && b[0] && a[0] === b[0] ? 0.15 : 0;
418
+ const prefix2 = a[1] && b[1] && a[1] === b[1] ? 0.10 : 0;
419
+
420
+ // length penalty if wildly different
421
+ const lenPenalty = Math.min(0.25, Math.abs(a.length - b.length) * 0.05);
422
+
423
+ const score = Math.max(0, Math.min(1, jaccard + prefix1 + prefix2 - lenPenalty));
424
+ return score;
425
+ }
426
+
427
+ function compileAllowPatterns(patterns) {
428
+ const out = [];
429
+ for (const p of patterns || []) {
430
+ if (!p) continue;
431
+ if (p instanceof RegExp) {
432
+ out.push(p);
433
+ continue;
434
+ }
435
+ const s = String(p);
436
+ // Support "/.../i" style
437
+ const m = s.match(/^\/(.+)\/([gimsuy]*)$/);
438
+ if (m) {
439
+ try {
440
+ out.push(new RegExp(m[1], m[2]));
441
+ continue;
442
+ } catch {
443
+ // fall through
444
+ }
445
+ }
446
+ // Simple wildcard "*" and "?" -> regex
447
+ const esc = s
448
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
449
+ .replace(/\*/g, ".*")
450
+ .replace(/\?/g, ".");
451
+ try {
452
+ out.push(new RegExp("^" + esc + "$", "i"));
453
+ } catch {
454
+ // ignore bad patterns
455
+ }
456
+ }
457
+ return out;
458
+ }
459
+
460
+ function findMissingRoutes(truthpack) {
461
+ const findings = [];
462
+
463
+ const server = truthpack?.routes?.server || [];
464
+ const refs = truthpack?.routes?.clientRefs || [];
465
+ const gaps = truthpack?.routes?.gaps || [];
466
+
467
+ const hasGaps = gaps.length > 0;
468
+
469
+ // Allowlist/suppressions (lets users kill known false positives cleanly)
470
+ const allowMethods = new Set((truthpack?.routes?.allowlistMethods || []).map((m) => String(m).toUpperCase()));
471
+ const allowPatterns = compileAllowPatterns(truthpack?.routes?.allowlist || truthpack?.routes?.allowlistPatterns || []);
472
+ const ignorePatterns = compileAllowPatterns(truthpack?.routes?.ignore || truthpack?.routes?.ignorePatterns || []);
473
+
474
+ const isSuppressed = (method, pNorm) => {
475
+ const m = String(method || "*").toUpperCase();
476
+ if (allowMethods.size && !allowMethods.has(m) && !allowMethods.has("*")) {
477
+ // method not allowed by allowMethods -> don't suppress
478
+ }
479
+ for (const rx of ignorePatterns) if (rxTest(rx, `${m} ${pNorm}`) || rxTest(rx, pNorm)) return true;
480
+ for (const rx of allowPatterns) if (rxTest(rx, `${m} ${pNorm}`) || rxTest(rx, pNorm)) return true;
481
+ return false;
482
+ };
483
+
484
+ const serverCount = server.length;
485
+ const refCount = refs.length;
486
+
487
+ // Monorepo heuristic (keep, but make it less dumb)
488
+ const isLikelyMonorepo = refCount > Math.max(30, serverCount * 2.5);
489
+
490
+ const serverPaths = server.map((r) => normalizePath(r.path));
491
+ const refPaths = refs.map((r) => normalizePath(r.path));
492
+
493
+ // Dominant prefixes (commonly "/api")
494
+ const dominantServerPrefix = inferDominantPrefix(serverPaths, 0.7);
495
+ const dominantRefPrefix = inferDominantPrefix(refPaths, 0.7);
496
+
497
+ const serverPrefix = dominantServerPrefix || null;
498
+ const refPrefix = dominantRefPrefix || null;
499
+
500
+ const index = buildServerRouteIndex(server);
501
+
502
+ // Route-map quality gating:
503
+ // If we have unresolved gaps or tiny server map, DO NOT BLOCK unless obviously invented.
504
+ const routeMapQuality =
505
+ serverCount >= 10 && !hasGaps ? "strong" :
506
+ serverCount >= 5 ? "medium" :
507
+ "weak";
508
+
509
+ // More generous caps (but still bounded)
510
+ const MAX_WARNINGS = isLikelyMonorepo ? 35 : 60;
511
+ const MAX_BLOCKS = 15;
512
+
513
+ let warnCount = 0;
514
+ let blockCount = 0;
515
+
516
+ // Summaries to help you fix extraction instead of drowning in noise
517
+ const unmatchedByPrefix = new Map();
518
+ const externalRefs = [];
519
+
520
+ function addUnmatchedPrefix(pNorm) {
521
+ const seg = firstSegment(pNorm) || "/";
522
+ unmatchedByPrefix.set(seg, (unmatchedByPrefix.get(seg) || 0) + 1);
523
+ }
524
+
525
+ function tryMatch(method, pNorm) {
526
+ const shortlist = shortlistServerRoutes(index, method, pNorm);
527
+
528
+ // Try exact + canonicalized matching
529
+ const pCanon = canonicalizeDynamicSegments(pNorm);
530
+
531
+ for (const r of shortlist) {
532
+ if (routeMatches(r, method, pNorm) || routeMatches(r, "*", pNorm)) return { ok: true, matched: r };
533
+ // Try canonicalized path vs canonicalized server path
534
+ if (routeMatches({ ...r, path: r._canon }, method, pCanon) || routeMatches({ ...r, path: r._canon }, "*", pCanon)) {
535
+ return { ok: true, matched: r, usedCanon: true };
536
+ }
537
+ }
538
+ return { ok: false };
539
+ }
540
+
541
+ function closestSuggestions(method, pNorm) {
542
+ // Use a bounded scan: shortlist first, then broaden if needed
543
+ const shortlist = shortlistServerRoutes(index, method, pNorm);
544
+ const pool = shortlist.length ? shortlist : index.all;
545
+
546
+ const scored = pool
547
+ .map((r) => ({ r, score: routeSimilarityScore(pNorm, r._pathNorm) }))
548
+ .sort((a, b) => b.score - a.score)
549
+ .slice(0, 3);
550
+
551
+ // Raise threshold to 0.50 to only show genuinely similar routes
552
+ // This reduces "did you mean" noise for unrelated routes
553
+ return scored.filter((x) => x.score >= 0.50).map((x) => ({
554
+ method: x.r._method,
555
+ path: x.r._pathNorm,
556
+ score: Number(x.score.toFixed(2)),
557
+ }));
558
+ }
559
+
560
+ function detectMethodMismatch(pNorm, method) {
561
+ // If the path exists but only under other method(s), that's not "missing route"
562
+ const methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "*"];
563
+ const hits = [];
564
+ for (const m of methods) {
565
+ if (String(m) === String(method).toUpperCase()) continue;
566
+ const res = tryMatch(m, pNorm);
567
+ if (res.ok) hits.push(m);
568
+ }
569
+ return hits.length ? hits : null;
570
+ }
571
+
572
+ for (const ref of refs) {
573
+ const rawPath = ref?.path;
574
+ const method = String(ref?.method || "*").toUpperCase();
575
+
576
+ // Normalize & classify
577
+ const u = safeUrlParse(rawPath);
578
+ const pNorm = normalizePath(rawPath);
579
+
580
+ // Skip asset-ish refs
581
+ if (pathLooksLikeAsset(pNorm)) continue;
582
+
583
+ // External refs: if full URL and host isn't localhost, treat as external service.
584
+ if (u && u.host && !/^(localhost|127\.0\.0\.1)(:\d+)?$/i.test(u.host)) {
585
+ externalRefs.push({ host: u.host, method, path: pNorm, evidence: ref.evidence || [] });
586
+ continue;
587
+ }
588
+
589
+ // Skip suppressed
590
+ if (isSuppressed(method, pNorm)) continue;
591
+
592
+ // Build candidate variants (this kills a lot of false positives)
593
+ const candidates = new Set();
594
+ candidates.add(pNorm);
595
+
596
+ // trailing slash variant
597
+ if (pNorm.length > 1) {
598
+ candidates.add(pNorm + "/");
599
+ candidates.add(pNorm.replace(/\/+$/g, ""));
600
+ }
601
+
602
+ // Toggle dominant server prefix (ex: /api) if mismatch likely
603
+ if (serverPrefix && serverPrefix !== "/" && !pNorm.startsWith(serverPrefix + "/") && pNorm !== serverPrefix) {
604
+ candidates.add(normalizePath(serverPrefix + pNorm));
605
+ }
606
+ if (serverPrefix && serverPrefix !== "/" && pNorm.startsWith(serverPrefix + "/")) {
607
+ candidates.add(normalizePath(pNorm.slice(serverPrefix.length)));
608
+ }
609
+
610
+ // Toggle dominant ref prefix similarly (sometimes refs have /api but server routes stored without it)
611
+ if (refPrefix && refPrefix !== "/" && !pNorm.startsWith(refPrefix + "/") && pNorm !== refPrefix) {
612
+ candidates.add(normalizePath(refPrefix + pNorm));
613
+ }
614
+ if (refPrefix && refPrefix !== "/" && pNorm.startsWith(refPrefix + "/")) {
615
+ candidates.add(normalizePath(pNorm.slice(refPrefix.length)));
616
+ }
617
+
618
+ // Canonicalized variant
619
+ candidates.add(canonicalizeDynamicSegments(pNorm));
620
+
621
+ // Try match all candidates
622
+ let matched = null;
623
+ let usedCanon = false;
624
+ for (const cand of candidates) {
625
+ const res = tryMatch(method, cand);
626
+ if (res.ok) {
627
+ matched = res.matched;
628
+ usedCanon = !!res.usedCanon;
629
+ break;
630
+ }
631
+ }
632
+ if (matched) continue;
633
+
634
+ addUnmatchedPrefix(pNorm);
635
+
636
+ // Method mismatch detection (not missing route)
637
+ const methodMismatch = detectMethodMismatch(pNorm, method);
638
+ if (methodMismatch) {
639
+ // Keep as WARN (not missing route) — reduces noise dramatically
640
+ if (warnCount >= MAX_WARNINGS) continue;
641
+ warnCount++;
642
+
643
+ findings.push({
644
+ id: stableId("F_ROUTE_METHOD_MISMATCH", `${method} ${pNorm}`),
645
+ severity: "WARN",
646
+ category: "MissingRoute",
647
+ title: `Method mismatch for route: ${method} ${pNorm}`,
648
+ why: `A server route exists for this path, but not for method ${method}. This is often a client bug or an incorrect assumption.`,
649
+ confidence: routeMapQuality === "strong" ? "high" : "med",
650
+ evidence: ref.evidence || [],
651
+ fixHints: [
652
+ `Check the client call method. Server supports: ${methodMismatch.join(", ")} for ${pNorm}`,
653
+ "If this is intentional, update the server to accept this method or adjust the client.",
654
+ ],
655
+ });
656
+ continue;
657
+ }
658
+
659
+ const invented = looksInventedRoute(pNorm);
660
+ const internal = isInternalUtilityRoute(pNorm);
661
+
662
+ // Skip internal utility routes entirely - they're almost never real issues
663
+ if (internal) continue;
664
+
665
+ // Similarity suggestions
666
+ const suggestions = closestSuggestions(method, pNorm);
667
+
668
+ // Confidence + severity gating - CONSERVATIVE by default to reduce noise
669
+ let confidence = "low";
670
+ let severity = "WARN";
671
+
672
+ if (invented) {
673
+ // Only BLOCK truly invented routes - and require high confidence
674
+ severity = "BLOCK";
675
+ confidence = "high";
676
+ } else if (routeMapQuality === "strong" && !isLikelyMonorepo) {
677
+ // Even with strong route map, be conservative
678
+ const best = suggestions[0]?.score ?? 0;
679
+ if (best < 0.30) {
680
+ // No close matches - more likely to be a real issue, but still WARN
681
+ confidence = "med";
682
+ severity = "WARN";
683
+ } else if (best >= 0.70) {
684
+ // Very close match exists - probably a typo, keep as low-priority WARN
685
+ confidence = "low";
686
+ severity = "WARN";
687
+ } else {
688
+ // Moderate similarity - unclear
689
+ confidence = "low";
690
+ severity = "WARN";
691
+ }
692
+ } else {
693
+ // Weak route map or monorepo - don't trust findings, always WARN with low confidence
694
+ confidence = "low";
695
+ severity = "WARN";
696
+ }
697
+
698
+ // caps
699
+ if (severity === "BLOCK") {
700
+ if (blockCount >= MAX_BLOCKS) continue;
701
+ blockCount++;
702
+ } else {
703
+ if (warnCount >= MAX_WARNINGS) continue;
704
+ warnCount++;
705
+ }
706
+
707
+ const didYouMean = suggestions.length
708
+ ? `Closest server routes: ${suggestions.map((s) => `${s.method} ${s.path} (${s.score})`).join(" • ")}`
709
+ : "No close server route candidates were found (based on static segment similarity).";
710
+
711
+ findings.push({
712
+ id: stableId("F_MISSING_ROUTE", `${method} ${pNorm}`),
713
+ severity,
714
+ category: "MissingRoute",
715
+ title: `Client references route not found in detected server map: ${method} ${pNorm}`,
716
+ why:
717
+ severity === "BLOCK"
718
+ ? "This looks invented. Shipping this will break flows (404 / silent failure)."
719
+ : routeMapQuality === "weak" || hasGaps
720
+ ? "Route reference didn't match the detected server map. Route detection may be incomplete (dynamic registration, plugins, prefixes)."
721
+ : "Route reference didn't match the detected server map. This can be a real missing endpoint or an undetected server route.",
722
+ confidence,
723
+ evidence: ref.evidence || [],
724
+ fixHints: [
725
+ didYouMean,
726
+ usedCanon ? "Note: matching tried canonicalized ID segments (/:id normalization)." : "Matching tried normalization (origin/query/trailing slash/prefix toggles).",
727
+ hasGaps ? `Route map had ${gaps.length} unresolved sources; fix route extraction to reduce false positives.` : "If this is a real endpoint, add it server-side or correct the client call.",
728
+ isLikelyMonorepo ? "Monorepo/microservices likely: consider allowlisting external services or feeding service domains into truthpack." : "If this is an external service call, store it as external/allowlisted so it won't be flagged.",
729
+ ],
730
+ });
731
+ }
732
+
733
+ // Route map diagnostics (actionable, reduces "blame the analyzer" loops)
734
+ if (hasGaps) {
735
+ findings.push({
736
+ id: stableId("F_ROUTE_MAP_GAPS", String(gaps.length)),
737
+ severity: "WARN",
738
+ category: "RouteMapGaps",
739
+ title: `Route map incomplete (${gaps.length} unresolved route sources)`,
740
+ why: "Dynamic registration, unresolved plugin imports, or non-standard routing prevented complete detection. Missing route findings may be false positives.",
741
+ confidence: "med",
742
+ evidence: [],
743
+ fixHints: [
744
+ "Fix route extraction: resolve Fastify plugins and prefix registration (fastify.register(...,{ prefix })) and inline fastify.get/post routes.",
745
+ "If using Next App Router: ensure route handlers (app/**/route.ts) are included in server route extraction.",
746
+ "Add allowlistPatterns for external services to silence expected gaps.",
747
+ ],
748
+ });
749
+ }
750
+
751
+ // External refs summary (useful in microservices)
752
+ if (externalRefs.length) {
753
+ const topHosts = new Map();
754
+ for (const r of externalRefs) topHosts.set(r.host, (topHosts.get(r.host) || 0) + 1);
755
+ const hostSummary = [...topHosts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5)
756
+ .map(([h, n]) => `${h} (${n})`).join(", ");
757
+
758
+ findings.push({
759
+ id: stableId("F_EXTERNAL_ROUTE_REFS", hostSummary),
760
+ severity: "INFO",
761
+ category: "MissingRoute",
762
+ title: `External service routes detected in client refs (${externalRefs.length})`,
763
+ why: "These are full-URL calls to non-local hosts; they are not expected to match server route maps.",
764
+ confidence: "high",
765
+ evidence: [],
766
+ fixHints: [
767
+ `Top hosts: ${hostSummary}`,
768
+ "If you want to validate external APIs, add a separate analyzer that checks OpenAPI/spec contracts for those services.",
769
+ "Optionally add allowlistPatterns like '/^https?:\\/\\/(api\\.stripe\\.com|...)/' at truthpack.routes.allowlistPatterns.",
770
+ ],
771
+ });
772
+ }
773
+
774
+ // Biggest unmatched prefixes (points directly at extraction gaps)
775
+ if (unmatchedByPrefix.size) {
776
+ const top = [...unmatchedByPrefix.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
777
+ const summary = top.map(([k, v]) => `${k}:${v}`).join(" • ");
778
+ findings.push({
779
+ id: stableId("F_ROUTE_UNMATCHED_PREFIXES", summary),
780
+ severity: "INFO",
781
+ category: "MissingRoute",
782
+ title: "Unmatched client route prefixes (helps fix extraction/allowlists)",
783
+ why: "When one prefix dominates unmatched refs, it usually means server extraction missed a router/plugin prefix or the client is calling a different service.",
784
+ confidence: "med",
785
+ evidence: [],
786
+ fixHints: [
787
+ `Top unmatched prefixes: ${summary}`,
788
+ serverPrefix ? `Dominant server prefix inferred: ${serverPrefix}` : "No dominant server prefix detected.",
789
+ "If these should be local, improve route extraction for that prefix (Fastify register(prefix), Next middleware rewrites, basePath, etc.).",
790
+ ],
791
+ });
792
+ }
793
+
794
+ return findings;
795
+ }
796
+
797
+ /* ============================================================================
798
+ * ENV GAPS ANALYZER (tightened + fewer false positives)
799
+ * ========================================================================== */
800
+
801
+ function findEnvGaps(truthpack) {
802
+ const findings = [];
803
+ const used = truthpack?.env?.vars || [];
804
+ const declared = new Set(truthpack?.env?.declared || []);
805
+ const declaredSources = truthpack?.env?.declaredSources || [];
806
+
807
+ // Well-known system/CI env vars that shouldn't be flagged
808
+ const systemEnvVars = new Set([
809
+ "HOME","USER","PATH","PWD","SHELL","TERM","LANG","TZ","TMPDIR","TEMP","TMP","COLORTERM","FORCE_COLOR","NO_COLOR",
810
+ "APPDATA","LOCALAPPDATA","USERPROFILE","COMPUTERNAME","USERNAME","HOMEDRIVE","HOMEPATH","SYSTEMROOT","WINDIR",
811
+ "PROGRAMFILES","PROGRAMDATA","COMMONPROGRAMFILES",
812
+ "NODE_ENV","NODE_OPTIONS","NODE_PATH","NODE_DEBUG","NODE_NO_WARNINGS",
813
+ "CI","CONTINUOUS_INTEGRATION","BUILD_NUMBER","BUILD_ID",
814
+ "GITHUB_ACTIONS","GITHUB_WORKFLOW","GITHUB_RUN_ID","GITHUB_RUN_NUMBER","GITHUB_SHA","GITHUB_REF","GITHUB_ACTOR",
815
+ "GITLAB_CI","CI_COMMIT_SHA","CI_PIPELINE_ID","CI_JOB_ID",
816
+ "CIRCLECI","CIRCLE_BUILD_NUM","CIRCLE_SHA1","CIRCLE_BRANCH",
817
+ "TRAVIS","TRAVIS_BUILD_NUMBER","TRAVIS_COMMIT",
818
+ "JENKINS_URL","BUILD_TAG","GIT_COMMIT",
819
+ "BUILDKITE","BUILDKITE_BUILD_NUMBER","BUILDKITE_COMMIT",
820
+ "CODEBUILD_BUILD_ID","CODEBUILD_RESOLVED_SOURCE_VERSION",
821
+ "VERCEL","VERCEL_ENV","VERCEL_URL","VERCEL_GIT_COMMIT_SHA",
822
+ "NETLIFY","CONTEXT","DEPLOY_PRIME_URL",
823
+ "RAILWAY_ENVIRONMENT","RAILWAY_GIT_COMMIT_SHA",
824
+ "HEROKU","DYNO","RENDER","FLY_APP_NAME",
825
+ "HTTP_PROXY","HTTPS_PROXY","NO_PROXY","http_proxy","https_proxy","no_proxy",
826
+ "HOSTNAME","HOST",
827
+ "DEBUG","VERBOSE","LOG_LEVEL",
828
+ "EDITOR","VISUAL","VSCODE_PID","TERM_SESSION_ID",
829
+ "PORT","npm_package_version","npm_package_name",
830
+ ]);
831
+
832
+ // Patterns for env vars that are commonly optional/internal
833
+ const optionalPatterns = [
834
+ /^(OPENAI|ANTHROPIC|COHERE|AZURE|AWS|GCP|GOOGLE)_/i,
835
+ /^(STRIPE|PAYPAL|PLAID)_/i,
836
+ /^(SENDGRID|RESEND|MAILGUN|SES)_/i,
837
+ /^(SENTRY|DATADOG|NEWRELIC|LOGROCKET)_/i,
838
+ /^(REDIS|POSTGRES|MYSQL|MONGO|DATABASE)_/i,
839
+ /^(NEXT_|NUXT_|VITE_|REACT_APP_)/i,
840
+ /^(VIBECHECK|GUARDRAIL)_/i,
841
+ /_(URL|KEY|SECRET|TOKEN|ID|PASSWORD|HOST|PORT)$/i,
842
+ /^(ENABLE_|DISABLE_|USE_|SKIP_|ALLOW_|NO_)/i,
843
+ /^(MAX_|MIN_|DEFAULT_|TIMEOUT_|LIMIT_|RATE_)/i,
844
+ /^(LOG_|DEBUG_|VERBOSE_|TRACE_)/i,
845
+ /^(TEST_|DEV_|STAGING_|PROD_)/i,
846
+ /^(ARTIFACTS_|CACHE_|TMP_|OUTPUT_)/i,
847
+ /^npm_/i,
848
+ ];
849
+
850
+ function isOptionalEnvVar(name) {
851
+ return optionalPatterns.some((p) => rxTest(p, name));
852
+ }
853
+
854
+ // Heuristic: treat vars referenced only in tooling/scripts as WARN (not BLOCK)
855
+ function evidenceIsToolingOnly(v) {
856
+ const refs = v.references || [];
857
+ if (!refs.length) return false;
858
+ return refs.every((r) => {
859
+ const f = String(r.file || "");
860
+ return /(^|\/)(scripts|tools|bin|cli|devops|infra|config)\//i.test(f);
861
+ });
862
+ }
863
+
864
+ for (const v of used) {
865
+ if (!v?.name) continue;
866
+ if (declared.has(v.name)) continue;
867
+ if (systemEnvVars.has(v.name)) continue;
868
+ if (isOptionalEnvVar(v.name)) continue;
869
+
870
+ const toolingOnly = evidenceIsToolingOnly(v);
871
+
872
+ // Only BLOCK if it's truly required, no fallback, and not tooling-only
873
+ const isReallyRequired = !!v.required && !v.hasFallback && !toolingOnly;
874
+ const sev = isReallyRequired ? "BLOCK" : "WARN";
875
+
876
+ findings.push({
877
+ id: `F_ENV_UNDECLARED_${v.name}`,
878
+ severity: sev,
879
+ category: "EnvContract",
880
+ title: `Env var used but not declared in env templates: ${v.name}`,
881
+ why: isReallyRequired
882
+ ? "Required env var is used with no fallback and not documented. This ships broken installs."
883
+ : toolingOnly
884
+ ? "Env var appears used in tooling/scripts. Document it if users need it; otherwise ignore."
885
+ : "Env var appears optional/guarded but should still be documented to prevent guesswork.",
886
+ confidence: isReallyRequired ? "high" : "low",
887
+ evidence: v.references || [],
888
+ fixHints: [
889
+ `Add to .env.example: ${v.name}=your_value_here`,
890
+ isReallyRequired
891
+ ? `Add fallback: const value = process.env.${v.name} ?? 'default';`
892
+ : `Guard usage: if (process.env.${v.name}) { /* use it */ }`,
893
+ "Docs: https://12factor.net/config",
894
+ ],
895
+ });
896
+ }
897
+
898
+ // Declared but never used
899
+ const usedSet = new Set(used.map((v) => v.name));
900
+ for (const name of declared) {
901
+ if (usedSet.has(name)) continue;
902
+ findings.push({
903
+ id: `F_ENV_UNUSED_${name}`,
904
+ severity: "WARN",
905
+ category: "EnvContract",
906
+ title: `Env var declared but never used: ${name}`,
907
+ why: "Dead config creates confusion and invites hallucinated wiring.",
908
+ confidence: "med",
909
+ evidence: [],
910
+ fixHints: [
911
+ "Remove it from templates if obsolete, or wire it intentionally.",
912
+ "If used only in infra/runtime, document that explicitly (where/why).",
913
+ ],
914
+ });
915
+ }
916
+
917
+ if (!declaredSources.length && used.length) {
918
+ findings.push({
919
+ id: "F_ENV_NO_TEMPLATE",
920
+ severity: "WARN",
921
+ category: "EnvContract",
922
+ title: "No .env.example/.env.template found",
923
+ why: "Without an env contract, humans and AI guess env vars and ship broken setups.",
924
+ confidence: "high",
925
+ evidence: [],
926
+ fixHints: ["Add a .env.example listing required/optional vars with comments."],
927
+ });
928
+ }
929
+
930
+ return findings;
931
+ }
932
+
933
+ /* ============================================================================
934
+ * FAKE SUCCESS ANALYZER (kept, but made safer & less noisy)
935
+ * ========================================================================== */
936
+
937
+ function isToastSuccessCall(node) {
938
+ return !!(
939
+ t.isCallExpression(node) &&
940
+ t.isMemberExpression(node.callee) &&
941
+ t.isIdentifier(node.callee.object, { name: "toast" }) &&
942
+ t.isIdentifier(node.callee.property, { name: "success" })
943
+ );
944
+ }
945
+
946
+ function isRouterPushCall(node) {
947
+ return (
948
+ t.isCallExpression(node) &&
949
+ ((t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.property, { name: "push" })) ||
950
+ (t.isIdentifier(node.callee) && node.callee.name === "navigate"))
951
+ );
952
+ }
953
+
954
+ function isFetchCall(node) {
955
+ return !!(t.isCallExpression(node) && t.isIdentifier(node.callee, { name: "fetch" }));
956
+ }
957
+
958
+ function isAxiosCall(node) {
959
+ return !!(
960
+ t.isCallExpression(node) &&
961
+ t.isMemberExpression(node.callee) &&
962
+ t.isIdentifier(node.callee.object, { name: "axios" }) &&
963
+ t.isIdentifier(node.callee.property) &&
964
+ ["get", "post", "put", "patch", "delete"].includes(node.callee.property.name)
965
+ );
966
+ }
967
+
968
+ function findFakeSuccess(repoRoot) {
969
+ const findings = [];
970
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
971
+ cwd: repoRoot,
972
+ absolute: true,
973
+ ignore: STANDARD_IGNORE_PATTERNS,
974
+ });
975
+
976
+ for (const fileAbs of files) {
977
+ const code = readFileCached(fileAbs);
978
+
979
+ // V3: FAST PATH OPTIMIZATION
980
+ // AST parsing is 100x slower than regex. Skip files that don't contain
981
+ // relevant keywords (toast/push/navigate AND fetch/axios).
982
+ const hasSuccessUI = /\b(toast|\.push|navigate)\b/.test(code);
983
+ const hasNetworkCall = /\b(fetch|axios)\b/.test(code);
984
+ if (!hasSuccessUI || !hasNetworkCall) {
985
+ continue;
986
+ }
987
+
988
+ let ast;
989
+ try {
990
+ ast = parseFile(code, fileAbs);
991
+ } catch {
992
+ continue;
993
+ }
994
+
995
+ try {
996
+ traverse(ast, {
997
+ Function(pathFn) {
998
+ // Collect call sites with positions to reduce false positives.
999
+ const successCalls = [];
1000
+ const networkCalls = [];
1001
+ const okChecks = [];
1002
+
1003
+ pathFn.traverse({
1004
+ CallExpression(p) {
1005
+ const n = p.node;
1006
+
1007
+ if (isToastSuccessCall(n) || isRouterPushCall(n)) {
1008
+ successCalls.push({ loc: n.loc, pos: n.start ?? 0 });
1009
+ }
1010
+
1011
+ if (isFetchCall(n) || isAxiosCall(n)) {
1012
+ const isAwaited = p.parentPath && p.parentPath.isAwaitExpression();
1013
+ networkCalls.push({ pos: n.start ?? 0, awaited: !!isAwaited });
1014
+ }
1015
+ },
1016
+ IfStatement(p) {
1017
+ const test = p.node.test;
1018
+ const txt = code.slice(test.start || 0, test.end || 0);
1019
+ // Check for response status validation (res.ok, status)
1020
+ if (/\b(res|response)\b/i.test(txt) && /\b(ok|status)\b/i.test(txt)) okChecks.push({ pos: p.node.start ?? 0 });
1021
+ // Check for response body validation (data, error, success property checks)
1022
+ if (/\b(data|result|response)\b/i.test(txt) && /\b(error|success|\.data|\.result)\b/i.test(txt)) okChecks.push({ pos: p.node.start ?? 0 });
1023
+ },
1024
+ // Also check for try-catch around the network call as a form of validation
1025
+ TryStatement(tryPath) {
1026
+ if (tryPath.node.handler) {
1027
+ okChecks.push({ pos: tryPath.node.start ?? 0 });
1028
+ }
1029
+ },
1030
+ });
1031
+
1032
+ if (!successCalls.length || !networkCalls.length) return;
1033
+
1034
+ // For each success call: if there exists an awaited network call before it, it's less severe.
1035
+ for (const sc of successCalls) {
1036
+ const netBefore = networkCalls.filter((n) => n.pos < sc.pos);
1037
+ const awaitedBefore = netBefore.some((n) => n.awaited);
1038
+ const okCheckBefore = okChecks.some((c) => c.pos < sc.pos);
1039
+
1040
+ // If no awaited call before success -> strong signal
1041
+ const severity = awaitedBefore ? (okCheckBefore ? null : "WARN") : "BLOCK";
1042
+ if (!severity) continue;
1043
+
1044
+ const ev = evidenceFromLoc(fileAbs, repoRoot, sc.loc, "Success UI call in networked flow");
1045
+ findings.push({
1046
+ id: stableId("F_FAKE_SUCCESS", `${path.relative(repoRoot, fileAbs)}:${sc.pos}:${severity}`),
1047
+ severity,
1048
+ category: "FakeSuccess",
1049
+ title:
1050
+ severity === "BLOCK"
1051
+ ? "Success UI triggered without awaiting network call"
1052
+ : "Success UI triggered without verifying network result (res.ok/status)",
1053
+ why:
1054
+ severity === "BLOCK"
1055
+ ? "This ships lies. Users see success even when the request never completed."
1056
+ : "You're not gating success on a real response; this often ships false success.",
1057
+ confidence: "med",
1058
+ evidence: ev ? [ev] : [],
1059
+ fixHints: [
1060
+ "const res = await fetch(...); if (!res.ok) throw new Error('Request failed');",
1061
+ "const data = await res.json(); if (data.success) toast.success('Done!'); else toast.error(data.error);",
1062
+ "Use try-catch: try { await api(); toast.success(); } catch (e) { toast.error(e.message); }",
1063
+ ],
1064
+ });
1065
+ }
1066
+ },
1067
+ });
1068
+ } catch {
1069
+ // Babel traverse can fail on some edge-case files; skip them
1070
+ continue;
1071
+ }
1072
+ }
1073
+
1074
+ return findings;
1075
+ }
1076
+
1077
+ /* ============================================================================
1078
+ * GHOST AUTH ANALYZER (kept)
1079
+ * ========================================================================== */
1080
+
1081
+ function looksSensitive(pathStr) {
1082
+ const p = String(pathStr || "");
1083
+ return (
1084
+ p.startsWith("/api/admin") ||
1085
+ p.startsWith("/api/billing") ||
1086
+ p.startsWith("/api/stripe") ||
1087
+ p.startsWith("/api/org") ||
1088
+ p.startsWith("/api/team") ||
1089
+ p.startsWith("/api/account") ||
1090
+ p.startsWith("/api/settings") ||
1091
+ p.startsWith("/api/users") ||
1092
+ p.startsWith("/api/user")
1093
+ );
1094
+ }
1095
+
1096
+ function hasRouteLevelProtection(routeDef) {
1097
+ const hooks = routeDef.hooks || [];
1098
+ return !!(hooks.includes("preHandler") || hooks.includes("onRequest") || hooks.includes("preValidation"));
1099
+ }
1100
+
1101
+ function handlerHasAuthSignal(repoRoot, handlerRel) {
1102
+ const abs = path.join(repoRoot, handlerRel);
1103
+ if (!fs.existsSync(abs)) return false;
1104
+ const code = readFileCached(abs);
1105
+
1106
+ return (
1107
+ /\bgetServerSession\b|\bauth\(\)\b|\bclerk\b|@clerk\/nextjs|\bcreateRouteHandlerClient\b|@supabase/i.test(code) ||
1108
+ /\b(jwtVerify|authorization|bearer|verifyToken|verifyJWT)\b/i.test(code) ||
1109
+ /\b(isAdmin|adminOnly|permissions|rbac)\b/i.test(code)
1110
+ );
1111
+ }
1112
+
1113
+ function isProtectedByNextMiddleware(truthpack, routePath) {
1114
+ const patterns = truthpack?.auth?.nextMatcherPatterns || [];
1115
+ return !!matcherCoversPath(patterns, routePath);
1116
+ }
1117
+
1118
+ function findGhostAuth(truthpack, repoRoot) {
1119
+ const findings = [];
1120
+ const server = truthpack?.routes?.server || [];
1121
+
1122
+ // Track mutation routes without CSRF protection
1123
+ const mutationMethods = new Set(["POST", "PUT", "PATCH", "DELETE"]);
1124
+
1125
+ for (const r of server) {
1126
+ const isMutation = mutationMethods.has(String(r.method).toUpperCase());
1127
+
1128
+ if (!looksSensitive(r.path)) continue;
1129
+
1130
+ const middlewareProtected = isProtectedByNextMiddleware(truthpack, r.path);
1131
+ const routeHooksProtected = hasRouteLevelProtection(r);
1132
+ const handlerProtected = r.handler ? handlerHasAuthSignal(repoRoot, r.handler) : false;
1133
+
1134
+ const protectedSomehow = middlewareProtected || routeHooksProtected || handlerProtected;
1135
+
1136
+ if (!protectedSomehow) {
1137
+ findings.push({
1138
+ id: stableId("F_GHOST_AUTH", `${r.method} ${r.path}`),
1139
+ severity: "BLOCK",
1140
+ category: "GhostAuth",
1141
+ title: `Sensitive endpoint appears unprotected: ${r.method} ${r.path}`,
1142
+ why: "If the server doesn't enforce auth, it's public. UI gating is irrelevant.",
1143
+ confidence: "med",
1144
+ evidence: (r.evidence || []).slice(0, 2),
1145
+ fixHints: [
1146
+ `Next.js: const session = await getServerSession(); if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });`,
1147
+ `Fastify: fastify.addHook('preHandler', async (req, reply) => { if (!req.user) reply.code(401).send({ error: 'Unauthorized' }); });`,
1148
+ "Or add to middleware.ts matcher: export const config = { matcher: ['/api/admin/:path*'] };",
1149
+ "Docs: https://nextjs.org/docs/app/building-your-application/authentication",
1150
+ ],
1151
+ });
1152
+ }
1153
+
1154
+ // Check for CSRF protection on mutations
1155
+ if (isMutation && r.handler) {
1156
+ const abs = path.join(repoRoot, r.handler);
1157
+ if (fs.existsSync(abs)) {
1158
+ const code = readFileCached(abs);
1159
+ const hasCSRFCheck = /\b(csrf|csrfToken|_csrf|x-csrf-token|xsrf|anti-forgery)\b/i.test(code);
1160
+ const isAPIRoute = r.path.startsWith("/api/");
1161
+
1162
+ // Only warn for non-API routes (API routes typically use bearer tokens)
1163
+ if (!hasCSRFCheck && !isAPIRoute) {
1164
+ findings.push({
1165
+ id: stableId("F_GHOST_AUTH_NO_CSRF", `${r.method} ${r.path}`),
1166
+ severity: "WARN",
1167
+ category: "GhostAuth",
1168
+ title: `Mutation endpoint without CSRF protection: ${r.method} ${r.path}`,
1169
+ why: "State-changing endpoints should verify CSRF tokens to prevent cross-site request forgery attacks.",
1170
+ confidence: "low",
1171
+ evidence: (r.evidence || []).slice(0, 2),
1172
+ fixHints: [
1173
+ "Add CSRF token validation for form submissions.",
1174
+ "Or use SameSite cookies + Origin header validation.",
1175
+ "API routes using bearer tokens are generally exempt.",
1176
+ ],
1177
+ });
1178
+ }
1179
+ }
1180
+ }
1181
+ }
1182
+
1183
+ const patterns = truthpack?.auth?.nextMatcherPatterns || [];
1184
+ if (patterns.length) {
1185
+ const coversApi = patterns.some((p) => String(p).includes("/api"));
1186
+ if (!coversApi) {
1187
+ findings.push({
1188
+ id: "F_MIDDLEWARE_NOT_COVERING_API",
1189
+ severity: "WARN",
1190
+ category: "GhostAuth",
1191
+ title: "Next middleware exists but does not appear to cover /api routes",
1192
+ why: "People assume middleware protects APIs. Often it doesn't. Verify matcher patterns.",
1193
+ confidence: "high",
1194
+ evidence: (truthpack?.auth?.nextMiddleware?.[0]?.evidence || []).slice(0, 3),
1195
+ fixHints: ["Add /api/:path* to middleware matcher if your design expects API auth protection."],
1196
+ });
1197
+ }
1198
+ }
1199
+
1200
+ return findings;
1201
+ }
1202
+
1203
+ /* ============================================================================
1204
+ * STRIPE WEBHOOK VIOLATIONS (kept)
1205
+ * ========================================================================== */
1206
+
1207
+ function findStripeWebhookViolations(truthpack) {
1208
+ const findings = [];
1209
+ const billing = truthpack?.billing;
1210
+
1211
+ if (!billing?.hasStripe) return findings;
1212
+
1213
+ const candidates = billing.webhookCandidates || [];
1214
+
1215
+ if (!candidates.length) {
1216
+ findings.push({
1217
+ id: "F_STRIPE_NO_WEBHOOK_HANDLER",
1218
+ severity: "WARN",
1219
+ category: "Billing",
1220
+ title: "Stripe appears used but no webhook handler candidate detected",
1221
+ why: "Stripe billing usually needs webhooks; missing them causes subscription state desync.",
1222
+ confidence: "med",
1223
+ evidence: [],
1224
+ fixHints: ["Add a Stripe webhook handler with signature verification and idempotency."],
1225
+ });
1226
+ return findings;
1227
+ }
1228
+
1229
+ for (const w of candidates) {
1230
+ const verified = w.signals.webhookConstructEvent && w.signals.rawBodySignal && w.signals.readsStripeSignatureHeader;
1231
+ const idempotent = !!w.signals.idempotencySignal;
1232
+
1233
+ if (!verified) {
1234
+ findings.push({
1235
+ id: stableId("F_STRIPE_WEBHOOK_NOT_VERIFIED", w.file),
1236
+ severity: "BLOCK",
1237
+ category: "Billing",
1238
+ title: `Stripe webhook handler not clearly signature-verified: ${w.file}`,
1239
+ why: "Unverified webhooks = spoofable billing state.",
1240
+ confidence: "high",
1241
+ evidence: (w.evidence || []).slice(0, 4),
1242
+ fixHints: [
1243
+ "Use stripe.webhooks.constructEvent(rawBody, sigHeader, STRIPE_WEBHOOK_SECRET).",
1244
+ "Ensure raw body is used (disable bodyParser in pages router; in app router read req.text()/arrayBuffer).",
1245
+ "Reject if signature missing/invalid.",
1246
+ ],
1247
+ });
1248
+ }
1249
+
1250
+ if (!idempotent) {
1251
+ findings.push({
1252
+ id: stableId("F_STRIPE_WEBHOOK_NOT_IDEMPOTENT", w.file),
1253
+ severity: "BLOCK",
1254
+ category: "Billing",
1255
+ title: `Stripe webhook handler not clearly idempotent: ${w.file}`,
1256
+ why: "Stripe retries webhooks; without dedupe you can double-grant access or double-write state.",
1257
+ confidence: "med",
1258
+ evidence: (w.evidence || []).slice(0, 4),
1259
+ fixHints: [
1260
+ "Persist event.id as processed (DB/Redis). If seen, return 200 immediately.",
1261
+ "Wrap state mutation in a transaction keyed by event.id.",
1262
+ ],
1263
+ });
1264
+ }
1265
+ }
1266
+
1267
+ return findings;
1268
+ }
1269
+
1270
+ /* ============================================================================
1271
+ * PAID SURFACE NOT ENFORCED (kept)
1272
+ * ========================================================================== */
1273
+
1274
+ function findPaidSurfaceNotEnforced(truthpack) {
1275
+ const findings = [];
1276
+ const enforcement = truthpack?.enforcement;
1277
+ const checks = enforcement?.checks || [];
1278
+
1279
+ for (const c of checks) {
1280
+ if (c.enforced) continue;
1281
+ findings.push({
1282
+ id: stableId("F_PAID_SURFACE_NOT_ENFORCED", `${c.method} ${c.path}`),
1283
+ severity: "BLOCK",
1284
+ category: "Entitlements",
1285
+ title: `Paid surface appears un-enforced server-side: ${c.method} ${c.path}`,
1286
+ why: "If enforcement is only in the CLI/UI, users can call the endpoint directly.",
1287
+ confidence: "med",
1288
+ evidence: [],
1289
+ fixHints: [
1290
+ "Enforce in the server handler BEFORE doing work.",
1291
+ "Return 402/403 with a structured error code.",
1292
+ "Make the CLI treat that code as an upgrade prompt.",
1293
+ ],
1294
+ });
1295
+ }
1296
+ return findings;
1297
+ }
1298
+
1299
+ /* ============================================================================
1300
+ * OWNER MODE BYPASS (kept; uses deterministic regex testing)
1301
+ * ========================================================================== */
1302
+
1303
+ function findOwnerModeBypass(repoRoot) {
1304
+ const findings = [];
1305
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
1306
+ cwd: repoRoot,
1307
+ absolute: true,
1308
+ ignore: STANDARD_IGNORE_PATTERNS,
1309
+ });
1310
+
1311
+ const patterns = [
1312
+ /OWNER_MODE/i,
1313
+ /GUARDRAIL_OWNER_MODE/i,
1314
+ /VIBECHECK_OWNER_MODE/i,
1315
+ /process\.env\.[A-Z0-9_]*OWNER[A-Z0-9_]*/i,
1316
+ ];
1317
+
1318
+ for (const fileAbs of files) {
1319
+ const code = readFileCached(fileAbs);
1320
+ const hit = patterns.some((rx) => rxTest(rx, code));
1321
+ if (!hit) continue;
1322
+
1323
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
1324
+
1325
+ findings.push({
1326
+ id: stableId("F_OWNER_MODE_BYPASS", fileRel),
1327
+ severity: "BLOCK",
1328
+ category: "Security",
1329
+ title: `Owner mode / env bypass signal detected: ${fileRel}`,
1330
+ why: "This is a production backdoor unless cryptographically gated. It cannot ship.",
1331
+ confidence: "high",
1332
+ evidence: [],
1333
+ fixHints: [
1334
+ "Delete owner mode bypass. If you need dev override, require a signed admin token + non-prod environment.",
1335
+ "Add a test that asserts no OWNER_MODE env var grants entitlements.",
1336
+ ],
1337
+ });
1338
+ }
1339
+
1340
+ return findings;
1341
+ }
1342
+
1343
+ /* ============================================================================
1344
+ * MOCK DATA DETECTOR (fixed /g+.test() bug + better line discovery)
1345
+ * ========================================================================== */
1346
+
1347
+ function findMockData(repoRoot) {
1348
+ const engines = require("./engines/vibecheck-engines");
1349
+ const findings = [];
1350
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
1351
+ cwd: repoRoot,
1352
+ absolute: true,
1353
+ ignore: STANDARD_IGNORE_PATTERNS,
1354
+ });
1355
+
1356
+ for (const fileAbs of files) {
1357
+ try {
1358
+ const code = readFileCached(fileAbs);
1359
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
1360
+
1361
+ // Use unified engines
1362
+ const engineFindings = engines.analyzeMockData(code, fileRel);
1363
+
1364
+ // Convert engine findings to analyzer format
1365
+ for (const finding of engineFindings) {
1366
+ findings.push({
1367
+ id: stableId("F_MOCK_DATA", `${fileRel}:${finding.type}:${finding.line}`),
1368
+ severity: finding.severity,
1369
+ category: finding.category,
1370
+ title: finding.title,
1371
+ message: finding.message,
1372
+ file: finding.file,
1373
+ line: finding.line,
1374
+ why: "Mock/fake data in production causes embarrassing bugs and makes your app look unfinished.",
1375
+ confidence: finding.confidence,
1376
+ evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
1377
+ fixHints: [
1378
+ "Replace mock data with real API calls or database queries.",
1379
+ "If intentional sample data, move to a clearly marked demo mode.",
1380
+ ],
1381
+ });
1382
+ }
1383
+ } catch (err) {
1384
+ // Skip files that can't be analyzed
1385
+ continue;
1386
+ }
1387
+ }
1388
+
1389
+ return findings;
1390
+ }
1391
+
1392
+ /* ============================================================================
1393
+ * TODO/FIXME DETECTOR (fixed /g+.test() bug)
1394
+ * ========================================================================== */
1395
+
1396
+ function findTodoFixme(repoRoot) {
1397
+ // TODO/FIXME engine not in unified engines yet - keep using existing
1398
+ const { analyzeTodoFixme } = require("./engines/todo-fixme-engine");
1399
+ const engines = require("./engines/vibecheck-engines");
1400
+ const findings = [];
1401
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
1402
+ cwd: repoRoot,
1403
+ absolute: true,
1404
+ ignore: STANDARD_IGNORE_PATTERNS,
1405
+ });
1406
+
1407
+ for (const fileAbs of files) {
1408
+ try {
1409
+ const code = readFileCached(fileAbs);
1410
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
1411
+
1412
+ // Use AST-based engine (not in unified engines yet)
1413
+ const engineFindings = analyzeTodoFixme(code, fileRel);
1414
+
1415
+ // Convert engine findings to analyzer format
1416
+ for (const finding of engineFindings) {
1417
+ if (finding.type === "summary") {
1418
+ findings.push({
1419
+ id: "F_TODO_SUMMARY",
1420
+ severity: finding.severity,
1421
+ category: finding.category,
1422
+ title: finding.title,
1423
+ why: "Large numbers of TODO comments indicate significant unfinished work.",
1424
+ confidence: finding.confidence,
1425
+ evidence: [],
1426
+ fixHints: [
1427
+ "Review and address high-priority TODOs before shipping.",
1428
+ `Run: vibecheck scan --json | jq '.findings[] | select(.category == "TODO")'`,
1429
+ ],
1430
+ });
1431
+ } else {
1432
+ findings.push({
1433
+ id: stableId("F_TODO", `${fileRel}:${finding.line}:${finding.type}`),
1434
+ severity: finding.severity,
1435
+ category: finding.category,
1436
+ title: finding.title,
1437
+ message: finding.message,
1438
+ file: finding.file,
1439
+ line: finding.line,
1440
+ why: finding.severity === "BLOCK"
1441
+ ? "This comment indicates a known critical issue that must be addressed before shipping."
1442
+ : "Unfinished work markers suggest the code isn't production-ready.",
1443
+ confidence: finding.confidence,
1444
+ evidence: [{ file: fileRel, lines: `${finding.line}`, reason: finding.title }],
1445
+ fixHints: [
1446
+ "Complete the TODO or remove it if already done.",
1447
+ "If deferring, create a tracked issue and reference it in the comment.",
1448
+ ],
1449
+ });
1450
+ }
1451
+ }
1452
+ } catch (err) {
1453
+ // Skip files that can't be analyzed
1454
+ continue;
1455
+ }
1456
+ }
1457
+
1458
+ return findings;
1459
+ }
1460
+
1461
+ /* ============================================================================
1462
+ * CONSOLE.LOG DETECTOR (kept)
1463
+ * ========================================================================== */
1464
+
1465
+ function findConsoleLogs(repoRoot) {
1466
+ const engines = require("./engines/vibecheck-engines");
1467
+ const findings = [];
1468
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
1469
+ cwd: repoRoot,
1470
+ absolute: true,
1471
+ ignore: STANDARD_IGNORE_PATTERNS,
1472
+ });
1473
+
1474
+ for (const fileAbs of files) {
1475
+ try {
1476
+ const code = readFileCached(fileAbs);
1477
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
1478
+
1479
+ // Use unified engines
1480
+ const engineFindings = engines.analyzeConsoleLogs(code, fileRel);
1481
+
1482
+ // Convert engine findings to analyzer format
1483
+ for (const finding of engineFindings) {
1484
+ findings.push({
1485
+ id: stableId("F_CONSOLE_LOG", `${fileRel}:${finding.line}:${finding.type}`),
1486
+ severity: finding.severity,
1487
+ category: finding.category,
1488
+ title: finding.title,
1489
+ message: finding.message,
1490
+ file: finding.file,
1491
+ line: finding.line,
1492
+ why: "Console statements leak debugging info and clutter logs/console.",
1493
+ confidence: finding.confidence,
1494
+ evidence: [{ file: fileRel, lines: `${finding.line}`, reason: finding.codeSnippet || finding.title }],
1495
+ fixHints: ["Remove console.log or replace with a proper logger.", "Use a logger that can be silenced in production."],
1496
+ });
1497
+ }
1498
+ } catch (err) {
1499
+ // Skip files that can't be analyzed
1500
+ continue;
1501
+ }
1502
+ }
1503
+
1504
+ return findings;
1505
+ }
1506
+
1507
+ /* ============================================================================
1508
+ * HARDCODED SECRETS DETECTOR (kept)
1509
+ * ========================================================================== */
1510
+
1511
+ function findHardcodedSecrets(repoRoot) {
1512
+ const engines = require("./engines/vibecheck-engines");
1513
+ const findings = [];
1514
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx,json}"], {
1515
+ cwd: repoRoot,
1516
+ absolute: true,
1517
+ ignore: [...STANDARD_IGNORE_PATTERNS, "**/package*.json"],
1518
+ });
1519
+
1520
+ for (const fileAbs of files) {
1521
+ try {
1522
+ const code = readFileCached(fileAbs);
1523
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
1524
+
1525
+ // Use unified engines
1526
+ const engineFindings = engines.analyzeHardcodedSecrets(code, fileRel);
1527
+
1528
+ // Convert engine findings to analyzer format
1529
+ for (const finding of engineFindings) {
1530
+ findings.push({
1531
+ id: stableId("F_SECRET", `${fileRel}:${finding.type}:${finding.line}`),
1532
+ severity: finding.severity,
1533
+ category: finding.category,
1534
+ title: finding.title,
1535
+ message: finding.message,
1536
+ file: finding.file,
1537
+ line: finding.line,
1538
+ why: finding.severity === "BLOCK"
1539
+ ? "Hardcoded secrets get committed and leaked. This is critical."
1540
+ : "This string looks mathematically random, which usually indicates a hardcoded secret key.",
1541
+ confidence: finding.confidence,
1542
+ evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
1543
+ fixHints: finding.severity === "BLOCK"
1544
+ ? [
1545
+ "Move the secret to environment variables.",
1546
+ "Rotate the compromised secret immediately.",
1547
+ "Add the file to .gitignore if it shouldn't be committed.",
1548
+ ]
1549
+ : [
1550
+ "Move the secret to environment variables.",
1551
+ "If this is not a secret, consider using a more descriptive variable name.",
1552
+ ],
1553
+ });
1554
+ }
1555
+ } catch (err) {
1556
+ // Skip files that can't be analyzed
1557
+ continue;
1558
+ }
1559
+ }
1560
+
1561
+ return findings;
1562
+ }
1563
+
1564
+ /* ============================================================================
1565
+ * DEAD CODE / UNUSED EXPORTS DETECTOR (AST-based engine)
1566
+ * ========================================================================== */
1567
+
1568
+ function findDeadCode(repoRoot) {
1569
+ const engines = require("./engines/vibecheck-engines");
1570
+ const findings = [];
1571
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
1572
+ cwd: repoRoot,
1573
+ absolute: true,
1574
+ ignore: STANDARD_IGNORE_PATTERNS,
1575
+ });
1576
+
1577
+ for (const fileAbs of files) {
1578
+ try {
1579
+ const code = readFileCached(fileAbs);
1580
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
1581
+
1582
+ // Use unified engines
1583
+ const engineFindings = engines.analyzeDeadCode(code, fileRel);
1584
+
1585
+ // Convert engine findings to analyzer format
1586
+ for (const finding of engineFindings) {
1587
+ findings.push({
1588
+ id: stableId("F_DEAD_CODE", `${fileRel}:${finding.type}:${finding.line}`),
1589
+ severity: finding.severity,
1590
+ category: finding.category,
1591
+ title: finding.title,
1592
+ message: finding.message,
1593
+ file: finding.file,
1594
+ line: finding.line,
1595
+ why: "Dead code adds confusion and maintenance burden and usually indicates incomplete refactoring.",
1596
+ confidence: finding.confidence,
1597
+ evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
1598
+ fixHints: ["Remove the dead code entirely.", "If needed for reference, use git history instead of commenting."],
1599
+ });
1600
+ }
1601
+ } catch (err) {
1602
+ // Skip files that can't be analyzed
1603
+ continue;
1604
+ }
1605
+ }
1606
+
1607
+ return findings;
1608
+ }
1609
+
1610
+ /* ============================================================================
1611
+ * DEPRECATED API USAGE DETECTOR (kept; deterministic)
1612
+ * ========================================================================== */
1613
+
1614
+ function findDeprecatedApis(repoRoot) {
1615
+ const engines = require("./engines/vibecheck-engines");
1616
+ const findings = [];
1617
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
1618
+ cwd: repoRoot,
1619
+ absolute: true,
1620
+ ignore: STANDARD_IGNORE_PATTERNS,
1621
+ });
1622
+
1623
+ for (const fileAbs of files) {
1624
+ try {
1625
+ const code = readFileCached(fileAbs);
1626
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
1627
+
1628
+ // Use unified engines
1629
+ const engineFindings = engines.analyzeDeprecatedApi(code, fileRel);
1630
+
1631
+ // Convert engine findings to analyzer format
1632
+ for (const finding of engineFindings) {
1633
+ findings.push({
1634
+ id: stableId("F_DEPRECATED", `${fileRel}:${finding.type}:${finding.line}`),
1635
+ severity: finding.severity,
1636
+ category: finding.category,
1637
+ title: finding.title,
1638
+ message: finding.message,
1639
+ file: finding.file,
1640
+ line: finding.line,
1641
+ why: "Deprecated APIs may break in future versions and sometimes carry security issues.",
1642
+ confidence: finding.confidence,
1643
+ evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
1644
+ fixHints: finding.replacement ? [`Use ${finding.replacement} instead.`, "Check migration guides for the specific deprecation."] : ["Update to the modern API equivalent.", "Check migration guides for the specific deprecation."],
1645
+ });
1646
+ }
1647
+ } catch (err) {
1648
+ // Skip files that can't be analyzed
1649
+ continue;
1650
+ }
1651
+ }
1652
+
1653
+ return findings;
1654
+ }
1655
+
1656
+ /* ============================================================================
1657
+ * EMPTY CATCH BLOCKS DETECTOR (AST-based engine)
1658
+ * ========================================================================== */
1659
+
1660
+ function findEmptyCatch(repoRoot) {
1661
+ const engines = require("./engines/vibecheck-engines");
1662
+ const findings = [];
1663
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
1664
+ cwd: repoRoot,
1665
+ absolute: true,
1666
+ ignore: STANDARD_IGNORE_PATTERNS,
1667
+ });
1668
+
1669
+ for (const fileAbs of files) {
1670
+ try {
1671
+ const code = readFileCached(fileAbs);
1672
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
1673
+
1674
+ // Use unified engines
1675
+ const engineFindings = engines.analyzeEmptyCatch(code, fileRel);
1676
+
1677
+ // Convert engine findings to analyzer format
1678
+ for (const finding of engineFindings) {
1679
+ findings.push({
1680
+ id: stableId("F_EMPTY_CATCH", `${fileRel}:${finding.type}:${finding.line}`),
1681
+ severity: finding.severity,
1682
+ category: finding.category,
1683
+ title: finding.title,
1684
+ message: finding.message,
1685
+ file: finding.file,
1686
+ line: finding.line,
1687
+ why: "Empty catch blocks swallow errors and make debugging impossible.",
1688
+ confidence: finding.confidence,
1689
+ evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
1690
+ fixHints: ["Log the error or handle it appropriately.", "If intentionally ignoring, add a comment explaining why."],
1691
+ });
1692
+ }
1693
+ } catch (err) {
1694
+ // Skip files that can't be analyzed
1695
+ continue;
1696
+ }
1697
+ }
1698
+
1699
+ return findings;
1700
+ }
1701
+
1702
+ /* ============================================================================
1703
+ * UNSAFE REGEX DETECTOR (fixed /g+.test() bug)
1704
+ * ========================================================================== */
1705
+
1706
+ function findUnsafeRegex(repoRoot) {
1707
+ const engines = require("./engines/vibecheck-engines");
1708
+ const findings = [];
1709
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
1710
+ cwd: repoRoot,
1711
+ absolute: true,
1712
+ ignore: STANDARD_IGNORE_PATTERNS,
1713
+ });
1714
+
1715
+ for (const fileAbs of files) {
1716
+ try {
1717
+ const code = readFileCached(fileAbs);
1718
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
1719
+
1720
+ // Use unified engines
1721
+ const engineFindings = engines.analyzeUnsafeRegex(code, fileRel);
1722
+
1723
+ // Convert engine findings to analyzer format
1724
+ for (const finding of engineFindings) {
1725
+ findings.push({
1726
+ id: stableId("F_UNSAFE_REGEX", `${fileRel}:${finding.type}:${finding.line}`),
1727
+ severity: finding.severity,
1728
+ category: finding.category,
1729
+ title: finding.title,
1730
+ message: finding.message,
1731
+ file: finding.file,
1732
+ line: finding.line,
1733
+ why: "Unsafe regex patterns can cause denial of service via catastrophic backtracking.",
1734
+ confidence: finding.confidence,
1735
+ evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
1736
+ fixHints: ["Validate input length before applying regex.", "Consider safer parsing or a regex linter.", "Avoid nested quantifiers."],
1737
+ });
1738
+ }
1739
+ } catch (err) {
1740
+ // Skip files that can't be analyzed
1741
+ continue;
1742
+ }
1743
+ }
1744
+
1745
+ return findings;
1746
+ }
1747
+
1748
+ /* ============================================================================
1749
+ * NEW SECURITY & PERFORMANCE ANALYZERS
1750
+ * ========================================================================== */
1751
+
1752
+ function findSecurityVulnerabilities(repoRoot) {
1753
+ // Note: Security vulnerabilities engine not in unified engines yet
1754
+ // Keep using the existing engine for now
1755
+ const { analyzeSecurityVulnerabilities } = require("./engines/security-vulnerabilities-engine");
1756
+ const findings = [];
1757
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
1758
+ cwd: repoRoot,
1759
+ absolute: true,
1760
+ ignore: STANDARD_IGNORE_PATTERNS,
1761
+ });
1762
+
1763
+ for (const fileAbs of files) {
1764
+ try {
1765
+ const code = readFileCached(fileAbs);
1766
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
1767
+
1768
+ const engineFindings = analyzeSecurityVulnerabilities(code, fileRel);
1769
+
1770
+ for (const finding of engineFindings) {
1771
+ findings.push({
1772
+ id: stableId("F_SECURITY", `${fileRel}:${finding.type}:${finding.line}`),
1773
+ severity: finding.severity,
1774
+ category: finding.category,
1775
+ title: finding.title,
1776
+ message: finding.message,
1777
+ file: finding.file,
1778
+ line: finding.line,
1779
+ why: "Security vulnerabilities can lead to data breaches, unauthorized access, or system compromise.",
1780
+ confidence: finding.confidence,
1781
+ evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
1782
+ fixHints: [
1783
+ "Review security best practices for the detected vulnerability type.",
1784
+ "Use parameterized queries for SQL operations.",
1785
+ "Sanitize and validate all user inputs.",
1786
+ "Use Content Security Policy (CSP) headers for XSS protection.",
1787
+ ],
1788
+ });
1789
+ }
1790
+ } catch (err) {
1791
+ continue;
1792
+ }
1793
+ }
1794
+
1795
+ return findings;
1796
+ }
1797
+
1798
+ function findPerformanceIssues(repoRoot) {
1799
+ const engines = require("./engines/vibecheck-engines");
1800
+ const findings = [];
1801
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
1802
+ cwd: repoRoot,
1803
+ absolute: true,
1804
+ ignore: STANDARD_IGNORE_PATTERNS,
1805
+ });
1806
+
1807
+ for (const fileAbs of files) {
1808
+ try {
1809
+ const code = readFileCached(fileAbs);
1810
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
1811
+
1812
+ // Use unified engines
1813
+ const engineFindings = engines.analyzePerformanceIssues(code, fileRel);
1814
+
1815
+ for (const finding of engineFindings) {
1816
+ findings.push({
1817
+ id: stableId("F_PERF", `${fileRel}:${finding.type}:${finding.line}`),
1818
+ severity: finding.severity,
1819
+ category: finding.category,
1820
+ title: finding.title,
1821
+ message: finding.message,
1822
+ file: finding.file,
1823
+ line: finding.line,
1824
+ why: "Performance issues can degrade user experience and increase server costs.",
1825
+ confidence: finding.confidence,
1826
+ evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
1827
+ fixHints: [
1828
+ "Optimize algorithms and data structures.",
1829
+ "Use pagination for large datasets.",
1830
+ "Remove unnecessary re-renders.",
1831
+ "Use async/await for I/O operations.",
1832
+ ],
1833
+ });
1834
+ }
1835
+ } catch (err) {
1836
+ continue;
1837
+ }
1838
+ }
1839
+
1840
+ return findings;
1841
+ }
1842
+
1843
+ function findCodeQualityIssues(repoRoot) {
1844
+ const engines = require("./engines/vibecheck-engines");
1845
+ const findings = [];
1846
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
1847
+ cwd: repoRoot,
1848
+ absolute: true,
1849
+ ignore: STANDARD_IGNORE_PATTERNS,
1850
+ });
1851
+
1852
+ for (const fileAbs of files) {
1853
+ try {
1854
+ const code = readFileCached(fileAbs);
1855
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
1856
+
1857
+ // Use unified engines
1858
+ const engineFindings = engines.analyzeCodeQuality(code, fileRel);
1859
+
1860
+ for (const finding of engineFindings) {
1861
+ findings.push({
1862
+ id: stableId("F_QUALITY", `${fileRel}:${finding.type}:${finding.line}`),
1863
+ severity: finding.severity,
1864
+ category: finding.category,
1865
+ title: finding.title,
1866
+ message: finding.message,
1867
+ file: finding.file,
1868
+ line: finding.line,
1869
+ why: "Code quality issues make code harder to maintain, test, and extend.",
1870
+ confidence: finding.confidence,
1871
+ evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
1872
+ fixHints: [
1873
+ "Break down complex functions into smaller, focused functions.",
1874
+ "Extract magic numbers into named constants.",
1875
+ "Reduce nesting depth with early returns.",
1876
+ "Consider design patterns for common problems.",
1877
+ ],
1878
+ });
1879
+ }
1880
+ } catch (err) {
1881
+ continue;
1882
+ }
1883
+ }
1884
+
1885
+ return findings;
1886
+ }
1887
+
1888
+ function findCrossFileIssues(repoRoot) {
1889
+ const { analyzeCrossFile } = require("./engines/cross-file-analysis-engine");
1890
+ const fg = require("fast-glob");
1891
+
1892
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
1893
+ cwd: repoRoot,
1894
+ absolute: true,
1895
+ ignore: STANDARD_IGNORE_PATTERNS,
1896
+ });
1897
+
1898
+ const engineFindings = analyzeCrossFile(files, repoRoot);
1899
+ const findings = [];
1900
+
1901
+ for (const finding of engineFindings) {
1902
+ findings.push({
1903
+ id: stableId("F_CROSSFILE", `${finding.file}:${finding.type}`),
1904
+ severity: finding.severity,
1905
+ category: finding.category,
1906
+ title: finding.title,
1907
+ message: finding.message,
1908
+ file: finding.file,
1909
+ line: finding.line,
1910
+ why: "Cross-file issues indicate architectural problems that can cause maintenance difficulties.",
1911
+ confidence: finding.confidence,
1912
+ evidence: [{ file: finding.file, reason: finding.title }],
1913
+ fixHints: [
1914
+ "Remove unused exports or mark them as internal.",
1915
+ "Refactor to break circular dependencies.",
1916
+ "Standardize import paths across the codebase.",
1917
+ ],
1918
+ });
1919
+ }
1920
+
1921
+ return findings;
1922
+ }
1923
+
1924
+ function findTypeSafetyIssues(repoRoot) {
1925
+ const engines = require("./engines/vibecheck-engines");
1926
+ const findings = [];
1927
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
1928
+ cwd: repoRoot,
1929
+ absolute: true,
1930
+ ignore: STANDARD_IGNORE_PATTERNS,
1931
+ });
1932
+
1933
+ for (const fileAbs of files) {
1934
+ try {
1935
+ const code = readFileCached(fileAbs);
1936
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
1937
+
1938
+ // Use unified engines
1939
+ const engineFindings = engines.analyzeTypeAware(code, fileRel);
1940
+
1941
+ for (const finding of engineFindings) {
1942
+ findings.push({
1943
+ id: stableId("F_TYPE", `${fileRel}:${finding.type}:${finding.line}`),
1944
+ severity: finding.severity,
1945
+ category: finding.category,
1946
+ title: finding.title,
1947
+ message: finding.message,
1948
+ file: finding.file,
1949
+ line: finding.line,
1950
+ why: "Type safety issues can lead to runtime errors and make code harder to maintain.",
1951
+ confidence: finding.confidence,
1952
+ evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
1953
+ fixHints: [
1954
+ "Use proper TypeScript types instead of 'any'.",
1955
+ "Fix underlying type errors instead of suppressing them.",
1956
+ "Add explicit return type annotations.",
1957
+ ],
1958
+ });
1959
+ }
1960
+ } catch (err) {
1961
+ continue;
1962
+ }
1963
+ }
1964
+
1965
+ return findings;
1966
+ }
1967
+
1968
+ function findAccessibilityIssues(repoRoot) {
1969
+ const { analyzeAccessibility } = require("./engines/accessibility-engine");
1970
+ const findings = [];
1971
+ const files = fg.sync(["**/*.{tsx,jsx}"], {
1972
+ cwd: repoRoot,
1973
+ absolute: true,
1974
+ ignore: STANDARD_IGNORE_PATTERNS,
1975
+ });
1976
+
1977
+ for (const fileAbs of files) {
1978
+ try {
1979
+ const code = readFileCached(fileAbs);
1980
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
1981
+
1982
+ const engineFindings = analyzeAccessibility(code, fileRel);
1983
+
1984
+ for (const finding of engineFindings) {
1985
+ findings.push({
1986
+ id: stableId("F_A11Y", `${fileRel}:${finding.type}:${finding.line}`),
1987
+ severity: finding.severity,
1988
+ category: finding.category,
1989
+ title: finding.title,
1990
+ message: finding.message,
1991
+ file: finding.file,
1992
+ line: finding.line,
1993
+ why: "Accessibility issues prevent users with disabilities from using your application.",
1994
+ confidence: finding.confidence,
1995
+ evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
1996
+ fixHints: [
1997
+ "Add alt text to all images.",
1998
+ "Ensure all interactive elements have accessible labels.",
1999
+ "Add keyboard handlers for custom interactive elements.",
2000
+ "Test with screen readers.",
2001
+ ],
2002
+ });
2003
+ }
2004
+ } catch (err) {
2005
+ continue;
2006
+ }
2007
+ }
2008
+
2009
+ return findings;
2010
+ }
2011
+
2012
+ function findAPIConsistencyIssues(repoRoot) {
2013
+ const { analyzeAPIConsistency } = require("./engines/api-consistency-engine");
2014
+ const findings = [];
2015
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
2016
+ cwd: repoRoot,
2017
+ absolute: true,
2018
+ ignore: STANDARD_IGNORE_PATTERNS,
2019
+ });
2020
+
2021
+ for (const fileAbs of files) {
2022
+ try {
2023
+ const code = readFileCached(fileAbs);
2024
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
2025
+
2026
+ const engineFindings = analyzeAPIConsistency(code, fileRel);
2027
+
2028
+ for (const finding of engineFindings) {
2029
+ findings.push({
2030
+ id: stableId("F_API", `${fileRel}:${finding.type}:${finding.line}`),
2031
+ severity: finding.severity,
2032
+ category: finding.category,
2033
+ title: finding.title,
2034
+ message: finding.message,
2035
+ file: finding.file,
2036
+ line: finding.line,
2037
+ why: "API consistency issues make APIs harder to use and maintain.",
2038
+ confidence: finding.confidence,
2039
+ evidence: [{ file: fileRel, reason: finding.title, line: finding.line }],
2040
+ fixHints: [
2041
+ "Standardize response formats across all API routes.",
2042
+ "Add consistent error handling.",
2043
+ "Always return explicit HTTP status codes.",
2044
+ ],
2045
+ });
2046
+ }
2047
+ } catch (err) {
2048
+ continue;
2049
+ }
2050
+ }
2051
+
2052
+ return findings;
2053
+ }
2054
+
2055
+ /* ============================================================================
2056
+ * OPTIMISTIC NO ROLLBACK DETECTOR
2057
+ * Finds optimistic UI updates that don't rollback on failure
2058
+ * ========================================================================== */
2059
+
2060
+ function findOptimisticNoRollback(repoRoot) {
2061
+ const findings = [];
2062
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
2063
+ cwd: repoRoot,
2064
+ absolute: true,
2065
+ ignore: STANDARD_IGNORE_PATTERNS,
2066
+ });
2067
+
2068
+ for (const fileAbs of files) {
2069
+ const code = readFileCached(fileAbs);
2070
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
2071
+
2072
+ // Fast path: skip files without optimistic patterns
2073
+ const hasOptimisticUpdate = /\b(setOptimistic|optimisticUpdate|setState.*\bfetch|useMutation.*onMutate)\b/i.test(code);
2074
+ const hasStateUpdate = /\b(setState|set[A-Z]\w*|dispatch|update[A-Z])\b/.test(code);
2075
+ const hasNetworkCall = /\b(fetch|axios|useMutation|mutate)\b/.test(code);
2076
+
2077
+ if (!hasStateUpdate || !hasNetworkCall) continue;
2078
+
2079
+ let ast;
2080
+ try {
2081
+ ast = parseFile(code, fileAbs);
2082
+ } catch {
2083
+ continue;
2084
+ }
2085
+
2086
+ try {
2087
+ traverse(ast, {
2088
+ CallExpression(pathNode) {
2089
+ const n = pathNode.node;
2090
+
2091
+ // Look for setState/dispatch calls followed by fetch without catch/finally rollback
2092
+ if (!t.isMemberExpression(n.callee)) return;
2093
+
2094
+ const methodName = n.callee.property?.name || "";
2095
+ const isStateUpdate = /^(setState|set[A-Z]|dispatch|update)/.test(methodName);
2096
+
2097
+ if (!isStateUpdate) return;
2098
+
2099
+ // Check if parent function has a try-catch with rollback
2100
+ let parentFn = pathNode.findParent(p => p.isFunction());
2101
+ if (!parentFn) return;
2102
+
2103
+ let hasRollback = false;
2104
+ let hasNetworkInSameBlock = false;
2105
+
2106
+ parentFn.traverse({
2107
+ CallExpression(inner) {
2108
+ const callee = inner.node.callee;
2109
+ if (isFetchCall(inner.node) || isAxiosCall(inner.node)) {
2110
+ hasNetworkInSameBlock = true;
2111
+ }
2112
+ },
2113
+ CatchClause(catchPath) {
2114
+ // Check if catch has a state update (rollback)
2115
+ catchPath.traverse({
2116
+ CallExpression(rollbackCall) {
2117
+ if (t.isMemberExpression(rollbackCall.node.callee)) {
2118
+ const name = rollbackCall.node.callee.property?.name || "";
2119
+ if (/^(setState|set[A-Z]|dispatch|update)/.test(name)) {
2120
+ hasRollback = true;
2121
+ }
2122
+ }
2123
+ }
2124
+ });
2125
+ }
2126
+ });
2127
+
2128
+ // If there's a state update, network call, but no rollback in catch
2129
+ if (hasNetworkInSameBlock && !hasRollback) {
2130
+ const loc = n.loc;
2131
+ findings.push({
2132
+ id: stableId("F_OPTIMISTIC_NO_ROLLBACK", `${fileRel}:${loc?.start?.line || 0}`),
2133
+ severity: "WARN",
2134
+ category: "OptimisticNoRollback",
2135
+ title: "Optimistic update without rollback on failure",
2136
+ why: "State is updated before network call completes, but there's no rollback if the request fails. Users see stale/incorrect data.",
2137
+ confidence: "med",
2138
+ evidence: [evidenceFromLoc(fileAbs, repoRoot, loc, "Optimistic state update")].filter(Boolean),
2139
+ fixHints: [
2140
+ "Add a catch block that reverts the state to previous value on failure.",
2141
+ "Use react-query's onMutate/onError for automatic rollback.",
2142
+ "Store previous state before update and restore it on error.",
2143
+ ],
2144
+ });
2145
+ }
2146
+ }
2147
+ });
2148
+ } catch {
2149
+ continue;
2150
+ }
2151
+ }
2152
+
2153
+ return findings;
2154
+ }
2155
+
2156
+ /* ============================================================================
2157
+ * SILENT CATCH DETECTOR (Enhanced)
2158
+ * Finds catch blocks that swallow errors without logging or re-throwing
2159
+ * ========================================================================== */
2160
+
2161
+ function findSilentCatch(repoRoot) {
2162
+ const findings = [];
2163
+ const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
2164
+ cwd: repoRoot,
2165
+ absolute: true,
2166
+ ignore: STANDARD_IGNORE_PATTERNS,
2167
+ });
2168
+
2169
+ for (const fileAbs of files) {
2170
+ const code = readFileCached(fileAbs);
2171
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
2172
+
2173
+ // Fast path: skip files without try-catch
2174
+ if (!/\bcatch\s*\(/.test(code)) continue;
2175
+
2176
+ let ast;
2177
+ try {
2178
+ ast = parseFile(code, fileAbs);
2179
+ } catch {
2180
+ continue;
2181
+ }
2182
+
2183
+ try {
2184
+ traverse(ast, {
2185
+ CatchClause(pathNode) {
2186
+ const catchBody = pathNode.node.body;
2187
+ const catchParam = pathNode.node.param?.name || "error";
2188
+
2189
+ // Check if catch body is empty or only has comments
2190
+ if (!catchBody.body || catchBody.body.length === 0) {
2191
+ // Empty catch - already covered by findEmptyCatch
2192
+ return;
2193
+ }
2194
+
2195
+ // Check if catch actually handles the error
2196
+ let logsError = false;
2197
+ let rethrowsError = false;
2198
+ let showsUserError = false;
2199
+ let hasConditionalReturn = false;
2200
+
2201
+ pathNode.traverse({
2202
+ CallExpression(inner) {
2203
+ const callee = inner.node.callee;
2204
+ const args = inner.node.arguments;
2205
+
2206
+ // Check for console.error, console.log, logger.error, etc.
2207
+ if (t.isMemberExpression(callee)) {
2208
+ const obj = callee.object?.name || "";
2209
+ const prop = callee.property?.name || "";
2210
+ if ((obj === "console" || /logger|log/i.test(obj)) && /error|warn|log/.test(prop)) {
2211
+ // Check if it logs the error variable
2212
+ const argsStr = args.map(a => code.slice(a.start, a.end)).join(",");
2213
+ if (argsStr.includes(catchParam) || argsStr.includes("error") || argsStr.includes("err")) {
2214
+ logsError = true;
2215
+ }
2216
+ }
2217
+ // Check for toast.error, notification.error, etc.
2218
+ if (/toast|notification|alert|message/i.test(obj) && /error|warn|fail/.test(prop)) {
2219
+ showsUserError = true;
2220
+ }
2221
+ }
2222
+ },
2223
+ ThrowStatement() {
2224
+ rethrowsError = true;
2225
+ },
2226
+ ReturnStatement(ret) {
2227
+ // Returning early might be intentional error handling
2228
+ const ifParent = ret.findParent(p => p.isIfStatement());
2229
+ if (ifParent) hasConditionalReturn = true;
2230
+ }
2231
+ });
2232
+
2233
+ // Silent catch: doesn't log, doesn't rethrow, doesn't show user error
2234
+ if (!logsError && !rethrowsError && !showsUserError && !hasConditionalReturn) {
2235
+ const loc = pathNode.node.loc;
2236
+ findings.push({
2237
+ id: stableId("F_SILENT_CATCH", `${fileRel}:${loc?.start?.line || 0}`),
2238
+ severity: "WARN",
2239
+ category: "SilentCatch",
2240
+ title: "Catch block swallows error silently",
2241
+ why: "Errors are caught but not logged, reported, or shown to users. This makes debugging nearly impossible and hides failures.",
2242
+ confidence: "med",
2243
+ evidence: [evidenceFromLoc(fileAbs, repoRoot, loc, "Silent catch block")].filter(Boolean),
2244
+ fixHints: [
2245
+ `Add console.error(${catchParam}) or a logger call.`,
2246
+ "Show user-friendly error message (toast, alert, etc.).",
2247
+ "Re-throw the error if it should propagate.",
2248
+ "If intentionally ignoring, add a comment explaining why.",
2249
+ ],
2250
+ });
2251
+ }
2252
+ }
2253
+ });
2254
+ } catch {
2255
+ continue;
2256
+ }
2257
+ }
2258
+
2259
+ return findings;
2260
+ }
2261
+
2262
+ /* ============================================================================
2263
+ * METHOD MISMATCH DETECTOR
2264
+ * Finds client-side GET requests to POST-only endpoints and vice versa
2265
+ * ========================================================================== */
2266
+
2267
+ function findMethodMismatch(truthpack) {
2268
+ if (!truthpack?.routes) return [];
2269
+
2270
+ const findings = [];
2271
+ const serverRoutes = truthpack.routes.server || [];
2272
+ const clientRefs = truthpack.routes.clientRefs || [];
2273
+
2274
+ // Build a map of route -> allowed methods
2275
+ const routeMethodMap = new Map();
2276
+ for (const route of serverRoutes) {
2277
+ const key = normalizeRoutePath(route.path);
2278
+ if (!routeMethodMap.has(key)) {
2279
+ routeMethodMap.set(key, new Set());
2280
+ }
2281
+ if (route.method) {
2282
+ routeMethodMap.get(key).add(route.method.toUpperCase());
2283
+ }
2284
+ }
2285
+
2286
+ // Check client refs for method mismatches
2287
+ for (const ref of clientRefs) {
2288
+ if (!ref.method || !ref.path) continue;
2289
+
2290
+ const clientMethod = ref.method.toUpperCase();
2291
+ const normalizedPath = normalizeRoutePath(ref.path);
2292
+
2293
+ // Find matching server route
2294
+ const allowedMethods = routeMethodMap.get(normalizedPath);
2295
+
2296
+ if (allowedMethods && allowedMethods.size > 0 && !allowedMethods.has(clientMethod)) {
2297
+ // Method mismatch found
2298
+ const allowed = Array.from(allowedMethods).join(", ");
2299
+ findings.push({
2300
+ id: stableId("F_METHOD_MISMATCH", `${ref.file || "unknown"}:${ref.line || 0}:${ref.path}`),
2301
+ severity: "BLOCK",
2302
+ category: "MethodMismatch",
2303
+ title: `${clientMethod} request to ${allowed}-only endpoint: ${ref.path}`,
2304
+ why: `Client makes ${clientMethod} request but server only accepts ${allowed}. This will fail with 405 Method Not Allowed.`,
2305
+ confidence: "high",
2306
+ evidence: ref.file ? [{
2307
+ file: ref.file,
2308
+ line: ref.line,
2309
+ reason: `Client ${clientMethod} to ${allowed}-only route`,
2310
+ }] : [],
2311
+ fixHints: [
2312
+ `Change client request method from ${clientMethod} to ${allowed}.`,
2313
+ `Add ${clientMethod} handler to the server route.`,
2314
+ "Verify the API contract matches documentation.",
2315
+ ],
2316
+ });
2317
+ }
2318
+ }
2319
+
2320
+ return findings;
2321
+ }
2322
+
2323
+ // Helper to normalize route paths for comparison
2324
+ function normalizeRoutePath(routePath) {
2325
+ if (!routePath) return "";
2326
+ return routePath
2327
+ .replace(/\[([^\]]+)\]/g, ":$1") // [id] -> :id
2328
+ .replace(/\/+/g, "/") // multiple slashes -> single
2329
+ .replace(/\/$/, "") // remove trailing slash
2330
+ .toLowerCase();
2331
+ }
2332
+
2333
+ /* ============================================================================
2334
+ * DEAD UI DETECTOR (Enhanced)
2335
+ * Finds buttons, forms, and links that do nothing
2336
+ * ========================================================================== */
2337
+
2338
+ function findDeadUI(repoRoot) {
2339
+ const findings = [];
2340
+ const files = fg.sync(["**/*.{tsx,jsx}"], {
2341
+ cwd: repoRoot,
2342
+ absolute: true,
2343
+ ignore: STANDARD_IGNORE_PATTERNS,
2344
+ });
2345
+
2346
+ for (const fileAbs of files) {
2347
+ const code = readFileCached(fileAbs);
2348
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
2349
+
2350
+ // Fast path: skip files without interactive elements
2351
+ if (!/<(button|Button|form|Form|a|Link)\b/.test(code)) continue;
2352
+
2353
+ let ast;
2354
+ try {
2355
+ ast = parseFile(code, fileAbs);
2356
+ } catch {
2357
+ continue;
2358
+ }
2359
+
2360
+ try {
2361
+ traverse(ast, {
2362
+ JSXElement(pathNode) {
2363
+ const opening = pathNode.node.openingElement;
2364
+ const tagName = opening.name?.name || opening.name?.property?.name || "";
2365
+
2366
+ // Check for buttons, forms, links
2367
+ if (!/^(button|Button|form|Form|a|Link)$/i.test(tagName)) return;
2368
+
2369
+ const attrs = opening.attributes || [];
2370
+ let hasOnClick = false;
2371
+ let hasOnSubmit = false;
2372
+ let hasHref = false;
2373
+ let hasAction = false;
2374
+ let hasType = false;
2375
+ let isDisabled = false;
2376
+
2377
+ for (const attr of attrs) {
2378
+ if (!t.isJSXAttribute(attr)) continue;
2379
+ const name = attr.name?.name || "";
2380
+
2381
+ if (name === "onClick" || name === "onPress") hasOnClick = true;
2382
+ if (name === "onSubmit") hasOnSubmit = true;
2383
+ if (name === "href" || name === "to") hasHref = true;
2384
+ if (name === "action") hasAction = true;
2385
+ if (name === "type") hasType = true;
2386
+ if (name === "disabled") isDisabled = true;
2387
+ }
2388
+
2389
+ // Button without onClick (unless it's a submit button or disabled)
2390
+ if (/button/i.test(tagName) && !hasOnClick && !isDisabled) {
2391
+ const isSubmitType = attrs.some(a =>
2392
+ t.isJSXAttribute(a) &&
2393
+ a.name?.name === "type" &&
2394
+ t.isStringLiteral(a.value) &&
2395
+ a.value.value === "submit"
2396
+ );
2397
+
2398
+ if (!isSubmitType) {
2399
+ const loc = opening.loc;
2400
+ findings.push({
2401
+ id: stableId("F_DEAD_UI", `${fileRel}:${loc?.start?.line || 0}:button`),
2402
+ severity: "WARN",
2403
+ category: "DeadUI",
2404
+ title: "Button without onClick handler",
2405
+ why: "This button does nothing when clicked. Users expect buttons to perform actions.",
2406
+ confidence: "med",
2407
+ evidence: [evidenceFromLoc(fileAbs, repoRoot, loc, "Dead button")].filter(Boolean),
2408
+ fixHints: [
2409
+ "<Button onClick={() => handleAction()}>Click Me</Button>",
2410
+ "For submit: <Button type=\"submit\">Submit</Button>",
2411
+ "For disabled: <Button disabled>Coming Soon</Button>",
2412
+ ],
2413
+ });
2414
+ }
2415
+ }
2416
+
2417
+ // Form without onSubmit or action
2418
+ if (/form/i.test(tagName) && !hasOnSubmit && !hasAction) {
2419
+ const loc = opening.loc;
2420
+ findings.push({
2421
+ id: stableId("F_DEAD_UI", `${fileRel}:${loc?.start?.line || 0}:form`),
2422
+ severity: "WARN",
2423
+ category: "DeadUI",
2424
+ title: "Form without onSubmit or action",
2425
+ why: "This form does nothing when submitted. Form data won't be processed.",
2426
+ confidence: "med",
2427
+ evidence: [evidenceFromLoc(fileAbs, repoRoot, loc, "Dead form")].filter(Boolean),
2428
+ fixHints: [
2429
+ "Add an onSubmit handler to process form data.",
2430
+ "Or add an action attribute for server-side submission.",
2431
+ ],
2432
+ });
2433
+ }
2434
+
2435
+ // Link without href
2436
+ if (/^(a|Link)$/i.test(tagName) && !hasHref) {
2437
+ const loc = opening.loc;
2438
+ findings.push({
2439
+ id: stableId("F_DEAD_UI", `${fileRel}:${loc?.start?.line || 0}:link`),
2440
+ severity: "WARN",
2441
+ category: "DeadUI",
2442
+ title: "Link without href or to prop",
2443
+ why: "This link goes nowhere. Users expect links to navigate somewhere.",
2444
+ confidence: "med",
2445
+ evidence: [evidenceFromLoc(fileAbs, repoRoot, loc, "Dead link")].filter(Boolean),
2446
+ fixHints: [
2447
+ "Add href prop with the target URL.",
2448
+ "If using Next.js Link, ensure href is provided.",
2449
+ "If it's a button styled as link, use a button element instead.",
2450
+ ],
2451
+ });
2452
+ }
2453
+ }
2454
+ });
2455
+ } catch {
2456
+ continue;
2457
+ }
2458
+ }
2459
+
2460
+ return findings;
2461
+ }
2462
+
2463
+ module.exports = {
2464
+ // V3: Cache management - call after scan completes to prevent memory leaks
2465
+ clearFileCache,
2466
+
2467
+ // V3: Entropy helper - exported for testing/reuse
2468
+ getShannonEntropy,
2469
+
2470
+ // Analyzers
2471
+ findMissingRoutes,
2472
+ findEnvGaps,
2473
+ findFakeSuccess,
2474
+ findGhostAuth,
2475
+ findStripeWebhookViolations,
2476
+ findPaidSurfaceNotEnforced,
2477
+ findOwnerModeBypass,
2478
+ findMockData,
2479
+ findTodoFixme,
2480
+ findConsoleLogs,
2481
+ findHardcodedSecrets,
2482
+ findDeadCode,
2483
+ findDeprecatedApis,
2484
+ findEmptyCatch,
2485
+ findUnsafeRegex,
2486
+ // Enhanced analyzers
2487
+ findSecurityVulnerabilities,
2488
+ findPerformanceIssues,
2489
+ findCodeQualityIssues,
2490
+ // Advanced analyzers
2491
+ findCrossFileIssues,
2492
+ findTypeSafetyIssues,
2493
+ findAccessibilityIssues,
2494
+ findAPIConsistencyIssues,
2495
+ // NEW: AI Hallucination Detectors
2496
+ findOptimisticNoRollback,
2497
+ findSilentCatch,
2498
+ findMethodMismatch,
2499
+ findDeadUI,
2500
+ };