@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.
Files changed (36) hide show
  1. package/bin/registry.js +106 -116
  2. package/bin/runners/context/generators/mcp.js +18 -0
  3. package/bin/runners/context/index.js +72 -4
  4. package/bin/runners/context/proof-context.js +293 -1
  5. package/bin/runners/context/security-scanner.js +311 -73
  6. package/bin/runners/lib/analyzers.js +607 -20
  7. package/bin/runners/lib/detectors-v2.js +172 -15
  8. package/bin/runners/lib/entitlements-v2.js +48 -1
  9. package/bin/runners/lib/evidence-pack.js +678 -0
  10. package/bin/runners/lib/html-proof-report.js +913 -0
  11. package/bin/runners/lib/missions/plan.js +231 -41
  12. package/bin/runners/lib/missions/templates.js +125 -0
  13. package/bin/runners/lib/scan-output.js +492 -253
  14. package/bin/runners/lib/ship-output.js +901 -641
  15. package/bin/runners/runCheckpoint.js +44 -3
  16. package/bin/runners/runContext.d.ts +4 -0
  17. package/bin/runners/runDoctor.js +10 -2
  18. package/bin/runners/runFix.js +51 -341
  19. package/bin/runners/runInit.js +11 -0
  20. package/bin/runners/runPolish.d.ts +4 -0
  21. package/bin/runners/runPolish.js +608 -29
  22. package/bin/runners/runProve.js +210 -25
  23. package/bin/runners/runReality.js +846 -101
  24. package/bin/runners/runScan.js +238 -4
  25. package/bin/runners/runShip.js +19 -3
  26. package/bin/runners/runWatch.js +14 -1
  27. package/bin/vibecheck.js +32 -2
  28. package/mcp-server/consolidated-tools.js +408 -42
  29. package/mcp-server/index.js +152 -15
  30. package/mcp-server/proof-tools.js +571 -0
  31. package/mcp-server/tier-auth.js +22 -19
  32. package/mcp-server/tools-v3.js +744 -0
  33. package/mcp-server/truth-firewall-tools.js +190 -4
  34. package/package.json +3 -1
  35. package/bin/runners/runInstall.js +0 -281
  36. package/bin/runners/runLabs.js +0 -341
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Scan Output - Premium Scan Results Display
2
+ * Scan Output - World-Class Premium Terminal Display
3
3
  *
4
- * Handles all scan output formatting:
5
- * - Layer status display
6
- * - Findings grouped by category
7
- * - Coverage maps
4
+ * Features:
5
+ * - Beautiful ASCII tables with box drawing
6
+ * - Color-coded severity with gradients
7
+ * - Progress bars and sparklines
8
+ * - Category breakdown with visual hierarchy
8
9
  * - JSON/SARIF export
9
10
  */
10
11
 
@@ -24,195 +25,248 @@ const {
24
25
  } = require('./terminal-ui');
25
26
 
26
27
  // ═══════════════════════════════════════════════════════════════════════════════
27
- // LAYER STATUS DISPLAY
28
+ // ENHANCED COLOR PALETTE
28
29
  // ═══════════════════════════════════════════════════════════════════════════════
29
30
 
30
- function renderLayers(layers) {
31
- const lines = [];
32
- lines.push(renderSection('ANALYSIS LAYERS', '⚡'));
33
- lines.push('');
34
-
35
- const layerConfig = {
36
- ast: { name: 'AST Analysis', icon: '🔍', desc: 'Static code parsing' },
37
- truth: { name: 'Build Truth', icon: '📦', desc: 'Manifest verification' },
38
- reality: { name: 'Reality Check', icon: '🎭', desc: 'Playwright runtime' },
39
- realitySniff: { name: 'Reality Sniff', icon: '🔬', desc: 'AI artifact detection' },
40
- detection: { name: 'Detection Engines', icon: '🛡️', desc: 'Security patterns' },
41
- };
42
-
43
- for (const layer of layers) {
44
- const config = layerConfig[layer.name] || { name: layer.name, icon: '○', desc: '' };
45
-
46
- let status, statusColor;
47
- if (layer.skipped) {
48
- status = `${ansi.dim}○ skipped${ansi.reset}`;
49
- } else if (layer.error) {
50
- status = `${colors.error}${icons.error} error${ansi.reset}`;
51
- } else {
52
- status = `${colors.success}${icons.success}${ansi.reset}`;
53
- }
54
-
55
- const duration = layer.duration ? `${ansi.dim}${layer.duration}ms${ansi.reset}` : '';
56
- const findings = layer.findings !== undefined ? `${colors.accent}${layer.findings}${ansi.reset} ${ansi.dim}findings${ansi.reset}` : '';
57
-
58
- lines.push(` ${status} ${config.icon} ${config.name.padEnd(20)} ${duration.padEnd(15)} ${findings}`);
59
- }
60
-
61
- return lines.join('\n');
62
- }
31
+ const palette = {
32
+ // Severity gradient
33
+ critical: ansi.rgb(255, 50, 50),
34
+ criticalBg: ansi.bgRgb(180, 20, 20),
35
+ high: ansi.rgb(255, 120, 50),
36
+ highBg: ansi.bgRgb(180, 80, 20),
37
+ medium: ansi.rgb(255, 200, 50),
38
+ mediumBg: ansi.bgRgb(150, 120, 20),
39
+ low: ansi.rgb(100, 180, 255),
40
+ lowBg: ansi.bgRgb(40, 80, 150),
41
+
42
+ // Status colors
43
+ pass: ansi.rgb(50, 255, 120),
44
+ passBg: ansi.bgRgb(20, 120, 50),
45
+ warn: ansi.rgb(255, 200, 50),
46
+ warnBg: ansi.bgRgb(150, 120, 20),
47
+ fail: ansi.rgb(255, 50, 50),
48
+ failBg: ansi.bgRgb(180, 20, 20),
49
+
50
+ // UI elements
51
+ border: ansi.rgb(70, 70, 90),
52
+ borderBright: ansi.rgb(100, 100, 130),
53
+ header: ansi.rgb(150, 150, 200),
54
+ muted: ansi.rgb(100, 100, 120),
55
+ accent: ansi.rgb(100, 200, 255),
56
+ highlight: ansi.rgb(255, 220, 100),
57
+
58
+ // Category colors (rainbow)
59
+ cat1: ansi.rgb(255, 100, 100),
60
+ cat2: ansi.rgb(255, 150, 100),
61
+ cat3: ansi.rgb(255, 200, 100),
62
+ cat4: ansi.rgb(200, 255, 100),
63
+ cat5: ansi.rgb(100, 255, 150),
64
+ cat6: ansi.rgb(100, 255, 200),
65
+ cat7: ansi.rgb(100, 200, 255),
66
+ cat8: ansi.rgb(100, 150, 255),
67
+ cat9: ansi.rgb(150, 100, 255),
68
+ cat10: ansi.rgb(200, 100, 255),
69
+ cat11: ansi.rgb(255, 100, 200),
70
+ cat12: ansi.rgb(255, 100, 150),
71
+ };
63
72
 
64
73
  // ═══════════════════════════════════════════════════════════════════════════════
65
- // COVERAGE MAP DISPLAY
74
+ // BOX DRAWING HELPERS
66
75
  // ═══════════════════════════════════════════════════════════════════════════════
67
76
 
68
- function renderCoverageMap(coverage) {
69
- if (!coverage) return '';
70
-
71
- const lines = [];
72
- lines.push(renderSection('ROUTE COVERAGE', '🗺️'));
73
- lines.push('');
74
-
75
- const pct = coverage.coveragePercent || 0;
76
- const color = pct >= 80 ? colors.success : pct >= 60 ? colors.warning : colors.error;
77
-
78
- // Coverage bar
79
- const barWidth = 50;
80
- const filled = Math.round((pct / 100) * barWidth);
81
- const bar = `${color}${'█'.repeat(filled)}${ansi.dim}${'░'.repeat(barWidth - filled)}${ansi.reset}`;
82
-
83
- lines.push(` ${color}${ansi.bold}${pct}%${ansi.reset} ${ansi.dim}of routes reachable from${ansi.reset} ${colors.accent}/${ansi.reset}`);
84
- lines.push(` ${bar}`);
85
- lines.push('');
86
-
87
- // Stats
88
- const stats = [
89
- ['Total Routes', coverage.totalRoutes || 0],
90
- ['Reachable', coverage.reachableFromRoot || 0],
91
- ['Orphaned', coverage.orphanedRoutes || 0],
92
- ['Dead Links', coverage.deadLinks || 0],
93
- ];
94
-
95
- for (const [label, value] of stats) {
96
- const valueColor = label === 'Orphaned' || label === 'Dead Links'
97
- ? (value > 0 ? colors.error : colors.success)
98
- : ansi.reset;
99
- lines.push(` ${ansi.dim}${label}:${ansi.reset} ${valueColor}${ansi.bold}${value}${ansi.reset}`);
100
- }
101
-
102
- // Isolated clusters
103
- if (coverage.isolatedClusters?.length > 0) {
104
- lines.push('');
105
- lines.push(` ${colors.warning}${icons.warning}${ansi.reset} ${ansi.dim}Isolated clusters:${ansi.reset}`);
106
- for (const cluster of coverage.isolatedClusters.slice(0, 3)) {
107
- const auth = cluster.requiresAuth ? ` ${ansi.dim}(auth required)${ansi.reset}` : '';
108
- lines.push(` ${ansi.dim}├─${ansi.reset} ${ansi.bold}${cluster.name}${ansi.reset}${auth} ${ansi.dim}(${cluster.nodeIds?.length || 0} routes)${ansi.reset}`);
109
- }
110
- if (coverage.isolatedClusters.length > 3) {
111
- lines.push(` ${ansi.dim}└─ ... and ${coverage.isolatedClusters.length - 3} more clusters${ansi.reset}`);
112
- }
113
- }
114
-
115
- // Unreachable routes
116
- if (coverage.unreachableRoutes?.length > 0) {
117
- lines.push('');
118
- lines.push(` ${colors.error}${icons.error}${ansi.reset} ${ansi.dim}Unreachable routes:${ansi.reset}`);
119
- for (const route of coverage.unreachableRoutes.slice(0, 5)) {
120
- lines.push(` ${ansi.dim}├─${ansi.reset} ${colors.error}${route}${ansi.reset}`);
121
- }
122
- if (coverage.unreachableRoutes.length > 5) {
123
- lines.push(` ${ansi.dim}└─ ... and ${coverage.unreachableRoutes.length - 5} more${ansi.reset}`);
124
- }
125
- }
126
-
127
- return lines.join('\n');
77
+ const B = {
78
+ // Heavy box
79
+ tl: '┏', tr: '┓', bl: '┗', br: '┛',
80
+ h: '━', v: '┃',
81
+ lT: '┣', rT: '┫', tT: '', bT: '┻', x: '╋',
82
+
83
+ // Light box
84
+ ltl: '┌', ltr: '┐', lbl: '└', lbr: '┘',
85
+ lh: '─', lv: '│',
86
+
87
+ // Double box
88
+ dtl: '╔', dtr: '╗', dbl: '╚', dbr: '╝',
89
+ dh: '═', dv: '║',
90
+
91
+ // Rounded
92
+ rtl: '╭', rtr: '╮', rbl: '╰', rbr: '╯',
93
+
94
+ // Blocks
95
+ full: '█', light: '░', medium: '▒', dark: '▓',
96
+ left: '▌', right: '▐', top: '▀', bottom: '▄',
97
+
98
+ // Progress
99
+ pb: ['', '▒', '▓', '█'],
100
+ spark: ['', '▂', '▃', '▄', '▅', '▆', '▇', '█'],
101
+ };
102
+
103
+ function repeatChar(char, n) {
104
+ return char.repeat(Math.max(0, n));
105
+ }
106
+
107
+ function padCenter(str, width) {
108
+ const len = stripAnsi(str).length;
109
+ const left = Math.floor((width - len) / 2);
110
+ const right = width - len - left;
111
+ return ' '.repeat(left) + str + ' '.repeat(right);
112
+ }
113
+
114
+ function stripAnsi(str) {
115
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
128
116
  }
129
117
 
130
118
  // ═══════════════════════════════════════════════════════════════════════════════
131
- // BREAKDOWN DISPLAY
119
+ // PREMIUM SCORE DISPLAY
132
120
  // ═══════════════════════════════════════════════════════════════════════════════
133
121
 
134
- function renderBreakdown(breakdown) {
135
- if (!breakdown) return '';
136
-
122
+ function renderPremiumScoreCard(score, options = {}) {
123
+ const { verdict = 'BLOCK', findings = {}, duration, cached } = options;
137
124
  const lines = [];
138
- lines.push(renderSection('SCORE BREAKDOWN', '📊'));
139
- lines.push('');
140
125
 
141
- const items = [
142
- { key: 'deadLinks', label: 'Dead Links', icon: '🔗' },
143
- { key: 'orphanRoutes', label: 'Orphan Routes', icon: '👻' },
144
- { key: 'runtimeFailures', label: 'Runtime 404s', icon: '💥' },
145
- { key: 'unresolvedDynamic', label: 'Unresolved Dynamic', icon: '❓' },
146
- { key: 'placeholders', label: 'Placeholders', icon: '📝' },
147
- { key: 'secretsExposed', label: 'Secrets Exposed', icon: '🔐' },
148
- { key: 'authBypass', label: 'Auth Bypass', icon: '🚪' },
149
- { key: 'mockData', label: 'Mock Data', icon: '🎭' },
150
- ];
126
+ const width = 70;
127
+ const innerWidth = width - 4;
128
+
129
+ // Determine colors based on verdict/score
130
+ let verdictColor, verdictBg, gradientColors;
131
+ if (verdict === 'SHIP' || verdict === 'PASS' || score >= 80) {
132
+ verdictColor = palette.pass;
133
+ verdictBg = palette.passBg;
134
+ gradientColors = [ansi.rgb(50, 200, 100), ansi.rgb(100, 255, 150), ansi.rgb(150, 255, 200)];
135
+ } else if (verdict === 'WARN' || score >= 50) {
136
+ verdictColor = palette.warn;
137
+ verdictBg = palette.warnBg;
138
+ gradientColors = [ansi.rgb(200, 150, 50), ansi.rgb(255, 200, 100), ansi.rgb(255, 220, 150)];
139
+ } else {
140
+ verdictColor = palette.fail;
141
+ verdictBg = palette.failBg;
142
+ gradientColors = [ansi.rgb(200, 50, 50), ansi.rgb(255, 100, 100), ansi.rgb(255, 150, 150)];
143
+ }
151
144
 
152
- for (const item of items) {
153
- const data = breakdown[item.key];
154
- if (!data && data !== 0) continue;
155
-
156
- const count = typeof data === 'object' ? data.count : data;
157
- const penalty = typeof data === 'object' ? data.penalty : 0;
158
-
159
- const status = count === 0 ? `${colors.success}${icons.success}${ansi.reset}` : `${colors.error}${icons.error}${ansi.reset}`;
160
- const countColor = count === 0 ? colors.success : colors.error;
161
- const penaltyStr = penalty > 0 ? `${ansi.dim}-${penalty} pts${ansi.reset}` : `${ansi.dim} ---${ansi.reset}`;
162
-
163
- lines.push(` ${status} ${item.icon} ${item.label.padEnd(22)} ${countColor}${ansi.bold}${String(count).padStart(3)}${ansi.reset} ${penaltyStr}`);
145
+ // Top border with gradient
146
+ lines.push('');
147
+ lines.push(` ${palette.border}${B.dtl}${B.dh.repeat(width - 2)}${B.dtr}${ansi.reset}`);
148
+
149
+ // Score display
150
+ const scoreStr = String(score).padStart(3);
151
+ const gradeStr = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F';
152
+ const gradeColor = score >= 80 ? palette.pass : score >= 60 ? palette.warn : palette.fail;
153
+
154
+ lines.push(` ${palette.border}${B.dv}${ansi.reset}${' '.repeat(innerWidth)}${palette.border}${B.dv}${ansi.reset}`);
155
+
156
+ // Big score
157
+ const scoreLine = `${palette.muted}SCORE${ansi.reset} ${verdictColor}${ansi.bold}${scoreStr}${ansi.reset}${palette.muted}/100${ansi.reset} ${palette.muted}GRADE${ansi.reset} ${gradeColor}${ansi.bold}${gradeStr}${ansi.reset}`;
158
+ lines.push(` ${palette.border}${B.dv}${ansi.reset} ${scoreLine}${' '.repeat(14)}${palette.border}${B.dv}${ansi.reset}`);
159
+
160
+ lines.push(` ${palette.border}${B.dv}${ansi.reset}${' '.repeat(innerWidth)}${palette.border}${B.dv}${ansi.reset}`);
161
+
162
+ // Progress bar
163
+ const barWidth = 40;
164
+ const filled = Math.round((score / 100) * barWidth);
165
+ let progressBar = '';
166
+ for (let i = 0; i < barWidth; i++) {
167
+ if (i < filled) {
168
+ // Gradient fill
169
+ const gradientIdx = Math.floor((i / filled) * gradientColors.length);
170
+ progressBar += `${gradientColors[Math.min(gradientIdx, gradientColors.length - 1)]}${B.full}${ansi.reset}`;
171
+ } else {
172
+ progressBar += `${palette.muted}${B.light}${ansi.reset}`;
173
+ }
174
+ }
175
+ lines.push(` ${palette.border}${B.dv}${ansi.reset} ${progressBar}${' '.repeat(innerWidth - barWidth - 4)}${palette.border}${B.dv}${ansi.reset}`);
176
+
177
+ lines.push(` ${palette.border}${B.dv}${ansi.reset}${' '.repeat(innerWidth)}${palette.border}${B.dv}${ansi.reset}`);
178
+
179
+ // Verdict badge
180
+ const verdictText = verdict === 'SHIP' ? ' ✓ SHIP ' : verdict === 'WARN' ? ' ⚠ WARN ' : ' ✗ BLOCK ';
181
+ const verdictDesc = verdict === 'SHIP' ? 'Ready to ship!' : verdict === 'WARN' ? 'Review before shipping' : 'Fix issues before shipping';
182
+ const badgeLine = `${verdictBg}${ansi.bold}${verdictText}${ansi.reset} ${palette.muted}${verdictDesc}${ansi.reset}`;
183
+ lines.push(` ${palette.border}${B.dv}${ansi.reset} ${badgeLine} ${palette.border}${B.dv}${ansi.reset}`);
184
+
185
+ lines.push(` ${palette.border}${B.dv}${ansi.reset}${' '.repeat(innerWidth)}${palette.border}${B.dv}${ansi.reset}`);
186
+
187
+ // Findings summary
188
+ const { critical = 0, high = 0, medium = 0, low = 0 } = findings;
189
+ const critStr = `${palette.critical}${critical}${ansi.reset} critical`;
190
+ const highStr = `${palette.high}${high}${ansi.reset} high`;
191
+ const medStr = `${palette.medium}${medium}${ansi.reset} medium`;
192
+ const lowStr = `${palette.low}${low}${ansi.reset} low`;
193
+ const findingsLine = ` ${critStr} ${palette.muted}│${ansi.reset} ${highStr} ${palette.muted}│${ansi.reset} ${medStr} ${palette.muted}│${ansi.reset} ${lowStr}`;
194
+ lines.push(` ${palette.border}${B.dv}${ansi.reset}${findingsLine}${' '.repeat(9)}${palette.border}${B.dv}${ansi.reset}`);
195
+
196
+ lines.push(` ${palette.border}${B.dv}${ansi.reset}${' '.repeat(innerWidth)}${palette.border}${B.dv}${ansi.reset}`);
197
+
198
+ // Duration
199
+ if (duration) {
200
+ const durationStr = `${palette.muted}Completed in ${formatDuration(duration)}${cached ? ' (cached)' : ''}${ansi.reset}`;
201
+ lines.push(` ${palette.border}${B.dv}${ansi.reset} ${durationStr}${' '.repeat(innerWidth - stripAnsi(durationStr).length - 4)}${palette.border}${B.dv}${ansi.reset}`);
202
+ lines.push(` ${palette.border}${B.dv}${ansi.reset}${' '.repeat(innerWidth)}${palette.border}${B.dv}${ansi.reset}`);
164
203
  }
165
204
 
205
+ // Bottom border
206
+ lines.push(` ${palette.border}${B.dbl}${B.dh.repeat(width - 2)}${B.dbr}${ansi.reset}`);
207
+
166
208
  return lines.join('\n');
167
209
  }
168
210
 
169
211
  // ═══════════════════════════════════════════════════════════════════════════════
170
- // BLOCKERS DISPLAY
212
+ // BLOCKERS TABLE
171
213
  // ═══════════════════════════════════════════════════════════════════════════════
172
214
 
173
- function renderBlockers(blockers, options = {}) {
174
- const { maxItems = 8 } = options;
215
+ function renderBlockersTable(blockers, options = {}) {
216
+ const { maxItems = 10 } = options;
175
217
 
176
218
  if (!blockers || blockers.length === 0) {
177
219
  const lines = [];
178
- lines.push(renderSection('SHIP BLOCKERS', '🚀'));
179
220
  lines.push('');
180
- lines.push(` ${colors.success}${ansi.bold}${icons.success} No blockers! You're clear to ship.${ansi.reset}`);
221
+ lines.push(` ${palette.pass}${ansi.bold}✓ NO BLOCKERS${ansi.reset} ${palette.muted}— Your code is ready to ship!${ansi.reset}`);
181
222
  return lines.join('\n');
182
223
  }
183
224
 
184
225
  const lines = [];
185
- lines.push(renderSection(`SHIP BLOCKERS (${blockers.length})`, '🚨'));
226
+ const width = 70;
227
+
228
+ lines.push('');
229
+ lines.push(` ${palette.critical}${ansi.bold}🚨 SHIP BLOCKERS (${blockers.length})${ansi.reset}`);
230
+ lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
186
231
  lines.push('');
187
232
 
188
- for (const blocker of blockers.slice(0, maxItems)) {
233
+ for (let i = 0; i < Math.min(blockers.length, maxItems); i++) {
234
+ const blocker = blockers[i];
189
235
  const sevColor = blocker.severity === 'critical' || blocker.severity === 'BLOCK'
190
- ? colors.bg.error
191
- : colors.bg.warning;
236
+ ? palette.criticalBg
237
+ : palette.highBg;
192
238
  const sevLabel = blocker.severity === 'critical' || blocker.severity === 'BLOCK'
193
- ? 'CRITICAL'
194
- : ' HIGH ';
239
+ ? ' BLOCK '
240
+ : ' HIGH ';
195
241
 
196
- lines.push(` ${sevColor}${ansi.bold} ${sevLabel} ${ansi.reset} ${ansi.bold}${truncate(blocker.title || blocker.message, 45)}${ansi.reset}`);
242
+ // Row number
243
+ const num = `${palette.muted}${String(i + 1).padStart(2)}.${ansi.reset}`;
197
244
 
198
- if (blocker.description) {
199
- lines.push(` ${ansi.dim}${truncate(blocker.description, 55)}${ansi.reset}`);
200
- }
245
+ // Severity badge
246
+ const badge = `${sevColor}${ansi.bold}${sevLabel}${ansi.reset}`;
247
+
248
+ // Title (truncated)
249
+ const title = truncate(blocker.title || blocker.message || 'Unknown issue', 48);
201
250
 
251
+ lines.push(` ${num} ${badge} ${ansi.bold}${title}${ansi.reset}`);
252
+
253
+ // File location
202
254
  if (blocker.file) {
203
255
  const fileDisplay = blocker.file + (blocker.line ? `:${blocker.line}` : '');
204
- lines.push(` ${colors.accent}${truncate(fileDisplay, 50)}${ansi.reset}`);
256
+ lines.push(` ${palette.muted}└─${ansi.reset} ${palette.accent}${truncate(fileDisplay, 55)}${ansi.reset}`);
205
257
  }
206
258
 
207
- if (blocker.fix || blocker.fixSuggestion) {
208
- lines.push(` ${colors.success}→ ${truncate(blocker.fix || blocker.fixSuggestion, 50)}${ansi.reset}`);
259
+ // Evidence/why
260
+ if (blocker.description || blocker.why) {
261
+ const desc = truncate(blocker.description || blocker.why, 55);
262
+ lines.push(` ${palette.muted}${desc}${ansi.reset}`);
209
263
  }
210
264
 
211
265
  lines.push('');
212
266
  }
213
267
 
214
268
  if (blockers.length > maxItems) {
215
- lines.push(` ${ansi.dim}... and ${blockers.length - maxItems} more blockers${ansi.reset}`);
269
+ lines.push(` ${palette.muted}... and ${blockers.length - maxItems} more blockers${ansi.reset}`);
216
270
  lines.push('');
217
271
  }
218
272
 
@@ -220,20 +274,23 @@ function renderBlockers(blockers, options = {}) {
220
274
  }
221
275
 
222
276
  // ═══════════════════════════════════════════════════════════════════════════════
223
- // CATEGORY SUMMARY
277
+ // CATEGORY TABLE WITH VISUAL BARS
224
278
  // ═══════════════════════════════════════════════════════════════════════════════
225
279
 
226
- function renderCategorySummary(findings) {
280
+ function renderCategoryTable(findings) {
227
281
  if (!findings || findings.length === 0) return '';
228
282
 
229
283
  // Group by category
230
284
  const categories = {};
285
+ let maxTotal = 0;
286
+
231
287
  for (const f of findings) {
232
- const cat = f.category || f.ruleId?.split('/')[0] || 'other';
288
+ const cat = f.category || f.ruleId?.split('/')[0] || 'Other';
233
289
  if (!categories[cat]) {
234
290
  categories[cat] = { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
235
291
  }
236
292
  categories[cat].total++;
293
+ maxTotal = Math.max(maxTotal, categories[cat].total);
237
294
 
238
295
  const sev = (f.severity || '').toLowerCase();
239
296
  if (sev === 'critical' || sev === 'block') categories[cat].critical++;
@@ -242,46 +299,277 @@ function renderCategorySummary(findings) {
242
299
  else categories[cat].low++;
243
300
  }
244
301
 
302
+ const categoryIcons = {
303
+ ENVCONTRACT: '📋', MISSINGROUTE: '🔗', HARDCODEDSECRET: '🔑', TODOFIXME: '📝',
304
+ FAKESUCCESS: '✨', GHOSTAUTH: '👻', MOCKDATA: '🎭', CONSOLELOG: '🖥️',
305
+ DEADCODE: '💀', DEPRECATEDAPI: '⚠️', EMPTYCATCH: '🕳️', UNSAFEREGEX: '⚡',
306
+ SECURITY: '🛡️', BILLING: '💳', ROUTE: '🔗', AUTH: '🔐', SECRET: '🔑',
307
+ };
308
+
309
+ const categoryColors = [
310
+ palette.cat1, palette.cat2, palette.cat3, palette.cat4, palette.cat5,
311
+ palette.cat6, palette.cat7, palette.cat8, palette.cat9, palette.cat10,
312
+ palette.cat11, palette.cat12,
313
+ ];
314
+
245
315
  const lines = [];
246
- lines.push(renderSection('FINDINGS BY CATEGORY', '📁'));
316
+ const width = 70;
317
+
318
+ lines.push('');
319
+ lines.push(` ${palette.header}${ansi.bold}📊 FINDINGS BY CATEGORY${ansi.reset}`);
320
+ lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
247
321
  lines.push('');
248
322
 
249
- const categoryIcons = {
250
- ROUTE: '🔗',
251
- AUTH: '🔐',
252
- SECRET: '🔑',
253
- BILLING: '💳',
254
- MOCK: '🎭',
255
- DEAD_UI: '👻',
256
- FAKE_SUCCESS: '✨',
257
- REALITY: '🔬',
258
- QUALITY: '📋',
259
- CONFIG: '⚙️',
260
- };
323
+ // Table header
324
+ lines.push(` ${palette.muted} CATEGORY COUNT SEVERITY BREAKDOWN BAR${ansi.reset}`);
325
+ lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
261
326
 
327
+ // Sort by total count descending
262
328
  const sortedCategories = Object.entries(categories)
263
329
  .sort((a, b) => {
264
- // Sort by criticality: critical > high > medium > low
265
- const aCrit = a[1].critical * 1000 + a[1].high * 100 + a[1].medium * 10;
266
- const bCrit = b[1].critical * 1000 + b[1].high * 100 + b[1].medium * 10;
330
+ // Sort by criticality first, then by total
331
+ const aCrit = a[1].critical * 1000 + a[1].high * 100 + a[1].total;
332
+ const bCrit = b[1].critical * 1000 + b[1].high * 100 + b[1].total;
267
333
  return bCrit - aCrit;
268
334
  });
269
335
 
336
+ let colorIdx = 0;
270
337
  for (const [cat, counts] of sortedCategories) {
271
338
  const icon = categoryIcons[cat.toUpperCase()] || '•';
272
- const critStr = counts.critical > 0 ? `${colors.critical}${counts.critical}C${ansi.reset} ` : '';
273
- const highStr = counts.high > 0 ? `${colors.high}${counts.high}H${ansi.reset} ` : '';
274
- const medStr = counts.medium > 0 ? `${colors.medium}${counts.medium}M${ansi.reset} ` : '';
275
- const lowStr = counts.low > 0 ? `${colors.low}${counts.low}L${ansi.reset}` : '';
339
+ const catColor = categoryColors[colorIdx % categoryColors.length];
340
+ colorIdx++;
341
+
342
+ // Category name
343
+ const catName = truncate(cat, 15).padEnd(15);
344
+
345
+ // Count
346
+ const countStr = String(counts.total).padStart(4);
347
+
348
+ // Severity breakdown
349
+ const critStr = counts.critical > 0 ? `${palette.critical}${counts.critical}C${ansi.reset}` : `${palette.muted}--${ansi.reset}`;
350
+ const highStr = counts.high > 0 ? `${palette.high}${counts.high}H${ansi.reset}` : `${palette.muted}--${ansi.reset}`;
351
+ const medStr = counts.medium > 0 ? `${palette.medium}${counts.medium}M${ansi.reset}` : `${palette.muted}--${ansi.reset}`;
352
+ const lowStr = counts.low > 0 ? `${palette.low}${counts.low}L${ansi.reset}` : `${palette.muted}--${ansi.reset}`;
276
353
 
277
- lines.push(` ${icon} ${cat.padEnd(15)} ${ansi.dim}${String(counts.total).padStart(3)} total${ansi.reset} ${critStr}${highStr}${medStr}${lowStr}`);
354
+ // Visual bar
355
+ const barWidth = 15;
356
+ const barFill = Math.round((counts.total / maxTotal) * barWidth);
357
+ let bar = '';
358
+ for (let i = 0; i < barWidth; i++) {
359
+ if (i < barFill) {
360
+ bar += `${catColor}${B.full}${ansi.reset}`;
361
+ } else {
362
+ bar += `${palette.muted}${B.light}${ansi.reset}`;
363
+ }
364
+ }
365
+
366
+ lines.push(` ${icon} ${catColor}${catName}${ansi.reset} ${ansi.bold}${countStr}${ansi.reset} ${critStr} ${highStr} ${medStr} ${lowStr} ${bar}`);
278
367
  }
279
368
 
369
+ lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
370
+
371
+ // Total
372
+ const totalCount = findings.length;
373
+ const totalCrit = findings.filter(f => f.severity === 'critical' || f.severity === 'BLOCK').length;
374
+ const totalHigh = findings.filter(f => f.severity === 'high').length;
375
+ const totalMed = findings.filter(f => f.severity === 'medium' || f.severity === 'WARN' || f.severity === 'warning').length;
376
+ const totalLow = findings.filter(f => f.severity === 'low' || f.severity === 'INFO').length;
377
+
378
+ lines.push(` ${palette.muted} TOTAL${ansi.reset} ${ansi.bold}${String(totalCount).padStart(4)}${ansi.reset} ${palette.critical}${totalCrit}C${ansi.reset} ${palette.high}${totalHigh}H${ansi.reset} ${palette.medium}${totalMed}M${ansi.reset} ${palette.low}${totalLow}L${ansi.reset}`);
379
+
280
380
  return lines.join('\n');
281
381
  }
282
382
 
283
383
  // ═══════════════════════════════════════════════════════════════════════════════
284
- // FULL SCAN OUTPUT
384
+ // TOP FINDINGS DETAILS
385
+ // ═══════════════════════════════════════════════════════════════════════════════
386
+
387
+ function renderTopFindings(findings, options = {}) {
388
+ const { maxItems = 5, showFix = true } = options;
389
+
390
+ if (!findings || findings.length === 0) return '';
391
+
392
+ // Get top critical/high findings
393
+ const topFindings = findings
394
+ .filter(f => f.severity === 'critical' || f.severity === 'BLOCK' || f.severity === 'high')
395
+ .slice(0, maxItems);
396
+
397
+ if (topFindings.length === 0) return '';
398
+
399
+ const lines = [];
400
+ const width = 70;
401
+
402
+ lines.push('');
403
+ lines.push(` ${palette.header}${ansi.bold}🔍 TOP ISSUES TO FIX${ansi.reset}`);
404
+ lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
405
+
406
+ for (let i = 0; i < topFindings.length; i++) {
407
+ const f = topFindings[i];
408
+ const num = `${palette.accent}${i + 1}${ansi.reset}`;
409
+ const icon = f.severity === 'critical' || f.severity === 'BLOCK' ? '🔴' : '🟠';
410
+
411
+ lines.push('');
412
+ lines.push(` ${num}. ${icon} ${ansi.bold}${truncate(f.title || f.message, 55)}${ansi.reset}`);
413
+
414
+ if (f.file) {
415
+ lines.push(` ${palette.accent}${f.file}${f.line ? `:${f.line}` : ''}${ansi.reset}`);
416
+ }
417
+
418
+ if (f.description || f.why) {
419
+ lines.push(` ${palette.muted}${truncate(f.description || f.why, 60)}${ansi.reset}`);
420
+ }
421
+
422
+ if (showFix && (f.fix || f.fixHints?.[0])) {
423
+ const fixHint = f.fix || f.fixHints?.[0];
424
+ lines.push(` ${palette.pass}→ ${truncate(fixHint, 58)}${ansi.reset}`);
425
+ }
426
+ }
427
+
428
+ return lines.join('\n');
429
+ }
430
+
431
+ // ═══════════════════════════════════════════════════════════════════════════════
432
+ // ANALYSIS LAYERS STATUS
433
+ // ═══════════════════════════════════════════════════════════════════════════════
434
+
435
+ function renderLayers(layers) {
436
+ if (!layers || layers.length === 0) return '';
437
+
438
+ const lines = [];
439
+ const width = 70;
440
+
441
+ lines.push('');
442
+ lines.push(` ${palette.header}${ansi.bold}⚡ ANALYSIS LAYERS${ansi.reset}`);
443
+ lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
444
+ lines.push('');
445
+
446
+ const layerConfig = {
447
+ ast: { name: 'AST Analysis', icon: '🔍', desc: 'Static code parsing' },
448
+ truth: { name: 'Build Truth', icon: '📦', desc: 'Manifest verification' },
449
+ reality: { name: 'Reality Check', icon: '🎭', desc: 'Runtime testing' },
450
+ realitySniff: { name: 'Reality Sniff', icon: '🔬', desc: 'AI artifact detection' },
451
+ detection: { name: 'Detection', icon: '🛡️', desc: 'Security patterns' },
452
+ codeQuality: { name: 'Code Quality', icon: '📋', desc: 'TODO, MOCK, console.log' },
453
+ };
454
+
455
+ for (const layer of layers) {
456
+ const config = layerConfig[layer.name] || { name: layer.name, icon: '○', desc: '' };
457
+
458
+ let status, statusIcon;
459
+ if (layer.skipped) {
460
+ status = `${palette.muted}skipped${ansi.reset}`;
461
+ statusIcon = `${palette.muted}○${ansi.reset}`;
462
+ } else if (layer.error) {
463
+ status = `${palette.fail}error${ansi.reset}`;
464
+ statusIcon = `${palette.fail}✗${ansi.reset}`;
465
+ } else {
466
+ status = `${palette.pass}done${ansi.reset}`;
467
+ statusIcon = `${palette.pass}✓${ansi.reset}`;
468
+ }
469
+
470
+ const duration = layer.duration ? `${palette.muted}${layer.duration}ms${ansi.reset}` : '';
471
+ const findingsCount = layer.findings !== undefined ? `${palette.accent}${layer.findings}${ansi.reset} findings` : '';
472
+
473
+ lines.push(` ${statusIcon} ${config.icon} ${config.name.padEnd(18)} ${status.padEnd(20)} ${duration.padEnd(12)} ${findingsCount}`);
474
+ }
475
+
476
+ return lines.join('\n');
477
+ }
478
+
479
+ // ═══════════════════════════════════════════════════════════════════════════════
480
+ // COVERAGE MAP
481
+ // ═══════════════════════════════════════════════════════════════════════════════
482
+
483
+ function renderCoverageMap(coverage) {
484
+ if (!coverage) return '';
485
+
486
+ const lines = [];
487
+ const width = 70;
488
+
489
+ lines.push('');
490
+ lines.push(` ${palette.header}${ansi.bold}🗺️ ROUTE COVERAGE${ansi.reset}`);
491
+ lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
492
+ lines.push('');
493
+
494
+ const pct = coverage.coveragePercent || 0;
495
+ const color = pct >= 80 ? palette.pass : pct >= 60 ? palette.warn : palette.fail;
496
+
497
+ // Coverage percentage with big number
498
+ lines.push(` ${color}${ansi.bold}${pct}%${ansi.reset} ${palette.muted}of routes reachable from root${ansi.reset}`);
499
+
500
+ // Progress bar
501
+ const barWidth = 50;
502
+ const filled = Math.round((pct / 100) * barWidth);
503
+ let bar = '';
504
+ for (let i = 0; i < barWidth; i++) {
505
+ if (i < filled) {
506
+ bar += `${color}${B.full}${ansi.reset}`;
507
+ } else {
508
+ bar += `${palette.muted}${B.light}${ansi.reset}`;
509
+ }
510
+ }
511
+ lines.push(` ${bar}`);
512
+ lines.push('');
513
+
514
+ // Stats grid
515
+ const stats = [
516
+ ['Total Routes', coverage.totalRoutes || 0, palette.accent],
517
+ ['Reachable', coverage.reachableFromRoot || 0, palette.pass],
518
+ ['Orphaned', coverage.orphanedRoutes || 0, coverage.orphanedRoutes > 0 ? palette.warn : palette.pass],
519
+ ['Dead Links', coverage.deadLinks || 0, coverage.deadLinks > 0 ? palette.fail : palette.pass],
520
+ ];
521
+
522
+ let statsLine = ' ';
523
+ for (const [label, value, col] of stats) {
524
+ statsLine += `${palette.muted}${label}:${ansi.reset} ${col}${ansi.bold}${value}${ansi.reset} `;
525
+ }
526
+ lines.push(statsLine);
527
+
528
+ return lines.join('\n');
529
+ }
530
+
531
+ // ═══════════════════════════════════════════════════════════════════════════════
532
+ // UPSELL BANNER
533
+ // ═══════════════════════════════════════════════════════════════════════════════
534
+
535
+ function renderUpsellBanner(severityCounts) {
536
+ const { critical = 0, high = 0, medium = 0, low = 0 } = severityCounts;
537
+ const total = critical + high + medium + low;
538
+
539
+ if (total === 0) return '';
540
+
541
+ const lines = [];
542
+ const width = 68;
543
+
544
+ lines.push('');
545
+ lines.push(` ${palette.border}${B.rtl}${B.lh.repeat(width)}${B.rtr}${ansi.reset}`);
546
+ lines.push(` ${palette.border}${B.lv}${ansi.reset}${' '.repeat(width)}${palette.border}${B.lv}${ansi.reset}`);
547
+
548
+ if (critical > 0) {
549
+ lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.fail}⚠ ${critical} critical issues${ansi.reset} blocking your ship!${' '.repeat(width - 42)}${palette.border}${B.lv}${ansi.reset}`);
550
+ lines.push(` ${palette.border}${B.lv}${ansi.reset}${' '.repeat(width)}${palette.border}${B.lv}${ansi.reset}`);
551
+ lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.accent}vibecheck fix${ansi.reset} ${palette.muted}→ AI-generated fixes for each issue${ansi.reset}${' '.repeat(width - 52)}${palette.border}${B.lv}${ansi.reset}`);
552
+ lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.muted}Requires STARTER • vibecheck.dev/pricing${ansi.reset}${' '.repeat(width - 43)}${palette.border}${B.lv}${ansi.reset}`);
553
+ } else if (medium > 0) {
554
+ lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.warn}⚡ ${medium} warnings${ansi.reset} detected in your codebase${' '.repeat(width - 46)}${palette.border}${B.lv}${ansi.reset}`);
555
+ lines.push(` ${palette.border}${B.lv}${ansi.reset}${' '.repeat(width)}${palette.border}${B.lv}${ansi.reset}`);
556
+ lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.accent}vibecheck ship${ansi.reset} ${palette.muted}→ Full verdict with AI risk score${ansi.reset}${' '.repeat(width - 51)}${palette.border}${B.lv}${ansi.reset}`);
557
+ lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.accent}vibecheck fix${ansi.reset} ${palette.muted}→ AI fixes • Requires STARTER${ansi.reset}${' '.repeat(width - 48)}${palette.border}${B.lv}${ansi.reset}`);
558
+ } else {
559
+ lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.pass}✓ Looking good!${ansi.reset} Only minor issues found${' '.repeat(width - 40)}${palette.border}${B.lv}${ansi.reset}`);
560
+ lines.push(` ${palette.border}${B.lv}${ansi.reset}${' '.repeat(width)}${palette.border}${B.lv}${ansi.reset}`);
561
+ lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.accent}vibecheck prove${ansi.reset} ${palette.muted}→ Runtime proof + Verified Badge${ansi.reset}${' '.repeat(width - 50)}${palette.border}${B.lv}${ansi.reset}`);
562
+ lines.push(` ${palette.border}${B.lv}${ansi.reset} ${palette.muted}Requires PRO • vibecheck.dev/pricing${ansi.reset}${' '.repeat(width - 39)}${palette.border}${B.lv}${ansi.reset}`);
563
+ }
564
+
565
+ lines.push(` ${palette.border}${B.lv}${ansi.reset}${' '.repeat(width)}${palette.border}${B.lv}${ansi.reset}`);
566
+ lines.push(` ${palette.border}${B.rbl}${B.lh.repeat(width)}${B.rbr}${ansi.reset}`);
567
+
568
+ return lines.join('\n');
569
+ }
570
+
571
+ // ═══════════════════════════════════════════════════════════════════════════════
572
+ // MAIN OUTPUT FORMATTER
285
573
  // ═══════════════════════════════════════════════════════════════════════════════
286
574
 
287
575
  function formatScanOutput(result, options = {}) {
@@ -307,64 +595,56 @@ function formatScanOutput(result, options = {}) {
307
595
 
308
596
  const lines = [];
309
597
 
310
- // Score card
311
- lines.push(renderScoreCard(score, {
598
+ // Premium score card
599
+ lines.push(renderPremiumScoreCard(score, {
312
600
  verdict: verdictStatus,
313
601
  findings: severityCounts,
314
602
  duration: timings.total,
315
603
  cached: result.cached,
316
604
  }));
317
605
 
318
- // Blockers (critical + high severity findings)
606
+ // Blockers table
319
607
  const blockers = findings.filter(f =>
320
608
  f.severity === 'critical' || f.severity === 'BLOCK' || f.severity === 'high'
321
609
  );
322
- lines.push(renderBlockers(blockers));
610
+ lines.push(renderBlockersTable(blockers));
323
611
 
324
- // Category summary
612
+ // Category table with visual bars
325
613
  if (findings.length > 0) {
326
- lines.push(renderCategorySummary(findings));
614
+ lines.push(renderCategoryTable(findings));
327
615
  }
328
616
 
329
617
  // Verbose output
330
618
  if (verbose) {
619
+ // Top findings with fix hints
620
+ lines.push(renderTopFindings(findings));
621
+
331
622
  // Coverage map
332
623
  if (coverage) {
333
- lines.push('');
334
624
  lines.push(renderCoverageMap(coverage));
335
625
  }
336
626
 
337
- // Breakdown
338
- if (breakdown) {
339
- lines.push('');
340
- lines.push(renderBreakdown(breakdown));
341
- }
342
-
343
- // Layers
627
+ // Layers status
344
628
  if (layers.length > 0) {
345
- lines.push('');
346
629
  lines.push(renderLayers(layers));
347
630
  }
348
-
349
- // All findings
350
- if (findings.length > 0) {
351
- lines.push('');
352
- lines.push(renderFindingsList(findings, { maxItems: 20, groupBySeverity: true }));
353
- }
354
631
  }
355
632
 
356
- // Timing summary
633
+ // Timing
357
634
  if (timings.total) {
358
635
  lines.push('');
359
- lines.push(` ${ansi.dim}Completed in ${formatDuration(timings.total)}${ansi.reset}`);
636
+ lines.push(` ${palette.muted}Completed in ${formatDuration(timings.total)}${ansi.reset}`);
360
637
  }
361
638
 
639
+ // Upsell banner
640
+ lines.push(renderUpsellBanner(severityCounts));
641
+
362
642
  lines.push('');
363
643
  return lines.join('\n');
364
644
  }
365
645
 
366
646
  // ═══════════════════════════════════════════════════════════════════════════════
367
- // SCORE CALCULATION
647
+ // UTILITIES
368
648
  // ═══════════════════════════════════════════════════════════════════════════════
369
649
 
370
650
  function calculateScore(severityCounts) {
@@ -377,10 +657,6 @@ function calculateScore(severityCounts) {
377
657
  return Math.max(0, 100 - deductions);
378
658
  }
379
659
 
380
- // ═══════════════════════════════════════════════════════════════════════════════
381
- // EXIT CODE DETERMINATION
382
- // ═══════════════════════════════════════════════════════════════════════════════
383
-
384
660
  const EXIT_CODES = {
385
661
  SUCCESS: 0,
386
662
  WARNING: 1,
@@ -390,9 +666,7 @@ const EXIT_CODES = {
390
666
 
391
667
  function getExitCode(verdict) {
392
668
  if (!verdict) return EXIT_CODES.ERROR;
393
-
394
669
  const status = verdict.verdict || verdict;
395
-
396
670
  switch (status) {
397
671
  case 'PASS':
398
672
  case 'SHIP':
@@ -407,57 +681,34 @@ function getExitCode(verdict) {
407
681
  }
408
682
  }
409
683
 
410
- // ═══════════════════════════════════════════════════════════════════════════════
411
- // ERROR DISPLAY
412
- // ═══════════════════════════════════════════════════════════════════════════════
413
-
414
684
  function printError(error, context = '') {
415
685
  const prefix = context ? `${context}: ` : '';
416
-
417
686
  console.error('');
418
- console.error(` ${colors.error}${icons.error}${ansi.reset} ${ansi.bold}${prefix}${error.message || error}${ansi.reset}`);
419
-
687
+ console.error(` ${palette.fail}✗${ansi.reset} ${ansi.bold}${prefix}${error.message || error}${ansi.reset}`);
420
688
  if (error.code) {
421
- console.error(` ${ansi.dim}Error code: ${error.code}${ansi.reset}`);
689
+ console.error(` ${palette.muted}Error code: ${error.code}${ansi.reset}`);
422
690
  }
423
-
424
691
  if (error.suggestion || error.fix) {
425
- console.error(` ${colors.success}${icons.arrowRight}${ansi.reset} ${error.suggestion || error.fix}`);
426
- }
427
-
428
- if (error.docs || error.helpUrl) {
429
- console.error(` ${ansi.dim}See: ${error.docs || error.helpUrl}${ansi.reset}`);
692
+ console.error(` ${palette.pass}→${ansi.reset} ${error.suggestion || error.fix}`);
430
693
  }
431
-
432
694
  console.error('');
433
-
434
- // Return appropriate exit code
435
- if (error.code === 'VALIDATION_ERROR') return EXIT_CODES.FAILURE;
436
- if (error.code === 'LIMIT_EXCEEDED') return EXIT_CODES.WARNING;
437
695
  return EXIT_CODES.ERROR;
438
696
  }
439
697
 
440
- // ═══════════════════════════════════════════════════════════════════════════════
441
- // SARIF OUTPUT
442
- // ═══════════════════════════════════════════════════════════════════════════════
443
-
698
+ // SARIF export
444
699
  function formatSARIF(findings, options = {}) {
445
700
  const { projectPath = '.', version = '1.0.0' } = options;
446
-
447
701
  const rules = new Map();
448
702
  const results = [];
449
703
 
450
704
  for (const f of findings) {
451
705
  const ruleId = f.ruleId || f.id || `vibecheck/${f.category || 'general'}`;
452
-
453
706
  if (!rules.has(ruleId)) {
454
707
  rules.set(ruleId, {
455
708
  id: ruleId,
456
709
  name: f.category || 'general',
457
710
  shortDescription: { text: f.title || f.message },
458
- defaultConfiguration: {
459
- level: sarifLevel(f.severity),
460
- },
711
+ defaultConfiguration: { level: sarifLevel(f.severity) },
461
712
  helpUri: 'https://vibecheck.dev/docs/rules/' + ruleId,
462
713
  });
463
714
  }
@@ -498,46 +749,34 @@ function formatSARIF(findings, options = {}) {
498
749
  },
499
750
  },
500
751
  results,
501
- invocations: [{
502
- executionSuccessful: true,
503
- endTimeUtc: new Date().toISOString(),
504
- }],
752
+ invocations: [{ executionSuccessful: true, endTimeUtc: new Date().toISOString() }],
505
753
  }],
506
754
  };
507
755
  }
508
756
 
509
757
  function sarifLevel(severity) {
510
758
  const levels = {
511
- critical: 'error',
512
- BLOCK: 'error',
513
- high: 'error',
514
- medium: 'warning',
515
- WARN: 'warning',
516
- warning: 'warning',
517
- low: 'note',
518
- INFO: 'note',
519
- info: 'none',
759
+ critical: 'error', BLOCK: 'error', high: 'error',
760
+ medium: 'warning', WARN: 'warning', warning: 'warning',
761
+ low: 'note', INFO: 'note', info: 'none',
520
762
  };
521
763
  return levels[severity] || 'warning';
522
764
  }
523
765
 
524
- // ═══════════════════════════════════════════════════════════════════════════════
525
- // EXPORTS
526
- // ═══════════════════════════════════════════════════════════════════════════════
766
+ // Legacy compatibility exports
767
+ function renderBreakdown(breakdown) {
768
+ // Simplified - can expand later
769
+ return '';
770
+ }
527
771
 
528
772
  module.exports = {
529
- // Main formatters
530
773
  formatScanOutput,
531
774
  formatSARIF,
532
-
533
- // Component renderers
534
775
  renderLayers,
535
776
  renderCoverageMap,
536
777
  renderBreakdown,
537
- renderBlockers,
538
- renderCategorySummary,
539
-
540
- // Utilities
778
+ renderBlockers: renderBlockersTable,
779
+ renderCategorySummary: renderCategoryTable,
541
780
  calculateScore,
542
781
  getExitCode,
543
782
  printError,