@vibecheckai/cli 3.4.0 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/bin/registry.js +154 -338
  2. package/bin/runners/context/generators/mcp.js +13 -15
  3. package/bin/runners/context/proof-context.js +1 -248
  4. package/bin/runners/lib/analysis-core.js +180 -198
  5. package/bin/runners/lib/analyzers.js +223 -1669
  6. package/bin/runners/lib/cli-output.js +210 -242
  7. package/bin/runners/lib/detectors-v2.js +785 -547
  8. package/bin/runners/lib/entitlements-v2.js +458 -96
  9. package/bin/runners/lib/error-handler.js +9 -16
  10. package/bin/runners/lib/global-flags.js +0 -37
  11. package/bin/runners/lib/route-truth.js +322 -1167
  12. package/bin/runners/lib/scan-output.js +469 -448
  13. package/bin/runners/lib/ship-output.js +27 -280
  14. package/bin/runners/lib/terminal-ui.js +733 -231
  15. package/bin/runners/lib/truth.js +321 -1004
  16. package/bin/runners/lib/unified-output.js +158 -162
  17. package/bin/runners/lib/upsell.js +204 -104
  18. package/bin/runners/runAllowlist.js +324 -0
  19. package/bin/runners/runAuth.js +95 -324
  20. package/bin/runners/runCheckpoint.js +21 -39
  21. package/bin/runners/runContext.js +24 -136
  22. package/bin/runners/runDoctor.js +67 -115
  23. package/bin/runners/runEvidencePack.js +219 -0
  24. package/bin/runners/runFix.js +5 -6
  25. package/bin/runners/runGuard.js +118 -212
  26. package/bin/runners/runInit.js +2 -14
  27. package/bin/runners/runInstall.js +281 -0
  28. package/bin/runners/runLabs.js +341 -0
  29. package/bin/runners/runMcp.js +52 -130
  30. package/bin/runners/runPolish.js +20 -43
  31. package/bin/runners/runProve.js +3 -13
  32. package/bin/runners/runReality.js +0 -14
  33. package/bin/runners/runReport.js +2 -3
  34. package/bin/runners/runScan.js +44 -511
  35. package/bin/runners/runShip.js +14 -28
  36. package/bin/runners/runValidate.js +2 -19
  37. package/bin/runners/runWatch.js +54 -118
  38. package/bin/vibecheck.js +41 -148
  39. package/mcp-server/ARCHITECTURE.md +339 -0
  40. package/mcp-server/__tests__/cache.test.ts +313 -0
  41. package/mcp-server/__tests__/executor.test.ts +239 -0
  42. package/mcp-server/__tests__/fixtures/exclusion-test/.cache/webpack/cache.pack +1 -0
  43. package/mcp-server/__tests__/fixtures/exclusion-test/.next/server/chunk.js +3 -0
  44. package/mcp-server/__tests__/fixtures/exclusion-test/.turbo/cache.json +3 -0
  45. package/mcp-server/__tests__/fixtures/exclusion-test/.venv/lib/env.py +3 -0
  46. package/mcp-server/__tests__/fixtures/exclusion-test/dist/bundle.js +3 -0
  47. package/mcp-server/__tests__/fixtures/exclusion-test/package.json +5 -0
  48. package/mcp-server/__tests__/fixtures/exclusion-test/src/app.ts +5 -0
  49. package/mcp-server/__tests__/fixtures/exclusion-test/venv/lib/config.py +4 -0
  50. package/mcp-server/__tests__/ids.test.ts +345 -0
  51. package/mcp-server/__tests__/integration/tools.test.ts +410 -0
  52. package/mcp-server/__tests__/registry.test.ts +365 -0
  53. package/mcp-server/__tests__/sandbox.test.ts +323 -0
  54. package/mcp-server/__tests__/schemas.test.ts +372 -0
  55. package/mcp-server/benchmarks/run-benchmarks.ts +304 -0
  56. package/mcp-server/examples/doctor.request.json +14 -0
  57. package/mcp-server/examples/doctor.response.json +53 -0
  58. package/mcp-server/examples/error.response.json +15 -0
  59. package/mcp-server/examples/scan.request.json +14 -0
  60. package/mcp-server/examples/scan.response.json +108 -0
  61. package/mcp-server/handlers/tool-handler.ts +671 -0
  62. package/mcp-server/index-v3.ts +293 -0
  63. package/mcp-server/index.js +1072 -1573
  64. package/mcp-server/index.old.js +4137 -0
  65. package/mcp-server/lib/cache.ts +341 -0
  66. package/mcp-server/lib/errors.ts +346 -0
  67. package/mcp-server/lib/executor.ts +792 -0
  68. package/mcp-server/lib/ids.ts +238 -0
  69. package/mcp-server/lib/logger.ts +368 -0
  70. package/mcp-server/lib/metrics.ts +365 -0
  71. package/mcp-server/lib/sandbox.ts +337 -0
  72. package/mcp-server/lib/validator.ts +229 -0
  73. package/mcp-server/package-lock.json +165 -0
  74. package/mcp-server/package.json +32 -7
  75. package/mcp-server/premium-tools.js +2 -2
  76. package/mcp-server/registry/tools.json +476 -0
  77. package/mcp-server/schemas/error-envelope.schema.json +125 -0
  78. package/mcp-server/schemas/finding.schema.json +167 -0
  79. package/mcp-server/schemas/report-artifact.schema.json +88 -0
  80. package/mcp-server/schemas/run-request.schema.json +75 -0
  81. package/mcp-server/schemas/verdict.schema.json +168 -0
  82. package/mcp-server/tier-auth.d.ts +71 -0
  83. package/mcp-server/tier-auth.js +371 -183
  84. package/mcp-server/truth-context.js +90 -131
  85. package/mcp-server/truth-firewall-tools.js +1000 -1611
  86. package/mcp-server/tsconfig.json +34 -0
  87. package/mcp-server/vibecheck-tools.js +2 -2
  88. package/mcp-server/vitest.config.ts +16 -0
  89. package/package.json +3 -4
  90. package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +0 -474
  91. package/bin/runners/lib/agent-firewall/change-packet/builder.js +0 -488
  92. package/bin/runners/lib/agent-firewall/change-packet/schema.json +0 -228
  93. package/bin/runners/lib/agent-firewall/change-packet/store.js +0 -200
  94. package/bin/runners/lib/agent-firewall/claims/claim-types.js +0 -21
  95. package/bin/runners/lib/agent-firewall/claims/extractor.js +0 -303
  96. package/bin/runners/lib/agent-firewall/claims/patterns.js +0 -24
  97. package/bin/runners/lib/agent-firewall/critic/index.js +0 -151
  98. package/bin/runners/lib/agent-firewall/critic/judge.js +0 -432
  99. package/bin/runners/lib/agent-firewall/critic/prompts.js +0 -305
  100. package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +0 -88
  101. package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +0 -75
  102. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +0 -127
  103. package/bin/runners/lib/agent-firewall/evidence/resolver.js +0 -102
  104. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +0 -213
  105. package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +0 -145
  106. package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +0 -19
  107. package/bin/runners/lib/agent-firewall/fs-hook/installer.js +0 -87
  108. package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +0 -184
  109. package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +0 -163
  110. package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +0 -107
  111. package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +0 -68
  112. package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +0 -66
  113. package/bin/runners/lib/agent-firewall/interceptor/base.js +0 -304
  114. package/bin/runners/lib/agent-firewall/interceptor/cursor.js +0 -35
  115. package/bin/runners/lib/agent-firewall/interceptor/vscode.js +0 -35
  116. package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +0 -34
  117. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +0 -465
  118. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +0 -604
  119. package/bin/runners/lib/agent-firewall/lawbook/index.js +0 -304
  120. package/bin/runners/lib/agent-firewall/lawbook/registry.js +0 -514
  121. package/bin/runners/lib/agent-firewall/lawbook/schema.js +0 -420
  122. package/bin/runners/lib/agent-firewall/logger.js +0 -141
  123. package/bin/runners/lib/agent-firewall/policy/default-policy.json +0 -90
  124. package/bin/runners/lib/agent-firewall/policy/engine.js +0 -103
  125. package/bin/runners/lib/agent-firewall/policy/loader.js +0 -451
  126. package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +0 -50
  127. package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +0 -50
  128. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +0 -86
  129. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +0 -162
  130. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +0 -189
  131. package/bin/runners/lib/agent-firewall/policy/rules/scope.js +0 -93
  132. package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +0 -57
  133. package/bin/runners/lib/agent-firewall/policy/schema.json +0 -183
  134. package/bin/runners/lib/agent-firewall/policy/verdict.js +0 -54
  135. package/bin/runners/lib/agent-firewall/proposal/extractor.js +0 -394
  136. package/bin/runners/lib/agent-firewall/proposal/index.js +0 -212
  137. package/bin/runners/lib/agent-firewall/proposal/schema.js +0 -251
  138. package/bin/runners/lib/agent-firewall/proposal/validator.js +0 -386
  139. package/bin/runners/lib/agent-firewall/reality/index.js +0 -332
  140. package/bin/runners/lib/agent-firewall/reality/state.js +0 -625
  141. package/bin/runners/lib/agent-firewall/reality/watcher.js +0 -322
  142. package/bin/runners/lib/agent-firewall/risk/index.js +0 -173
  143. package/bin/runners/lib/agent-firewall/risk/scorer.js +0 -328
  144. package/bin/runners/lib/agent-firewall/risk/thresholds.js +0 -321
  145. package/bin/runners/lib/agent-firewall/risk/vectors.js +0 -421
  146. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +0 -472
  147. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +0 -346
  148. package/bin/runners/lib/agent-firewall/simulator/index.js +0 -181
  149. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +0 -380
  150. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +0 -661
  151. package/bin/runners/lib/agent-firewall/time-machine/index.js +0 -267
  152. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +0 -436
  153. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +0 -490
  154. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +0 -530
  155. package/bin/runners/lib/agent-firewall/truthpack/index.js +0 -67
  156. package/bin/runners/lib/agent-firewall/truthpack/loader.js +0 -137
  157. package/bin/runners/lib/agent-firewall/unblock/planner.js +0 -337
  158. package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +0 -118
  159. package/bin/runners/lib/api-client.js +0 -269
  160. package/bin/runners/lib/authority-badge.js +0 -425
  161. package/bin/runners/lib/engines/accessibility-engine.js +0 -190
  162. package/bin/runners/lib/engines/api-consistency-engine.js +0 -162
  163. package/bin/runners/lib/engines/ast-cache.js +0 -99
  164. package/bin/runners/lib/engines/code-quality-engine.js +0 -255
  165. package/bin/runners/lib/engines/console-logs-engine.js +0 -115
  166. package/bin/runners/lib/engines/cross-file-analysis-engine.js +0 -268
  167. package/bin/runners/lib/engines/dead-code-engine.js +0 -198
  168. package/bin/runners/lib/engines/deprecated-api-engine.js +0 -226
  169. package/bin/runners/lib/engines/empty-catch-engine.js +0 -150
  170. package/bin/runners/lib/engines/file-filter.js +0 -131
  171. package/bin/runners/lib/engines/hardcoded-secrets-engine.js +0 -251
  172. package/bin/runners/lib/engines/mock-data-engine.js +0 -272
  173. package/bin/runners/lib/engines/parallel-processor.js +0 -71
  174. package/bin/runners/lib/engines/performance-issues-engine.js +0 -265
  175. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +0 -243
  176. package/bin/runners/lib/engines/todo-fixme-engine.js +0 -115
  177. package/bin/runners/lib/engines/type-aware-engine.js +0 -152
  178. package/bin/runners/lib/engines/unsafe-regex-engine.js +0 -225
  179. package/bin/runners/lib/engines/vibecheck-engines/README.md +0 -53
  180. package/bin/runners/lib/engines/vibecheck-engines/index.js +0 -15
  181. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +0 -164
  182. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +0 -291
  183. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +0 -83
  184. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +0 -198
  185. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +0 -275
  186. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +0 -167
  187. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +0 -217
  188. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +0 -139
  189. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +0 -140
  190. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +0 -164
  191. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +0 -234
  192. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +0 -217
  193. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +0 -78
  194. package/bin/runners/lib/engines/vibecheck-engines/package.json +0 -13
  195. package/bin/runners/lib/exit-codes.js +0 -275
  196. package/bin/runners/lib/fingerprint.js +0 -377
  197. package/bin/runners/lib/help-formatter.js +0 -413
  198. package/bin/runners/lib/logger.js +0 -38
  199. package/bin/runners/lib/ship-output-enterprise.js +0 -239
  200. package/bin/runners/lib/unified-cli-output.js +0 -604
  201. package/bin/runners/runAgent.d.ts +0 -5
  202. package/bin/runners/runAgent.js +0 -161
  203. package/bin/runners/runApprove.js +0 -1200
  204. package/bin/runners/runClassify.js +0 -859
  205. package/bin/runners/runContext.d.ts +0 -4
  206. package/bin/runners/runFirewall.d.ts +0 -5
  207. package/bin/runners/runFirewall.js +0 -134
  208. package/bin/runners/runFirewallHook.d.ts +0 -5
  209. package/bin/runners/runFirewallHook.js +0 -56
  210. package/bin/runners/runPolish.d.ts +0 -4
  211. package/bin/runners/runProof.zip +0 -0
  212. package/bin/runners/runTruth.d.ts +0 -5
  213. package/bin/runners/runTruth.js +0 -101
  214. package/mcp-server/HARDENING_SUMMARY.md +0 -299
  215. package/mcp-server/agent-firewall-interceptor.js +0 -500
  216. package/mcp-server/authority-tools.js +0 -569
  217. package/mcp-server/conductor/conflict-resolver.js +0 -588
  218. package/mcp-server/conductor/execution-planner.js +0 -544
  219. package/mcp-server/conductor/index.js +0 -377
  220. package/mcp-server/conductor/lock-manager.js +0 -615
  221. package/mcp-server/conductor/request-queue.js +0 -550
  222. package/mcp-server/conductor/session-manager.js +0 -500
  223. package/mcp-server/conductor/tools.js +0 -510
  224. package/mcp-server/lib/api-client.cjs +0 -13
  225. package/mcp-server/lib/logger.cjs +0 -30
  226. package/mcp-server/logger.js +0 -173
  227. package/mcp-server/tools-v3.js +0 -706
  228. package/mcp-server/vibecheck-mcp-server-3.2.0.tgz +0 -0
@@ -1,622 +1,860 @@
1
1
  /**
2
- * Truth Context – MCP Tools for Evidence‑Backed AI
3
- *
4
- * Core context-engine tools that surface **truth-backed** context for AI agents.
5
- * Every response is grounded in concrete evidence with file/line citations
6
- * and explicit confidence scores.
7
- *
8
- * This is the "Truth Firewall", exposed to agents as an "Evidence Pack" / "Truth Pack". [web:3]
9
- *
10
- * Tools:
11
- * - vibecheck.ctx – Build a repo-level Truth Pack (routes, auth, billing, env, schema)
12
- * - vibecheck.verify_claim – Check whether a claim is backed by real evidence
13
- * - vibecheck.evidence – Pull code-level evidence for a specific file/function
2
+ * Canonical Detectors v2
3
+ *
4
+ * Implementation of all spec-defined detectors that produce v2-compliant findings.
5
+ * Each detector has a unique ID and produces evidence-backed findings.
6
+ *
7
+ * Detector Categories:
8
+ * - Routes (D_ROUTE_*)
9
+ * - Auth Coverage (D_AUTH_*)
10
+ * - Env (D_ENV_*)
11
+ * - Fake Success / Truth (D_FAKE_SUCCESS_*, D_SILENT_CATCH)
12
+ * - Dead UI (D_DEAD_*, D_UI_*)
13
+ * - Billing/Stripe (D_STRIPE_*)
14
+ * - Entitlements (D_LOCAL_BYPASS_*)
15
+ * - Drift (D_CONTRACTS_*)
14
16
  */
15
17
 
16
- import fs from "fs/promises";
17
- import path from "path";
18
- import { execSync } from "child_process";
19
-
20
- // ============================================================================
21
- // TRUTH CONTEXT TOOLS
22
- // ============================================================================
23
-
24
- export const TRUTH_CONTEXT_TOOLS = [
25
- {
26
- name: "vibecheck.ctx",
27
- description: `📋 Build a repo Truth Pack: routes, auth, billing, env vars, schema.
28
-
29
- Generates an evidence-backed context bundle with file/line citations.
30
- Use this before the agent makes any architectural or behavioral claims
31
- about the codebase.
32
-
33
- Returns:
34
- - routes: All detected routes with handlers and middleware
35
- - auth: Auth guards, protected routes, auth flow indicators
36
- - billing: Payment gates, subscription checks, paid feature indicators
37
- - env: Environment variables (declared vs used, mismatches)
38
- - schema: Database schema and TypeScript contracts
39
- - confidence: Aggregate confidence score (0–1) for the extracted view`,
40
- inputSchema: {
41
- type: "object",
42
- properties: {
43
- scope: {
44
- type: "string",
45
- enum: ["all", "routes", "auth", "billing", "env", "schema"],
46
- description: "Which slice of context to extract (default: all)",
47
- default: "all",
48
- },
49
- path: {
50
- type: "string",
51
- description: "Project root path (default: current working directory)",
52
- },
53
- },
54
- },
55
- },
56
- {
57
- name: "vibecheck.verify_claim",
58
- description: `🔍 Truth Firewall check – verify that a claim is backed by code.
59
-
60
- Run this before asserting that something exists, is configured, or is enforced.
61
- Returns concrete evidence (file/line) when the claim is supported,
62
- or a structured rejection with an explanation when it is not.
63
-
64
- Examples:
65
- - "Route /api/users exists" → VERIFIED with handler at src/routes/users.ts:45
66
- - "Auth is required for /admin" → VERIFIED via middleware at src/middleware/auth.ts:12
67
- - "Stripe is configured" → REJECTED: No evidence of Stripe integration found`,
68
- inputSchema: {
69
- type: "object",
70
- properties: {
71
- claim_type: {
72
- type: "string",
73
- enum: [
74
- "route",
75
- "endpoint",
76
- "env_var",
77
- "middleware",
78
- "auth_guard",
79
- "billing_gate",
80
- "file",
81
- "function",
82
- ],
83
- description: "Category of claim to verify",
84
- },
85
- claim: {
86
- type: "string",
87
- description:
88
- "The claim subject (e.g. '/api/users', 'AUTH_SECRET', 'authMiddleware')",
89
- },
90
- path: {
91
- type: "string",
92
- description: "Project root path (default: current working directory)",
93
- },
94
- },
95
- required: ["claim_type", "claim"],
96
- },
97
- },
98
- {
99
- name: "vibecheck.evidence",
100
- description: `📎 Retrieve code evidence for a file or symbol.
101
-
102
- Returns an annotated code snippet with line numbers for precise citation.
103
- Use this when the agent needs to quote or reason about specific code blocks
104
- in its response.`,
105
- inputSchema: {
106
- type: "object",
107
- properties: {
108
- file: {
109
- type: "string",
110
- description: "File path relative to the project root",
111
- },
112
- function_name: {
113
- type: "string",
114
- description: "Optional function/class name to locate within the file",
115
- },
116
- line: {
117
- type: "number",
118
- description: "Optional 1-based line number to center the snippet on",
119
- },
120
- context_lines: {
121
- type: "number",
122
- description:
123
- "Number of lines of context before/after the target (default: 10)",
124
- default: 10,
125
- },
126
- path: {
127
- type: "string",
128
- description: "Project root path (default: current working directory)",
129
- },
130
- },
131
- required: ["file"],
132
- },
133
- },
134
- ];
135
-
136
- // ============================================================================
137
- // TOOL DISPATCH
138
- // ============================================================================
139
-
140
- export async function handleTruthContextTool(toolName, args) {
141
- const projectPath = args.path || process.cwd();
142
-
143
- switch (toolName) {
144
- case "vibecheck.ctx":
145
- return await getTruthPack(projectPath, args.scope || "all");
146
- case "vibecheck.verify_claim":
147
- return await verifyClaim(projectPath, args.claim_type, args.claim);
148
- case "vibecheck.evidence":
149
- return await getEvidence(projectPath, args.file, args);
150
- default:
151
- return { error: `Unknown tool: ${toolName}` };
152
- }
153
- }
18
+ "use strict";
154
19
 
155
- // ============================================================================
156
- // CONTEXT EXTRACTION
157
- // ============================================================================
20
+ const fs = require("fs");
21
+ const path = require("path");
22
+ const crypto = require("crypto");
23
+ const { createFindingV2, createEvidence, generateFingerprint } = require("./schema-validator");
158
24
 
159
- async function getTruthPack(projectPath, scope) {
160
- const truthPack = {
161
- version: "1.0.0",
162
- generatedAt: new Date().toISOString(),
163
- projectPath,
164
- scope,
165
- confidence: 0,
166
- sections: {},
167
- };
25
+ // =============================================================================
26
+ // B1) Routes Detectors
27
+ // =============================================================================
168
28
 
169
- try {
170
- if (scope === "all" || scope === "routes") {
171
- truthPack.sections.routes = await extractRoutes(projectPath);
172
- }
173
- if (scope === "all" || scope === "auth") {
174
- truthPack.sections.auth = await extractAuth(projectPath);
175
- }
176
- if (scope === "all" || scope === "billing") {
177
- truthPack.sections.billing = await extractBilling(projectPath);
178
- }
179
- if (scope === "all" || scope === "env") {
180
- truthPack.sections.env = await extractEnvVars(projectPath);
29
+ /**
30
+ * D_ROUTE_MISSING (BLOCK)
31
+ * Trigger: clientCalls contains /api/x but truthpack routes has no matching endpoint
32
+ */
33
+ function detectRouteMissing(truthpack) {
34
+ const findings = [];
35
+ const serverRoutes = truthpack.routes || [];
36
+ const clientCalls = truthpack.clientCalls || [];
37
+
38
+ for (const call of clientCalls) {
39
+ const resolved = call.resolvedPath || call.urlTemplate;
40
+ const method = call.method || "UNKNOWN";
41
+
42
+ const match = serverRoutes.find(r =>
43
+ routeMatches(r.path, resolved) &&
44
+ (r.methods.includes(method) || r.methods.includes("*"))
45
+ );
46
+
47
+ if (!match) {
48
+ findings.push(createFindingV2({
49
+ detectorId: "ROUTE_MISSING",
50
+ severity: "BLOCK",
51
+ category: "Routes",
52
+ scope: "client",
53
+ title: `Client calls ${method} ${resolved} but no server route exists`,
54
+ why: "AI frequently invents endpoints. This will cause 404 errors or silent failures in production.",
55
+ confidence: call.confidence || "medium",
56
+ evidence: call.evidence || [
57
+ createEvidence({
58
+ kind: "file",
59
+ reason: "Client call site",
60
+ file: call.evidence?.[0]?.file || "unknown",
61
+ lines: call.evidence?.[0]?.lines || "1-1",
62
+ })
63
+ ],
64
+ fixHints: [
65
+ "Create the missing server route handler",
66
+ "Or update the client to call an existing route",
67
+ "Check truthpack.routes for available endpoints"
68
+ ],
69
+ }));
181
70
  }
182
- if (scope === "all" || scope === "schema") {
183
- truthPack.sections.schema = await extractSchema(projectPath);
71
+ }
72
+
73
+ return findings;
74
+ }
75
+
76
+ /**
77
+ * D_ROUTE_METHOD_MISMATCH (BLOCK/WARN)
78
+ * Trigger: client uses POST but server exposes GET (or vice versa)
79
+ */
80
+ function detectRouteMethodMismatch(truthpack) {
81
+ const findings = [];
82
+ const serverRoutes = truthpack.routes || [];
83
+ const clientCalls = truthpack.clientCalls || [];
84
+
85
+ for (const call of clientCalls) {
86
+ const resolved = call.resolvedPath || call.urlTemplate;
87
+ const clientMethod = call.method || "UNKNOWN";
88
+
89
+ const pathMatch = serverRoutes.find(r => routeMatches(r.path, resolved));
90
+
91
+ if (pathMatch && !pathMatch.methods.includes(clientMethod) && !pathMatch.methods.includes("*")) {
92
+ const isCritical = /checkout|login|save|pay|submit|register/i.test(resolved);
93
+
94
+ findings.push(createFindingV2({
95
+ detectorId: "ROUTE_METHOD_MISMATCH",
96
+ severity: isCritical ? "BLOCK" : "WARN",
97
+ category: "Routes",
98
+ scope: "client",
99
+ title: `Method mismatch: client uses ${clientMethod} but server only handles ${pathMatch.methods.join("/")} for ${resolved}`,
100
+ why: "Method mismatch will cause 405 errors. Critical paths (checkout/login) must match exactly.",
101
+ confidence: "high",
102
+ evidence: [
103
+ createEvidence({
104
+ kind: "file",
105
+ reason: "Client call site",
106
+ file: call.evidence?.[0]?.file || "unknown",
107
+ lines: call.evidence?.[0]?.lines || "1-1",
108
+ }),
109
+ createEvidence({
110
+ kind: "file",
111
+ reason: "Server route handler",
112
+ file: pathMatch.handler?.file || "unknown",
113
+ lines: "1-1",
114
+ })
115
+ ],
116
+ fixHints: [
117
+ `Update client to use ${pathMatch.methods[0]} method`,
118
+ `Or add ${clientMethod} handler to server route`
119
+ ],
120
+ }));
184
121
  }
122
+ }
185
123
 
186
- const sections = Object.values(truthPack.sections);
187
- if (sections.length > 0) {
188
- truthPack.confidence =
189
- sections.reduce((sum, section) => sum + (section.confidence || 0), 0) /
190
- sections.length;
124
+ return findings;
125
+ }
126
+
127
+ /**
128
+ * D_ROUTE_PREFIX_DRIFT (WARN→BLOCK)
129
+ * Trigger: Fastify registered under /api/v1 but client hits /api/
130
+ */
131
+ function detectRoutePrefixDrift(truthpack) {
132
+ const findings = [];
133
+ const fastifyPrefixes = truthpack.stack?.fastify?.prefixes || [];
134
+ const clientCalls = truthpack.clientCalls || [];
135
+
136
+ if (fastifyPrefixes.length === 0) return findings;
137
+
138
+ const prefixSet = new Set(fastifyPrefixes);
139
+ let mismatchCount = 0;
140
+
141
+ for (const call of clientCalls) {
142
+ const resolved = call.resolvedPath || call.urlTemplate;
143
+
144
+ // Check if client path uses a known prefix
145
+ const usesKnownPrefix = fastifyPrefixes.some(prefix => resolved.startsWith(prefix));
146
+
147
+ if (!usesKnownPrefix && resolved.startsWith("/api/")) {
148
+ mismatchCount++;
191
149
  }
150
+ }
192
151
 
193
- return truthPack;
194
- } catch (error) {
195
- return {
196
- error: error.message,
197
- projectPath,
198
- suggestion: "Run `vibecheck init` to set up the project",
199
- };
152
+ if (mismatchCount > 0) {
153
+ findings.push(createFindingV2({
154
+ detectorId: "ROUTE_PREFIX_DRIFT",
155
+ severity: mismatchCount > 5 ? "BLOCK" : "WARN",
156
+ category: "Routes",
157
+ scope: "client",
158
+ title: `${mismatchCount} client calls don't match Fastify prefixes: ${fastifyPrefixes.join(", ")}`,
159
+ why: "Prefix drift causes silent failures. Clients must use the correct API prefix.",
160
+ confidence: "medium",
161
+ evidence: [
162
+ createEvidence({
163
+ kind: "file",
164
+ reason: "Fastify entry file with prefix registration",
165
+ file: truthpack.stack?.fastify?.entryFile || "unknown",
166
+ lines: "1-1",
167
+ })
168
+ ],
169
+ fixHints: [
170
+ `Update client calls to use correct prefix (${fastifyPrefixes[0] || "/api"})`,
171
+ "Or update Fastify prefix registration to match client expectations"
172
+ ],
173
+ }));
200
174
  }
201
- }
202
175
 
203
- async function extractRoutes(projectPath) {
204
- const routes = [];
205
- const routePatterns = [
206
- /app\.(get|post|put|patch|delete|use)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
207
- /router\.(get|post|put|patch|delete|use)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
208
- /@(Get|Post|Put|Patch|Delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
209
- ];
176
+ return findings;
177
+ }
210
178
 
211
- const files = await findSourceFiles(projectPath, [".ts", ".js", ".tsx", ".jsx"]);
179
+ // =============================================================================
180
+ // B2) Auth Coverage Detectors
181
+ // =============================================================================
212
182
 
213
- for (const file of files.slice(0, 50)) {
214
- try {
215
- const content = await fs.readFile(file, "utf8");
216
- const relPath = path.relative(projectPath, file);
217
-
218
- for (const pattern of routePatterns) {
219
- let match;
220
- pattern.lastIndex = 0;
221
- while ((match = pattern.exec(content)) !== null) {
222
- const line = content.substring(0, match.index).split("\n").length;
223
- routes.push({
224
- method: match[1].toUpperCase(),
225
- path: match[2],
226
- file: relPath,
227
- line,
228
- evidence: {
229
- snippet: content.split("\n")[line - 1]?.trim(),
230
- verifiedAt: new Date().toISOString(),
231
- },
232
- });
233
- }
183
+ /**
184
+ * D_AUTH_PROTECTED_ROUTE_ACCESSIBLE_ANON (BLOCK) - runtime
185
+ * Trigger: --verify-auth ANON pass can access protected route
186
+ */
187
+ function detectAuthProtectedAccessibleAnon(realityReport, authContract) {
188
+ const findings = [];
189
+ if (!realityReport?.run?.pass === "anon") return findings;
190
+
191
+ const protectedPatterns = authContract?.protectedRoutes || [];
192
+ const anonPages = realityReport.pages || [];
193
+ const anonNetwork = realityReport.network || [];
194
+
195
+ for (const pattern of protectedPatterns) {
196
+ // Check if anon pass successfully accessed a protected route
197
+ const accessed = anonNetwork.filter(req =>
198
+ matchPattern(pattern.pattern, req.url) &&
199
+ req.status >= 200 && req.status < 300
200
+ );
201
+
202
+ for (const req of accessed) {
203
+ if (pattern.expect?.anon === "deny" || pattern.expect?.anon === "redirect") {
204
+ findings.push(createFindingV2({
205
+ detectorId: "AUTH_PROTECTED_ROUTE_ACCESSIBLE_ANON",
206
+ severity: "BLOCK",
207
+ category: "AuthCoverage",
208
+ scope: "runtime",
209
+ title: `Protected route ${req.url} accessible to anonymous users`,
210
+ why: "Auth contract expects denial/redirect but got 2xx. This is a security bypass.",
211
+ confidence: "high",
212
+ evidence: [
213
+ createEvidence({
214
+ kind: "request",
215
+ reason: "Successful anonymous request to protected route",
216
+ url: req.url,
217
+ httpStatus: req.status,
218
+ requestId: req.id,
219
+ })
220
+ ],
221
+ fixHints: [
222
+ "Add server-side auth middleware to this route",
223
+ "Ensure Next.js middleware matcher covers this path",
224
+ "Verify auth contract pattern is correct"
225
+ ],
226
+ repro: {
227
+ steps: [
228
+ `Navigate to ${req.url} without authentication`,
229
+ "Observe that the page loads successfully (should be denied)"
230
+ ],
231
+ url: req.url,
232
+ },
233
+ }));
234
234
  }
235
- } catch {
236
- // Skip unreadable files
237
235
  }
238
236
  }
239
237
 
240
- return {
241
- count: routes.length,
242
- routes: routes.slice(0, 100),
243
- confidence: routes.length > 0 ? 0.8 : 0.2,
244
- };
238
+ return findings;
245
239
  }
246
240
 
247
- async function extractAuth(projectPath) {
248
- const authIndicators = [];
249
- const authPatterns = [
250
- /auth(enticate|orize|Middleware|Guard|Check)/gi,
251
- /isAuthenticated|requireAuth|verifyToken|jwt\.verify/gi,
252
- /passport\.(authenticate|use)/gi,
253
- /session\.|cookie\./gi,
254
- ];
255
-
256
- const files = await findSourceFiles(projectPath, [".ts", ".js"]);
257
-
258
- for (const file of files.slice(0, 50)) {
259
- try {
260
- const content = await fs.readFile(file, "utf8");
261
- const relPath = path.relative(projectPath, file);
262
-
263
- for (const pattern of authPatterns) {
264
- let match;
265
- pattern.lastIndex = 0;
266
- while ((match = pattern.exec(content)) !== null) {
267
- const line = content.substring(0, match.index).split("\n").length;
268
- authIndicators.push({
269
- type: "auth_indicator",
270
- match: match[0],
271
- file: relPath,
272
- line,
273
- });
274
- }
275
- }
276
- } catch {
277
- // Skip
241
+ /**
242
+ * D_AUTH_PROTECTED_ROUTE_BLOCKED_WHEN_AUTHED (BLOCK) - runtime
243
+ * Trigger: AUTH pass still denied/redirected repeatedly
244
+ */
245
+ function detectAuthProtectedBlockedWhenAuthed(realityReport, authContract) {
246
+ const findings = [];
247
+ if (!realityReport?.run?.pass === "authed") return findings;
248
+
249
+ const protectedPatterns = authContract?.protectedRoutes || [];
250
+ const authedNetwork = realityReport.network || [];
251
+
252
+ for (const pattern of protectedPatterns) {
253
+ const blocked = authedNetwork.filter(req =>
254
+ matchPattern(pattern.pattern, req.url) &&
255
+ (req.status === 401 || req.status === 403 || req.status >= 300 && req.status < 400)
256
+ );
257
+
258
+ if (blocked.length > 2 && pattern.expect?.authed === "allow") {
259
+ findings.push(createFindingV2({
260
+ detectorId: "AUTH_PROTECTED_ROUTE_BLOCKED_WHEN_AUTHED",
261
+ severity: "BLOCK",
262
+ category: "AuthCoverage",
263
+ scope: "runtime",
264
+ title: `Protected route ${pattern.pattern} blocks authenticated users`,
265
+ why: "Auth contract expects allow but authenticated user is denied/redirected repeatedly.",
266
+ confidence: "high",
267
+ evidence: blocked.slice(0, 3).map(req => createEvidence({
268
+ kind: "request",
269
+ reason: "Request denied despite authentication",
270
+ url: req.url,
271
+ httpStatus: req.status,
272
+ requestId: req.id,
273
+ })),
274
+ fixHints: [
275
+ "Check if session/token is being passed correctly",
276
+ "Verify middleware is not over-blocking",
277
+ "Check for redirect loops"
278
+ ],
279
+ }));
278
280
  }
279
281
  }
280
282
 
281
- return {
282
- count: authIndicators.length,
283
- indicators: authIndicators.slice(0, 50),
284
- confidence:
285
- authIndicators.length > 5
286
- ? 0.8
287
- : authIndicators.length > 0
288
- ? 0.5
289
- : 0.1,
290
- };
283
+ return findings;
291
284
  }
292
285
 
293
- async function extractBilling(projectPath) {
294
- const billingIndicators = [];
295
- const billingPatterns = [
296
- /stripe|paddle|lemonsqueezy|gumroad/gi,
297
- /subscription|payment|checkout|invoice/gi,
298
- /isPro|isPremium|isEnterprise|hasPaid/gi,
299
- /price|tier|plan/gi,
300
- ];
301
-
302
- const files = await findSourceFiles(projectPath, [".ts", ".js"]);
286
+ /**
287
+ * D_AUTH_CONTRACT_DRIFT (WARN/BLOCK)
288
+ * Trigger: contracts/auth.json patterns don't match middleware matcher
289
+ */
290
+ function detectAuthContractDrift(truthpack, authContract) {
291
+ const findings = [];
292
+ const middlewareMatchers = new Set(truthpack.auth?.middlewareMatchers || []);
293
+ const contractPatterns = new Set(authContract?.protectedRoutes?.map(r => r.pattern) || []);
294
+
295
+ // Patterns in contract but not in middleware
296
+ for (const pattern of contractPatterns) {
297
+ if (!middlewareMatchers.has(pattern)) {
298
+ findings.push(createFindingV2({
299
+ detectorId: "AUTH_CONTRACT_DRIFT",
300
+ severity: "BLOCK",
301
+ category: "Drift",
302
+ scope: "contracts",
303
+ title: `Auth pattern "${pattern}" in contract but not in middleware`,
304
+ why: "Contract expects protection but middleware doesn't enforce it. Security boundary may be exposed.",
305
+ confidence: "high",
306
+ evidence: [
307
+ createEvidence({
308
+ kind: "file",
309
+ reason: "Auth contract file",
310
+ file: ".vibecheck/contracts/auth.json",
311
+ lines: "1-1",
312
+ })
313
+ ],
314
+ fixHints: [
315
+ "Add pattern to middleware matcher",
316
+ "Or run 'vibecheck ctx sync' to update contract"
317
+ ],
318
+ }));
319
+ }
320
+ }
303
321
 
304
- for (const file of files.slice(0, 30)) {
305
- try {
306
- const content = await fs.readFile(file, "utf8");
307
- const relPath = path.relative(projectPath, file);
308
-
309
- for (const pattern of billingPatterns) {
310
- let match;
311
- pattern.lastIndex = 0;
312
- while ((match = pattern.exec(content)) !== null) {
313
- const line = content.substring(0, match.index).split("\n").length;
314
- billingIndicators.push({
315
- type: "billing_indicator",
316
- match: match[0],
317
- file: relPath,
318
- line,
319
- });
320
- }
321
- }
322
- } catch {
323
- // Skip
322
+ // Patterns in middleware but not in contract (WARN - might be intentional)
323
+ for (const pattern of middlewareMatchers) {
324
+ if (!contractPatterns.has(pattern)) {
325
+ findings.push(createFindingV2({
326
+ detectorId: "AUTH_CONTRACT_DRIFT",
327
+ severity: "WARN",
328
+ category: "Drift",
329
+ scope: "contracts",
330
+ title: `Middleware pattern "${pattern}" not declared in auth contract`,
331
+ why: "Middleware protects a pattern not in contract. AI agents won't know about this protection.",
332
+ confidence: "medium",
333
+ evidence: [
334
+ createEvidence({
335
+ kind: "file",
336
+ reason: "Middleware file",
337
+ file: truthpack.stack?.next?.middlewareFile || "middleware.ts",
338
+ lines: "1-1",
339
+ })
340
+ ],
341
+ fixHints: [
342
+ "Run 'vibecheck ctx sync' to update contract"
343
+ ],
344
+ }));
324
345
  }
325
346
  }
326
347
 
327
- return {
328
- count: billingIndicators.length,
329
- indicators: billingIndicators.slice(0, 30),
330
- confidence:
331
- billingIndicators.length > 3
332
- ? 0.7
333
- : billingIndicators.length > 0
334
- ? 0.4
335
- : 0.1,
336
- };
348
+ return findings;
337
349
  }
338
350
 
339
- async function extractEnvVars(projectPath) {
340
- const declared = [];
341
- const used = [];
351
+ // =============================================================================
352
+ // B3) Env Detectors
353
+ // =============================================================================
342
354
 
343
- const envFiles = [".env.example", ".env.local.example", ".env.sample"];
344
- for (const envFile of envFiles) {
345
- try {
346
- const content = await fs.readFile(path.join(projectPath, envFile), "utf8");
347
- const lines = content.split("\n");
348
- for (let i = 0; i < lines.length; i++) {
349
- const match = lines[i].match(/^([A-Z][A-Z0-9_]*)=/);
350
- if (match) {
351
- declared.push({
352
- name: match[1],
353
- file: envFile,
354
- line: i + 1,
355
- });
356
- }
355
+ /**
356
+ * D_ENV_USED_BUT_UNDECLARED (WARN→BLOCK if required)
357
+ * Trigger: truthpack envUsage includes FOO but contracts/env.json does not
358
+ * Enhanced with better required/optional detection
359
+ */
360
+ function detectEnvUsedButUndeclared(truthpack, envContract) {
361
+ const findings = [];
362
+ const envUsage = truthpack.envUsage || [];
363
+ const declaredVars = new Set(envContract?.vars?.map(v => v.name) || []);
364
+
365
+ for (const usage of envUsage) {
366
+ if (!declaredVars.has(usage.name)) {
367
+ // Check if usage pattern suggests optional
368
+ const usageCode = usage.locations?.[0]?.snippet || "";
369
+ const hasOptionalPattern = hasOptionalUsagePattern(usageCode);
370
+
371
+ // Determine if required based on name patterns and usage
372
+ const isRequired = !hasOptionalPattern &&
373
+ (usage.inferredRequiredness === "required" || isLikelyRequired(usage.name));
374
+
375
+ // Skip reporting for common optional env vars that are typically not documented
376
+ const commonOptionalVars = [
377
+ /^DEBUG$/i,
378
+ /^LOG_LEVEL$/i,
379
+ /^NODE_ENV$/i,
380
+ /^CI$/i,
381
+ /^VERCEL/i,
382
+ /^GITHUB_/i,
383
+ /^NEXT_PUBLIC_VERCEL/i,
384
+ ];
385
+
386
+ if (commonOptionalVars.some(p => p.test(usage.name))) {
387
+ continue;
357
388
  }
358
- } catch {
359
- // File does not exist
389
+
390
+ findings.push(createFindingV2({
391
+ detectorId: "ENV_USED_BUT_UNDECLARED",
392
+ severity: isRequired ? "BLOCK" : "WARN",
393
+ category: "Env",
394
+ scope: "server",
395
+ title: `Env var ${usage.name} used but not declared in contract`,
396
+ why: isRequired
397
+ ? "Required env var missing from contract. Deployment will fail if not set."
398
+ : "Env var used but not documented. AI won't know about this dependency.",
399
+ confidence: isRequired ? "high" : "medium",
400
+ evidence: usage.locations?.slice(0, 3).map(loc => createEvidence({
401
+ kind: "file",
402
+ reason: `Usage of ${usage.name}`,
403
+ file: loc.file,
404
+ lines: loc.lines,
405
+ snippet: loc.snippetHash,
406
+ })) || [],
407
+ fixHints: [
408
+ "Add to .env.example with appropriate default/placeholder",
409
+ "Run 'vibecheck ctx sync' to update env contract",
410
+ isRequired ? "Ensure this var is set in all environments" : null
411
+ ].filter(Boolean),
412
+ }));
360
413
  }
361
414
  }
362
415
 
363
- const files = await findSourceFiles(projectPath, [".ts", ".js"]);
364
- for (const file of files.slice(0, 30)) {
365
- try {
366
- const content = await fs.readFile(file, "utf8");
367
- const relPath = path.relative(projectPath, file);
368
- const pattern = /process\.env\.([A-Z][A-Z0-9_]*)/g;
369
- let match;
370
- while ((match = pattern.exec(content)) !== null) {
371
- const line = content.substring(0, match.index).split("\n").length;
372
- used.push({
373
- name: match[1],
374
- file: relPath,
375
- line,
376
- });
377
- }
378
- } catch {
379
- // Skip
380
- }
381
- }
416
+ return findings;
417
+ }
418
+
419
+ // =============================================================================
420
+ // B4) Fake Success / Truth Detectors
421
+ // =============================================================================
382
422
 
383
- const declaredNames = new Set(declared.map((d) => d.name));
384
- const usedNames = new Set(used.map((u) => u.name));
385
- const undeclared = [...usedNames].filter((name) => !declaredNames.has(name));
386
- const unused = [...declaredNames].filter((name) => !usedNames.has(name));
387
-
388
- return {
389
- declared: declared.slice(0, 50),
390
- used: used.slice(0, 50),
391
- mismatches: {
392
- undeclared,
393
- unused,
394
- },
395
- confidence: undeclared.length === 0 ? 0.9 : 0.5,
396
- };
423
+ /**
424
+ * D_FAKE_SUCCESS_TOAST_BEFORE_AWAIT (BLOCK/WARN)
425
+ * Trigger: toast.success before awaited network call
426
+ */
427
+ function detectFakeSuccessToastBeforeAwait(projectPath) {
428
+ const findings = [];
429
+ // This requires AST analysis - see existing findFakeSuccess in analyzers.js
430
+ // Placeholder for integration
431
+ return findings;
397
432
  }
398
433
 
399
- async function extractSchema(projectPath) {
400
- const schemas = [];
434
+ /**
435
+ * D_FAKE_SUCCESS_RESPONSE_OK_IGNORED (BLOCK)
436
+ * Trigger: fetch result not checked before success UI
437
+ */
438
+ function detectFakeSuccessResponseIgnored(projectPath) {
439
+ const findings = [];
440
+ // This requires AST analysis
441
+ return findings;
442
+ }
401
443
 
402
- try {
403
- const prismaPath = path.join(projectPath, "prisma", "schema.prisma");
404
- const content = await fs.readFile(prismaPath, "utf8");
405
- const modelMatches = content.matchAll(/model\s+(\w+)\s*\{/g);
406
- for (const match of modelMatches) {
407
- schemas.push({
408
- type: "prisma_model",
409
- name: match[1],
410
- file: "prisma/schema.prisma",
411
- });
444
+ /**
445
+ * D_SILENT_CATCH (WARN→BLOCK)
446
+ * Trigger: catch (e) {} OR catch { return null } + UI success continues
447
+ */
448
+ function detectSilentCatch(projectPath) {
449
+ const findings = [];
450
+ // This requires AST analysis
451
+ return findings;
452
+ }
453
+
454
+ // =============================================================================
455
+ // B5) Dead UI Detectors (runtime-first)
456
+ // =============================================================================
457
+
458
+ /**
459
+ * D_DEAD_CLICK_NO_EFFECT (BLOCK/WARN)
460
+ * Trigger: action click yields no navigation, no DOM change, no network, no console
461
+ */
462
+ function detectDeadClickNoEffect(realityReport) {
463
+ const findings = [];
464
+ const actions = realityReport?.actions || [];
465
+
466
+ for (const action of actions) {
467
+ if (action.type !== "click") continue;
468
+
469
+ const noNavigation = action.pageUrl === action.afterPageUrl;
470
+ const noDomChange = action.beforeDomHash === action.afterDomHash;
471
+ const noNetwork = !action.networkRequestIds || action.networkRequestIds.length === 0;
472
+
473
+ if (noNavigation && noDomChange && noNetwork) {
474
+ const isCritical = /save|submit|pay|login|continue|checkout/i.test(action.label || "");
475
+
476
+ findings.push(createFindingV2({
477
+ detectorId: "DEAD_CLICK_NO_EFFECT",
478
+ severity: isCritical ? "BLOCK" : "WARN",
479
+ category: "DeadUI",
480
+ scope: "runtime",
481
+ title: `Click on "${action.label || action.selector}" has no effect`,
482
+ why: "Button/link does nothing - no navigation, no DOM change, no network call. User will think app is broken.",
483
+ confidence: "high",
484
+ evidence: [
485
+ createEvidence({
486
+ kind: "screenshot",
487
+ reason: "Screenshot before click",
488
+ artifactPath: action.screenshotBefore,
489
+ }),
490
+ createEvidence({
491
+ kind: "screenshot",
492
+ reason: "Screenshot after click (unchanged)",
493
+ artifactPath: action.screenshotAfter,
494
+ })
495
+ ].filter(e => e.artifactPath),
496
+ fixHints: [
497
+ "Wire click handler to actual functionality",
498
+ "If disabled, add aria-disabled and disabled styling",
499
+ "If feature not ready, remove or hide the element"
500
+ ],
501
+ repro: {
502
+ steps: [
503
+ `Navigate to ${action.pageUrl}`,
504
+ `Click on element: ${action.selector || action.label}`,
505
+ "Observe: nothing happens"
506
+ ],
507
+ url: action.pageUrl,
508
+ },
509
+ }));
412
510
  }
413
- } catch {
414
- // No Prisma schema
415
511
  }
416
512
 
417
- const files = await findSourceFiles(projectPath, [".ts", ".tsx"]);
418
- for (const file of files.slice(0, 20)) {
419
- try {
420
- const content = await fs.readFile(file, "utf8");
421
- const relPath = path.relative(projectPath, file);
422
-
423
- const typeMatches = content.matchAll(/(?:interface|type)\s+(\w+)/g);
424
- for (const match of typeMatches) {
425
- const line = content.substring(0, match.index).split("\n").length;
426
- schemas.push({
427
- type: "typescript_type",
428
- name: match[1],
429
- file: relPath,
430
- line,
431
- });
513
+ return findings;
514
+ }
515
+
516
+ /**
517
+ * D_UI_ACTION_CAUSES_4XX_5XX (BLOCK)
518
+ * Trigger: click leads to request with 4xx/5xx
519
+ */
520
+ function detectUIActionCauses4xx5xx(realityReport) {
521
+ const findings = [];
522
+ const actions = realityReport?.actions || [];
523
+ const network = realityReport?.network || [];
524
+
525
+ for (const action of actions) {
526
+ if (!action.networkRequestIds) continue;
527
+
528
+ for (const reqId of action.networkRequestIds) {
529
+ const req = network.find(r => r.id === reqId);
530
+ if (req && (req.status >= 400 || req.failed)) {
531
+ findings.push(createFindingV2({
532
+ detectorId: "UI_ACTION_CAUSES_4XX_5XX",
533
+ severity: "BLOCK",
534
+ category: "DeadUI",
535
+ scope: "runtime",
536
+ title: `Click on "${action.label || action.selector}" causes ${req.status} error`,
537
+ why: `UI action triggers failed API call (${req.status}). User will see broken functionality.`,
538
+ confidence: "high",
539
+ evidence: [
540
+ createEvidence({
541
+ kind: "request",
542
+ reason: `Failed request triggered by UI action`,
543
+ url: req.url,
544
+ httpStatus: req.status,
545
+ requestId: req.id,
546
+ })
547
+ ],
548
+ fixHints: [
549
+ "Fix the API endpoint to return success",
550
+ "Add proper error handling in UI",
551
+ "If endpoint doesn't exist, create it"
552
+ ],
553
+ repro: {
554
+ steps: [
555
+ `Navigate to ${action.pageUrl}`,
556
+ `Click on element: ${action.selector || action.label}`,
557
+ `Observe: API returns ${req.status}`
558
+ ],
559
+ url: action.pageUrl,
560
+ },
561
+ }));
432
562
  }
433
- } catch {
434
- // Skip
435
563
  }
436
564
  }
437
565
 
438
- return {
439
- count: schemas.length,
440
- schemas: schemas.slice(0, 50),
441
- confidence:
442
- schemas.length > 5 ? 0.7 : schemas.length > 0 ? 0.4 : 0.2,
443
- };
566
+ return findings;
444
567
  }
445
568
 
446
- // ============================================================================
447
- // CLAIM VERIFICATION
448
- // ============================================================================
569
+ // =============================================================================
570
+ // B6) Billing/Stripe Detectors
571
+ // =============================================================================
449
572
 
450
- async function verifyClaim(projectPath, claimType, claim) {
451
- const result = {
452
- claim: { type: claimType, value: claim },
453
- verified: false,
454
- evidence: null,
455
- confidence: 0,
456
- rejection: null,
457
- };
573
+ /**
574
+ * D_STRIPE_WEBHOOK_NO_SIGNATURE_VERIFY (BLOCK)
575
+ * Trigger: webhook route handler lacks signature verification
576
+ */
577
+ function detectStripeWebhookNoSigVerify(truthpack) {
578
+ const findings = [];
579
+ const webhookRoutes = truthpack.externals?.stripe?.webhookRoutes || [];
580
+
581
+ // This requires checking if routes have proper verification
582
+ // Placeholder - actual implementation would scan the handler files
583
+
584
+ return findings;
585
+ }
458
586
 
459
- try {
460
- switch (claimType) {
461
- case "file": {
462
- const filePath = path.join(projectPath, claim);
463
- try {
464
- await fs.access(filePath);
465
- const stats = await fs.stat(filePath);
466
- result.verified = true;
467
- result.confidence = 1.0;
468
- result.evidence = {
469
- file: claim,
470
- exists: true,
471
- size: stats.size,
472
- verifiedAt: new Date().toISOString(),
473
- };
474
- } catch {
475
- result.rejection = `File does not exist: ${claim}`;
476
- }
477
- break;
478
- }
587
+ // =============================================================================
588
+ // B7) Entitlements Detectors
589
+ // =============================================================================
479
590
 
480
- case "route":
481
- case "endpoint": {
482
- const routes = await extractRoutes(projectPath);
483
- const matchingRoute = routes.routes.find(
484
- (route) => route.path === claim || route.path.includes(claim),
485
- );
486
- if (matchingRoute) {
487
- result.verified = true;
488
- result.confidence = 0.9;
489
- result.evidence = matchingRoute;
490
- } else {
491
- result.rejection = `No route matching "${claim}" found in codebase`;
492
- }
493
- break;
494
- }
591
+ /**
592
+ * D_LOCAL_BYPASS_PAID_FEATURE (BLOCK)
593
+ * Trigger: code checks env var like OWNER_MODE=true to unlock features
594
+ */
595
+ function detectLocalBypassPaidFeature(projectPath) {
596
+ const findings = [];
597
+ // This requires scanning for patterns like OWNER_MODE, SKIP_AUTH, etc.
598
+ return findings;
599
+ }
495
600
 
496
- case "env_var": {
497
- const envData = await extractEnvVars(projectPath);
498
- const isDeclared = envData.declared.some((env) => env.name === claim);
499
- const isUsed = envData.used.some((env) => env.name === claim);
500
- if (isDeclared || isUsed) {
501
- result.verified = true;
502
- result.confidence = isDeclared && isUsed ? 1.0 : 0.7;
503
- result.evidence = {
504
- declared: isDeclared,
505
- used: isUsed,
506
- locations: [
507
- ...envData.declared.filter((env) => env.name === claim),
508
- ...envData.used.filter((env) => env.name === claim),
509
- ],
510
- };
511
- } else {
512
- result.rejection = `Environment variable "${claim}" not found`;
513
- }
514
- break;
515
- }
601
+ // =============================================================================
602
+ // B8) Drift Detectors
603
+ // =============================================================================
516
604
 
517
- default:
518
- result.rejection = `Claim type "${claimType}" verification is not implemented yet`;
605
+ /**
606
+ * D_CONTRACTS_OUT_OF_DATE (WARN/BLOCK)
607
+ * Trigger: truthpack fingerprint changed but contracts still reflect old fingerprint
608
+ */
609
+ function detectContractsOutOfDate(truthpack, contracts) {
610
+ const findings = [];
611
+ const truthpackFingerprint = truthpack.fingerprint;
612
+
613
+ for (const [type, contract] of Object.entries(contracts)) {
614
+ if (contract.projectFingerprint && contract.projectFingerprint !== truthpackFingerprint) {
615
+ findings.push(createFindingV2({
616
+ detectorId: "CONTRACTS_OUT_OF_DATE",
617
+ severity: type === "routes" || type === "auth" ? "BLOCK" : "WARN",
618
+ category: "Drift",
619
+ scope: "contracts",
620
+ title: `${type} contract out of date (fingerprint mismatch)`,
621
+ why: `Contract was generated from old codebase. AI agents will use stale information.`,
622
+ confidence: "high",
623
+ evidence: [
624
+ createEvidence({
625
+ kind: "hash",
626
+ reason: "Contract fingerprint",
627
+ file: `.vibecheck/contracts/${type}.json`,
628
+ lines: "1-1",
629
+ })
630
+ ],
631
+ fixHints: [
632
+ "Run 'vibecheck ctx sync' to regenerate contracts"
633
+ ],
634
+ }));
519
635
  }
520
- } catch (error) {
521
- result.rejection = `Verification error: ${error.message}`;
522
636
  }
523
637
 
524
- return result;
638
+ return findings;
525
639
  }
526
640
 
527
- // ============================================================================
528
- // EVIDENCE EXTRACTION
529
- // ============================================================================
641
+ // =============================================================================
642
+ // Helpers
643
+ // =============================================================================
530
644
 
531
- async function getEvidence(projectPath, file, options) {
532
- const filePath = path.join(projectPath, file);
533
-
534
- try {
535
- const content = await fs.readFile(filePath, "utf8");
536
- const lines = content.split("\n");
537
-
538
- let targetLine = options.line || 1;
539
- const contextLines = options.context_lines || 10;
540
-
541
- if (options.function_name) {
542
- const pattern = new RegExp(
543
- `(function|const|let|var|class)\\s+${options.function_name}`,
544
- "i",
545
- );
546
- for (let i = 0; i < lines.length; i++) {
547
- if (pattern.test(lines[i])) {
548
- targetLine = i + 1;
549
- break;
550
- }
551
- }
645
+ /**
646
+ * Enhanced route matching with support for:
647
+ * - Dynamic segments (:id, [id], [slug], [...slug])
648
+ * - Optional catch-all routes [[...slug]]
649
+ * - Query string stripping
650
+ * - Trailing slash normalization
651
+ */
652
+ function routeMatches(pattern, actual) {
653
+ // Normalize: strip query strings and trailing slashes
654
+ const normalizedPattern = pattern.split("?")[0].replace(/\/+$/, "") || "/";
655
+ const normalizedActual = actual.split("?")[0].replace(/\/+$/, "") || "/";
656
+
657
+ const patternParts = normalizedPattern.split("/").filter(Boolean);
658
+ const actualParts = normalizedActual.split("/").filter(Boolean);
659
+
660
+ // Handle catch-all routes: [...slug] or [[...slug]]
661
+ const hasCatchAll = patternParts.some(p =>
662
+ p.startsWith("[...") || p.startsWith("[[...")
663
+ );
664
+
665
+ if (hasCatchAll) {
666
+ const catchAllIndex = patternParts.findIndex(p =>
667
+ p.startsWith("[...") || p.startsWith("[[...")
668
+ );
669
+
670
+ // For catch-all, pattern up to catch-all must match
671
+ for (let i = 0; i < catchAllIndex; i++) {
672
+ const p = patternParts[i];
673
+ if (isDynamicSegment(p)) continue;
674
+ if (p !== actualParts[i]) return false;
675
+ }
676
+
677
+ // Catch-all matches any remaining segments (including none for [[...]])
678
+ const isOptional = patternParts[catchAllIndex].startsWith("[[...");
679
+ if (!isOptional && actualParts.length <= catchAllIndex) {
680
+ return false;
552
681
  }
682
+
683
+ return true;
684
+ }
685
+
686
+ // Standard matching: lengths must match
687
+ if (patternParts.length !== actualParts.length) return false;
553
688
 
554
- const startLine = Math.max(1, targetLine - contextLines);
555
- const endLine = Math.min(lines.length, targetLine + contextLines);
556
-
557
- const snippet = lines
558
- .slice(startLine - 1, endLine)
559
- .map(
560
- (line, index) =>
561
- `${String(startLine + index).padStart(4, " ")} | ${line}`,
562
- )
563
- .join("\n");
564
-
565
- return {
566
- file,
567
- targetLine,
568
- startLine,
569
- endLine,
570
- totalLines: lines.length,
571
- snippet,
572
- verifiedAt: new Date().toISOString(),
573
- };
574
- } catch (error) {
575
- return {
576
- error: `Cannot read file: ${error.message}`,
577
- file,
578
- };
689
+ for (let i = 0; i < patternParts.length; i++) {
690
+ const p = patternParts[i];
691
+ if (isDynamicSegment(p)) continue;
692
+ if (p !== actualParts[i]) return false;
579
693
  }
694
+ return true;
580
695
  }
581
696
 
582
- // ============================================================================
583
- // UTILITIES
584
- // ============================================================================
585
-
586
- async function findSourceFiles(projectPath, extensions) {
587
- const files = [];
697
+ /**
698
+ * Check if a route segment is dynamic
699
+ */
700
+ function isDynamicSegment(segment) {
701
+ return segment.startsWith(":") || // Express-style :id
702
+ segment.startsWith("[") || // Next.js-style [id]
703
+ segment === "*" || // Wildcard
704
+ segment.startsWith("$"); // Remix-style $id
705
+ }
588
706
 
589
- async function walk(dir) {
707
+ /**
708
+ * Enhanced glob pattern matching with proper escaping
709
+ */
710
+ function matchPattern(pattern, url) {
711
+ try {
712
+ // Normalize URL: extract pathname
713
+ let pathname;
590
714
  try {
591
- const entries = await fs.readdir(dir, { withFileTypes: true });
592
- for (const entry of entries) {
593
- const fullPath = path.join(dir, entry.name);
594
- if (entry.isDirectory()) {
595
- if (
596
- !entry.name.startsWith(".") &&
597
- entry.name !== "node_modules" &&
598
- entry.name !== "dist" &&
599
- entry.name !== "build"
600
- ) {
601
- await walk(fullPath);
602
- }
603
- } else if (entry.isFile()) {
604
- const ext = path.extname(entry.name).toLowerCase();
605
- if (extensions.includes(ext)) {
606
- files.push(fullPath);
607
- }
608
- }
609
- }
715
+ pathname = new URL(url, "http://localhost").pathname;
610
716
  } catch {
611
- // Skip inaccessible directories
717
+ pathname = url.split("?")[0];
612
718
  }
719
+
720
+ // Escape special regex chars except * and ?
721
+ const escaped = pattern
722
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
723
+ .replace(/\*\*/g, "{{GLOBSTAR}}")
724
+ .replace(/\*/g, "[^/]*")
725
+ .replace(/\?/g, ".")
726
+ .replace(/{{GLOBSTAR}}/g, ".*");
727
+
728
+ const regex = new RegExp("^" + escaped + "$");
729
+ return regex.test(url) || regex.test(pathname);
730
+ } catch {
731
+ // Fallback to simple comparison
732
+ return pattern === url;
613
733
  }
734
+ }
614
735
 
615
- await walk(projectPath);
616
- return files;
736
+ /**
737
+ * Enhanced env var requirement detection
738
+ * Returns { required: boolean, confidence: 'high' | 'medium' | 'low', reason: string }
739
+ */
740
+ function isLikelyRequired(name) {
741
+ // High-confidence required patterns
742
+ const highConfidenceRequired = [
743
+ /^DATABASE_URL$/i,
744
+ /^NEXTAUTH_SECRET$/i,
745
+ /^NEXTAUTH_URL$/i,
746
+ /^JWT_SECRET$/i,
747
+ /^AUTH_SECRET$/i,
748
+ /^SESSION_SECRET$/i,
749
+ /^ENCRYPTION_KEY$/i,
750
+ /^STRIPE_SECRET_KEY$/i,
751
+ /^STRIPE_WEBHOOK_SECRET$/i,
752
+ /^OPENAI_API_KEY$/i,
753
+ /^ANTHROPIC_API_KEY$/i,
754
+ ];
755
+
756
+ // Medium-confidence required patterns
757
+ const mediumConfidenceRequired = [
758
+ /SECRET$/i,
759
+ /TOKEN$/i,
760
+ /API_KEY$/i,
761
+ /PRIVATE_KEY$/i,
762
+ /PASSWORD$/i,
763
+ /CREDENTIALS$/i,
764
+ ];
765
+
766
+ // Patterns that indicate optional env vars
767
+ const optionalPatterns = [
768
+ /^DEBUG/i,
769
+ /^LOG_/i,
770
+ /^ENABLE_/i,
771
+ /^DISABLE_/i,
772
+ /^FEATURE_/i,
773
+ /^FLAG_/i,
774
+ /^ANALYTICS/i,
775
+ /^TELEMETRY/i,
776
+ /^SENTRY/i,
777
+ /^PORT$/i,
778
+ /^HOST$/i,
779
+ /^NODE_ENV$/i,
780
+ ];
781
+
782
+ // Check optional first (overrides required)
783
+ if (optionalPatterns.some(p => p.test(name))) {
784
+ return false;
785
+ }
786
+
787
+ // Check high-confidence required
788
+ if (highConfidenceRequired.some(p => p.test(name))) {
789
+ return true;
790
+ }
791
+
792
+ // Check medium-confidence required
793
+ if (mediumConfidenceRequired.some(p => p.test(name))) {
794
+ return true;
795
+ }
796
+
797
+ return false;
798
+ }
799
+
800
+ /**
801
+ * Check if an env var usage suggests it's optional
802
+ */
803
+ function hasOptionalUsagePattern(code) {
804
+ const optionalPatterns = [
805
+ /\|\|\s*['"]?undefined['"]?/, // || undefined
806
+ /\?\?\s*['"]?undefined['"]?/, // ?? undefined
807
+ /\|\|\s*null/, // || null
808
+ /\?\?\s*null/, // ?? null
809
+ /\|\|\s*false/, // || false
810
+ /\?\?\s*false/, // ?? false
811
+ /if\s*\(\s*process\.env\./, // Conditional usage
812
+ /process\.env\.\w+\s*\?\s*\./, // Optional chaining
813
+ ];
814
+
815
+ return optionalPatterns.some(p => p.test(code));
617
816
  }
618
817
 
619
- export default {
620
- TRUTH_CONTEXT_TOOLS,
621
- handleTruthContextTool,
818
+ // =============================================================================
819
+ // Exports
820
+ // =============================================================================
821
+
822
+ module.exports = {
823
+ // Routes
824
+ detectRouteMissing,
825
+ detectRouteMethodMismatch,
826
+ detectRoutePrefixDrift,
827
+
828
+ // Auth
829
+ detectAuthProtectedAccessibleAnon,
830
+ detectAuthProtectedBlockedWhenAuthed,
831
+ detectAuthContractDrift,
832
+
833
+ // Env
834
+ detectEnvUsedButUndeclared,
835
+
836
+ // Fake Success
837
+ detectFakeSuccessToastBeforeAwait,
838
+ detectFakeSuccessResponseIgnored,
839
+ detectSilentCatch,
840
+
841
+ // Dead UI
842
+ detectDeadClickNoEffect,
843
+ detectUIActionCauses4xx5xx,
844
+
845
+ // Billing
846
+ detectStripeWebhookNoSigVerify,
847
+
848
+ // Entitlements
849
+ detectLocalBypassPaidFeature,
850
+
851
+ // Drift
852
+ detectContractsOutOfDate,
853
+
854
+ // Helpers
855
+ routeMatches,
856
+ matchPattern,
857
+ isLikelyRequired,
858
+ isDynamicSegment,
859
+ hasOptionalUsagePattern,
622
860
  };