@vibecheckai/cli 3.0.10 → 3.1.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 (162) hide show
  1. package/bin/.generated +25 -0
  2. package/bin/dev/run-v2-torture.js +30 -30
  3. package/bin/registry.js +105 -0
  4. package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
  5. package/bin/runners/lib/analysis-core.js +271 -271
  6. package/bin/runners/lib/analyzers.js +579 -579
  7. package/bin/runners/lib/auth-truth.js +193 -193
  8. package/bin/runners/lib/backup.js +62 -62
  9. package/bin/runners/lib/billing.js +107 -107
  10. package/bin/runners/lib/claims.js +118 -118
  11. package/bin/runners/lib/cli-output.js +368 -0
  12. package/bin/runners/lib/cli-ui.js +540 -540
  13. package/bin/runners/lib/contracts/auth-contract.js +202 -202
  14. package/bin/runners/lib/contracts/env-contract.js +181 -181
  15. package/bin/runners/lib/contracts/external-contract.js +206 -206
  16. package/bin/runners/lib/contracts/guard.js +168 -168
  17. package/bin/runners/lib/contracts/index.js +89 -89
  18. package/bin/runners/lib/contracts/plan-validator.js +311 -311
  19. package/bin/runners/lib/contracts/route-contract.js +199 -199
  20. package/bin/runners/lib/contracts.js +804 -804
  21. package/bin/runners/lib/detect.js +89 -89
  22. package/bin/runners/lib/detectors-v2.js +703 -703
  23. package/bin/runners/lib/doctor/autofix.js +254 -254
  24. package/bin/runners/lib/doctor/index.js +37 -37
  25. package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
  26. package/bin/runners/lib/doctor/modules/index.js +46 -46
  27. package/bin/runners/lib/doctor/modules/network.js +250 -250
  28. package/bin/runners/lib/doctor/modules/project.js +312 -312
  29. package/bin/runners/lib/doctor/modules/runtime.js +224 -224
  30. package/bin/runners/lib/doctor/modules/security.js +348 -348
  31. package/bin/runners/lib/doctor/modules/system.js +213 -213
  32. package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
  33. package/bin/runners/lib/doctor/reporter.js +262 -262
  34. package/bin/runners/lib/doctor/service.js +262 -262
  35. package/bin/runners/lib/doctor/types.js +113 -113
  36. package/bin/runners/lib/doctor/ui.js +263 -263
  37. package/bin/runners/lib/doctor-v2.js +608 -608
  38. package/bin/runners/lib/drift.js +425 -425
  39. package/bin/runners/lib/enforcement.js +72 -72
  40. package/bin/runners/lib/enterprise-detect.js +603 -603
  41. package/bin/runners/lib/enterprise-init.js +942 -942
  42. package/bin/runners/lib/entitlements-v2.js +490 -493
  43. package/bin/runners/lib/env-resolver.js +417 -417
  44. package/bin/runners/lib/env-template.js +66 -66
  45. package/bin/runners/lib/env.js +189 -189
  46. package/bin/runners/lib/extractors/client-calls.js +990 -990
  47. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
  48. package/bin/runners/lib/extractors/fastify-routes.js +426 -426
  49. package/bin/runners/lib/extractors/index.js +363 -363
  50. package/bin/runners/lib/extractors/next-routes.js +524 -524
  51. package/bin/runners/lib/extractors/proof-graph.js +431 -431
  52. package/bin/runners/lib/extractors/route-matcher.js +451 -451
  53. package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
  54. package/bin/runners/lib/extractors/ui-bindings.js +547 -547
  55. package/bin/runners/lib/findings-schema.js +281 -281
  56. package/bin/runners/lib/firewall-prompt.js +50 -50
  57. package/bin/runners/lib/graph/graph-builder.js +265 -265
  58. package/bin/runners/lib/graph/html-renderer.js +413 -413
  59. package/bin/runners/lib/graph/index.js +32 -32
  60. package/bin/runners/lib/graph/runtime-collector.js +215 -215
  61. package/bin/runners/lib/graph/static-extractor.js +518 -518
  62. package/bin/runners/lib/html-report.js +650 -650
  63. package/bin/runners/lib/init-wizard.js +308 -308
  64. package/bin/runners/lib/llm.js +75 -75
  65. package/bin/runners/lib/meter.js +61 -61
  66. package/bin/runners/lib/missions/evidence.js +126 -126
  67. package/bin/runners/lib/missions/plan.js +69 -69
  68. package/bin/runners/lib/missions/templates.js +192 -192
  69. package/bin/runners/lib/patch.js +40 -40
  70. package/bin/runners/lib/permissions/auth-model.js +213 -213
  71. package/bin/runners/lib/permissions/idor-prover.js +205 -205
  72. package/bin/runners/lib/permissions/index.js +45 -45
  73. package/bin/runners/lib/permissions/matrix-builder.js +198 -198
  74. package/bin/runners/lib/pkgjson.js +28 -28
  75. package/bin/runners/lib/policy.js +295 -295
  76. package/bin/runners/lib/preflight.js +142 -142
  77. package/bin/runners/lib/reality/correlation-detectors.js +359 -359
  78. package/bin/runners/lib/reality/index.js +318 -318
  79. package/bin/runners/lib/reality/request-hashing.js +416 -416
  80. package/bin/runners/lib/reality/request-mapper.js +453 -453
  81. package/bin/runners/lib/reality/safety-rails.js +463 -463
  82. package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
  83. package/bin/runners/lib/reality/toast-detector.js +393 -393
  84. package/bin/runners/lib/reality-findings.js +84 -84
  85. package/bin/runners/lib/receipts.js +179 -0
  86. package/bin/runners/lib/redact.js +29 -29
  87. package/bin/runners/lib/replay/capsule-manager.js +154 -154
  88. package/bin/runners/lib/replay/index.js +263 -263
  89. package/bin/runners/lib/replay/player.js +348 -348
  90. package/bin/runners/lib/replay/recorder.js +331 -331
  91. package/bin/runners/lib/report-engine.js +447 -447
  92. package/bin/runners/lib/report-html.js +1499 -1499
  93. package/bin/runners/lib/report-templates.js +969 -969
  94. package/bin/runners/lib/report.js +135 -135
  95. package/bin/runners/lib/route-detection.js +1140 -1140
  96. package/bin/runners/lib/route-truth.js +477 -477
  97. package/bin/runners/lib/sandbox/index.js +59 -59
  98. package/bin/runners/lib/sandbox/proof-chain.js +399 -399
  99. package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
  100. package/bin/runners/lib/sandbox/worktree.js +174 -174
  101. package/bin/runners/lib/schema-validator.js +350 -350
  102. package/bin/runners/lib/schemas/contracts.schema.json +160 -160
  103. package/bin/runners/lib/schemas/finding.schema.json +100 -100
  104. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
  105. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
  106. package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
  107. package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
  108. package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
  109. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
  110. package/bin/runners/lib/schemas/validator.js +438 -438
  111. package/bin/runners/lib/score-history.js +282 -282
  112. package/bin/runners/lib/share-pack.js +239 -239
  113. package/bin/runners/lib/snippets.js +67 -67
  114. package/bin/runners/lib/truth.js +667 -667
  115. package/bin/runners/lib/upsell.js +510 -0
  116. package/bin/runners/lib/usage.js +153 -0
  117. package/bin/runners/lib/validate-patch.js +156 -156
  118. package/bin/runners/lib/verdict-engine.js +628 -628
  119. package/bin/runners/reality/engine.js +917 -917
  120. package/bin/runners/reality/flows.js +122 -122
  121. package/bin/runners/reality/report.js +378 -378
  122. package/bin/runners/reality/session.js +193 -193
  123. package/bin/runners/runAuth.js +51 -0
  124. package/bin/runners/runBadge.js +31 -4
  125. package/bin/runners/runClaimVerifier.js +483 -483
  126. package/bin/runners/runContext.js +56 -56
  127. package/bin/runners/runContextCompiler.js +385 -385
  128. package/bin/runners/runCtx.js +674 -674
  129. package/bin/runners/runCtxDiff.js +301 -301
  130. package/bin/runners/runCtxGuard.js +176 -176
  131. package/bin/runners/runCtxSync.js +116 -116
  132. package/bin/runners/runDoctor.js +72 -3
  133. package/bin/runners/runFix.js +13 -0
  134. package/bin/runners/runGate.js +17 -17
  135. package/bin/runners/runGraph.js +454 -440
  136. package/bin/runners/runGuard.js +168 -168
  137. package/bin/runners/runInitGha.js +164 -164
  138. package/bin/runners/runInstall.js +277 -277
  139. package/bin/runners/runInteractive.js +388 -388
  140. package/bin/runners/runLabs.js +340 -340
  141. package/bin/runners/runMcp.js +865 -42
  142. package/bin/runners/runMissionGenerator.js +282 -282
  143. package/bin/runners/runPR.js +255 -255
  144. package/bin/runners/runPermissions.js +304 -290
  145. package/bin/runners/runPreflight.js +580 -0
  146. package/bin/runners/runProve.js +1252 -1193
  147. package/bin/runners/runReality.js +1328 -1328
  148. package/bin/runners/runReplay.js +499 -499
  149. package/bin/runners/runReport.js +584 -584
  150. package/bin/runners/runShare.js +212 -212
  151. package/bin/runners/runShip.js +98 -19
  152. package/bin/runners/runStatus.js +138 -138
  153. package/bin/runners/runTruthpack.js +636 -636
  154. package/bin/runners/runVerify.js +272 -0
  155. package/bin/runners/runWatch.js +407 -407
  156. package/bin/vibecheck.js +110 -95
  157. package/mcp-server/consolidated-tools.js +804 -804
  158. package/mcp-server/tools/index.js +72 -72
  159. package/mcp-server/truth-context.js +581 -581
  160. package/mcp-server/truth-firewall-tools.js +1500 -1500
  161. package/package.json +1 -1
  162. package/bin/runners/runProof.zip +0 -0
@@ -1,425 +1,425 @@
1
- /**
2
- * Contract Drift Detection
3
- *
4
- * Detects when code has drifted from contracts (routes/env/auth/external).
5
- * Per spec: "routes/env/auth drift → usually BLOCK (because AI will lie here)"
6
- */
7
-
8
- "use strict";
9
-
10
- const path = require("path");
11
- const fs = require("fs");
12
- const crypto = require("crypto");
13
- const { createDriftFinding, migrateFindingToV2 } = require("./findings-schema");
14
-
15
- /**
16
- * Generate a stable fingerprint for a finding (for dedupe across runs)
17
- */
18
- function fingerprint(type, ...parts) {
19
- const data = [type, ...parts].join("|");
20
- return crypto.createHash("sha256").update(data).digest("hex").slice(0, 12);
21
- }
22
-
23
- /**
24
- * Find drift between contracts and current truthpack
25
- * Returns findings array with BLOCK/WARN severity based on category
26
- */
27
- function findContractDrift(contracts, truthpack, opts = {}) {
28
- const findings = [];
29
-
30
- // Route drift - BLOCK severity (AI can invent fake endpoints)
31
- if (contracts?.routes && truthpack?.routes) {
32
- const routeFindings = detectRouteDrift(contracts.routes, truthpack);
33
- findings.push(...routeFindings);
34
- }
35
-
36
- // Env drift - BLOCK for required, WARN for optional
37
- if (contracts?.env && truthpack?.env) {
38
- const envFindings = detectEnvDrift(contracts.env, truthpack);
39
- findings.push(...envFindings);
40
- }
41
-
42
- // Auth drift - BLOCK severity (security critical)
43
- if (contracts?.auth && truthpack?.auth) {
44
- const authFindings = detectAuthDrift(contracts.auth, truthpack);
45
- findings.push(...authFindings);
46
- }
47
-
48
- // External drift - WARN severity (service changes)
49
- if (contracts?.external) {
50
- const externalFindings = detectExternalDrift(contracts.external, truthpack);
51
- findings.push(...externalFindings);
52
- }
53
-
54
- return findings;
55
- }
56
-
57
- /**
58
- * Detect route drift: new routes not in contract, removed routes still in contract
59
- */
60
- function detectRouteDrift(routeContract, truthpack) {
61
- const findings = [];
62
- const contractRoutes = new Map();
63
-
64
- for (const r of routeContract.routes || []) {
65
- const key = `${r.method}_${r.path}`;
66
- contractRoutes.set(key, r);
67
- }
68
-
69
- const serverRoutes = truthpack?.routes?.server || [];
70
- const currentRoutes = new Set();
71
-
72
- // Check for new routes not in contract
73
- for (const route of serverRoutes) {
74
- const key = `${route.method}_${route.path}`;
75
- currentRoutes.add(key);
76
-
77
- if (!contractRoutes.has(key)) {
78
- // Check parameterized match
79
- const match = findParameterizedMatch(routeContract.routes, route.method, route.path);
80
- if (!match) {
81
- findings.push({
82
- id: fingerprint("DRIFT_NEW_ROUTE", route.method, route.path),
83
- category: "ContractDrift",
84
- type: "new_route_not_in_contract",
85
- severity: "BLOCK",
86
- scope: "server",
87
- title: `New route ${route.method} ${route.path} not declared in contract`,
88
- message: `Route was added to code but not synced to contracts. AI agents cannot safely reference this route.`,
89
- why: "Contracts are the source of truth. Undeclared routes can cause AI hallucinations.",
90
- evidence: route.evidence || [],
91
- fixHints: [
92
- "Run 'vibecheck ctx sync' to update contracts",
93
- "Or remove the route if unintended"
94
- ],
95
- fingerprint: fingerprint("DRIFT_NEW_ROUTE", route.method, route.path)
96
- });
97
- }
98
- }
99
- }
100
-
101
- // Check for removed routes still in contract
102
- for (const [key, contractRoute] of contractRoutes) {
103
- if (!currentRoutes.has(key)) {
104
- // Check if any current route matches parameterized
105
- const stillExists = serverRoutes.some(r =>
106
- matchesParameterized(contractRoute.path, r.path) &&
107
- (contractRoute.method === "*" || contractRoute.method === r.method)
108
- );
109
-
110
- if (!stillExists) {
111
- findings.push({
112
- id: fingerprint("DRIFT_REMOVED_ROUTE", contractRoute.method, contractRoute.path),
113
- category: "ContractDrift",
114
- type: "route_removed_from_code",
115
- severity: "BLOCK",
116
- scope: "contracts",
117
- title: `Route ${contractRoute.method} ${contractRoute.path} in contract but removed from code`,
118
- message: `Contract declares a route that no longer exists. AI agents will reference a ghost endpoint.`,
119
- why: "Stale contracts cause AI to generate code calling non-existent endpoints.",
120
- evidence: contractRoute.evidence || [],
121
- fixHints: [
122
- "Run 'vibecheck ctx sync' to update contracts",
123
- "Or restore the route if removal was unintended"
124
- ],
125
- fingerprint: fingerprint("DRIFT_REMOVED_ROUTE", contractRoute.method, contractRoute.path)
126
- });
127
- }
128
- }
129
- }
130
-
131
- // Check for client refs not in contract (same as existing analyzer but with drift framing)
132
- const clientRefs = truthpack?.routes?.clientRefs || [];
133
- for (const ref of clientRefs) {
134
- const key = `${ref.method}_${ref.path}`;
135
- if (!contractRoutes.has(key)) {
136
- const match = findParameterizedMatch(routeContract.routes, ref.method, ref.path);
137
- if (!match) {
138
- findings.push({
139
- id: fingerprint("DRIFT_CLIENT_UNDECLARED", ref.method, ref.path),
140
- category: "ContractDrift",
141
- type: "client_refs_undeclared_route",
142
- severity: "BLOCK",
143
- scope: "client",
144
- title: `Client calls ${ref.method} ${ref.path} which is not in contract`,
145
- message: `Frontend references a route not declared in contracts. This will cause runtime errors.`,
146
- why: "Client code should only call routes that exist in the contract.",
147
- evidence: ref.evidence || [],
148
- fixHints: [
149
- "Create the missing route handler",
150
- "Or update the client to use a valid route",
151
- "Then run 'vibecheck ctx sync'"
152
- ],
153
- fingerprint: fingerprint("DRIFT_CLIENT_UNDECLARED", ref.method, ref.path)
154
- });
155
- }
156
- }
157
- }
158
-
159
- return findings;
160
- }
161
-
162
- /**
163
- * Detect env drift: new vars not in contract, required vars removed
164
- */
165
- function detectEnvDrift(envContract, truthpack) {
166
- const findings = [];
167
- const contractVars = new Map();
168
-
169
- for (const v of envContract.vars || []) {
170
- contractVars.set(v.name, v);
171
- }
172
-
173
- const usedVars = truthpack?.env?.vars || [];
174
- const currentVarNames = new Set(usedVars.map(v => v.name));
175
-
176
- // Check for new env vars used but not in contract
177
- for (const usedVar of usedVars) {
178
- if (!contractVars.has(usedVar.name)) {
179
- const isLikelyRequired = isRequiredEnvVar(usedVar.name);
180
- findings.push({
181
- id: fingerprint("DRIFT_NEW_ENV", usedVar.name),
182
- category: "ContractDrift",
183
- type: "new_env_not_in_contract",
184
- severity: isLikelyRequired ? "BLOCK" : "WARN",
185
- scope: "server",
186
- title: `Env var ${usedVar.name} used but not in contract`,
187
- message: `Code uses ${usedVar.name} but it's not declared in env contract.`,
188
- why: "Undeclared env vars can cause deployment failures and AI confusion.",
189
- evidence: usedVar.references || [],
190
- fixHints: [
191
- "Run 'vibecheck ctx sync' to update contracts",
192
- `Add ${usedVar.name} to .env.example`
193
- ],
194
- fingerprint: fingerprint("DRIFT_NEW_ENV", usedVar.name)
195
- });
196
- }
197
- }
198
-
199
- // Check for required contract vars no longer used
200
- for (const [name, spec] of contractVars) {
201
- if (spec.required && !currentVarNames.has(name)) {
202
- findings.push({
203
- id: fingerprint("DRIFT_REMOVED_ENV", name),
204
- category: "ContractDrift",
205
- type: "required_env_removed",
206
- severity: "WARN",
207
- scope: "contracts",
208
- title: `Required env var ${name} in contract but not used in code`,
209
- message: `Contract marks ${name} as required but code no longer uses it.`,
210
- why: "Stale env contracts cause confusion about what's actually needed.",
211
- evidence: [],
212
- fixHints: [
213
- "Run 'vibecheck ctx sync' to update contracts",
214
- "Or check if the var was accidentally removed from code"
215
- ],
216
- fingerprint: fingerprint("DRIFT_REMOVED_ENV", name)
217
- });
218
- }
219
- }
220
-
221
- return findings;
222
- }
223
-
224
- /**
225
- * Detect auth drift: protected patterns changed
226
- */
227
- function detectAuthDrift(authContract, truthpack) {
228
- const findings = [];
229
- const contractPatterns = new Set(authContract.protectedPatterns || []);
230
- const currentPatterns = new Set(truthpack?.auth?.nextMatcherPatterns || []);
231
-
232
- // New protected patterns not in contract
233
- for (const pattern of currentPatterns) {
234
- if (!contractPatterns.has(pattern)) {
235
- findings.push({
236
- id: fingerprint("DRIFT_NEW_AUTH", pattern),
237
- category: "ContractDrift",
238
- type: "new_auth_pattern",
239
- severity: "BLOCK",
240
- scope: "server",
241
- title: `New auth pattern "${pattern}" not in contract`,
242
- message: `Middleware protects a new pattern that isn't in the auth contract.`,
243
- why: "Auth contracts define security boundaries. Undeclared patterns may not be properly tested.",
244
- evidence: [],
245
- fixHints: [
246
- "Run 'vibecheck ctx sync' to update contracts",
247
- "Verify the new pattern is intentional"
248
- ],
249
- fingerprint: fingerprint("DRIFT_NEW_AUTH", pattern)
250
- });
251
- }
252
- }
253
-
254
- // Protected patterns in contract but removed from code
255
- for (const pattern of contractPatterns) {
256
- if (!currentPatterns.has(pattern)) {
257
- findings.push({
258
- id: fingerprint("DRIFT_REMOVED_AUTH", pattern),
259
- category: "ContractDrift",
260
- type: "auth_pattern_removed",
261
- severity: "BLOCK",
262
- scope: "contracts",
263
- title: `Auth pattern "${pattern}" in contract but removed from middleware`,
264
- message: `Contract expects protection for "${pattern}" but middleware no longer enforces it.`,
265
- why: "Removing auth patterns can expose protected routes. This is a security regression.",
266
- evidence: [],
267
- fixHints: [
268
- "Restore the auth pattern in middleware if removal was unintended",
269
- "Or run 'vibecheck ctx sync' to accept the change"
270
- ],
271
- fingerprint: fingerprint("DRIFT_REMOVED_AUTH", pattern)
272
- });
273
- }
274
- }
275
-
276
- return findings;
277
- }
278
-
279
- /**
280
- * Detect external service drift
281
- */
282
- function detectExternalDrift(externalContract, truthpack) {
283
- const findings = [];
284
- const contractServices = new Map();
285
-
286
- for (const svc of externalContract.services || []) {
287
- contractServices.set(svc.name, svc);
288
- }
289
-
290
- // Check Stripe webhook changes
291
- const billing = truthpack?.billing || {};
292
- if (contractServices.has("stripe")) {
293
- const contractStripe = contractServices.get("stripe");
294
- const currentWebhooks = billing.webhookCandidates || [];
295
-
296
- // New webhook not in contract
297
- for (const wh of currentWebhooks) {
298
- const inContract = contractStripe.webhooks?.some(cw => cw.path === wh.path);
299
- if (!inContract) {
300
- findings.push({
301
- id: fingerprint("DRIFT_NEW_WEBHOOK", wh.path),
302
- category: "ContractDrift",
303
- type: "new_webhook_not_in_contract",
304
- severity: "WARN",
305
- scope: "server",
306
- title: `New Stripe webhook at ${wh.path} not in contract`,
307
- message: `A webhook handler was added but not declared in external contract.`,
308
- why: "Webhook contracts ensure proper signature verification and idempotency.",
309
- evidence: wh.evidence || [],
310
- fixHints: [
311
- "Run 'vibecheck ctx sync' to update contracts"
312
- ],
313
- fingerprint: fingerprint("DRIFT_NEW_WEBHOOK", wh.path)
314
- });
315
- }
316
- }
317
- }
318
-
319
- return findings;
320
- }
321
-
322
- function findParameterizedMatch(routes, method, pathToMatch) {
323
- for (const r of routes || []) {
324
- if (r.method !== "*" && r.method !== method) continue;
325
- if (matchesParameterized(r.path, pathToMatch)) return r;
326
- }
327
- return null;
328
- }
329
-
330
- function matchesParameterized(pattern, actual) {
331
- const patternParts = pattern.split("/").filter(Boolean);
332
- const actualParts = actual.split("/").filter(Boolean);
333
-
334
- if (patternParts.length !== actualParts.length) return false;
335
-
336
- for (let i = 0; i < patternParts.length; i++) {
337
- const p = patternParts[i];
338
- if (p.startsWith(":") || p.startsWith("*") || p.startsWith("[")) continue;
339
- if (p !== actualParts[i]) return false;
340
- }
341
- return true;
342
- }
343
-
344
- function isRequiredEnvVar(name) {
345
- const requiredPatterns = [
346
- /^DATABASE_URL$/i,
347
- /^NEXTAUTH_SECRET$/i,
348
- /^NEXTAUTH_URL$/i,
349
- /^JWT_SECRET$/i,
350
- /^STRIPE_SECRET_KEY$/i,
351
- /^STRIPE_WEBHOOK_SECRET$/i,
352
- /^API_KEY$/i,
353
- /SECRET/i,
354
- /TOKEN$/i,
355
- ];
356
- return requiredPatterns.some(p => p.test(name));
357
- }
358
-
359
- /**
360
- * Load contracts from disk
361
- */
362
- function loadContracts(repoRoot) {
363
- const contractDir = path.join(repoRoot, ".vibecheck", "contracts");
364
- const contracts = {};
365
-
366
- const files = {
367
- routes: "routes.json",
368
- env: "env.json",
369
- auth: "auth.json",
370
- external: "external.json"
371
- };
372
-
373
- for (const [key, file] of Object.entries(files)) {
374
- const filePath = path.join(contractDir, file);
375
- if (fs.existsSync(filePath)) {
376
- try {
377
- contracts[key] = JSON.parse(fs.readFileSync(filePath, "utf8"));
378
- } catch (e) {
379
- // Silently skip invalid files
380
- }
381
- }
382
- }
383
-
384
- return contracts;
385
- }
386
-
387
- /**
388
- * Check if contracts exist
389
- */
390
- function hasContracts(repoRoot) {
391
- const contractDir = path.join(repoRoot, ".vibecheck", "contracts");
392
- if (!fs.existsSync(contractDir)) return false;
393
-
394
- const files = ["routes.json", "env.json", "auth.json", "external.json"];
395
- return files.some(f => fs.existsSync(path.join(contractDir, f)));
396
- }
397
-
398
- /**
399
- * Get drift summary for quick checks
400
- */
401
- function getDriftSummary(findings) {
402
- const blocks = findings.filter(f => f.severity === "BLOCK");
403
- const warns = findings.filter(f => f.severity === "WARN");
404
-
405
- return {
406
- hasDrift: findings.length > 0,
407
- blocks: blocks.length,
408
- warns: warns.length,
409
- verdict: blocks.length > 0 ? "BLOCK" : warns.length > 0 ? "WARN" : "PASS",
410
- categories: {
411
- routes: findings.filter(f => f.type.includes("route")).length,
412
- env: findings.filter(f => f.type.includes("env")).length,
413
- auth: findings.filter(f => f.type.includes("auth")).length,
414
- external: findings.filter(f => f.type.includes("webhook")).length
415
- }
416
- };
417
- }
418
-
419
- module.exports = {
420
- findContractDrift,
421
- loadContracts,
422
- hasContracts,
423
- getDriftSummary,
424
- fingerprint
425
- };
1
+ /**
2
+ * Contract Drift Detection
3
+ *
4
+ * Detects when code has drifted from contracts (routes/env/auth/external).
5
+ * Per spec: "routes/env/auth drift → usually BLOCK (because AI will lie here)"
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const path = require("path");
11
+ const fs = require("fs");
12
+ const crypto = require("crypto");
13
+ const { createDriftFinding, migrateFindingToV2 } = require("./findings-schema");
14
+
15
+ /**
16
+ * Generate a stable fingerprint for a finding (for dedupe across runs)
17
+ */
18
+ function fingerprint(type, ...parts) {
19
+ const data = [type, ...parts].join("|");
20
+ return crypto.createHash("sha256").update(data).digest("hex").slice(0, 12);
21
+ }
22
+
23
+ /**
24
+ * Find drift between contracts and current truthpack
25
+ * Returns findings array with BLOCK/WARN severity based on category
26
+ */
27
+ function findContractDrift(contracts, truthpack, opts = {}) {
28
+ const findings = [];
29
+
30
+ // Route drift - BLOCK severity (AI can invent fake endpoints)
31
+ if (contracts?.routes && truthpack?.routes) {
32
+ const routeFindings = detectRouteDrift(contracts.routes, truthpack);
33
+ findings.push(...routeFindings);
34
+ }
35
+
36
+ // Env drift - BLOCK for required, WARN for optional
37
+ if (contracts?.env && truthpack?.env) {
38
+ const envFindings = detectEnvDrift(contracts.env, truthpack);
39
+ findings.push(...envFindings);
40
+ }
41
+
42
+ // Auth drift - BLOCK severity (security critical)
43
+ if (contracts?.auth && truthpack?.auth) {
44
+ const authFindings = detectAuthDrift(contracts.auth, truthpack);
45
+ findings.push(...authFindings);
46
+ }
47
+
48
+ // External drift - WARN severity (service changes)
49
+ if (contracts?.external) {
50
+ const externalFindings = detectExternalDrift(contracts.external, truthpack);
51
+ findings.push(...externalFindings);
52
+ }
53
+
54
+ return findings;
55
+ }
56
+
57
+ /**
58
+ * Detect route drift: new routes not in contract, removed routes still in contract
59
+ */
60
+ function detectRouteDrift(routeContract, truthpack) {
61
+ const findings = [];
62
+ const contractRoutes = new Map();
63
+
64
+ for (const r of routeContract.routes || []) {
65
+ const key = `${r.method}_${r.path}`;
66
+ contractRoutes.set(key, r);
67
+ }
68
+
69
+ const serverRoutes = truthpack?.routes?.server || [];
70
+ const currentRoutes = new Set();
71
+
72
+ // Check for new routes not in contract
73
+ for (const route of serverRoutes) {
74
+ const key = `${route.method}_${route.path}`;
75
+ currentRoutes.add(key);
76
+
77
+ if (!contractRoutes.has(key)) {
78
+ // Check parameterized match
79
+ const match = findParameterizedMatch(routeContract.routes, route.method, route.path);
80
+ if (!match) {
81
+ findings.push({
82
+ id: fingerprint("DRIFT_NEW_ROUTE", route.method, route.path),
83
+ category: "ContractDrift",
84
+ type: "new_route_not_in_contract",
85
+ severity: "BLOCK",
86
+ scope: "server",
87
+ title: `New route ${route.method} ${route.path} not declared in contract`,
88
+ message: `Route was added to code but not synced to contracts. AI agents cannot safely reference this route.`,
89
+ why: "Contracts are the source of truth. Undeclared routes can cause AI hallucinations.",
90
+ evidence: route.evidence || [],
91
+ fixHints: [
92
+ "Run 'vibecheck ctx sync' to update contracts",
93
+ "Or remove the route if unintended"
94
+ ],
95
+ fingerprint: fingerprint("DRIFT_NEW_ROUTE", route.method, route.path)
96
+ });
97
+ }
98
+ }
99
+ }
100
+
101
+ // Check for removed routes still in contract
102
+ for (const [key, contractRoute] of contractRoutes) {
103
+ if (!currentRoutes.has(key)) {
104
+ // Check if any current route matches parameterized
105
+ const stillExists = serverRoutes.some(r =>
106
+ matchesParameterized(contractRoute.path, r.path) &&
107
+ (contractRoute.method === "*" || contractRoute.method === r.method)
108
+ );
109
+
110
+ if (!stillExists) {
111
+ findings.push({
112
+ id: fingerprint("DRIFT_REMOVED_ROUTE", contractRoute.method, contractRoute.path),
113
+ category: "ContractDrift",
114
+ type: "route_removed_from_code",
115
+ severity: "BLOCK",
116
+ scope: "contracts",
117
+ title: `Route ${contractRoute.method} ${contractRoute.path} in contract but removed from code`,
118
+ message: `Contract declares a route that no longer exists. AI agents will reference a ghost endpoint.`,
119
+ why: "Stale contracts cause AI to generate code calling non-existent endpoints.",
120
+ evidence: contractRoute.evidence || [],
121
+ fixHints: [
122
+ "Run 'vibecheck ctx sync' to update contracts",
123
+ "Or restore the route if removal was unintended"
124
+ ],
125
+ fingerprint: fingerprint("DRIFT_REMOVED_ROUTE", contractRoute.method, contractRoute.path)
126
+ });
127
+ }
128
+ }
129
+ }
130
+
131
+ // Check for client refs not in contract (same as existing analyzer but with drift framing)
132
+ const clientRefs = truthpack?.routes?.clientRefs || [];
133
+ for (const ref of clientRefs) {
134
+ const key = `${ref.method}_${ref.path}`;
135
+ if (!contractRoutes.has(key)) {
136
+ const match = findParameterizedMatch(routeContract.routes, ref.method, ref.path);
137
+ if (!match) {
138
+ findings.push({
139
+ id: fingerprint("DRIFT_CLIENT_UNDECLARED", ref.method, ref.path),
140
+ category: "ContractDrift",
141
+ type: "client_refs_undeclared_route",
142
+ severity: "BLOCK",
143
+ scope: "client",
144
+ title: `Client calls ${ref.method} ${ref.path} which is not in contract`,
145
+ message: `Frontend references a route not declared in contracts. This will cause runtime errors.`,
146
+ why: "Client code should only call routes that exist in the contract.",
147
+ evidence: ref.evidence || [],
148
+ fixHints: [
149
+ "Create the missing route handler",
150
+ "Or update the client to use a valid route",
151
+ "Then run 'vibecheck ctx sync'"
152
+ ],
153
+ fingerprint: fingerprint("DRIFT_CLIENT_UNDECLARED", ref.method, ref.path)
154
+ });
155
+ }
156
+ }
157
+ }
158
+
159
+ return findings;
160
+ }
161
+
162
+ /**
163
+ * Detect env drift: new vars not in contract, required vars removed
164
+ */
165
+ function detectEnvDrift(envContract, truthpack) {
166
+ const findings = [];
167
+ const contractVars = new Map();
168
+
169
+ for (const v of envContract.vars || []) {
170
+ contractVars.set(v.name, v);
171
+ }
172
+
173
+ const usedVars = truthpack?.env?.vars || [];
174
+ const currentVarNames = new Set(usedVars.map(v => v.name));
175
+
176
+ // Check for new env vars used but not in contract
177
+ for (const usedVar of usedVars) {
178
+ if (!contractVars.has(usedVar.name)) {
179
+ const isLikelyRequired = isRequiredEnvVar(usedVar.name);
180
+ findings.push({
181
+ id: fingerprint("DRIFT_NEW_ENV", usedVar.name),
182
+ category: "ContractDrift",
183
+ type: "new_env_not_in_contract",
184
+ severity: isLikelyRequired ? "BLOCK" : "WARN",
185
+ scope: "server",
186
+ title: `Env var ${usedVar.name} used but not in contract`,
187
+ message: `Code uses ${usedVar.name} but it's not declared in env contract.`,
188
+ why: "Undeclared env vars can cause deployment failures and AI confusion.",
189
+ evidence: usedVar.references || [],
190
+ fixHints: [
191
+ "Run 'vibecheck ctx sync' to update contracts",
192
+ `Add ${usedVar.name} to .env.example`
193
+ ],
194
+ fingerprint: fingerprint("DRIFT_NEW_ENV", usedVar.name)
195
+ });
196
+ }
197
+ }
198
+
199
+ // Check for required contract vars no longer used
200
+ for (const [name, spec] of contractVars) {
201
+ if (spec.required && !currentVarNames.has(name)) {
202
+ findings.push({
203
+ id: fingerprint("DRIFT_REMOVED_ENV", name),
204
+ category: "ContractDrift",
205
+ type: "required_env_removed",
206
+ severity: "WARN",
207
+ scope: "contracts",
208
+ title: `Required env var ${name} in contract but not used in code`,
209
+ message: `Contract marks ${name} as required but code no longer uses it.`,
210
+ why: "Stale env contracts cause confusion about what's actually needed.",
211
+ evidence: [],
212
+ fixHints: [
213
+ "Run 'vibecheck ctx sync' to update contracts",
214
+ "Or check if the var was accidentally removed from code"
215
+ ],
216
+ fingerprint: fingerprint("DRIFT_REMOVED_ENV", name)
217
+ });
218
+ }
219
+ }
220
+
221
+ return findings;
222
+ }
223
+
224
+ /**
225
+ * Detect auth drift: protected patterns changed
226
+ */
227
+ function detectAuthDrift(authContract, truthpack) {
228
+ const findings = [];
229
+ const contractPatterns = new Set(authContract.protectedPatterns || []);
230
+ const currentPatterns = new Set(truthpack?.auth?.nextMatcherPatterns || []);
231
+
232
+ // New protected patterns not in contract
233
+ for (const pattern of currentPatterns) {
234
+ if (!contractPatterns.has(pattern)) {
235
+ findings.push({
236
+ id: fingerprint("DRIFT_NEW_AUTH", pattern),
237
+ category: "ContractDrift",
238
+ type: "new_auth_pattern",
239
+ severity: "BLOCK",
240
+ scope: "server",
241
+ title: `New auth pattern "${pattern}" not in contract`,
242
+ message: `Middleware protects a new pattern that isn't in the auth contract.`,
243
+ why: "Auth contracts define security boundaries. Undeclared patterns may not be properly tested.",
244
+ evidence: [],
245
+ fixHints: [
246
+ "Run 'vibecheck ctx sync' to update contracts",
247
+ "Verify the new pattern is intentional"
248
+ ],
249
+ fingerprint: fingerprint("DRIFT_NEW_AUTH", pattern)
250
+ });
251
+ }
252
+ }
253
+
254
+ // Protected patterns in contract but removed from code
255
+ for (const pattern of contractPatterns) {
256
+ if (!currentPatterns.has(pattern)) {
257
+ findings.push({
258
+ id: fingerprint("DRIFT_REMOVED_AUTH", pattern),
259
+ category: "ContractDrift",
260
+ type: "auth_pattern_removed",
261
+ severity: "BLOCK",
262
+ scope: "contracts",
263
+ title: `Auth pattern "${pattern}" in contract but removed from middleware`,
264
+ message: `Contract expects protection for "${pattern}" but middleware no longer enforces it.`,
265
+ why: "Removing auth patterns can expose protected routes. This is a security regression.",
266
+ evidence: [],
267
+ fixHints: [
268
+ "Restore the auth pattern in middleware if removal was unintended",
269
+ "Or run 'vibecheck ctx sync' to accept the change"
270
+ ],
271
+ fingerprint: fingerprint("DRIFT_REMOVED_AUTH", pattern)
272
+ });
273
+ }
274
+ }
275
+
276
+ return findings;
277
+ }
278
+
279
+ /**
280
+ * Detect external service drift
281
+ */
282
+ function detectExternalDrift(externalContract, truthpack) {
283
+ const findings = [];
284
+ const contractServices = new Map();
285
+
286
+ for (const svc of externalContract.services || []) {
287
+ contractServices.set(svc.name, svc);
288
+ }
289
+
290
+ // Check Stripe webhook changes
291
+ const billing = truthpack?.billing || {};
292
+ if (contractServices.has("stripe")) {
293
+ const contractStripe = contractServices.get("stripe");
294
+ const currentWebhooks = billing.webhookCandidates || [];
295
+
296
+ // New webhook not in contract
297
+ for (const wh of currentWebhooks) {
298
+ const inContract = contractStripe.webhooks?.some(cw => cw.path === wh.path);
299
+ if (!inContract) {
300
+ findings.push({
301
+ id: fingerprint("DRIFT_NEW_WEBHOOK", wh.path),
302
+ category: "ContractDrift",
303
+ type: "new_webhook_not_in_contract",
304
+ severity: "WARN",
305
+ scope: "server",
306
+ title: `New Stripe webhook at ${wh.path} not in contract`,
307
+ message: `A webhook handler was added but not declared in external contract.`,
308
+ why: "Webhook contracts ensure proper signature verification and idempotency.",
309
+ evidence: wh.evidence || [],
310
+ fixHints: [
311
+ "Run 'vibecheck ctx sync' to update contracts"
312
+ ],
313
+ fingerprint: fingerprint("DRIFT_NEW_WEBHOOK", wh.path)
314
+ });
315
+ }
316
+ }
317
+ }
318
+
319
+ return findings;
320
+ }
321
+
322
+ function findParameterizedMatch(routes, method, pathToMatch) {
323
+ for (const r of routes || []) {
324
+ if (r.method !== "*" && r.method !== method) continue;
325
+ if (matchesParameterized(r.path, pathToMatch)) return r;
326
+ }
327
+ return null;
328
+ }
329
+
330
+ function matchesParameterized(pattern, actual) {
331
+ const patternParts = pattern.split("/").filter(Boolean);
332
+ const actualParts = actual.split("/").filter(Boolean);
333
+
334
+ if (patternParts.length !== actualParts.length) return false;
335
+
336
+ for (let i = 0; i < patternParts.length; i++) {
337
+ const p = patternParts[i];
338
+ if (p.startsWith(":") || p.startsWith("*") || p.startsWith("[")) continue;
339
+ if (p !== actualParts[i]) return false;
340
+ }
341
+ return true;
342
+ }
343
+
344
+ function isRequiredEnvVar(name) {
345
+ const requiredPatterns = [
346
+ /^DATABASE_URL$/i,
347
+ /^NEXTAUTH_SECRET$/i,
348
+ /^NEXTAUTH_URL$/i,
349
+ /^JWT_SECRET$/i,
350
+ /^STRIPE_SECRET_KEY$/i,
351
+ /^STRIPE_WEBHOOK_SECRET$/i,
352
+ /^API_KEY$/i,
353
+ /SECRET/i,
354
+ /TOKEN$/i,
355
+ ];
356
+ return requiredPatterns.some(p => p.test(name));
357
+ }
358
+
359
+ /**
360
+ * Load contracts from disk
361
+ */
362
+ function loadContracts(repoRoot) {
363
+ const contractDir = path.join(repoRoot, ".vibecheck", "contracts");
364
+ const contracts = {};
365
+
366
+ const files = {
367
+ routes: "routes.json",
368
+ env: "env.json",
369
+ auth: "auth.json",
370
+ external: "external.json"
371
+ };
372
+
373
+ for (const [key, file] of Object.entries(files)) {
374
+ const filePath = path.join(contractDir, file);
375
+ if (fs.existsSync(filePath)) {
376
+ try {
377
+ contracts[key] = JSON.parse(fs.readFileSync(filePath, "utf8"));
378
+ } catch (e) {
379
+ // Silently skip invalid files
380
+ }
381
+ }
382
+ }
383
+
384
+ return contracts;
385
+ }
386
+
387
+ /**
388
+ * Check if contracts exist
389
+ */
390
+ function hasContracts(repoRoot) {
391
+ const contractDir = path.join(repoRoot, ".vibecheck", "contracts");
392
+ if (!fs.existsSync(contractDir)) return false;
393
+
394
+ const files = ["routes.json", "env.json", "auth.json", "external.json"];
395
+ return files.some(f => fs.existsSync(path.join(contractDir, f)));
396
+ }
397
+
398
+ /**
399
+ * Get drift summary for quick checks
400
+ */
401
+ function getDriftSummary(findings) {
402
+ const blocks = findings.filter(f => f.severity === "BLOCK");
403
+ const warns = findings.filter(f => f.severity === "WARN");
404
+
405
+ return {
406
+ hasDrift: findings.length > 0,
407
+ blocks: blocks.length,
408
+ warns: warns.length,
409
+ verdict: blocks.length > 0 ? "BLOCK" : warns.length > 0 ? "WARN" : "PASS",
410
+ categories: {
411
+ routes: findings.filter(f => f.type.includes("route")).length,
412
+ env: findings.filter(f => f.type.includes("env")).length,
413
+ auth: findings.filter(f => f.type.includes("auth")).length,
414
+ external: findings.filter(f => f.type.includes("webhook")).length
415
+ }
416
+ };
417
+ }
418
+
419
+ module.exports = {
420
+ findContractDrift,
421
+ loadContracts,
422
+ hasContracts,
423
+ getDriftSummary,
424
+ fingerprint
425
+ };