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