@vibecheckai/cli 3.2.2 â 3.2.4
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/.generated +25 -25
- package/bin/dev/run-v2-torture.js +30 -30
- package/bin/runners/ENHANCEMENT_GUIDE.md +121 -121
- package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
- package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
- package/bin/runners/lib/agent-firewall/claims/extractor.js +117 -28
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +23 -14
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +72 -1
- package/bin/runners/lib/agent-firewall/interceptor/base.js +2 -2
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +6 -0
- package/bin/runners/lib/agent-firewall/policy/engine.js +34 -3
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +29 -4
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +12 -0
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +21 -0
- package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
- package/bin/runners/lib/analyzers.js +606 -325
- package/bin/runners/lib/auth-truth.js +193 -193
- package/bin/runners/lib/backup.js +62 -62
- package/bin/runners/lib/billing.js +107 -107
- package/bin/runners/lib/claims.js +118 -118
- package/bin/runners/lib/cli-ui.js +540 -540
- package/bin/runners/lib/contracts/auth-contract.js +202 -202
- package/bin/runners/lib/contracts/env-contract.js +181 -181
- package/bin/runners/lib/contracts/external-contract.js +206 -206
- package/bin/runners/lib/contracts/guard.js +168 -168
- package/bin/runners/lib/contracts/index.js +89 -89
- package/bin/runners/lib/contracts/plan-validator.js +311 -311
- package/bin/runners/lib/contracts/route-contract.js +199 -199
- package/bin/runners/lib/contracts.js +804 -804
- package/bin/runners/lib/detect.js +89 -89
- package/bin/runners/lib/doctor/autofix.js +254 -254
- package/bin/runners/lib/doctor/index.js +37 -37
- package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
- package/bin/runners/lib/doctor/modules/index.js +46 -46
- package/bin/runners/lib/doctor/modules/network.js +250 -250
- package/bin/runners/lib/doctor/modules/project.js +312 -312
- package/bin/runners/lib/doctor/modules/runtime.js +224 -224
- package/bin/runners/lib/doctor/modules/security.js +348 -348
- package/bin/runners/lib/doctor/modules/system.js +213 -213
- package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
- package/bin/runners/lib/doctor/reporter.js +262 -262
- package/bin/runners/lib/doctor/service.js +262 -262
- package/bin/runners/lib/doctor/types.js +113 -113
- package/bin/runners/lib/doctor/ui.js +263 -263
- package/bin/runners/lib/doctor-v2.js +608 -608
- package/bin/runners/lib/drift.js +425 -425
- package/bin/runners/lib/enforcement.js +72 -72
- package/bin/runners/lib/engines/accessibility-engine.js +190 -0
- package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
- package/bin/runners/lib/engines/ast-cache.js +99 -0
- package/bin/runners/lib/engines/code-quality-engine.js +255 -0
- package/bin/runners/lib/engines/console-logs-engine.js +115 -0
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
- package/bin/runners/lib/engines/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
- package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
- package/bin/runners/lib/engines/file-filter.js +131 -0
- package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
- package/bin/runners/lib/engines/mock-data-engine.js +272 -0
- package/bin/runners/lib/engines/parallel-processor.js +71 -0
- package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
- package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
- package/bin/runners/lib/engines/type-aware-engine.js +152 -0
- package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
- package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
- package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
- package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
- package/bin/runners/lib/enterprise-detect.js +603 -603
- package/bin/runners/lib/enterprise-init.js +942 -942
- package/bin/runners/lib/env-resolver.js +417 -417
- package/bin/runners/lib/env-template.js +66 -66
- package/bin/runners/lib/env.js +189 -189
- package/bin/runners/lib/extractors/client-calls.js +990 -990
- package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
- package/bin/runners/lib/extractors/fastify-routes.js +426 -426
- package/bin/runners/lib/extractors/index.js +363 -363
- package/bin/runners/lib/extractors/next-routes.js +524 -524
- package/bin/runners/lib/extractors/proof-graph.js +431 -431
- package/bin/runners/lib/extractors/route-matcher.js +451 -451
- package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
- package/bin/runners/lib/extractors/ui-bindings.js +547 -547
- package/bin/runners/lib/findings-schema.js +281 -281
- package/bin/runners/lib/firewall-prompt.js +50 -50
- package/bin/runners/lib/global-flags.js +213 -213
- package/bin/runners/lib/graph/graph-builder.js +265 -265
- package/bin/runners/lib/graph/html-renderer.js +413 -413
- package/bin/runners/lib/graph/index.js +32 -32
- package/bin/runners/lib/graph/runtime-collector.js +215 -215
- package/bin/runners/lib/graph/static-extractor.js +518 -518
- package/bin/runners/lib/html-report.js +650 -650
- package/bin/runners/lib/interactive-menu.js +1496 -1496
- package/bin/runners/lib/llm.js +75 -75
- package/bin/runners/lib/meter.js +61 -61
- package/bin/runners/lib/missions/evidence.js +126 -126
- package/bin/runners/lib/patch.js +40 -40
- package/bin/runners/lib/permissions/auth-model.js +213 -213
- package/bin/runners/lib/permissions/idor-prover.js +205 -205
- package/bin/runners/lib/permissions/index.js +45 -45
- package/bin/runners/lib/permissions/matrix-builder.js +198 -198
- package/bin/runners/lib/pkgjson.js +28 -28
- package/bin/runners/lib/policy.js +295 -295
- package/bin/runners/lib/preflight.js +142 -142
- package/bin/runners/lib/reality/correlation-detectors.js +359 -359
- package/bin/runners/lib/reality/index.js +318 -318
- package/bin/runners/lib/reality/request-hashing.js +416 -416
- package/bin/runners/lib/reality/request-mapper.js +453 -453
- package/bin/runners/lib/reality/safety-rails.js +463 -463
- package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
- package/bin/runners/lib/reality/toast-detector.js +393 -393
- package/bin/runners/lib/reality-findings.js +84 -84
- package/bin/runners/lib/receipts.js +179 -179
- package/bin/runners/lib/redact.js +29 -29
- package/bin/runners/lib/replay/capsule-manager.js +154 -154
- package/bin/runners/lib/replay/index.js +263 -263
- package/bin/runners/lib/replay/player.js +348 -348
- package/bin/runners/lib/replay/recorder.js +331 -331
- package/bin/runners/lib/report-output.js +187 -187
- package/bin/runners/lib/report.js +135 -135
- package/bin/runners/lib/route-detection.js +1140 -1140
- package/bin/runners/lib/sandbox/index.js +59 -59
- package/bin/runners/lib/sandbox/proof-chain.js +399 -399
- package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
- package/bin/runners/lib/sandbox/worktree.js +174 -174
- package/bin/runners/lib/scan-output.js +525 -190
- package/bin/runners/lib/schema-validator.js +350 -350
- package/bin/runners/lib/schemas/contracts.schema.json +160 -160
- package/bin/runners/lib/schemas/finding.schema.json +100 -100
- package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
- package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
- package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
- package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
- package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
- package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
- package/bin/runners/lib/schemas/validator.js +438 -438
- package/bin/runners/lib/score-history.js +282 -282
- package/bin/runners/lib/share-pack.js +239 -239
- package/bin/runners/lib/snippets.js +67 -67
- package/bin/runners/lib/status-output.js +253 -253
- package/bin/runners/lib/terminal-ui.js +351 -271
- package/bin/runners/lib/upsell.js +510 -510
- package/bin/runners/lib/usage.js +153 -153
- package/bin/runners/lib/validate-patch.js +156 -156
- package/bin/runners/lib/verdict-engine.js +628 -628
- package/bin/runners/reality/engine.js +917 -917
- package/bin/runners/reality/flows.js +122 -122
- package/bin/runners/reality/report.js +378 -378
- package/bin/runners/reality/session.js +193 -193
- package/bin/runners/runGuard.js +168 -168
- package/bin/runners/runProof.zip +0 -0
- package/bin/runners/runProve.js +8 -0
- package/bin/runners/runReality.js +14 -0
- package/bin/runners/runScan.js +17 -1
- package/bin/runners/runTruth.js +15 -3
- package/mcp-server/tier-auth.js +4 -4
- package/mcp-server/tools/index.js +72 -72
- package/package.json +1 -1
|
@@ -1,628 +1,628 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Verdict Engine v2
|
|
3
|
-
*
|
|
4
|
-
* Merges all findings from detectors, computes verdict, generates ship report.
|
|
5
|
-
* Single source of truth for SHIP/WARN/BLOCK decisions.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
"use strict";
|
|
9
|
-
|
|
10
|
-
const fs = require("fs");
|
|
11
|
-
const path = require("path");
|
|
12
|
-
const crypto = require("crypto");
|
|
13
|
-
|
|
14
|
-
const { createFinding, getSeverity, generateFingerprint } = require("./schemas/validator");
|
|
15
|
-
|
|
16
|
-
// =============================================================================
|
|
17
|
-
// VERDICT COMPUTATION
|
|
18
|
-
// =============================================================================
|
|
19
|
-
|
|
20
|
-
// Coverage thresholds defaults
|
|
21
|
-
const DEFAULT_COVERAGE_THRESHOLDS = {
|
|
22
|
-
minActionCoverage: 0, // --min-action-coverage
|
|
23
|
-
minRouteCoverage: 0, // --min-route-coverage
|
|
24
|
-
requireAuthCoverage: false, // --require-auth-coverage
|
|
25
|
-
minAuthCoverage: 80, // Minimum % when requireAuthCoverage is true
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Compute verdict from findings
|
|
30
|
-
*/
|
|
31
|
-
function computeVerdict(findings, options = {}) {
|
|
32
|
-
const {
|
|
33
|
-
failOnWarn = false,
|
|
34
|
-
coverage = null,
|
|
35
|
-
thresholds = {},
|
|
36
|
-
} = options;
|
|
37
|
-
|
|
38
|
-
const effectiveThresholds = { ...DEFAULT_COVERAGE_THRESHOLDS, ...thresholds };
|
|
39
|
-
|
|
40
|
-
const stats = {
|
|
41
|
-
total: findings.length,
|
|
42
|
-
bySeverity: { BLOCK: 0, WARN: 0, INFO: 0 },
|
|
43
|
-
byCategory: {},
|
|
44
|
-
byScope: {},
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const blockReasons = [];
|
|
48
|
-
|
|
49
|
-
for (const f of findings) {
|
|
50
|
-
// Count by severity
|
|
51
|
-
stats.bySeverity[f.severity] = (stats.bySeverity[f.severity] || 0) + 1;
|
|
52
|
-
|
|
53
|
-
// Count by category
|
|
54
|
-
stats.byCategory[f.category] = (stats.byCategory[f.category] || 0) + 1;
|
|
55
|
-
|
|
56
|
-
// Count by scope
|
|
57
|
-
stats.byScope[f.scope] = (stats.byScope[f.scope] || 0) + 1;
|
|
58
|
-
|
|
59
|
-
// Collect block reasons
|
|
60
|
-
if (f.severity === "BLOCK") {
|
|
61
|
-
blockReasons.push(f.title);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Check coverage thresholds
|
|
66
|
-
const coverageViolations = [];
|
|
67
|
-
if (coverage) {
|
|
68
|
-
if (effectiveThresholds.minActionCoverage > 0 &&
|
|
69
|
-
coverage.uiActionsVerifiedPct < effectiveThresholds.minActionCoverage) {
|
|
70
|
-
coverageViolations.push(
|
|
71
|
-
`UI action coverage ${coverage.uiActionsVerifiedPct.toFixed(0)}% < ${effectiveThresholds.minActionCoverage}%`
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
if (effectiveThresholds.minRouteCoverage > 0 &&
|
|
75
|
-
coverage.clientCallsMappedPct < effectiveThresholds.minRouteCoverage) {
|
|
76
|
-
coverageViolations.push(
|
|
77
|
-
`Route coverage ${coverage.clientCallsMappedPct.toFixed(0)}% < ${effectiveThresholds.minRouteCoverage}%`
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
if (effectiveThresholds.requireAuthCoverage &&
|
|
81
|
-
coverage.authVerifiedPct !== null &&
|
|
82
|
-
coverage.authVerifiedPct < effectiveThresholds.minAuthCoverage) {
|
|
83
|
-
coverageViolations.push(
|
|
84
|
-
`Auth coverage ${coverage.authVerifiedPct.toFixed(0)}% < ${effectiveThresholds.minAuthCoverage}%`
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Determine verdict
|
|
90
|
-
let status = "SHIP";
|
|
91
|
-
let exitCode = 0;
|
|
92
|
-
|
|
93
|
-
if (stats.bySeverity.BLOCK > 0 || coverageViolations.length > 0) {
|
|
94
|
-
status = "BLOCK";
|
|
95
|
-
exitCode = 2;
|
|
96
|
-
if (coverageViolations.length > 0) {
|
|
97
|
-
blockReasons.push(...coverageViolations.map(v => `Coverage: ${v}`));
|
|
98
|
-
}
|
|
99
|
-
} else if (stats.bySeverity.WARN > 0) {
|
|
100
|
-
status = "WARN";
|
|
101
|
-
exitCode = failOnWarn ? 1 : 0;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Generate summary
|
|
105
|
-
const summary = generateVerdictSummary(status, stats);
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
status,
|
|
109
|
-
exitCode,
|
|
110
|
-
summary,
|
|
111
|
-
blockReasons: blockReasons.slice(0, 5),
|
|
112
|
-
stats,
|
|
113
|
-
coverageViolations,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Generate human-readable verdict summary
|
|
119
|
-
*/
|
|
120
|
-
function generateVerdictSummary(status, stats) {
|
|
121
|
-
const parts = [];
|
|
122
|
-
|
|
123
|
-
if (status === "SHIP") {
|
|
124
|
-
if (stats.bySeverity.WARN > 0) {
|
|
125
|
-
parts.push(`â
SHIP with ${stats.bySeverity.WARN} warning(s)`);
|
|
126
|
-
} else {
|
|
127
|
-
parts.push("â
SHIP - All checks passed");
|
|
128
|
-
}
|
|
129
|
-
} else if (status === "WARN") {
|
|
130
|
-
parts.push(`â ī¸ WARN - ${stats.bySeverity.WARN} warning(s) found`);
|
|
131
|
-
} else {
|
|
132
|
-
parts.push(`đĢ BLOCK - ${stats.bySeverity.BLOCK} blocker(s) found`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Add category breakdown
|
|
136
|
-
const categories = Object.entries(stats.byCategory)
|
|
137
|
-
.filter(([_, count]) => count > 0)
|
|
138
|
-
.map(([cat, count]) => `${cat}: ${count}`)
|
|
139
|
-
.join(", ");
|
|
140
|
-
|
|
141
|
-
if (categories) {
|
|
142
|
-
parts.push(`Categories: ${categories}`);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return parts.join("\n");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// =============================================================================
|
|
149
|
-
// COVERAGE METRICS
|
|
150
|
-
// =============================================================================
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Compute coverage metrics from truthpack and reality data
|
|
154
|
-
*/
|
|
155
|
-
function computeCoverageMetrics(options = {}) {
|
|
156
|
-
const { truthpack, realityReport, proofGraph, findings } = options;
|
|
157
|
-
|
|
158
|
-
const metrics = {
|
|
159
|
-
clientCalls: {
|
|
160
|
-
total: 0,
|
|
161
|
-
mappedToServerRoutes: 0,
|
|
162
|
-
mappedToServerRoutesPct: 0,
|
|
163
|
-
},
|
|
164
|
-
runtimeRequests: {
|
|
165
|
-
total: 0,
|
|
166
|
-
mappedToClientCalls: 0,
|
|
167
|
-
mappedToClientCallsPct: 0,
|
|
168
|
-
},
|
|
169
|
-
uiActions: {
|
|
170
|
-
total: 0,
|
|
171
|
-
withMeaningfulChange: 0,
|
|
172
|
-
withMeaningfulChangePct: 0,
|
|
173
|
-
},
|
|
174
|
-
auth: {
|
|
175
|
-
protectedRoutes: 0,
|
|
176
|
-
verified: 0,
|
|
177
|
-
protectedRoutesVerifiedPct: null,
|
|
178
|
-
},
|
|
179
|
-
unmappedApiRequests: [],
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
// Client calls coverage
|
|
183
|
-
if (truthpack?.clientCalls?.calls) {
|
|
184
|
-
metrics.clientCalls.total = truthpack.clientCalls.calls.length;
|
|
185
|
-
|
|
186
|
-
// Count those with linked server routes (from proof graph or matching)
|
|
187
|
-
if (proofGraph?.edges) {
|
|
188
|
-
const clientCallsWithMatches = new Set(
|
|
189
|
-
proofGraph.edges
|
|
190
|
-
.filter(e => e.type === "MATCHES" || e.type === "CALLS")
|
|
191
|
-
.map(e => e.from)
|
|
192
|
-
);
|
|
193
|
-
metrics.clientCalls.mappedToServerRoutes = clientCallsWithMatches.size;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
metrics.clientCalls.mappedToServerRoutesPct = metrics.clientCalls.total > 0
|
|
197
|
-
? (metrics.clientCalls.mappedToServerRoutes / metrics.clientCalls.total) * 100
|
|
198
|
-
: 100;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Runtime requests coverage
|
|
202
|
-
if (realityReport?.requests) {
|
|
203
|
-
metrics.runtimeRequests.total = realityReport.requests.length;
|
|
204
|
-
|
|
205
|
-
// Count mapped requests
|
|
206
|
-
const mapped = realityReport.requests.filter(r => r.mappedClientCallId || r.matched);
|
|
207
|
-
metrics.runtimeRequests.mappedToClientCalls = mapped.length;
|
|
208
|
-
|
|
209
|
-
metrics.runtimeRequests.mappedToClientCallsPct = metrics.runtimeRequests.total > 0
|
|
210
|
-
? (metrics.runtimeRequests.mappedToClientCalls / metrics.runtimeRequests.total) * 100
|
|
211
|
-
: 100;
|
|
212
|
-
|
|
213
|
-
// Top 5 unmapped requests
|
|
214
|
-
const unmapped = realityReport.requests
|
|
215
|
-
.filter(r => !r.mappedClientCallId && !r.matched)
|
|
216
|
-
.map(r => r.canonicalPath || r.url)
|
|
217
|
-
.filter(Boolean);
|
|
218
|
-
|
|
219
|
-
// Dedupe and take top 5
|
|
220
|
-
metrics.unmappedApiRequests = [...new Set(unmapped)].slice(0, 5);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// UI actions coverage
|
|
224
|
-
if (realityReport?.actions) {
|
|
225
|
-
metrics.uiActions.total = realityReport.actions.length;
|
|
226
|
-
|
|
227
|
-
// Count actions with meaningful UI change
|
|
228
|
-
const withChange = realityReport.actions.filter(a =>
|
|
229
|
-
a.meaningfulChange || a.uiChangeScore > 0.5
|
|
230
|
-
);
|
|
231
|
-
metrics.uiActions.withMeaningfulChange = withChange.length;
|
|
232
|
-
|
|
233
|
-
metrics.uiActions.withMeaningfulChangePct = metrics.uiActions.total > 0
|
|
234
|
-
? (metrics.uiActions.withMeaningfulChange / metrics.uiActions.total) * 100
|
|
235
|
-
: 100;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Auth coverage (only when verify-auth was used)
|
|
239
|
-
if (truthpack?.auth?.protectedPatterns?.length > 0 && realityReport?.auth) {
|
|
240
|
-
metrics.auth.protectedRoutes = truthpack.auth.protectedPatterns.length;
|
|
241
|
-
metrics.auth.verified = realityReport.auth.verified?.length || 0;
|
|
242
|
-
|
|
243
|
-
metrics.auth.protectedRoutesVerifiedPct = metrics.auth.protectedRoutes > 0
|
|
244
|
-
? (metrics.auth.verified / metrics.auth.protectedRoutes) * 100
|
|
245
|
-
: null;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Flattened percentage fields for easy access
|
|
249
|
-
return {
|
|
250
|
-
...metrics,
|
|
251
|
-
clientCallsMappedPct: metrics.clientCalls.mappedToServerRoutesPct,
|
|
252
|
-
runtimeRequestsMappedPct: metrics.runtimeRequests.mappedToClientCallsPct,
|
|
253
|
-
uiActionsVerifiedPct: metrics.uiActions.withMeaningfulChangePct,
|
|
254
|
-
authVerifiedPct: metrics.auth.protectedRoutesVerifiedPct,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// =============================================================================
|
|
259
|
-
// SHIP REPORT GENERATION
|
|
260
|
-
// =============================================================================
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Generate ship report from findings and metadata
|
|
264
|
-
*/
|
|
265
|
-
function generateShipReport(options = {}) {
|
|
266
|
-
const {
|
|
267
|
-
findings = [],
|
|
268
|
-
truthpack = null,
|
|
269
|
-
realityReport = null,
|
|
270
|
-
proofGraph = null,
|
|
271
|
-
repoRoot = process.cwd(),
|
|
272
|
-
failOnWarn = false,
|
|
273
|
-
startTime = Date.now(),
|
|
274
|
-
} = options;
|
|
275
|
-
|
|
276
|
-
const verdict = computeVerdict(findings, { failOnWarn });
|
|
277
|
-
|
|
278
|
-
// Build proof chain for top blockers
|
|
279
|
-
const proofChain = buildProofChain(findings, proofGraph);
|
|
280
|
-
|
|
281
|
-
// Compute coverage metrics
|
|
282
|
-
const coverage = computeCoverageMetrics({
|
|
283
|
-
truthpack,
|
|
284
|
-
realityReport,
|
|
285
|
-
proofGraph,
|
|
286
|
-
findings,
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// Runtime summary
|
|
290
|
-
const runtime = realityReport ? {
|
|
291
|
-
ran: true,
|
|
292
|
-
url: realityReport.meta?.baseUrl,
|
|
293
|
-
actionsCount: realityReport.actions?.length || 0,
|
|
294
|
-
requestsCount: realityReport.requests?.length || 0,
|
|
295
|
-
toastsDetected: realityReport.signals?.filter(s => s.kind?.startsWith("toast_")).length || 0,
|
|
296
|
-
} : { ran: false };
|
|
297
|
-
|
|
298
|
-
const report = {
|
|
299
|
-
meta: {
|
|
300
|
-
version: "2.0.0",
|
|
301
|
-
generatedAt: new Date().toISOString(),
|
|
302
|
-
repoRoot,
|
|
303
|
-
commit: {
|
|
304
|
-
sha: process.env.VIBECHECK_COMMIT_SHA || getGitCommit(repoRoot),
|
|
305
|
-
branch: process.env.VIBECHECK_BRANCH || getGitBranch(repoRoot),
|
|
306
|
-
},
|
|
307
|
-
durationMs: Date.now() - startTime,
|
|
308
|
-
truthpackHash: truthpack?.index?.hashes?.truthpackHash || null,
|
|
309
|
-
},
|
|
310
|
-
verdict: {
|
|
311
|
-
status: verdict.status,
|
|
312
|
-
exitCode: verdict.exitCode,
|
|
313
|
-
summary: verdict.summary,
|
|
314
|
-
blockReasons: verdict.blockReasons,
|
|
315
|
-
},
|
|
316
|
-
findings,
|
|
317
|
-
stats: verdict.stats,
|
|
318
|
-
proofChain,
|
|
319
|
-
coverage,
|
|
320
|
-
artifacts: {
|
|
321
|
-
truthpack: ".vibecheck/truthpack.json",
|
|
322
|
-
realityReport: realityReport ? ".vibecheck/reality/last_reality.json" : null,
|
|
323
|
-
proofGraph: proofGraph ? ".vibecheck/proof-graph.json" : null,
|
|
324
|
-
missionPack: null, // Set by fix command
|
|
325
|
-
},
|
|
326
|
-
runtime,
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
return report;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Build proof chain for top blockers
|
|
334
|
-
*/
|
|
335
|
-
function buildProofChain(findings, proofGraph) {
|
|
336
|
-
const blockers = findings
|
|
337
|
-
.filter(f => f.severity === "BLOCK")
|
|
338
|
-
.slice(0, 5);
|
|
339
|
-
|
|
340
|
-
if (!proofGraph || blockers.length === 0) {
|
|
341
|
-
return { topBlockers: [] };
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const topBlockers = blockers.map(finding => {
|
|
345
|
-
const chain = [];
|
|
346
|
-
|
|
347
|
-
// Find proof node if referenced
|
|
348
|
-
if (finding.proofNode && proofGraph.nodes) {
|
|
349
|
-
const node = proofGraph.nodes.find(n => n.id === finding.proofNode);
|
|
350
|
-
if (node) {
|
|
351
|
-
chain.push({
|
|
352
|
-
nodeType: node.type,
|
|
353
|
-
label: node.label,
|
|
354
|
-
evidence: node.data ? JSON.stringify(node.data).slice(0, 100) : null,
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// Follow edges to build chain
|
|
358
|
-
const edges = proofGraph.edges?.filter(e => e.source === node.id || e.target === node.id) || [];
|
|
359
|
-
for (const edge of edges.slice(0, 3)) {
|
|
360
|
-
const linkedNode = proofGraph.nodes.find(n => n.id === (edge.source === node.id ? edge.target : edge.source));
|
|
361
|
-
if (linkedNode) {
|
|
362
|
-
chain.push({
|
|
363
|
-
nodeType: linkedNode.type,
|
|
364
|
-
label: linkedNode.label,
|
|
365
|
-
evidence: edge.type,
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Add evidence from finding
|
|
373
|
-
if (chain.length === 0 && finding.evidence?.length > 0) {
|
|
374
|
-
const ev = finding.evidence[0];
|
|
375
|
-
chain.push({
|
|
376
|
-
nodeType: ev.kind || "file",
|
|
377
|
-
label: ev.file ? `${ev.file}:${ev.lines}` : ev.url || "unknown",
|
|
378
|
-
evidence: ev.reason,
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return {
|
|
383
|
-
findingId: finding.id,
|
|
384
|
-
title: finding.title,
|
|
385
|
-
chain,
|
|
386
|
-
};
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
return { topBlockers };
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Write ship report to disk
|
|
394
|
-
*/
|
|
395
|
-
function writeShipReport(repoRoot, report) {
|
|
396
|
-
const dir = path.join(repoRoot, ".vibecheck");
|
|
397
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
398
|
-
|
|
399
|
-
// Write last_ship.json
|
|
400
|
-
const lastShipPath = path.join(dir, "last_ship.json");
|
|
401
|
-
fs.writeFileSync(lastShipPath, JSON.stringify(report, null, 2));
|
|
402
|
-
|
|
403
|
-
// Write timestamped copy
|
|
404
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
405
|
-
const archiveDir = path.join(dir, "ship");
|
|
406
|
-
fs.mkdirSync(archiveDir, { recursive: true });
|
|
407
|
-
fs.writeFileSync(
|
|
408
|
-
path.join(archiveDir, `ship_${timestamp}.json`),
|
|
409
|
-
JSON.stringify(report, null, 2)
|
|
410
|
-
);
|
|
411
|
-
|
|
412
|
-
return lastShipPath;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// =============================================================================
|
|
416
|
-
// PR COMMENT RENDERER
|
|
417
|
-
// =============================================================================
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Render PR comment markdown from ship report
|
|
421
|
-
*/
|
|
422
|
-
function renderPRComment(report, options = {}) {
|
|
423
|
-
const { includeDetails = true, maxFindings = 10 } = options;
|
|
424
|
-
|
|
425
|
-
const lines = [];
|
|
426
|
-
|
|
427
|
-
// Header with verdict
|
|
428
|
-
const verdictEmoji = {
|
|
429
|
-
SHIP: "â
",
|
|
430
|
-
WARN: "â ī¸",
|
|
431
|
-
BLOCK: "đĢ",
|
|
432
|
-
}[report.verdict.status] || "â";
|
|
433
|
-
|
|
434
|
-
lines.push(`## ${verdictEmoji} Vibecheck: ${report.verdict.status}`);
|
|
435
|
-
lines.push("");
|
|
436
|
-
|
|
437
|
-
// Stats summary
|
|
438
|
-
const { stats } = report;
|
|
439
|
-
if (stats.total > 0) {
|
|
440
|
-
lines.push(`**Findings:** ${stats.total} total`);
|
|
441
|
-
if (stats.bySeverity.BLOCK > 0) lines.push(`- đĢ ${stats.bySeverity.BLOCK} blocker(s)`);
|
|
442
|
-
if (stats.bySeverity.WARN > 0) lines.push(`- â ī¸ ${stats.bySeverity.WARN} warning(s)`);
|
|
443
|
-
if (stats.bySeverity.INFO > 0) lines.push(`- âšī¸ ${stats.bySeverity.INFO} info`);
|
|
444
|
-
lines.push("");
|
|
445
|
-
} else {
|
|
446
|
-
lines.push("**No issues found!**");
|
|
447
|
-
lines.push("");
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Top blockers with proof chain
|
|
451
|
-
if (report.proofChain?.topBlockers?.length > 0) {
|
|
452
|
-
lines.push("### Top Blockers");
|
|
453
|
-
lines.push("");
|
|
454
|
-
|
|
455
|
-
for (const blocker of report.proofChain.topBlockers.slice(0, 3)) {
|
|
456
|
-
lines.push(`**${blocker.title}**`);
|
|
457
|
-
|
|
458
|
-
if (blocker.chain?.length > 0) {
|
|
459
|
-
const chainStr = blocker.chain
|
|
460
|
-
.map(c => `${c.nodeType}: ${c.label}`)
|
|
461
|
-
.join(" â ");
|
|
462
|
-
lines.push(`> Proof: ${chainStr}`);
|
|
463
|
-
}
|
|
464
|
-
lines.push("");
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Detailed findings (collapsible)
|
|
469
|
-
if (includeDetails && report.findings.length > 0) {
|
|
470
|
-
lines.push("<details>");
|
|
471
|
-
lines.push("<summary>All Findings</summary>");
|
|
472
|
-
lines.push("");
|
|
473
|
-
|
|
474
|
-
const shown = report.findings.slice(0, maxFindings);
|
|
475
|
-
for (const f of shown) {
|
|
476
|
-
const emoji = f.severity === "BLOCK" ? "đĢ" : f.severity === "WARN" ? "â ī¸" : "âšī¸";
|
|
477
|
-
lines.push(`- ${emoji} **${f.category}**: ${f.title}`);
|
|
478
|
-
|
|
479
|
-
if (f.evidence?.[0]?.file) {
|
|
480
|
-
lines.push(` - đ \`${f.evidence[0].file}:${f.evidence[0].lines || ""}\``);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (report.findings.length > maxFindings) {
|
|
485
|
-
lines.push("");
|
|
486
|
-
lines.push(`_...and ${report.findings.length - maxFindings} more_`);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
lines.push("");
|
|
490
|
-
lines.push("</details>");
|
|
491
|
-
lines.push("");
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Runtime coverage
|
|
495
|
-
if (report.runtime?.ran) {
|
|
496
|
-
lines.push("### Runtime Verification");
|
|
497
|
-
lines.push(`- Actions: ${report.runtime.actionsCount}`);
|
|
498
|
-
lines.push(`- Requests: ${report.runtime.requestsCount}`);
|
|
499
|
-
lines.push(`- Toasts detected: ${report.runtime.toastsDetected}`);
|
|
500
|
-
|
|
501
|
-
if (report.runtime.coverage?.percent !== undefined) {
|
|
502
|
-
lines.push(`- Route coverage: ${report.runtime.coverage.percent.toFixed(0)}%`);
|
|
503
|
-
}
|
|
504
|
-
lines.push("");
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Artifacts
|
|
508
|
-
lines.push("### Artifacts");
|
|
509
|
-
lines.push(`- đ Ship report: \`${report.artifacts?.truthpack || ".vibecheck/truthpack.json"}\``);
|
|
510
|
-
if (report.artifacts?.realityReport) {
|
|
511
|
-
lines.push(`- đ Reality report: \`${report.artifacts.realityReport}\``);
|
|
512
|
-
}
|
|
513
|
-
lines.push("");
|
|
514
|
-
|
|
515
|
-
// Footer
|
|
516
|
-
lines.push("---");
|
|
517
|
-
lines.push(`_Generated by [Vibecheck](https://github.com/guardiavault-oss/Vibecheck) at ${report.meta.generatedAt}_`);
|
|
518
|
-
|
|
519
|
-
return lines.join("\n");
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Write PR comment to file
|
|
524
|
-
*/
|
|
525
|
-
function writePRComment(repoRoot, comment) {
|
|
526
|
-
const dir = path.join(repoRoot, ".vibecheck");
|
|
527
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
528
|
-
|
|
529
|
-
const commentPath = path.join(dir, "pr_comment.md");
|
|
530
|
-
fs.writeFileSync(commentPath, comment);
|
|
531
|
-
|
|
532
|
-
return commentPath;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// =============================================================================
|
|
536
|
-
// HELPERS
|
|
537
|
-
// =============================================================================
|
|
538
|
-
|
|
539
|
-
function getGitCommit(repoRoot) {
|
|
540
|
-
try {
|
|
541
|
-
const headPath = path.join(repoRoot, ".git", "HEAD");
|
|
542
|
-
const head = fs.readFileSync(headPath, "utf8").trim();
|
|
543
|
-
|
|
544
|
-
if (head.startsWith("ref:")) {
|
|
545
|
-
const refPath = path.join(repoRoot, ".git", head.slice(5).trim());
|
|
546
|
-
return fs.readFileSync(refPath, "utf8").trim().slice(0, 12);
|
|
547
|
-
}
|
|
548
|
-
return head.slice(0, 12);
|
|
549
|
-
} catch {
|
|
550
|
-
return "unknown";
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function getGitBranch(repoRoot) {
|
|
555
|
-
try {
|
|
556
|
-
const headPath = path.join(repoRoot, ".git", "HEAD");
|
|
557
|
-
const head = fs.readFileSync(headPath, "utf8").trim();
|
|
558
|
-
|
|
559
|
-
if (head.startsWith("ref: refs/heads/")) {
|
|
560
|
-
return head.slice("ref: refs/heads/".length);
|
|
561
|
-
}
|
|
562
|
-
return "HEAD";
|
|
563
|
-
} catch {
|
|
564
|
-
return "unknown";
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// =============================================================================
|
|
569
|
-
// FINDING DEDUPLICATION
|
|
570
|
-
// =============================================================================
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* Deduplicate findings by fingerprint
|
|
574
|
-
*/
|
|
575
|
-
function deduplicateFindings(findings) {
|
|
576
|
-
const seen = new Set();
|
|
577
|
-
const deduped = [];
|
|
578
|
-
|
|
579
|
-
for (const f of findings) {
|
|
580
|
-
const fp = f.fingerprint || generateFingerprint([f.detectorId, f.title, f.evidence?.[0]?.file]);
|
|
581
|
-
if (!seen.has(fp)) {
|
|
582
|
-
seen.add(fp);
|
|
583
|
-
deduped.push(f);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
return deduped;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Merge findings from multiple sources
|
|
592
|
-
*/
|
|
593
|
-
function mergeFindings(...findingArrays) {
|
|
594
|
-
const all = [];
|
|
595
|
-
for (const arr of findingArrays) {
|
|
596
|
-
if (Array.isArray(arr)) {
|
|
597
|
-
all.push(...arr);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
return deduplicateFindings(all);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// =============================================================================
|
|
604
|
-
// EXPORTS
|
|
605
|
-
// =============================================================================
|
|
606
|
-
|
|
607
|
-
module.exports = {
|
|
608
|
-
// Verdict
|
|
609
|
-
computeVerdict,
|
|
610
|
-
generateVerdictSummary,
|
|
611
|
-
DEFAULT_COVERAGE_THRESHOLDS,
|
|
612
|
-
|
|
613
|
-
// Coverage
|
|
614
|
-
computeCoverageMetrics,
|
|
615
|
-
|
|
616
|
-
// Ship report
|
|
617
|
-
generateShipReport,
|
|
618
|
-
writeShipReport,
|
|
619
|
-
buildProofChain,
|
|
620
|
-
|
|
621
|
-
// PR comment
|
|
622
|
-
renderPRComment,
|
|
623
|
-
writePRComment,
|
|
624
|
-
|
|
625
|
-
// Finding utilities
|
|
626
|
-
deduplicateFindings,
|
|
627
|
-
mergeFindings,
|
|
628
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Verdict Engine v2
|
|
3
|
+
*
|
|
4
|
+
* Merges all findings from detectors, computes verdict, generates ship report.
|
|
5
|
+
* Single source of truth for SHIP/WARN/BLOCK decisions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const crypto = require("crypto");
|
|
13
|
+
|
|
14
|
+
const { createFinding, getSeverity, generateFingerprint } = require("./schemas/validator");
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// VERDICT COMPUTATION
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
// Coverage thresholds defaults
|
|
21
|
+
const DEFAULT_COVERAGE_THRESHOLDS = {
|
|
22
|
+
minActionCoverage: 0, // --min-action-coverage
|
|
23
|
+
minRouteCoverage: 0, // --min-route-coverage
|
|
24
|
+
requireAuthCoverage: false, // --require-auth-coverage
|
|
25
|
+
minAuthCoverage: 80, // Minimum % when requireAuthCoverage is true
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Compute verdict from findings
|
|
30
|
+
*/
|
|
31
|
+
function computeVerdict(findings, options = {}) {
|
|
32
|
+
const {
|
|
33
|
+
failOnWarn = false,
|
|
34
|
+
coverage = null,
|
|
35
|
+
thresholds = {},
|
|
36
|
+
} = options;
|
|
37
|
+
|
|
38
|
+
const effectiveThresholds = { ...DEFAULT_COVERAGE_THRESHOLDS, ...thresholds };
|
|
39
|
+
|
|
40
|
+
const stats = {
|
|
41
|
+
total: findings.length,
|
|
42
|
+
bySeverity: { BLOCK: 0, WARN: 0, INFO: 0 },
|
|
43
|
+
byCategory: {},
|
|
44
|
+
byScope: {},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const blockReasons = [];
|
|
48
|
+
|
|
49
|
+
for (const f of findings) {
|
|
50
|
+
// Count by severity
|
|
51
|
+
stats.bySeverity[f.severity] = (stats.bySeverity[f.severity] || 0) + 1;
|
|
52
|
+
|
|
53
|
+
// Count by category
|
|
54
|
+
stats.byCategory[f.category] = (stats.byCategory[f.category] || 0) + 1;
|
|
55
|
+
|
|
56
|
+
// Count by scope
|
|
57
|
+
stats.byScope[f.scope] = (stats.byScope[f.scope] || 0) + 1;
|
|
58
|
+
|
|
59
|
+
// Collect block reasons
|
|
60
|
+
if (f.severity === "BLOCK") {
|
|
61
|
+
blockReasons.push(f.title);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check coverage thresholds
|
|
66
|
+
const coverageViolations = [];
|
|
67
|
+
if (coverage) {
|
|
68
|
+
if (effectiveThresholds.minActionCoverage > 0 &&
|
|
69
|
+
coverage.uiActionsVerifiedPct < effectiveThresholds.minActionCoverage) {
|
|
70
|
+
coverageViolations.push(
|
|
71
|
+
`UI action coverage ${coverage.uiActionsVerifiedPct.toFixed(0)}% < ${effectiveThresholds.minActionCoverage}%`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (effectiveThresholds.minRouteCoverage > 0 &&
|
|
75
|
+
coverage.clientCallsMappedPct < effectiveThresholds.minRouteCoverage) {
|
|
76
|
+
coverageViolations.push(
|
|
77
|
+
`Route coverage ${coverage.clientCallsMappedPct.toFixed(0)}% < ${effectiveThresholds.minRouteCoverage}%`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (effectiveThresholds.requireAuthCoverage &&
|
|
81
|
+
coverage.authVerifiedPct !== null &&
|
|
82
|
+
coverage.authVerifiedPct < effectiveThresholds.minAuthCoverage) {
|
|
83
|
+
coverageViolations.push(
|
|
84
|
+
`Auth coverage ${coverage.authVerifiedPct.toFixed(0)}% < ${effectiveThresholds.minAuthCoverage}%`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Determine verdict
|
|
90
|
+
let status = "SHIP";
|
|
91
|
+
let exitCode = 0;
|
|
92
|
+
|
|
93
|
+
if (stats.bySeverity.BLOCK > 0 || coverageViolations.length > 0) {
|
|
94
|
+
status = "BLOCK";
|
|
95
|
+
exitCode = 2;
|
|
96
|
+
if (coverageViolations.length > 0) {
|
|
97
|
+
blockReasons.push(...coverageViolations.map(v => `Coverage: ${v}`));
|
|
98
|
+
}
|
|
99
|
+
} else if (stats.bySeverity.WARN > 0) {
|
|
100
|
+
status = "WARN";
|
|
101
|
+
exitCode = failOnWarn ? 1 : 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Generate summary
|
|
105
|
+
const summary = generateVerdictSummary(status, stats);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
status,
|
|
109
|
+
exitCode,
|
|
110
|
+
summary,
|
|
111
|
+
blockReasons: blockReasons.slice(0, 5),
|
|
112
|
+
stats,
|
|
113
|
+
coverageViolations,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate human-readable verdict summary
|
|
119
|
+
*/
|
|
120
|
+
function generateVerdictSummary(status, stats) {
|
|
121
|
+
const parts = [];
|
|
122
|
+
|
|
123
|
+
if (status === "SHIP") {
|
|
124
|
+
if (stats.bySeverity.WARN > 0) {
|
|
125
|
+
parts.push(`â
SHIP with ${stats.bySeverity.WARN} warning(s)`);
|
|
126
|
+
} else {
|
|
127
|
+
parts.push("â
SHIP - All checks passed");
|
|
128
|
+
}
|
|
129
|
+
} else if (status === "WARN") {
|
|
130
|
+
parts.push(`â ī¸ WARN - ${stats.bySeverity.WARN} warning(s) found`);
|
|
131
|
+
} else {
|
|
132
|
+
parts.push(`đĢ BLOCK - ${stats.bySeverity.BLOCK} blocker(s) found`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Add category breakdown
|
|
136
|
+
const categories = Object.entries(stats.byCategory)
|
|
137
|
+
.filter(([_, count]) => count > 0)
|
|
138
|
+
.map(([cat, count]) => `${cat}: ${count}`)
|
|
139
|
+
.join(", ");
|
|
140
|
+
|
|
141
|
+
if (categories) {
|
|
142
|
+
parts.push(`Categories: ${categories}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return parts.join("\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// =============================================================================
|
|
149
|
+
// COVERAGE METRICS
|
|
150
|
+
// =============================================================================
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Compute coverage metrics from truthpack and reality data
|
|
154
|
+
*/
|
|
155
|
+
function computeCoverageMetrics(options = {}) {
|
|
156
|
+
const { truthpack, realityReport, proofGraph, findings } = options;
|
|
157
|
+
|
|
158
|
+
const metrics = {
|
|
159
|
+
clientCalls: {
|
|
160
|
+
total: 0,
|
|
161
|
+
mappedToServerRoutes: 0,
|
|
162
|
+
mappedToServerRoutesPct: 0,
|
|
163
|
+
},
|
|
164
|
+
runtimeRequests: {
|
|
165
|
+
total: 0,
|
|
166
|
+
mappedToClientCalls: 0,
|
|
167
|
+
mappedToClientCallsPct: 0,
|
|
168
|
+
},
|
|
169
|
+
uiActions: {
|
|
170
|
+
total: 0,
|
|
171
|
+
withMeaningfulChange: 0,
|
|
172
|
+
withMeaningfulChangePct: 0,
|
|
173
|
+
},
|
|
174
|
+
auth: {
|
|
175
|
+
protectedRoutes: 0,
|
|
176
|
+
verified: 0,
|
|
177
|
+
protectedRoutesVerifiedPct: null,
|
|
178
|
+
},
|
|
179
|
+
unmappedApiRequests: [],
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Client calls coverage
|
|
183
|
+
if (truthpack?.clientCalls?.calls) {
|
|
184
|
+
metrics.clientCalls.total = truthpack.clientCalls.calls.length;
|
|
185
|
+
|
|
186
|
+
// Count those with linked server routes (from proof graph or matching)
|
|
187
|
+
if (proofGraph?.edges) {
|
|
188
|
+
const clientCallsWithMatches = new Set(
|
|
189
|
+
proofGraph.edges
|
|
190
|
+
.filter(e => e.type === "MATCHES" || e.type === "CALLS")
|
|
191
|
+
.map(e => e.from)
|
|
192
|
+
);
|
|
193
|
+
metrics.clientCalls.mappedToServerRoutes = clientCallsWithMatches.size;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
metrics.clientCalls.mappedToServerRoutesPct = metrics.clientCalls.total > 0
|
|
197
|
+
? (metrics.clientCalls.mappedToServerRoutes / metrics.clientCalls.total) * 100
|
|
198
|
+
: 100;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Runtime requests coverage
|
|
202
|
+
if (realityReport?.requests) {
|
|
203
|
+
metrics.runtimeRequests.total = realityReport.requests.length;
|
|
204
|
+
|
|
205
|
+
// Count mapped requests
|
|
206
|
+
const mapped = realityReport.requests.filter(r => r.mappedClientCallId || r.matched);
|
|
207
|
+
metrics.runtimeRequests.mappedToClientCalls = mapped.length;
|
|
208
|
+
|
|
209
|
+
metrics.runtimeRequests.mappedToClientCallsPct = metrics.runtimeRequests.total > 0
|
|
210
|
+
? (metrics.runtimeRequests.mappedToClientCalls / metrics.runtimeRequests.total) * 100
|
|
211
|
+
: 100;
|
|
212
|
+
|
|
213
|
+
// Top 5 unmapped requests
|
|
214
|
+
const unmapped = realityReport.requests
|
|
215
|
+
.filter(r => !r.mappedClientCallId && !r.matched)
|
|
216
|
+
.map(r => r.canonicalPath || r.url)
|
|
217
|
+
.filter(Boolean);
|
|
218
|
+
|
|
219
|
+
// Dedupe and take top 5
|
|
220
|
+
metrics.unmappedApiRequests = [...new Set(unmapped)].slice(0, 5);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// UI actions coverage
|
|
224
|
+
if (realityReport?.actions) {
|
|
225
|
+
metrics.uiActions.total = realityReport.actions.length;
|
|
226
|
+
|
|
227
|
+
// Count actions with meaningful UI change
|
|
228
|
+
const withChange = realityReport.actions.filter(a =>
|
|
229
|
+
a.meaningfulChange || a.uiChangeScore > 0.5
|
|
230
|
+
);
|
|
231
|
+
metrics.uiActions.withMeaningfulChange = withChange.length;
|
|
232
|
+
|
|
233
|
+
metrics.uiActions.withMeaningfulChangePct = metrics.uiActions.total > 0
|
|
234
|
+
? (metrics.uiActions.withMeaningfulChange / metrics.uiActions.total) * 100
|
|
235
|
+
: 100;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Auth coverage (only when verify-auth was used)
|
|
239
|
+
if (truthpack?.auth?.protectedPatterns?.length > 0 && realityReport?.auth) {
|
|
240
|
+
metrics.auth.protectedRoutes = truthpack.auth.protectedPatterns.length;
|
|
241
|
+
metrics.auth.verified = realityReport.auth.verified?.length || 0;
|
|
242
|
+
|
|
243
|
+
metrics.auth.protectedRoutesVerifiedPct = metrics.auth.protectedRoutes > 0
|
|
244
|
+
? (metrics.auth.verified / metrics.auth.protectedRoutes) * 100
|
|
245
|
+
: null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Flattened percentage fields for easy access
|
|
249
|
+
return {
|
|
250
|
+
...metrics,
|
|
251
|
+
clientCallsMappedPct: metrics.clientCalls.mappedToServerRoutesPct,
|
|
252
|
+
runtimeRequestsMappedPct: metrics.runtimeRequests.mappedToClientCallsPct,
|
|
253
|
+
uiActionsVerifiedPct: metrics.uiActions.withMeaningfulChangePct,
|
|
254
|
+
authVerifiedPct: metrics.auth.protectedRoutesVerifiedPct,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// =============================================================================
|
|
259
|
+
// SHIP REPORT GENERATION
|
|
260
|
+
// =============================================================================
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Generate ship report from findings and metadata
|
|
264
|
+
*/
|
|
265
|
+
function generateShipReport(options = {}) {
|
|
266
|
+
const {
|
|
267
|
+
findings = [],
|
|
268
|
+
truthpack = null,
|
|
269
|
+
realityReport = null,
|
|
270
|
+
proofGraph = null,
|
|
271
|
+
repoRoot = process.cwd(),
|
|
272
|
+
failOnWarn = false,
|
|
273
|
+
startTime = Date.now(),
|
|
274
|
+
} = options;
|
|
275
|
+
|
|
276
|
+
const verdict = computeVerdict(findings, { failOnWarn });
|
|
277
|
+
|
|
278
|
+
// Build proof chain for top blockers
|
|
279
|
+
const proofChain = buildProofChain(findings, proofGraph);
|
|
280
|
+
|
|
281
|
+
// Compute coverage metrics
|
|
282
|
+
const coverage = computeCoverageMetrics({
|
|
283
|
+
truthpack,
|
|
284
|
+
realityReport,
|
|
285
|
+
proofGraph,
|
|
286
|
+
findings,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Runtime summary
|
|
290
|
+
const runtime = realityReport ? {
|
|
291
|
+
ran: true,
|
|
292
|
+
url: realityReport.meta?.baseUrl,
|
|
293
|
+
actionsCount: realityReport.actions?.length || 0,
|
|
294
|
+
requestsCount: realityReport.requests?.length || 0,
|
|
295
|
+
toastsDetected: realityReport.signals?.filter(s => s.kind?.startsWith("toast_")).length || 0,
|
|
296
|
+
} : { ran: false };
|
|
297
|
+
|
|
298
|
+
const report = {
|
|
299
|
+
meta: {
|
|
300
|
+
version: "2.0.0",
|
|
301
|
+
generatedAt: new Date().toISOString(),
|
|
302
|
+
repoRoot,
|
|
303
|
+
commit: {
|
|
304
|
+
sha: process.env.VIBECHECK_COMMIT_SHA || getGitCommit(repoRoot),
|
|
305
|
+
branch: process.env.VIBECHECK_BRANCH || getGitBranch(repoRoot),
|
|
306
|
+
},
|
|
307
|
+
durationMs: Date.now() - startTime,
|
|
308
|
+
truthpackHash: truthpack?.index?.hashes?.truthpackHash || null,
|
|
309
|
+
},
|
|
310
|
+
verdict: {
|
|
311
|
+
status: verdict.status,
|
|
312
|
+
exitCode: verdict.exitCode,
|
|
313
|
+
summary: verdict.summary,
|
|
314
|
+
blockReasons: verdict.blockReasons,
|
|
315
|
+
},
|
|
316
|
+
findings,
|
|
317
|
+
stats: verdict.stats,
|
|
318
|
+
proofChain,
|
|
319
|
+
coverage,
|
|
320
|
+
artifacts: {
|
|
321
|
+
truthpack: ".vibecheck/truthpack.json",
|
|
322
|
+
realityReport: realityReport ? ".vibecheck/reality/last_reality.json" : null,
|
|
323
|
+
proofGraph: proofGraph ? ".vibecheck/proof-graph.json" : null,
|
|
324
|
+
missionPack: null, // Set by fix command
|
|
325
|
+
},
|
|
326
|
+
runtime,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
return report;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Build proof chain for top blockers
|
|
334
|
+
*/
|
|
335
|
+
function buildProofChain(findings, proofGraph) {
|
|
336
|
+
const blockers = findings
|
|
337
|
+
.filter(f => f.severity === "BLOCK")
|
|
338
|
+
.slice(0, 5);
|
|
339
|
+
|
|
340
|
+
if (!proofGraph || blockers.length === 0) {
|
|
341
|
+
return { topBlockers: [] };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const topBlockers = blockers.map(finding => {
|
|
345
|
+
const chain = [];
|
|
346
|
+
|
|
347
|
+
// Find proof node if referenced
|
|
348
|
+
if (finding.proofNode && proofGraph.nodes) {
|
|
349
|
+
const node = proofGraph.nodes.find(n => n.id === finding.proofNode);
|
|
350
|
+
if (node) {
|
|
351
|
+
chain.push({
|
|
352
|
+
nodeType: node.type,
|
|
353
|
+
label: node.label,
|
|
354
|
+
evidence: node.data ? JSON.stringify(node.data).slice(0, 100) : null,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Follow edges to build chain
|
|
358
|
+
const edges = proofGraph.edges?.filter(e => e.source === node.id || e.target === node.id) || [];
|
|
359
|
+
for (const edge of edges.slice(0, 3)) {
|
|
360
|
+
const linkedNode = proofGraph.nodes.find(n => n.id === (edge.source === node.id ? edge.target : edge.source));
|
|
361
|
+
if (linkedNode) {
|
|
362
|
+
chain.push({
|
|
363
|
+
nodeType: linkedNode.type,
|
|
364
|
+
label: linkedNode.label,
|
|
365
|
+
evidence: edge.type,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Add evidence from finding
|
|
373
|
+
if (chain.length === 0 && finding.evidence?.length > 0) {
|
|
374
|
+
const ev = finding.evidence[0];
|
|
375
|
+
chain.push({
|
|
376
|
+
nodeType: ev.kind || "file",
|
|
377
|
+
label: ev.file ? `${ev.file}:${ev.lines}` : ev.url || "unknown",
|
|
378
|
+
evidence: ev.reason,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
findingId: finding.id,
|
|
384
|
+
title: finding.title,
|
|
385
|
+
chain,
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return { topBlockers };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Write ship report to disk
|
|
394
|
+
*/
|
|
395
|
+
function writeShipReport(repoRoot, report) {
|
|
396
|
+
const dir = path.join(repoRoot, ".vibecheck");
|
|
397
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
398
|
+
|
|
399
|
+
// Write last_ship.json
|
|
400
|
+
const lastShipPath = path.join(dir, "last_ship.json");
|
|
401
|
+
fs.writeFileSync(lastShipPath, JSON.stringify(report, null, 2));
|
|
402
|
+
|
|
403
|
+
// Write timestamped copy
|
|
404
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
405
|
+
const archiveDir = path.join(dir, "ship");
|
|
406
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
407
|
+
fs.writeFileSync(
|
|
408
|
+
path.join(archiveDir, `ship_${timestamp}.json`),
|
|
409
|
+
JSON.stringify(report, null, 2)
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
return lastShipPath;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// =============================================================================
|
|
416
|
+
// PR COMMENT RENDERER
|
|
417
|
+
// =============================================================================
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Render PR comment markdown from ship report
|
|
421
|
+
*/
|
|
422
|
+
function renderPRComment(report, options = {}) {
|
|
423
|
+
const { includeDetails = true, maxFindings = 10 } = options;
|
|
424
|
+
|
|
425
|
+
const lines = [];
|
|
426
|
+
|
|
427
|
+
// Header with verdict
|
|
428
|
+
const verdictEmoji = {
|
|
429
|
+
SHIP: "â
",
|
|
430
|
+
WARN: "â ī¸",
|
|
431
|
+
BLOCK: "đĢ",
|
|
432
|
+
}[report.verdict.status] || "â";
|
|
433
|
+
|
|
434
|
+
lines.push(`## ${verdictEmoji} Vibecheck: ${report.verdict.status}`);
|
|
435
|
+
lines.push("");
|
|
436
|
+
|
|
437
|
+
// Stats summary
|
|
438
|
+
const { stats } = report;
|
|
439
|
+
if (stats.total > 0) {
|
|
440
|
+
lines.push(`**Findings:** ${stats.total} total`);
|
|
441
|
+
if (stats.bySeverity.BLOCK > 0) lines.push(`- đĢ ${stats.bySeverity.BLOCK} blocker(s)`);
|
|
442
|
+
if (stats.bySeverity.WARN > 0) lines.push(`- â ī¸ ${stats.bySeverity.WARN} warning(s)`);
|
|
443
|
+
if (stats.bySeverity.INFO > 0) lines.push(`- âšī¸ ${stats.bySeverity.INFO} info`);
|
|
444
|
+
lines.push("");
|
|
445
|
+
} else {
|
|
446
|
+
lines.push("**No issues found!**");
|
|
447
|
+
lines.push("");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Top blockers with proof chain
|
|
451
|
+
if (report.proofChain?.topBlockers?.length > 0) {
|
|
452
|
+
lines.push("### Top Blockers");
|
|
453
|
+
lines.push("");
|
|
454
|
+
|
|
455
|
+
for (const blocker of report.proofChain.topBlockers.slice(0, 3)) {
|
|
456
|
+
lines.push(`**${blocker.title}**`);
|
|
457
|
+
|
|
458
|
+
if (blocker.chain?.length > 0) {
|
|
459
|
+
const chainStr = blocker.chain
|
|
460
|
+
.map(c => `${c.nodeType}: ${c.label}`)
|
|
461
|
+
.join(" â ");
|
|
462
|
+
lines.push(`> Proof: ${chainStr}`);
|
|
463
|
+
}
|
|
464
|
+
lines.push("");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Detailed findings (collapsible)
|
|
469
|
+
if (includeDetails && report.findings.length > 0) {
|
|
470
|
+
lines.push("<details>");
|
|
471
|
+
lines.push("<summary>All Findings</summary>");
|
|
472
|
+
lines.push("");
|
|
473
|
+
|
|
474
|
+
const shown = report.findings.slice(0, maxFindings);
|
|
475
|
+
for (const f of shown) {
|
|
476
|
+
const emoji = f.severity === "BLOCK" ? "đĢ" : f.severity === "WARN" ? "â ī¸" : "âšī¸";
|
|
477
|
+
lines.push(`- ${emoji} **${f.category}**: ${f.title}`);
|
|
478
|
+
|
|
479
|
+
if (f.evidence?.[0]?.file) {
|
|
480
|
+
lines.push(` - đ \`${f.evidence[0].file}:${f.evidence[0].lines || ""}\``);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (report.findings.length > maxFindings) {
|
|
485
|
+
lines.push("");
|
|
486
|
+
lines.push(`_...and ${report.findings.length - maxFindings} more_`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
lines.push("");
|
|
490
|
+
lines.push("</details>");
|
|
491
|
+
lines.push("");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Runtime coverage
|
|
495
|
+
if (report.runtime?.ran) {
|
|
496
|
+
lines.push("### Runtime Verification");
|
|
497
|
+
lines.push(`- Actions: ${report.runtime.actionsCount}`);
|
|
498
|
+
lines.push(`- Requests: ${report.runtime.requestsCount}`);
|
|
499
|
+
lines.push(`- Toasts detected: ${report.runtime.toastsDetected}`);
|
|
500
|
+
|
|
501
|
+
if (report.runtime.coverage?.percent !== undefined) {
|
|
502
|
+
lines.push(`- Route coverage: ${report.runtime.coverage.percent.toFixed(0)}%`);
|
|
503
|
+
}
|
|
504
|
+
lines.push("");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Artifacts
|
|
508
|
+
lines.push("### Artifacts");
|
|
509
|
+
lines.push(`- đ Ship report: \`${report.artifacts?.truthpack || ".vibecheck/truthpack.json"}\``);
|
|
510
|
+
if (report.artifacts?.realityReport) {
|
|
511
|
+
lines.push(`- đ Reality report: \`${report.artifacts.realityReport}\``);
|
|
512
|
+
}
|
|
513
|
+
lines.push("");
|
|
514
|
+
|
|
515
|
+
// Footer
|
|
516
|
+
lines.push("---");
|
|
517
|
+
lines.push(`_Generated by [Vibecheck](https://github.com/guardiavault-oss/Vibecheck) at ${report.meta.generatedAt}_`);
|
|
518
|
+
|
|
519
|
+
return lines.join("\n");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Write PR comment to file
|
|
524
|
+
*/
|
|
525
|
+
function writePRComment(repoRoot, comment) {
|
|
526
|
+
const dir = path.join(repoRoot, ".vibecheck");
|
|
527
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
528
|
+
|
|
529
|
+
const commentPath = path.join(dir, "pr_comment.md");
|
|
530
|
+
fs.writeFileSync(commentPath, comment);
|
|
531
|
+
|
|
532
|
+
return commentPath;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// =============================================================================
|
|
536
|
+
// HELPERS
|
|
537
|
+
// =============================================================================
|
|
538
|
+
|
|
539
|
+
function getGitCommit(repoRoot) {
|
|
540
|
+
try {
|
|
541
|
+
const headPath = path.join(repoRoot, ".git", "HEAD");
|
|
542
|
+
const head = fs.readFileSync(headPath, "utf8").trim();
|
|
543
|
+
|
|
544
|
+
if (head.startsWith("ref:")) {
|
|
545
|
+
const refPath = path.join(repoRoot, ".git", head.slice(5).trim());
|
|
546
|
+
return fs.readFileSync(refPath, "utf8").trim().slice(0, 12);
|
|
547
|
+
}
|
|
548
|
+
return head.slice(0, 12);
|
|
549
|
+
} catch {
|
|
550
|
+
return "unknown";
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function getGitBranch(repoRoot) {
|
|
555
|
+
try {
|
|
556
|
+
const headPath = path.join(repoRoot, ".git", "HEAD");
|
|
557
|
+
const head = fs.readFileSync(headPath, "utf8").trim();
|
|
558
|
+
|
|
559
|
+
if (head.startsWith("ref: refs/heads/")) {
|
|
560
|
+
return head.slice("ref: refs/heads/".length);
|
|
561
|
+
}
|
|
562
|
+
return "HEAD";
|
|
563
|
+
} catch {
|
|
564
|
+
return "unknown";
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// =============================================================================
|
|
569
|
+
// FINDING DEDUPLICATION
|
|
570
|
+
// =============================================================================
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Deduplicate findings by fingerprint
|
|
574
|
+
*/
|
|
575
|
+
function deduplicateFindings(findings) {
|
|
576
|
+
const seen = new Set();
|
|
577
|
+
const deduped = [];
|
|
578
|
+
|
|
579
|
+
for (const f of findings) {
|
|
580
|
+
const fp = f.fingerprint || generateFingerprint([f.detectorId, f.title, f.evidence?.[0]?.file]);
|
|
581
|
+
if (!seen.has(fp)) {
|
|
582
|
+
seen.add(fp);
|
|
583
|
+
deduped.push(f);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return deduped;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Merge findings from multiple sources
|
|
592
|
+
*/
|
|
593
|
+
function mergeFindings(...findingArrays) {
|
|
594
|
+
const all = [];
|
|
595
|
+
for (const arr of findingArrays) {
|
|
596
|
+
if (Array.isArray(arr)) {
|
|
597
|
+
all.push(...arr);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return deduplicateFindings(all);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// =============================================================================
|
|
604
|
+
// EXPORTS
|
|
605
|
+
// =============================================================================
|
|
606
|
+
|
|
607
|
+
module.exports = {
|
|
608
|
+
// Verdict
|
|
609
|
+
computeVerdict,
|
|
610
|
+
generateVerdictSummary,
|
|
611
|
+
DEFAULT_COVERAGE_THRESHOLDS,
|
|
612
|
+
|
|
613
|
+
// Coverage
|
|
614
|
+
computeCoverageMetrics,
|
|
615
|
+
|
|
616
|
+
// Ship report
|
|
617
|
+
generateShipReport,
|
|
618
|
+
writeShipReport,
|
|
619
|
+
buildProofChain,
|
|
620
|
+
|
|
621
|
+
// PR comment
|
|
622
|
+
renderPRComment,
|
|
623
|
+
writePRComment,
|
|
624
|
+
|
|
625
|
+
// Finding utilities
|
|
626
|
+
deduplicateFindings,
|
|
627
|
+
mergeFindings,
|
|
628
|
+
};
|