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