@vibecheckai/cli 3.7.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -63
- package/bin/_deprecations.js +447 -19
- package/bin/_router.js +1 -1
- package/bin/registry.js +347 -280
- package/bin/runners/context/generators/cursor-enhanced.js +2439 -0
- package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
- package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
- package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
- package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
- package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
- package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
- package/bin/runners/lib/agent-firewall/index.js +200 -0
- package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
- package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
- package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +634 -0
- package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
- package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
- package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
- package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
- package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
- package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
- package/bin/runners/lib/agent-firewall/interceptor/base.js +7 -3
- package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
- package/bin/runners/lib/agent-firewall/session/index.js +26 -0
- package/bin/runners/lib/artifact-envelope.js +540 -0
- package/bin/runners/lib/auth-shared.js +977 -0
- package/bin/runners/lib/checkpoint.js +941 -0
- package/bin/runners/lib/cleanup/engine.js +571 -0
- package/bin/runners/lib/cleanup/index.js +53 -0
- package/bin/runners/lib/cleanup/output.js +375 -0
- package/bin/runners/lib/cleanup/rules.js +1060 -0
- package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
- package/bin/runners/lib/doctor/failure-signatures.js +526 -0
- package/bin/runners/lib/doctor/fix-script.js +336 -0
- package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
- package/bin/runners/lib/doctor/modules/index.js +62 -3
- package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
- package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
- package/bin/runners/lib/doctor/safe-repair.js +384 -0
- package/bin/runners/lib/engine/ast-cache.js +210 -210
- package/bin/runners/lib/engine/auth-extractor.js +211 -211
- package/bin/runners/lib/engine/billing-extractor.js +112 -112
- package/bin/runners/lib/engine/enforcement-extractor.js +100 -100
- package/bin/runners/lib/engine/env-extractor.js +207 -207
- package/bin/runners/lib/engine/express-extractor.js +208 -208
- package/bin/runners/lib/engine/extractors.js +849 -849
- package/bin/runners/lib/engine/index.js +207 -207
- package/bin/runners/lib/engine/repo-index.js +514 -514
- package/bin/runners/lib/engine/types.js +124 -124
- package/bin/runners/lib/engines/attack-detector.js +1192 -0
- package/bin/runners/lib/entitlements-v2.js +2 -2
- package/bin/runners/lib/missions/briefing.js +427 -0
- package/bin/runners/lib/missions/checkpoint.js +753 -0
- package/bin/runners/lib/missions/hardening.js +851 -0
- package/bin/runners/lib/missions/plan.js +421 -32
- package/bin/runners/lib/missions/safety-gates.js +645 -0
- package/bin/runners/lib/missions/schema.js +478 -0
- package/bin/runners/lib/packs/bundle.js +675 -0
- package/bin/runners/lib/packs/evidence-pack.js +671 -0
- package/bin/runners/lib/packs/pack-factory.js +837 -0
- package/bin/runners/lib/packs/permissions-pack.js +686 -0
- package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
- package/bin/runners/lib/safelist/index.js +96 -0
- package/bin/runners/lib/safelist/integration.js +334 -0
- package/bin/runners/lib/safelist/matcher.js +696 -0
- package/bin/runners/lib/safelist/schema.js +948 -0
- package/bin/runners/lib/safelist/store.js +438 -0
- package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
- package/bin/runners/lib/ship-gate.js +832 -0
- package/bin/runners/lib/ship-manifest.js +1153 -0
- package/bin/runners/lib/ship-output.js +1 -1
- package/bin/runners/lib/unified-cli-output.js +710 -383
- package/bin/runners/lib/upsell.js +3 -3
- package/bin/runners/lib/why-tree.js +650 -0
- package/bin/runners/runAllowlist.js +33 -4
- package/bin/runners/runApprove.js +240 -1122
- package/bin/runners/runAudit.js +692 -0
- package/bin/runners/runAuth.js +325 -29
- package/bin/runners/runCheckpoint.js +442 -494
- package/bin/runners/runCleanup.js +343 -0
- package/bin/runners/runDoctor.js +269 -19
- package/bin/runners/runFix.js +411 -32
- package/bin/runners/runForge.js +411 -0
- package/bin/runners/runIntent.js +906 -0
- package/bin/runners/runKickoff.js +878 -0
- package/bin/runners/runLaunch.js +2000 -0
- package/bin/runners/runLink.js +785 -0
- package/bin/runners/runMcp.js +1741 -837
- package/bin/runners/runPacks.js +2089 -0
- package/bin/runners/runPolish.js +41 -0
- package/bin/runners/runReality.js +178 -1
- package/bin/runners/runSafelist.js +1190 -0
- package/bin/runners/runScan.js +21 -9
- package/bin/runners/runShield.js +1282 -0
- package/bin/runners/runShip.js +395 -16
- package/bin/vibecheck.js +34 -6
- package/mcp-server/README.md +117 -158
- package/mcp-server/handlers/index.ts +2 -2
- package/mcp-server/handlers/tool-handler.ts +50 -11
- package/mcp-server/index.js +16 -0
- package/mcp-server/intent-firewall-interceptor.js +529 -0
- package/mcp-server/lib/executor.ts +5 -5
- package/mcp-server/lib/index.ts +14 -4
- package/mcp-server/lib/sandbox.test.ts +4 -4
- package/mcp-server/lib/sandbox.ts +2 -2
- package/mcp-server/manifest.json +473 -0
- package/mcp-server/package.json +1 -1
- package/mcp-server/registry/tool-registry.js +315 -523
- package/mcp-server/registry/tools.json +442 -428
- package/mcp-server/registry.test.ts +18 -12
- package/mcp-server/tier-auth.js +68 -11
- package/mcp-server/tools-v3.js +70 -16
- package/mcp-server/tsconfig.json +1 -0
- package/package.json +2 -1
- package/bin/runners/runProof.zip +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Upsell Copy Module - Central copy generator for tier upgrades
|
|
3
3
|
*
|
|
4
|
-
* Simple 2-tier model: FREE and PRO ($
|
|
4
|
+
* Simple 2-tier model: FREE and PRO ($49/mo)
|
|
5
5
|
*
|
|
6
6
|
* Rules:
|
|
7
7
|
* - Blunt, confident, minimal tone
|
|
@@ -48,7 +48,7 @@ const sym = {
|
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
51
|
-
// SIMPLE 2-TIER CONFIG: FREE and PRO ($
|
|
51
|
+
// SIMPLE 2-TIER CONFIG: FREE and PRO ($49/mo)
|
|
52
52
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
53
53
|
const TIER_LABELS = {
|
|
54
54
|
free: "FREE",
|
|
@@ -61,7 +61,7 @@ const TIER_COLORS = {
|
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
const PRICING_URL = "https://vibecheckai.dev/pricing";
|
|
64
|
-
const PRO_PRICE = "$
|
|
64
|
+
const PRO_PRICE = "$49/mo";
|
|
65
65
|
|
|
66
66
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
67
67
|
// DENIAL COPY - Command-specific reasons and alternatives
|
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why Tree - Evidence Chain Builder
|
|
3
|
+
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
* CLEAR "WHY" FOR EVERY VERDICT
|
|
6
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
7
|
+
*
|
|
8
|
+
* Builds a human-readable evidence chain for verdicts:
|
|
9
|
+
* - Top 3 blockers with file:line evidence
|
|
10
|
+
* - Fix missions (actionable next steps)
|
|
11
|
+
* - Proof chain (how we know this is true)
|
|
12
|
+
*
|
|
13
|
+
* @module why-tree
|
|
14
|
+
* @version 1.1.0 - Hardened
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
"use strict";
|
|
18
|
+
|
|
19
|
+
const path = require("path");
|
|
20
|
+
const crypto = require("crypto");
|
|
21
|
+
|
|
22
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
23
|
+
// CONSTANTS
|
|
24
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
25
|
+
|
|
26
|
+
const MAX_STRING_LENGTH = 500;
|
|
27
|
+
const MAX_BLOCKERS = 10;
|
|
28
|
+
const MAX_WARNINGS = 20;
|
|
29
|
+
const MAX_FIX_MISSIONS = 10;
|
|
30
|
+
const MAX_EVIDENCE_ITEMS = 5;
|
|
31
|
+
|
|
32
|
+
// Characters to strip from output (prevent terminal injection)
|
|
33
|
+
const UNSAFE_CHARS = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\u001B]/g;
|
|
34
|
+
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
36
|
+
// INPUT SANITIZATION
|
|
37
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Sanitize a string for safe output (prevent injection)
|
|
41
|
+
* @param {*} str - Input to sanitize
|
|
42
|
+
* @param {number} maxLen - Maximum length
|
|
43
|
+
* @returns {string|null} Sanitized string or null
|
|
44
|
+
*/
|
|
45
|
+
function sanitize(str, maxLen = MAX_STRING_LENGTH) {
|
|
46
|
+
if (str === null || str === undefined) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const s = String(str)
|
|
51
|
+
.replace(UNSAFE_CHARS, "") // Remove unsafe characters
|
|
52
|
+
.trim();
|
|
53
|
+
|
|
54
|
+
if (!s) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return s.length > maxLen ? s.slice(0, maxLen - 3) + "..." : s;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate and sanitize a finding object
|
|
63
|
+
* @param {*} finding - Finding to validate
|
|
64
|
+
* @returns {Object|null} Validated finding or null
|
|
65
|
+
*/
|
|
66
|
+
function validateFinding(finding) {
|
|
67
|
+
if (!finding || typeof finding !== "object") {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Must have at least a title or message
|
|
72
|
+
const title = sanitize(finding.title || finding.message);
|
|
73
|
+
if (!title) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
id: sanitize(finding.id || finding.fingerprint, 64),
|
|
79
|
+
category: sanitize(finding.category, 50) || "Unknown",
|
|
80
|
+
severity: validateSeverity(finding.severity),
|
|
81
|
+
title,
|
|
82
|
+
why: sanitize(finding.why || finding.explanation),
|
|
83
|
+
confidence: validateConfidence(finding.confidence),
|
|
84
|
+
evidence: validateEvidence(finding.evidence),
|
|
85
|
+
fixHints: validateFixHints(finding.fixHints),
|
|
86
|
+
fingerprint: sanitize(finding.fingerprint, 64),
|
|
87
|
+
detectorId: sanitize(finding.detectorId, 50),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validate severity
|
|
93
|
+
*/
|
|
94
|
+
function validateSeverity(sev) {
|
|
95
|
+
const valid = ["BLOCK", "WARN", "INFO"];
|
|
96
|
+
const s = String(sev || "").toUpperCase();
|
|
97
|
+
return valid.includes(s) ? s : "INFO";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validate confidence
|
|
102
|
+
*/
|
|
103
|
+
function validateConfidence(conf) {
|
|
104
|
+
const valid = ["high", "medium", "low"];
|
|
105
|
+
const c = String(conf || "").toLowerCase();
|
|
106
|
+
return valid.includes(c) ? c : "medium";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Validate evidence array
|
|
111
|
+
*/
|
|
112
|
+
function validateEvidence(evidence) {
|
|
113
|
+
if (!Array.isArray(evidence)) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return evidence
|
|
118
|
+
.slice(0, MAX_EVIDENCE_ITEMS)
|
|
119
|
+
.map(e => {
|
|
120
|
+
if (!e || typeof e !== "object") return null;
|
|
121
|
+
return {
|
|
122
|
+
file: sanitize(e.file || e.path, 300),
|
|
123
|
+
lines: sanitize(e.lines, 20),
|
|
124
|
+
line: typeof e.line === "number" ? Math.max(0, Math.floor(e.line)) : null,
|
|
125
|
+
snippet: sanitize(e.snippet, 200),
|
|
126
|
+
kind: sanitize(e.kind, 20) || "file",
|
|
127
|
+
};
|
|
128
|
+
})
|
|
129
|
+
.filter(Boolean);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Validate fix hints
|
|
134
|
+
*/
|
|
135
|
+
function validateFixHints(hints) {
|
|
136
|
+
if (!Array.isArray(hints)) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
return hints
|
|
140
|
+
.slice(0, 5)
|
|
141
|
+
.map(h => sanitize(h, MAX_STRING_LENGTH))
|
|
142
|
+
.filter(Boolean);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
146
|
+
// WHY TREE BUILDER
|
|
147
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Build a comprehensive why tree from findings.
|
|
151
|
+
*
|
|
152
|
+
* The why tree answers: "Why did we get this verdict?"
|
|
153
|
+
*
|
|
154
|
+
* @param {Array} findings - All findings
|
|
155
|
+
* @param {Object} options - Options
|
|
156
|
+
* @returns {Object} Why tree structure (always returns valid object)
|
|
157
|
+
*/
|
|
158
|
+
function buildWhyTree(findings = [], options = {}) {
|
|
159
|
+
// Validate options
|
|
160
|
+
const opts = options || {};
|
|
161
|
+
const maxBlockers = Math.min(Math.max(1, Number(opts.maxBlockers) || 3), MAX_BLOCKERS);
|
|
162
|
+
const maxWarnings = Math.min(Math.max(1, Number(opts.maxWarnings) || 5), MAX_WARNINGS);
|
|
163
|
+
const maxFixMissions = Math.min(Math.max(1, Number(opts.maxFixMissions) || 5), MAX_FIX_MISSIONS);
|
|
164
|
+
const includeEvidence = opts.includeEvidence !== false;
|
|
165
|
+
const includeProofChain = opts.includeProofChain !== false;
|
|
166
|
+
|
|
167
|
+
// Validate and filter findings
|
|
168
|
+
const validFindings = (Array.isArray(findings) ? findings : [])
|
|
169
|
+
.map(validateFinding)
|
|
170
|
+
.filter(Boolean);
|
|
171
|
+
|
|
172
|
+
// Separate by severity
|
|
173
|
+
const blockers = validFindings
|
|
174
|
+
.filter(f => f.severity === "BLOCK")
|
|
175
|
+
.sort((a, b) => getConfidenceScore(b) - getConfidenceScore(a));
|
|
176
|
+
|
|
177
|
+
const warnings = validFindings
|
|
178
|
+
.filter(f => f.severity === "WARN")
|
|
179
|
+
.sort((a, b) => getConfidenceScore(b) - getConfidenceScore(a));
|
|
180
|
+
|
|
181
|
+
const info = validFindings.filter(f => f.severity === "INFO");
|
|
182
|
+
|
|
183
|
+
// Build tree structure with safe defaults
|
|
184
|
+
const tree = {
|
|
185
|
+
verdict: blockers.length > 0 ? "BLOCK" : warnings.length > 0 ? "WARN" : "SHIP",
|
|
186
|
+
summary: buildSummary(blockers, warnings),
|
|
187
|
+
|
|
188
|
+
// Top blockers with full evidence
|
|
189
|
+
topBlockers: blockers.slice(0, maxBlockers).map((b, i) => ({
|
|
190
|
+
rank: i + 1,
|
|
191
|
+
...buildIssueNode(b, includeEvidence, includeProofChain),
|
|
192
|
+
})),
|
|
193
|
+
|
|
194
|
+
// Top warnings (if no blockers)
|
|
195
|
+
topWarnings: blockers.length === 0
|
|
196
|
+
? warnings.slice(0, maxWarnings).map((w, i) => ({
|
|
197
|
+
rank: i + 1,
|
|
198
|
+
...buildIssueNode(w, includeEvidence, false),
|
|
199
|
+
}))
|
|
200
|
+
: [],
|
|
201
|
+
|
|
202
|
+
// Fix missions (actionable next steps)
|
|
203
|
+
fixMissions: buildFixMissions(blockers, warnings, maxFixMissions),
|
|
204
|
+
|
|
205
|
+
// Stats
|
|
206
|
+
stats: {
|
|
207
|
+
totalFindings: validFindings.length,
|
|
208
|
+
blockers: blockers.length,
|
|
209
|
+
warnings: warnings.length,
|
|
210
|
+
info: info.length,
|
|
211
|
+
byCategory: groupByCategory(validFindings),
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
return tree;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Build summary text based on findings
|
|
220
|
+
*/
|
|
221
|
+
function buildSummary(blockers, warnings) {
|
|
222
|
+
if (blockers.length === 0 && warnings.length === 0) {
|
|
223
|
+
return "All checks passed. Ready to ship.";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const parts = [];
|
|
227
|
+
|
|
228
|
+
if (blockers.length > 0) {
|
|
229
|
+
parts.push(`${blockers.length} blocker${blockers.length !== 1 ? "s" : ""} must be fixed`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (warnings.length > 0) {
|
|
233
|
+
parts.push(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""} to review`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return parts.join(", ") + ".";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Build an issue node with evidence
|
|
241
|
+
*/
|
|
242
|
+
function buildIssueNode(finding, includeEvidence, includeProofChain) {
|
|
243
|
+
const node = {
|
|
244
|
+
id: finding.fingerprint || finding.id || sha256Short(finding.title || ""),
|
|
245
|
+
category: finding.category,
|
|
246
|
+
severity: finding.severity,
|
|
247
|
+
title: finding.title || finding.message,
|
|
248
|
+
why: finding.why || finding.explanation || null,
|
|
249
|
+
confidence: finding.confidence || "medium",
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Add evidence
|
|
253
|
+
if (includeEvidence && finding.evidence?.length > 0) {
|
|
254
|
+
node.evidence = finding.evidence.slice(0, 3).map(formatEvidence);
|
|
255
|
+
node.primaryLocation = formatPrimaryLocation(finding);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Add fix hints
|
|
259
|
+
if (finding.fixHints?.length > 0) {
|
|
260
|
+
node.fixHint = finding.fixHints[0];
|
|
261
|
+
node.allFixHints = finding.fixHints;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Add proof chain (how we know this)
|
|
265
|
+
if (includeProofChain) {
|
|
266
|
+
node.proofChain = buildProofChain(finding);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return node;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Format evidence for display
|
|
274
|
+
*/
|
|
275
|
+
function formatEvidence(ev) {
|
|
276
|
+
return {
|
|
277
|
+
file: ev.file || ev.path || null,
|
|
278
|
+
lines: ev.lines || (ev.line ? String(ev.line) : null),
|
|
279
|
+
snippet: ev.snippet ? truncate(ev.snippet, 100) : null,
|
|
280
|
+
kind: ev.kind || "file",
|
|
281
|
+
snippetHash: ev.snippetHash || null,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Format primary location as a clickable reference
|
|
287
|
+
*/
|
|
288
|
+
function formatPrimaryLocation(finding) {
|
|
289
|
+
const ev = finding.evidence?.[0];
|
|
290
|
+
if (!ev) return null;
|
|
291
|
+
|
|
292
|
+
const file = ev.file || ev.path;
|
|
293
|
+
if (!file) return null;
|
|
294
|
+
|
|
295
|
+
const line = ev.lines?.split("-")[0] || ev.line;
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
file,
|
|
299
|
+
line: line ? parseInt(line) : null,
|
|
300
|
+
display: line ? `${file}:${line}` : file,
|
|
301
|
+
vscodeUri: line ? `vscode://file/${file}:${line}` : `vscode://file/${file}`,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Build proof chain (how we know this finding is true)
|
|
307
|
+
*/
|
|
308
|
+
function buildProofChain(finding) {
|
|
309
|
+
const chain = [];
|
|
310
|
+
|
|
311
|
+
// Add detector info
|
|
312
|
+
if (finding.detectorId || finding.detector) {
|
|
313
|
+
chain.push({
|
|
314
|
+
step: "detected",
|
|
315
|
+
by: finding.detectorId || finding.detector,
|
|
316
|
+
type: finding.detectorType || "static",
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Add evidence verification
|
|
321
|
+
if (finding.evidence?.length > 0) {
|
|
322
|
+
const ev = finding.evidence[0];
|
|
323
|
+
chain.push({
|
|
324
|
+
step: "verified",
|
|
325
|
+
method: ev.kind === "runtime" ? "runtime" : "file-citation",
|
|
326
|
+
confidence: finding.confidence || "medium",
|
|
327
|
+
at: ev.file ? `${ev.file}:${ev.lines || ev.line || ""}` : "unknown",
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Add cross-reference if available
|
|
332
|
+
if (finding.crossRef) {
|
|
333
|
+
chain.push({
|
|
334
|
+
step: "cross-referenced",
|
|
335
|
+
with: finding.crossRef.type,
|
|
336
|
+
matches: finding.crossRef.matches,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return chain.length > 0 ? chain : null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Build fix missions (prioritized action items)
|
|
345
|
+
*/
|
|
346
|
+
function buildFixMissions(blockers, warnings, max) {
|
|
347
|
+
const missions = [];
|
|
348
|
+
|
|
349
|
+
// Priority 1: Blockers
|
|
350
|
+
for (const b of blockers.slice(0, max)) {
|
|
351
|
+
missions.push({
|
|
352
|
+
priority: "critical",
|
|
353
|
+
category: b.category,
|
|
354
|
+
target: b.evidence?.[0]?.file || "unknown",
|
|
355
|
+
action: b.fixHints?.[0] || `Fix ${b.category} issue: ${truncate(b.title, 50)}`,
|
|
356
|
+
findingId: b.fingerprint || b.id,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Priority 2: Warnings (if room)
|
|
361
|
+
const remaining = max - missions.length;
|
|
362
|
+
if (remaining > 0) {
|
|
363
|
+
for (const w of warnings.slice(0, remaining)) {
|
|
364
|
+
missions.push({
|
|
365
|
+
priority: "recommended",
|
|
366
|
+
category: w.category,
|
|
367
|
+
target: w.evidence?.[0]?.file || "unknown",
|
|
368
|
+
action: w.fixHints?.[0] || `Address ${w.category} warning: ${truncate(w.title, 50)}`,
|
|
369
|
+
findingId: w.fingerprint || w.id,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return missions;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Group findings by category
|
|
379
|
+
*/
|
|
380
|
+
function groupByCategory(findings) {
|
|
381
|
+
const groups = {};
|
|
382
|
+
|
|
383
|
+
for (const f of findings) {
|
|
384
|
+
const cat = f.category || "Other";
|
|
385
|
+
if (!groups[cat]) {
|
|
386
|
+
groups[cat] = { total: 0, blockers: 0, warnings: 0 };
|
|
387
|
+
}
|
|
388
|
+
groups[cat].total++;
|
|
389
|
+
if (f.severity === "BLOCK") groups[cat].blockers++;
|
|
390
|
+
if (f.severity === "WARN") groups[cat].warnings++;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return groups;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
397
|
+
// RENDERING (Human-readable output)
|
|
398
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Render why tree as terminal output (plain text with ANSI colors)
|
|
402
|
+
*/
|
|
403
|
+
function renderWhyTreeTerminal(tree, options = {}) {
|
|
404
|
+
const { colorize = true, width = 72 } = options;
|
|
405
|
+
const lines = [];
|
|
406
|
+
|
|
407
|
+
// ANSI helpers
|
|
408
|
+
const c = colorize ? {
|
|
409
|
+
reset: "\x1b[0m",
|
|
410
|
+
bold: "\x1b[1m",
|
|
411
|
+
dim: "\x1b[2m",
|
|
412
|
+
red: "\x1b[38;2;255;100;100m",
|
|
413
|
+
yellow: "\x1b[38;2;255;200;100m",
|
|
414
|
+
green: "\x1b[38;2;100;255;150m",
|
|
415
|
+
cyan: "\x1b[38;2;100;200;255m",
|
|
416
|
+
gray: "\x1b[38;2;150;150;150m",
|
|
417
|
+
} : { reset: "", bold: "", dim: "", red: "", yellow: "", green: "", cyan: "", gray: "" };
|
|
418
|
+
|
|
419
|
+
// Header
|
|
420
|
+
const verdictColor = tree.verdict === "SHIP" ? c.green : tree.verdict === "WARN" ? c.yellow : c.red;
|
|
421
|
+
lines.push("");
|
|
422
|
+
lines.push(` ${c.bold}WHY: ${verdictColor}${tree.verdict}${c.reset}`);
|
|
423
|
+
lines.push(` ${c.dim}${tree.summary}${c.reset}`);
|
|
424
|
+
lines.push("");
|
|
425
|
+
|
|
426
|
+
// Top blockers
|
|
427
|
+
if (tree.topBlockers.length > 0) {
|
|
428
|
+
lines.push(` ${c.bold}Top Blockers:${c.reset}`);
|
|
429
|
+
lines.push("");
|
|
430
|
+
|
|
431
|
+
for (const blocker of tree.topBlockers) {
|
|
432
|
+
lines.push(` ${c.red}${blocker.rank}.${c.reset} ${c.bold}${blocker.title}${c.reset}`);
|
|
433
|
+
|
|
434
|
+
if (blocker.why) {
|
|
435
|
+
lines.push(` ${c.dim}Why: ${blocker.why}${c.reset}`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (blocker.primaryLocation) {
|
|
439
|
+
lines.push(` ${c.cyan}→ ${blocker.primaryLocation.display}${c.reset}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (blocker.fixHint) {
|
|
443
|
+
lines.push(` ${c.green}Fix: ${blocker.fixHint}${c.reset}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
lines.push("");
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Top warnings (if no blockers)
|
|
451
|
+
if (tree.topWarnings.length > 0) {
|
|
452
|
+
lines.push(` ${c.bold}Warnings:${c.reset}`);
|
|
453
|
+
lines.push("");
|
|
454
|
+
|
|
455
|
+
for (const warning of tree.topWarnings.slice(0, 3)) {
|
|
456
|
+
lines.push(` ${c.yellow}${warning.rank}.${c.reset} ${warning.title}`);
|
|
457
|
+
|
|
458
|
+
if (warning.primaryLocation) {
|
|
459
|
+
lines.push(` ${c.cyan}→ ${warning.primaryLocation.display}${c.reset}`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
lines.push("");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Fix missions
|
|
467
|
+
if (tree.fixMissions.length > 0) {
|
|
468
|
+
lines.push(` ${c.bold}Fix Missions:${c.reset}`);
|
|
469
|
+
lines.push("");
|
|
470
|
+
|
|
471
|
+
for (const mission of tree.fixMissions.slice(0, 3)) {
|
|
472
|
+
const priorityColor = mission.priority === "critical" ? c.red : c.yellow;
|
|
473
|
+
lines.push(` ${priorityColor}[${mission.priority.toUpperCase()}]${c.reset} ${mission.action}`);
|
|
474
|
+
lines.push(` ${c.dim}Target: ${mission.target}${c.reset}`);
|
|
475
|
+
lines.push("");
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Stats summary
|
|
480
|
+
lines.push(` ${c.dim}${"─".repeat(width - 4)}${c.reset}`);
|
|
481
|
+
lines.push(` ${c.dim}Stats: ${tree.stats.blockers} blockers, ${tree.stats.warnings} warnings, ${tree.stats.info} info${c.reset}`);
|
|
482
|
+
lines.push("");
|
|
483
|
+
|
|
484
|
+
return lines.join("\n");
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Render why tree as Markdown (for PR comments, reports)
|
|
489
|
+
*/
|
|
490
|
+
function renderWhyTreeMarkdown(tree) {
|
|
491
|
+
const lines = [];
|
|
492
|
+
|
|
493
|
+
// Verdict header
|
|
494
|
+
const emoji = tree.verdict === "SHIP" ? "✅" : tree.verdict === "WARN" ? "⚠️" : "🚫";
|
|
495
|
+
lines.push(`## ${emoji} Verdict: ${tree.verdict}`);
|
|
496
|
+
lines.push("");
|
|
497
|
+
lines.push(tree.summary);
|
|
498
|
+
lines.push("");
|
|
499
|
+
|
|
500
|
+
// Top blockers
|
|
501
|
+
if (tree.topBlockers.length > 0) {
|
|
502
|
+
lines.push("### Top Blockers");
|
|
503
|
+
lines.push("");
|
|
504
|
+
|
|
505
|
+
for (const blocker of tree.topBlockers) {
|
|
506
|
+
lines.push(`**${blocker.rank}. ${blocker.title}**`);
|
|
507
|
+
|
|
508
|
+
if (blocker.why) {
|
|
509
|
+
lines.push(`> ${blocker.why}`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (blocker.primaryLocation) {
|
|
513
|
+
lines.push(`- 📁 \`${blocker.primaryLocation.display}\``);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (blocker.fixHint) {
|
|
517
|
+
lines.push(`- 🔧 **Fix:** ${blocker.fixHint}`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Proof chain
|
|
521
|
+
if (blocker.proofChain?.length > 0) {
|
|
522
|
+
const chainStr = blocker.proofChain.map(c => c.by || c.method || c.with).join(" → ");
|
|
523
|
+
lines.push(`- 📋 Proof: ${chainStr}`);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
lines.push("");
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Top warnings
|
|
531
|
+
if (tree.topWarnings.length > 0) {
|
|
532
|
+
lines.push("### Warnings");
|
|
533
|
+
lines.push("");
|
|
534
|
+
|
|
535
|
+
for (const warning of tree.topWarnings.slice(0, 5)) {
|
|
536
|
+
lines.push(`- ⚠️ **${warning.title}**`);
|
|
537
|
+
if (warning.primaryLocation) {
|
|
538
|
+
lines.push(` - \`${warning.primaryLocation.display}\``);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
lines.push("");
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Fix missions
|
|
545
|
+
if (tree.fixMissions.length > 0) {
|
|
546
|
+
lines.push("### Fix Missions");
|
|
547
|
+
lines.push("");
|
|
548
|
+
|
|
549
|
+
for (const mission of tree.fixMissions.slice(0, 5)) {
|
|
550
|
+
const emoji = mission.priority === "critical" ? "🔴" : "🟡";
|
|
551
|
+
lines.push(`${emoji} **${mission.action}**`);
|
|
552
|
+
lines.push(` - Target: \`${mission.target}\``);
|
|
553
|
+
}
|
|
554
|
+
lines.push("");
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Stats
|
|
558
|
+
lines.push("---");
|
|
559
|
+
lines.push(`📊 **Stats:** ${tree.stats.blockers} blockers, ${tree.stats.warnings} warnings, ${tree.stats.totalFindings} total findings`);
|
|
560
|
+
|
|
561
|
+
return lines.join("\n");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Render why tree as JSON (for API/programmatic use)
|
|
566
|
+
*/
|
|
567
|
+
function renderWhyTreeJSON(tree) {
|
|
568
|
+
return JSON.stringify(tree, null, 2);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
572
|
+
// HELPERS
|
|
573
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
574
|
+
|
|
575
|
+
function getConfidenceScore(finding) {
|
|
576
|
+
if (!finding) return 0;
|
|
577
|
+
const scores = { high: 3, medium: 2, low: 1 };
|
|
578
|
+
return scores[finding.confidence] || 2;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function truncate(str, len) {
|
|
582
|
+
if (!str) return "";
|
|
583
|
+
const s = String(str);
|
|
584
|
+
if (s.length <= len) return s;
|
|
585
|
+
return s.slice(0, Math.max(0, len - 3)) + "...";
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function sha256Short(input) {
|
|
589
|
+
try {
|
|
590
|
+
return crypto.createHash("sha256").update(String(input || "")).digest("hex").slice(0, 8);
|
|
591
|
+
} catch {
|
|
592
|
+
return "00000000";
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Escape special characters for safe terminal output
|
|
598
|
+
*/
|
|
599
|
+
function escapeTerminal(str) {
|
|
600
|
+
if (!str) return "";
|
|
601
|
+
return String(str).replace(UNSAFE_CHARS, "");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Escape special characters for safe markdown output
|
|
606
|
+
*/
|
|
607
|
+
function escapeMarkdown(str) {
|
|
608
|
+
if (!str) return "";
|
|
609
|
+
return String(str)
|
|
610
|
+
.replace(UNSAFE_CHARS, "")
|
|
611
|
+
.replace(/[<>&]/g, c => ({
|
|
612
|
+
"<": "<",
|
|
613
|
+
">": ">",
|
|
614
|
+
"&": "&",
|
|
615
|
+
}[c]));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
619
|
+
// EXPORTS
|
|
620
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
621
|
+
|
|
622
|
+
module.exports = {
|
|
623
|
+
// Main builder
|
|
624
|
+
buildWhyTree,
|
|
625
|
+
|
|
626
|
+
// Renderers
|
|
627
|
+
renderWhyTreeTerminal,
|
|
628
|
+
renderWhyTreeMarkdown,
|
|
629
|
+
renderWhyTreeJSON,
|
|
630
|
+
|
|
631
|
+
// Individual builders (for custom use)
|
|
632
|
+
buildIssueNode,
|
|
633
|
+
buildFixMissions,
|
|
634
|
+
buildProofChain,
|
|
635
|
+
formatPrimaryLocation,
|
|
636
|
+
|
|
637
|
+
// Validation (for external use)
|
|
638
|
+
validateFinding,
|
|
639
|
+
sanitize,
|
|
640
|
+
|
|
641
|
+
// Utilities
|
|
642
|
+
escapeTerminal,
|
|
643
|
+
escapeMarkdown,
|
|
644
|
+
|
|
645
|
+
// Constants
|
|
646
|
+
MAX_STRING_LENGTH,
|
|
647
|
+
MAX_BLOCKERS,
|
|
648
|
+
MAX_WARNINGS,
|
|
649
|
+
MAX_FIX_MISSIONS,
|
|
650
|
+
};
|