@vibecheckai/cli 3.1.8 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) 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/agent-firewall/change-packet/builder.js +214 -0
  7. package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
  8. package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
  9. package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
  10. package/bin/runners/lib/agent-firewall/claims/extractor.js +214 -0
  11. package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
  12. package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
  13. package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
  14. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +118 -0
  15. package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
  16. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +142 -0
  17. package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
  18. package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
  19. package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
  20. package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
  21. package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
  22. package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
  23. package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
  24. package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
  25. package/bin/runners/lib/agent-firewall/interceptor/base.js +304 -0
  26. package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
  27. package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
  28. package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
  29. package/bin/runners/lib/agent-firewall/policy/default-policy.json +84 -0
  30. package/bin/runners/lib/agent-firewall/policy/engine.js +72 -0
  31. package/bin/runners/lib/agent-firewall/policy/loader.js +143 -0
  32. package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
  33. package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
  34. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +61 -0
  35. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +50 -0
  36. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +50 -0
  37. package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
  38. package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
  39. package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
  40. package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
  41. package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
  42. package/bin/runners/lib/agent-firewall/truthpack/loader.js +116 -0
  43. package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
  44. package/bin/runners/lib/analysis-core.js +198 -180
  45. package/bin/runners/lib/analyzers.js +1394 -224
  46. package/bin/runners/lib/detectors-v2.js +560 -641
  47. package/bin/runners/lib/entitlements-v2.js +48 -1
  48. package/bin/runners/lib/evidence-pack.js +678 -0
  49. package/bin/runners/lib/fingerprint.js +377 -0
  50. package/bin/runners/lib/html-proof-report.js +913 -0
  51. package/bin/runners/lib/missions/plan.js +231 -41
  52. package/bin/runners/lib/missions/templates.js +125 -0
  53. package/bin/runners/lib/route-truth.js +1167 -322
  54. package/bin/runners/lib/scan-output.js +558 -235
  55. package/bin/runners/lib/ship-output.js +901 -641
  56. package/bin/runners/lib/truth.js +1004 -321
  57. package/bin/runners/runAgent.js +161 -0
  58. package/bin/runners/runCheckpoint.js +44 -3
  59. package/bin/runners/runContext.d.ts +4 -0
  60. package/bin/runners/runDoctor.js +10 -2
  61. package/bin/runners/runFirewall.js +134 -0
  62. package/bin/runners/runFirewallHook.js +56 -0
  63. package/bin/runners/runFix.js +51 -341
  64. package/bin/runners/runInit.js +11 -0
  65. package/bin/runners/runPolish.d.ts +4 -0
  66. package/bin/runners/runPolish.js +608 -29
  67. package/bin/runners/runProve.js +210 -25
  68. package/bin/runners/runReality.js +846 -101
  69. package/bin/runners/runScan.js +351 -14
  70. package/bin/runners/runShip.js +19 -3
  71. package/bin/runners/runTruth.js +89 -0
  72. package/bin/runners/runWatch.js +14 -1
  73. package/bin/vibecheck.js +32 -2
  74. package/mcp-server/agent-firewall-interceptor.js +164 -0
  75. package/mcp-server/consolidated-tools.js +408 -42
  76. package/mcp-server/index.js +498 -327
  77. package/mcp-server/proof-tools.js +571 -0
  78. package/mcp-server/tier-auth.js +22 -19
  79. package/mcp-server/tools-v3.js +744 -0
  80. package/mcp-server/truth-context.js +131 -90
  81. package/mcp-server/truth-firewall-tools.js +1494 -941
  82. package/package.json +3 -1
  83. package/bin/runners/runInstall.js +0 -281
  84. 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,327 @@ 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 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
+ };
72
+
73
+ // ═══════════════════════════════════════════════════════════════════════════════
74
+ // BOX DRAWING HELPERS
75
+ // ═══════════════════════════════════════════════════════════════════════════════
76
+
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, '');
116
+ }
117
+
118
+ // ═══════════════════════════════════════════════════════════════════════════════
119
+ // PREMIUM SCORE DISPLAY
120
+ // ═══════════════════════════════════════════════════════════════════════════════
121
+
122
+ function renderPremiumScoreCard(score, options = {}) {
123
+ const { verdict = 'BLOCK', findings = {}, duration, cached } = options;
31
124
  const lines = [];
32
- lines.push(renderSection('ANALYSIS LAYERS', '⚡'));
33
- lines.push('');
34
125
 
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
- };
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
+ }
42
144
 
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}`;
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}`;
51
171
  } else {
52
- status = `${colors.success}${icons.success}${ansi.reset}`;
172
+ progressBar += `${palette.muted}${B.light}${ansi.reset}`;
53
173
  }
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
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}`);
203
+ }
204
+
205
+ // Bottom border
206
+ lines.push(` ${palette.border}${B.dbl}${B.dh.repeat(width - 2)}${B.dbr}${ansi.reset}`);
60
207
 
61
208
  return lines.join('\n');
62
209
  }
63
210
 
64
211
  // ═══════════════════════════════════════════════════════════════════════════════
65
- // COVERAGE MAP DISPLAY
212
+ // DIFF SUMMARY (NEW / FIXED / PERSISTING)
66
213
  // ═══════════════════════════════════════════════════════════════════════════════
67
214
 
68
- function renderCoverageMap(coverage) {
69
- if (!coverage) return '';
215
+ function renderDiffSummary(diff) {
216
+ if (!diff) return '';
70
217
 
71
218
  const lines = [];
72
- lines.push(renderSection('ROUTE COVERAGE', '🗺️'));
73
- lines.push('');
219
+ const width = 70;
74
220
 
75
- const pct = coverage.coveragePercent || 0;
76
- const color = pct >= 80 ? colors.success : pct >= 60 ? colors.warning : colors.error;
221
+ lines.push('');
222
+ lines.push(` ${palette.header}${ansi.bold}📊 CHANGES SINCE LAST SCAN${ansi.reset}`);
223
+ lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
224
+ lines.push('');
77
225
 
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}`;
226
+ const parts = [];
82
227
 
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('');
228
+ // NEW findings (bad - things got worse)
229
+ if (diff.summary.newCount > 0) {
230
+ parts.push(` ${ansi.bgRgb(180, 50, 50)}${ansi.bold} +${diff.summary.newCount} NEW ${ansi.reset}`);
231
+ lines.push(` ${palette.critical}▲${ansi.reset} ${ansi.bold}${diff.summary.newCount}${ansi.reset} ${palette.critical}new issues${ansi.reset} introduced`);
232
+ }
86
233
 
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
- ];
234
+ // FIXED findings (good - things got better)
235
+ if (diff.summary.fixedCount > 0) {
236
+ parts.push(` ${ansi.bgRgb(50, 150, 80)}${ansi.bold} -${diff.summary.fixedCount} FIXED ${ansi.reset}`);
237
+ lines.push(` ${palette.pass}▼${ansi.reset} ${ansi.bold}${diff.summary.fixedCount}${ansi.reset} ${palette.pass}issues fixed${ansi.reset} since last scan`);
238
+ }
94
239
 
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}`);
240
+ // PERSISTING (neutral - still there)
241
+ if (diff.summary.persistingCount > 0) {
242
+ lines.push(` ${palette.muted}●${ansi.reset} ${ansi.bold}${diff.summary.persistingCount}${ansi.reset} ${palette.muted}persisting issues${ansi.reset}`);
100
243
  }
101
244
 
102
- // Isolated clusters
103
- if (coverage.isolatedClusters?.length > 0) {
245
+ // Show top new issues
246
+ if (diff.new && diff.new.length > 0) {
104
247
  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}`);
248
+ lines.push(` ${palette.critical}${ansi.bold}New issues:${ansi.reset}`);
249
+ for (const finding of diff.new.slice(0, 3)) {
250
+ const file = finding.file || finding.evidence?.[0]?.file || '';
251
+ const shortFile = file.split('/').slice(-2).join('/');
252
+ lines.push(` ${palette.critical}+${ansi.reset} ${finding.category}: ${palette.muted}${shortFile}${ansi.reset}`);
109
253
  }
110
- if (coverage.isolatedClusters.length > 3) {
111
- lines.push(` ${ansi.dim}└─ ... and ${coverage.isolatedClusters.length - 3} more clusters${ansi.reset}`);
254
+ if (diff.new.length > 3) {
255
+ lines.push(` ${palette.muted}... and ${diff.new.length - 3} more${ansi.reset}`);
112
256
  }
113
257
  }
114
258
 
115
- // Unreachable routes
116
- if (coverage.unreachableRoutes?.length > 0) {
259
+ // Show top fixed issues
260
+ if (diff.fixed && diff.fixed.length > 0) {
117
261
  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}`);
262
+ lines.push(` ${palette.pass}${ansi.bold}Fixed issues:${ansi.reset}`);
263
+ for (const finding of diff.fixed.slice(0, 3)) {
264
+ const file = finding.file || '';
265
+ const shortFile = file.split('/').slice(-2).join('/');
266
+ lines.push(` ${palette.pass}-${ansi.reset} ${finding.category}: ${palette.muted}${shortFile}${ansi.reset}`);
121
267
  }
122
- if (coverage.unreachableRoutes.length > 5) {
123
- lines.push(` ${ansi.dim}└─ ... and ${coverage.unreachableRoutes.length - 5} more${ansi.reset}`);
268
+ if (diff.fixed.length > 3) {
269
+ lines.push(` ${palette.muted}... and ${diff.fixed.length - 3} more${ansi.reset}`);
124
270
  }
125
271
  }
126
272
 
127
- return lines.join('\n');
128
- }
129
-
130
- // ═══════════════════════════════════════════════════════════════════════════════
131
- // BREAKDOWN DISPLAY
132
- // ═══════════════════════════════════════════════════════════════════════════════
133
-
134
- function renderBreakdown(breakdown) {
135
- if (!breakdown) return '';
136
-
137
- const lines = [];
138
- lines.push(renderSection('SCORE BREAKDOWN', '📊'));
139
273
  lines.push('');
140
274
 
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
- ];
151
-
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}`);
164
- }
165
-
166
275
  return lines.join('\n');
167
276
  }
168
277
 
169
278
  // ═══════════════════════════════════════════════════════════════════════════════
170
- // BLOCKERS DISPLAY
279
+ // BLOCKERS TABLE
171
280
  // ═══════════════════════════════════════════════════════════════════════════════
172
281
 
173
- function renderBlockers(blockers, options = {}) {
174
- const { maxItems = 8 } = options;
282
+ function renderBlockersTable(blockers, options = {}) {
283
+ const { maxItems = 10, showStatus = false } = options;
175
284
 
176
285
  if (!blockers || blockers.length === 0) {
177
286
  const lines = [];
178
- lines.push(renderSection('SHIP BLOCKERS', '🚀'));
179
287
  lines.push('');
180
- lines.push(` ${colors.success}${ansi.bold}${icons.success} No blockers! You're clear to ship.${ansi.reset}`);
288
+ lines.push(` ${palette.pass}${ansi.bold}✓ NO BLOCKERS${ansi.reset} ${palette.muted}— Your code is ready to ship!${ansi.reset}`);
181
289
  return lines.join('\n');
182
290
  }
183
291
 
184
292
  const lines = [];
185
- lines.push(renderSection(`SHIP BLOCKERS (${blockers.length})`, '🚨'));
293
+ const width = 70;
294
+
295
+ // Count new blockers for header
296
+ const newCount = blockers.filter(b => b.status === 'NEW').length;
297
+ const headerExtra = newCount > 0 ? ` ${ansi.bgRgb(180, 50, 50)}${ansi.bold} +${newCount} NEW ${ansi.reset}` : '';
298
+
299
+ lines.push('');
300
+ lines.push(` ${palette.critical}${ansi.bold}🚨 SHIP BLOCKERS (${blockers.length})${ansi.reset}${headerExtra}`);
301
+ lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
186
302
  lines.push('');
187
303
 
188
- for (const blocker of blockers.slice(0, maxItems)) {
304
+ for (let i = 0; i < Math.min(blockers.length, maxItems); i++) {
305
+ const blocker = blockers[i];
189
306
  const sevColor = blocker.severity === 'critical' || blocker.severity === 'BLOCK'
190
- ? colors.bg.error
191
- : colors.bg.warning;
307
+ ? palette.criticalBg
308
+ : palette.highBg;
192
309
  const sevLabel = blocker.severity === 'critical' || blocker.severity === 'BLOCK'
193
- ? 'CRITICAL'
194
- : ' HIGH ';
310
+ ? ' BLOCK '
311
+ : ' HIGH ';
195
312
 
196
- lines.push(` ${sevColor}${ansi.bold} ${sevLabel} ${ansi.reset} ${ansi.bold}${truncate(blocker.title || blocker.message, 45)}${ansi.reset}`);
313
+ // Row number
314
+ const num = `${palette.muted}${String(i + 1).padStart(2)}.${ansi.reset}`;
197
315
 
198
- if (blocker.description) {
199
- lines.push(` ${ansi.dim}${truncate(blocker.description, 55)}${ansi.reset}`);
316
+ // Severity badge
317
+ const badge = `${sevColor}${ansi.bold}${sevLabel}${ansi.reset}`;
318
+
319
+ // Status badge (NEW/PERSISTING)
320
+ let statusBadge = '';
321
+ if (showStatus && blocker.status === 'NEW') {
322
+ statusBadge = ` ${ansi.bgRgb(180, 50, 50)}${ansi.bold} NEW ${ansi.reset}`;
200
323
  }
201
324
 
202
- if (blocker.file) {
203
- const fileDisplay = blocker.file + (blocker.line ? `:${blocker.line}` : '');
204
- lines.push(` ${colors.accent}${truncate(fileDisplay, 50)}${ansi.reset}`);
325
+ // Title (truncated)
326
+ const title = truncate(blocker.title || blocker.message || 'Unknown issue', 42);
327
+
328
+ lines.push(` ${num} ${badge}${statusBadge} ${ansi.bold}${title}${ansi.reset}`);
329
+
330
+ // File location
331
+ if (blocker.file || blocker.evidence?.[0]?.file) {
332
+ const file = blocker.file || blocker.evidence?.[0]?.file;
333
+ const line = blocker.line || blocker.evidence?.[0]?.lines;
334
+ const fileDisplay = file + (line ? `:${line}` : '');
335
+ lines.push(` ${palette.muted}└─${ansi.reset} ${palette.accent}${truncate(fileDisplay, 55)}${ansi.reset}`);
205
336
  }
206
337
 
207
- if (blocker.fix || blocker.fixSuggestion) {
208
- lines.push(` ${colors.success}→ ${truncate(blocker.fix || blocker.fixSuggestion, 50)}${ansi.reset}`);
338
+ // Evidence/why
339
+ if (blocker.description || blocker.why) {
340
+ const desc = truncate(blocker.description || blocker.why, 55);
341
+ lines.push(` ${palette.muted}${desc}${ansi.reset}`);
209
342
  }
210
343
 
211
344
  lines.push('');
212
345
  }
213
346
 
214
347
  if (blockers.length > maxItems) {
215
- lines.push(` ${ansi.dim}... and ${blockers.length - maxItems} more blockers${ansi.reset}`);
348
+ lines.push(` ${palette.muted}... and ${blockers.length - maxItems} more blockers${ansi.reset}`);
216
349
  lines.push('');
217
350
  }
218
351
 
@@ -220,20 +353,23 @@ function renderBlockers(blockers, options = {}) {
220
353
  }
221
354
 
222
355
  // ═══════════════════════════════════════════════════════════════════════════════
223
- // CATEGORY SUMMARY
356
+ // CATEGORY TABLE WITH VISUAL BARS
224
357
  // ═══════════════════════════════════════════════════════════════════════════════
225
358
 
226
- function renderCategorySummary(findings) {
359
+ function renderCategoryTable(findings) {
227
360
  if (!findings || findings.length === 0) return '';
228
361
 
229
362
  // Group by category
230
363
  const categories = {};
364
+ let maxTotal = 0;
365
+
231
366
  for (const f of findings) {
232
- const cat = f.category || f.ruleId?.split('/')[0] || 'other';
367
+ const cat = f.category || f.ruleId?.split('/')[0] || 'Other';
233
368
  if (!categories[cat]) {
234
369
  categories[cat] = { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
235
370
  }
236
371
  categories[cat].total++;
372
+ maxTotal = Math.max(maxTotal, categories[cat].total);
237
373
 
238
374
  const sev = (f.severity || '').toLowerCase();
239
375
  if (sev === 'critical' || sev === 'block') categories[cat].critical++;
@@ -242,46 +378,277 @@ function renderCategorySummary(findings) {
242
378
  else categories[cat].low++;
243
379
  }
244
380
 
381
+ const categoryIcons = {
382
+ ENVCONTRACT: '📋', MISSINGROUTE: '🔗', HARDCODEDSECRET: '🔑', TODOFIXME: '📝',
383
+ FAKESUCCESS: '✨', GHOSTAUTH: '👻', MOCKDATA: '🎭', CONSOLELOG: '🖥️',
384
+ DEADCODE: '💀', DEPRECATEDAPI: '⚠️', EMPTYCATCH: '🕳️', UNSAFEREGEX: '⚡',
385
+ SECURITY: '🛡️', BILLING: '💳', ROUTE: '🔗', AUTH: '🔐', SECRET: '🔑',
386
+ };
387
+
388
+ const categoryColors = [
389
+ palette.cat1, palette.cat2, palette.cat3, palette.cat4, palette.cat5,
390
+ palette.cat6, palette.cat7, palette.cat8, palette.cat9, palette.cat10,
391
+ palette.cat11, palette.cat12,
392
+ ];
393
+
245
394
  const lines = [];
246
- lines.push(renderSection('FINDINGS BY CATEGORY', '📁'));
395
+ const width = 70;
396
+
397
+ lines.push('');
398
+ lines.push(` ${palette.header}${ansi.bold}📊 FINDINGS BY CATEGORY${ansi.reset}`);
399
+ lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
247
400
  lines.push('');
248
401
 
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
- };
402
+ // Table header
403
+ lines.push(` ${palette.muted} CATEGORY COUNT SEVERITY BREAKDOWN BAR${ansi.reset}`);
404
+ lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
261
405
 
406
+ // Sort by total count descending
262
407
  const sortedCategories = Object.entries(categories)
263
408
  .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;
409
+ // Sort by criticality first, then by total
410
+ const aCrit = a[1].critical * 1000 + a[1].high * 100 + a[1].total;
411
+ const bCrit = b[1].critical * 1000 + b[1].high * 100 + b[1].total;
267
412
  return bCrit - aCrit;
268
413
  });
269
414
 
415
+ let colorIdx = 0;
270
416
  for (const [cat, counts] of sortedCategories) {
271
417
  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}` : '';
418
+ const catColor = categoryColors[colorIdx % categoryColors.length];
419
+ colorIdx++;
420
+
421
+ // Category name
422
+ const catName = truncate(cat, 15).padEnd(15);
423
+
424
+ // Count
425
+ const countStr = String(counts.total).padStart(4);
426
+
427
+ // Severity breakdown
428
+ const critStr = counts.critical > 0 ? `${palette.critical}${counts.critical}C${ansi.reset}` : `${palette.muted}--${ansi.reset}`;
429
+ const highStr = counts.high > 0 ? `${palette.high}${counts.high}H${ansi.reset}` : `${palette.muted}--${ansi.reset}`;
430
+ const medStr = counts.medium > 0 ? `${palette.medium}${counts.medium}M${ansi.reset}` : `${palette.muted}--${ansi.reset}`;
431
+ const lowStr = counts.low > 0 ? `${palette.low}${counts.low}L${ansi.reset}` : `${palette.muted}--${ansi.reset}`;
432
+
433
+ // Visual bar
434
+ const barWidth = 15;
435
+ const barFill = Math.round((counts.total / maxTotal) * barWidth);
436
+ let bar = '';
437
+ for (let i = 0; i < barWidth; i++) {
438
+ if (i < barFill) {
439
+ bar += `${catColor}${B.full}${ansi.reset}`;
440
+ } else {
441
+ bar += `${palette.muted}${B.light}${ansi.reset}`;
442
+ }
443
+ }
276
444
 
277
- lines.push(` ${icon} ${cat.padEnd(15)} ${ansi.dim}${String(counts.total).padStart(3)} total${ansi.reset} ${critStr}${highStr}${medStr}${lowStr}`);
445
+ lines.push(` ${icon} ${catColor}${catName}${ansi.reset} ${ansi.bold}${countStr}${ansi.reset} ${critStr} ${highStr} ${medStr} ${lowStr} ${bar}`);
278
446
  }
279
447
 
448
+ lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
449
+
450
+ // Total
451
+ const totalCount = findings.length;
452
+ const totalCrit = findings.filter(f => f.severity === 'critical' || f.severity === 'BLOCK').length;
453
+ const totalHigh = findings.filter(f => f.severity === 'high').length;
454
+ const totalMed = findings.filter(f => f.severity === 'medium' || f.severity === 'WARN' || f.severity === 'warning').length;
455
+ const totalLow = findings.filter(f => f.severity === 'low' || f.severity === 'INFO').length;
456
+
457
+ 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}`);
458
+
280
459
  return lines.join('\n');
281
460
  }
282
461
 
283
462
  // ═══════════════════════════════════════════════════════════════════════════════
284
- // FULL SCAN OUTPUT
463
+ // TOP FINDINGS DETAILS
464
+ // ═══════════════════════════════════════════════════════════════════════════════
465
+
466
+ function renderTopFindings(findings, options = {}) {
467
+ const { maxItems = 5, showFix = true } = options;
468
+
469
+ if (!findings || findings.length === 0) return '';
470
+
471
+ // Get top critical/high findings
472
+ const topFindings = findings
473
+ .filter(f => f.severity === 'critical' || f.severity === 'BLOCK' || f.severity === 'high')
474
+ .slice(0, maxItems);
475
+
476
+ if (topFindings.length === 0) return '';
477
+
478
+ const lines = [];
479
+ const width = 70;
480
+
481
+ lines.push('');
482
+ lines.push(` ${palette.header}${ansi.bold}🔍 TOP ISSUES TO FIX${ansi.reset}`);
483
+ lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
484
+
485
+ for (let i = 0; i < topFindings.length; i++) {
486
+ const f = topFindings[i];
487
+ const num = `${palette.accent}${i + 1}${ansi.reset}`;
488
+ const icon = f.severity === 'critical' || f.severity === 'BLOCK' ? '🔴' : '🟠';
489
+
490
+ lines.push('');
491
+ lines.push(` ${num}. ${icon} ${ansi.bold}${truncate(f.title || f.message, 55)}${ansi.reset}`);
492
+
493
+ if (f.file) {
494
+ lines.push(` ${palette.accent}${f.file}${f.line ? `:${f.line}` : ''}${ansi.reset}`);
495
+ }
496
+
497
+ if (f.description || f.why) {
498
+ lines.push(` ${palette.muted}${truncate(f.description || f.why, 60)}${ansi.reset}`);
499
+ }
500
+
501
+ if (showFix && (f.fix || f.fixHints?.[0])) {
502
+ const fixHint = f.fix || f.fixHints?.[0];
503
+ lines.push(` ${palette.pass}→ ${truncate(fixHint, 58)}${ansi.reset}`);
504
+ }
505
+ }
506
+
507
+ return lines.join('\n');
508
+ }
509
+
510
+ // ═══════════════════════════════════════════════════════════════════════════════
511
+ // ANALYSIS LAYERS STATUS
512
+ // ═══════════════════════════════════════════════════════════════════════════════
513
+
514
+ function renderLayers(layers) {
515
+ if (!layers || layers.length === 0) return '';
516
+
517
+ const lines = [];
518
+ const width = 70;
519
+
520
+ lines.push('');
521
+ lines.push(` ${palette.header}${ansi.bold}⚡ ANALYSIS LAYERS${ansi.reset}`);
522
+ lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
523
+ lines.push('');
524
+
525
+ const layerConfig = {
526
+ ast: { name: 'AST Analysis', icon: '🔍', desc: 'Static code parsing' },
527
+ truth: { name: 'Build Truth', icon: '📦', desc: 'Manifest verification' },
528
+ reality: { name: 'Reality Check', icon: '🎭', desc: 'Runtime testing' },
529
+ realitySniff: { name: 'Reality Sniff', icon: '🔬', desc: 'AI artifact detection' },
530
+ detection: { name: 'Detection', icon: '🛡️', desc: 'Security patterns' },
531
+ codeQuality: { name: 'Code Quality', icon: '📋', desc: 'TODO, MOCK, console.log' },
532
+ };
533
+
534
+ for (const layer of layers) {
535
+ const config = layerConfig[layer.name] || { name: layer.name, icon: '○', desc: '' };
536
+
537
+ let status, statusIcon;
538
+ if (layer.skipped) {
539
+ status = `${palette.muted}skipped${ansi.reset}`;
540
+ statusIcon = `${palette.muted}○${ansi.reset}`;
541
+ } else if (layer.error) {
542
+ status = `${palette.fail}error${ansi.reset}`;
543
+ statusIcon = `${palette.fail}✗${ansi.reset}`;
544
+ } else {
545
+ status = `${palette.pass}done${ansi.reset}`;
546
+ statusIcon = `${palette.pass}✓${ansi.reset}`;
547
+ }
548
+
549
+ const duration = layer.duration ? `${palette.muted}${layer.duration}ms${ansi.reset}` : '';
550
+ const findingsCount = layer.findings !== undefined ? `${palette.accent}${layer.findings}${ansi.reset} findings` : '';
551
+
552
+ lines.push(` ${statusIcon} ${config.icon} ${config.name.padEnd(18)} ${status.padEnd(20)} ${duration.padEnd(12)} ${findingsCount}`);
553
+ }
554
+
555
+ return lines.join('\n');
556
+ }
557
+
558
+ // ═══════════════════════════════════════════════════════════════════════════════
559
+ // COVERAGE MAP
560
+ // ═══════════════════════════════════════════════════════════════════════════════
561
+
562
+ function renderCoverageMap(coverage) {
563
+ if (!coverage) return '';
564
+
565
+ const lines = [];
566
+ const width = 70;
567
+
568
+ lines.push('');
569
+ lines.push(` ${palette.header}${ansi.bold}🗺️ ROUTE COVERAGE${ansi.reset}`);
570
+ lines.push(` ${palette.border}${B.lh.repeat(width - 4)}${ansi.reset}`);
571
+ lines.push('');
572
+
573
+ const pct = coverage.coveragePercent || 0;
574
+ const color = pct >= 80 ? palette.pass : pct >= 60 ? palette.warn : palette.fail;
575
+
576
+ // Coverage percentage with big number
577
+ lines.push(` ${color}${ansi.bold}${pct}%${ansi.reset} ${palette.muted}of routes reachable from root${ansi.reset}`);
578
+
579
+ // Progress bar
580
+ const barWidth = 50;
581
+ const filled = Math.round((pct / 100) * barWidth);
582
+ let bar = '';
583
+ for (let i = 0; i < barWidth; i++) {
584
+ if (i < filled) {
585
+ bar += `${color}${B.full}${ansi.reset}`;
586
+ } else {
587
+ bar += `${palette.muted}${B.light}${ansi.reset}`;
588
+ }
589
+ }
590
+ lines.push(` ${bar}`);
591
+ lines.push('');
592
+
593
+ // Stats grid
594
+ const stats = [
595
+ ['Total Routes', coverage.totalRoutes || 0, palette.accent],
596
+ ['Reachable', coverage.reachableFromRoot || 0, palette.pass],
597
+ ['Orphaned', coverage.orphanedRoutes || 0, coverage.orphanedRoutes > 0 ? palette.warn : palette.pass],
598
+ ['Dead Links', coverage.deadLinks || 0, coverage.deadLinks > 0 ? palette.fail : palette.pass],
599
+ ];
600
+
601
+ let statsLine = ' ';
602
+ for (const [label, value, col] of stats) {
603
+ statsLine += `${palette.muted}${label}:${ansi.reset} ${col}${ansi.bold}${value}${ansi.reset} `;
604
+ }
605
+ lines.push(statsLine);
606
+
607
+ return lines.join('\n');
608
+ }
609
+
610
+ // ═══════════════════════════════════════════════════════════════════════════════
611
+ // UPSELL BANNER
612
+ // ═══════════════════════════════════════════════════════════════════════════════
613
+
614
+ function renderUpsellBanner(severityCounts) {
615
+ const { critical = 0, high = 0, medium = 0, low = 0 } = severityCounts;
616
+ const total = critical + high + medium + low;
617
+
618
+ if (total === 0) return '';
619
+
620
+ const lines = [];
621
+ const width = 68;
622
+
623
+ lines.push('');
624
+ lines.push(` ${palette.border}${B.rtl}${B.lh.repeat(width)}${B.rtr}${ansi.reset}`);
625
+ lines.push(` ${palette.border}${B.lv}${ansi.reset}${' '.repeat(width)}${palette.border}${B.lv}${ansi.reset}`);
626
+
627
+ if (critical > 0) {
628
+ 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}`);
629
+ lines.push(` ${palette.border}${B.lv}${ansi.reset}${' '.repeat(width)}${palette.border}${B.lv}${ansi.reset}`);
630
+ 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}`);
631
+ 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}`);
632
+ } else if (medium > 0) {
633
+ 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}`);
634
+ lines.push(` ${palette.border}${B.lv}${ansi.reset}${' '.repeat(width)}${palette.border}${B.lv}${ansi.reset}`);
635
+ 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}`);
636
+ 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}`);
637
+ } else {
638
+ 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}`);
639
+ lines.push(` ${palette.border}${B.lv}${ansi.reset}${' '.repeat(width)}${palette.border}${B.lv}${ansi.reset}`);
640
+ 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}`);
641
+ 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}`);
642
+ }
643
+
644
+ lines.push(` ${palette.border}${B.lv}${ansi.reset}${' '.repeat(width)}${palette.border}${B.lv}${ansi.reset}`);
645
+ lines.push(` ${palette.border}${B.rbl}${B.lh.repeat(width)}${B.rbr}${ansi.reset}`);
646
+
647
+ return lines.join('\n');
648
+ }
649
+
650
+ // ═══════════════════════════════════════════════════════════════════════════════
651
+ // MAIN OUTPUT FORMATTER
285
652
  // ═══════════════════════════════════════════════════════════════════════════════
286
653
 
287
654
  function formatScanOutput(result, options = {}) {
@@ -291,7 +658,7 @@ function formatScanOutput(result, options = {}) {
291
658
  return JSON.stringify(result, null, 2);
292
659
  }
293
660
 
294
- const { verdict, findings = [], layers = [], coverage, breakdown, timings = {} } = result;
661
+ const { verdict, findings = [], layers = [], coverage, breakdown, timings = {}, diff } = result;
295
662
 
296
663
  // Count findings by severity
297
664
  const severityCounts = {
@@ -307,64 +674,61 @@ function formatScanOutput(result, options = {}) {
307
674
 
308
675
  const lines = [];
309
676
 
310
- // Score card
311
- lines.push(renderScoreCard(score, {
677
+ // Diff summary (if baseline comparison was done)
678
+ if (diff && (diff.summary.newCount > 0 || diff.summary.fixedCount > 0)) {
679
+ lines.push(renderDiffSummary(diff));
680
+ }
681
+
682
+ // Premium score card
683
+ lines.push(renderPremiumScoreCard(score, {
312
684
  verdict: verdictStatus,
313
685
  findings: severityCounts,
314
686
  duration: timings.total,
315
687
  cached: result.cached,
316
688
  }));
317
689
 
318
- // Blockers (critical + high severity findings)
690
+ // Blockers table (with status badges)
319
691
  const blockers = findings.filter(f =>
320
692
  f.severity === 'critical' || f.severity === 'BLOCK' || f.severity === 'high'
321
693
  );
322
- lines.push(renderBlockers(blockers));
694
+ lines.push(renderBlockersTable(blockers, { showStatus: !!diff }));
323
695
 
324
- // Category summary
696
+ // Category table with visual bars
325
697
  if (findings.length > 0) {
326
- lines.push(renderCategorySummary(findings));
698
+ lines.push(renderCategoryTable(findings));
327
699
  }
328
700
 
329
701
  // Verbose output
330
702
  if (verbose) {
703
+ // Top findings with fix hints
704
+ lines.push(renderTopFindings(findings));
705
+
331
706
  // Coverage map
332
707
  if (coverage) {
333
- lines.push('');
334
708
  lines.push(renderCoverageMap(coverage));
335
709
  }
336
710
 
337
- // Breakdown
338
- if (breakdown) {
339
- lines.push('');
340
- lines.push(renderBreakdown(breakdown));
341
- }
342
-
343
- // Layers
711
+ // Layers status
344
712
  if (layers.length > 0) {
345
- lines.push('');
346
713
  lines.push(renderLayers(layers));
347
714
  }
348
-
349
- // All findings
350
- if (findings.length > 0) {
351
- lines.push('');
352
- lines.push(renderFindingsList(findings, { maxItems: 20, groupBySeverity: true }));
353
- }
354
715
  }
355
716
 
356
- // Timing summary
717
+ // Timing
357
718
  if (timings.total) {
358
719
  lines.push('');
359
- lines.push(` ${ansi.dim}Completed in ${formatDuration(timings.total)}${ansi.reset}`);
720
+ lines.push(` ${palette.muted}Completed in ${formatDuration(timings.total)}${ansi.reset}`);
360
721
  }
361
722
 
723
+ // Upsell banner
724
+ lines.push(renderUpsellBanner(severityCounts));
725
+
362
726
  lines.push('');
363
727
  return lines.join('\n');
364
728
  }
365
729
 
366
730
  // ═══════════════════════════════════════════════════════════════════════════════
367
- // SCORE CALCULATION
731
+ // UTILITIES
368
732
  // ═══════════════════════════════════════════════════════════════════════════════
369
733
 
370
734
  function calculateScore(severityCounts) {
@@ -377,10 +741,6 @@ function calculateScore(severityCounts) {
377
741
  return Math.max(0, 100 - deductions);
378
742
  }
379
743
 
380
- // ═══════════════════════════════════════════════════════════════════════════════
381
- // EXIT CODE DETERMINATION
382
- // ═══════════════════════════════════════════════════════════════════════════════
383
-
384
744
  const EXIT_CODES = {
385
745
  SUCCESS: 0,
386
746
  WARNING: 1,
@@ -390,9 +750,7 @@ const EXIT_CODES = {
390
750
 
391
751
  function getExitCode(verdict) {
392
752
  if (!verdict) return EXIT_CODES.ERROR;
393
-
394
753
  const status = verdict.verdict || verdict;
395
-
396
754
  switch (status) {
397
755
  case 'PASS':
398
756
  case 'SHIP':
@@ -407,57 +765,34 @@ function getExitCode(verdict) {
407
765
  }
408
766
  }
409
767
 
410
- // ═══════════════════════════════════════════════════════════════════════════════
411
- // ERROR DISPLAY
412
- // ═══════════════════════════════════════════════════════════════════════════════
413
-
414
768
  function printError(error, context = '') {
415
769
  const prefix = context ? `${context}: ` : '';
416
-
417
770
  console.error('');
418
- console.error(` ${colors.error}${icons.error}${ansi.reset} ${ansi.bold}${prefix}${error.message || error}${ansi.reset}`);
419
-
771
+ console.error(` ${palette.fail}✗${ansi.reset} ${ansi.bold}${prefix}${error.message || error}${ansi.reset}`);
420
772
  if (error.code) {
421
- console.error(` ${ansi.dim}Error code: ${error.code}${ansi.reset}`);
773
+ console.error(` ${palette.muted}Error code: ${error.code}${ansi.reset}`);
422
774
  }
423
-
424
775
  if (error.suggestion || error.fix) {
425
- console.error(` ${colors.success}${icons.arrowRight}${ansi.reset} ${error.suggestion || error.fix}`);
776
+ console.error(` ${palette.pass}→${ansi.reset} ${error.suggestion || error.fix}`);
426
777
  }
427
-
428
- if (error.docs || error.helpUrl) {
429
- console.error(` ${ansi.dim}See: ${error.docs || error.helpUrl}${ansi.reset}`);
430
- }
431
-
432
778
  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
779
  return EXIT_CODES.ERROR;
438
780
  }
439
781
 
440
- // ═══════════════════════════════════════════════════════════════════════════════
441
- // SARIF OUTPUT
442
- // ═══════════════════════════════════════════════════════════════════════════════
443
-
782
+ // SARIF export
444
783
  function formatSARIF(findings, options = {}) {
445
784
  const { projectPath = '.', version = '1.0.0' } = options;
446
-
447
785
  const rules = new Map();
448
786
  const results = [];
449
787
 
450
788
  for (const f of findings) {
451
789
  const ruleId = f.ruleId || f.id || `vibecheck/${f.category || 'general'}`;
452
-
453
790
  if (!rules.has(ruleId)) {
454
791
  rules.set(ruleId, {
455
792
  id: ruleId,
456
793
  name: f.category || 'general',
457
794
  shortDescription: { text: f.title || f.message },
458
- defaultConfiguration: {
459
- level: sarifLevel(f.severity),
460
- },
795
+ defaultConfiguration: { level: sarifLevel(f.severity) },
461
796
  helpUri: 'https://vibecheck.dev/docs/rules/' + ruleId,
462
797
  });
463
798
  }
@@ -498,46 +833,34 @@ function formatSARIF(findings, options = {}) {
498
833
  },
499
834
  },
500
835
  results,
501
- invocations: [{
502
- executionSuccessful: true,
503
- endTimeUtc: new Date().toISOString(),
504
- }],
836
+ invocations: [{ executionSuccessful: true, endTimeUtc: new Date().toISOString() }],
505
837
  }],
506
838
  };
507
839
  }
508
840
 
509
841
  function sarifLevel(severity) {
510
842
  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',
843
+ critical: 'error', BLOCK: 'error', high: 'error',
844
+ medium: 'warning', WARN: 'warning', warning: 'warning',
845
+ low: 'note', INFO: 'note', info: 'none',
520
846
  };
521
847
  return levels[severity] || 'warning';
522
848
  }
523
849
 
524
- // ═══════════════════════════════════════════════════════════════════════════════
525
- // EXPORTS
526
- // ═══════════════════════════════════════════════════════════════════════════════
850
+ // Legacy compatibility exports
851
+ function renderBreakdown(breakdown) {
852
+ // Simplified - can expand later
853
+ return '';
854
+ }
527
855
 
528
856
  module.exports = {
529
- // Main formatters
530
857
  formatScanOutput,
531
858
  formatSARIF,
532
-
533
- // Component renderers
534
859
  renderLayers,
535
860
  renderCoverageMap,
536
861
  renderBreakdown,
537
- renderBlockers,
538
- renderCategorySummary,
539
-
540
- // Utilities
862
+ renderBlockers: renderBlockersTable,
863
+ renderCategorySummary: renderCategoryTable,
541
864
  calculateScore,
542
865
  getExitCode,
543
866
  printError,