@vibecheckai/cli 3.4.0 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/bin/registry.js +154 -338
  2. package/bin/runners/context/generators/mcp.js +13 -15
  3. package/bin/runners/context/proof-context.js +1 -248
  4. package/bin/runners/lib/analysis-core.js +180 -198
  5. package/bin/runners/lib/analyzers.js +223 -1669
  6. package/bin/runners/lib/cli-output.js +210 -242
  7. package/bin/runners/lib/detectors-v2.js +785 -547
  8. package/bin/runners/lib/entitlements-v2.js +458 -96
  9. package/bin/runners/lib/error-handler.js +9 -16
  10. package/bin/runners/lib/global-flags.js +0 -37
  11. package/bin/runners/lib/route-truth.js +322 -1167
  12. package/bin/runners/lib/scan-output.js +469 -448
  13. package/bin/runners/lib/ship-output.js +27 -280
  14. package/bin/runners/lib/terminal-ui.js +733 -231
  15. package/bin/runners/lib/truth.js +321 -1004
  16. package/bin/runners/lib/unified-output.js +158 -162
  17. package/bin/runners/lib/upsell.js +204 -104
  18. package/bin/runners/runAllowlist.js +324 -0
  19. package/bin/runners/runAuth.js +95 -324
  20. package/bin/runners/runCheckpoint.js +21 -39
  21. package/bin/runners/runContext.js +24 -136
  22. package/bin/runners/runDoctor.js +67 -115
  23. package/bin/runners/runEvidencePack.js +219 -0
  24. package/bin/runners/runFix.js +5 -6
  25. package/bin/runners/runGuard.js +118 -212
  26. package/bin/runners/runInit.js +2 -14
  27. package/bin/runners/runInstall.js +281 -0
  28. package/bin/runners/runLabs.js +341 -0
  29. package/bin/runners/runMcp.js +52 -130
  30. package/bin/runners/runPolish.js +20 -43
  31. package/bin/runners/runProve.js +3 -13
  32. package/bin/runners/runReality.js +0 -14
  33. package/bin/runners/runReport.js +2 -3
  34. package/bin/runners/runScan.js +44 -511
  35. package/bin/runners/runShip.js +14 -28
  36. package/bin/runners/runValidate.js +2 -19
  37. package/bin/runners/runWatch.js +54 -118
  38. package/bin/vibecheck.js +41 -148
  39. package/mcp-server/ARCHITECTURE.md +339 -0
  40. package/mcp-server/__tests__/cache.test.ts +313 -0
  41. package/mcp-server/__tests__/executor.test.ts +239 -0
  42. package/mcp-server/__tests__/fixtures/exclusion-test/.cache/webpack/cache.pack +1 -0
  43. package/mcp-server/__tests__/fixtures/exclusion-test/.next/server/chunk.js +3 -0
  44. package/mcp-server/__tests__/fixtures/exclusion-test/.turbo/cache.json +3 -0
  45. package/mcp-server/__tests__/fixtures/exclusion-test/.venv/lib/env.py +3 -0
  46. package/mcp-server/__tests__/fixtures/exclusion-test/dist/bundle.js +3 -0
  47. package/mcp-server/__tests__/fixtures/exclusion-test/package.json +5 -0
  48. package/mcp-server/__tests__/fixtures/exclusion-test/src/app.ts +5 -0
  49. package/mcp-server/__tests__/fixtures/exclusion-test/venv/lib/config.py +4 -0
  50. package/mcp-server/__tests__/ids.test.ts +345 -0
  51. package/mcp-server/__tests__/integration/tools.test.ts +410 -0
  52. package/mcp-server/__tests__/registry.test.ts +365 -0
  53. package/mcp-server/__tests__/sandbox.test.ts +323 -0
  54. package/mcp-server/__tests__/schemas.test.ts +372 -0
  55. package/mcp-server/benchmarks/run-benchmarks.ts +304 -0
  56. package/mcp-server/examples/doctor.request.json +14 -0
  57. package/mcp-server/examples/doctor.response.json +53 -0
  58. package/mcp-server/examples/error.response.json +15 -0
  59. package/mcp-server/examples/scan.request.json +14 -0
  60. package/mcp-server/examples/scan.response.json +108 -0
  61. package/mcp-server/handlers/tool-handler.ts +671 -0
  62. package/mcp-server/index-v3.ts +293 -0
  63. package/mcp-server/index.js +1072 -1573
  64. package/mcp-server/index.old.js +4137 -0
  65. package/mcp-server/lib/cache.ts +341 -0
  66. package/mcp-server/lib/errors.ts +346 -0
  67. package/mcp-server/lib/executor.ts +792 -0
  68. package/mcp-server/lib/ids.ts +238 -0
  69. package/mcp-server/lib/logger.ts +368 -0
  70. package/mcp-server/lib/metrics.ts +365 -0
  71. package/mcp-server/lib/sandbox.ts +337 -0
  72. package/mcp-server/lib/validator.ts +229 -0
  73. package/mcp-server/package-lock.json +165 -0
  74. package/mcp-server/package.json +32 -7
  75. package/mcp-server/premium-tools.js +2 -2
  76. package/mcp-server/registry/tools.json +476 -0
  77. package/mcp-server/schemas/error-envelope.schema.json +125 -0
  78. package/mcp-server/schemas/finding.schema.json +167 -0
  79. package/mcp-server/schemas/report-artifact.schema.json +88 -0
  80. package/mcp-server/schemas/run-request.schema.json +75 -0
  81. package/mcp-server/schemas/verdict.schema.json +168 -0
  82. package/mcp-server/tier-auth.d.ts +71 -0
  83. package/mcp-server/tier-auth.js +371 -183
  84. package/mcp-server/truth-context.js +90 -131
  85. package/mcp-server/truth-firewall-tools.js +1000 -1611
  86. package/mcp-server/tsconfig.json +34 -0
  87. package/mcp-server/vibecheck-tools.js +2 -2
  88. package/mcp-server/vitest.config.ts +16 -0
  89. package/package.json +3 -4
  90. package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +0 -474
  91. package/bin/runners/lib/agent-firewall/change-packet/builder.js +0 -488
  92. package/bin/runners/lib/agent-firewall/change-packet/schema.json +0 -228
  93. package/bin/runners/lib/agent-firewall/change-packet/store.js +0 -200
  94. package/bin/runners/lib/agent-firewall/claims/claim-types.js +0 -21
  95. package/bin/runners/lib/agent-firewall/claims/extractor.js +0 -303
  96. package/bin/runners/lib/agent-firewall/claims/patterns.js +0 -24
  97. package/bin/runners/lib/agent-firewall/critic/index.js +0 -151
  98. package/bin/runners/lib/agent-firewall/critic/judge.js +0 -432
  99. package/bin/runners/lib/agent-firewall/critic/prompts.js +0 -305
  100. package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +0 -88
  101. package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +0 -75
  102. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +0 -127
  103. package/bin/runners/lib/agent-firewall/evidence/resolver.js +0 -102
  104. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +0 -213
  105. package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +0 -145
  106. package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +0 -19
  107. package/bin/runners/lib/agent-firewall/fs-hook/installer.js +0 -87
  108. package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +0 -184
  109. package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +0 -163
  110. package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +0 -107
  111. package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +0 -68
  112. package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +0 -66
  113. package/bin/runners/lib/agent-firewall/interceptor/base.js +0 -304
  114. package/bin/runners/lib/agent-firewall/interceptor/cursor.js +0 -35
  115. package/bin/runners/lib/agent-firewall/interceptor/vscode.js +0 -35
  116. package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +0 -34
  117. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +0 -465
  118. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +0 -604
  119. package/bin/runners/lib/agent-firewall/lawbook/index.js +0 -304
  120. package/bin/runners/lib/agent-firewall/lawbook/registry.js +0 -514
  121. package/bin/runners/lib/agent-firewall/lawbook/schema.js +0 -420
  122. package/bin/runners/lib/agent-firewall/logger.js +0 -141
  123. package/bin/runners/lib/agent-firewall/policy/default-policy.json +0 -90
  124. package/bin/runners/lib/agent-firewall/policy/engine.js +0 -103
  125. package/bin/runners/lib/agent-firewall/policy/loader.js +0 -451
  126. package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +0 -50
  127. package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +0 -50
  128. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +0 -86
  129. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +0 -162
  130. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +0 -189
  131. package/bin/runners/lib/agent-firewall/policy/rules/scope.js +0 -93
  132. package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +0 -57
  133. package/bin/runners/lib/agent-firewall/policy/schema.json +0 -183
  134. package/bin/runners/lib/agent-firewall/policy/verdict.js +0 -54
  135. package/bin/runners/lib/agent-firewall/proposal/extractor.js +0 -394
  136. package/bin/runners/lib/agent-firewall/proposal/index.js +0 -212
  137. package/bin/runners/lib/agent-firewall/proposal/schema.js +0 -251
  138. package/bin/runners/lib/agent-firewall/proposal/validator.js +0 -386
  139. package/bin/runners/lib/agent-firewall/reality/index.js +0 -332
  140. package/bin/runners/lib/agent-firewall/reality/state.js +0 -625
  141. package/bin/runners/lib/agent-firewall/reality/watcher.js +0 -322
  142. package/bin/runners/lib/agent-firewall/risk/index.js +0 -173
  143. package/bin/runners/lib/agent-firewall/risk/scorer.js +0 -328
  144. package/bin/runners/lib/agent-firewall/risk/thresholds.js +0 -321
  145. package/bin/runners/lib/agent-firewall/risk/vectors.js +0 -421
  146. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +0 -472
  147. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +0 -346
  148. package/bin/runners/lib/agent-firewall/simulator/index.js +0 -181
  149. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +0 -380
  150. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +0 -661
  151. package/bin/runners/lib/agent-firewall/time-machine/index.js +0 -267
  152. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +0 -436
  153. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +0 -490
  154. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +0 -530
  155. package/bin/runners/lib/agent-firewall/truthpack/index.js +0 -67
  156. package/bin/runners/lib/agent-firewall/truthpack/loader.js +0 -137
  157. package/bin/runners/lib/agent-firewall/unblock/planner.js +0 -337
  158. package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +0 -118
  159. package/bin/runners/lib/api-client.js +0 -269
  160. package/bin/runners/lib/authority-badge.js +0 -425
  161. package/bin/runners/lib/engines/accessibility-engine.js +0 -190
  162. package/bin/runners/lib/engines/api-consistency-engine.js +0 -162
  163. package/bin/runners/lib/engines/ast-cache.js +0 -99
  164. package/bin/runners/lib/engines/code-quality-engine.js +0 -255
  165. package/bin/runners/lib/engines/console-logs-engine.js +0 -115
  166. package/bin/runners/lib/engines/cross-file-analysis-engine.js +0 -268
  167. package/bin/runners/lib/engines/dead-code-engine.js +0 -198
  168. package/bin/runners/lib/engines/deprecated-api-engine.js +0 -226
  169. package/bin/runners/lib/engines/empty-catch-engine.js +0 -150
  170. package/bin/runners/lib/engines/file-filter.js +0 -131
  171. package/bin/runners/lib/engines/hardcoded-secrets-engine.js +0 -251
  172. package/bin/runners/lib/engines/mock-data-engine.js +0 -272
  173. package/bin/runners/lib/engines/parallel-processor.js +0 -71
  174. package/bin/runners/lib/engines/performance-issues-engine.js +0 -265
  175. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +0 -243
  176. package/bin/runners/lib/engines/todo-fixme-engine.js +0 -115
  177. package/bin/runners/lib/engines/type-aware-engine.js +0 -152
  178. package/bin/runners/lib/engines/unsafe-regex-engine.js +0 -225
  179. package/bin/runners/lib/engines/vibecheck-engines/README.md +0 -53
  180. package/bin/runners/lib/engines/vibecheck-engines/index.js +0 -15
  181. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +0 -164
  182. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +0 -291
  183. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +0 -83
  184. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +0 -198
  185. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +0 -275
  186. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +0 -167
  187. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +0 -217
  188. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +0 -139
  189. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +0 -140
  190. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +0 -164
  191. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +0 -234
  192. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +0 -217
  193. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +0 -78
  194. package/bin/runners/lib/engines/vibecheck-engines/package.json +0 -13
  195. package/bin/runners/lib/exit-codes.js +0 -275
  196. package/bin/runners/lib/fingerprint.js +0 -377
  197. package/bin/runners/lib/help-formatter.js +0 -413
  198. package/bin/runners/lib/logger.js +0 -38
  199. package/bin/runners/lib/ship-output-enterprise.js +0 -239
  200. package/bin/runners/lib/unified-cli-output.js +0 -604
  201. package/bin/runners/runAgent.d.ts +0 -5
  202. package/bin/runners/runAgent.js +0 -161
  203. package/bin/runners/runApprove.js +0 -1200
  204. package/bin/runners/runClassify.js +0 -859
  205. package/bin/runners/runContext.d.ts +0 -4
  206. package/bin/runners/runFirewall.d.ts +0 -5
  207. package/bin/runners/runFirewall.js +0 -134
  208. package/bin/runners/runFirewallHook.d.ts +0 -5
  209. package/bin/runners/runFirewallHook.js +0 -56
  210. package/bin/runners/runPolish.d.ts +0 -4
  211. package/bin/runners/runProof.zip +0 -0
  212. package/bin/runners/runTruth.d.ts +0 -5
  213. package/bin/runners/runTruth.js +0 -101
  214. package/mcp-server/HARDENING_SUMMARY.md +0 -299
  215. package/mcp-server/agent-firewall-interceptor.js +0 -500
  216. package/mcp-server/authority-tools.js +0 -569
  217. package/mcp-server/conductor/conflict-resolver.js +0 -588
  218. package/mcp-server/conductor/execution-planner.js +0 -544
  219. package/mcp-server/conductor/index.js +0 -377
  220. package/mcp-server/conductor/lock-manager.js +0 -615
  221. package/mcp-server/conductor/request-queue.js +0 -550
  222. package/mcp-server/conductor/session-manager.js +0 -500
  223. package/mcp-server/conductor/tools.js +0 -510
  224. package/mcp-server/lib/api-client.cjs +0 -13
  225. package/mcp-server/lib/logger.cjs +0 -30
  226. package/mcp-server/logger.js +0 -173
  227. package/mcp-server/tools-v3.js +0 -706
  228. package/mcp-server/vibecheck-mcp-server-3.2.0.tgz +0 -0
@@ -1,79 +1,25 @@
1
1
  /**
2
- * Truth Firewall MCP Tools (Enhanced)
3
- *
4
- * Goals:
5
- * - Evidence-first outputs (file/line/snippet + stable hash)
6
- * - Policy-aware enforcement (strict/balanced/permissive)
7
- * - Project fingerprint invalidates stale claims
8
- * - Safe filesystem access (no path traversal)
9
- * - Patch verification with allowlist + timeouts
10
- * - Faster evidence search (optional ripgrep)
11
- */
12
-
13
- import fs from "fs/promises";
14
- import fssync from "fs";
15
- import path from "path";
16
- import crypto from "crypto";
17
- import { execSync, spawnSync } from "child_process";
18
- import { createRequire } from "module";
19
-
20
- // Route Truth v1 integration - AST-based route extraction (Fastify + Next.js)
21
- const require = createRequire(import.meta.url);
22
- const { RouteIndex, validateRouteExists: routeTruthValidate, canonicalizePath: routeTruthCanonicalize } = require("../bin/runners/lib/route-truth.js");
23
-
24
- // =============================================================================
25
- // TYPES (JSDoc for IDE support)
26
- // =============================================================================
27
-
28
- /**
29
- * @typedef {"strict" | "balanced" | "permissive"} PolicyName
30
- * @typedef {"true" | "false" | "unknown"} ClaimResultValue
31
- * @typedef {"high" | "medium" | "med" | "low" | number} ConfidenceLabel
2
+ * Truth Firewall MCP Tools
32
3
  *
33
- * @typedef {Object} EvidenceItem
34
- * @property {string} file
35
- * @property {number} [line] - start line
36
- * @property {string} [lines] - "12-18"
37
- * @property {string} [snippet]
38
- * @property {string} [hash]
39
- * @property {number} [confidence]
4
+ * The "best-in-world" hallucination stopper toolkit.
5
+ * Agents cannot work without these tools - they enforce truth.
40
6
  *
41
- * @typedef {Object} NormalizedEvidence
42
- * @property {string} file
43
- * @property {number} line
44
- * @property {string} [lines]
45
- * @property {string} snippet
46
- * @property {string} hash
47
- * @property {number} confidence
48
- *
49
- * @typedef {Object} EnforcementDecisionAllowed
50
- * @property {true} allowed
51
- * @property {number} confidence
52
- * @property {string} [reason]
53
- *
54
- * @typedef {Object} EnforcementDecisionBlocked
55
- * @property {false} allowed
56
- * @property {number} [confidence]
57
- * @property {string} reason
58
- * @property {string} [suggestion]
59
- * @property {string[]} [blockedActions]
60
- *
61
- * @typedef {EnforcementDecisionAllowed | EnforcementDecisionBlocked} EnforcementDecision
62
- *
63
- * @typedef {Object} ProjectFingerprint
64
- * @property {string} hash
65
- * @property {string} commitHash
66
- * @property {string[]} fileHashes
67
- * @property {string} generatedAt
68
- *
69
- * @typedef {Object} ToolResponseMeta
70
- * @property {true} ok
71
- * @property {string} version
72
- * @property {ProjectFingerprint} projectFingerprint
73
- * @property {string} attribution
74
- * @property {string} generatedAt
7
+ * Core Tools:
8
+ * get_truthpack() - Get the truth pack for this repo
9
+ * compile_context(task) - Get task-targeted context
10
+ * validate_claim(claim) - Verify a claim has evidence (CRITICAL)
11
+ * search_evidence(query) - Find evidence for a claim
12
+ * find_counterexamples() - Falsify a claim
13
+ * propose_patch() - Create proof-carrying patch
14
+ * verify_patch() - Verify a patch meets requirements
15
+ * check_invariants() - Check all invariants
75
16
  */
76
17
 
18
+ import fs from 'fs/promises';
19
+ import path from 'path';
20
+ import crypto from 'crypto';
21
+ import { execSync } from 'child_process';
22
+
77
23
  // =============================================================================
78
24
  // TOOL DEFINITIONS
79
25
  // =============================================================================
@@ -81,36 +27,42 @@ const { RouteIndex, validateRouteExists: routeTruthValidate, canonicalizePath: r
81
27
  export const TRUTH_FIREWALL_TOOLS = [
82
28
  {
83
29
  name: "vibecheck.get_truthpack",
84
- description: `📦 Get the Truth Pack — verified ground truth about this codebase.
30
+ description: `📦 Get the Truth Pack — the verified ground truth about this codebase.
85
31
 
86
32
  Returns evidence-backed facts about routes, auth, billing, env vars, and schema.
87
- Every claim should point to files/lines with confidence scores.
33
+ Every claim has file/line citations and confidence scores.
88
34
 
89
- Use this BEFORE making assertions about the repo.`,
35
+ ⚠️ CRITICAL: Use this BEFORE making any assertions about the codebase.`,
90
36
  inputSchema: {
91
37
  type: "object",
92
38
  properties: {
93
39
  scope: {
94
40
  type: "string",
95
41
  enum: ["all", "routes", "auth", "billing", "env", "schema", "graph"],
42
+ description: "What to include (default: all)",
96
43
  default: "all",
97
44
  },
98
- refresh: { type: "boolean", default: false },
45
+ refresh: {
46
+ type: "boolean",
47
+ description: "Force recompile even if cached (default: false)",
48
+ default: false,
49
+ },
99
50
  },
100
51
  },
101
52
  },
102
-
53
+
103
54
  {
104
55
  name: "vibecheck.validate_claim",
105
56
  description: `🔍 TRUTH FIREWALL — Validate a claim before acting on it.
106
57
 
107
- Returns: true | false | unknown + enforcement decision
108
- Policy matters:
109
- - strict/balanced: unknown blocks dependent actions
110
- - permissive: unknown allowed (but flagged)
58
+ Returns: true | false | unknown
59
+ - If 'unknown': you MUST NOT proceed with actions that depend on this claim
60
+ - If 'false': the claim is disproven, do not proceed
61
+ - If 'true': proceed with evidence citations
111
62
 
112
63
  Examples:
113
64
  { "claim": "route_exists", "subject": { "method": "POST", "path": "/api/login" } }
65
+ { "claim": "auth_enforced", "subject": { "path": "/dashboard" } }
114
66
  { "claim": "env_var_exists", "subject": { "name": "STRIPE_SECRET_KEY" } }`,
115
67
  inputSchema: {
116
68
  type: "object",
@@ -119,7 +71,7 @@ Examples:
119
71
  type: "string",
120
72
  enum: [
121
73
  "route_exists",
122
- "route_guarded",
74
+ "route_guarded",
123
75
  "env_var_exists",
124
76
  "env_var_used",
125
77
  "middleware_applied",
@@ -130,358 +82,371 @@ Examples:
130
82
  "model_exists",
131
83
  "component_exists",
132
84
  ],
85
+ description: "Type of claim to verify",
133
86
  },
134
87
  subject: {
135
88
  type: "object",
89
+ description: "What the claim is about",
136
90
  properties: {
137
- method: { type: "string" },
138
- path: { type: "string" },
139
- name: { type: "string" },
91
+ method: { type: "string", description: "HTTP method (for routes)" },
92
+ path: { type: "string", description: "Route path or file path" },
93
+ name: { type: "string", description: "Name of function/component/env var" },
140
94
  },
141
95
  },
142
- expected: { type: "boolean", default: true },
143
- policy: {
144
- type: "string",
145
- enum: ["strict", "balanced", "permissive"],
146
- default: "strict",
147
- },
148
- refresh: {
96
+ expected: {
149
97
  type: "boolean",
150
- default: false,
151
- description: "Force refresh of underlying truthpack/contracts before verifying",
98
+ description: "Expected result (default: true)",
99
+ default: true,
152
100
  },
153
101
  },
154
102
  required: ["claim", "subject"],
155
103
  },
156
104
  },
157
-
105
+
158
106
  {
159
107
  name: "vibecheck.compile_context",
160
- description: `🎯 Get minimal sufficient context for a task (not a token bomb).
108
+ description: `🎯 Get task-targeted context — minimal sufficient context for your task.
161
109
 
162
- Returns relevant nodes, edges, evidence, and invariants.`,
110
+ Big context = noise. Small context = missing facts.
111
+ This compiles exactly what you need for the task.
112
+
113
+ Returns relevant nodes, edges, evidence, and applicable invariants.`,
163
114
  inputSchema: {
164
115
  type: "object",
165
116
  properties: {
166
- task: { type: "string" },
167
- files: { type: "array", items: { type: "string" } },
168
- policy: { type: "string", enum: ["strict", "balanced", "permissive"], default: "balanced" },
169
- maxItems: { type: "number", default: 50 },
117
+ task: {
118
+ type: "string",
119
+ description: "What you're trying to do (e.g., 'fix dead login button', 'add Stripe checkout')",
120
+ },
121
+ files: {
122
+ type: "array",
123
+ items: { type: "string" },
124
+ description: "Specific files to include",
125
+ },
126
+ policy: {
127
+ type: "string",
128
+ enum: ["strict", "balanced", "permissive"],
129
+ description: "How strict to be about context (default: balanced)",
130
+ default: "balanced",
131
+ },
170
132
  },
171
133
  required: ["task"],
172
134
  },
173
135
  },
174
-
136
+
175
137
  {
176
138
  name: "vibecheck.search_evidence",
177
139
  description: `📎 Search for evidence in the codebase.
178
140
 
179
- Supports text mode or regex mode. Returns file/line/snippet + hashes.`,
141
+ Returns code snippets with file/line citations.
142
+ Use this to find proof for claims.`,
180
143
  inputSchema: {
181
144
  type: "object",
182
145
  properties: {
183
- query: { type: "string" },
184
- mode: { type: "string", enum: ["text", "regex"], default: "text" },
185
- type: { type: "string", enum: ["route", "handler", "middleware", "component", "env_var", "model", "any"], default: "any" },
186
- limit: { type: "number", default: 10 },
187
- caseSensitive: { type: "boolean", default: false },
188
- includeTests: { type: "boolean", default: false },
146
+ query: {
147
+ type: "string",
148
+ description: "What to search for (e.g., 'login handler', 'auth middleware', 'Stripe webhook')",
149
+ },
150
+ type: {
151
+ type: "string",
152
+ enum: ["route", "handler", "middleware", "component", "env_var", "model", "any"],
153
+ description: "Type of evidence to find (default: any)",
154
+ default: "any",
155
+ },
156
+ limit: {
157
+ type: "number",
158
+ description: "Max results (default: 10)",
159
+ default: 10,
160
+ },
189
161
  },
190
162
  required: ["query"],
191
163
  },
192
164
  },
193
-
165
+
194
166
  {
195
167
  name: "vibecheck.find_counterexamples",
196
- description: `🔴 FALSIFICATION — find counterexamples that disprove a claim.
168
+ description: `🔴 Find counterexamples that would disprove a claim.
169
+
170
+ This is the FALSIFICATION mechanism.
171
+ If counterexamples exist, the claim becomes false or low confidence.
197
172
 
198
- Use for auth, billing, security.`,
173
+ Use this for high-stakes claims about auth, billing, security.`,
199
174
  inputSchema: {
200
175
  type: "object",
201
176
  properties: {
202
- claim: { type: "string", enum: ["auth_enforced", "billing_gate_exists", "route_guarded", "no_bypass"] },
177
+ claim: {
178
+ type: "string",
179
+ enum: ["auth_enforced", "billing_gate_exists", "route_guarded", "no_bypass"],
180
+ description: "Claim to falsify",
181
+ },
203
182
  subject: {
204
183
  type: "object",
205
- properties: { path: { type: "string" }, name: { type: "string" } },
184
+ description: "What the claim is about",
185
+ properties: {
186
+ path: { type: "string" },
187
+ name: { type: "string" },
188
+ },
206
189
  },
207
- policy: { type: "string", enum: ["strict", "balanced", "permissive"], default: "strict" },
208
190
  },
209
191
  required: ["claim", "subject"],
210
192
  },
211
193
  },
212
-
194
+
213
195
  {
214
196
  name: "vibecheck.propose_patch",
215
197
  description: `📝 Propose a proof-carrying patch.
216
198
 
217
- Patches must attach:
218
- - findings fixed
219
- - claim dependencies (validated)
220
- - verification commands
199
+ When proposing changes, you MUST attach:
200
+ - Which findings it fixes
201
+ - Which claims it depends on (must be verified)
202
+ - Evidence references
203
+ - Verification commands
221
204
 
222
205
  Patches without proof are NOT eligible for auto-apply.`,
223
206
  inputSchema: {
224
207
  type: "object",
225
208
  properties: {
226
- diff: { type: "string" },
227
- fixes: { type: "array", items: { type: "string" } },
228
- claims: { type: "array", items: { type: "string" } },
229
- verification: { type: "array", items: { type: "string" } },
230
- policy: { type: "string", enum: ["strict", "balanced", "permissive"], default: "strict" },
231
- save: { type: "boolean", default: true },
209
+ diff: {
210
+ type: "string",
211
+ description: "The diff content",
212
+ },
213
+ fixes: {
214
+ type: "array",
215
+ items: { type: "string" },
216
+ description: "Finding IDs this patch fixes",
217
+ },
218
+ claims: {
219
+ type: "array",
220
+ items: { type: "string" },
221
+ description: "Claim IDs this patch depends on (must all be verified)",
222
+ },
223
+ verification: {
224
+ type: "array",
225
+ items: { type: "string" },
226
+ description: "Commands to verify the patch (e.g., 'vibecheck ship', 'pnpm test')",
227
+ },
232
228
  },
233
229
  required: ["diff", "fixes"],
234
230
  },
235
231
  },
236
-
237
- {
238
- name: "vibecheck.verify_patch",
239
- description: `✅ Verify a patch meets requirements.
240
-
241
- Runs verification commands with allowlist + timeouts.
242
- Returns pass/fail and command output (truncated).`,
243
- inputSchema: {
244
- type: "object",
245
- properties: {
246
- patchId: { type: "string", description: "Patch ID from propose_patch (optional)" },
247
- diff: { type: "string", description: "If no patchId, provide diff text (not auto-applied)" },
248
- commands: { type: "array", items: { type: "string" }, description: "Commands to run" },
249
- policy: { type: "string", enum: ["strict", "balanced", "permissive"], default: "strict" },
250
- timeoutMs: { type: "number", default: 120000 },
251
- },
252
- required: ["commands"],
253
- },
254
- },
255
-
232
+
256
233
  {
257
234
  name: "vibecheck.check_invariants",
258
- description: `⚖️ Check invariants (ship-killer rules).
235
+ description: `⚖️ Check all invariants (product religion rules).
259
236
 
260
- Examples:
261
- - No paid feature without server enforcement
237
+ Returns:
238
+ - Ship killers: BLOCK deployment
239
+ - Warnings: Require acknowledgment
240
+
241
+ Invariants include:
242
+ - No paid feature without server-side enforcement
262
243
  - No success UI without confirmed success
263
- - No silent catch in auth/billing`,
244
+ - No route reference without matching route map entry
245
+ - No silent catch in auth/billing flows`,
264
246
  inputSchema: {
265
247
  type: "object",
266
248
  properties: {
267
- category: { type: "string", enum: ["all", "auth", "billing", "security", "ux", "api"], default: "all" },
268
- policy: { type: "string", enum: ["strict", "balanced", "permissive"], default: "strict" },
249
+ category: {
250
+ type: "string",
251
+ enum: ["all", "auth", "billing", "security", "ux", "api"],
252
+ description: "Category to check (default: all)",
253
+ default: "all",
254
+ },
269
255
  },
270
256
  },
271
257
  },
272
-
258
+
273
259
  {
274
260
  name: "vibecheck.add_assumption",
275
- description: `⚠️ Log an assumption (budget enforced).`,
261
+ description: `⚠️ Log an assumption (with budget enforcement).
262
+
263
+ Assumptions stack up and cause failures. Track them explicitly.
264
+
265
+ Rules:
266
+ - Max 2 assumptions per mission
267
+ - Must provide verification steps
268
+ - If budget exceeded, you MUST use proof tooling instead`,
276
269
  inputSchema: {
277
270
  type: "object",
278
271
  properties: {
279
- description: { type: "string" },
280
- reason: { type: "string" },
281
- verificationSteps: { type: "array", items: { type: "string" } },
272
+ description: {
273
+ type: "string",
274
+ description: "What you're assuming",
275
+ },
276
+ reason: {
277
+ type: "string",
278
+ description: "Why you need this assumption",
279
+ },
280
+ verificationSteps: {
281
+ type: "array",
282
+ items: { type: "string" },
283
+ description: "How to verify this assumption later",
284
+ },
282
285
  },
283
286
  required: ["description", "verificationSteps"],
284
287
  },
285
288
  },
286
-
289
+
287
290
  {
288
291
  name: "vibecheck.validate_plan",
289
- description: `🛡️ Validate an AI plan against contracts (routes/env/auth/external).
292
+ description: `🛡️ HALLUCINATION STOPPER — Validate an AI plan against contracts.
293
+
294
+ Before executing a plan, you MUST validate it against contracts.
295
+ Rejects plans that:
296
+ - Reference routes not in routes.json
297
+ - Use env vars not in env.json
298
+ - Make auth assumptions that contradict auth.json
299
+ - Reference external services not in external.json
300
+
301
+ ⚠️ CRITICAL: If validation fails, you MUST NOT proceed with the plan.
302
+
303
+ Example:
304
+ { "plan": "Create POST /api/checkout endpoint using STRIPE_SECRET_KEY" }
290
305
 
291
- If validation fails: do NOT proceed.`,
306
+ Returns: { valid: boolean, violations: [], warnings: [], suggestions: [] }`,
292
307
  inputSchema: {
293
308
  type: "object",
294
309
  properties: {
295
- plan: { type: "string" },
296
- strict: { type: "boolean", default: false },
310
+ plan: {
311
+ type: "string",
312
+ description: "The plan text or JSON to validate",
313
+ },
314
+ strict: {
315
+ type: "boolean",
316
+ description: "If true, warnings also cause rejection (default: false)",
317
+ default: false,
318
+ },
297
319
  },
298
320
  required: ["plan"],
299
321
  },
300
322
  },
301
-
323
+
302
324
  {
303
325
  name: "vibecheck.check_drift",
304
- description: `📊 Detect contract drift (routes/env/auth/external).`,
326
+ description: `📊 Check for contract drift — detect when code has changed but contracts are stale.
327
+
328
+ Returns drift findings for:
329
+ - Routes added/removed but not in contract
330
+ - Env vars used but not declared
331
+ - Auth patterns changed
332
+ - External services added
333
+
334
+ Per spec: routes/env/auth drift → BLOCK (AI will lie about these)`,
305
335
  inputSchema: {
306
336
  type: "object",
307
337
  properties: {
308
- category: { type: "string", enum: ["all", "routes", "env", "auth", "external"], default: "all" },
338
+ category: {
339
+ type: "string",
340
+ enum: ["all", "routes", "env", "auth", "external"],
341
+ description: "Category to check (default: all)",
342
+ default: "all",
343
+ },
309
344
  },
310
345
  },
311
346
  },
312
-
347
+
313
348
  {
314
349
  name: "vibecheck.get_contracts",
315
- description: `📜 Get contracts from .vibecheck/contracts/`,
350
+ description: `📜 Get the current contracts (routes/env/auth/external).
351
+
352
+ Contracts are the "you may not lie" rules for this repo.
353
+ AI output must satisfy these contracts.
354
+
355
+ Returns the contract files from .vibecheck/contracts/`,
316
356
  inputSchema: {
317
357
  type: "object",
318
358
  properties: {
319
- type: { type: "string", enum: ["all", "routes", "env", "auth", "external"], default: "all" },
359
+ type: {
360
+ type: "string",
361
+ enum: ["all", "routes", "env", "auth", "external"],
362
+ description: "Which contract to get (default: all)",
363
+ default: "all",
364
+ },
320
365
  },
321
366
  },
322
367
  },
323
368
  ];
324
369
 
325
370
  // =============================================================================
326
- // TOOL HANDLER
371
+ // TOOL HANDLERS
327
372
  // =============================================================================
328
373
 
329
374
  export async function handleTruthFirewallTool(toolName, args, projectPath = process.cwd()) {
330
375
  switch (toolName) {
331
376
  case "vibecheck.get_truthpack":
332
- return wrapMcpResponse(await getTruthPack(projectPath, args), projectPath);
333
-
377
+ return await getTruthPack(projectPath, args);
378
+
334
379
  case "vibecheck.validate_claim":
335
- return wrapMcpResponse(await validateClaim(projectPath, args), projectPath);
336
-
380
+ return await validateClaim(projectPath, args);
381
+
337
382
  case "vibecheck.compile_context":
338
- return wrapMcpResponse(await compileContext(projectPath, args), projectPath);
339
-
383
+ return await compileContext(projectPath, args);
384
+
340
385
  case "vibecheck.search_evidence":
341
- return wrapMcpResponse(await searchEvidence(projectPath, args), projectPath);
342
-
386
+ return await searchEvidence(projectPath, args);
387
+
343
388
  case "vibecheck.find_counterexamples":
344
- return wrapMcpResponse(await findCounterexamples(projectPath, args), projectPath);
345
-
389
+ return await findCounterexamples(projectPath, args);
390
+
346
391
  case "vibecheck.propose_patch":
347
- return wrapMcpResponse(await proposePatch(projectPath, args), projectPath);
348
-
349
- case "vibecheck.verify_patch":
350
- return wrapMcpResponse(await verifyPatch(projectPath, args), projectPath);
351
-
392
+ return await proposePatch(projectPath, args);
393
+
352
394
  case "vibecheck.check_invariants":
353
- return wrapMcpResponse(await checkInvariants(projectPath, args), projectPath);
354
-
395
+ return await checkInvariants(projectPath, args);
396
+
355
397
  case "vibecheck.add_assumption":
356
- return wrapMcpResponse(await addAssumption(projectPath, args), projectPath);
357
-
398
+ return await addAssumption(projectPath, args);
399
+
358
400
  case "vibecheck.validate_plan":
359
- return wrapMcpResponse(await getPlanValidationResult(projectPath, args), projectPath);
360
-
401
+ return await validatePlanTool(projectPath, args);
402
+
361
403
  case "vibecheck.check_drift":
362
- return wrapMcpResponse(await checkDriftTool(projectPath, args), projectPath);
363
-
404
+ return await checkDriftTool(projectPath, args);
405
+
364
406
  case "vibecheck.get_contracts":
365
- return wrapMcpResponse(await getContractsTool(projectPath, args), projectPath);
366
-
407
+ return await getContractsTool(projectPath, args);
408
+
367
409
  default:
368
- return wrapMcpResponse({ error: `Unknown tool: ${toolName}` }, projectPath);
410
+ return { error: `Unknown tool: ${toolName}` };
369
411
  }
370
412
  }
371
413
 
372
414
  // =============================================================================
373
- // STATE (Per-project aware)
415
+ // IMPLEMENTATION
374
416
  // =============================================================================
375
417
 
376
- /**
377
- * @typedef {Object} CachedClaim
378
- * @property {string} projectHash
379
- * @property {number} timestamp
380
- * @property {*} result
381
- */
382
-
418
+ // In-memory state
383
419
  const state = {
384
- truthPackByProject: new Map(),
385
- assumptionsByProject: new Map(),
420
+ truthPack: null,
421
+ assumptions: [],
386
422
  verifiedClaims: new Map(),
387
- lastValidationByProject: new Map(),
388
- routeIndexByProject: new Map(), // Route Truth v1 index cache
389
423
  maxAssumptions: 2,
424
+ lastValidationByProject: new Map(),
390
425
  };
391
426
 
392
- const MAX_EVIDENCE_SNIPPET = 240;
393
- const MAX_CMD_OUTPUT = 12_000;
394
-
395
- // =============================================================================
396
- // POLICY CONFIG
397
- // =============================================================================
398
-
399
- const POLICY_CONFIG = {
400
- strict: {
401
- minConfidence: 0.8,
402
- allowUnknown: false,
403
- requireValidation: true,
404
- blockOnDrift: true,
405
- validationTTL: 5 * 60 * 1000,
406
- },
407
- balanced: {
408
- minConfidence: 0.6,
409
- allowUnknown: false,
410
- requireValidation: true,
411
- blockOnDrift: false,
412
- validationTTL: 10 * 60 * 1000,
413
- },
414
- permissive: {
415
- minConfidence: 0.4,
416
- allowUnknown: true,
417
- requireValidation: false,
418
- blockOnDrift: false,
419
- validationTTL: 30 * 60 * 1000,
420
- },
421
- };
422
-
423
- export function getPolicyConfig(policy = "strict") {
424
- return POLICY_CONFIG[policy] || POLICY_CONFIG.strict;
425
- }
427
+ const MAX_EVIDENCE_SNIPPET = 200;
426
428
 
427
429
  function confidenceToScore(confidence) {
428
430
  if (typeof confidence === "number") return confidence;
429
431
  switch (confidence) {
430
- case "high": return 0.9;
432
+ case "high":
433
+ return 0.9;
431
434
  case "medium":
432
- case "med": return 0.7;
433
- case "low": return 0.5;
434
- default: return 0.6;
435
- }
436
- }
437
-
438
- // =============================================================================
439
- // SAFETY: PROJECT-ROOT SANDBOX
440
- // =============================================================================
441
-
442
- function safeProjectJoin(projectPath, rel) {
443
- const root = path.resolve(projectPath);
444
- const abs = path.resolve(root, rel);
445
- if (!abs.startsWith(root + path.sep) && abs !== root) {
446
- throw new Error(`Refusing to access path outside project root: ${rel}`);
435
+ return 0.7;
436
+ case "low":
437
+ return 0.5;
438
+ default:
439
+ return 0.6;
447
440
  }
448
- return abs;
449
- }
450
-
451
- async function safeReadFile(projectPath, rel) {
452
- const abs = safeProjectJoin(projectPath, rel);
453
- return await fs.readFile(abs, "utf8");
454
- }
455
-
456
- // =============================================================================
457
- // EVIDENCE NORMALIZATION
458
- // =============================================================================
459
-
460
- function sha16(s) {
461
- return crypto.createHash("sha256").update(s).digest("hex").slice(0, 16);
462
441
  }
463
442
 
464
- function parseLineRange(lines) {
465
- if (typeof lines === "number") return { start: Math.max(1, lines), end: Math.max(1, lines) };
466
- if (!lines) return { start: 1, end: 1 };
467
- const s = String(lines).trim();
468
- const m = s.match(/^(\d+)(?:\s*-\s*(\d+))?$/);
469
- if (!m) return { start: 1, end: 1 };
470
- const a = Number(m[1]);
471
- const b = m[2] ? Number(m[2]) : a;
472
- return { start: Math.max(1, a), end: Math.max(1, b) };
473
- }
474
-
475
- async function readSnippet(projectPath, file, lines) {
443
+ async function readSnippet(projectPath, file, line) {
476
444
  if (!file) return "";
477
445
  try {
478
- const content = await safeReadFile(projectPath, file);
479
- const arr = content.split(/\r?\n/);
480
- const { start, end } = parseLineRange(lines);
481
- const s = Math.max(1, Math.min(arr.length, start));
482
- const e = Math.max(s, Math.min(arr.length, end));
483
- const snippet = arr.slice(s - 1, e).join("\n");
484
- return snippet.slice(0, MAX_EVIDENCE_SNIPPET);
446
+ const content = await fs.readFile(path.join(projectPath, file), "utf8");
447
+ const lines = content.split("\n");
448
+ const idx = Math.max(0, Math.min(lines.length - 1, line - 1));
449
+ return (lines[idx] || "").slice(0, MAX_EVIDENCE_SNIPPET);
485
450
  } catch {
486
451
  return "";
487
452
  }
@@ -489,914 +454,410 @@ async function readSnippet(projectPath, file, lines) {
489
454
 
490
455
  async function normalizeEvidence(projectPath, evidence, fallback, confidence) {
491
456
  const raw = Array.isArray(evidence) ? evidence : evidence ? [evidence] : [];
492
- const out = [];
457
+ const normalized = [];
493
458
 
494
459
  for (const item of raw) {
495
460
  const file = item?.file || fallback?.file || "";
496
- const rangeStr = item?.lines ?? item?.line ?? fallback?.line ?? 1;
497
- const { start, end } = parseLineRange(rangeStr);
498
- const snippet = (item?.snippet || (await readSnippet(projectPath, file, `${start}-${end}`)) || "").slice(0, MAX_EVIDENCE_SNIPPET);
499
- const hash = item?.hash || sha16(`${file}:${start}:${snippet}`);
461
+ const line = Number(item?.line || item?.lines || fallback?.line || 1);
462
+ const snippet =
463
+ item?.snippet ||
464
+ item?.evidence ||
465
+ (await readSnippet(projectPath, file, line));
500
466
 
501
- out.push({
467
+ normalized.push({
502
468
  file,
503
- line: start,
504
- lines: end !== start ? `${start}-${end}` : `${start}`,
469
+ line,
505
470
  snippet,
506
- hash,
507
471
  confidence: item?.confidence ?? confidenceToScore(confidence),
508
472
  });
509
473
  }
510
474
 
511
- if (out.length === 0 && fallback?.file) {
512
- const file = fallback.file;
513
- const line = fallback.line || 1;
514
- const snippet = await readSnippet(projectPath, file, line);
515
- out.push({
516
- file,
517
- line,
518
- lines: `${line}`,
519
- snippet,
520
- hash: sha16(`${file}:${line}:${snippet}`),
475
+ if (normalized.length === 0 && fallback?.file) {
476
+ normalized.push({
477
+ file: fallback.file,
478
+ line: fallback.line || 1,
479
+ snippet: await readSnippet(projectPath, fallback.file, fallback.line || 1),
521
480
  confidence: confidenceToScore(confidence),
522
481
  });
523
482
  }
524
483
 
525
- return out;
484
+ return normalized;
526
485
  }
527
486
 
528
- // =============================================================================
529
- // POLICY ENFORCEMENT (FIXED)
530
- // =============================================================================
531
-
532
- /**
533
- * Correct confidence derivation (your original had precedence issues).
534
- */
535
- export function enforceClaimResult(result, policy = "strict") {
536
- const config = getPolicyConfig(policy);
537
-
538
- const derived =
539
- result?.confidence !== undefined
540
- ? confidenceToScore(result.confidence)
541
- : (result?.result === "true" ? 0.9 : result?.result === "false" ? 0.9 : 0.3);
487
+ export function hasRecentClaimValidation(projectPath, maxAgeMs = 5 * 60 * 1000) {
488
+ const last = state.lastValidationByProject.get(projectPath);
489
+ return typeof last === "number" && Date.now() - last <= maxAgeMs;
490
+ }
542
491
 
543
- if (result.result === "unknown" && !config.allowUnknown) {
544
- return {
545
- allowed: false,
546
- confidence: derived,
547
- reason: `Unknown claims are not allowed in ${policy} mode`,
548
- suggestion: "Use search_evidence / get_truthpack / refresh=true to gather proof.",
549
- blockedActions: ["fix", "autopilot_apply", "propose_patch"],
550
- };
492
+ async function getTruthPack(projectPath, args) {
493
+ const scope = args.scope || 'all';
494
+ const refresh = args.refresh || false;
495
+
496
+ if (state.truthPack && !refresh) {
497
+ return filterTruthPack(state.truthPack, scope);
551
498
  }
552
-
553
- if (derived < config.minConfidence) {
554
- return {
555
- allowed: false,
556
- confidence: derived,
557
- reason: `Confidence ${(derived * 100).toFixed(0)}% below ${policy} threshold ${(config.minConfidence * 100).toFixed(0)}%`,
558
- suggestion: "Find more evidence or lower strictness (permissive policy).",
559
- };
499
+
500
+ // Build truth pack
501
+ const truthPack = {
502
+ version: '1.0.0',
503
+ generatedAt: new Date().toISOString(),
504
+ projectPath,
505
+ commitHash: getCommitHash(projectPath),
506
+ sections: {},
507
+ confidence: 0,
508
+ };
509
+
510
+ if (scope === 'all' || scope === 'routes') {
511
+ truthPack.sections.routes = await extractRoutes(projectPath);
560
512
  }
561
-
562
- if (result.result === "false") {
563
- return {
564
- allowed: false,
565
- confidence: derived,
566
- reason: "Claim is disproven. Do not proceed with dependent actions.",
567
- };
513
+ if (scope === 'all' || scope === 'auth') {
514
+ truthPack.sections.auth = await extractAuth(projectPath);
568
515
  }
569
-
570
- return { allowed: true, confidence: derived };
571
- }
572
-
573
- // =============================================================================
574
- // CLAIM VALIDATION WITH RACE CONDITION PROTECTION
575
- //
576
- // SECURITY FIX: Previous implementation had a TOCTOU race condition:
577
- // 1. Thread A: hasRecentClaimValidation() returns true
578
- // 2. Thread B: invalidates the claim (file change, etc.)
579
- // 3. Thread A: proceeds with stale claim → invalid state
580
- //
581
- // New implementation uses atomic check-and-consume pattern with per-project locks.
582
- // =============================================================================
583
-
584
- /**
585
- * Per-project validation locks to prevent concurrent operations
586
- * from using the same validation state.
587
- */
588
- const validationLocks = new Map(); // Map<projectPath, { locked: boolean, queue: Promise }>
589
-
590
- /**
591
- * Acquire a validation lock for a project (serializes validation checks).
592
- */
593
- function acquireValidationLock(projectPath) {
594
- let lockState = validationLocks.get(projectPath);
595
- if (!lockState) {
596
- lockState = { locked: false, queue: Promise.resolve() };
597
- validationLocks.set(projectPath, lockState);
516
+ if (scope === 'all' || scope === 'billing') {
517
+ truthPack.sections.billing = await extractBilling(projectPath);
518
+ }
519
+ if (scope === 'all' || scope === 'env') {
520
+ truthPack.sections.env = await extractEnv(projectPath);
521
+ }
522
+ if (scope === 'all' || scope === 'schema') {
523
+ truthPack.sections.schema = await extractSchema(projectPath);
524
+ }
525
+ if (scope === 'all' || scope === 'graph') {
526
+ truthPack.sections.graph = await extractGraph(projectPath);
598
527
  }
599
528
 
600
- const acquirePromise = lockState.queue.then(() => {
601
- lockState.locked = true;
602
- return () => {
603
- lockState.locked = false;
604
- };
605
- });
529
+ // Calculate confidence
530
+ const sections = Object.values(truthPack.sections);
531
+ truthPack.confidence = sections.reduce((sum, s) => sum + (s.confidence || 0), 0) / sections.length;
606
532
 
607
- lockState.queue = acquirePromise.catch(() => {});
608
- return acquirePromise;
609
- }
610
-
611
- /**
612
- * Check claim validation freshness (basic check, no lock).
613
- * Use checkAndConsumeClaimValidation for atomic operations.
614
- */
615
- export function hasRecentClaimValidation(projectPath, policy = "strict") {
616
- const last = state.lastValidationByProject.get(projectPath);
617
- if (typeof last !== "number") return false;
618
- const ttl = getPolicyConfig(policy).validationTTL;
619
- return Date.now() - last <= ttl;
533
+ state.truthPack = truthPack;
534
+ return truthPack;
620
535
  }
621
536
 
622
- /**
623
- * Atomic check-and-consume claim validation.
624
- *
625
- * SECURITY: Use this for operations that depend on claim validation.
626
- * It ensures no other operation can use the same validation state concurrently.
627
- *
628
- * @param {string} projectPath - Project path
629
- * @param {string} policy - Policy name (strict/balanced/permissive)
630
- * @param {string} operationId - Unique ID for this operation (for audit)
631
- * @returns {Promise<{ valid: boolean, consumedAt?: number, reason?: string }>}
632
- */
633
- export async function checkAndConsumeClaimValidation(projectPath, policy = "strict", operationId = null) {
634
- const release = await acquireValidationLock(projectPath);
537
+ async function validateClaim(projectPath, args) {
538
+ const { claim, subject, expected = true } = args;
539
+ const claimId = `claim_${crypto.createHash('sha256').update(JSON.stringify({ claim, subject })).digest('hex').slice(0, 12)}`;
635
540
 
636
- try {
637
- const last = state.lastValidationByProject.get(projectPath);
638
- const now = Date.now();
639
-
640
- if (typeof last !== "number") {
641
- return {
642
- valid: false,
643
- reason: "No claim validation found for this project"
644
- };
645
- }
646
-
647
- const ttl = getPolicyConfig(policy).validationTTL;
648
- const age = now - last;
649
-
650
- if (age > ttl) {
651
- return {
652
- valid: false,
653
- reason: `Claim validation expired (age: ${Math.round(age / 1000)}s, TTL: ${Math.round(ttl / 1000)}s)`
654
- };
541
+ // Check cache
542
+ if (state.verifiedClaims.has(claimId)) {
543
+ const cached = state.verifiedClaims.get(claimId);
544
+ if (Date.now() - cached.timestamp < 5 * 60 * 1000) {
545
+ return { ...cached.result, cached: true };
655
546
  }
656
-
657
- // Mark this validation as consumed by updating the timestamp
658
- // This prevents replay/reuse of the same validation
659
- state.lastValidationByProject.set(projectPath, now);
660
-
661
- return {
662
- valid: true,
663
- consumedAt: now,
664
- operationId,
665
- };
666
-
667
- } finally {
668
- release();
669
547
  }
670
- }
671
-
672
- // =============================================================================
673
- // FINGERPRINT + WRAPPER
674
- // =============================================================================
675
-
676
- function getCommitHash(projectPath) {
548
+
549
+ let result = { result: 'unknown', confidence: 'low', evidence: [], nextSteps: [] };
550
+
677
551
  try {
678
- return execSync("git rev-parse HEAD", { cwd: projectPath, encoding: "utf8" }).trim();
679
- } catch {
680
- return "unknown";
552
+ switch (claim) {
553
+ case 'route_exists':
554
+ result = await verifyRouteExists(projectPath, subject);
555
+ break;
556
+ case 'file_exists':
557
+ result = await verifyFileExists(projectPath, subject);
558
+ break;
559
+ case 'env_var_exists':
560
+ case 'env_var_used':
561
+ result = await verifyEnvVar(projectPath, subject, claim);
562
+ break;
563
+ case 'auth_enforced':
564
+ case 'route_guarded':
565
+ result = await verifyRouteGuarded(projectPath, subject);
566
+ break;
567
+ case 'function_exists':
568
+ case 'component_exists':
569
+ case 'model_exists':
570
+ result = await verifyEntityExists(projectPath, subject, claim);
571
+ break;
572
+ default:
573
+ result.nextSteps = [`Claim type "${claim}" not yet implemented. Use search_evidence instead.`];
574
+ }
575
+ } catch (error) {
576
+ result.nextSteps = [`Verification error: ${error.message}`];
577
+ }
578
+
579
+ // If unknown, add helpful next steps
580
+ if (result.result === 'unknown') {
581
+ result.nextSteps.push(
582
+ 'call vibecheck.search_evidence to find related code',
583
+ 'call vibecheck.get_truthpack to get full context',
584
+ );
585
+ result.warning = '⚠️ UNKNOWN claims BLOCK dependent actions. Verify before proceeding.';
681
586
  }
587
+
588
+ // Cache result
589
+ state.verifiedClaims.set(claimId, { result, timestamp: Date.now(), projectPath });
590
+ state.lastValidationByProject.set(projectPath, Date.now());
591
+
592
+ return {
593
+ claimId,
594
+ ...result,
595
+ evidence: await normalizeEvidence(projectPath, result.evidence, {
596
+ file: subject?.path || subject?.name,
597
+ line: 1,
598
+ }, result.confidence),
599
+ timestamp: new Date().toISOString(),
600
+ };
682
601
  }
683
602
 
684
- export function getProjectFingerprint(projectPath) {
685
- const commitHash = getCommitHash(projectPath);
686
- const keyFiles = [
687
- "package.json",
688
- "pnpm-lock.yaml",
689
- "package-lock.json",
690
- "yarn.lock",
691
- "prisma/schema.prisma",
692
- "next.config.js",
693
- "next.config.ts",
694
- ".vibecheck/contracts/routes.json",
695
- ".vibecheck/contracts/env.json",
696
- ".vibecheck/contracts/auth.json",
697
- ".vibecheck/contracts/external.json",
698
- ];
699
-
700
- const fileHashes = [];
701
- for (const rel of keyFiles) {
702
- try {
703
- const abs = safeProjectJoin(projectPath, rel);
704
- if (!fssync.existsSync(abs)) continue;
705
- const content = fssync.readFileSync(abs, "utf8");
706
- fileHashes.push(`${rel}:${sha16(content)}`);
707
- } catch { /* ignore */ }
708
- }
709
-
710
- const material = [commitHash, ...fileHashes].join("|");
711
- return {
712
- hash: sha16(material),
713
- commitHash,
714
- fileHashes,
715
- generatedAt: new Date().toISOString(),
716
- };
717
- }
718
-
719
- const CONTEXT_ATTRIBUTION = "🧠 Context enhanced by vibecheck";
720
-
721
- export function getContextAttribution() {
722
- return CONTEXT_ATTRIBUTION;
723
- }
724
-
725
- export function wrapMcpResponse(data, projectPath) {
726
- return {
727
- ok: true,
728
- version: "2.1.0",
729
- projectFingerprint: getProjectFingerprint(projectPath),
730
- attribution: CONTEXT_ATTRIBUTION,
731
- generatedAt: new Date().toISOString(),
732
- data,
733
- };
734
- }
735
-
736
- // =============================================================================
737
- // IMPLEMENTATION: TRUTHPACK
738
- // =============================================================================
739
-
740
- async function getTruthPack(projectPath, args) {
741
- const scope = args?.scope || "all";
742
- const refresh = Boolean(args?.refresh);
743
-
744
- if (refresh) {
745
- // Also clear route index on refresh
746
- state.routeIndexByProject.delete(projectPath);
747
- }
748
-
749
- if (!refresh && state.truthPackByProject.has(projectPath)) {
750
- return filterTruthPack(state.truthPackByProject.get(projectPath), scope);
751
- }
752
-
753
- const truthPack = {
754
- version: "2.0.0", // v2 with Route Truth v1 integration
755
- generatedAt: new Date().toISOString(),
756
- projectPath,
757
- commitHash: getCommitHash(projectPath),
758
- sections: {},
759
- confidence: 0,
760
- _attribution: CONTEXT_ATTRIBUTION,
761
- };
762
-
763
- if (scope === "all" || scope === "routes") truthPack.sections.routes = await extractRoutes(projectPath, refresh);
764
- if (scope === "all" || scope === "auth") truthPack.sections.auth = await extractAuth(projectPath);
765
- if (scope === "all" || scope === "billing") truthPack.sections.billing = await extractBilling(projectPath);
766
- if (scope === "all" || scope === "env") truthPack.sections.env = await extractEnv(projectPath);
767
- if (scope === "all" || scope === "schema") truthPack.sections.schema = await extractSchema(projectPath);
768
- if (scope === "all" || scope === "graph") truthPack.sections.graph = await extractGraph(projectPath);
769
-
770
- const sections = Object.values(truthPack.sections);
771
- truthPack.confidence = sections.length
772
- ? sections.reduce((sum, s) => sum + (s?.confidence || 0), 0) / sections.length
773
- : 0.4;
774
-
775
- state.truthPackByProject.set(projectPath, truthPack);
776
- return truthPack;
777
- }
778
-
779
- function filterTruthPack(pack, scope) {
780
- if (scope === "all") return pack;
781
- return { ...pack, sections: { [scope]: pack.sections?.[scope] } };
782
- }
783
-
784
- // =============================================================================
785
- // IMPLEMENTATION: validate_claim (policy-aware + fingerprint cache)
786
- // =============================================================================
787
-
788
- async function validateClaim(projectPath, args) {
789
- const { claim, subject, expected = true, policy = "strict", refresh = false } = args || {};
790
- const pol = policy || "strict";
791
-
792
- if (refresh) {
793
- // force refresh truthpack + route index for this project
794
- state.truthPackByProject.delete(projectPath);
795
- state.routeIndexByProject.delete(projectPath);
796
- }
797
-
798
- const fingerprint = getProjectFingerprint(projectPath);
799
- const claimKey = { claim, subject, expected };
800
- const claimId = `claim_${sha16(JSON.stringify(claimKey))}`;
801
-
802
- // policy TTL cache + fingerprint invalidation
803
- const cached = state.verifiedClaims.get(claimId);
804
- if (cached && cached.projectHash === fingerprint.hash) {
805
- const ttl = getPolicyConfig(pol).validationTTL;
806
- if (Date.now() - cached.timestamp <= ttl) {
807
- return { claimId, ...cached.result, cached: true };
808
- }
809
- }
810
-
811
- let result = { result: "unknown", confidence: "low", evidence: [], nextSteps: [] };
812
-
813
- try {
814
- switch (claim) {
815
- case "route_exists":
816
- result = await verifyRouteExists(projectPath, subject, refresh);
817
- break;
818
-
819
- case "file_exists":
820
- result = await verifyFileExists(projectPath, subject);
821
- break;
822
-
823
- case "env_var_exists":
824
- case "env_var_used":
825
- result = await verifyEnvVar(projectPath, subject, claim);
826
- break;
827
-
828
- case "auth_enforced":
829
- case "route_guarded":
830
- result = await verifyRouteGuarded(projectPath, subject);
831
- break;
832
-
833
- case "function_exists":
834
- case "component_exists":
835
- case "model_exists":
836
- result = await verifyEntityExists(projectPath, subject, claim);
837
- break;
838
-
839
- default:
840
- result = {
841
- result: "unknown",
842
- confidence: "low",
843
- evidence: [],
844
- nextSteps: [`Claim type "${claim}" not implemented. Use search_evidence.`],
845
- };
846
- break;
847
- }
848
- } catch (error) {
849
- result = {
850
- result: "unknown",
851
- confidence: "low",
852
- evidence: [],
853
- nextSteps: [`Verification error: ${error?.message || String(error)}`],
854
- };
855
- }
856
-
857
- // normalize evidence consistently
858
- const normalized = await normalizeEvidence(
859
- projectPath,
860
- result.evidence,
861
- { file: subject?.path || subject?.name, line: 1 },
862
- result.confidence
863
- );
864
-
865
- // enforcement (policy-driven)
866
- const enforcement = enforceClaimResult({ result: result.result, confidence: result.confidence }, pol);
867
-
868
- // guidance
869
- const nextSteps = Array.isArray(result.nextSteps) ? result.nextSteps : [];
870
- if (result.result === "unknown") {
871
- nextSteps.push("call vibecheck.search_evidence for proof", "call vibecheck.get_truthpack refresh=true");
872
- }
873
- if (result.result === "false" && expected === true) {
874
- nextSteps.push("Claim expected true but evaluated false: check path/method/name canonicalization.");
875
- }
876
-
877
- const finalResult = {
878
- claimId,
879
- claim,
880
- subject,
881
- expected,
882
- result: result.result,
883
- confidence: result.confidence,
884
- evidence: normalized,
885
- enforcement,
886
- nextSteps,
887
- timestamp: new Date().toISOString(),
888
- _attribution: CONTEXT_ATTRIBUTION,
889
- };
890
-
891
- state.verifiedClaims.set(claimId, { projectHash: fingerprint.hash, timestamp: Date.now(), result: finalResult });
892
- state.lastValidationByProject.set(projectPath, Date.now());
893
-
894
- return finalResult;
895
- }
896
-
897
- // =============================================================================
898
- // IMPLEMENTATION: compile_context
899
- // =============================================================================
900
-
901
603
  async function compileContext(projectPath, args) {
902
- const { task, files = [], policy = "balanced", maxItems = 50 } = args || {};
903
- const pol = policy || "balanced";
904
-
604
+ const { task, files = [], policy = 'balanced' } = args;
605
+
606
+ // Analyze task to determine relevant domains
905
607
  const domains = detectDomains(task);
906
608
  const keywords = extractKeywords(task);
907
- const truthPack = await getTruthPack(projectPath, { scope: "all", refresh: false });
908
-
909
- const routesAll = truthPack.sections?.routes?.routes || [];
910
- const relevantRoutes = routesAll.filter((r) => {
911
- const hay = `${r.path || ""} ${r.file || ""}`.toLowerCase();
912
- return keywords.some((k) => hay.includes(k));
913
- });
914
-
915
- const context = {
916
- routes: relevantRoutes.slice(0, Math.max(0, maxItems)),
917
- auth: domains.includes("auth") ? truthPack.sections?.auth : null,
918
- billing: domains.includes("billing") ? truthPack.sections?.billing : null,
919
- env: domains.includes("env") ? truthPack.sections?.env : null,
920
- focusFiles: Array.isArray(files) ? files : [],
921
- };
922
-
609
+
610
+ // Get relevant parts of truth pack
611
+ const truthPack = await getTruthPack(projectPath, { scope: 'all' });
612
+
613
+ // Filter to relevant content
614
+ const relevantRoutes = truthPack.sections.routes?.routes?.filter(r =>
615
+ keywords.some(k => r.path.includes(k) || r.file.includes(k))
616
+ ) || [];
617
+
618
+ const relevantAuth = domains.includes('auth') ? truthPack.sections.auth : null;
619
+ const relevantBilling = domains.includes('billing') ? truthPack.sections.billing : null;
620
+
621
+ // Get applicable invariants
923
622
  const invariants = getInvariantsForDomains(domains);
924
- const tokenCount = estimateTokens(context);
925
-
623
+
624
+ // Estimate token count
625
+ const tokenCount = estimateTokens({ relevantRoutes, relevantAuth, relevantBilling });
626
+
926
627
  return {
927
628
  task,
928
- policy: pol,
629
+ policy,
929
630
  domains,
930
- context,
631
+ context: {
632
+ routes: relevantRoutes.slice(0, policy === 'strict' ? 10 : 50),
633
+ auth: relevantAuth,
634
+ billing: relevantBilling,
635
+ },
931
636
  invariants,
932
637
  tokenCount,
933
- warnings: generateContextWarnings(domains, pol, relevantRoutes.length),
934
- _attribution: CONTEXT_ATTRIBUTION,
638
+ warnings: generateContextWarnings(domains, policy, relevantRoutes.length),
935
639
  };
936
640
  }
937
641
 
938
- // =============================================================================
939
- // IMPLEMENTATION: search_evidence (rg accel + safe scan)
940
- // =============================================================================
941
-
942
- function escapeRegex(s) {
943
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
944
- }
945
-
946
- function isTestFilePath(rel) {
947
- return /(^|\/)(__tests__|test|tests|spec)\//i.test(rel) || /\.(test|spec)\.(ts|tsx|js|jsx)$/i.test(rel);
948
- }
949
-
950
642
  async function searchEvidence(projectPath, args) {
951
- const {
952
- query,
953
- mode = "text",
954
- type = "any",
955
- limit = 10,
956
- caseSensitive = false,
957
- includeTests = false,
958
- } = args || {};
959
-
960
- const q = String(query || "").trim();
961
- if (!q) return { query: q, count: 0, results: [], _attribution: CONTEXT_ATTRIBUTION };
962
-
963
- // Try ripgrep for speed (optional)
964
- const rgResults = tryRipgrep(projectPath, q, { mode, caseSensitive, limit, includeTests });
965
- if (rgResults) {
966
- return { query: q, count: rgResults.length, results: rgResults, engine: "ripgrep", _attribution: CONTEXT_ATTRIBUTION };
967
- }
968
-
969
- const files = await findSourceFiles(projectPath, { includeTests });
970
- const flags = caseSensitive ? "g" : "gi";
971
- const re = new RegExp(mode === "regex" ? q : escapeRegex(q), flags);
972
-
643
+ const { query, type = 'any', limit = 10 } = args;
973
644
  const results = [];
974
- for (const fileAbs of files) {
975
- const relPath = path.relative(projectPath, fileAbs).replace(/\\/g, "/");
976
- if (!includeTests && isTestFilePath(relPath)) continue;
977
-
645
+
646
+ const files = await findSourceFiles(projectPath);
647
+ const pattern = new RegExp(query, 'gi');
648
+
649
+ for (const file of files.slice(0, 100)) {
978
650
  try {
979
- const content = await fs.readFile(fileAbs, "utf8");
980
- const lines = content.split(/\r?\n/);
981
-
651
+ const content = await fs.readFile(file, 'utf8');
652
+ const lines = content.split('\n');
653
+ const relPath = path.relative(projectPath, file);
654
+
982
655
  for (let i = 0; i < lines.length; i++) {
983
- if (re.test(lines[i])) {
984
- const snippet = lines.slice(Math.max(0, i - 2), Math.min(lines.length, i + 3)).join("\n").slice(0, 320);
656
+ if (pattern.test(lines[i])) {
657
+ const snippet = lines.slice(Math.max(0, i - 2), Math.min(lines.length, i + 3)).join('\n');
985
658
  results.push({
986
659
  file: relPath,
987
660
  line: i + 1,
988
- lines: `${i + 1}`,
989
- snippet,
990
- hash: sha16(`${relPath}:${i + 1}:${lines[i]}`),
661
+ snippet: snippet.slice(0, 300),
662
+ hash: crypto.createHash('sha256').update(lines[i]).digest('hex').slice(0, 16),
991
663
  confidence: 0.6,
992
664
  });
665
+
993
666
  if (results.length >= limit) break;
994
667
  }
995
- re.lastIndex = 0;
668
+ pattern.lastIndex = 0;
996
669
  }
997
-
670
+
998
671
  if (results.length >= limit) break;
999
- } catch { /* ignore */ }
1000
- }
1001
-
1002
- return { query: q, count: results.length, results, engine: "scan", _attribution: CONTEXT_ATTRIBUTION };
1003
- }
1004
-
1005
- function tryRipgrep(projectPath, query, opts) {
1006
- try {
1007
- const rgArgs = ["-n", "--hidden", "--no-heading", "--color", "never"];
1008
- rgArgs.push("--glob", "!**/node_modules/**");
1009
- rgArgs.push("--glob", "!**/.next/**");
1010
- rgArgs.push("--glob", "!**/dist/**");
1011
- rgArgs.push("--glob", "!**/build/**");
1012
- rgArgs.push("--glob", "!**/coverage/**");
1013
- if (!opts.includeTests) {
1014
- rgArgs.push("--glob", "!**/__tests__/**");
1015
- rgArgs.push("--glob", "!**/tests/**");
1016
- rgArgs.push("--glob", "!**/*.test.*");
1017
- rgArgs.push("--glob", "!**/*.spec.*");
1018
- }
1019
- if (!opts.caseSensitive) rgArgs.push("-i");
1020
- if (opts.mode === "text") rgArgs.push("-F"); // fixed string
1021
- rgArgs.push("--max-count", String(Math.max(1, opts.limit)));
1022
- rgArgs.push(query);
1023
- rgArgs.push(".");
1024
-
1025
- const out = spawnSync("rg", rgArgs, { cwd: projectPath, encoding: "utf8" });
1026
- if (out.error || out.status !== 0) return null;
1027
-
1028
- const lines = String(out.stdout || "").split(/\r?\n/).filter(Boolean);
1029
- const results = lines.slice(0, opts.limit).map((l) => {
1030
- // format: file:line:match
1031
- const m = l.match(/^(.+?):(\d+):(.*)$/);
1032
- if (!m) return null;
1033
- const file = m[1].replace(/\\/g, "/");
1034
- const lineNum = Number(m[2]);
1035
- const text = m[3] || "";
1036
- return {
1037
- file,
1038
- line: lineNum,
1039
- lines: `${lineNum}`,
1040
- snippet: text.slice(0, 320),
1041
- hash: sha16(`${file}:${lineNum}:${text}`),
1042
- confidence: 0.65,
1043
- };
1044
- }).filter(Boolean);
1045
-
1046
- return results;
1047
- } catch {
1048
- return null;
672
+ } catch {}
1049
673
  }
674
+
675
+ return {
676
+ query,
677
+ count: results.length,
678
+ results,
679
+ };
1050
680
  }
1051
681
 
1052
- // =============================================================================
1053
- // IMPLEMENTATION: find_counterexamples
1054
- // =============================================================================
1055
-
1056
682
  async function findCounterexamples(projectPath, args) {
1057
- const { claim, subject, policy = "strict" } = args || {};
1058
- const pol = policy || "strict";
1059
-
683
+ const { claim, subject } = args;
1060
684
  const counterexamples = [];
1061
-
1062
- if (claim === "auth_enforced" || claim === "route_guarded") {
1063
- // Look for client-only guards (classic bypass)
1064
- const guardEvidence = await searchEvidence(projectPath, { query: "useEffect|client|localStorage|sessionStorage", mode: "regex", limit: 20 });
1065
- const middlewareEvidence = await searchEvidence(projectPath, { query: "middleware", mode: "text", limit: 10 });
1066
-
1067
- if (guardEvidence.count > 0 && middlewareEvidence.count === 0) {
1068
- counterexamples.push({
1069
- type: "bypass_possible",
1070
- severity: "ship_killer",
1071
- description: "Auth appears client-only (no middleware/server guard evidence found).",
1072
- evidence: guardEvidence.results.slice(0, 3),
1073
- });
1074
- }
1075
- }
1076
-
1077
- if (claim === "billing_gate_exists") {
1078
- const billingEvidence = await searchEvidence(projectPath, { query: "isPro|tier|plan|subscription|stripe", mode: "regex", limit: 20 });
1079
- for (const ev of billingEvidence.results) {
1080
- if (/localStorage|sessionStorage|client/i.test(ev.snippet)) {
1081
- counterexamples.push({
1082
- type: "bypass_possible",
1083
- severity: "ship_killer",
1084
- description: "Billing gate likely client-side (bypassable).",
1085
- evidence: ev,
1086
- });
685
+
686
+ switch (claim) {
687
+ case 'auth_enforced':
688
+ // Check for client-only guards
689
+ const authResult = await verifyRouteGuarded(projectPath, subject);
690
+ if (authResult.result === 'true' && authResult.evidence) {
691
+ // Check if guard is client-only
692
+ for (const ev of authResult.evidence) {
693
+ const content = await fs.readFile(path.join(projectPath, ev.file), 'utf8');
694
+ if (content.includes('client') && !content.includes('middleware')) {
695
+ counterexamples.push({
696
+ type: 'bypass_possible',
697
+ description: 'Auth appears to be client-side only - can be bypassed',
698
+ evidence: ev,
699
+ severity: 'ship_killer',
700
+ });
701
+ }
702
+ }
1087
703
  }
1088
- }
704
+ break;
705
+
706
+ case 'billing_gate_exists':
707
+ // Check for client-only tier checks
708
+ const evidence = await searchEvidence(projectPath, { query: subject.name || 'tier', limit: 5 });
709
+ for (const ev of evidence.results) {
710
+ if (ev.snippet.includes('localStorage') || ev.snippet.includes('client')) {
711
+ counterexamples.push({
712
+ type: 'bypass_possible',
713
+ description: 'Billing check appears client-side only',
714
+ evidence: ev,
715
+ severity: 'ship_killer',
716
+ });
717
+ }
718
+ }
719
+ break;
1089
720
  }
1090
-
721
+
1091
722
  return {
1092
723
  claim,
1093
724
  subject,
1094
- policy: pol,
1095
725
  counterexamples,
1096
726
  claimDemoted: counterexamples.length > 0,
1097
- _attribution: CONTEXT_ATTRIBUTION,
1098
727
  };
1099
728
  }
1100
729
 
1101
- // =============================================================================
1102
- // IMPLEMENTATION: propose_patch + verify_patch
1103
- // =============================================================================
1104
-
1105
- function ensureDir(dirAbs) {
1106
- if (!fssync.existsSync(dirAbs)) fssync.mkdirSync(dirAbs, { recursive: true });
1107
- }
1108
-
1109
730
  async function proposePatch(projectPath, args) {
1110
- const { diff, fixes, claims = [], verification = [], policy = "strict", save = true } = args || {};
1111
- const pol = policy || "strict";
1112
-
731
+ const { diff, fixes, claims = [], verification = [] } = args;
732
+
733
+ // Validate all dependent claims
1113
734
  const claimValidation = [];
1114
735
  for (const claimId of claims) {
1115
736
  const cached = state.verifiedClaims.get(claimId);
1116
737
  if (!cached) {
1117
- claimValidation.push({ claimId, valid: false, error: "Claim not verified" });
1118
- continue;
738
+ claimValidation.push({ claimId, valid: false, error: 'Claim not verified' });
739
+ } else if (cached.result.result === 'unknown') {
740
+ claimValidation.push({ claimId, valid: false, error: 'Claim is unknown - cannot proceed' });
741
+ } else if (cached.result.result === 'false') {
742
+ claimValidation.push({ claimId, valid: false, error: 'Claim is false - invalid dependency' });
743
+ } else {
744
+ claimValidation.push({ claimId, valid: true });
1119
745
  }
1120
- const res = cached.result;
1121
- if (res?.result === "unknown") claimValidation.push({ claimId, valid: false, error: "Claim is unknown" });
1122
- else if (res?.result === "false") claimValidation.push({ claimId, valid: false, error: "Claim is false" });
1123
- else claimValidation.push({ claimId, valid: true });
1124
746
  }
1125
-
1126
- const allClaimsValid = claimValidation.every((c) => c.valid);
1127
- const patchId = `patch_${crypto.randomUUID().slice(0, 12)}`;
1128
-
747
+
748
+ const allClaimsValid = claimValidation.every(c => c.valid);
749
+
1129
750
  const patch = {
1130
- patchId,
1131
- diff: String(diff || "").slice(0, 50_000),
1132
- fixes: Array.isArray(fixes) ? fixes : [],
1133
- dependsOnClaims: Array.isArray(claims) ? claims : [],
1134
- verification: (Array.isArray(verification) && verification.length > 0) ? verification : ["vibecheck ship", "pnpm test"],
751
+ patchId: `patch_${crypto.randomUUID().slice(0, 12)}`,
752
+ diff: diff.slice(0, 5000), // Truncate for storage
753
+ fixes,
754
+ dependsOnClaims: claims,
755
+ verification: verification.length > 0 ? verification : ['vibecheck ship', 'pnpm test'],
1135
756
  createdAt: new Date().toISOString(),
1136
- eligible: allClaimsValid && (Array.isArray(fixes) && fixes.length > 0),
757
+ eligible: allClaimsValid && fixes.length > 0,
1137
758
  claimValidation,
1138
- policy: pol,
1139
759
  };
1140
-
1141
- if (!patch.eligible) {
1142
- patch.blockers = claimValidation.filter((c) => !c.valid);
1143
- patch.message = "⚠️ Patch NOT eligible for auto-apply. Fix blockers first.";
1144
- }
1145
-
1146
- if (save) {
1147
- try {
1148
- const dir = safeProjectJoin(projectPath, ".vibecheck/patches");
1149
- ensureDir(dir);
1150
- const out = path.join(dir, `${patchId}.json`);
1151
- await fs.writeFile(out, JSON.stringify(patch, null, 2), "utf8");
1152
- patch.savedTo = path.relative(projectPath, out).replace(/\\/g, "/");
1153
- } catch (e) {
1154
- patch.saveError = e?.message || String(e);
1155
- }
1156
- }
1157
-
1158
- return patch;
1159
- }
1160
-
1161
- /**
1162
- * Validate command against strict allowlist.
1163
- *
1164
- * SECURITY FIX: Previous allowlist was too permissive, allowing arbitrary code execution:
1165
- * - "node -e 'require(\"child_process\").exec(\"rm -rf /\")'" would pass
1166
- * - "npm exec malicious-package" would pass
1167
- * - "pnpm dlx evil-tool" would pass
1168
- *
1169
- * New allowlist only permits specific safe subcommands.
1170
- */
1171
- function commandAllowlisted(cmd) {
1172
- const trimmed = cmd.trim();
1173
-
1174
- // Reject commands with shell metacharacters that could enable injection
1175
- // These are dangerous even in "safe" commands: ; | & $ ` \ ( ) { } < > \n
1176
- if (/[;|&$`\\(){}<>\n]/.test(trimmed)) {
1177
- return false;
1178
- }
1179
760
 
1180
- // Reject commands that use flags commonly used for code execution
1181
- if (/\s-[eEc]\s|\s--eval\s|\s--exec\s/i.test(trimmed)) {
1182
- return false;
761
+ if (!patch.eligible) {
762
+ patch.blockers = claimValidation.filter(c => !c.valid);
763
+ patch.message = '⚠️ Patch NOT eligible for auto-apply. Fix blockers first.';
1183
764
  }
1184
765
 
1185
- // Strict allowlist: only specific commands with specific safe subcommands
1186
- const strictAllow = [
1187
- // Vibecheck CLI - only specific safe commands
1188
- /^vibecheck\s+(ship|scan|ctx|lint|status)\b/,
1189
- /^vibecheck\s+--help\b/,
1190
- /^vibecheck\s+--version\b/,
1191
-
1192
- // Package managers - only test/build/lint (no exec, dlx, or install scripts)
1193
- /^pnpm\s+(test|build|lint|typecheck|check|run\s+(test|build|lint|typecheck))\b/,
1194
- /^npm\s+(test|run\s+(test|build|lint|typecheck))\b/,
1195
- /^yarn\s+(test|build|lint|typecheck|run\s+(test|build|lint|typecheck))\b/,
1196
- /^bun\s+(test|run\s+(test|build|lint))\b/,
1197
-
1198
- // TypeScript compiler - only type checking (no emit)
1199
- /^tsc\s+(--noEmit|--build)\b/,
1200
- /^tsc$/, // Default tsc with no args is safe
1201
-
1202
- // Linters - safe read-only operations
1203
- /^eslint\s+/, // ESLint with any args (read-only)
1204
- /^eslint$/,
1205
-
1206
- // Test runners - only run tests
1207
- /^vitest\s*(run|--run)?\b/,
1208
- /^vitest$/,
1209
- /^jest\s*(--ci|--coverage|--passWithNoTests)?\b/,
1210
- /^jest$/,
1211
-
1212
- // Playwright - only test mode (no codegen which opens browsers)
1213
- /^playwright\s+test\b/,
1214
- /^npx\s+playwright\s+test\b/,
1215
- ];
1216
-
1217
- return strictAllow.some((re) => re.test(trimmed));
1218
- }
1219
-
1220
- async function verifyPatch(projectPath, args) {
1221
- const { patchId, diff, commands, policy = "strict", timeoutMs = 120000 } = args || {};
1222
- const pol = policy || "strict";
1223
-
1224
- let patch = null;
1225
- if (patchId) {
1226
- try {
1227
- const abs = safeProjectJoin(projectPath, `.vibecheck/patches/${patchId}.json`);
1228
- const content = await fs.readFile(abs, "utf8");
1229
- patch = JSON.parse(content);
1230
- } catch (error) {
1231
- // Invalid JSON or file not found - treat as no patch
1232
- patch = null;
1233
- }
1234
- }
1235
-
1236
- // NOTE: This does NOT auto-apply diff. It only runs verification commands.
1237
- // Auto-apply should be a separate tool with explicit guardrails.
1238
- const cmds = Array.isArray(commands) ? commands : [];
1239
- const results = [];
1240
-
1241
- for (const cmd of cmds) {
1242
- if (!commandAllowlisted(cmd)) {
1243
- results.push({ cmd, ok: false, blocked: true, reason: "Command not allowlisted" });
1244
- continue;
1245
- }
1246
- const started = Date.now();
1247
- const out = spawnSync(cmd, {
1248
- cwd: projectPath,
1249
- shell: true,
1250
- encoding: "utf8",
1251
- timeout: Math.max(1000, Number(timeoutMs) || 120000),
1252
- maxBuffer: 1024 * 1024 * 5,
1253
- });
1254
-
1255
- const stdout = String(out.stdout || "").slice(0, MAX_CMD_OUTPUT);
1256
- const stderr = String(out.stderr || "").slice(0, MAX_CMD_OUTPUT);
1257
-
1258
- results.push({
1259
- cmd,
1260
- ok: out.status === 0 && !out.error,
1261
- status: out.status,
1262
- durationMs: Date.now() - started,
1263
- stdout,
1264
- stderr,
1265
- error: out.error ? String(out.error.message || out.error) : null,
1266
- });
1267
- }
1268
-
1269
- const pass = results.every((r) => r.ok);
1270
-
1271
- return {
1272
- patchId: patch?.patchId || patchId || null,
1273
- hasPatchRecord: !!patch,
1274
- policy: pol,
1275
- pass,
1276
- results,
1277
- note: "verify_patch runs commands only. Applying diffs should be explicit + guarded.",
1278
- _attribution: CONTEXT_ATTRIBUTION,
1279
- };
766
+ return patch;
1280
767
  }
1281
768
 
1282
- // =============================================================================
1283
- // IMPLEMENTATION: invariants
1284
- // =============================================================================
1285
-
1286
769
  async function checkInvariants(projectPath, args) {
1287
- const { category = "all", policy = "strict" } = args || {};
1288
- const pol = policy || "strict";
1289
-
770
+ const category = args.category || 'all';
1290
771
  const shipKillers = [];
1291
772
  const warnings = [];
1292
-
1293
- // 1) Silent catches in auth/billing/middleware are ship killers
1294
- const silentCatches = await searchEvidence(projectPath, {
1295
- query: String.raw`catch\s*\(\s*\w*\s*\)\s*\{\s*(?:\}|\/\/|console\.log|return\s*;|return\s*null\s*;)`,
1296
- mode: "regex",
1297
- limit: 50,
773
+
774
+ // Check for silent catches in auth/billing
775
+ const silentCatches = await searchEvidence(projectPath, {
776
+ query: 'catch.*\\{\\s*\\}|catch.*\\{\\s*//|catch.*console\\.log',
777
+ limit: 20
1298
778
  });
1299
-
779
+
1300
780
  for (const ev of silentCatches.results) {
1301
- if (/auth|billing|middleware|payment/i.test(ev.file)) {
781
+ if (ev.file.includes('auth') || ev.file.includes('billing') || ev.file.includes('middleware')) {
1302
782
  shipKillers.push({
1303
- invariant: "security_no_silent_catch",
1304
- rule: "No silent catch in auth/billing/middleware",
783
+ invariant: 'security_no_silent_catch',
784
+ rule: 'No silent catch in auth/billing flows',
1305
785
  evidence: ev,
1306
786
  });
1307
787
  }
1308
788
  }
1309
-
1310
- // 2) Hardcoded secrets (ship killer)
789
+
790
+ // Check for hardcoded secrets
1311
791
  const secrets = await searchEvidence(projectPath, {
1312
- query: String.raw`(sk_live_|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z\-_]{35}|xox[baprs]-[0-9A-Za-z\-]{10,})`,
1313
- mode: "regex",
1314
- limit: 20,
792
+ query: 'sk_live_|sk_test_|apiKey.*=.*["\'][a-zA-Z0-9]{20,}',
793
+ limit: 10,
1315
794
  });
1316
-
795
+
1317
796
  for (const ev of secrets.results) {
1318
- if (!/\.example|\.test\.|\.spec\./i.test(ev.file)) {
797
+ if (!ev.file.includes('.test.') && !ev.file.includes('.example')) {
1319
798
  shipKillers.push({
1320
- invariant: "security_no_exposed_secrets",
1321
- rule: "No hardcoded secrets or API keys",
799
+ invariant: 'security_no_exposed_secrets',
800
+ rule: 'No hardcoded secrets or API keys',
1322
801
  evidence: ev,
1323
802
  });
1324
803
  }
1325
804
  }
1326
-
1327
- // 3) “Success UI without confirmed success” (warning by default)
1328
- const fakeSuccess = await searchEvidence(projectPath, {
1329
- query: String.raw`toast\.(success|info)|setSuccess\s*\(|"success"|success:\s*true`,
1330
- mode: "regex",
1331
- limit: 30,
1332
- });
1333
-
1334
- for (const ev of fakeSuccess.results) {
1335
- // This is heuristic: you’ll tighten it by correlating with network calls later.
1336
- warnings.push({
1337
- invariant: "ux_no_fake_success",
1338
- rule: "Success UI should be tied to confirmed success (network/response)",
1339
- evidence: ev,
1340
- });
1341
- }
1342
-
1343
- const passed = shipKillers.length === 0;
805
+
1344
806
  return {
1345
- policy: pol,
1346
- category,
1347
- passed,
807
+ passed: shipKillers.length === 0,
1348
808
  shipKillers,
1349
809
  warnings,
1350
- summary: passed ? "✅ Invariants pass" : `❌ ${shipKillers.length} ship killers found`,
1351
- _attribution: CONTEXT_ATTRIBUTION,
810
+ summary: shipKillers.length === 0
811
+ ? '✅ All invariants pass'
812
+ : `❌ ${shipKillers.length} ship killers found - deployment blocked`,
1352
813
  };
1353
814
  }
1354
815
 
1355
- // =============================================================================
1356
- // IMPLEMENTATION: assumptions
1357
- // =============================================================================
1358
-
1359
816
  async function addAssumption(projectPath, args) {
1360
- const { description, reason, verificationSteps } = args || {};
1361
-
1362
- const list = state.assumptionsByProject.get(projectPath) || [];
1363
- if (list.length >= state.maxAssumptions) {
817
+ const { description, reason, verificationSteps } = args;
818
+
819
+ if (state.assumptions.length >= state.maxAssumptions) {
1364
820
  return {
1365
- error: `Assumption budget exceeded (${list.length}/${state.maxAssumptions})`,
1366
- message: "⚠️ Verify or delete assumptions. Don’t stack guesses.",
1367
- currentAssumptions: list,
1368
- _attribution: CONTEXT_ATTRIBUTION,
821
+ error: `Assumption budget exceeded (${state.assumptions.length}/${state.maxAssumptions})`,
822
+ message: '⚠️ You MUST verify existing assumptions or use proof tooling instead of assuming.',
823
+ currentAssumptions: state.assumptions,
1369
824
  };
1370
825
  }
1371
-
826
+
1372
827
  const assumption = {
1373
- id: `assumption_${Date.now()}_${crypto.randomUUID().slice(0, 6)}`,
1374
- description: String(description || ""),
1375
- reason: reason ? String(reason) : "",
1376
- verificationSteps: Array.isArray(verificationSteps) ? verificationSteps : [],
828
+ id: `assumption_${Date.now()}`,
829
+ description,
830
+ reason,
831
+ verificationSteps,
1377
832
  madeAt: new Date().toISOString(),
1378
833
  verified: false,
1379
834
  };
1380
-
1381
- list.push(assumption);
1382
- state.assumptionsByProject.set(projectPath, list);
1383
-
835
+
836
+ state.assumptions.push(assumption);
837
+
1384
838
  return {
1385
839
  assumption,
1386
- budget: { used: list.length, max: state.maxAssumptions, remaining: state.maxAssumptions - list.length },
1387
- warning: list.length >= state.maxAssumptions ? "⚠️ Assumption limit reached." : null,
1388
- _attribution: CONTEXT_ATTRIBUTION,
840
+ budget: {
841
+ used: state.assumptions.length,
842
+ max: state.maxAssumptions,
843
+ remaining: state.maxAssumptions - state.assumptions.length,
844
+ },
845
+ warning: state.assumptions.length >= state.maxAssumptions - 1
846
+ ? '⚠️ Approaching assumption limit. Consider verifying claims instead.'
847
+ : null,
1389
848
  };
1390
849
  }
1391
850
 
1392
851
  // =============================================================================
1393
- // PLAN VALIDATION & DRIFT
852
+ // PLAN VALIDATION & DRIFT DETECTION (Spec 10.3)
1394
853
  // =============================================================================
1395
854
 
1396
- async function getPlanValidationResult(projectPath, args) {
1397
- const { plan, strict = false } = args || {};
1398
-
855
+ async function validatePlanTool(projectPath, args) {
856
+ const { plan, strict = false } = args;
857
+
858
+ // Load contracts
1399
859
  const contracts = await loadContractsFromDisk(projectPath);
860
+
1400
861
  if (!contracts || Object.keys(contracts).length === 0) {
1401
862
  return {
1402
863
  valid: true,
@@ -1404,72 +865,87 @@ async function getPlanValidationResult(projectPath, args) {
1404
865
  violations: [],
1405
866
  warnings: [],
1406
867
  suggestions: ['Generate contracts with: vibecheck ctx sync'],
1407
- _attribution: CONTEXT_ATTRIBUTION,
1408
868
  };
1409
869
  }
1410
-
870
+
871
+ // Parse plan to extract actions
1411
872
  const actions = parsePlanActions(plan);
1412
-
1413
873
  const violations = [];
1414
874
  const warnings = [];
1415
875
  const suggestions = [];
1416
-
1417
- // routes
876
+
877
+ // Validate route references
1418
878
  if (contracts.routes && actions.routes.length > 0) {
1419
- const contractRoutes = (contracts.routes.routes || []).map((r) => ({
1420
- method: String(r.method || "*").toUpperCase(),
1421
- path: canonicalizePath(String(r.path || "")),
1422
- }));
1423
-
1424
- for (const r of actions.routes) {
1425
- const wanted = { method: String(r.method || "GET").toUpperCase(), path: canonicalizePath(r.path) };
1426
- const exists = contractRoutes.some((cr) => (cr.method === "*" || cr.method === wanted.method) && matchesParameterizedPath(cr.path, wanted.path));
1427
- if (!exists) {
1428
- violations.push({
1429
- type: "invented_route",
1430
- severity: "BLOCK",
1431
- route: wanted.path,
1432
- method: wanted.method,
1433
- message: `Plan references route ${wanted.method} ${wanted.path} not in routes contract`,
1434
- });
879
+ const contractRoutes = new Set(contracts.routes.routes?.map(r => r.path) || []);
880
+
881
+ for (const route of actions.routes) {
882
+ if (!contractRoutes.has(route.path)) {
883
+ // Check parameterized match
884
+ const match = contracts.routes.routes?.find(r => matchesParameterizedPath(r.path, route.path));
885
+ if (!match) {
886
+ violations.push({
887
+ type: 'invented_route',
888
+ severity: 'BLOCK',
889
+ route: route.path,
890
+ method: route.method,
891
+ message: `Plan references route ${route.method} ${route.path} which does not exist in contract`,
892
+ suggestion: `Available routes: ${contracts.routes.routes?.slice(0, 5).map(r => r.path).join(', ')}...`,
893
+ });
894
+ }
1435
895
  }
1436
896
  }
1437
897
  }
1438
-
1439
- // env vars
898
+
899
+ // Validate env var references
1440
900
  if (contracts.env && actions.envVars.length > 0) {
1441
- const contractVars = new Set((contracts.env.vars || []).map((v) => v.name));
1442
- for (const v of actions.envVars) {
1443
- if (!contractVars.has(v)) {
901
+ const contractVars = new Set(contracts.env.vars?.map(v => v.name) || []);
902
+
903
+ for (const varName of actions.envVars) {
904
+ if (!contractVars.has(varName)) {
1444
905
  warnings.push({
1445
- type: "undeclared_env",
1446
- severity: "WARN",
1447
- name: v,
1448
- message: `Plan uses env var ${v} not declared in env contract`,
1449
- suggestion: "Add to .vibecheck/contracts/env.json and .env.example",
906
+ type: 'undeclared_env',
907
+ severity: 'WARN',
908
+ name: varName,
909
+ message: `Plan uses env var ${varName} which is not in contract`,
910
+ suggestion: 'Add to .vibecheck/contracts/env.json or .env.example',
1450
911
  });
1451
912
  }
1452
913
  }
1453
914
  }
1454
-
1455
- // external services
915
+
916
+ // Validate auth assumptions
917
+ if (contracts.auth && actions.authAssumptions.length > 0) {
918
+ for (const assumption of actions.authAssumptions) {
919
+ if (assumption.type === 'no_auth') {
920
+ warnings.push({
921
+ type: 'auth_assumption',
922
+ severity: 'WARN',
923
+ message: 'Plan assumes some routes are public - verify against auth contract',
924
+ suggestion: `Protected patterns: ${contracts.auth.protectedPatterns?.slice(0, 3).join(', ')}...`,
925
+ });
926
+ }
927
+ }
928
+ }
929
+
930
+ // Validate external service usage
1456
931
  if (contracts.external && actions.externalCalls.length > 0) {
1457
- const contractServices = new Set((contracts.external.services || []).map((s) => s.name));
932
+ const contractServices = new Set(contracts.external.services?.map(s => s.name) || []);
933
+
1458
934
  for (const call of actions.externalCalls) {
1459
935
  if (!contractServices.has(call.service)) {
1460
936
  warnings.push({
1461
- type: "undeclared_service",
1462
- severity: "WARN",
937
+ type: 'undeclared_service',
938
+ severity: 'WARN',
1463
939
  service: call.service,
1464
- message: `Plan uses ${call.service} not declared in external contract`,
1465
- suggestion: "Add to .vibecheck/contracts/external.json",
940
+ message: `Plan uses ${call.service} which is not declared in external contract`,
941
+ suggestion: 'Add to .vibecheck/contracts/external.json',
1466
942
  });
1467
943
  }
1468
944
  }
1469
945
  }
1470
-
946
+
1471
947
  const valid = violations.length === 0 && (!strict || warnings.length === 0);
1472
-
948
+
1473
949
  return {
1474
950
  valid,
1475
951
  violations,
@@ -1477,48 +953,78 @@ async function getPlanValidationResult(projectPath, args) {
1477
953
  suggestions,
1478
954
  parsedActions: actions,
1479
955
  contractsLoaded: Object.keys(contracts),
1480
- message: valid ? "✅ Plan validated" : `❌ Plan invalid: ${violations.length} violations, ${warnings.length} warnings`,
1481
- _attribution: CONTEXT_ATTRIBUTION,
956
+ message: valid
957
+ ? '✅ Plan validated against contracts'
958
+ : `❌ Plan validation failed: ${violations.length} violations, ${warnings.length} warnings`,
1482
959
  };
1483
960
  }
1484
961
 
1485
962
  async function checkDriftTool(projectPath, args) {
1486
- const category = args?.category || "all";
1487
-
963
+ const category = args.category || 'all';
964
+
965
+ // Load contracts
1488
966
  const contracts = await loadContractsFromDisk(projectPath);
967
+
1489
968
  if (!contracts || Object.keys(contracts).length === 0) {
1490
- return { hasDrift: false, message: 'No contracts found. Run "vibecheck ctx sync".', findings: [], _attribution: CONTEXT_ATTRIBUTION };
969
+ return {
970
+ hasDrift: false,
971
+ message: 'No contracts found. Run "vibecheck ctx sync" to generate contracts.',
972
+ findings: [],
973
+ };
1491
974
  }
1492
-
975
+
976
+ // Build current truthpack
1493
977
  const truthpack = await buildCurrentTruthpack(projectPath);
978
+
979
+ // Detect drift
1494
980
  const findings = [];
1495
-
1496
- if (category === "all" || category === "routes") findings.push(...detectRouteDrift(contracts.routes, truthpack));
1497
- if (category === "all" || category === "env") findings.push(...detectEnvDrift(contracts.env, truthpack));
1498
- if (category === "all" || category === "auth") findings.push(...detectAuthDrift(contracts.auth, truthpack));
1499
-
1500
- const blocks = findings.filter((f) => f.severity === "BLOCK");
1501
- const warns = findings.filter((f) => f.severity === "WARN");
1502
-
981
+
982
+ if (category === 'all' || category === 'routes') {
983
+ const routeDrift = detectRouteDrift(contracts.routes, truthpack);
984
+ findings.push(...routeDrift);
985
+ }
986
+
987
+ if (category === 'all' || category === 'env') {
988
+ const envDrift = detectEnvDrift(contracts.env, truthpack);
989
+ findings.push(...envDrift);
990
+ }
991
+
992
+ if (category === 'all' || category === 'auth') {
993
+ const authDrift = detectAuthDrift(contracts.auth, truthpack);
994
+ findings.push(...authDrift);
995
+ }
996
+
997
+ const blocks = findings.filter(f => f.severity === 'BLOCK');
998
+ const warns = findings.filter(f => f.severity === 'WARN');
999
+
1503
1000
  return {
1504
1001
  hasDrift: findings.length > 0,
1505
- verdict: blocks.length > 0 ? "BLOCK" : warns.length > 0 ? "WARN" : "PASS",
1506
- summary: { blocks: blocks.length, warns: warns.length, total: findings.length },
1002
+ verdict: blocks.length > 0 ? 'BLOCK' : warns.length > 0 ? 'WARN' : 'PASS',
1003
+ summary: {
1004
+ blocks: blocks.length,
1005
+ warns: warns.length,
1006
+ total: findings.length,
1007
+ },
1507
1008
  findings,
1508
- message: findings.length === 0 ? "✅ No drift detected" : `⚠️ Drift detected: ${blocks.length} blocks, ${warns.length} warnings`,
1509
- _attribution: CONTEXT_ATTRIBUTION,
1009
+ message: findings.length === 0
1010
+ ? '✅ No drift detected - contracts match codebase'
1011
+ : `⚠️ Drift detected: ${blocks.length} blocks, ${warns.length} warnings. Run 'vibecheck ctx sync' to update.`,
1510
1012
  };
1511
1013
  }
1512
1014
 
1513
1015
  async function getContractsTool(projectPath, args) {
1514
- const type = args?.type || "all";
1016
+ const type = args.type || 'all';
1515
1017
  const contracts = await loadContractsFromDisk(projectPath);
1516
-
1018
+
1517
1019
  if (!contracts || Object.keys(contracts).length === 0) {
1518
- return { found: false, message: 'No contracts found. Run "vibecheck ctx sync".', contracts: {}, _attribution: CONTEXT_ATTRIBUTION };
1020
+ return {
1021
+ found: false,
1022
+ message: 'No contracts found. Run "vibecheck ctx sync" to generate contracts.',
1023
+ contracts: {},
1024
+ };
1519
1025
  }
1520
-
1521
- if (type === "all") {
1026
+
1027
+ if (type === 'all') {
1522
1028
  return {
1523
1029
  found: true,
1524
1030
  contracts,
@@ -1528,461 +1034,358 @@ async function getContractsTool(projectPath, args) {
1528
1034
  authPatterns: contracts.auth?.protectedPatterns?.length || 0,
1529
1035
  services: contracts.external?.services?.length || 0,
1530
1036
  },
1531
- _attribution: CONTEXT_ATTRIBUTION,
1532
1037
  };
1533
1038
  }
1534
-
1535
- return { found: !!contracts[type], contracts: { [type]: contracts[type] }, _attribution: CONTEXT_ATTRIBUTION };
1039
+
1040
+ return {
1041
+ found: !!contracts[type],
1042
+ contracts: { [type]: contracts[type] },
1043
+ };
1536
1044
  }
1537
1045
 
1046
+ // Helper: Load contracts from disk
1538
1047
  async function loadContractsFromDisk(projectPath) {
1539
- const contractDir = safeProjectJoin(projectPath, ".vibecheck/contracts");
1048
+ const contractDir = path.join(projectPath, '.vibecheck', 'contracts');
1540
1049
  const contracts = {};
1541
-
1050
+
1542
1051
  const files = {
1543
- routes: "routes.json",
1544
- env: "env.json",
1545
- auth: "auth.json",
1546
- external: "external.json",
1052
+ routes: 'routes.json',
1053
+ env: 'env.json',
1054
+ auth: 'auth.json',
1055
+ external: 'external.json',
1547
1056
  };
1548
-
1057
+
1549
1058
  for (const [key, file] of Object.entries(files)) {
1059
+ const filePath = path.join(contractDir, file);
1550
1060
  try {
1551
- const abs = path.join(contractDir, file);
1552
- const content = await fs.readFile(abs, "utf8");
1061
+ const content = await fs.readFile(filePath, 'utf8');
1553
1062
  contracts[key] = JSON.parse(content);
1554
- } catch (error) {
1555
- // Invalid JSON or file not found - skip this contract
1556
- // ignore
1557
- }
1063
+ } catch {}
1558
1064
  }
1559
-
1065
+
1560
1066
  return contracts;
1561
1067
  }
1562
1068
 
1563
- // =============================================================================
1564
- // PARSING HELPERS
1565
- // =============================================================================
1566
-
1567
- function canonicalizePath(p) {
1568
- let s = String(p || "").trim();
1569
- if (!s.startsWith("/")) s = "/" + s;
1570
- s = s.replace(/\/+/g, "/");
1571
- if (s.length > 1) s = s.replace(/\/$/, "");
1572
- return s;
1573
- }
1574
-
1575
- function dedupe(arr, keyFn) {
1576
- const seen = new Set();
1577
- const out = [];
1578
- for (const item of arr) {
1579
- const k = keyFn(item);
1580
- if (seen.has(k)) continue;
1581
- seen.add(k);
1582
- out.push(item);
1583
- }
1584
- return out;
1585
- }
1586
-
1069
+ // Helper: Parse plan to extract actions
1587
1070
  function parsePlanActions(plan) {
1588
1071
  const actions = {
1589
1072
  routes: [],
1590
1073
  envVars: [],
1074
+ files: [],
1591
1075
  authAssumptions: [],
1592
1076
  externalCalls: [],
1593
1077
  };
1594
-
1595
- const planText = typeof plan === "string" ? plan : JSON.stringify(plan);
1596
-
1597
- // routes
1078
+
1079
+ const planText = typeof plan === 'string' ? plan : JSON.stringify(plan);
1080
+
1081
+ // Extract route references
1598
1082
  const routePatterns = [
1599
- /(?:GET|POST|PUT|PATCH|DELETE)\s+([/][^\s"'`]+)/gi,
1083
+ /(?:fetch|axios|api\.?)\s*\(\s*['"`]([/][^'"`]+)['"`]/gi,
1084
+ /(?:GET|POST|PUT|PATCH|DELETE)\s+([/][^\s]+)/gi,
1600
1085
  /\/api\/[a-z0-9/_-]+/gi,
1601
1086
  ];
1602
-
1087
+
1603
1088
  for (const pattern of routePatterns) {
1604
1089
  let match;
1605
1090
  while ((match = pattern.exec(planText)) !== null) {
1606
- const p = (match[1] || match[0]).replace(/['"`]/g, "");
1607
- if (p.startsWith("/")) {
1608
- actions.routes.push({ path: canonicalizePath(p), method: inferMethodFromText(match[0]) });
1091
+ const p = match[1] || match[0];
1092
+ if (p.startsWith('/')) {
1093
+ actions.routes.push({
1094
+ path: p.replace(/['"`]/g, ''),
1095
+ method: inferMethodFromText(match[0]),
1096
+ });
1609
1097
  }
1610
1098
  }
1611
1099
  }
1612
-
1613
- actions.routes = dedupe(actions.routes, (r) => `${r.method}:${r.path}`);
1614
-
1615
- // env vars
1100
+
1101
+ // Extract env var references
1616
1102
  const envPatterns = [
1617
- /process\.env\.([A-Z_][A-Z0-9_]*)/g,
1618
- /import\.meta\.env\.([A-Z_][A-Z0-9_]*)/g,
1103
+ /process\.env\.([A-Z_][A-Z0-9_]*)/gi,
1104
+ /import\.meta\.env\.([A-Z_][A-Z0-9_]*)/gi,
1619
1105
  ];
1106
+
1620
1107
  for (const pattern of envPatterns) {
1621
1108
  let match;
1622
- while ((match = pattern.exec(planText)) !== null) actions.envVars.push(match[1]);
1109
+ while ((match = pattern.exec(planText)) !== null) {
1110
+ if (match[1] && /^[A-Z]/.test(match[1])) {
1111
+ actions.envVars.push(match[1]);
1112
+ }
1113
+ }
1114
+ }
1115
+
1116
+ // Extract auth assumptions
1117
+ if (/(?:authenticated|logged in|auth required|protected)/i.test(planText)) {
1118
+ actions.authAssumptions.push({ type: 'requires_auth' });
1623
1119
  }
1624
- actions.envVars = dedupe(actions.envVars, (v) => v);
1625
-
1626
- // auth assumptions
1627
- if (/(authenticated|logged in|auth required|protected)/i.test(planText)) actions.authAssumptions.push({ type: "requires_auth" });
1628
- if (/(public|no auth|unauthenticated)/i.test(planText)) actions.authAssumptions.push({ type: "no_auth" });
1629
-
1630
- // external services
1631
- const services = [
1632
- { re: /stripe\./i, service: "stripe" },
1633
- { re: /github\./i, service: "github" },
1634
- { re: /sendgrid\./i, service: "sendgrid" },
1635
- { re: /twilio\./i, service: "twilio" },
1636
- { re: /supabase\./i, service: "supabase" },
1120
+ if (/(?:public|no auth|unauthenticated)/i.test(planText)) {
1121
+ actions.authAssumptions.push({ type: 'no_auth' });
1122
+ }
1123
+
1124
+ // Extract external service references
1125
+ const servicePatterns = [
1126
+ { pattern: /stripe\./gi, service: 'stripe' },
1127
+ { pattern: /github\./gi, service: 'github' },
1128
+ { pattern: /sendgrid\./gi, service: 'sendgrid' },
1129
+ { pattern: /twilio\./gi, service: 'twilio' },
1130
+ { pattern: /supabase\./gi, service: 'supabase' },
1637
1131
  ];
1638
- for (const s of services) {
1639
- if (s.re.test(planText)) actions.externalCalls.push({ service: s.service });
1132
+
1133
+ for (const { pattern, service } of servicePatterns) {
1134
+ if (pattern.test(planText)) {
1135
+ actions.externalCalls.push({ service });
1136
+ }
1640
1137
  }
1641
- actions.externalCalls = dedupe(actions.externalCalls, (c) => c.service);
1642
-
1138
+
1643
1139
  return actions;
1644
1140
  }
1645
1141
 
1646
1142
  function inferMethodFromText(text) {
1647
- const upper = String(text || "").toUpperCase();
1648
- if (upper.includes("POST")) return "POST";
1649
- if (upper.includes("PUT")) return "PUT";
1650
- if (upper.includes("PATCH")) return "PATCH";
1651
- if (upper.includes("DELETE")) return "DELETE";
1652
- return "GET";
1143
+ const upper = text.toUpperCase();
1144
+ if (upper.includes('POST')) return 'POST';
1145
+ if (upper.includes('PUT')) return 'PUT';
1146
+ if (upper.includes('PATCH')) return 'PATCH';
1147
+ if (upper.includes('DELETE')) return 'DELETE';
1148
+ return 'GET';
1653
1149
  }
1654
1150
 
1655
1151
  function matchesParameterizedPath(pattern, actual) {
1656
- const pParts = canonicalizePath(pattern).split("/").filter(Boolean);
1657
- const aParts = canonicalizePath(actual).split("/").filter(Boolean);
1658
- if (pParts.length !== aParts.length) return false;
1659
-
1660
- for (let i = 0; i < pParts.length; i++) {
1661
- const p = pParts[i];
1662
- if (p.startsWith(":") || p.startsWith("*") || p.startsWith("[")) continue;
1663
- if (p !== aParts[i]) return false;
1152
+ const patternParts = pattern.split('/').filter(Boolean);
1153
+ const actualParts = actual.split('/').filter(Boolean);
1154
+ if (patternParts.length !== actualParts.length) return false;
1155
+ for (let i = 0; i < patternParts.length; i++) {
1156
+ const p = patternParts[i];
1157
+ if (p.startsWith(':') || p.startsWith('*') || p.startsWith('[')) continue;
1158
+ if (p !== actualParts[i]) return false;
1664
1159
  }
1665
1160
  return true;
1666
1161
  }
1667
1162
 
1668
- // =============================================================================
1669
- // DRIFT DETECTORS (improved: added removals)
1670
- // =============================================================================
1671
-
1672
- async function buildCurrentTruthpack(projectPath, refresh = false) {
1673
- const routes = await extractRoutes(projectPath, refresh);
1163
+ // Helper: Build current truthpack (lightweight version)
1164
+ async function buildCurrentTruthpack(projectPath) {
1165
+ const routes = await extractRoutes(projectPath);
1674
1166
  const env = await extractEnv(projectPath);
1675
1167
  const auth = await extractAuth(projectPath);
1676
-
1168
+
1677
1169
  return {
1678
1170
  routes: {
1679
- server: routes.routes || [],
1680
- gaps: routes.gaps || [],
1681
- engine: routes.engine,
1171
+ server: routes.routes,
1172
+ clientRefs: [],
1173
+ },
1174
+ env: {
1175
+ vars: env.used,
1176
+ declared: env.declared.map(d => d.name),
1177
+ },
1178
+ auth: {
1179
+ nextMatcherPatterns: [], // Would need middleware parsing
1682
1180
  },
1683
- env: { vars: env.used || [], declared: (env.declared || []).map((d) => d.name) },
1684
- auth: { nextMatcherPatterns: auth?.nextMatcherPatterns || [] },
1685
1181
  };
1686
1182
  }
1687
1183
 
1184
+ // Helper: Detect route drift
1688
1185
  function detectRouteDrift(routeContract, truthpack) {
1689
1186
  const findings = [];
1690
1187
  if (!routeContract?.routes) return findings;
1691
-
1692
- // Use Route Truth v1 canonicalization for consistency
1693
- const canonicalize = routeTruthCanonicalize || canonicalizePath;
1694
-
1695
- const contractSet = new Set(routeContract.routes.map((r) => `${String(r.method || "*").toUpperCase()}_${canonicalize(r.path)}`));
1696
- const server = truthpack?.routes?.server || [];
1697
- const serverSet = new Set(server.map((r) => `${String(r.method || "*").toUpperCase()}_${canonicalize(r.path)}`));
1698
- const gaps = truthpack?.routes?.gaps || [];
1699
-
1700
- // new routes (in code, not in contract)
1701
- for (const key of serverSet) {
1702
- if (!contractSet.has(key)) {
1703
- const [method, routePath] = key.split("_");
1704
- const routeInfo = server.find((r) => canonicalize(r.path) === routePath && String(r.method || "*").toUpperCase() === method);
1705
-
1706
- findings.push({
1707
- type: "new_route_not_in_contract",
1708
- severity: "BLOCK",
1709
- title: `New route not in contract: ${method} ${routePath}`,
1710
- message: "Route exists in code but not synced to routes contract.",
1711
- file: routeInfo?.file,
1712
- framework: routeInfo?.framework,
1713
- });
1714
- }
1715
- }
1716
-
1717
- // removed routes (in contract, not in code)
1718
- for (const key of contractSet) {
1719
- if (!serverSet.has(key)) {
1720
- const hasGaps = gaps.length > 0;
1188
+
1189
+ const contractRoutes = new Map(routeContract.routes.map(r => [`${r.method}_${r.path}`, r]));
1190
+ const serverRoutes = truthpack?.routes?.server || [];
1191
+
1192
+ for (const route of serverRoutes) {
1193
+ const key = `${route.method}_${route.path}`;
1194
+ if (!contractRoutes.has(key)) {
1721
1195
  findings.push({
1722
- type: "contract_route_missing_in_code",
1723
- severity: hasGaps ? "WARN" : "WARN", // Demote if gaps exist
1724
- title: `Contract route missing in code: ${key.replace("_", " ")}`,
1725
- message: hasGaps
1726
- ? "Contract lists a route not detected in code. Note: some plugins couldn't be resolved."
1727
- : "Contract lists a route not detected in code (stale contract?).",
1728
- mayBeExtractorGap: hasGaps,
1196
+ type: 'new_route_not_in_contract',
1197
+ severity: 'BLOCK',
1198
+ title: `New route ${route.method} ${route.path} not in contract`,
1199
+ message: 'Route added to code but not synced to contracts.',
1729
1200
  });
1730
1201
  }
1731
1202
  }
1732
-
1733
- // Report gaps as info
1734
- if (gaps.length > 0) {
1735
- findings.push({
1736
- type: "extractor_gaps",
1737
- severity: "INFO",
1738
- title: `Route extractor has ${gaps.length} unresolved module(s)`,
1739
- message: "Some Fastify plugins or imports couldn't be resolved - routes may be incomplete.",
1740
- gaps: gaps.slice(0, 5), // Limit to first 5
1741
- });
1742
- }
1743
-
1203
+
1744
1204
  return findings;
1745
1205
  }
1746
1206
 
1207
+ // Helper: Detect env drift
1747
1208
  function detectEnvDrift(envContract, truthpack) {
1748
1209
  const findings = [];
1749
1210
  if (!envContract?.vars) return findings;
1750
-
1751
- const contractVars = new Set(envContract.vars.map((v) => v.name));
1752
- const usedVars = new Set((truthpack?.env?.vars || []).map((v) => v.name));
1753
-
1754
- for (const name of usedVars) {
1755
- if (!contractVars.has(name)) {
1756
- findings.push({
1757
- type: "new_env_not_in_contract",
1758
- severity: "WARN",
1759
- title: `Env var used but not in contract: ${name}`,
1760
- message: "Env var used in code but not declared in contracts.",
1761
- });
1762
- }
1763
- }
1764
-
1765
- for (const name of contractVars) {
1766
- if (!usedVars.has(name)) {
1211
+
1212
+ const contractVars = new Set(envContract.vars.map(v => v.name));
1213
+ const usedVars = truthpack?.env?.vars || [];
1214
+
1215
+ for (const v of usedVars) {
1216
+ if (!contractVars.has(v.name)) {
1767
1217
  findings.push({
1768
- type: "contract_env_unused",
1769
- severity: "WARN",
1770
- title: `Env var in contract appears unused: ${name}`,
1771
- message: "Contract env var not detected in usage (stale contract or extractor gap).",
1218
+ type: 'new_env_not_in_contract',
1219
+ severity: 'WARN',
1220
+ title: `Env var ${v.name} used but not in contract`,
1221
+ message: 'Env var used in code but not declared in contracts.',
1772
1222
  });
1773
1223
  }
1774
1224
  }
1775
-
1225
+
1776
1226
  return findings;
1777
1227
  }
1778
1228
 
1229
+ // Helper: Detect auth drift
1779
1230
  function detectAuthDrift(authContract, truthpack) {
1780
1231
  const findings = [];
1781
1232
  if (!authContract?.protectedPatterns) return findings;
1782
-
1783
- const contract = new Set(authContract.protectedPatterns);
1784
- const current = new Set(truthpack?.auth?.nextMatcherPatterns || []);
1785
-
1786
- for (const pattern of current) {
1787
- if (!contract.has(pattern)) {
1233
+
1234
+ const contractPatterns = new Set(authContract.protectedPatterns);
1235
+ const currentPatterns = new Set(truthpack?.auth?.nextMatcherPatterns || []);
1236
+
1237
+ for (const pattern of currentPatterns) {
1238
+ if (!contractPatterns.has(pattern)) {
1788
1239
  findings.push({
1789
- type: "new_auth_pattern",
1790
- severity: "BLOCK",
1791
- title: `New auth pattern not in contract: ${pattern}`,
1792
- message: "Auth matcher/pattern changed but contracts not updated.",
1240
+ type: 'new_auth_pattern',
1241
+ severity: 'BLOCK',
1242
+ title: `New auth pattern "${pattern}" not in contract`,
1243
+ message: 'Auth pattern added but not in contracts.',
1793
1244
  });
1794
1245
  }
1795
1246
  }
1796
-
1247
+
1797
1248
  return findings;
1798
1249
  }
1799
1250
 
1800
1251
  // =============================================================================
1801
- // DOMAIN HELPERS
1802
- // =============================================================================
1803
-
1804
- function detectDomains(task) {
1805
- const domains = [];
1806
- const t = String(task || "");
1807
- if (/auth|login|logout|session|password/i.test(t)) domains.push("auth");
1808
- if (/billing|payment|stripe|subscription/i.test(t)) domains.push("billing");
1809
- if (/env|secret|config/i.test(t)) domains.push("env");
1810
- if (/route|api|endpoint/i.test(t)) domains.push("api");
1811
- if (/component|button|ui|form/i.test(t)) domains.push("ui");
1812
- return domains.length ? domains : ["general"];
1813
- }
1814
-
1815
- function extractKeywords(task) {
1816
- return String(task || "")
1817
- .toLowerCase()
1818
- .replace(/[^a-z0-9\s/_-]/g, " ")
1819
- .split(/\s+/)
1820
- .filter((w) => w.length > 2)
1821
- .slice(0, 40);
1822
- }
1823
-
1824
- function getInvariantsForDomains(domains) {
1825
- const invariants = [];
1826
- if (domains.includes("auth")) invariants.push("No protected route without server middleware");
1827
- if (domains.includes("billing")) invariants.push("No paid feature without server-side enforcement");
1828
- invariants.push("No success UI without confirmed success");
1829
- invariants.push("No invented routes/env vars/functions in plans");
1830
- return invariants;
1831
- }
1832
-
1833
- function estimateTokens(context) {
1834
- let tokens = 0;
1835
- if (context?.routes) tokens += context.routes.length * 50;
1836
- if (context?.auth) tokens += 220;
1837
- if (context?.billing) tokens += 220;
1838
- if (context?.env) tokens += 180;
1839
- return tokens;
1840
- }
1841
-
1842
- function generateContextWarnings(domains, policy, routeCount) {
1843
- const warnings = [];
1844
- if (domains.includes("auth") || domains.includes("billing")) warnings.push("High-stakes domain: verify claims before edits.");
1845
- if (routeCount > 50 && policy === "strict") warnings.push("Large route surface: narrow task or specify files.");
1846
- return warnings;
1847
- }
1848
-
1849
- // =============================================================================
1850
- // SOURCE FILE DISCOVERY (safe + bounded)
1252
+ // HELPERS
1851
1253
  // =============================================================================
1852
1254
 
1853
- async function findSourceFiles(projectPath, opts) {
1854
- const files = [];
1855
- const ignoreDirs = new Set(["node_modules", "dist", "build", ".git", ".next", "coverage", "out"]);
1856
-
1857
- async function walk(dirAbs) {
1858
- let entries = [];
1859
- try {
1860
- entries = await fs.readdir(dirAbs, { withFileTypes: true });
1861
- } catch {
1862
- return;
1863
- }
1864
-
1865
- for (const entry of entries) {
1866
- const full = path.join(dirAbs, entry.name);
1867
- if (entry.isDirectory()) {
1868
- if (ignoreDirs.has(entry.name) || entry.name.startsWith(".")) continue;
1869
- await walk(full);
1870
- } else if (entry.isFile()) {
1871
- if (!/\.(ts|tsx|js|jsx)$/.test(entry.name)) continue;
1872
- const rel = path.relative(projectPath, full).replace(/\\/g, "/");
1873
- if (!opts?.includeTests && isTestFilePath(rel)) continue;
1874
- files.push(full);
1875
- if (files.length > 2500) return; // hard cap
1876
- }
1877
- }
1255
+ function getCommitHash(projectPath) {
1256
+ try {
1257
+ return execSync('git rev-parse HEAD', { cwd: projectPath, encoding: 'utf8' }).trim();
1258
+ } catch {
1259
+ return 'unknown';
1878
1260
  }
1879
-
1880
- await walk(path.resolve(projectPath));
1881
- return files;
1882
1261
  }
1883
1262
 
1884
- // =============================================================================
1885
- // ROUTE TRUTH V1 INTEGRATION (AST-based, follows Fastify register prefixes + Next.js app/pages)
1886
- // =============================================================================
1887
-
1888
1263
  /**
1889
- * Get or build the Route Truth v1 index for a project.
1890
- * This is the SINGLE SOURCE OF TRUTH for route reality.
1264
+ * Generate a project fingerprint for stale assumption detection (Spec 10.2)
1265
+ * Includes: commit hash, key file hashes, timestamp
1891
1266
  */
1892
- async function getRouteIndex(projectPath, refresh = false) {
1893
- if (!refresh && state.routeIndexByProject.has(projectPath)) {
1894
- return state.routeIndexByProject.get(projectPath);
1267
+ export function getProjectFingerprint(projectPath) {
1268
+ const commitHash = getCommitHash(projectPath);
1269
+ const keyFiles = [
1270
+ 'package.json',
1271
+ 'prisma/schema.prisma',
1272
+ 'next.config.js',
1273
+ 'next.config.ts',
1274
+ '.vibecheck/contracts/routes.json',
1275
+ ];
1276
+
1277
+ const fileHashes = [];
1278
+ for (const file of keyFiles) {
1279
+ try {
1280
+ const content = require('fs').readFileSync(path.join(projectPath, file), 'utf8');
1281
+ const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 8);
1282
+ fileHashes.push(`${file}:${hash}`);
1283
+ } catch {}
1895
1284
  }
1896
-
1897
- const index = new RouteIndex();
1898
- await index.build(projectPath);
1899
- state.routeIndexByProject.set(projectPath, index);
1900
- return index;
1285
+
1286
+ const fingerprintData = [
1287
+ commitHash,
1288
+ ...fileHashes,
1289
+ ].join('|');
1290
+
1291
+ return {
1292
+ hash: crypto.createHash('sha256').update(fingerprintData).digest('hex').slice(0, 16),
1293
+ commitHash,
1294
+ fileHashes,
1295
+ generatedAt: new Date().toISOString(),
1296
+ };
1901
1297
  }
1902
1298
 
1903
1299
  /**
1904
- * Extract routes using Route Truth v1 (AST-based).
1905
- * - Fastify: follows register() prefixes, resolves relative plugin imports
1906
- * - Next.js: App Router (route.ts exports) + Pages Router (/pages/api/**)
1907
- * - Proper path canonicalization (dynamic segments, splats)
1300
+ * Wrap MCP response with standard metadata including fingerprint (Spec 10.2)
1908
1301
  */
1909
- async function extractRoutes(projectPath, refresh = false) {
1910
- const index = await getRouteIndex(projectPath, refresh);
1911
- const routeMap = index.getRouteMap();
1912
-
1913
- // Transform to truthpack format
1914
- const routes = (routeMap.server || []).map((r) => ({
1915
- method: r.method,
1916
- path: r.path,
1917
- file: r.handler,
1918
- line: r.evidence?.[0]?.lines?.split("-")[0] || 1,
1919
- framework: r.framework,
1920
- routerType: r.routerType,
1921
- confidence: r.confidence === "high" ? 0.95 : r.confidence === "med" ? 0.8 : 0.6,
1922
- evidence: r.evidence,
1923
- }));
1924
-
1925
- const gaps = routeMap.gaps || [];
1926
- const hasGaps = gaps.length > 0;
1927
-
1302
+ export function wrapMcpResponse(data, projectPath) {
1928
1303
  return {
1929
- count: routes.length,
1930
- routes,
1931
- gaps,
1932
- confidence: hasGaps ? 0.7 : (routes.length > 0 ? 0.95 : 0.3),
1933
- engine: "route-truth-v1",
1934
- _note: hasGaps
1935
- ? `⚠️ ${gaps.length} unresolved plugins/modules - some routes may be missing`
1936
- : "AST-based extraction (Fastify register prefixes + Next.js app/pages)",
1304
+ ok: true,
1305
+ version: '2.0.0',
1306
+ projectFingerprint: getProjectFingerprint(projectPath),
1307
+ data,
1937
1308
  };
1938
1309
  }
1939
1310
 
1311
+ async function extractRoutes(projectPath) {
1312
+ const routes = [];
1313
+ const files = await findSourceFiles(projectPath);
1314
+ const routePatterns = [
1315
+ /\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
1316
+ /router\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
1317
+ ];
1318
+
1319
+ for (const file of files.slice(0, 50)) {
1320
+ try {
1321
+ const content = await fs.readFile(file, 'utf8');
1322
+ const relPath = path.relative(projectPath, file);
1323
+
1324
+ for (const pattern of routePatterns) {
1325
+ let match;
1326
+ pattern.lastIndex = 0;
1327
+ while ((match = pattern.exec(content)) !== null) {
1328
+ const line = content.substring(0, match.index).split('\n').length;
1329
+ routes.push({
1330
+ method: match[1].toUpperCase(),
1331
+ path: match[2],
1332
+ file: relPath,
1333
+ line,
1334
+ });
1335
+ }
1336
+ }
1337
+ } catch {}
1338
+ }
1339
+
1340
+ return { count: routes.length, routes, confidence: routes.length > 0 ? 0.8 : 0.2 };
1341
+ }
1342
+
1940
1343
  async function extractAuth(projectPath) {
1941
- // TODO: parse Next middleware matcher for real; this is evidence-based heuristics
1942
- const evidence = await searchEvidence(projectPath, { query: "middleware|auth|authenticate|authorize|jwt|session", mode: "regex", limit: 30 });
1943
- return {
1944
- count: evidence.count,
1945
- indicators: evidence.results,
1946
- nextMatcherPatterns: [],
1947
- confidence: evidence.count > 5 ? 0.8 : 0.4,
1948
- };
1344
+ const evidence = await searchEvidence(projectPath, {
1345
+ query: 'auth|authenticate|authorize|middleware|guard|jwt|session',
1346
+ limit: 30
1347
+ });
1348
+ return { count: evidence.count, indicators: evidence.results, confidence: evidence.count > 5 ? 0.8 : 0.4 };
1949
1349
  }
1950
1350
 
1951
1351
  async function extractBilling(projectPath) {
1952
- const evidence = await searchEvidence(projectPath, { query: "stripe|billing|payment|subscription|checkout|tier|isPro", mode: "regex", limit: 25 });
1953
- return { count: evidence.count, indicators: evidence.results, confidence: evidence.count > 3 ? 0.75 : 0.3 };
1352
+ const evidence = await searchEvidence(projectPath, {
1353
+ query: 'stripe|billing|payment|subscription|checkout|tier|isPro',
1354
+ limit: 20
1355
+ });
1356
+ return { count: evidence.count, indicators: evidence.results, confidence: evidence.count > 3 ? 0.7 : 0.3 };
1954
1357
  }
1955
1358
 
1956
1359
  async function extractEnv(projectPath) {
1957
1360
  const declared = [];
1958
1361
  const used = [];
1959
-
1960
- // .env.example declarations
1362
+
1363
+ // Check .env.example
1961
1364
  try {
1962
- const content = await safeReadFile(projectPath, ".env.example");
1963
- const lines = content.split(/\r?\n/);
1365
+ const content = await fs.readFile(path.join(projectPath, '.env.example'), 'utf8');
1366
+ const lines = content.split('\n');
1964
1367
  for (let i = 0; i < lines.length; i++) {
1965
- const m = lines[i].match(/^([A-Z][A-Z0-9_]*)=/);
1966
- if (m) declared.push({ name: m[1], line: i + 1 });
1368
+ const match = lines[i].match(/^([A-Z][A-Z0-9_]*)=/);
1369
+ if (match) declared.push({ name: match[1], line: i + 1 });
1967
1370
  }
1968
- } catch { /* ignore */ }
1969
-
1970
- // usage
1971
- const usage = await searchEvidence(projectPath, { query: "process\\.env\\.([A-Z][A-Z0-9_]*)", mode: "regex", limit: 200, includeTests: false });
1972
- for (const ev of usage.results) {
1973
- const m = ev.snippet.match(/process\.env\.([A-Z][A-Z0-9_]*)/);
1974
- if (m) used.push({ name: m[1], file: ev.file, line: ev.line });
1371
+ } catch {}
1372
+
1373
+ // Check process.env usage
1374
+ const evidence = await searchEvidence(projectPath, { query: 'process\\.env\\.([A-Z][A-Z0-9_]*)', limit: 50 });
1375
+ for (const ev of evidence.results) {
1376
+ const match = ev.snippet.match(/process\.env\.([A-Z][A-Z0-9_]*)/);
1377
+ if (match) used.push({ name: match[1], file: ev.file, line: ev.line });
1975
1378
  }
1976
-
1977
- const declaredNames = new Set(declared.map((d) => d.name));
1978
- const usedNames = new Set(used.map((u) => u.name));
1979
-
1379
+
1380
+ const declaredNames = new Set(declared.map(d => d.name));
1381
+ const usedNames = new Set(used.map(u => u.name));
1382
+
1980
1383
  return {
1981
1384
  declared,
1982
1385
  used,
1983
1386
  mismatches: {
1984
- undeclared: Array.from(usedNames).filter((n) => !declaredNames.has(n)),
1985
- unused: Array.from(declaredNames).filter((n) => !usedNames.has(n)),
1387
+ undeclared: Array.from(usedNames).filter(n => !declaredNames.has(n)),
1388
+ unused: Array.from(declaredNames).filter(n => !usedNames.has(n)),
1986
1389
  },
1987
1390
  confidence: 0.8,
1988
1391
  };
@@ -1990,194 +1393,180 @@ async function extractEnv(projectPath) {
1990
1393
 
1991
1394
  async function extractSchema(projectPath) {
1992
1395
  const schemas = [];
1396
+
1397
+ // Check Prisma
1993
1398
  try {
1994
- const content = await safeReadFile(projectPath, "prisma/schema.prisma");
1399
+ const content = await fs.readFile(path.join(projectPath, 'prisma', 'schema.prisma'), 'utf8');
1995
1400
  const models = content.matchAll(/model\s+(\w+)\s*\{/g);
1996
- for (const m of models) schemas.push({ type: "prisma_model", name: m[1] });
1997
- } catch { /* ignore */ }
1401
+ for (const match of models) {
1402
+ schemas.push({ type: 'prisma_model', name: match[1] });
1403
+ }
1404
+ } catch {}
1405
+
1998
1406
  return { count: schemas.length, schemas, confidence: schemas.length > 0 ? 0.9 : 0.3 };
1999
1407
  }
2000
1408
 
2001
- async function extractGraph(_projectPath) {
2002
- return { nodes: [], edges: [], message: "Graph extraction not implemented in this module." };
1409
+ async function extractGraph(projectPath) {
1410
+ return { nodes: [], edges: [], message: 'Graph extraction requires full build. Use get_truthpack with refresh=true.' };
2003
1411
  }
2004
1412
 
2005
- // =============================================================================
2006
- // CLAIM VERIFIERS (tightened)
2007
- // =============================================================================
2008
-
2009
- /**
2010
- * Verify route_exists claim using Route Truth v1 index.
2011
- * This uses proper AST-based route detection with:
2012
- * - Fastify register() prefix resolution
2013
- * - Next.js App/Pages router support
2014
- * - Parameterized path matching (:id, *slug)
2015
- * - Closest route suggestions on miss
2016
- */
2017
- async function verifyRouteExists(projectPath, subject, refresh = false) {
2018
- const index = await getRouteIndex(projectPath, refresh);
2019
-
2020
- const claim = {
2021
- method: subject?.method || "*",
2022
- path: subject?.path,
2023
- };
2024
-
2025
- // Use Route Truth v1 validation (handles parameterized matching, gaps, etc.)
2026
- const result = await routeTruthValidate(claim, projectPath, index);
2027
-
2028
- if (result.result === "true") {
2029
- const matched = result.matchedRoute;
2030
- return {
2031
- result: "true",
2032
- confidence: result.confidence === "high" ? "high" : result.confidence === "med" ? "medium" : "low",
2033
- evidence: (matched?.evidence || []).map((e) => ({
2034
- file: e.file,
2035
- lines: e.lines,
2036
- snippet: "",
2037
- hash: e.snippetHash || sha16(`${e.file}:${e.lines}`),
2038
- })),
2039
- matchedRoute: {
2040
- method: matched?.method,
2041
- path: matched?.path,
2042
- file: matched?.handler,
2043
- framework: matched?.framework,
2044
- },
2045
- };
2046
- }
2047
-
2048
- if (result.result === "unknown") {
2049
- // Has gaps (unresolved plugins) - can't be certain
1413
+ async function verifyRouteExists(projectPath, subject) {
1414
+ const routes = await extractRoutes(projectPath);
1415
+ const match = routes.routes.find(r =>
1416
+ r.path === subject.path ||
1417
+ (subject.method && r.method === subject.method.toUpperCase() && r.path === subject.path)
1418
+ );
1419
+
1420
+ if (match) {
2050
1421
  return {
2051
- result: "unknown",
2052
- confidence: "low",
2053
- evidence: [],
2054
- gaps: result.gaps,
2055
- closestRoutes: (result.closestRoutes || []).map((r) => ({
2056
- method: r.method,
2057
- path: r.path,
2058
- file: r.handler,
2059
- })),
2060
- nextSteps: result.nextSteps || [
2061
- "Some plugins/modules could not be resolved",
2062
- "Route may exist but extractor couldn't follow import chain",
2063
- ],
1422
+ result: 'true',
1423
+ confidence: 'high',
1424
+ evidence: [{ file: match.file, lines: `${match.line}`, hash: '' }],
2064
1425
  };
2065
1426
  }
2066
-
2067
- // result === "false" - route definitely doesn't exist
2068
- const closest = (result.closestRoutes || []).map((r) => ({
2069
- method: r.method,
2070
- path: r.path,
2071
- file: r.handler,
2072
- }));
2073
-
2074
- return {
2075
- result: "false",
2076
- confidence: "high",
2077
- evidence: [],
2078
- closestRoutes: closest,
2079
- nextSteps: result.nextSteps || (closest.length
2080
- ? [`Route not found. Did you mean: ${closest.map((r) => `${r.method} ${r.path}`).join(", ")}?`]
2081
- : ["Route not found. No similar routes detected."]),
2082
- };
1427
+
1428
+ return { result: 'false', confidence: 'high', evidence: [], nextSteps: ['Route not found in codebase'] };
2083
1429
  }
2084
1430
 
2085
1431
  async function verifyFileExists(projectPath, subject) {
2086
- const rel = String(subject?.path || subject?.name || "");
2087
- if (!rel) return { result: "unknown", confidence: "low", evidence: [], nextSteps: ["Missing subject.path"] };
1432
+ const filePath = path.join(projectPath, subject.path || subject.name);
2088
1433
  try {
2089
- const abs = safeProjectJoin(projectPath, rel);
2090
- await fs.access(abs);
2091
- return { result: "true", confidence: "high", evidence: [{ file: rel, lines: "1", hash: sha16(rel) }] };
1434
+ await fs.access(filePath);
1435
+ return { result: 'true', confidence: 'high', evidence: [{ file: subject.path || subject.name, lines: '1', hash: '' }] };
2092
1436
  } catch {
2093
- return { result: "false", confidence: "high", evidence: [] };
1437
+ return { result: 'false', confidence: 'high', evidence: [] };
2094
1438
  }
2095
1439
  }
2096
1440
 
2097
1441
  async function verifyEnvVar(projectPath, subject, claim) {
2098
1442
  const env = await extractEnv(projectPath);
2099
- const name = String(subject?.name || "");
2100
- if (!name) return { result: "unknown", confidence: "low", evidence: [], nextSteps: ["Missing subject.name"] };
2101
-
2102
- const declared = env.declared?.find((d) => d.name === name);
2103
- const used = (env.used || []).filter((u) => u.name === name);
2104
-
2105
- if (claim === "env_var_exists") {
2106
- if (declared) {
2107
- return { result: "true", confidence: "high", evidence: [{ file: ".env.example", lines: `${declared.line}`, hash: sha16(`${name}:${declared.line}`) }] };
2108
- }
2109
- // "exists" might be in real env, but contract says no -> unknown
2110
- return { result: "unknown", confidence: "low", evidence: [], nextSteps: ["Not declared in .env.example / contract"] };
2111
- }
2112
-
2113
- // env_var_used
2114
- if (used.length > 0) {
1443
+ const name = subject.name;
1444
+
1445
+ const isDeclared = env.declared.some(d => d.name === name);
1446
+ const isUsed = env.used.some(u => u.name === name);
1447
+
1448
+ if (claim === 'env_var_exists') {
1449
+ return {
1450
+ result: isDeclared ? 'true' : 'unknown',
1451
+ confidence: isDeclared ? 'high' : 'low',
1452
+ evidence: isDeclared ? [{ file: '.env.example', lines: '1', hash: '' }] : [],
1453
+ };
1454
+ } else {
2115
1455
  return {
2116
- result: "true",
2117
- confidence: "high",
2118
- evidence: used.map((u) => ({ file: u.file, lines: `${u.line}`, hash: sha16(`${u.file}:${u.line}:${name}`) })),
1456
+ result: isUsed ? 'true' : 'false',
1457
+ confidence: 'high',
1458
+ evidence: env.used.filter(u => u.name === name).map(u => ({ file: u.file, lines: `${u.line}`, hash: '' })),
2119
1459
  };
2120
1460
  }
2121
-
2122
- return { result: "false", confidence: "high", evidence: [] };
2123
1461
  }
2124
1462
 
2125
1463
  async function verifyRouteGuarded(projectPath, subject) {
2126
- const routePath = canonicalizePath(String(subject?.path || ""));
2127
- if (!routePath) return { result: "unknown", confidence: "low", evidence: [], nextSteps: ["Missing subject.path"] };
2128
-
2129
- // heuristic: route mentioned near middleware/auth patterns
2130
- const evidence = await searchEvidence(projectPath, {
2131
- query: String.raw`(middleware|auth|authorize|session).*(\Q${routePath}\E)|\Q${routePath}\E.*(middleware|auth|authorize|session)`.replace(/\Q|\E/g, ""),
2132
- mode: "regex",
2133
- limit: 10,
2134
- });
2135
-
1464
+ const routePath = subject.path;
1465
+ const evidence = await searchEvidence(projectPath, { query: `${routePath}.*auth|middleware.*${routePath}`, limit: 5 });
1466
+
2136
1467
  if (evidence.count > 0) {
2137
1468
  return {
2138
- result: "true",
2139
- confidence: "med",
2140
- evidence: evidence.results.map((e) => ({ file: e.file, lines: `${e.line}`, hash: e.hash, snippet: e.snippet })),
1469
+ result: 'true',
1470
+ confidence: 'medium',
1471
+ evidence: evidence.results.map(e => ({ file: e.file, lines: `${e.line}`, hash: e.hash })),
2141
1472
  };
2142
1473
  }
2143
-
2144
- return { result: "unknown", confidence: "low", evidence: [], nextSteps: ["No guard evidence found (may still exist; extractor is heuristic)"] };
1474
+
1475
+ return { result: 'unknown', confidence: 'low', evidence: [], nextSteps: ['Could not find auth guards for this route'] };
2145
1476
  }
2146
1477
 
2147
- async function verifyEntityExists(projectPath, subject, _claim) {
2148
- const name = String(subject?.name || "");
2149
- if (!name) return { result: "unknown", confidence: "low", evidence: [], nextSteps: ["Missing subject.name"] };
2150
-
2151
- const evidence = await searchEvidence(projectPath, { query: name, mode: "text", limit: 8 });
1478
+ async function verifyEntityExists(projectPath, subject, claim) {
1479
+ const name = subject.name;
1480
+ const evidence = await searchEvidence(projectPath, { query: name, limit: 5 });
1481
+
2152
1482
  if (evidence.count > 0) {
2153
1483
  return {
2154
- result: "true",
2155
- confidence: "med",
2156
- evidence: evidence.results.map((e) => ({ file: e.file, lines: `${e.line}`, hash: e.hash, snippet: e.snippet })),
1484
+ result: 'true',
1485
+ confidence: 'medium',
1486
+ evidence: evidence.results.map(e => ({ file: e.file, lines: `${e.line}`, hash: e.hash })),
2157
1487
  };
2158
1488
  }
1489
+
1490
+ return { result: 'unknown', confidence: 'low', evidence: [] };
1491
+ }
2159
1492
 
2160
- return { result: "unknown", confidence: "low", evidence: [] };
1493
+ function filterTruthPack(pack, scope) {
1494
+ if (scope === 'all') return pack;
1495
+ return {
1496
+ ...pack,
1497
+ sections: { [scope]: pack.sections[scope] },
1498
+ };
2161
1499
  }
2162
1500
 
2163
- // =============================================================================
2164
- // EXPORTS
2165
- // =============================================================================
1501
+ function detectDomains(task) {
1502
+ const domains = [];
1503
+ if (/auth|login|logout|session|password/i.test(task)) domains.push('auth');
1504
+ if (/billing|payment|stripe|subscription/i.test(task)) domains.push('billing');
1505
+ if (/route|api|endpoint/i.test(task)) domains.push('api');
1506
+ if (/component|button|ui|form/i.test(task)) domains.push('ui');
1507
+ return domains.length > 0 ? domains : ['general'];
1508
+ }
2166
1509
 
2167
- export {
2168
- getRouteIndex, // Route Truth v1 index - single source of truth for routes
2169
- extractRoutes, // Route extraction via Route Truth v1
2170
- };
1510
+ function extractKeywords(task) {
1511
+ return task.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(w => w.length > 2);
1512
+ }
1513
+
1514
+ function getInvariantsForDomains(domains) {
1515
+ const invariants = [];
1516
+ if (domains.includes('auth')) {
1517
+ invariants.push('No protected route without server middleware');
1518
+ }
1519
+ if (domains.includes('billing')) {
1520
+ invariants.push('No paid feature without server-side enforcement');
1521
+ }
1522
+ invariants.push('No success UI without confirmed success');
1523
+ return invariants;
1524
+ }
1525
+
1526
+ function estimateTokens(context) {
1527
+ let tokens = 0;
1528
+ if (context.relevantRoutes) tokens += context.relevantRoutes.length * 50;
1529
+ if (context.relevantAuth) tokens += 200;
1530
+ if (context.relevantBilling) tokens += 200;
1531
+ return tokens;
1532
+ }
1533
+
1534
+ function generateContextWarnings(domains, policy, routeCount) {
1535
+ const warnings = [];
1536
+ if (domains.includes('auth') || domains.includes('billing')) {
1537
+ warnings.push('High-stakes domain - verify all claims before changes');
1538
+ }
1539
+ if (routeCount > 20 && policy === 'strict') {
1540
+ warnings.push('Large context - consider narrowing scope');
1541
+ }
1542
+ return warnings;
1543
+ }
1544
+
1545
+ async function findSourceFiles(projectPath) {
1546
+ const files = [];
1547
+ const ignoreDirs = ['node_modules', 'dist', 'build', '.git', '.next', 'coverage'];
1548
+
1549
+ async function walk(dir) {
1550
+ try {
1551
+ const entries = await fs.readdir(dir, { withFileTypes: true });
1552
+ for (const entry of entries) {
1553
+ const fullPath = path.join(dir, entry.name);
1554
+ if (entry.isDirectory()) {
1555
+ if (!ignoreDirs.includes(entry.name) && !entry.name.startsWith('.')) {
1556
+ await walk(fullPath);
1557
+ }
1558
+ } else if (entry.isFile() && /\.(ts|tsx|js|jsx)$/.test(entry.name)) {
1559
+ files.push(fullPath);
1560
+ }
1561
+ }
1562
+ } catch {}
1563
+ }
1564
+
1565
+ await walk(projectPath);
1566
+ return files;
1567
+ }
2171
1568
 
2172
1569
  export default {
2173
1570
  TRUTH_FIREWALL_TOOLS,
2174
1571
  handleTruthFirewallTool,
2175
- hasRecentClaimValidation,
2176
- enforceClaimResult,
2177
- getPolicyConfig,
2178
- getProjectFingerprint,
2179
- wrapMcpResponse,
2180
- getContextAttribution,
2181
- getRouteIndex, // Also on default export
2182
- CONTEXT_ATTRIBUTION,
2183
1572
  };