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