@vibecheckai/cli 3.1.8 → 3.2.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/bin/registry.js +106 -116
- package/bin/runners/context/generators/mcp.js +18 -0
- package/bin/runners/context/index.js +72 -4
- package/bin/runners/context/proof-context.js +293 -1
- package/bin/runners/context/security-scanner.js +311 -73
- package/bin/runners/lib/analyzers.js +607 -20
- package/bin/runners/lib/detectors-v2.js +172 -15
- package/bin/runners/lib/entitlements-v2.js +48 -1
- package/bin/runners/lib/evidence-pack.js +678 -0
- package/bin/runners/lib/html-proof-report.js +913 -0
- package/bin/runners/lib/missions/plan.js +231 -41
- package/bin/runners/lib/missions/templates.js +125 -0
- package/bin/runners/lib/scan-output.js +492 -253
- package/bin/runners/lib/ship-output.js +901 -641
- package/bin/runners/runCheckpoint.js +44 -3
- package/bin/runners/runContext.d.ts +4 -0
- package/bin/runners/runDoctor.js +10 -2
- package/bin/runners/runFix.js +51 -341
- package/bin/runners/runInit.js +11 -0
- package/bin/runners/runPolish.d.ts +4 -0
- package/bin/runners/runPolish.js +608 -29
- package/bin/runners/runProve.js +210 -25
- package/bin/runners/runReality.js +846 -101
- package/bin/runners/runScan.js +238 -4
- package/bin/runners/runShip.js +19 -3
- package/bin/runners/runWatch.js +14 -1
- package/bin/vibecheck.js +32 -2
- package/mcp-server/consolidated-tools.js +408 -42
- package/mcp-server/index.js +152 -15
- package/mcp-server/proof-tools.js +571 -0
- package/mcp-server/tier-auth.js +22 -19
- package/mcp-server/tools-v3.js +744 -0
- package/mcp-server/truth-firewall-tools.js +190 -4
- package/package.json +3 -1
- package/bin/runners/runInstall.js +0 -281
- package/bin/runners/runLabs.js +0 -341
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence Pack Builder
|
|
3
|
+
*
|
|
4
|
+
* Creates shareable "evidence packs" from vibecheck proof runs.
|
|
5
|
+
* Bundles videos, traces, screenshots, and findings into a single artifact.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { buildEvidencePack } = require('./lib/evidence-pack');
|
|
9
|
+
* const pack = await buildEvidencePack(projectRoot, { includeVideos: true });
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const crypto = require("crypto");
|
|
17
|
+
const archiver = require("archiver");
|
|
18
|
+
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
20
|
+
// EVIDENCE PACK SCHEMA
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Evidence Pack Structure:
|
|
25
|
+
* {
|
|
26
|
+
* meta: { ... },
|
|
27
|
+
* summary: { ... },
|
|
28
|
+
* findings: [...],
|
|
29
|
+
* allowlist: { ... },
|
|
30
|
+
* artifacts: { ... }
|
|
31
|
+
* }
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const EVIDENCE_PACK_VERSION = "1.0.0";
|
|
35
|
+
|
|
36
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
37
|
+
// ALLOWLIST SCHEMA
|
|
38
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Allowlist Entry Structure:
|
|
42
|
+
* {
|
|
43
|
+
* id: string,
|
|
44
|
+
* findingId: string,
|
|
45
|
+
* pattern: string | RegExp,
|
|
46
|
+
* reason: string, // Why this is allowed
|
|
47
|
+
* addedAt: ISO timestamp,
|
|
48
|
+
* addedBy: string, // user/ci/auto
|
|
49
|
+
* expiresAt?: ISO timestamp,
|
|
50
|
+
* scope: 'global' | 'file' | 'line',
|
|
51
|
+
* file?: string,
|
|
52
|
+
* lines?: [start, end]
|
|
53
|
+
* }
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
const DEFAULT_ALLOWLIST_PATH = ".vibecheck/allowlist.json";
|
|
57
|
+
|
|
58
|
+
function loadAllowlist(projectRoot) {
|
|
59
|
+
const allowlistPath = path.join(projectRoot, DEFAULT_ALLOWLIST_PATH);
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(fs.readFileSync(allowlistPath, "utf8"));
|
|
62
|
+
} catch {
|
|
63
|
+
return { version: "1.0.0", entries: [], lastUpdated: null };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function saveAllowlist(projectRoot, allowlist) {
|
|
68
|
+
const allowlistPath = path.join(projectRoot, DEFAULT_ALLOWLIST_PATH);
|
|
69
|
+
fs.mkdirSync(path.dirname(allowlistPath), { recursive: true });
|
|
70
|
+
allowlist.lastUpdated = new Date().toISOString();
|
|
71
|
+
fs.writeFileSync(allowlistPath, JSON.stringify(allowlist, null, 2), "utf8");
|
|
72
|
+
return allowlistPath;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function addToAllowlist(projectRoot, entry) {
|
|
76
|
+
const allowlist = loadAllowlist(projectRoot);
|
|
77
|
+
|
|
78
|
+
// Generate ID if not provided
|
|
79
|
+
if (!entry.id) {
|
|
80
|
+
entry.id = `AL_${crypto.randomBytes(4).toString("hex")}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Set defaults
|
|
84
|
+
entry.addedAt = entry.addedAt || new Date().toISOString();
|
|
85
|
+
entry.addedBy = entry.addedBy || "user";
|
|
86
|
+
entry.scope = entry.scope || "global";
|
|
87
|
+
|
|
88
|
+
// Check for duplicates
|
|
89
|
+
const existing = allowlist.entries.find(e =>
|
|
90
|
+
e.pattern === entry.pattern && e.scope === entry.scope
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (existing) {
|
|
94
|
+
// Update existing entry
|
|
95
|
+
Object.assign(existing, entry);
|
|
96
|
+
} else {
|
|
97
|
+
allowlist.entries.push(entry);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
saveAllowlist(projectRoot, allowlist);
|
|
101
|
+
return entry;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isAllowlisted(finding, allowlist) {
|
|
105
|
+
if (!allowlist || !allowlist.entries) return false;
|
|
106
|
+
|
|
107
|
+
const now = new Date();
|
|
108
|
+
|
|
109
|
+
for (const entry of allowlist.entries) {
|
|
110
|
+
// Check expiration
|
|
111
|
+
if (entry.expiresAt && new Date(entry.expiresAt) < now) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check by finding ID
|
|
116
|
+
if (entry.findingId && entry.findingId === finding.id) {
|
|
117
|
+
return { allowed: true, reason: entry.reason, entry };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check by pattern
|
|
121
|
+
if (entry.pattern) {
|
|
122
|
+
const pattern = typeof entry.pattern === 'string'
|
|
123
|
+
? new RegExp(entry.pattern, 'i')
|
|
124
|
+
: entry.pattern;
|
|
125
|
+
|
|
126
|
+
// Match against finding title, page, or file
|
|
127
|
+
if (
|
|
128
|
+
pattern.test(finding.title || '') ||
|
|
129
|
+
pattern.test(finding.page || '') ||
|
|
130
|
+
pattern.test(finding.file || '') ||
|
|
131
|
+
(finding.evidence && finding.evidence.some(e => pattern.test(e.file || '')))
|
|
132
|
+
) {
|
|
133
|
+
// Check scope
|
|
134
|
+
if (entry.scope === 'global') {
|
|
135
|
+
return { allowed: true, reason: entry.reason, entry };
|
|
136
|
+
}
|
|
137
|
+
if (entry.scope === 'file' && entry.file === finding.file) {
|
|
138
|
+
return { allowed: true, reason: entry.reason, entry };
|
|
139
|
+
}
|
|
140
|
+
if (entry.scope === 'line' && entry.file === finding.file) {
|
|
141
|
+
const findingLine = finding.line || (finding.evidence?.[0]?.line);
|
|
142
|
+
if (findingLine && entry.lines) {
|
|
143
|
+
const [start, end] = entry.lines;
|
|
144
|
+
if (findingLine >= start && findingLine <= end) {
|
|
145
|
+
return { allowed: true, reason: entry.reason, entry };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { allowed: false };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function filterByAllowlist(findings, allowlist) {
|
|
157
|
+
const filtered = [];
|
|
158
|
+
const allowed = [];
|
|
159
|
+
|
|
160
|
+
for (const finding of findings) {
|
|
161
|
+
const result = isAllowlisted(finding, allowlist);
|
|
162
|
+
if (result.allowed) {
|
|
163
|
+
allowed.push({
|
|
164
|
+
finding,
|
|
165
|
+
reason: result.reason,
|
|
166
|
+
entryId: result.entry?.id
|
|
167
|
+
});
|
|
168
|
+
} else {
|
|
169
|
+
filtered.push(finding);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { filtered, allowed };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
177
|
+
// EVIDENCE STRUCTURE (What, Where, Why)
|
|
178
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Enhanced Evidence Structure:
|
|
182
|
+
* {
|
|
183
|
+
* what: string, // What was found
|
|
184
|
+
* where: { // Where it was found
|
|
185
|
+
* file: string,
|
|
186
|
+
* line: number,
|
|
187
|
+
* column?: number,
|
|
188
|
+
* snippet?: string,
|
|
189
|
+
* url?: string // For runtime findings
|
|
190
|
+
* },
|
|
191
|
+
* why: string, // Why this is a problem
|
|
192
|
+
* confidence: number, // 0.0 - 1.0
|
|
193
|
+
* category: string,
|
|
194
|
+
* severity: 'BLOCK' | 'WARN' | 'INFO',
|
|
195
|
+
* fixHint?: string,
|
|
196
|
+
* related?: Evidence[]
|
|
197
|
+
* }
|
|
198
|
+
*/
|
|
199
|
+
|
|
200
|
+
function enrichEvidence(finding) {
|
|
201
|
+
const evidence = {
|
|
202
|
+
what: finding.title || finding.message || 'Unknown finding',
|
|
203
|
+
where: {},
|
|
204
|
+
why: finding.reason || finding.why || 'Potential issue detected',
|
|
205
|
+
confidence: finding.confidence || 0.8,
|
|
206
|
+
category: finding.category || 'Unknown',
|
|
207
|
+
severity: finding.severity || 'WARN'
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Populate where
|
|
211
|
+
if (finding.file) {
|
|
212
|
+
evidence.where.file = finding.file;
|
|
213
|
+
}
|
|
214
|
+
if (finding.line) {
|
|
215
|
+
evidence.where.line = finding.line;
|
|
216
|
+
}
|
|
217
|
+
if (finding.page || finding.url) {
|
|
218
|
+
evidence.where.url = finding.page || finding.url;
|
|
219
|
+
}
|
|
220
|
+
if (finding.snippet || finding.code) {
|
|
221
|
+
evidence.where.snippet = finding.snippet || finding.code;
|
|
222
|
+
}
|
|
223
|
+
if (finding.screenshot) {
|
|
224
|
+
evidence.where.screenshot = finding.screenshot;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Add legacy evidence array if present
|
|
228
|
+
if (finding.evidence && Array.isArray(finding.evidence)) {
|
|
229
|
+
evidence.where.file = evidence.where.file || finding.evidence[0]?.file;
|
|
230
|
+
evidence.where.line = evidence.where.line || finding.evidence[0]?.line;
|
|
231
|
+
evidence.where.snippet = evidence.where.snippet || finding.evidence[0]?.snippet;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Add fix hint
|
|
235
|
+
if (finding.fixHints && finding.fixHints.length > 0) {
|
|
236
|
+
evidence.fixHint = finding.fixHints[0];
|
|
237
|
+
} else if (finding.fix) {
|
|
238
|
+
evidence.fixHint = finding.fix;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return evidence;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
245
|
+
// EVIDENCE PACK BUILDER
|
|
246
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
247
|
+
|
|
248
|
+
async function buildEvidencePack(projectRoot, options = {}) {
|
|
249
|
+
const {
|
|
250
|
+
includeVideos = true,
|
|
251
|
+
includeTraces = true,
|
|
252
|
+
includeScreenshots = true,
|
|
253
|
+
includeHar = true,
|
|
254
|
+
outputPath = null,
|
|
255
|
+
proveDir = null,
|
|
256
|
+
realityDir = null,
|
|
257
|
+
applyAllowlist = true
|
|
258
|
+
} = options;
|
|
259
|
+
|
|
260
|
+
const outDir = path.join(projectRoot, ".vibecheck", "evidence-packs");
|
|
261
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
262
|
+
|
|
263
|
+
// Load latest reports
|
|
264
|
+
const proveReportPath = proveDir
|
|
265
|
+
? path.join(proveDir, "prove_report.json")
|
|
266
|
+
: findLatestReport(projectRoot, "prove");
|
|
267
|
+
const realityReportPath = realityDir
|
|
268
|
+
? path.join(realityDir, "reality_report.json")
|
|
269
|
+
: findLatestReport(projectRoot, "reality");
|
|
270
|
+
const shipReportPath = path.join(projectRoot, ".vibecheck", "last_ship.json");
|
|
271
|
+
|
|
272
|
+
let proveReport = null;
|
|
273
|
+
let realityReport = null;
|
|
274
|
+
let shipReport = null;
|
|
275
|
+
|
|
276
|
+
try { proveReport = JSON.parse(fs.readFileSync(proveReportPath, "utf8")); } catch {}
|
|
277
|
+
try { realityReport = JSON.parse(fs.readFileSync(realityReportPath, "utf8")); } catch {}
|
|
278
|
+
try { shipReport = JSON.parse(fs.readFileSync(shipReportPath, "utf8")); } catch {}
|
|
279
|
+
|
|
280
|
+
if (!proveReport && !realityReport && !shipReport) {
|
|
281
|
+
throw new Error("No proof reports found. Run vibecheck prove or reality first.");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Load allowlist
|
|
285
|
+
const allowlist = applyAllowlist ? loadAllowlist(projectRoot) : null;
|
|
286
|
+
|
|
287
|
+
// Collect all findings
|
|
288
|
+
let allFindings = [];
|
|
289
|
+
if (proveReport?.findings) allFindings.push(...proveReport.findings);
|
|
290
|
+
if (realityReport?.findings) allFindings.push(...realityReport.findings);
|
|
291
|
+
if (shipReport?.findings) allFindings.push(...shipReport.findings);
|
|
292
|
+
|
|
293
|
+
// Dedupe findings by ID
|
|
294
|
+
const seenIds = new Set();
|
|
295
|
+
allFindings = allFindings.filter(f => {
|
|
296
|
+
if (seenIds.has(f.id)) return false;
|
|
297
|
+
seenIds.add(f.id);
|
|
298
|
+
return true;
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Apply allowlist filtering
|
|
302
|
+
let findings = allFindings;
|
|
303
|
+
let allowedFindings = [];
|
|
304
|
+
if (allowlist) {
|
|
305
|
+
const result = filterByAllowlist(allFindings, allowlist);
|
|
306
|
+
findings = result.filtered;
|
|
307
|
+
allowedFindings = result.allowed;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Enrich evidence
|
|
311
|
+
const enrichedFindings = findings.map(enrichEvidence);
|
|
312
|
+
|
|
313
|
+
// Build summary
|
|
314
|
+
const summary = {
|
|
315
|
+
totalFindings: allFindings.length,
|
|
316
|
+
filteredFindings: findings.length,
|
|
317
|
+
allowlistedCount: allowedFindings.length,
|
|
318
|
+
blocks: findings.filter(f => f.severity === 'BLOCK').length,
|
|
319
|
+
warns: findings.filter(f => f.severity === 'WARN').length,
|
|
320
|
+
verdict: proveReport?.verdict || shipReport?.meta?.verdict || 'UNKNOWN',
|
|
321
|
+
coverage: realityReport?.coverage || null
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// Collect artifacts
|
|
325
|
+
const artifacts = {
|
|
326
|
+
screenshots: [],
|
|
327
|
+
videos: [],
|
|
328
|
+
traces: [],
|
|
329
|
+
har: []
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// Find artifact directories
|
|
333
|
+
const realityBase = realityReportPath ? path.dirname(realityReportPath) : null;
|
|
334
|
+
|
|
335
|
+
if (realityBase) {
|
|
336
|
+
// Screenshots
|
|
337
|
+
if (includeScreenshots) {
|
|
338
|
+
const shotsDir = path.join(realityBase, "screenshots");
|
|
339
|
+
if (fs.existsSync(shotsDir)) {
|
|
340
|
+
artifacts.screenshots = fs.readdirSync(shotsDir)
|
|
341
|
+
.filter(f => /\.(png|jpg|jpeg)$/i.test(f))
|
|
342
|
+
.map(f => path.join(shotsDir, f));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Videos
|
|
347
|
+
if (includeVideos) {
|
|
348
|
+
const videosDir = path.join(realityBase, "videos");
|
|
349
|
+
if (fs.existsSync(videosDir)) {
|
|
350
|
+
artifacts.videos = fs.readdirSync(videosDir)
|
|
351
|
+
.filter(f => /\.(webm|mp4)$/i.test(f))
|
|
352
|
+
.map(f => path.join(videosDir, f));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Traces
|
|
357
|
+
if (includeTraces) {
|
|
358
|
+
const tracesDir = path.join(realityBase, "traces");
|
|
359
|
+
if (fs.existsSync(tracesDir)) {
|
|
360
|
+
artifacts.traces = fs.readdirSync(tracesDir)
|
|
361
|
+
.filter(f => /\.zip$/i.test(f))
|
|
362
|
+
.map(f => path.join(tracesDir, f));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// HAR
|
|
367
|
+
if (includeHar) {
|
|
368
|
+
const harDir = path.join(realityBase, "har");
|
|
369
|
+
if (fs.existsSync(harDir)) {
|
|
370
|
+
artifacts.har = fs.readdirSync(harDir)
|
|
371
|
+
.filter(f => /\.har$/i.test(f))
|
|
372
|
+
.map(f => path.join(harDir, f));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Build evidence pack manifest
|
|
378
|
+
const packId = `EP_${Date.now()}_${crypto.randomBytes(4).toString("hex")}`;
|
|
379
|
+
const manifest = {
|
|
380
|
+
id: packId,
|
|
381
|
+
version: EVIDENCE_PACK_VERSION,
|
|
382
|
+
meta: {
|
|
383
|
+
createdAt: new Date().toISOString(),
|
|
384
|
+
projectName: path.basename(projectRoot),
|
|
385
|
+
projectRoot: projectRoot,
|
|
386
|
+
proveReport: proveReportPath ? path.relative(projectRoot, proveReportPath) : null,
|
|
387
|
+
realityReport: realityReportPath ? path.relative(projectRoot, realityReportPath) : null,
|
|
388
|
+
shipReport: shipReportPath && fs.existsSync(shipReportPath) ? path.relative(projectRoot, shipReportPath) : null
|
|
389
|
+
},
|
|
390
|
+
summary,
|
|
391
|
+
findings: enrichedFindings,
|
|
392
|
+
allowlist: allowlist ? {
|
|
393
|
+
applied: true,
|
|
394
|
+
entriesCount: allowlist.entries?.length || 0,
|
|
395
|
+
allowedFindings: allowedFindings.map(a => ({
|
|
396
|
+
findingId: a.finding.id,
|
|
397
|
+
reason: a.reason
|
|
398
|
+
}))
|
|
399
|
+
} : { applied: false },
|
|
400
|
+
artifacts: {
|
|
401
|
+
screenshots: artifacts.screenshots.map(p => path.relative(projectRoot, p)),
|
|
402
|
+
videos: artifacts.videos.map(p => path.relative(projectRoot, p)),
|
|
403
|
+
traces: artifacts.traces.map(p => path.relative(projectRoot, p)),
|
|
404
|
+
har: artifacts.har.map(p => path.relative(projectRoot, p))
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Write manifest
|
|
409
|
+
const manifestPath = path.join(outDir, `${packId}_manifest.json`);
|
|
410
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
411
|
+
|
|
412
|
+
// Create zip bundle if requested
|
|
413
|
+
let zipPath = null;
|
|
414
|
+
if (outputPath || artifacts.videos.length > 0 || artifacts.traces.length > 0) {
|
|
415
|
+
zipPath = outputPath || path.join(outDir, `${packId}.zip`);
|
|
416
|
+
await createEvidenceZip(projectRoot, manifest, artifacts, zipPath);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
id: packId,
|
|
421
|
+
manifest,
|
|
422
|
+
manifestPath,
|
|
423
|
+
zipPath,
|
|
424
|
+
summary
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function createEvidenceZip(projectRoot, manifest, artifacts, outputPath) {
|
|
429
|
+
return new Promise((resolve, reject) => {
|
|
430
|
+
// Check if archiver is available, otherwise skip zip creation
|
|
431
|
+
let archiver;
|
|
432
|
+
try {
|
|
433
|
+
archiver = require("archiver");
|
|
434
|
+
} catch {
|
|
435
|
+
// archiver not installed, skip zip creation
|
|
436
|
+
resolve({ skipped: true, reason: "archiver not installed" });
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const output = fs.createWriteStream(outputPath);
|
|
441
|
+
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
442
|
+
|
|
443
|
+
output.on("close", () => resolve({ path: outputPath, size: archive.pointer() }));
|
|
444
|
+
archive.on("error", reject);
|
|
445
|
+
|
|
446
|
+
archive.pipe(output);
|
|
447
|
+
|
|
448
|
+
// Add manifest
|
|
449
|
+
archive.append(JSON.stringify(manifest, null, 2), { name: "manifest.json" });
|
|
450
|
+
|
|
451
|
+
// Add artifacts
|
|
452
|
+
for (const screenshot of artifacts.screenshots) {
|
|
453
|
+
if (fs.existsSync(screenshot)) {
|
|
454
|
+
archive.file(screenshot, { name: `screenshots/${path.basename(screenshot)}` });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
for (const video of artifacts.videos) {
|
|
459
|
+
if (fs.existsSync(video)) {
|
|
460
|
+
archive.file(video, { name: `videos/${path.basename(video)}` });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
for (const trace of artifacts.traces) {
|
|
465
|
+
if (fs.existsSync(trace)) {
|
|
466
|
+
archive.file(trace, { name: `traces/${path.basename(trace)}` });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
for (const har of artifacts.har) {
|
|
471
|
+
if (fs.existsSync(har)) {
|
|
472
|
+
archive.file(har, { name: `har/${path.basename(har)}` });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
archive.finalize();
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function findLatestReport(projectRoot, type) {
|
|
481
|
+
const baseDir = path.join(projectRoot, ".vibecheck", type);
|
|
482
|
+
|
|
483
|
+
// Check for latest.json pointer
|
|
484
|
+
const latestPath = path.join(baseDir, "latest.json");
|
|
485
|
+
try {
|
|
486
|
+
const latest = JSON.parse(fs.readFileSync(latestPath, "utf8"));
|
|
487
|
+
const reportPath = path.join(projectRoot, latest.latest, `${type}_report.json`);
|
|
488
|
+
if (fs.existsSync(reportPath)) return reportPath;
|
|
489
|
+
} catch {}
|
|
490
|
+
|
|
491
|
+
// Fall back to scanning directories
|
|
492
|
+
try {
|
|
493
|
+
const dirs = fs.readdirSync(baseDir)
|
|
494
|
+
.filter(d => /^\d{8}_\d{6}$/.test(d))
|
|
495
|
+
.sort()
|
|
496
|
+
.reverse();
|
|
497
|
+
|
|
498
|
+
for (const dir of dirs) {
|
|
499
|
+
const reportPath = path.join(baseDir, dir, `${type}_report.json`);
|
|
500
|
+
if (fs.existsSync(reportPath)) return reportPath;
|
|
501
|
+
}
|
|
502
|
+
} catch {}
|
|
503
|
+
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
508
|
+
// MARKDOWN REPORT GENERATOR
|
|
509
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
510
|
+
|
|
511
|
+
function generateMarkdownReport(pack) {
|
|
512
|
+
const { manifest, summary } = pack;
|
|
513
|
+
const verdict = summary.verdict;
|
|
514
|
+
const verdictEmoji = verdict === 'SHIP' ? '✅' : verdict === 'WARN' ? '⚠️' : '🛑';
|
|
515
|
+
|
|
516
|
+
let md = `# Evidence Pack Report
|
|
517
|
+
|
|
518
|
+
## ${verdictEmoji} Verdict: ${verdict}
|
|
519
|
+
|
|
520
|
+
**Generated:** ${manifest.meta.createdAt}
|
|
521
|
+
**Project:** ${manifest.meta.projectName}
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
## Summary
|
|
526
|
+
|
|
527
|
+
| Metric | Value |
|
|
528
|
+
|--------|-------|
|
|
529
|
+
| Total Findings | ${summary.totalFindings} |
|
|
530
|
+
| Filtered (after allowlist) | ${summary.filteredFindings} |
|
|
531
|
+
| Allowlisted | ${summary.allowlistedCount} |
|
|
532
|
+
| **BLOCK** | ${summary.blocks} |
|
|
533
|
+
| **WARN** | ${summary.warns} |
|
|
534
|
+
|
|
535
|
+
`;
|
|
536
|
+
|
|
537
|
+
if (summary.coverage) {
|
|
538
|
+
md += `
|
|
539
|
+
### Coverage
|
|
540
|
+
|
|
541
|
+
- **Pages Hit:** ${summary.coverage.hit} / ${summary.coverage.total}
|
|
542
|
+
- **Coverage:** ${summary.coverage.percent}%
|
|
543
|
+
|
|
544
|
+
`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (manifest.findings.length > 0) {
|
|
548
|
+
md += `
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## Findings
|
|
552
|
+
|
|
553
|
+
`;
|
|
554
|
+
|
|
555
|
+
for (const finding of manifest.findings.slice(0, 20)) {
|
|
556
|
+
const severityIcon = finding.severity === 'BLOCK' ? '🛑' : finding.severity === 'WARN' ? '⚠️' : 'ℹ️';
|
|
557
|
+
|
|
558
|
+
md += `### ${severityIcon} ${finding.what}
|
|
559
|
+
|
|
560
|
+
- **Category:** ${finding.category}
|
|
561
|
+
- **Confidence:** ${Math.round(finding.confidence * 100)}%
|
|
562
|
+
`;
|
|
563
|
+
|
|
564
|
+
if (finding.where.file) {
|
|
565
|
+
md += `- **File:** \`${finding.where.file}\``;
|
|
566
|
+
if (finding.where.line) md += `:${finding.where.line}`;
|
|
567
|
+
md += '\n';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (finding.where.url) {
|
|
571
|
+
md += `- **URL:** ${finding.where.url}\n`;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (finding.where.screenshot) {
|
|
575
|
+
md += `- **Screenshot:** ${finding.where.screenshot}\n`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
md += `
|
|
579
|
+
**Why:** ${finding.why}
|
|
580
|
+
`;
|
|
581
|
+
|
|
582
|
+
if (finding.fixHint) {
|
|
583
|
+
md += `
|
|
584
|
+
**Fix:** ${finding.fixHint}
|
|
585
|
+
`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
md += '\n';
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (manifest.findings.length > 20) {
|
|
592
|
+
md += `\n_...and ${manifest.findings.length - 20} more findings. See manifest.json for full details._\n`;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (manifest.artifacts.videos.length > 0 || manifest.artifacts.traces.length > 0) {
|
|
597
|
+
md += `
|
|
598
|
+
---
|
|
599
|
+
|
|
600
|
+
## Visual Artifacts
|
|
601
|
+
|
|
602
|
+
`;
|
|
603
|
+
|
|
604
|
+
if (manifest.artifacts.videos.length > 0) {
|
|
605
|
+
md += `### Videos
|
|
606
|
+
|
|
607
|
+
`;
|
|
608
|
+
for (const video of manifest.artifacts.videos) {
|
|
609
|
+
md += `- [${path.basename(video)}](${video})\n`;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (manifest.artifacts.traces.length > 0) {
|
|
614
|
+
md += `
|
|
615
|
+
### Traces
|
|
616
|
+
|
|
617
|
+
View traces at [trace.playwright.dev](https://trace.playwright.dev)
|
|
618
|
+
|
|
619
|
+
`;
|
|
620
|
+
for (const trace of manifest.artifacts.traces) {
|
|
621
|
+
md += `- [${path.basename(trace)}](${trace})\n`;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (manifest.allowlist.applied && manifest.allowlist.allowedFindings.length > 0) {
|
|
627
|
+
md += `
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
## Allowlisted Findings
|
|
631
|
+
|
|
632
|
+
The following findings were filtered by the allowlist:
|
|
633
|
+
|
|
634
|
+
| Finding ID | Reason |
|
|
635
|
+
|------------|--------|
|
|
636
|
+
`;
|
|
637
|
+
|
|
638
|
+
for (const allowed of manifest.allowlist.allowedFindings.slice(0, 10)) {
|
|
639
|
+
md += `| \`${allowed.findingId}\` | ${allowed.reason || 'No reason provided'} |\n`;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (manifest.allowlist.allowedFindings.length > 10) {
|
|
643
|
+
md += `\n_...and ${manifest.allowlist.allowedFindings.length - 10} more._\n`;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
md += `
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
_Generated by vibecheck evidence-pack v${EVIDENCE_PACK_VERSION}_
|
|
651
|
+
`;
|
|
652
|
+
|
|
653
|
+
return md;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
657
|
+
// EXPORTS
|
|
658
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
659
|
+
|
|
660
|
+
module.exports = {
|
|
661
|
+
// Core functions
|
|
662
|
+
buildEvidencePack,
|
|
663
|
+
generateMarkdownReport,
|
|
664
|
+
|
|
665
|
+
// Allowlist functions
|
|
666
|
+
loadAllowlist,
|
|
667
|
+
saveAllowlist,
|
|
668
|
+
addToAllowlist,
|
|
669
|
+
isAllowlisted,
|
|
670
|
+
filterByAllowlist,
|
|
671
|
+
|
|
672
|
+
// Evidence helpers
|
|
673
|
+
enrichEvidence,
|
|
674
|
+
|
|
675
|
+
// Constants
|
|
676
|
+
EVIDENCE_PACK_VERSION,
|
|
677
|
+
DEFAULT_ALLOWLIST_PATH
|
|
678
|
+
};
|