@vibecheckai/cli 3.0.4 → 3.0.7

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 (108) hide show
  1. package/bin/dev/run-v2-torture.js +30 -0
  2. package/bin/runners/context/index.js +1 -1
  3. package/bin/runners/lib/analyzers.js +38 -0
  4. package/bin/runners/lib/assets/vibecheck-logo.png +0 -0
  5. package/bin/runners/lib/contracts/auth-contract.js +8 -0
  6. package/bin/runners/lib/contracts/env-contract.js +3 -0
  7. package/bin/runners/lib/contracts/external-contract.js +10 -2
  8. package/bin/runners/lib/contracts/route-contract.js +7 -0
  9. package/bin/runners/lib/contracts.js +804 -0
  10. package/bin/runners/lib/detectors-v2.js +703 -0
  11. package/bin/runners/lib/drift.js +425 -0
  12. package/bin/runners/lib/entitlements-v2.js +3 -1
  13. package/bin/runners/lib/entitlements.js +11 -3
  14. package/bin/runners/lib/env-resolver.js +417 -0
  15. package/bin/runners/lib/extractors/client-calls.js +990 -0
  16. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -0
  17. package/bin/runners/lib/extractors/fastify-routes.js +426 -0
  18. package/bin/runners/lib/extractors/index.js +363 -0
  19. package/bin/runners/lib/extractors/next-routes.js +524 -0
  20. package/bin/runners/lib/extractors/proof-graph.js +431 -0
  21. package/bin/runners/lib/extractors/route-matcher.js +451 -0
  22. package/bin/runners/lib/extractors/truthpack-v2.js +377 -0
  23. package/bin/runners/lib/extractors/ui-bindings.js +547 -0
  24. package/bin/runners/lib/findings-schema.js +281 -0
  25. package/bin/runners/lib/html-report.js +650 -0
  26. package/bin/runners/lib/missions/templates.js +45 -0
  27. package/bin/runners/lib/policy.js +295 -0
  28. package/bin/runners/lib/reality/correlation-detectors.js +359 -0
  29. package/bin/runners/lib/reality/index.js +318 -0
  30. package/bin/runners/lib/reality/request-hashing.js +416 -0
  31. package/bin/runners/lib/reality/request-mapper.js +453 -0
  32. package/bin/runners/lib/reality/safety-rails.js +463 -0
  33. package/bin/runners/lib/reality/semantic-snapshot.js +408 -0
  34. package/bin/runners/lib/reality/toast-detector.js +393 -0
  35. package/bin/runners/lib/report-html.js +5 -0
  36. package/bin/runners/lib/report-templates.js +5 -0
  37. package/bin/runners/lib/report.js +135 -0
  38. package/bin/runners/lib/route-truth.js +10 -10
  39. package/bin/runners/lib/schema-validator.js +350 -0
  40. package/bin/runners/lib/schemas/contracts.schema.json +160 -0
  41. package/bin/runners/lib/schemas/finding.schema.json +100 -0
  42. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -0
  43. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -0
  44. package/bin/runners/lib/schemas/reality-report.schema.json +162 -0
  45. package/bin/runners/lib/schemas/share-pack.schema.json +180 -0
  46. package/bin/runners/lib/schemas/ship-report.schema.json +117 -0
  47. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -0
  48. package/bin/runners/lib/schemas/validator.js +438 -0
  49. package/bin/runners/lib/ui.js +562 -0
  50. package/bin/runners/lib/verdict-engine.js +628 -0
  51. package/bin/runners/runAIAgent.js +228 -1
  52. package/bin/runners/runBadge.js +181 -1
  53. package/bin/runners/runCtx.js +7 -2
  54. package/bin/runners/runCtxDiff.js +301 -0
  55. package/bin/runners/runGuard.js +168 -0
  56. package/bin/runners/runInitGha.js +78 -15
  57. package/bin/runners/runLabs.js +341 -0
  58. package/bin/runners/runLaunch.js +180 -1
  59. package/bin/runners/runMdc.js +203 -1
  60. package/bin/runners/runProof.zip +0 -0
  61. package/bin/runners/runProve.js +23 -0
  62. package/bin/runners/runReplay.js +114 -84
  63. package/bin/runners/runScan.js +111 -32
  64. package/bin/runners/runShip.js +23 -2
  65. package/bin/runners/runTruthpack.js +9 -7
  66. package/bin/runners/runValidate.js +161 -1
  67. package/bin/vibecheck.js +416 -770
  68. package/mcp-server/.guardrail/audit/audit.log.jsonl +2 -0
  69. package/mcp-server/.specs/architecture.mdc +90 -0
  70. package/mcp-server/.specs/security.mdc +30 -0
  71. package/mcp-server/README.md +252 -0
  72. package/mcp-server/agent-checkpoint.js +364 -0
  73. package/mcp-server/architect-tools.js +707 -0
  74. package/mcp-server/audit-mcp.js +206 -0
  75. package/mcp-server/codebase-architect-tools.js +838 -0
  76. package/mcp-server/consolidated-tools.js +804 -0
  77. package/mcp-server/hygiene-tools.js +428 -0
  78. package/mcp-server/index-v1.js +698 -0
  79. package/mcp-server/index.js +2092 -0
  80. package/mcp-server/index.old.js +4137 -0
  81. package/mcp-server/intelligence-tools.js +664 -0
  82. package/mcp-server/intent-drift-tools.js +873 -0
  83. package/mcp-server/mdc-generator.js +298 -0
  84. package/mcp-server/package-lock.json +165 -0
  85. package/mcp-server/package.json +47 -0
  86. package/mcp-server/premium-tools.js +1275 -0
  87. package/mcp-server/test-mcp.js +108 -0
  88. package/mcp-server/test-tools.js +36 -0
  89. package/mcp-server/tier-auth.js +147 -0
  90. package/mcp-server/tools/index.js +72 -0
  91. package/mcp-server/tools-reorganized.ts +244 -0
  92. package/mcp-server/truth-context.js +581 -0
  93. package/mcp-server/truth-firewall-tools.js +1500 -0
  94. package/mcp-server/vibecheck-2.0-tools.js +748 -0
  95. package/mcp-server/vibecheck-tools.js +1075 -0
  96. package/package.json +10 -8
  97. package/bin/guardrail.js +0 -834
  98. package/bin/runners/runAudit.js +0 -2
  99. package/bin/runners/runAutopilot.js +0 -2
  100. package/bin/runners/runCertify.js +0 -2
  101. package/bin/runners/runDashboard.js +0 -10
  102. package/bin/runners/runEnhancedShip.js +0 -2
  103. package/bin/runners/runFixPacks.js +0 -2
  104. package/bin/runners/runNaturalLanguage.js +0 -3
  105. package/bin/runners/runProof.js +0 -2
  106. package/bin/runners/runRealitySniff.js +0 -2
  107. package/bin/runners/runUpgrade.js +0 -2
  108. package/bin/runners/runVerifyAgentOutput.js +0 -2
@@ -0,0 +1,703 @@
1
+ /**
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_*)
16
+ */
17
+
18
+ "use strict";
19
+
20
+ const fs = require("fs");
21
+ const path = require("path");
22
+ const crypto = require("crypto");
23
+ const { createFindingV2, createEvidence, generateFingerprint } = require("./schema-validator");
24
+
25
+ // =============================================================================
26
+ // B1) Routes Detectors
27
+ // =============================================================================
28
+
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
+ }));
70
+ }
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
+ }));
121
+ }
122
+ }
123
+
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++;
149
+ }
150
+ }
151
+
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
+ }));
174
+ }
175
+
176
+ return findings;
177
+ }
178
+
179
+ // =============================================================================
180
+ // B2) Auth Coverage Detectors
181
+ // =============================================================================
182
+
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
+ }
235
+ }
236
+ }
237
+
238
+ return findings;
239
+ }
240
+
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
+ }));
280
+ }
281
+ }
282
+
283
+ return findings;
284
+ }
285
+
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
+ }
321
+
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
+ }));
345
+ }
346
+ }
347
+
348
+ return findings;
349
+ }
350
+
351
+ // =============================================================================
352
+ // B3) Env Detectors
353
+ // =============================================================================
354
+
355
+ /**
356
+ * D_ENV_USED_BUT_UNDECLARED (WARN→BLOCK if required)
357
+ * Trigger: truthpack envUsage includes FOO but contracts/env.json does not
358
+ */
359
+ function detectEnvUsedButUndeclared(truthpack, envContract) {
360
+ const findings = [];
361
+ const envUsage = truthpack.envUsage || [];
362
+ const declaredVars = new Set(envContract?.vars?.map(v => v.name) || []);
363
+
364
+ for (const usage of envUsage) {
365
+ if (!declaredVars.has(usage.name)) {
366
+ const isRequired = usage.inferredRequiredness === "required" || isLikelyRequired(usage.name);
367
+
368
+ findings.push(createFindingV2({
369
+ detectorId: "ENV_USED_BUT_UNDECLARED",
370
+ severity: isRequired ? "BLOCK" : "WARN",
371
+ category: "Env",
372
+ scope: "server",
373
+ title: `Env var ${usage.name} used but not declared in contract`,
374
+ why: isRequired
375
+ ? "Required env var missing from contract. Deployment will fail if not set."
376
+ : "Env var used but not documented. AI won't know about this dependency.",
377
+ confidence: isRequired ? "high" : "medium",
378
+ evidence: usage.locations?.slice(0, 3).map(loc => createEvidence({
379
+ kind: "file",
380
+ reason: `Usage of ${usage.name}`,
381
+ file: loc.file,
382
+ lines: loc.lines,
383
+ snippet: loc.snippetHash,
384
+ })) || [],
385
+ fixHints: [
386
+ "Add to .env.example with appropriate default/placeholder",
387
+ "Run 'vibecheck ctx sync' to update env contract",
388
+ isRequired ? "Ensure this var is set in all environments" : null
389
+ ].filter(Boolean),
390
+ }));
391
+ }
392
+ }
393
+
394
+ return findings;
395
+ }
396
+
397
+ // =============================================================================
398
+ // B4) Fake Success / Truth Detectors
399
+ // =============================================================================
400
+
401
+ /**
402
+ * D_FAKE_SUCCESS_TOAST_BEFORE_AWAIT (BLOCK/WARN)
403
+ * Trigger: toast.success before awaited network call
404
+ */
405
+ function detectFakeSuccessToastBeforeAwait(projectPath) {
406
+ const findings = [];
407
+ // This requires AST analysis - see existing findFakeSuccess in analyzers.js
408
+ // Placeholder for integration
409
+ return findings;
410
+ }
411
+
412
+ /**
413
+ * D_FAKE_SUCCESS_RESPONSE_OK_IGNORED (BLOCK)
414
+ * Trigger: fetch result not checked before success UI
415
+ */
416
+ function detectFakeSuccessResponseIgnored(projectPath) {
417
+ const findings = [];
418
+ // This requires AST analysis
419
+ return findings;
420
+ }
421
+
422
+ /**
423
+ * D_SILENT_CATCH (WARN→BLOCK)
424
+ * Trigger: catch (e) {} OR catch { return null } + UI success continues
425
+ */
426
+ function detectSilentCatch(projectPath) {
427
+ const findings = [];
428
+ // This requires AST analysis
429
+ return findings;
430
+ }
431
+
432
+ // =============================================================================
433
+ // B5) Dead UI Detectors (runtime-first)
434
+ // =============================================================================
435
+
436
+ /**
437
+ * D_DEAD_CLICK_NO_EFFECT (BLOCK/WARN)
438
+ * Trigger: action click yields no navigation, no DOM change, no network, no console
439
+ */
440
+ function detectDeadClickNoEffect(realityReport) {
441
+ const findings = [];
442
+ const actions = realityReport?.actions || [];
443
+
444
+ for (const action of actions) {
445
+ if (action.type !== "click") continue;
446
+
447
+ const noNavigation = action.pageUrl === action.afterPageUrl;
448
+ const noDomChange = action.beforeDomHash === action.afterDomHash;
449
+ const noNetwork = !action.networkRequestIds || action.networkRequestIds.length === 0;
450
+
451
+ if (noNavigation && noDomChange && noNetwork) {
452
+ const isCritical = /save|submit|pay|login|continue|checkout/i.test(action.label || "");
453
+
454
+ findings.push(createFindingV2({
455
+ detectorId: "DEAD_CLICK_NO_EFFECT",
456
+ severity: isCritical ? "BLOCK" : "WARN",
457
+ category: "DeadUI",
458
+ scope: "runtime",
459
+ title: `Click on "${action.label || action.selector}" has no effect`,
460
+ why: "Button/link does nothing - no navigation, no DOM change, no network call. User will think app is broken.",
461
+ confidence: "high",
462
+ evidence: [
463
+ createEvidence({
464
+ kind: "screenshot",
465
+ reason: "Screenshot before click",
466
+ artifactPath: action.screenshotBefore,
467
+ }),
468
+ createEvidence({
469
+ kind: "screenshot",
470
+ reason: "Screenshot after click (unchanged)",
471
+ artifactPath: action.screenshotAfter,
472
+ })
473
+ ].filter(e => e.artifactPath),
474
+ fixHints: [
475
+ "Wire click handler to actual functionality",
476
+ "If disabled, add aria-disabled and disabled styling",
477
+ "If feature not ready, remove or hide the element"
478
+ ],
479
+ repro: {
480
+ steps: [
481
+ `Navigate to ${action.pageUrl}`,
482
+ `Click on element: ${action.selector || action.label}`,
483
+ "Observe: nothing happens"
484
+ ],
485
+ url: action.pageUrl,
486
+ },
487
+ }));
488
+ }
489
+ }
490
+
491
+ return findings;
492
+ }
493
+
494
+ /**
495
+ * D_UI_ACTION_CAUSES_4XX_5XX (BLOCK)
496
+ * Trigger: click leads to request with 4xx/5xx
497
+ */
498
+ function detectUIActionCauses4xx5xx(realityReport) {
499
+ const findings = [];
500
+ const actions = realityReport?.actions || [];
501
+ const network = realityReport?.network || [];
502
+
503
+ for (const action of actions) {
504
+ if (!action.networkRequestIds) continue;
505
+
506
+ for (const reqId of action.networkRequestIds) {
507
+ const req = network.find(r => r.id === reqId);
508
+ if (req && (req.status >= 400 || req.failed)) {
509
+ findings.push(createFindingV2({
510
+ detectorId: "UI_ACTION_CAUSES_4XX_5XX",
511
+ severity: "BLOCK",
512
+ category: "DeadUI",
513
+ scope: "runtime",
514
+ title: `Click on "${action.label || action.selector}" causes ${req.status} error`,
515
+ why: `UI action triggers failed API call (${req.status}). User will see broken functionality.`,
516
+ confidence: "high",
517
+ evidence: [
518
+ createEvidence({
519
+ kind: "request",
520
+ reason: `Failed request triggered by UI action`,
521
+ url: req.url,
522
+ httpStatus: req.status,
523
+ requestId: req.id,
524
+ })
525
+ ],
526
+ fixHints: [
527
+ "Fix the API endpoint to return success",
528
+ "Add proper error handling in UI",
529
+ "If endpoint doesn't exist, create it"
530
+ ],
531
+ repro: {
532
+ steps: [
533
+ `Navigate to ${action.pageUrl}`,
534
+ `Click on element: ${action.selector || action.label}`,
535
+ `Observe: API returns ${req.status}`
536
+ ],
537
+ url: action.pageUrl,
538
+ },
539
+ }));
540
+ }
541
+ }
542
+ }
543
+
544
+ return findings;
545
+ }
546
+
547
+ // =============================================================================
548
+ // B6) Billing/Stripe Detectors
549
+ // =============================================================================
550
+
551
+ /**
552
+ * D_STRIPE_WEBHOOK_NO_SIGNATURE_VERIFY (BLOCK)
553
+ * Trigger: webhook route handler lacks signature verification
554
+ */
555
+ function detectStripeWebhookNoSigVerify(truthpack) {
556
+ const findings = [];
557
+ const webhookRoutes = truthpack.externals?.stripe?.webhookRoutes || [];
558
+
559
+ // This requires checking if routes have proper verification
560
+ // Placeholder - actual implementation would scan the handler files
561
+
562
+ return findings;
563
+ }
564
+
565
+ // =============================================================================
566
+ // B7) Entitlements Detectors
567
+ // =============================================================================
568
+
569
+ /**
570
+ * D_LOCAL_BYPASS_PAID_FEATURE (BLOCK)
571
+ * Trigger: code checks env var like OWNER_MODE=true to unlock features
572
+ */
573
+ function detectLocalBypassPaidFeature(projectPath) {
574
+ const findings = [];
575
+ // This requires scanning for patterns like OWNER_MODE, SKIP_AUTH, etc.
576
+ return findings;
577
+ }
578
+
579
+ // =============================================================================
580
+ // B8) Drift Detectors
581
+ // =============================================================================
582
+
583
+ /**
584
+ * D_CONTRACTS_OUT_OF_DATE (WARN/BLOCK)
585
+ * Trigger: truthpack fingerprint changed but contracts still reflect old fingerprint
586
+ */
587
+ function detectContractsOutOfDate(truthpack, contracts) {
588
+ const findings = [];
589
+ const truthpackFingerprint = truthpack.fingerprint;
590
+
591
+ for (const [type, contract] of Object.entries(contracts)) {
592
+ if (contract.projectFingerprint && contract.projectFingerprint !== truthpackFingerprint) {
593
+ findings.push(createFindingV2({
594
+ detectorId: "CONTRACTS_OUT_OF_DATE",
595
+ severity: type === "routes" || type === "auth" ? "BLOCK" : "WARN",
596
+ category: "Drift",
597
+ scope: "contracts",
598
+ title: `${type} contract out of date (fingerprint mismatch)`,
599
+ why: `Contract was generated from old codebase. AI agents will use stale information.`,
600
+ confidence: "high",
601
+ evidence: [
602
+ createEvidence({
603
+ kind: "hash",
604
+ reason: "Contract fingerprint",
605
+ file: `.vibecheck/contracts/${type}.json`,
606
+ lines: "1-1",
607
+ })
608
+ ],
609
+ fixHints: [
610
+ "Run 'vibecheck ctx sync' to regenerate contracts"
611
+ ],
612
+ }));
613
+ }
614
+ }
615
+
616
+ return findings;
617
+ }
618
+
619
+ // =============================================================================
620
+ // Helpers
621
+ // =============================================================================
622
+
623
+ function routeMatches(pattern, actual) {
624
+ const patternParts = pattern.split("/").filter(Boolean);
625
+ const actualParts = actual.split("/").filter(Boolean);
626
+
627
+ if (patternParts.length !== actualParts.length) return false;
628
+
629
+ for (let i = 0; i < patternParts.length; i++) {
630
+ const p = patternParts[i];
631
+ if (p.startsWith(":") || p.startsWith("[") || p === "*") continue;
632
+ if (p !== actualParts[i]) return false;
633
+ }
634
+ return true;
635
+ }
636
+
637
+ function matchPattern(pattern, url) {
638
+ // Convert glob-like pattern to regex
639
+ const regex = new RegExp(
640
+ "^" + pattern
641
+ .replace(/\*/g, ".*")
642
+ .replace(/\//g, "\\/")
643
+ + "$"
644
+ );
645
+ return regex.test(url) || regex.test(new URL(url, "http://localhost").pathname);
646
+ }
647
+
648
+ function isLikelyRequired(name) {
649
+ const requiredPatterns = [
650
+ /^DATABASE_URL$/i,
651
+ /^NEXTAUTH_SECRET$/i,
652
+ /^NEXTAUTH_URL$/i,
653
+ /^JWT_SECRET$/i,
654
+ /^STRIPE_SECRET_KEY$/i,
655
+ /^STRIPE_WEBHOOK_SECRET$/i,
656
+ /SECRET/i,
657
+ /TOKEN$/i,
658
+ /API_KEY$/i,
659
+ ];
660
+ return requiredPatterns.some(p => p.test(name));
661
+ }
662
+
663
+ // =============================================================================
664
+ // Exports
665
+ // =============================================================================
666
+
667
+ module.exports = {
668
+ // Routes
669
+ detectRouteMissing,
670
+ detectRouteMethodMismatch,
671
+ detectRoutePrefixDrift,
672
+
673
+ // Auth
674
+ detectAuthProtectedAccessibleAnon,
675
+ detectAuthProtectedBlockedWhenAuthed,
676
+ detectAuthContractDrift,
677
+
678
+ // Env
679
+ detectEnvUsedButUndeclared,
680
+
681
+ // Fake Success
682
+ detectFakeSuccessToastBeforeAwait,
683
+ detectFakeSuccessResponseIgnored,
684
+ detectSilentCatch,
685
+
686
+ // Dead UI
687
+ detectDeadClickNoEffect,
688
+ detectUIActionCauses4xx5xx,
689
+
690
+ // Billing
691
+ detectStripeWebhookNoSigVerify,
692
+
693
+ // Entitlements
694
+ detectLocalBypassPaidFeature,
695
+
696
+ // Drift
697
+ detectContractsOutOfDate,
698
+
699
+ // Helpers
700
+ routeMatches,
701
+ matchPattern,
702
+ isLikelyRequired,
703
+ };