@vibecheckai/cli 3.5.1 → 3.5.2

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 (272) hide show
  1. package/bin/registry.js +406 -154
  2. package/bin/runners/context/analyzer.js +52 -1
  3. package/bin/runners/context/generators/mcp.js +15 -13
  4. package/bin/runners/context/git-context.js +3 -1
  5. package/bin/runners/context/proof-context.js +248 -1
  6. package/bin/runners/context/team-conventions.js +33 -7
  7. package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
  8. package/bin/runners/lib/agent-firewall/change-packet/builder.js +488 -0
  9. package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
  10. package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
  11. package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
  12. package/bin/runners/lib/agent-firewall/claims/extractor.js +303 -0
  13. package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
  14. package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
  15. package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
  16. package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
  17. package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
  18. package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
  19. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +127 -0
  20. package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
  21. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +213 -0
  22. package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
  23. package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
  24. package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
  25. package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
  26. package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
  27. package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
  28. package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
  29. package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
  30. package/bin/runners/lib/agent-firewall/interceptor/base.js +304 -0
  31. package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
  32. package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
  33. package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
  34. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
  35. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
  36. package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
  37. package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
  38. package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
  39. package/bin/runners/lib/agent-firewall/logger.js +141 -0
  40. package/bin/runners/lib/agent-firewall/policy/default-policy.json +90 -0
  41. package/bin/runners/lib/agent-firewall/policy/engine.js +103 -0
  42. package/bin/runners/lib/agent-firewall/policy/loader.js +451 -0
  43. package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
  44. package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
  45. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +86 -0
  46. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +162 -0
  47. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +189 -0
  48. package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
  49. package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
  50. package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
  51. package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
  52. package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
  53. package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
  54. package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
  55. package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
  56. package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
  57. package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
  58. package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
  59. package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
  60. package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
  61. package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
  62. package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
  63. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
  64. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
  65. package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
  66. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
  67. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
  68. package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
  69. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
  70. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
  71. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
  72. package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
  73. package/bin/runners/lib/agent-firewall/truthpack/loader.js +137 -0
  74. package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
  75. package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
  76. package/bin/runners/lib/analysis-core.js +220 -182
  77. package/bin/runners/lib/analyzers.js +2145 -224
  78. package/bin/runners/lib/api-client.js +269 -0
  79. package/bin/runners/lib/authority-badge.js +425 -0
  80. package/bin/runners/lib/cli-output.js +242 -210
  81. package/bin/runners/lib/default-config.js +127 -0
  82. package/bin/runners/lib/detectors-v2.js +547 -785
  83. package/bin/runners/lib/doctor/modules/security.js +3 -1
  84. package/bin/runners/lib/engine/ast-cache.js +210 -0
  85. package/bin/runners/lib/engine/auth-extractor.js +211 -0
  86. package/bin/runners/lib/engine/billing-extractor.js +112 -0
  87. package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
  88. package/bin/runners/lib/engine/env-extractor.js +207 -0
  89. package/bin/runners/lib/engine/express-extractor.js +208 -0
  90. package/bin/runners/lib/engine/extractors.js +849 -0
  91. package/bin/runners/lib/engine/index.js +207 -0
  92. package/bin/runners/lib/engine/repo-index.js +514 -0
  93. package/bin/runners/lib/engine/types.js +124 -0
  94. package/bin/runners/lib/engines/accessibility-engine.js +190 -0
  95. package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
  96. package/bin/runners/lib/engines/ast-cache.js +99 -0
  97. package/bin/runners/lib/engines/code-quality-engine.js +255 -0
  98. package/bin/runners/lib/engines/console-logs-engine.js +115 -0
  99. package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
  100. package/bin/runners/lib/engines/dead-code-engine.js +198 -0
  101. package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
  102. package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
  103. package/bin/runners/lib/engines/file-filter.js +131 -0
  104. package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
  105. package/bin/runners/lib/engines/mock-data-engine.js +272 -0
  106. package/bin/runners/lib/engines/parallel-processor.js +71 -0
  107. package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
  108. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
  109. package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
  110. package/bin/runners/lib/engines/type-aware-engine.js +152 -0
  111. package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
  112. package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
  113. package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
  114. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
  115. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
  116. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
  117. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
  118. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
  119. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
  120. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
  121. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
  122. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
  123. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
  124. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
  125. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
  126. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
  127. package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
  128. package/bin/runners/lib/entitlements-v2.js +152 -446
  129. package/bin/runners/lib/error-handler.js +60 -12
  130. package/bin/runners/lib/error-messages.js +289 -0
  131. package/bin/runners/lib/evidence-pack.js +7 -1
  132. package/bin/runners/lib/exit-codes.js +275 -0
  133. package/bin/runners/lib/finding-id.js +69 -0
  134. package/bin/runners/lib/finding-sorter.js +89 -0
  135. package/bin/runners/lib/fingerprint.js +377 -0
  136. package/bin/runners/lib/global-flags.js +37 -0
  137. package/bin/runners/lib/help-formatter.js +413 -0
  138. package/bin/runners/lib/logger.js +38 -0
  139. package/bin/runners/lib/next-action.js +560 -0
  140. package/bin/runners/lib/prerequisites.js +149 -0
  141. package/bin/runners/lib/route-detection.js +137 -68
  142. package/bin/runners/lib/route-truth.js +1167 -322
  143. package/bin/runners/lib/scan-output.js +504 -463
  144. package/bin/runners/lib/scan-runner.js +135 -0
  145. package/bin/runners/lib/schemas/ajv-validator.js +464 -0
  146. package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
  147. package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
  148. package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
  149. package/bin/runners/lib/schemas/run-request.schema.json +108 -0
  150. package/bin/runners/lib/schemas/validator.js +27 -0
  151. package/bin/runners/lib/schemas/verdict.schema.json +140 -0
  152. package/bin/runners/lib/ship-output-enterprise.js +239 -0
  153. package/bin/runners/lib/ship-output.js +328 -31
  154. package/bin/runners/lib/terminal-ui.js +234 -731
  155. package/bin/runners/lib/truth.js +1332 -308
  156. package/bin/runners/lib/unified-cli-output.js +604 -0
  157. package/bin/runners/lib/unified-output.js +163 -155
  158. package/bin/runners/lib/upsell.js +104 -204
  159. package/bin/runners/runAgent.d.ts +5 -0
  160. package/bin/runners/runAgent.js +161 -0
  161. package/bin/runners/runAllowlist.js +166 -101
  162. package/bin/runners/runApprove.js +1200 -0
  163. package/bin/runners/runAuth.js +373 -95
  164. package/bin/runners/runCheckpoint.js +59 -21
  165. package/bin/runners/runClassify.js +926 -0
  166. package/bin/runners/runContext.d.ts +4 -0
  167. package/bin/runners/runContext.js +136 -24
  168. package/bin/runners/runDoctor.js +115 -67
  169. package/bin/runners/runEvidencePack.js +239 -96
  170. package/bin/runners/runFirewall.d.ts +5 -0
  171. package/bin/runners/runFirewall.js +134 -0
  172. package/bin/runners/runFirewallHook.d.ts +5 -0
  173. package/bin/runners/runFirewallHook.js +56 -0
  174. package/bin/runners/runFix.js +6 -5
  175. package/bin/runners/runGuard.js +212 -118
  176. package/bin/runners/runInit.js +66 -21
  177. package/bin/runners/runLabs.js +204 -121
  178. package/bin/runners/runMcp.js +131 -60
  179. package/bin/runners/runPolish.d.ts +4 -0
  180. package/bin/runners/runPolish.js +43 -20
  181. package/bin/runners/runProof.zip +0 -0
  182. package/bin/runners/runProve.js +15 -5
  183. package/bin/runners/runQuickstart.js +531 -0
  184. package/bin/runners/runReality.js +14 -0
  185. package/bin/runners/runReport.js +36 -4
  186. package/bin/runners/runScan.js +689 -91
  187. package/bin/runners/runShip.js +96 -40
  188. package/bin/runners/runTruth.d.ts +5 -0
  189. package/bin/runners/runTruth.js +101 -0
  190. package/bin/runners/runValidate.js +21 -4
  191. package/bin/runners/runWatch.js +118 -54
  192. package/bin/scan.js +6 -1
  193. package/bin/vibecheck.js +297 -52
  194. package/mcp-server/HARDENING_SUMMARY.md +299 -0
  195. package/mcp-server/agent-firewall-interceptor.js +500 -0
  196. package/mcp-server/authority-tools.js +569 -0
  197. package/mcp-server/conductor/conflict-resolver.js +588 -0
  198. package/mcp-server/conductor/execution-planner.js +544 -0
  199. package/mcp-server/conductor/index.js +377 -0
  200. package/mcp-server/conductor/lock-manager.js +615 -0
  201. package/mcp-server/conductor/request-queue.js +550 -0
  202. package/mcp-server/conductor/session-manager.js +500 -0
  203. package/mcp-server/conductor/tools.js +510 -0
  204. package/mcp-server/deprecation-middleware.js +282 -0
  205. package/mcp-server/handlers/index.ts +15 -0
  206. package/mcp-server/handlers/tool-handler.ts +474 -591
  207. package/mcp-server/index.js +1748 -1099
  208. package/mcp-server/lib/api-client.cjs +13 -0
  209. package/mcp-server/lib/cache-wrapper.cjs +383 -0
  210. package/mcp-server/lib/error-envelope.js +138 -0
  211. package/mcp-server/lib/executor.ts +428 -721
  212. package/mcp-server/lib/index.ts +19 -0
  213. package/mcp-server/lib/logger.cjs +30 -0
  214. package/mcp-server/lib/rate-limiter.js +166 -0
  215. package/mcp-server/lib/sandbox.test.ts +519 -0
  216. package/mcp-server/lib/sandbox.ts +342 -284
  217. package/mcp-server/lib/types.ts +267 -0
  218. package/mcp-server/logger.js +173 -0
  219. package/mcp-server/package.json +11 -27
  220. package/mcp-server/premium-tools.js +2 -2
  221. package/mcp-server/registry/tool-registry.js +794 -0
  222. package/mcp-server/registry/tools.json +507 -378
  223. package/mcp-server/registry.test.ts +334 -0
  224. package/mcp-server/tests/tier-gating.test.js +297 -0
  225. package/mcp-server/tier-auth.js +492 -347
  226. package/mcp-server/tools-v3.js +950 -0
  227. package/mcp-server/truth-context.js +131 -90
  228. package/mcp-server/truth-firewall-tools.js +1612 -1001
  229. package/mcp-server/tsconfig.json +8 -5
  230. package/mcp-server/vibecheck-2.0-tools.js +14 -1
  231. package/mcp-server/vibecheck-mcp-server-3.2.0.tgz +0 -0
  232. package/mcp-server/vibecheck-tools.js +2 -2
  233. package/package.json +4 -3
  234. package/bin/runners/runInstall.js +0 -281
  235. package/mcp-server/ARCHITECTURE.md +0 -339
  236. package/mcp-server/__tests__/cache.test.ts +0 -313
  237. package/mcp-server/__tests__/executor.test.ts +0 -239
  238. package/mcp-server/__tests__/fixtures/exclusion-test/.cache/webpack/cache.pack +0 -1
  239. package/mcp-server/__tests__/fixtures/exclusion-test/.next/server/chunk.js +0 -3
  240. package/mcp-server/__tests__/fixtures/exclusion-test/.turbo/cache.json +0 -3
  241. package/mcp-server/__tests__/fixtures/exclusion-test/.venv/lib/env.py +0 -3
  242. package/mcp-server/__tests__/fixtures/exclusion-test/dist/bundle.js +0 -3
  243. package/mcp-server/__tests__/fixtures/exclusion-test/package.json +0 -5
  244. package/mcp-server/__tests__/fixtures/exclusion-test/src/app.ts +0 -5
  245. package/mcp-server/__tests__/fixtures/exclusion-test/venv/lib/config.py +0 -4
  246. package/mcp-server/__tests__/ids.test.ts +0 -345
  247. package/mcp-server/__tests__/integration/tools.test.ts +0 -410
  248. package/mcp-server/__tests__/registry.test.ts +0 -365
  249. package/mcp-server/__tests__/sandbox.test.ts +0 -323
  250. package/mcp-server/__tests__/schemas.test.ts +0 -372
  251. package/mcp-server/benchmarks/run-benchmarks.ts +0 -304
  252. package/mcp-server/examples/doctor.request.json +0 -14
  253. package/mcp-server/examples/doctor.response.json +0 -53
  254. package/mcp-server/examples/error.response.json +0 -15
  255. package/mcp-server/examples/scan.request.json +0 -14
  256. package/mcp-server/examples/scan.response.json +0 -108
  257. package/mcp-server/index-v3.ts +0 -293
  258. package/mcp-server/index.old.js +0 -4137
  259. package/mcp-server/lib/cache.ts +0 -341
  260. package/mcp-server/lib/errors.ts +0 -346
  261. package/mcp-server/lib/ids.ts +0 -238
  262. package/mcp-server/lib/logger.ts +0 -368
  263. package/mcp-server/lib/metrics.ts +0 -365
  264. package/mcp-server/lib/validator.ts +0 -229
  265. package/mcp-server/package-lock.json +0 -165
  266. package/mcp-server/schemas/error-envelope.schema.json +0 -125
  267. package/mcp-server/schemas/finding.schema.json +0 -167
  268. package/mcp-server/schemas/report-artifact.schema.json +0 -88
  269. package/mcp-server/schemas/run-request.schema.json +0 -75
  270. package/mcp-server/schemas/verdict.schema.json +0 -168
  271. package/mcp-server/tier-auth.d.ts +0 -71
  272. package/mcp-server/vitest.config.ts +0 -16
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Finding Fingerprint System
3
+ *
4
+ * Provides stable, content-aware fingerprints for findings that allow:
5
+ * - De-duplication across runs without a database
6
+ * - Detection of NEW / FIXED / PERSISTING findings
7
+ * - Comparison even when line numbers shift
8
+ *
9
+ * Fingerprint = hash of: ruleId + file + contextHash (code around the finding)
10
+ */
11
+
12
+ "use strict";
13
+
14
+ const fs = require("fs");
15
+ const path = require("path");
16
+ const crypto = require("crypto");
17
+
18
+ // ═══════════════════════════════════════════════════════════════════════════════
19
+ // FINGERPRINT GENERATION
20
+ // ═══════════════════════════════════════════════════════════════════════════════
21
+
22
+ /**
23
+ * Generate a stable fingerprint for a finding.
24
+ *
25
+ * The fingerprint is resilient to line number changes by:
26
+ * 1. Using a context window of code around the finding
27
+ * 2. Normalizing whitespace in the context
28
+ * 3. Including rule ID and file path
29
+ *
30
+ * @param {Object} finding - The finding object
31
+ * @param {string} repoRoot - Repository root path
32
+ * @returns {string} - Stable fingerprint hash
33
+ */
34
+ function generateFingerprint(finding, repoRoot) {
35
+ const components = [];
36
+
37
+ // 1. Rule ID (category + severity)
38
+ const ruleId = `${finding.category || "unknown"}:${finding.severity || "unknown"}`;
39
+ components.push(ruleId);
40
+
41
+ // 2. File path (relative, normalized)
42
+ const file = extractFile(finding);
43
+ if (file) {
44
+ const relPath = file.startsWith("/") || file.includes(":\\")
45
+ ? path.relative(repoRoot, file).replace(/\\/g, "/")
46
+ : file.replace(/\\/g, "/");
47
+ components.push(relPath);
48
+ }
49
+
50
+ // 3. Context hash (code around the finding)
51
+ const contextHash = getContextHash(finding, repoRoot);
52
+ if (contextHash) {
53
+ components.push(contextHash);
54
+ }
55
+
56
+ // 4. Title/message for uniqueness (normalized)
57
+ const titleNorm = normalizeMessage(finding.title || finding.message || "");
58
+ if (titleNorm) {
59
+ components.push(titleNorm.slice(0, 100)); // Cap to prevent huge fingerprints
60
+ }
61
+
62
+ // Generate hash
63
+ const input = components.join("|");
64
+ return crypto.createHash("sha256").update(input).digest("hex").slice(0, 16);
65
+ }
66
+
67
+ /**
68
+ * Extract file path from finding (handles various formats)
69
+ */
70
+ function extractFile(finding) {
71
+ if (finding.file) return finding.file;
72
+ if (finding.location?.file) return finding.location.file;
73
+ if (finding.evidence?.[0]?.file) return finding.evidence[0].file;
74
+
75
+ // Try to extract from title/message
76
+ const titleMatch = (finding.title || "").match(/:\s*([^\s:]+\.(ts|tsx|js|jsx|json))/);
77
+ if (titleMatch) return titleMatch[1];
78
+
79
+ return null;
80
+ }
81
+
82
+ /**
83
+ * Extract line range from finding
84
+ */
85
+ function extractLines(finding) {
86
+ if (finding.lines) {
87
+ const match = String(finding.lines).match(/(\d+)(?:-(\d+))?/);
88
+ if (match) {
89
+ return { start: parseInt(match[1]), end: parseInt(match[2] || match[1]) };
90
+ }
91
+ }
92
+ if (finding.location?.line) {
93
+ return { start: finding.location.line, end: finding.location.endLine || finding.location.line };
94
+ }
95
+ if (finding.evidence?.[0]?.lines) {
96
+ const match = String(finding.evidence[0].lines).match(/(\d+)(?:-(\d+))?/);
97
+ if (match) {
98
+ return { start: parseInt(match[1]), end: parseInt(match[2] || match[1]) };
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * Get a hash of the code context around the finding.
106
+ * Uses a window of ±3 lines to be resilient to minor edits.
107
+ */
108
+ function getContextHash(finding, repoRoot) {
109
+ const file = extractFile(finding);
110
+ const lines = extractLines(finding);
111
+
112
+ if (!file || !lines) {
113
+ // Fall back to snippet hash if available
114
+ if (finding.evidence?.[0]?.snippetHash) {
115
+ return finding.evidence[0].snippetHash.slice(0, 12);
116
+ }
117
+ return null;
118
+ }
119
+
120
+ try {
121
+ const absPath = path.isAbsolute(file) ? file : path.join(repoRoot, file);
122
+ if (!fs.existsSync(absPath)) return null;
123
+
124
+ const content = fs.readFileSync(absPath, "utf8");
125
+ const allLines = content.split(/\r?\n/);
126
+
127
+ // Get context window (±3 lines)
128
+ const contextStart = Math.max(0, lines.start - 4);
129
+ const contextEnd = Math.min(allLines.length, lines.end + 3);
130
+
131
+ const context = allLines
132
+ .slice(contextStart, contextEnd)
133
+ .map(line => line.trim()) // Normalize whitespace
134
+ .filter(line => line.length > 0) // Skip empty lines
135
+ .join("\n");
136
+
137
+ return crypto.createHash("sha256").update(context).digest("hex").slice(0, 12);
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Normalize message for comparison (strip variable parts)
145
+ */
146
+ function normalizeMessage(msg) {
147
+ return msg
148
+ .replace(/:\d+(-\d+)?/g, "") // Remove line numbers
149
+ .replace(/\([^)]*\)/g, "") // Remove parenthetical notes
150
+ .replace(/["'`][^"'`]+["'`]/g, "X") // Replace quoted strings
151
+ .replace(/\s+/g, " ") // Normalize whitespace
152
+ .trim()
153
+ .toLowerCase();
154
+ }
155
+
156
+ // ═══════════════════════════════════════════════════════════════════════════════
157
+ // BASELINE MANAGEMENT
158
+ // ═══════════════════════════════════════════════════════════════════════════════
159
+
160
+ const BASELINE_FILE = ".vibecheck/baseline.json";
161
+
162
+ /**
163
+ * Load baseline from disk
164
+ */
165
+ function loadBaseline(repoRoot) {
166
+ const baselinePath = path.join(repoRoot, BASELINE_FILE);
167
+
168
+ try {
169
+ if (!fs.existsSync(baselinePath)) {
170
+ return { fingerprints: new Map(), timestamp: null, version: "1.0.0" };
171
+ }
172
+
173
+ const data = JSON.parse(fs.readFileSync(baselinePath, "utf8"));
174
+
175
+ return {
176
+ fingerprints: new Map(Object.entries(data.fingerprints || {})),
177
+ timestamp: data.timestamp,
178
+ version: data.version || "1.0.0",
179
+ metadata: data.metadata || {},
180
+ };
181
+ } catch {
182
+ return { fingerprints: new Map(), timestamp: null, version: "1.0.0" };
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Save baseline to disk
188
+ */
189
+ function saveBaseline(repoRoot, findings, metadata = {}) {
190
+ const baselinePath = path.join(repoRoot, BASELINE_FILE);
191
+
192
+ // Ensure directory exists
193
+ const dir = path.dirname(baselinePath);
194
+ if (!fs.existsSync(dir)) {
195
+ fs.mkdirSync(dir, { recursive: true });
196
+ }
197
+
198
+ // Build fingerprint map
199
+ const fingerprints = {};
200
+ for (const finding of findings) {
201
+ if (finding.fingerprint) {
202
+ fingerprints[finding.fingerprint] = {
203
+ category: finding.category,
204
+ severity: finding.severity,
205
+ file: extractFile(finding),
206
+ title: (finding.title || finding.message || "").slice(0, 200),
207
+ firstSeen: finding.firstSeen || new Date().toISOString(),
208
+ };
209
+ }
210
+ }
211
+
212
+ const data = {
213
+ version: "1.0.0",
214
+ timestamp: new Date().toISOString(),
215
+ metadata: {
216
+ totalFindings: findings.length,
217
+ criticalCount: findings.filter(f => f.severity === "BLOCK" || f.severity === "critical").length,
218
+ ...metadata,
219
+ },
220
+ fingerprints,
221
+ };
222
+
223
+ fs.writeFileSync(baselinePath, JSON.stringify(data, null, 2));
224
+
225
+ return data;
226
+ }
227
+
228
+ // ═══════════════════════════════════════════════════════════════════════════════
229
+ // DIFF CALCULATION
230
+ // ═══════════════════════════════════════════════════════════════════════════════
231
+
232
+ /**
233
+ * Compare current findings against baseline to determine status.
234
+ *
235
+ * @param {Array} findings - Current findings (with fingerprints)
236
+ * @param {Object} baseline - Loaded baseline
237
+ * @returns {Object} - Diff result with NEW, FIXED, PERSISTING counts
238
+ */
239
+ function diffFindings(findings, baseline) {
240
+ const currentFP = new Set(findings.map(f => f.fingerprint).filter(Boolean));
241
+ const baselineFP = baseline.fingerprints;
242
+
243
+ const result = {
244
+ new: [],
245
+ fixed: [],
246
+ persisting: [],
247
+ summary: {
248
+ newCount: 0,
249
+ fixedCount: 0,
250
+ persistingCount: 0,
251
+ totalCurrent: findings.length,
252
+ totalBaseline: baselineFP.size,
253
+ },
254
+ };
255
+
256
+ // Find NEW and PERSISTING
257
+ for (const finding of findings) {
258
+ if (!finding.fingerprint) continue;
259
+
260
+ if (baselineFP.has(finding.fingerprint)) {
261
+ finding.status = "PERSISTING";
262
+ finding.firstSeen = baselineFP.get(finding.fingerprint).firstSeen;
263
+ result.persisting.push(finding);
264
+ } else {
265
+ finding.status = "NEW";
266
+ finding.firstSeen = new Date().toISOString();
267
+ result.new.push(finding);
268
+ }
269
+ }
270
+
271
+ // Find FIXED (in baseline but not in current)
272
+ for (const [fp, meta] of baselineFP) {
273
+ if (!currentFP.has(fp)) {
274
+ result.fixed.push({
275
+ fingerprint: fp,
276
+ status: "FIXED",
277
+ ...meta,
278
+ });
279
+ }
280
+ }
281
+
282
+ // Update counts
283
+ result.summary.newCount = result.new.length;
284
+ result.summary.fixedCount = result.fixed.length;
285
+ result.summary.persistingCount = result.persisting.length;
286
+
287
+ return result;
288
+ }
289
+
290
+ // ═══════════════════════════════════════════════════════════════════════════════
291
+ // ENRICHMENT
292
+ // ═══════════════════════════════════════════════════════════════════════════════
293
+
294
+ /**
295
+ * Enrich findings with fingerprints and diff status.
296
+ *
297
+ * @param {Array} findings - Raw findings from analyzers
298
+ * @param {string} repoRoot - Repository root
299
+ * @param {boolean} compareBaseline - Whether to compare against baseline
300
+ * @returns {Object} - { findings, diff, baseline }
301
+ */
302
+ function enrichFindings(findings, repoRoot, compareBaseline = true) {
303
+ // Add fingerprints to all findings
304
+ for (const finding of findings) {
305
+ finding.fingerprint = generateFingerprint(finding, repoRoot);
306
+ }
307
+
308
+ // Load baseline and diff if requested
309
+ let diff = null;
310
+ let baseline = null;
311
+
312
+ if (compareBaseline) {
313
+ baseline = loadBaseline(repoRoot);
314
+
315
+ if (baseline.fingerprints.size > 0) {
316
+ diff = diffFindings(findings, baseline);
317
+ } else {
318
+ // First run - all findings are "new" but we don't show the label
319
+ for (const finding of findings) {
320
+ finding.status = null; // No status on first run
321
+ }
322
+ }
323
+ }
324
+
325
+ return { findings, diff, baseline };
326
+ }
327
+
328
+ /**
329
+ * Format diff summary for terminal output
330
+ */
331
+ function formatDiffSummary(diff) {
332
+ if (!diff) return null;
333
+
334
+ const parts = [];
335
+
336
+ if (diff.summary.newCount > 0) {
337
+ parts.push(`\x1b[38;2;255;100;100m+${diff.summary.newCount} NEW\x1b[0m`);
338
+ }
339
+
340
+ if (diff.summary.fixedCount > 0) {
341
+ parts.push(`\x1b[38;2;100;255;150m-${diff.summary.fixedCount} FIXED\x1b[0m`);
342
+ }
343
+
344
+ if (diff.summary.persistingCount > 0) {
345
+ parts.push(`\x1b[38;2;200;200;200m${diff.summary.persistingCount} persisting\x1b[0m`);
346
+ }
347
+
348
+ return parts.length > 0 ? parts.join(" ") : null;
349
+ }
350
+
351
+ /**
352
+ * Get status badge for a finding
353
+ */
354
+ function getStatusBadge(finding) {
355
+ switch (finding.status) {
356
+ case "NEW":
357
+ return "\x1b[48;2;255;80;80m\x1b[1m NEW \x1b[0m";
358
+ case "FIXED":
359
+ return "\x1b[48;2;50;180;100m\x1b[1m FIXED \x1b[0m";
360
+ case "PERSISTING":
361
+ return "\x1b[38;2;150;150;150m●\x1b[0m"; // Subtle dot for persisting
362
+ default:
363
+ return "";
364
+ }
365
+ }
366
+
367
+ module.exports = {
368
+ generateFingerprint,
369
+ loadBaseline,
370
+ saveBaseline,
371
+ diffFindings,
372
+ enrichFindings,
373
+ formatDiffSummary,
374
+ getStatusBadge,
375
+ extractFile,
376
+ extractLines,
377
+ };
@@ -128,6 +128,41 @@ function shouldShowBanner(flags = {}) {
128
128
  return true;
129
129
  }
130
130
 
131
+ // ═══════════════════════════════════════════════════════════════════════════════
132
+ // OUTPUT SUPPRESSION CHECK
133
+ // ═══════════════════════════════════════════════════════════════════════════════
134
+
135
+ /**
136
+ * Check if non-essential output should be suppressed
137
+ * Use this before any console output that isn't errors or JSON
138
+ *
139
+ * @param {object} flags - Parsed flags object
140
+ * @returns {boolean} true if output should be suppressed
141
+ */
142
+ function shouldSuppressOutput(flags = {}) {
143
+ // Suppress if quiet or CI mode
144
+ if (flags.quiet || flags.ci) return true;
145
+
146
+ // Suppress if JSON mode (only structured output allowed)
147
+ if (flags.json) return true;
148
+
149
+ // Check environment
150
+ if (process.env.VIBECHECK_QUIET === "true") return true;
151
+
152
+ return false;
153
+ }
154
+
155
+ /**
156
+ * Check if we're in JSON output mode
157
+ * Use this to decide between pretty output and structured JSON
158
+ *
159
+ * @param {object} flags - Parsed flags object
160
+ * @returns {boolean} true if JSON output is requested
161
+ */
162
+ function isJsonMode(flags = {}) {
163
+ return flags.json === true;
164
+ }
165
+
131
166
  // ═══════════════════════════════════════════════════════════════════════════════
132
167
  // CI DETECTION
133
168
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -204,6 +239,8 @@ function loadConfig(flags = {}) {
204
239
  module.exports = {
205
240
  parseGlobalFlags,
206
241
  shouldShowBanner,
242
+ shouldSuppressOutput,
243
+ isJsonMode,
207
244
  isCI,
208
245
  loadConfig,
209
246
  lazy,