@vibecheckai/cli 3.2.5 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/bin/.generated +25 -25
  2. package/bin/dev/run-v2-torture.js +30 -30
  3. package/bin/registry.js +192 -5
  4. package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
  5. package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
  6. package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
  7. package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
  8. package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
  9. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
  10. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
  11. package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
  12. package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
  13. package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
  14. package/bin/runners/lib/agent-firewall/logger.js +141 -0
  15. package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
  16. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
  17. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
  18. package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
  19. package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
  20. package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
  21. package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
  22. package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
  23. package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
  24. package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
  25. package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
  26. package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
  27. package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
  28. package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
  29. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
  30. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
  31. package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
  32. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
  33. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
  34. package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
  35. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
  36. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
  37. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
  38. package/bin/runners/lib/analyzers.js +81 -18
  39. package/bin/runners/lib/api-client.js +269 -0
  40. package/bin/runners/lib/auth-truth.js +193 -193
  41. package/bin/runners/lib/authority-badge.js +425 -0
  42. package/bin/runners/lib/backup.js +62 -62
  43. package/bin/runners/lib/billing.js +107 -107
  44. package/bin/runners/lib/claims.js +118 -118
  45. package/bin/runners/lib/cli-output.js +7 -1
  46. package/bin/runners/lib/cli-ui.js +540 -540
  47. package/bin/runners/lib/contracts/auth-contract.js +202 -202
  48. package/bin/runners/lib/contracts/env-contract.js +181 -181
  49. package/bin/runners/lib/contracts/external-contract.js +206 -206
  50. package/bin/runners/lib/contracts/guard.js +168 -168
  51. package/bin/runners/lib/contracts/index.js +89 -89
  52. package/bin/runners/lib/contracts/plan-validator.js +311 -311
  53. package/bin/runners/lib/contracts/route-contract.js +199 -199
  54. package/bin/runners/lib/contracts.js +804 -804
  55. package/bin/runners/lib/detect.js +89 -89
  56. package/bin/runners/lib/doctor/autofix.js +254 -254
  57. package/bin/runners/lib/doctor/index.js +37 -37
  58. package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
  59. package/bin/runners/lib/doctor/modules/index.js +46 -46
  60. package/bin/runners/lib/doctor/modules/network.js +250 -250
  61. package/bin/runners/lib/doctor/modules/project.js +312 -312
  62. package/bin/runners/lib/doctor/modules/runtime.js +224 -224
  63. package/bin/runners/lib/doctor/modules/security.js +348 -348
  64. package/bin/runners/lib/doctor/modules/system.js +213 -213
  65. package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
  66. package/bin/runners/lib/doctor/reporter.js +262 -262
  67. package/bin/runners/lib/doctor/service.js +262 -262
  68. package/bin/runners/lib/doctor/types.js +113 -113
  69. package/bin/runners/lib/doctor/ui.js +263 -263
  70. package/bin/runners/lib/doctor-v2.js +608 -608
  71. package/bin/runners/lib/drift.js +425 -425
  72. package/bin/runners/lib/enforcement.js +72 -72
  73. package/bin/runners/lib/enterprise-detect.js +603 -603
  74. package/bin/runners/lib/enterprise-init.js +942 -942
  75. package/bin/runners/lib/env-resolver.js +417 -417
  76. package/bin/runners/lib/env-template.js +66 -66
  77. package/bin/runners/lib/env.js +189 -189
  78. package/bin/runners/lib/error-handler.js +16 -9
  79. package/bin/runners/lib/exit-codes.js +275 -0
  80. package/bin/runners/lib/extractors/client-calls.js +990 -990
  81. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
  82. package/bin/runners/lib/extractors/fastify-routes.js +426 -426
  83. package/bin/runners/lib/extractors/index.js +363 -363
  84. package/bin/runners/lib/extractors/next-routes.js +524 -524
  85. package/bin/runners/lib/extractors/proof-graph.js +431 -431
  86. package/bin/runners/lib/extractors/route-matcher.js +451 -451
  87. package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
  88. package/bin/runners/lib/extractors/ui-bindings.js +547 -547
  89. package/bin/runners/lib/findings-schema.js +281 -281
  90. package/bin/runners/lib/firewall-prompt.js +50 -50
  91. package/bin/runners/lib/global-flags.js +37 -0
  92. package/bin/runners/lib/graph/graph-builder.js +265 -265
  93. package/bin/runners/lib/graph/html-renderer.js +413 -413
  94. package/bin/runners/lib/graph/index.js +32 -32
  95. package/bin/runners/lib/graph/runtime-collector.js +215 -215
  96. package/bin/runners/lib/graph/static-extractor.js +518 -518
  97. package/bin/runners/lib/help-formatter.js +413 -0
  98. package/bin/runners/lib/html-report.js +650 -650
  99. package/bin/runners/lib/llm.js +75 -75
  100. package/bin/runners/lib/logger.js +38 -0
  101. package/bin/runners/lib/meter.js +61 -61
  102. package/bin/runners/lib/missions/evidence.js +126 -126
  103. package/bin/runners/lib/patch.js +40 -40
  104. package/bin/runners/lib/permissions/auth-model.js +213 -213
  105. package/bin/runners/lib/permissions/idor-prover.js +205 -205
  106. package/bin/runners/lib/permissions/index.js +45 -45
  107. package/bin/runners/lib/permissions/matrix-builder.js +198 -198
  108. package/bin/runners/lib/pkgjson.js +28 -28
  109. package/bin/runners/lib/policy.js +295 -295
  110. package/bin/runners/lib/preflight.js +142 -142
  111. package/bin/runners/lib/reality/correlation-detectors.js +359 -359
  112. package/bin/runners/lib/reality/index.js +318 -318
  113. package/bin/runners/lib/reality/request-hashing.js +416 -416
  114. package/bin/runners/lib/reality/request-mapper.js +453 -453
  115. package/bin/runners/lib/reality/safety-rails.js +463 -463
  116. package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
  117. package/bin/runners/lib/reality/toast-detector.js +393 -393
  118. package/bin/runners/lib/reality-findings.js +84 -84
  119. package/bin/runners/lib/receipts.js +179 -179
  120. package/bin/runners/lib/redact.js +29 -29
  121. package/bin/runners/lib/replay/capsule-manager.js +154 -154
  122. package/bin/runners/lib/replay/index.js +263 -263
  123. package/bin/runners/lib/replay/player.js +348 -348
  124. package/bin/runners/lib/replay/recorder.js +331 -331
  125. package/bin/runners/lib/report.js +135 -135
  126. package/bin/runners/lib/route-detection.js +1140 -1140
  127. package/bin/runners/lib/sandbox/index.js +59 -59
  128. package/bin/runners/lib/sandbox/proof-chain.js +399 -399
  129. package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
  130. package/bin/runners/lib/sandbox/worktree.js +174 -174
  131. package/bin/runners/lib/schema-validator.js +350 -350
  132. package/bin/runners/lib/schemas/contracts.schema.json +160 -160
  133. package/bin/runners/lib/schemas/finding.schema.json +100 -100
  134. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
  135. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
  136. package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
  137. package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
  138. package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
  139. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
  140. package/bin/runners/lib/schemas/validator.js +438 -438
  141. package/bin/runners/lib/score-history.js +282 -282
  142. package/bin/runners/lib/share-pack.js +239 -239
  143. package/bin/runners/lib/snippets.js +67 -67
  144. package/bin/runners/lib/unified-cli-output.js +604 -0
  145. package/bin/runners/lib/upsell.js +658 -510
  146. package/bin/runners/lib/usage.js +153 -153
  147. package/bin/runners/lib/validate-patch.js +156 -156
  148. package/bin/runners/lib/verdict-engine.js +628 -628
  149. package/bin/runners/reality/engine.js +917 -917
  150. package/bin/runners/reality/flows.js +122 -122
  151. package/bin/runners/reality/report.js +378 -378
  152. package/bin/runners/reality/session.js +193 -193
  153. package/bin/runners/runAgent.d.ts +5 -0
  154. package/bin/runners/runApprove.js +1200 -0
  155. package/bin/runners/runAuth.js +324 -95
  156. package/bin/runners/runCheckpoint.js +39 -21
  157. package/bin/runners/runClassify.js +859 -0
  158. package/bin/runners/runContext.js +136 -24
  159. package/bin/runners/runDoctor.js +108 -68
  160. package/bin/runners/runFirewall.d.ts +5 -0
  161. package/bin/runners/runFirewallHook.d.ts +5 -0
  162. package/bin/runners/runFix.js +6 -5
  163. package/bin/runners/runGuard.js +262 -168
  164. package/bin/runners/runInit.js +3 -2
  165. package/bin/runners/runMcp.js +130 -52
  166. package/bin/runners/runPolish.js +43 -20
  167. package/bin/runners/runProve.js +1 -2
  168. package/bin/runners/runReport.js +3 -2
  169. package/bin/runners/runScan.js +145 -44
  170. package/bin/runners/runShip.js +3 -4
  171. package/bin/runners/runTruth.d.ts +5 -0
  172. package/bin/runners/runValidate.js +19 -2
  173. package/bin/runners/runWatch.js +104 -53
  174. package/bin/vibecheck.js +106 -19
  175. package/mcp-server/HARDENING_SUMMARY.md +299 -0
  176. package/mcp-server/agent-firewall-interceptor.js +367 -31
  177. package/mcp-server/authority-tools.js +569 -0
  178. package/mcp-server/conductor/conflict-resolver.js +588 -0
  179. package/mcp-server/conductor/execution-planner.js +544 -0
  180. package/mcp-server/conductor/index.js +377 -0
  181. package/mcp-server/conductor/lock-manager.js +615 -0
  182. package/mcp-server/conductor/request-queue.js +550 -0
  183. package/mcp-server/conductor/session-manager.js +500 -0
  184. package/mcp-server/conductor/tools.js +510 -0
  185. package/mcp-server/index.js +1199 -208
  186. package/mcp-server/lib/api-client.cjs +305 -0
  187. package/mcp-server/lib/logger.cjs +30 -0
  188. package/mcp-server/logger.js +173 -0
  189. package/mcp-server/package.json +2 -2
  190. package/mcp-server/premium-tools.js +2 -2
  191. package/mcp-server/tier-auth.js +351 -136
  192. package/mcp-server/tools/index.js +72 -72
  193. package/mcp-server/truth-firewall-tools.js +145 -15
  194. package/mcp-server/vibecheck-tools.js +2 -2
  195. package/package.json +2 -3
  196. package/mcp-server/index.old.js +0 -4137
  197. package/mcp-server/package-lock.json +0 -165
@@ -1,804 +1,804 @@
1
- /**
2
- * Contracts Generator + Drift Detector v2
3
- *
4
- * Generates contracts from truthpack and detects drift between runs.
5
- * The hallucination stopper - AI can't invent endpoints, env vars, or auth.
6
- */
7
-
8
- "use strict";
9
-
10
- const fs = require("fs");
11
- const path = require("path");
12
- const crypto = require("crypto");
13
-
14
- const { createFinding, getSeverity } = require("./schemas/validator");
15
-
16
- // =============================================================================
17
- // CONTRACT TYPES
18
- // =============================================================================
19
-
20
- const CONTRACT_TYPES = {
21
- ROUTES: "routes",
22
- ENV: "env",
23
- AUTH: "auth",
24
- EXTERNAL: "external",
25
- };
26
-
27
- // =============================================================================
28
- // CONTRACT GENERATION
29
- // =============================================================================
30
-
31
- /**
32
- * Generate all contracts from truthpack
33
- */
34
- function generateContracts(truthpack, options = {}) {
35
- const { includeInternal = false } = options;
36
-
37
- const contracts = {
38
- meta: {
39
- version: "2.0.0",
40
- generatedAt: new Date().toISOString(),
41
- truthpackHash: computeHash(JSON.stringify(truthpack)),
42
- },
43
- routes: generateRouteContract(truthpack),
44
- env: generateEnvContract(truthpack),
45
- auth: generateAuthContract(truthpack),
46
- external: generateExternalContract(truthpack),
47
- };
48
-
49
- // Compute contract hashes
50
- contracts.hashes = {
51
- routes: computeHash(JSON.stringify(contracts.routes)),
52
- env: computeHash(JSON.stringify(contracts.env)),
53
- auth: computeHash(JSON.stringify(contracts.auth)),
54
- external: computeHash(JSON.stringify(contracts.external)),
55
- };
56
-
57
- return contracts;
58
- }
59
-
60
- /**
61
- * Generate route contract
62
- */
63
- function generateRouteContract(truthpack) {
64
- const routes = truthpack?.routes?.server || [];
65
-
66
- return {
67
- endpoints: routes.map(r => ({
68
- method: r.method,
69
- path: r.path,
70
- file: r.file,
71
- confidence: r.confidence,
72
- authRequired: r.authRequired || false,
73
- paidOnly: r.paidOnly || false,
74
- })),
75
- count: routes.length,
76
- byMethod: countByField(routes, "method"),
77
- byConfidence: countByField(routes, "confidence"),
78
- };
79
- }
80
-
81
- /**
82
- * Generate env contract
83
- */
84
- function generateEnvContract(truthpack) {
85
- const env = truthpack?.env || {};
86
- const vars = env.vars || [];
87
- const declared = env.declared || [];
88
-
89
- // Classify vars by prefix
90
- const classified = {
91
- public: vars.filter(v => v.name.startsWith("NEXT_PUBLIC_") || v.name.startsWith("VITE_")),
92
- server: vars.filter(v => !v.name.startsWith("NEXT_PUBLIC_") && !v.name.startsWith("VITE_")),
93
- };
94
-
95
- // Required vs optional (heuristic: used in multiple files = required)
96
- const required = vars.filter(v => v.usageCount > 1 || v.required);
97
- const optional = vars.filter(v => v.usageCount <= 1 && !v.required);
98
-
99
- return {
100
- variables: vars.map(v => ({
101
- name: v.name,
102
- required: v.required || v.usageCount > 1,
103
- public: v.name.startsWith("NEXT_PUBLIC_") || v.name.startsWith("VITE_"),
104
- declaredIn: declared.includes(v.name) ? ".env.example" : null,
105
- usageCount: v.usageCount || 1,
106
- })),
107
- count: vars.length,
108
- requiredCount: required.length,
109
- publicCount: classified.public.length,
110
- declared: declared,
111
- };
112
- }
113
-
114
- /**
115
- * Generate auth contract
116
- */
117
- function generateAuthContract(truthpack) {
118
- const auth = truthpack?.auth || {};
119
-
120
- return {
121
- providers: auth.providers || [],
122
- protectedPatterns: auth.nextMatcherPatterns || [],
123
- middleware: {
124
- next: auth.nextMiddleware?.length > 0,
125
- fastify: auth.fastify?.preHandlerHooks?.length > 0,
126
- },
127
- rbacDetected: auth.rbacPatterns?.length > 0,
128
- signals: {
129
- sessionChecks: auth.sessionChecks || [],
130
- redirects: auth.redirects || [],
131
- },
132
- };
133
- }
134
-
135
- /**
136
- * Generate external services contract
137
- */
138
- function generateExternalContract(truthpack) {
139
- const billing = truthpack?.billing || {};
140
- const integrations = truthpack?.integrations || [];
141
-
142
- return {
143
- stripe: {
144
- detected: billing.stripeDetected || false,
145
- webhookVerified: billing.webhookSignatureVerification || false,
146
- products: billing.products || [],
147
- },
148
- services: integrations.map(i => ({
149
- name: i.name,
150
- type: i.type,
151
- envVars: i.envVars || [],
152
- })),
153
- };
154
- }
155
-
156
- // =============================================================================
157
- // DRIFT DETECTION
158
- // =============================================================================
159
-
160
- /**
161
- * Detect drift between two contract sets
162
- */
163
- function detectDrift(currentContracts, previousContracts, options = {}) {
164
- const { strict = false } = options;
165
-
166
- const findings = [];
167
-
168
- // Route drift
169
- const routeDrift = detectRouteDrift(
170
- currentContracts.routes,
171
- previousContracts.routes,
172
- { strict }
173
- );
174
- findings.push(...routeDrift);
175
-
176
- // Env drift
177
- const envDrift = detectEnvDrift(
178
- currentContracts.env,
179
- previousContracts.env,
180
- { strict }
181
- );
182
- findings.push(...envDrift);
183
-
184
- // Auth drift
185
- const authDrift = detectAuthDrift(
186
- currentContracts.auth,
187
- previousContracts.auth,
188
- { strict }
189
- );
190
- findings.push(...authDrift);
191
-
192
- // External drift
193
- const externalDrift = detectExternalDrift(
194
- currentContracts.external,
195
- previousContracts.external,
196
- { strict }
197
- );
198
- findings.push(...externalDrift);
199
-
200
- return {
201
- hasDrift: findings.length > 0,
202
- findings,
203
- summary: {
204
- routes: routeDrift.length,
205
- env: envDrift.length,
206
- auth: authDrift.length,
207
- external: externalDrift.length,
208
- },
209
- };
210
- }
211
-
212
- /**
213
- * Detect route drift
214
- */
215
- function detectRouteDrift(current, previous, options = {}) {
216
- const findings = [];
217
- const { strict } = options;
218
-
219
- const currentPaths = new Set(current.endpoints.map(e => `${e.method}:${e.path}`));
220
- const previousPaths = new Set(previous.endpoints.map(e => `${e.method}:${e.path}`));
221
-
222
- // New routes (not in previous)
223
- for (const ep of current.endpoints) {
224
- const key = `${ep.method}:${ep.path}`;
225
- if (!previousPaths.has(key)) {
226
- findings.push(createFinding({
227
- detectorId: "D_DRIFT_ROUTE_NEW",
228
- severity: "INFO",
229
- category: "ContractDrift",
230
- scope: "contracts",
231
- title: `New route added: ${ep.method} ${ep.path}`,
232
- why: "Route was added since last contract sync",
233
- confidence: "high",
234
- path: ep.path,
235
- method: ep.method,
236
- evidence: [{
237
- kind: "file",
238
- file: ep.file,
239
- reason: "New route definition",
240
- }],
241
- }));
242
- }
243
- }
244
-
245
- // Removed routes (in previous but not current)
246
- for (const ep of previous.endpoints) {
247
- const key = `${ep.method}:${ep.path}`;
248
- if (!currentPaths.has(key)) {
249
- findings.push(createFinding({
250
- detectorId: "D_DRIFT_ROUTE_REMOVED",
251
- severity: strict ? "BLOCK" : "WARN",
252
- category: "ContractDrift",
253
- scope: "contracts",
254
- title: `Route removed: ${ep.method} ${ep.path}`,
255
- why: "Route existed in previous contract but is now missing",
256
- confidence: "high",
257
- path: ep.path,
258
- method: ep.method,
259
- evidence: [{
260
- kind: "file",
261
- file: ep.file,
262
- reason: "Route no longer exists",
263
- }],
264
- }));
265
- }
266
- }
267
-
268
- // Auth requirement changes
269
- for (const ep of current.endpoints) {
270
- const key = `${ep.method}:${ep.path}`;
271
- const prev = previous.endpoints.find(p => `${p.method}:${p.path}` === key);
272
-
273
- if (prev && prev.authRequired && !ep.authRequired) {
274
- findings.push(createFinding({
275
- detectorId: "D_DRIFT_AUTH_REMOVED",
276
- severity: "BLOCK",
277
- category: "ContractDrift",
278
- scope: "contracts",
279
- title: `Auth removed from route: ${ep.method} ${ep.path}`,
280
- why: "Route previously required auth but no longer does",
281
- confidence: "high",
282
- path: ep.path,
283
- method: ep.method,
284
- evidence: [{
285
- kind: "file",
286
- file: ep.file,
287
- reason: "Auth requirement removed",
288
- }],
289
- }));
290
- }
291
- }
292
-
293
- return findings;
294
- }
295
-
296
- /**
297
- * Detect env drift
298
- */
299
- function detectEnvDrift(current, previous, options = {}) {
300
- const findings = [];
301
- const { strict } = options;
302
-
303
- const currentVars = new Set(current.variables.map(v => v.name));
304
- const previousVars = new Set(previous.variables.map(v => v.name));
305
-
306
- // New required env vars
307
- for (const v of current.variables) {
308
- if (v.required && !previousVars.has(v.name)) {
309
- findings.push(createFinding({
310
- detectorId: "D_DRIFT_ENV_NEW_REQUIRED",
311
- severity: strict ? "BLOCK" : "WARN",
312
- category: "ContractDrift",
313
- scope: "contracts",
314
- title: `New required env var: ${v.name}`,
315
- why: "New required environment variable added",
316
- confidence: "high",
317
- evidence: [{
318
- kind: "file",
319
- reason: `${v.name} is required but not in previous contract`,
320
- }],
321
- }));
322
- }
323
- }
324
-
325
- // Required vars removed (may break existing deployments)
326
- for (const v of previous.variables) {
327
- if (v.required && !currentVars.has(v.name)) {
328
- findings.push(createFinding({
329
- detectorId: "D_DRIFT_ENV_REMOVED",
330
- severity: "INFO",
331
- category: "ContractDrift",
332
- scope: "contracts",
333
- title: `Env var removed: ${v.name}`,
334
- why: "Environment variable no longer used",
335
- confidence: "high",
336
- evidence: [{
337
- kind: "file",
338
- reason: `${v.name} was in previous contract but not current`,
339
- }],
340
- }));
341
- }
342
- }
343
-
344
- // Public → Server (security concern)
345
- for (const v of current.variables) {
346
- const prev = previous.variables.find(p => p.name === v.name);
347
- if (prev && prev.public && !v.public) {
348
- findings.push(createFinding({
349
- detectorId: "D_DRIFT_ENV_PUBLIC_TO_SERVER",
350
- severity: "INFO",
351
- category: "ContractDrift",
352
- scope: "contracts",
353
- title: `Env var moved from public to server: ${v.name}`,
354
- why: "Variable changed from public to server-only (good for security)",
355
- confidence: "medium",
356
- evidence: [{
357
- kind: "file",
358
- reason: `${v.name} prefix changed`,
359
- }],
360
- }));
361
- }
362
- }
363
-
364
- return findings;
365
- }
366
-
367
- /**
368
- * Detect auth drift
369
- */
370
- function detectAuthDrift(current, previous, options = {}) {
371
- const findings = [];
372
-
373
- // Protected patterns removed
374
- const currentPatterns = new Set(current.protectedPatterns);
375
- const previousPatterns = new Set(previous.protectedPatterns);
376
-
377
- for (const pattern of previousPatterns) {
378
- if (!currentPatterns.has(pattern)) {
379
- findings.push(createFinding({
380
- detectorId: "D_DRIFT_AUTH_PATTERN_REMOVED",
381
- severity: "BLOCK",
382
- category: "ContractDrift",
383
- scope: "contracts",
384
- title: `Protected pattern removed: ${pattern}`,
385
- why: "Auth protection pattern was removed from middleware",
386
- confidence: "high",
387
- evidence: [{
388
- kind: "file",
389
- reason: `Pattern "${pattern}" no longer in matcher`,
390
- }],
391
- }));
392
- }
393
- }
394
-
395
- // Middleware disabled
396
- if (previous.middleware.next && !current.middleware.next) {
397
- findings.push(createFinding({
398
- detectorId: "D_DRIFT_AUTH_MIDDLEWARE_DISABLED",
399
- severity: "BLOCK",
400
- category: "ContractDrift",
401
- scope: "contracts",
402
- title: "Next.js auth middleware disabled",
403
- why: "Auth middleware was active but is now disabled",
404
- confidence: "high",
405
- evidence: [{
406
- kind: "file",
407
- reason: "middleware.ts no longer exports auth matcher",
408
- }],
409
- }));
410
- }
411
-
412
- return findings;
413
- }
414
-
415
- /**
416
- * Detect external service drift
417
- */
418
- function detectExternalDrift(current, previous, options = {}) {
419
- const findings = [];
420
-
421
- // Stripe webhook verification removed
422
- if (previous.stripe.webhookVerified && !current.stripe.webhookVerified) {
423
- findings.push(createFinding({
424
- detectorId: "D_DRIFT_STRIPE_VERIFY_REMOVED",
425
- severity: "BLOCK",
426
- category: "ContractDrift",
427
- scope: "contracts",
428
- title: "Stripe webhook verification removed",
429
- why: "Stripe webhook signature verification was present but removed",
430
- confidence: "high",
431
- evidence: [{
432
- kind: "file",
433
- reason: "constructEvent or verifyHeader no longer called",
434
- }],
435
- }));
436
- }
437
-
438
- return findings;
439
- }
440
-
441
- // =============================================================================
442
- // CONTRACT SYNC (ctx sync)
443
- // =============================================================================
444
-
445
- /**
446
- * Sync contracts - generate and save
447
- */
448
- function syncContracts(truthpack, repoRoot) {
449
- const contracts = generateContracts(truthpack);
450
-
451
- const contractsDir = path.join(repoRoot, ".vibecheck", "contracts");
452
- fs.mkdirSync(contractsDir, { recursive: true });
453
-
454
- // Write individual contract files
455
- for (const type of Object.values(CONTRACT_TYPES)) {
456
- if (contracts[type]) {
457
- fs.writeFileSync(
458
- path.join(contractsDir, `${type}.json`),
459
- JSON.stringify(contracts[type], null, 2)
460
- );
461
- }
462
- }
463
-
464
- // Write combined contracts
465
- fs.writeFileSync(
466
- path.join(contractsDir, "contracts.json"),
467
- JSON.stringify(contracts, null, 2)
468
- );
469
-
470
- // Write hashes for quick drift check
471
- fs.writeFileSync(
472
- path.join(contractsDir, "hashes.json"),
473
- JSON.stringify(contracts.hashes, null, 2)
474
- );
475
-
476
- return contracts;
477
- }
478
-
479
- /**
480
- * Load previous contracts for drift comparison
481
- */
482
- function loadPreviousContracts(repoRoot) {
483
- const contractsPath = path.join(repoRoot, ".vibecheck", "contracts", "contracts.json");
484
-
485
- if (!fs.existsSync(contractsPath)) {
486
- return null;
487
- }
488
-
489
- try {
490
- return JSON.parse(fs.readFileSync(contractsPath, "utf8"));
491
- } catch {
492
- return null;
493
- }
494
- }
495
-
496
- /**
497
- * Guard contracts - check for drift and generate findings
498
- */
499
- function guardContracts(truthpack, repoRoot, options = {}) {
500
- const previous = loadPreviousContracts(repoRoot);
501
-
502
- if (!previous) {
503
- // No previous contracts, just sync
504
- const contracts = syncContracts(truthpack, repoRoot);
505
- return {
506
- isFirstRun: true,
507
- hasDrift: false,
508
- findings: [],
509
- contracts,
510
- };
511
- }
512
-
513
- const current = generateContracts(truthpack);
514
- const drift = detectDrift(current, previous, options);
515
-
516
- // If no blockers, update contracts
517
- const hasBlockers = drift.findings.some(f => f.severity === "BLOCK");
518
- if (!hasBlockers) {
519
- syncContracts(truthpack, repoRoot);
520
- }
521
-
522
- return {
523
- isFirstRun: false,
524
- hasDrift: drift.hasDrift,
525
- hasBlockers,
526
- findings: drift.findings,
527
- contracts: current,
528
- previous,
529
- };
530
- }
531
-
532
- // =============================================================================
533
- // DRIFT EXPLAINABILITY
534
- // =============================================================================
535
-
536
- /**
537
- * Generate detailed drift explanation with remediation
538
- */
539
- function explainDrift(driftResult) {
540
- const { findings, summary } = driftResult;
541
-
542
- const explanation = {
543
- summary: {
544
- hasDrift: driftResult.hasDrift,
545
- totalChanges: findings.length,
546
- byCategory: summary,
547
- verdict: findings.some(f => f.severity === "BLOCK") ? "BLOCK" :
548
- findings.some(f => f.severity === "WARN") ? "WARN" : "OK",
549
- },
550
- changes: [],
551
- remediation: [],
552
- };
553
-
554
- for (const finding of findings) {
555
- const change = {
556
- type: getChangeType(finding.detectorId),
557
- severity: finding.severity,
558
- title: finding.title,
559
- why: finding.why || getWhyMessage(finding),
560
- impact: getImpactMessage(finding),
561
- remediation: getRemediationCommand(finding),
562
- };
563
- explanation.changes.push(change);
564
-
565
- if (change.remediation) {
566
- explanation.remediation.push(change.remediation);
567
- }
568
- }
569
-
570
- return explanation;
571
- }
572
-
573
- /**
574
- * Get change type from detector ID
575
- */
576
- function getChangeType(detectorId) {
577
- const types = {
578
- "D_DRIFT_ROUTE_NEW": "added",
579
- "D_DRIFT_ROUTE_REMOVED": "removed",
580
- "D_DRIFT_AUTH_REMOVED": "modified",
581
- "D_DRIFT_ENV_NEW_REQUIRED": "added",
582
- "D_DRIFT_ENV_REMOVED": "removed",
583
- "D_DRIFT_ENV_PUBLIC_TO_SERVER": "modified",
584
- "D_DRIFT_AUTH_PATTERN_REMOVED": "removed",
585
- "D_DRIFT_AUTH_PATTERN_NEW": "added",
586
- "D_DRIFT_EXTERNAL_NEW": "added",
587
- "D_DRIFT_EXTERNAL_REMOVED": "removed",
588
- };
589
- return types[detectorId] || "unknown";
590
- }
591
-
592
- /**
593
- * Get why message for finding
594
- */
595
- function getWhyMessage(finding) {
596
- const messages = {
597
- "D_DRIFT_ROUTE_NEW": "New endpoint referenced by client but not in previous contract",
598
- "D_DRIFT_ROUTE_REMOVED": "Endpoint was in contract but no longer exists in codebase",
599
- "D_DRIFT_AUTH_REMOVED": "Route lost auth protection - potential security regression",
600
- "D_DRIFT_ENV_NEW_REQUIRED": "New required env var may break deployments without it",
601
- "D_DRIFT_ENV_REMOVED": "Env var no longer used - can be cleaned up",
602
- "D_DRIFT_AUTH_PATTERN_REMOVED": "Auth protection pattern removed from middleware",
603
- "D_DRIFT_AUTH_PATTERN_NEW": "New route pattern added to auth middleware",
604
- "D_DRIFT_EXTERNAL_NEW": "New external API dependency detected",
605
- "D_DRIFT_EXTERNAL_REMOVED": "External API dependency removed",
606
- };
607
- return messages[finding.detectorId] || "Contract changed since last sync";
608
- }
609
-
610
- /**
611
- * Get impact message for finding
612
- */
613
- function getImpactMessage(finding) {
614
- if (finding.severity === "BLOCK") {
615
- return "This change may cause runtime failures or security issues";
616
- }
617
- if (finding.severity === "WARN") {
618
- return "This change should be reviewed before shipping";
619
- }
620
- return "This change is informational";
621
- }
622
-
623
- /**
624
- * Get remediation command for finding
625
- */
626
- function getRemediationCommand(finding) {
627
- const detectorId = finding.detectorId;
628
-
629
- // If the change is intentional, sync contracts
630
- if (detectorId.includes("NEW") || detectorId.includes("ADDED")) {
631
- return {
632
- ifIntentional: "vibecheck ctx sync",
633
- description: "If this change is intentional, sync contracts",
634
- };
635
- }
636
-
637
- if (detectorId.includes("REMOVED")) {
638
- if (detectorId.includes("ROUTE")) {
639
- return {
640
- ifIntentional: "vibecheck ctx sync",
641
- ifAccidental: `Add route back or remove client call to ${finding.evidence?.[0]?.path || "the endpoint"}`,
642
- description: "Route was removed - verify this is intentional",
643
- };
644
- }
645
- if (detectorId.includes("AUTH")) {
646
- return {
647
- ifIntentional: "vibecheck ctx sync --force",
648
- ifAccidental: "Restore auth protection to the route",
649
- description: "Auth was removed - this is usually NOT intentional",
650
- };
651
- }
652
- }
653
-
654
- return {
655
- ifIntentional: "vibecheck ctx sync",
656
- description: "Review the change and sync if intentional",
657
- };
658
- }
659
-
660
- /**
661
- * Write contracts diff to disk
662
- */
663
- function writeContractsDiff(repoRoot, driftResult) {
664
- const dir = path.join(repoRoot, ".vibecheck");
665
- fs.mkdirSync(dir, { recursive: true });
666
-
667
- const explanation = explainDrift(driftResult);
668
-
669
- const diff = {
670
- meta: {
671
- generatedAt: new Date().toISOString(),
672
- verdict: explanation.summary.verdict,
673
- },
674
- ...explanation,
675
- };
676
-
677
- const diffPath = path.join(dir, "contracts_diff.json");
678
- fs.writeFileSync(diffPath, JSON.stringify(diff, null, 2));
679
-
680
- return { path: diffPath, explanation };
681
- }
682
-
683
- /**
684
- * Print drift report to console
685
- */
686
- function printDriftReport(driftResult) {
687
- const explanation = explainDrift(driftResult);
688
-
689
- console.log("\n" + "=".repeat(60));
690
- console.log("šŸ“‹ CONTRACT DRIFT REPORT");
691
- console.log("=".repeat(60));
692
-
693
- if (!driftResult.hasDrift) {
694
- console.log("\nāœ… No drift detected - contracts are in sync\n");
695
- return;
696
- }
697
-
698
- const verdictEmoji = {
699
- BLOCK: "🚫",
700
- WARN: "āš ļø",
701
- OK: "āœ…",
702
- }[explanation.summary.verdict];
703
-
704
- console.log(`\nVerdict: ${verdictEmoji} ${explanation.summary.verdict}`);
705
- console.log(`Changes: ${explanation.summary.totalChanges}`);
706
-
707
- // Group by severity
708
- const blockers = explanation.changes.filter(c => c.severity === "BLOCK");
709
- const warnings = explanation.changes.filter(c => c.severity === "WARN");
710
- const infos = explanation.changes.filter(c => c.severity === "INFO");
711
-
712
- if (blockers.length > 0) {
713
- console.log("\n🚫 BLOCKERS:");
714
- for (const change of blockers) {
715
- console.log(` ${change.type.toUpperCase()}: ${change.title}`);
716
- console.log(` └─ Why: ${change.why}`);
717
- if (change.remediation?.ifAccidental) {
718
- console.log(` └─ Fix: ${change.remediation.ifAccidental}`);
719
- }
720
- }
721
- }
722
-
723
- if (warnings.length > 0) {
724
- console.log("\nāš ļø WARNINGS:");
725
- for (const change of warnings) {
726
- console.log(` ${change.type.toUpperCase()}: ${change.title}`);
727
- console.log(` └─ Why: ${change.why}`);
728
- }
729
- }
730
-
731
- if (infos.length > 0) {
732
- console.log("\nā„¹ļø INFO:");
733
- for (const change of infos) {
734
- console.log(` ${change.type.toUpperCase()}: ${change.title}`);
735
- }
736
- }
737
-
738
- // Remediation
739
- console.log("\n" + "-".repeat(60));
740
- console.log("REMEDIATION:");
741
-
742
- if (explanation.summary.verdict === "BLOCK") {
743
- console.log("\n If changes are INTENTIONAL:");
744
- console.log(" $ vibecheck ctx sync --force");
745
- console.log("\n If changes are ACCIDENTAL:");
746
- console.log(" - Review the blockers above and fix the issues");
747
- console.log(" - Then run: vibecheck ctx sync");
748
- } else {
749
- console.log("\n To accept these changes:");
750
- console.log(" $ vibecheck ctx sync");
751
- }
752
-
753
- console.log("\n" + "=".repeat(60) + "\n");
754
- }
755
-
756
- // =============================================================================
757
- // HELPERS
758
- // =============================================================================
759
-
760
- function computeHash(content) {
761
- return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
762
- }
763
-
764
- function countByField(items, field) {
765
- const counts = {};
766
- for (const item of items) {
767
- const value = item[field] || "unknown";
768
- counts[value] = (counts[value] || 0) + 1;
769
- }
770
- return counts;
771
- }
772
-
773
- // =============================================================================
774
- // EXPORTS
775
- // =============================================================================
776
-
777
- module.exports = {
778
- // Contract types
779
- CONTRACT_TYPES,
780
-
781
- // Generation
782
- generateContracts,
783
- generateRouteContract,
784
- generateEnvContract,
785
- generateAuthContract,
786
- generateExternalContract,
787
-
788
- // Drift detection
789
- detectDrift,
790
- detectRouteDrift,
791
- detectEnvDrift,
792
- detectAuthDrift,
793
- detectExternalDrift,
794
-
795
- // Drift explainability
796
- explainDrift,
797
- writeContractsDiff,
798
- printDriftReport,
799
-
800
- // Sync/Guard
801
- syncContracts,
802
- loadPreviousContracts,
803
- guardContracts,
804
- };
1
+ /**
2
+ * Contracts Generator + Drift Detector v2
3
+ *
4
+ * Generates contracts from truthpack and detects drift between runs.
5
+ * The hallucination stopper - AI can't invent endpoints, env vars, or auth.
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const crypto = require("crypto");
13
+
14
+ const { createFinding, getSeverity } = require("./schemas/validator");
15
+
16
+ // =============================================================================
17
+ // CONTRACT TYPES
18
+ // =============================================================================
19
+
20
+ const CONTRACT_TYPES = {
21
+ ROUTES: "routes",
22
+ ENV: "env",
23
+ AUTH: "auth",
24
+ EXTERNAL: "external",
25
+ };
26
+
27
+ // =============================================================================
28
+ // CONTRACT GENERATION
29
+ // =============================================================================
30
+
31
+ /**
32
+ * Generate all contracts from truthpack
33
+ */
34
+ function generateContracts(truthpack, options = {}) {
35
+ const { includeInternal = false } = options;
36
+
37
+ const contracts = {
38
+ meta: {
39
+ version: "2.0.0",
40
+ generatedAt: new Date().toISOString(),
41
+ truthpackHash: computeHash(JSON.stringify(truthpack)),
42
+ },
43
+ routes: generateRouteContract(truthpack),
44
+ env: generateEnvContract(truthpack),
45
+ auth: generateAuthContract(truthpack),
46
+ external: generateExternalContract(truthpack),
47
+ };
48
+
49
+ // Compute contract hashes
50
+ contracts.hashes = {
51
+ routes: computeHash(JSON.stringify(contracts.routes)),
52
+ env: computeHash(JSON.stringify(contracts.env)),
53
+ auth: computeHash(JSON.stringify(contracts.auth)),
54
+ external: computeHash(JSON.stringify(contracts.external)),
55
+ };
56
+
57
+ return contracts;
58
+ }
59
+
60
+ /**
61
+ * Generate route contract
62
+ */
63
+ function generateRouteContract(truthpack) {
64
+ const routes = truthpack?.routes?.server || [];
65
+
66
+ return {
67
+ endpoints: routes.map(r => ({
68
+ method: r.method,
69
+ path: r.path,
70
+ file: r.file,
71
+ confidence: r.confidence,
72
+ authRequired: r.authRequired || false,
73
+ paidOnly: r.paidOnly || false,
74
+ })),
75
+ count: routes.length,
76
+ byMethod: countByField(routes, "method"),
77
+ byConfidence: countByField(routes, "confidence"),
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Generate env contract
83
+ */
84
+ function generateEnvContract(truthpack) {
85
+ const env = truthpack?.env || {};
86
+ const vars = env.vars || [];
87
+ const declared = env.declared || [];
88
+
89
+ // Classify vars by prefix
90
+ const classified = {
91
+ public: vars.filter(v => v.name.startsWith("NEXT_PUBLIC_") || v.name.startsWith("VITE_")),
92
+ server: vars.filter(v => !v.name.startsWith("NEXT_PUBLIC_") && !v.name.startsWith("VITE_")),
93
+ };
94
+
95
+ // Required vs optional (heuristic: used in multiple files = required)
96
+ const required = vars.filter(v => v.usageCount > 1 || v.required);
97
+ const optional = vars.filter(v => v.usageCount <= 1 && !v.required);
98
+
99
+ return {
100
+ variables: vars.map(v => ({
101
+ name: v.name,
102
+ required: v.required || v.usageCount > 1,
103
+ public: v.name.startsWith("NEXT_PUBLIC_") || v.name.startsWith("VITE_"),
104
+ declaredIn: declared.includes(v.name) ? ".env.example" : null,
105
+ usageCount: v.usageCount || 1,
106
+ })),
107
+ count: vars.length,
108
+ requiredCount: required.length,
109
+ publicCount: classified.public.length,
110
+ declared: declared,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Generate auth contract
116
+ */
117
+ function generateAuthContract(truthpack) {
118
+ const auth = truthpack?.auth || {};
119
+
120
+ return {
121
+ providers: auth.providers || [],
122
+ protectedPatterns: auth.nextMatcherPatterns || [],
123
+ middleware: {
124
+ next: auth.nextMiddleware?.length > 0,
125
+ fastify: auth.fastify?.preHandlerHooks?.length > 0,
126
+ },
127
+ rbacDetected: auth.rbacPatterns?.length > 0,
128
+ signals: {
129
+ sessionChecks: auth.sessionChecks || [],
130
+ redirects: auth.redirects || [],
131
+ },
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Generate external services contract
137
+ */
138
+ function generateExternalContract(truthpack) {
139
+ const billing = truthpack?.billing || {};
140
+ const integrations = truthpack?.integrations || [];
141
+
142
+ return {
143
+ stripe: {
144
+ detected: billing.stripeDetected || false,
145
+ webhookVerified: billing.webhookSignatureVerification || false,
146
+ products: billing.products || [],
147
+ },
148
+ services: integrations.map(i => ({
149
+ name: i.name,
150
+ type: i.type,
151
+ envVars: i.envVars || [],
152
+ })),
153
+ };
154
+ }
155
+
156
+ // =============================================================================
157
+ // DRIFT DETECTION
158
+ // =============================================================================
159
+
160
+ /**
161
+ * Detect drift between two contract sets
162
+ */
163
+ function detectDrift(currentContracts, previousContracts, options = {}) {
164
+ const { strict = false } = options;
165
+
166
+ const findings = [];
167
+
168
+ // Route drift
169
+ const routeDrift = detectRouteDrift(
170
+ currentContracts.routes,
171
+ previousContracts.routes,
172
+ { strict }
173
+ );
174
+ findings.push(...routeDrift);
175
+
176
+ // Env drift
177
+ const envDrift = detectEnvDrift(
178
+ currentContracts.env,
179
+ previousContracts.env,
180
+ { strict }
181
+ );
182
+ findings.push(...envDrift);
183
+
184
+ // Auth drift
185
+ const authDrift = detectAuthDrift(
186
+ currentContracts.auth,
187
+ previousContracts.auth,
188
+ { strict }
189
+ );
190
+ findings.push(...authDrift);
191
+
192
+ // External drift
193
+ const externalDrift = detectExternalDrift(
194
+ currentContracts.external,
195
+ previousContracts.external,
196
+ { strict }
197
+ );
198
+ findings.push(...externalDrift);
199
+
200
+ return {
201
+ hasDrift: findings.length > 0,
202
+ findings,
203
+ summary: {
204
+ routes: routeDrift.length,
205
+ env: envDrift.length,
206
+ auth: authDrift.length,
207
+ external: externalDrift.length,
208
+ },
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Detect route drift
214
+ */
215
+ function detectRouteDrift(current, previous, options = {}) {
216
+ const findings = [];
217
+ const { strict } = options;
218
+
219
+ const currentPaths = new Set(current.endpoints.map(e => `${e.method}:${e.path}`));
220
+ const previousPaths = new Set(previous.endpoints.map(e => `${e.method}:${e.path}`));
221
+
222
+ // New routes (not in previous)
223
+ for (const ep of current.endpoints) {
224
+ const key = `${ep.method}:${ep.path}`;
225
+ if (!previousPaths.has(key)) {
226
+ findings.push(createFinding({
227
+ detectorId: "D_DRIFT_ROUTE_NEW",
228
+ severity: "INFO",
229
+ category: "ContractDrift",
230
+ scope: "contracts",
231
+ title: `New route added: ${ep.method} ${ep.path}`,
232
+ why: "Route was added since last contract sync",
233
+ confidence: "high",
234
+ path: ep.path,
235
+ method: ep.method,
236
+ evidence: [{
237
+ kind: "file",
238
+ file: ep.file,
239
+ reason: "New route definition",
240
+ }],
241
+ }));
242
+ }
243
+ }
244
+
245
+ // Removed routes (in previous but not current)
246
+ for (const ep of previous.endpoints) {
247
+ const key = `${ep.method}:${ep.path}`;
248
+ if (!currentPaths.has(key)) {
249
+ findings.push(createFinding({
250
+ detectorId: "D_DRIFT_ROUTE_REMOVED",
251
+ severity: strict ? "BLOCK" : "WARN",
252
+ category: "ContractDrift",
253
+ scope: "contracts",
254
+ title: `Route removed: ${ep.method} ${ep.path}`,
255
+ why: "Route existed in previous contract but is now missing",
256
+ confidence: "high",
257
+ path: ep.path,
258
+ method: ep.method,
259
+ evidence: [{
260
+ kind: "file",
261
+ file: ep.file,
262
+ reason: "Route no longer exists",
263
+ }],
264
+ }));
265
+ }
266
+ }
267
+
268
+ // Auth requirement changes
269
+ for (const ep of current.endpoints) {
270
+ const key = `${ep.method}:${ep.path}`;
271
+ const prev = previous.endpoints.find(p => `${p.method}:${p.path}` === key);
272
+
273
+ if (prev && prev.authRequired && !ep.authRequired) {
274
+ findings.push(createFinding({
275
+ detectorId: "D_DRIFT_AUTH_REMOVED",
276
+ severity: "BLOCK",
277
+ category: "ContractDrift",
278
+ scope: "contracts",
279
+ title: `Auth removed from route: ${ep.method} ${ep.path}`,
280
+ why: "Route previously required auth but no longer does",
281
+ confidence: "high",
282
+ path: ep.path,
283
+ method: ep.method,
284
+ evidence: [{
285
+ kind: "file",
286
+ file: ep.file,
287
+ reason: "Auth requirement removed",
288
+ }],
289
+ }));
290
+ }
291
+ }
292
+
293
+ return findings;
294
+ }
295
+
296
+ /**
297
+ * Detect env drift
298
+ */
299
+ function detectEnvDrift(current, previous, options = {}) {
300
+ const findings = [];
301
+ const { strict } = options;
302
+
303
+ const currentVars = new Set(current.variables.map(v => v.name));
304
+ const previousVars = new Set(previous.variables.map(v => v.name));
305
+
306
+ // New required env vars
307
+ for (const v of current.variables) {
308
+ if (v.required && !previousVars.has(v.name)) {
309
+ findings.push(createFinding({
310
+ detectorId: "D_DRIFT_ENV_NEW_REQUIRED",
311
+ severity: strict ? "BLOCK" : "WARN",
312
+ category: "ContractDrift",
313
+ scope: "contracts",
314
+ title: `New required env var: ${v.name}`,
315
+ why: "New required environment variable added",
316
+ confidence: "high",
317
+ evidence: [{
318
+ kind: "file",
319
+ reason: `${v.name} is required but not in previous contract`,
320
+ }],
321
+ }));
322
+ }
323
+ }
324
+
325
+ // Required vars removed (may break existing deployments)
326
+ for (const v of previous.variables) {
327
+ if (v.required && !currentVars.has(v.name)) {
328
+ findings.push(createFinding({
329
+ detectorId: "D_DRIFT_ENV_REMOVED",
330
+ severity: "INFO",
331
+ category: "ContractDrift",
332
+ scope: "contracts",
333
+ title: `Env var removed: ${v.name}`,
334
+ why: "Environment variable no longer used",
335
+ confidence: "high",
336
+ evidence: [{
337
+ kind: "file",
338
+ reason: `${v.name} was in previous contract but not current`,
339
+ }],
340
+ }));
341
+ }
342
+ }
343
+
344
+ // Public → Server (security concern)
345
+ for (const v of current.variables) {
346
+ const prev = previous.variables.find(p => p.name === v.name);
347
+ if (prev && prev.public && !v.public) {
348
+ findings.push(createFinding({
349
+ detectorId: "D_DRIFT_ENV_PUBLIC_TO_SERVER",
350
+ severity: "INFO",
351
+ category: "ContractDrift",
352
+ scope: "contracts",
353
+ title: `Env var moved from public to server: ${v.name}`,
354
+ why: "Variable changed from public to server-only (good for security)",
355
+ confidence: "medium",
356
+ evidence: [{
357
+ kind: "file",
358
+ reason: `${v.name} prefix changed`,
359
+ }],
360
+ }));
361
+ }
362
+ }
363
+
364
+ return findings;
365
+ }
366
+
367
+ /**
368
+ * Detect auth drift
369
+ */
370
+ function detectAuthDrift(current, previous, options = {}) {
371
+ const findings = [];
372
+
373
+ // Protected patterns removed
374
+ const currentPatterns = new Set(current.protectedPatterns);
375
+ const previousPatterns = new Set(previous.protectedPatterns);
376
+
377
+ for (const pattern of previousPatterns) {
378
+ if (!currentPatterns.has(pattern)) {
379
+ findings.push(createFinding({
380
+ detectorId: "D_DRIFT_AUTH_PATTERN_REMOVED",
381
+ severity: "BLOCK",
382
+ category: "ContractDrift",
383
+ scope: "contracts",
384
+ title: `Protected pattern removed: ${pattern}`,
385
+ why: "Auth protection pattern was removed from middleware",
386
+ confidence: "high",
387
+ evidence: [{
388
+ kind: "file",
389
+ reason: `Pattern "${pattern}" no longer in matcher`,
390
+ }],
391
+ }));
392
+ }
393
+ }
394
+
395
+ // Middleware disabled
396
+ if (previous.middleware.next && !current.middleware.next) {
397
+ findings.push(createFinding({
398
+ detectorId: "D_DRIFT_AUTH_MIDDLEWARE_DISABLED",
399
+ severity: "BLOCK",
400
+ category: "ContractDrift",
401
+ scope: "contracts",
402
+ title: "Next.js auth middleware disabled",
403
+ why: "Auth middleware was active but is now disabled",
404
+ confidence: "high",
405
+ evidence: [{
406
+ kind: "file",
407
+ reason: "middleware.ts no longer exports auth matcher",
408
+ }],
409
+ }));
410
+ }
411
+
412
+ return findings;
413
+ }
414
+
415
+ /**
416
+ * Detect external service drift
417
+ */
418
+ function detectExternalDrift(current, previous, options = {}) {
419
+ const findings = [];
420
+
421
+ // Stripe webhook verification removed
422
+ if (previous.stripe.webhookVerified && !current.stripe.webhookVerified) {
423
+ findings.push(createFinding({
424
+ detectorId: "D_DRIFT_STRIPE_VERIFY_REMOVED",
425
+ severity: "BLOCK",
426
+ category: "ContractDrift",
427
+ scope: "contracts",
428
+ title: "Stripe webhook verification removed",
429
+ why: "Stripe webhook signature verification was present but removed",
430
+ confidence: "high",
431
+ evidence: [{
432
+ kind: "file",
433
+ reason: "constructEvent or verifyHeader no longer called",
434
+ }],
435
+ }));
436
+ }
437
+
438
+ return findings;
439
+ }
440
+
441
+ // =============================================================================
442
+ // CONTRACT SYNC (ctx sync)
443
+ // =============================================================================
444
+
445
+ /**
446
+ * Sync contracts - generate and save
447
+ */
448
+ function syncContracts(truthpack, repoRoot) {
449
+ const contracts = generateContracts(truthpack);
450
+
451
+ const contractsDir = path.join(repoRoot, ".vibecheck", "contracts");
452
+ fs.mkdirSync(contractsDir, { recursive: true });
453
+
454
+ // Write individual contract files
455
+ for (const type of Object.values(CONTRACT_TYPES)) {
456
+ if (contracts[type]) {
457
+ fs.writeFileSync(
458
+ path.join(contractsDir, `${type}.json`),
459
+ JSON.stringify(contracts[type], null, 2)
460
+ );
461
+ }
462
+ }
463
+
464
+ // Write combined contracts
465
+ fs.writeFileSync(
466
+ path.join(contractsDir, "contracts.json"),
467
+ JSON.stringify(contracts, null, 2)
468
+ );
469
+
470
+ // Write hashes for quick drift check
471
+ fs.writeFileSync(
472
+ path.join(contractsDir, "hashes.json"),
473
+ JSON.stringify(contracts.hashes, null, 2)
474
+ );
475
+
476
+ return contracts;
477
+ }
478
+
479
+ /**
480
+ * Load previous contracts for drift comparison
481
+ */
482
+ function loadPreviousContracts(repoRoot) {
483
+ const contractsPath = path.join(repoRoot, ".vibecheck", "contracts", "contracts.json");
484
+
485
+ if (!fs.existsSync(contractsPath)) {
486
+ return null;
487
+ }
488
+
489
+ try {
490
+ return JSON.parse(fs.readFileSync(contractsPath, "utf8"));
491
+ } catch {
492
+ return null;
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Guard contracts - check for drift and generate findings
498
+ */
499
+ function guardContracts(truthpack, repoRoot, options = {}) {
500
+ const previous = loadPreviousContracts(repoRoot);
501
+
502
+ if (!previous) {
503
+ // No previous contracts, just sync
504
+ const contracts = syncContracts(truthpack, repoRoot);
505
+ return {
506
+ isFirstRun: true,
507
+ hasDrift: false,
508
+ findings: [],
509
+ contracts,
510
+ };
511
+ }
512
+
513
+ const current = generateContracts(truthpack);
514
+ const drift = detectDrift(current, previous, options);
515
+
516
+ // If no blockers, update contracts
517
+ const hasBlockers = drift.findings.some(f => f.severity === "BLOCK");
518
+ if (!hasBlockers) {
519
+ syncContracts(truthpack, repoRoot);
520
+ }
521
+
522
+ return {
523
+ isFirstRun: false,
524
+ hasDrift: drift.hasDrift,
525
+ hasBlockers,
526
+ findings: drift.findings,
527
+ contracts: current,
528
+ previous,
529
+ };
530
+ }
531
+
532
+ // =============================================================================
533
+ // DRIFT EXPLAINABILITY
534
+ // =============================================================================
535
+
536
+ /**
537
+ * Generate detailed drift explanation with remediation
538
+ */
539
+ function explainDrift(driftResult) {
540
+ const { findings, summary } = driftResult;
541
+
542
+ const explanation = {
543
+ summary: {
544
+ hasDrift: driftResult.hasDrift,
545
+ totalChanges: findings.length,
546
+ byCategory: summary,
547
+ verdict: findings.some(f => f.severity === "BLOCK") ? "BLOCK" :
548
+ findings.some(f => f.severity === "WARN") ? "WARN" : "OK",
549
+ },
550
+ changes: [],
551
+ remediation: [],
552
+ };
553
+
554
+ for (const finding of findings) {
555
+ const change = {
556
+ type: getChangeType(finding.detectorId),
557
+ severity: finding.severity,
558
+ title: finding.title,
559
+ why: finding.why || getWhyMessage(finding),
560
+ impact: getImpactMessage(finding),
561
+ remediation: getRemediationCommand(finding),
562
+ };
563
+ explanation.changes.push(change);
564
+
565
+ if (change.remediation) {
566
+ explanation.remediation.push(change.remediation);
567
+ }
568
+ }
569
+
570
+ return explanation;
571
+ }
572
+
573
+ /**
574
+ * Get change type from detector ID
575
+ */
576
+ function getChangeType(detectorId) {
577
+ const types = {
578
+ "D_DRIFT_ROUTE_NEW": "added",
579
+ "D_DRIFT_ROUTE_REMOVED": "removed",
580
+ "D_DRIFT_AUTH_REMOVED": "modified",
581
+ "D_DRIFT_ENV_NEW_REQUIRED": "added",
582
+ "D_DRIFT_ENV_REMOVED": "removed",
583
+ "D_DRIFT_ENV_PUBLIC_TO_SERVER": "modified",
584
+ "D_DRIFT_AUTH_PATTERN_REMOVED": "removed",
585
+ "D_DRIFT_AUTH_PATTERN_NEW": "added",
586
+ "D_DRIFT_EXTERNAL_NEW": "added",
587
+ "D_DRIFT_EXTERNAL_REMOVED": "removed",
588
+ };
589
+ return types[detectorId] || "unknown";
590
+ }
591
+
592
+ /**
593
+ * Get why message for finding
594
+ */
595
+ function getWhyMessage(finding) {
596
+ const messages = {
597
+ "D_DRIFT_ROUTE_NEW": "New endpoint referenced by client but not in previous contract",
598
+ "D_DRIFT_ROUTE_REMOVED": "Endpoint was in contract but no longer exists in codebase",
599
+ "D_DRIFT_AUTH_REMOVED": "Route lost auth protection - potential security regression",
600
+ "D_DRIFT_ENV_NEW_REQUIRED": "New required env var may break deployments without it",
601
+ "D_DRIFT_ENV_REMOVED": "Env var no longer used - can be cleaned up",
602
+ "D_DRIFT_AUTH_PATTERN_REMOVED": "Auth protection pattern removed from middleware",
603
+ "D_DRIFT_AUTH_PATTERN_NEW": "New route pattern added to auth middleware",
604
+ "D_DRIFT_EXTERNAL_NEW": "New external API dependency detected",
605
+ "D_DRIFT_EXTERNAL_REMOVED": "External API dependency removed",
606
+ };
607
+ return messages[finding.detectorId] || "Contract changed since last sync";
608
+ }
609
+
610
+ /**
611
+ * Get impact message for finding
612
+ */
613
+ function getImpactMessage(finding) {
614
+ if (finding.severity === "BLOCK") {
615
+ return "This change may cause runtime failures or security issues";
616
+ }
617
+ if (finding.severity === "WARN") {
618
+ return "This change should be reviewed before shipping";
619
+ }
620
+ return "This change is informational";
621
+ }
622
+
623
+ /**
624
+ * Get remediation command for finding
625
+ */
626
+ function getRemediationCommand(finding) {
627
+ const detectorId = finding.detectorId;
628
+
629
+ // If the change is intentional, sync contracts
630
+ if (detectorId.includes("NEW") || detectorId.includes("ADDED")) {
631
+ return {
632
+ ifIntentional: "vibecheck ctx sync",
633
+ description: "If this change is intentional, sync contracts",
634
+ };
635
+ }
636
+
637
+ if (detectorId.includes("REMOVED")) {
638
+ if (detectorId.includes("ROUTE")) {
639
+ return {
640
+ ifIntentional: "vibecheck ctx sync",
641
+ ifAccidental: `Add route back or remove client call to ${finding.evidence?.[0]?.path || "the endpoint"}`,
642
+ description: "Route was removed - verify this is intentional",
643
+ };
644
+ }
645
+ if (detectorId.includes("AUTH")) {
646
+ return {
647
+ ifIntentional: "vibecheck ctx sync --force",
648
+ ifAccidental: "Restore auth protection to the route",
649
+ description: "Auth was removed - this is usually NOT intentional",
650
+ };
651
+ }
652
+ }
653
+
654
+ return {
655
+ ifIntentional: "vibecheck ctx sync",
656
+ description: "Review the change and sync if intentional",
657
+ };
658
+ }
659
+
660
+ /**
661
+ * Write contracts diff to disk
662
+ */
663
+ function writeContractsDiff(repoRoot, driftResult) {
664
+ const dir = path.join(repoRoot, ".vibecheck");
665
+ fs.mkdirSync(dir, { recursive: true });
666
+
667
+ const explanation = explainDrift(driftResult);
668
+
669
+ const diff = {
670
+ meta: {
671
+ generatedAt: new Date().toISOString(),
672
+ verdict: explanation.summary.verdict,
673
+ },
674
+ ...explanation,
675
+ };
676
+
677
+ const diffPath = path.join(dir, "contracts_diff.json");
678
+ fs.writeFileSync(diffPath, JSON.stringify(diff, null, 2));
679
+
680
+ return { path: diffPath, explanation };
681
+ }
682
+
683
+ /**
684
+ * Print drift report to console
685
+ */
686
+ function printDriftReport(driftResult) {
687
+ const explanation = explainDrift(driftResult);
688
+
689
+ console.log("\n" + "=".repeat(60));
690
+ console.log("šŸ“‹ CONTRACT DRIFT REPORT");
691
+ console.log("=".repeat(60));
692
+
693
+ if (!driftResult.hasDrift) {
694
+ console.log("\nāœ… No drift detected - contracts are in sync\n");
695
+ return;
696
+ }
697
+
698
+ const verdictEmoji = {
699
+ BLOCK: "🚫",
700
+ WARN: "āš ļø",
701
+ OK: "āœ…",
702
+ }[explanation.summary.verdict];
703
+
704
+ console.log(`\nVerdict: ${verdictEmoji} ${explanation.summary.verdict}`);
705
+ console.log(`Changes: ${explanation.summary.totalChanges}`);
706
+
707
+ // Group by severity
708
+ const blockers = explanation.changes.filter(c => c.severity === "BLOCK");
709
+ const warnings = explanation.changes.filter(c => c.severity === "WARN");
710
+ const infos = explanation.changes.filter(c => c.severity === "INFO");
711
+
712
+ if (blockers.length > 0) {
713
+ console.log("\n🚫 BLOCKERS:");
714
+ for (const change of blockers) {
715
+ console.log(` ${change.type.toUpperCase()}: ${change.title}`);
716
+ console.log(` └─ Why: ${change.why}`);
717
+ if (change.remediation?.ifAccidental) {
718
+ console.log(` └─ Fix: ${change.remediation.ifAccidental}`);
719
+ }
720
+ }
721
+ }
722
+
723
+ if (warnings.length > 0) {
724
+ console.log("\nāš ļø WARNINGS:");
725
+ for (const change of warnings) {
726
+ console.log(` ${change.type.toUpperCase()}: ${change.title}`);
727
+ console.log(` └─ Why: ${change.why}`);
728
+ }
729
+ }
730
+
731
+ if (infos.length > 0) {
732
+ console.log("\nā„¹ļø INFO:");
733
+ for (const change of infos) {
734
+ console.log(` ${change.type.toUpperCase()}: ${change.title}`);
735
+ }
736
+ }
737
+
738
+ // Remediation
739
+ console.log("\n" + "-".repeat(60));
740
+ console.log("REMEDIATION:");
741
+
742
+ if (explanation.summary.verdict === "BLOCK") {
743
+ console.log("\n If changes are INTENTIONAL:");
744
+ console.log(" $ vibecheck ctx sync --force");
745
+ console.log("\n If changes are ACCIDENTAL:");
746
+ console.log(" - Review the blockers above and fix the issues");
747
+ console.log(" - Then run: vibecheck ctx sync");
748
+ } else {
749
+ console.log("\n To accept these changes:");
750
+ console.log(" $ vibecheck ctx sync");
751
+ }
752
+
753
+ console.log("\n" + "=".repeat(60) + "\n");
754
+ }
755
+
756
+ // =============================================================================
757
+ // HELPERS
758
+ // =============================================================================
759
+
760
+ function computeHash(content) {
761
+ return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
762
+ }
763
+
764
+ function countByField(items, field) {
765
+ const counts = {};
766
+ for (const item of items) {
767
+ const value = item[field] || "unknown";
768
+ counts[value] = (counts[value] || 0) + 1;
769
+ }
770
+ return counts;
771
+ }
772
+
773
+ // =============================================================================
774
+ // EXPORTS
775
+ // =============================================================================
776
+
777
+ module.exports = {
778
+ // Contract types
779
+ CONTRACT_TYPES,
780
+
781
+ // Generation
782
+ generateContracts,
783
+ generateRouteContract,
784
+ generateEnvContract,
785
+ generateAuthContract,
786
+ generateExternalContract,
787
+
788
+ // Drift detection
789
+ detectDrift,
790
+ detectRouteDrift,
791
+ detectEnvDrift,
792
+ detectAuthDrift,
793
+ detectExternalDrift,
794
+
795
+ // Drift explainability
796
+ explainDrift,
797
+ writeContractsDiff,
798
+ printDriftReport,
799
+
800
+ // Sync/Guard
801
+ syncContracts,
802
+ loadPreviousContracts,
803
+ guardContracts,
804
+ };