vibecheck-ai 2.0.1 → 5.0.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 (456) hide show
  1. package/bin/.generated +25 -0
  2. package/bin/_deprecations.js +463 -0
  3. package/bin/_router.js +46 -0
  4. package/bin/cli-hygiene.js +241 -0
  5. package/bin/dev/run-v2-torture.js +30 -0
  6. package/bin/registry.js +656 -0
  7. package/bin/runners/CLI_REFACTOR_SUMMARY.md +229 -0
  8. package/bin/runners/ENHANCEMENT_GUIDE.md +121 -0
  9. package/bin/runners/REPORT_AUDIT.md +64 -0
  10. package/bin/runners/cli-utils.js +1070 -0
  11. package/bin/runners/context/ai-task-decomposer.js +337 -0
  12. package/bin/runners/context/analyzer.js +513 -0
  13. package/bin/runners/context/api-contracts.js +427 -0
  14. package/bin/runners/context/context-diff.js +342 -0
  15. package/bin/runners/context/context-pruner.js +291 -0
  16. package/bin/runners/context/dependency-graph.js +414 -0
  17. package/bin/runners/context/generators/claude.js +107 -0
  18. package/bin/runners/context/generators/codex.js +108 -0
  19. package/bin/runners/context/generators/copilot.js +119 -0
  20. package/bin/runners/context/generators/cursor-enhanced.js +2525 -0
  21. package/bin/runners/context/generators/cursor.js +514 -0
  22. package/bin/runners/context/generators/mcp.js +169 -0
  23. package/bin/runners/context/generators/windsurf.js +180 -0
  24. package/bin/runners/context/git-context.js +304 -0
  25. package/bin/runners/context/index.js +1110 -0
  26. package/bin/runners/context/insights.js +173 -0
  27. package/bin/runners/context/mcp-server/generate-rules.js +337 -0
  28. package/bin/runners/context/mcp-server/index.js +1176 -0
  29. package/bin/runners/context/mcp-server/package.json +24 -0
  30. package/bin/runners/context/memory.js +200 -0
  31. package/bin/runners/context/monorepo.js +215 -0
  32. package/bin/runners/context/multi-repo-federation.js +404 -0
  33. package/bin/runners/context/patterns.js +253 -0
  34. package/bin/runners/context/proof-context.js +1264 -0
  35. package/bin/runners/context/security-scanner.js +541 -0
  36. package/bin/runners/context/semantic-search.js +350 -0
  37. package/bin/runners/context/shared.js +264 -0
  38. package/bin/runners/context/team-conventions.js +336 -0
  39. package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -0
  40. package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
  41. package/bin/runners/lib/agent-firewall/change-packet/builder.js +488 -0
  42. package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
  43. package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
  44. package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
  45. package/bin/runners/lib/agent-firewall/claims/extractor.js +303 -0
  46. package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
  47. package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
  48. package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
  49. package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
  50. package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
  51. package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
  52. package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
  53. package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
  54. package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
  55. package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
  56. package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
  57. package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
  58. package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
  59. package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
  60. package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
  61. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +127 -0
  62. package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
  63. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +213 -0
  64. package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
  65. package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
  66. package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
  67. package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
  68. package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
  69. package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
  70. package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
  71. package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
  72. package/bin/runners/lib/agent-firewall/index.js +200 -0
  73. package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
  74. package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
  75. package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +634 -0
  76. package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
  77. package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
  78. package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
  79. package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
  80. package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
  81. package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
  82. package/bin/runners/lib/agent-firewall/interceptor/base.js +308 -0
  83. package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
  84. package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
  85. package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
  86. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
  87. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
  88. package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
  89. package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
  90. package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
  91. package/bin/runners/lib/agent-firewall/logger.js +141 -0
  92. package/bin/runners/lib/agent-firewall/policy/default-policy.json +90 -0
  93. package/bin/runners/lib/agent-firewall/policy/engine.js +103 -0
  94. package/bin/runners/lib/agent-firewall/policy/loader.js +451 -0
  95. package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
  96. package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
  97. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +79 -0
  98. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +227 -0
  99. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +191 -0
  100. package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
  101. package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
  102. package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
  103. package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
  104. package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
  105. package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
  106. package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
  107. package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
  108. package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
  109. package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
  110. package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
  111. package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
  112. package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
  113. package/bin/runners/lib/agent-firewall/risk/thresholds.js +322 -0
  114. package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
  115. package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
  116. package/bin/runners/lib/agent-firewall/session/index.js +26 -0
  117. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
  118. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
  119. package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
  120. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
  121. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
  122. package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
  123. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
  124. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
  125. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
  126. package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
  127. package/bin/runners/lib/agent-firewall/truthpack/loader.js +137 -0
  128. package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
  129. package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
  130. package/bin/runners/lib/ai-bridge.js +416 -0
  131. package/bin/runners/lib/analysis-core.js +309 -0
  132. package/bin/runners/lib/analyzers.js +2500 -0
  133. package/bin/runners/lib/api-client.js +269 -0
  134. package/bin/runners/lib/approve-output.js +235 -0
  135. package/bin/runners/lib/artifact-envelope.js +540 -0
  136. package/bin/runners/lib/assets/vibecheck-logo.png +0 -0
  137. package/bin/runners/lib/audit-bridge.js +391 -0
  138. package/bin/runners/lib/auth-shared.js +977 -0
  139. package/bin/runners/lib/auth-truth.js +193 -0
  140. package/bin/runners/lib/auth.js +215 -0
  141. package/bin/runners/lib/authority-badge.js +425 -0
  142. package/bin/runners/lib/backup.js +62 -0
  143. package/bin/runners/lib/billing.js +107 -0
  144. package/bin/runners/lib/checkpoint.js +941 -0
  145. package/bin/runners/lib/claims.js +118 -0
  146. package/bin/runners/lib/classify-output.js +204 -0
  147. package/bin/runners/lib/cleanup/engine.js +571 -0
  148. package/bin/runners/lib/cleanup/index.js +53 -0
  149. package/bin/runners/lib/cleanup/output.js +375 -0
  150. package/bin/runners/lib/cleanup/rules.js +1060 -0
  151. package/bin/runners/lib/cli-output.js +400 -0
  152. package/bin/runners/lib/cli-ui.js +540 -0
  153. package/bin/runners/lib/compliance-bridge-new.js +0 -0
  154. package/bin/runners/lib/compliance-bridge.js +165 -0
  155. package/bin/runners/lib/contracts/auth-contract.js +202 -0
  156. package/bin/runners/lib/contracts/env-contract.js +181 -0
  157. package/bin/runners/lib/contracts/external-contract.js +206 -0
  158. package/bin/runners/lib/contracts/guard.js +168 -0
  159. package/bin/runners/lib/contracts/index.js +89 -0
  160. package/bin/runners/lib/contracts/plan-validator.js +311 -0
  161. package/bin/runners/lib/contracts/route-contract.js +199 -0
  162. package/bin/runners/lib/contracts.js +804 -0
  163. package/bin/runners/lib/default-config.js +127 -0
  164. package/bin/runners/lib/detect.js +89 -0
  165. package/bin/runners/lib/detectors-v2.js +622 -0
  166. package/bin/runners/lib/doctor/autofix.js +254 -0
  167. package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
  168. package/bin/runners/lib/doctor/failure-signatures.js +526 -0
  169. package/bin/runners/lib/doctor/fix-script.js +336 -0
  170. package/bin/runners/lib/doctor/index.js +37 -0
  171. package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
  172. package/bin/runners/lib/doctor/modules/dependencies.js +325 -0
  173. package/bin/runners/lib/doctor/modules/index.js +105 -0
  174. package/bin/runners/lib/doctor/modules/network.js +250 -0
  175. package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
  176. package/bin/runners/lib/doctor/modules/project.js +312 -0
  177. package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
  178. package/bin/runners/lib/doctor/modules/runtime.js +224 -0
  179. package/bin/runners/lib/doctor/modules/security.js +350 -0
  180. package/bin/runners/lib/doctor/modules/system.js +213 -0
  181. package/bin/runners/lib/doctor/modules/vibecheck.js +394 -0
  182. package/bin/runners/lib/doctor/reporter.js +262 -0
  183. package/bin/runners/lib/doctor/safe-repair.js +384 -0
  184. package/bin/runners/lib/doctor/service.js +262 -0
  185. package/bin/runners/lib/doctor/types.js +113 -0
  186. package/bin/runners/lib/doctor/ui.js +263 -0
  187. package/bin/runners/lib/doctor-enhanced.js +233 -0
  188. package/bin/runners/lib/doctor-output.js +226 -0
  189. package/bin/runners/lib/doctor-v2.js +608 -0
  190. package/bin/runners/lib/drift.js +425 -0
  191. package/bin/runners/lib/enforcement.js +72 -0
  192. package/bin/runners/lib/engine/ast-cache.js +210 -0
  193. package/bin/runners/lib/engine/auth-extractor.js +211 -0
  194. package/bin/runners/lib/engine/billing-extractor.js +112 -0
  195. package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
  196. package/bin/runners/lib/engine/env-extractor.js +207 -0
  197. package/bin/runners/lib/engine/express-extractor.js +208 -0
  198. package/bin/runners/lib/engine/extractors.js +849 -0
  199. package/bin/runners/lib/engine/index.js +207 -0
  200. package/bin/runners/lib/engine/repo-index.js +514 -0
  201. package/bin/runners/lib/engine/types.js +124 -0
  202. package/bin/runners/lib/engines/accessibility-engine.js +190 -0
  203. package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
  204. package/bin/runners/lib/engines/ast-cache.js +99 -0
  205. package/bin/runners/lib/engines/attack-detector.js +1192 -0
  206. package/bin/runners/lib/engines/code-quality-engine.js +255 -0
  207. package/bin/runners/lib/engines/console-logs-engine.js +115 -0
  208. package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
  209. package/bin/runners/lib/engines/dead-code-engine.js +198 -0
  210. package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
  211. package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
  212. package/bin/runners/lib/engines/file-filter.js +131 -0
  213. package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
  214. package/bin/runners/lib/engines/mock-data-engine.js +272 -0
  215. package/bin/runners/lib/engines/parallel-processor.js +71 -0
  216. package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
  217. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
  218. package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
  219. package/bin/runners/lib/engines/type-aware-engine.js +152 -0
  220. package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
  221. package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
  222. package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
  223. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
  224. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
  225. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
  226. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
  227. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
  228. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
  229. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
  230. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
  231. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
  232. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
  233. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
  234. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
  235. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
  236. package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
  237. package/bin/runners/lib/enterprise-detect.js +603 -0
  238. package/bin/runners/lib/enterprise-init.js +942 -0
  239. package/bin/runners/lib/entitlements-v2.js +265 -0
  240. package/bin/runners/lib/entitlements.generated.js +0 -0
  241. package/bin/runners/lib/entitlements.js +340 -0
  242. package/bin/runners/lib/env-resolver.js +417 -0
  243. package/bin/runners/lib/env-template.js +66 -0
  244. package/bin/runners/lib/env.js +189 -0
  245. package/bin/runners/lib/error-handler.js +368 -0
  246. package/bin/runners/lib/error-messages.js +289 -0
  247. package/bin/runners/lib/evidence-pack.js +684 -0
  248. package/bin/runners/lib/exit-codes.js +275 -0
  249. package/bin/runners/lib/extractors/client-calls.js +990 -0
  250. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -0
  251. package/bin/runners/lib/extractors/fastify-routes.js +426 -0
  252. package/bin/runners/lib/extractors/index.js +363 -0
  253. package/bin/runners/lib/extractors/next-routes.js +524 -0
  254. package/bin/runners/lib/extractors/proof-graph.js +431 -0
  255. package/bin/runners/lib/extractors/route-matcher.js +451 -0
  256. package/bin/runners/lib/extractors/truthpack-v2.js +377 -0
  257. package/bin/runners/lib/extractors/ui-bindings.js +547 -0
  258. package/bin/runners/lib/finding-id.js +69 -0
  259. package/bin/runners/lib/finding-sorter.js +89 -0
  260. package/bin/runners/lib/findings-schema.js +281 -0
  261. package/bin/runners/lib/fingerprint.js +377 -0
  262. package/bin/runners/lib/firewall-prompt.js +50 -0
  263. package/bin/runners/lib/fix-output.js +228 -0
  264. package/bin/runners/lib/global-flags.js +250 -0
  265. package/bin/runners/lib/graph/graph-builder.js +265 -0
  266. package/bin/runners/lib/graph/html-renderer.js +413 -0
  267. package/bin/runners/lib/graph/index.js +32 -0
  268. package/bin/runners/lib/graph/runtime-collector.js +215 -0
  269. package/bin/runners/lib/graph/static-extractor.js +518 -0
  270. package/bin/runners/lib/help-formatter.js +413 -0
  271. package/bin/runners/lib/html-proof-report.js +913 -0
  272. package/bin/runners/lib/html-report.js +650 -0
  273. package/bin/runners/lib/init-wizard.js +601 -0
  274. package/bin/runners/lib/interactive-menu.js +1496 -0
  275. package/bin/runners/lib/json-output.js +76 -0
  276. package/bin/runners/lib/llm.js +75 -0
  277. package/bin/runners/lib/logger.js +38 -0
  278. package/bin/runners/lib/meter.js +61 -0
  279. package/bin/runners/lib/missions/briefing.js +427 -0
  280. package/bin/runners/lib/missions/checkpoint.js +753 -0
  281. package/bin/runners/lib/missions/evidence.js +126 -0
  282. package/bin/runners/lib/missions/hardening.js +851 -0
  283. package/bin/runners/lib/missions/plan.js +648 -0
  284. package/bin/runners/lib/missions/safety-gates.js +645 -0
  285. package/bin/runners/lib/missions/schema.js +478 -0
  286. package/bin/runners/lib/missions/templates.js +317 -0
  287. package/bin/runners/lib/next-action.js +560 -0
  288. package/bin/runners/lib/packs/bundle.js +675 -0
  289. package/bin/runners/lib/packs/evidence-pack.js +671 -0
  290. package/bin/runners/lib/packs/pack-factory.js +837 -0
  291. package/bin/runners/lib/packs/permissions-pack.js +686 -0
  292. package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
  293. package/bin/runners/lib/patch.js +40 -0
  294. package/bin/runners/lib/permissions/auth-model.js +213 -0
  295. package/bin/runners/lib/permissions/idor-prover.js +205 -0
  296. package/bin/runners/lib/permissions/index.js +45 -0
  297. package/bin/runners/lib/permissions/matrix-builder.js +198 -0
  298. package/bin/runners/lib/pkgjson.js +28 -0
  299. package/bin/runners/lib/policy.js +295 -0
  300. package/bin/runners/lib/polish/accessibility.js +62 -0
  301. package/bin/runners/lib/polish/analyzer.js +93 -0
  302. package/bin/runners/lib/polish/backend.js +87 -0
  303. package/bin/runners/lib/polish/configuration.js +83 -0
  304. package/bin/runners/lib/polish/documentation.js +83 -0
  305. package/bin/runners/lib/polish/frontend.js +817 -0
  306. package/bin/runners/lib/polish/index.js +27 -0
  307. package/bin/runners/lib/polish/infrastructure.js +80 -0
  308. package/bin/runners/lib/polish/internationalization.js +85 -0
  309. package/bin/runners/lib/polish/libraries.js +180 -0
  310. package/bin/runners/lib/polish/observability.js +75 -0
  311. package/bin/runners/lib/polish/performance.js +64 -0
  312. package/bin/runners/lib/polish/privacy.js +110 -0
  313. package/bin/runners/lib/polish/resilience.js +92 -0
  314. package/bin/runners/lib/polish/security.js +78 -0
  315. package/bin/runners/lib/polish/seo.js +71 -0
  316. package/bin/runners/lib/polish/styles.js +62 -0
  317. package/bin/runners/lib/polish/utils.js +104 -0
  318. package/bin/runners/lib/preflight.js +142 -0
  319. package/bin/runners/lib/prerequisites.js +149 -0
  320. package/bin/runners/lib/prove-output.js +220 -0
  321. package/bin/runners/lib/reality/correlation-detectors.js +359 -0
  322. package/bin/runners/lib/reality/index.js +318 -0
  323. package/bin/runners/lib/reality/request-hashing.js +416 -0
  324. package/bin/runners/lib/reality/request-mapper.js +453 -0
  325. package/bin/runners/lib/reality/safety-rails.js +463 -0
  326. package/bin/runners/lib/reality/semantic-snapshot.js +408 -0
  327. package/bin/runners/lib/reality/toast-detector.js +393 -0
  328. package/bin/runners/lib/reality-findings.js +84 -0
  329. package/bin/runners/lib/reality-output.js +231 -0
  330. package/bin/runners/lib/receipts.js +179 -0
  331. package/bin/runners/lib/redact.js +29 -0
  332. package/bin/runners/lib/replay/capsule-manager.js +154 -0
  333. package/bin/runners/lib/replay/index.js +263 -0
  334. package/bin/runners/lib/replay/player.js +348 -0
  335. package/bin/runners/lib/replay/recorder.js +331 -0
  336. package/bin/runners/lib/report-engine.js +626 -0
  337. package/bin/runners/lib/report-html.js +1233 -0
  338. package/bin/runners/lib/report-output.js +366 -0
  339. package/bin/runners/lib/report-templates.js +967 -0
  340. package/bin/runners/lib/report.js +135 -0
  341. package/bin/runners/lib/route-detection.js +1209 -0
  342. package/bin/runners/lib/route-truth.js +1322 -0
  343. package/bin/runners/lib/safelist/index.js +96 -0
  344. package/bin/runners/lib/safelist/integration.js +334 -0
  345. package/bin/runners/lib/safelist/matcher.js +696 -0
  346. package/bin/runners/lib/safelist/schema.js +948 -0
  347. package/bin/runners/lib/safelist/store.js +438 -0
  348. package/bin/runners/lib/sandbox/index.js +59 -0
  349. package/bin/runners/lib/sandbox/proof-chain.js +399 -0
  350. package/bin/runners/lib/sandbox/sandbox-runner.js +205 -0
  351. package/bin/runners/lib/sandbox/worktree.js +174 -0
  352. package/bin/runners/lib/scan-cache.js +330 -0
  353. package/bin/runners/lib/scan-output-schema.js +344 -0
  354. package/bin/runners/lib/scan-output.js +631 -0
  355. package/bin/runners/lib/scan-runner.js +135 -0
  356. package/bin/runners/lib/schema-validator.js +350 -0
  357. package/bin/runners/lib/schemas/ajv-validator.js +464 -0
  358. package/bin/runners/lib/schemas/contracts.schema.json +160 -0
  359. package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
  360. package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
  361. package/bin/runners/lib/schemas/finding.schema.json +100 -0
  362. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -0
  363. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -0
  364. package/bin/runners/lib/schemas/reality-report.schema.json +162 -0
  365. package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
  366. package/bin/runners/lib/schemas/run-request.schema.json +108 -0
  367. package/bin/runners/lib/schemas/share-pack.schema.json +180 -0
  368. package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
  369. package/bin/runners/lib/schemas/ship-report.schema.json +117 -0
  370. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -0
  371. package/bin/runners/lib/schemas/validator.js +465 -0
  372. package/bin/runners/lib/schemas/verdict.schema.json +140 -0
  373. package/bin/runners/lib/score-history.js +282 -0
  374. package/bin/runners/lib/security-bridge.js +249 -0
  375. package/bin/runners/lib/server-usage.js +513 -0
  376. package/bin/runners/lib/share-pack.js +239 -0
  377. package/bin/runners/lib/ship-gate.js +832 -0
  378. package/bin/runners/lib/ship-manifest.js +1153 -0
  379. package/bin/runners/lib/ship-output-enterprise.js +239 -0
  380. package/bin/runners/lib/ship-output.js +1128 -0
  381. package/bin/runners/lib/snippets.js +67 -0
  382. package/bin/runners/lib/status-output.js +340 -0
  383. package/bin/runners/lib/terminal-ui.js +356 -0
  384. package/bin/runners/lib/truth.js +1691 -0
  385. package/bin/runners/lib/ui.js +562 -0
  386. package/bin/runners/lib/unified-cli-output.js +947 -0
  387. package/bin/runners/lib/unified-output.js +197 -0
  388. package/bin/runners/lib/upsell.js +410 -0
  389. package/bin/runners/lib/usage.js +153 -0
  390. package/bin/runners/lib/validate-patch.js +156 -0
  391. package/bin/runners/lib/verdict-engine.js +628 -0
  392. package/bin/runners/lib/verification.js +345 -0
  393. package/bin/runners/lib/why-tree.js +650 -0
  394. package/bin/runners/reality/engine.js +917 -0
  395. package/bin/runners/reality/flows.js +122 -0
  396. package/bin/runners/reality/report.js +378 -0
  397. package/bin/runners/reality/session.js +193 -0
  398. package/bin/runners/runAIAgent.js +229 -0
  399. package/bin/runners/runAgent.d.ts +5 -0
  400. package/bin/runners/runAgent.js +161 -0
  401. package/bin/runners/runAllowlist.js +418 -0
  402. package/bin/runners/runApprove.js +320 -0
  403. package/bin/runners/runAudit.js +692 -0
  404. package/bin/runners/runAuth.js +731 -0
  405. package/bin/runners/runCI.js +353 -0
  406. package/bin/runners/runCheckpoint.js +530 -0
  407. package/bin/runners/runClassify.js +928 -0
  408. package/bin/runners/runCleanup.js +343 -0
  409. package/bin/runners/runContext.d.ts +4 -0
  410. package/bin/runners/runContext.js +175 -0
  411. package/bin/runners/runDoctor.js +877 -0
  412. package/bin/runners/runEvidencePack.js +362 -0
  413. package/bin/runners/runFirewall.d.ts +5 -0
  414. package/bin/runners/runFirewall.js +134 -0
  415. package/bin/runners/runFirewallHook.d.ts +5 -0
  416. package/bin/runners/runFirewallHook.js +56 -0
  417. package/bin/runners/runFix.js +1355 -0
  418. package/bin/runners/runForge.js +451 -0
  419. package/bin/runners/runGuard.js +262 -0
  420. package/bin/runners/runInit.js +1927 -0
  421. package/bin/runners/runIntent.js +906 -0
  422. package/bin/runners/runKickoff.js +878 -0
  423. package/bin/runners/runLabs.js +424 -0
  424. package/bin/runners/runLaunch.js +2000 -0
  425. package/bin/runners/runLink.js +785 -0
  426. package/bin/runners/runMcp.js +1875 -0
  427. package/bin/runners/runPacks.js +2089 -0
  428. package/bin/runners/runPolish.d.ts +4 -0
  429. package/bin/runners/runPolish.js +390 -0
  430. package/bin/runners/runPromptFirewall.js +211 -0
  431. package/bin/runners/runProve.js +1411 -0
  432. package/bin/runners/runQuickstart.js +531 -0
  433. package/bin/runners/runReality.js +2260 -0
  434. package/bin/runners/runReport.js +726 -0
  435. package/bin/runners/runRuntime.js +110 -0
  436. package/bin/runners/runSafelist.js +1190 -0
  437. package/bin/runners/runScan.js +688 -0
  438. package/bin/runners/runShield.js +1282 -0
  439. package/bin/runners/runShip.js +1660 -0
  440. package/bin/runners/runTruth.d.ts +5 -0
  441. package/bin/runners/runTruth.js +101 -0
  442. package/bin/runners/runValidate.js +179 -0
  443. package/bin/runners/runWatch.js +478 -0
  444. package/bin/runners/utils.js +360 -0
  445. package/bin/scan.js +617 -0
  446. package/bin/vibecheck.js +1617 -0
  447. package/dist/guardrail/index.d.ts +2405 -0
  448. package/dist/guardrail/index.js +9747 -0
  449. package/dist/guardrail/index.js.map +1 -0
  450. package/dist/scanner/index.d.ts +282 -0
  451. package/dist/scanner/index.js +3395 -0
  452. package/dist/scanner/index.js.map +1 -0
  453. package/package.json +123 -104
  454. package/README.md +0 -491
  455. package/dist/index.js +0 -99711
  456. package/dist/index.js.map +0 -1
@@ -0,0 +1,1190 @@
1
+ /**
2
+ * vibecheck safelist - Responsible Finding Suppression
3
+ *
4
+ * ═══════════════════════════════════════════════════════════════════════════════
5
+ * A SCALPEL, NOT A TRASH CAN
6
+ * ═══════════════════════════════════════════════════════════════════════════════
7
+ *
8
+ * Let teams suppress known findings responsibly with:
9
+ * - Required justification with category
10
+ * - Owner accountability
11
+ * - Optional expiration (enforced for some categories)
12
+ * - Audit trail
13
+ * - Repo-wide vs local-only scopes
14
+ * - Suppressed findings reported separately
15
+ *
16
+ * Usage:
17
+ * vibecheck safelist # List all entries
18
+ * vibecheck safelist add # Add entry (interactive)
19
+ * vibecheck safelist add --id <id> ... # Add entry (CLI)
20
+ * vibecheck safelist remove --id <id> # Remove entry
21
+ * vibecheck safelist check --id <id> # Check if suppressed
22
+ * vibecheck safelist report # Suppression report
23
+ * vibecheck safelist migrate # Migrate legacy allowlist
24
+ */
25
+
26
+ "use strict";
27
+
28
+ const fs = require("fs");
29
+ const path = require("path");
30
+ const safelist = require("./lib/safelist");
31
+ const { EXIT } = require("./lib/exit-codes");
32
+ const { parseGlobalFlags, shouldSuppressOutput, isJsonMode } = require("./lib/global-flags");
33
+
34
+ // ═══════════════════════════════════════════════════════════════════════════════
35
+ // UNIFIED CLI OUTPUT
36
+ // ═══════════════════════════════════════════════════════════════════════════════
37
+
38
+ let _cliOutput = null;
39
+ function getCliOutput() {
40
+ if (!_cliOutput) _cliOutput = require("./lib/unified-cli-output");
41
+ return _cliOutput;
42
+ }
43
+
44
+ // ═══════════════════════════════════════════════════════════════════════════════
45
+ // TERMINAL STYLING
46
+ // ═══════════════════════════════════════════════════════════════════════════════
47
+
48
+ const SUPPORTS_COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
49
+
50
+ // Use unified CLI output when available, fallback to local
51
+ let c, icons;
52
+ try {
53
+ const cli = getCliOutput();
54
+ c = cli.ansi;
55
+ icons = cli.sym;
56
+ } catch {
57
+ c = SUPPORTS_COLOR ? {
58
+ reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", italic: "\x1b[3m",
59
+ red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m", blue: "\x1b[34m",
60
+ magenta: "\x1b[35m", cyan: "\x1b[36m",
61
+ bgRed: "\x1b[41m", bgGreen: "\x1b[42m", bgYellow: "\x1b[43m",
62
+ } : {
63
+ reset: "", bold: "", dim: "", italic: "",
64
+ red: "", green: "", yellow: "", blue: "", magenta: "", cyan: "",
65
+ bgRed: "", bgGreen: "", bgYellow: "",
66
+ };
67
+ icons = {
68
+ check: "✓", cross: "✗", warning: "⚠", info: "ℹ",
69
+ add: "+", remove: "-", list: "📋", shield: "🛡️",
70
+ clock: "⏱️", person: "👤", lock: "🔒", unlock: "🔓",
71
+ expired: "⌛", review: "👁️",
72
+ };
73
+ }
74
+
75
+ // ═══════════════════════════════════════════════════════════════════════════════
76
+ // HELP
77
+ // ═══════════════════════════════════════════════════════════════════════════════
78
+
79
+ function printHelp(projectRoot = process.cwd()) {
80
+ const categories = Object.entries(safelist.JUSTIFICATION_CATEGORIES)
81
+ .map(([key, val]) => ` ${c.cyan}${key.padEnd(16)}${c.reset} ${val.description}`)
82
+ .join("\n");
83
+
84
+ // Print unified header
85
+ try {
86
+ const cli = getCliOutput();
87
+ cli.renderMinimalHeader("safelist", "free");
88
+ } catch {
89
+ console.log(`\n${c.bold}${icons.shield || "🛡️"} vibecheck safelist${c.reset}\n`);
90
+ }
91
+
92
+ console.log(`${c.bold}SAFELIST${c.reset} - Responsible Finding Suppression
93
+
94
+ ${c.dim}A scalpel, not a trash can. Suppress findings responsibly with
95
+ required justification, owner accountability, and optional expiration.${c.reset}
96
+
97
+ ${c.bold}USAGE${c.reset}
98
+ vibecheck safelist <action> [options]
99
+
100
+ ${c.bold}ACTIONS${c.reset}
101
+ ${c.cyan}list${c.reset} List all safelist entries ${c.dim}(default)${c.reset}
102
+ ${c.cyan}add${c.reset} Add a new entry (requires justification)
103
+ ${c.cyan}remove${c.reset} Remove an entry
104
+ ${c.cyan}check${c.reset} Check if a finding is suppressed
105
+ ${c.cyan}search${c.reset} Search entries by pattern, owner, file, etc.
106
+ ${c.cyan}report${c.reset} Show suppression health report
107
+ ${c.cyan}stats${c.reset} Show detailed statistics
108
+ ${c.cyan}migrate${c.reset} Migrate legacy allowlist.json
109
+ ${c.cyan}validate${c.reset} Validate safelist files
110
+ ${c.cyan}clean${c.reset} Remove expired entries
111
+
112
+ ${c.bold}ADD OPTIONS${c.reset}
113
+ ${c.cyan}--id <finding-id>${c.reset} Specific finding ID to suppress
114
+ ${c.cyan}--pattern <regex>${c.reset} Pattern to match (message, file, etc.)
115
+ ${c.cyan}--rule <rule-id>${c.reset} Rule ID to suppress everywhere
116
+ ${c.cyan}--file <path>${c.reset} File path (supports globs)
117
+ ${c.cyan}--lines <start-end>${c.reset} Line range (e.g., "10-20")
118
+
119
+ ${c.cyan}--reason <text>${c.reset} ${c.yellow}REQUIRED${c.reset} - Why this is suppressed
120
+ ${c.cyan}--category <cat>${c.reset} ${c.yellow}REQUIRED${c.reset} - Justification category (see below)
121
+ ${c.cyan}--ticket <url>${c.reset} Link to issue/ticket (required for some categories)
122
+
123
+ ${c.cyan}--owner <name>${c.reset} ${c.yellow}REQUIRED${c.reset} - Person/team responsible
124
+ ${c.cyan}--email <email>${c.reset} Owner contact email
125
+ ${c.cyan}--team <team>${c.reset} Team name
126
+
127
+ ${c.cyan}--scope <type>${c.reset} Scope: repo, local ${c.dim}(default: repo)${c.reset}
128
+ ${c.cyan}--expires <days>${c.reset} Auto-expire after N days
129
+ ${c.cyan}--review <days>${c.reset} Schedule review after N days
130
+
131
+ ${c.bold}GLOBAL OPTIONS${c.reset}
132
+ ${c.cyan}--json${c.reset} Output as JSON (for CI/automation)
133
+ ${c.cyan}--verbose, -v${c.reset} Show detailed information
134
+ ${c.cyan}--dry-run${c.reset} Preview changes without applying
135
+ ${c.cyan}--no-strict${c.reset} Disable strict validation warnings
136
+ ${c.cyan}--path <dir>${c.reset} Project root ${c.dim}(default: cwd)${c.reset}
137
+
138
+ ${c.bold}JUSTIFICATION CATEGORIES${c.reset}
139
+ ${categories}
140
+
141
+ ${c.bold}EXAMPLES${c.reset}
142
+
143
+ ${c.dim}# Suppress a specific finding (false positive)${c.reset}
144
+ vibecheck safelist add \\
145
+ --id MOCK_DATA_abc123 \\
146
+ --reason "Test fixture data, not production code" \\
147
+ --category false-positive \\
148
+ --owner "Jane Developer"
149
+
150
+ ${c.dim}# Suppress a pattern in test files${c.reset}
151
+ vibecheck safelist add \\
152
+ --pattern "lorem ipsum" \\
153
+ --file "**/*.test.ts" \\
154
+ --reason "Placeholder text in tests" \\
155
+ --category test-fixture \\
156
+ --owner "QA Team"
157
+
158
+ ${c.dim}# Accept risk for legacy code with expiration${c.reset}
159
+ vibecheck safelist add \\
160
+ --pattern "vulnerable-function" \\
161
+ --reason "Legacy code pending refactor - tracked in JIRA-123" \\
162
+ --category accepted-risk \\
163
+ --ticket "https://jira.example.com/JIRA-123" \\
164
+ --owner "Security Team" \\
165
+ --expires 90
166
+
167
+ ${c.dim}# Local-only suppression (not committed)${c.reset}
168
+ vibecheck safelist add \\
169
+ --id DEV_xyz \\
170
+ --reason "Local dev environment artifact" \\
171
+ --category temporary \\
172
+ --owner "Me" \\
173
+ --scope local \\
174
+ --expires 7
175
+
176
+ ${c.dim}# Show suppression health report${c.reset}
177
+ vibecheck safelist report
178
+
179
+ ${c.dim}# Clean up expired entries${c.reset}
180
+ vibecheck safelist clean
181
+
182
+ ${c.bold}SCOPE TYPES${c.reset}
183
+ ${c.cyan}repo${c.reset} Committed to version control, applies to everyone
184
+ ${c.cyan}local${c.reset} Machine-specific, .gitignored
185
+
186
+ ${c.bold}ENFORCEMENT${c.reset}
187
+ Safelist is checked by: ${safelist.SAFELIST_COMMANDS.map(cmd => c.cyan + cmd + c.reset).join(", ")}
188
+ Suppressed findings are reported separately (not hidden).
189
+ `);
190
+ }
191
+
192
+ // ═══════════════════════════════════════════════════════════════════════════════
193
+ // ARGUMENT PARSING
194
+ // ═══════════════════════════════════════════════════════════════════════════════
195
+
196
+ /**
197
+ * Check for conflicts with existing entries
198
+ */
199
+ function checkForConflicts(newEntry, existingSafelist) {
200
+ const conflicts = [];
201
+
202
+ if (!existingSafelist?.entries) return conflicts;
203
+
204
+ for (const existing of existingSafelist.entries) {
205
+ // Same finding ID
206
+ if (newEntry.target?.findingId && existing.target?.findingId === newEntry.target.findingId) {
207
+ conflicts.push({
208
+ id: existing.id,
209
+ type: "duplicate",
210
+ reason: `Same finding ID: ${newEntry.target.findingId}`,
211
+ });
212
+ continue;
213
+ }
214
+
215
+ // Same rule ID
216
+ if (newEntry.target?.ruleId && existing.target?.ruleId === newEntry.target.ruleId) {
217
+ conflicts.push({
218
+ id: existing.id,
219
+ type: "duplicate",
220
+ reason: `Same rule ID: ${newEntry.target.ruleId}`,
221
+ });
222
+ continue;
223
+ }
224
+
225
+ // Overlapping patterns
226
+ if (newEntry.target?.pattern && existing.target?.pattern) {
227
+ if (safelist.patternsOverlap(newEntry.target.pattern, existing.target.pattern)) {
228
+ conflicts.push({
229
+ id: existing.id,
230
+ type: "overlap",
231
+ reason: `Overlapping pattern with: ${existing.target.pattern}`,
232
+ });
233
+ continue;
234
+ }
235
+ }
236
+
237
+ // Same file (without different line ranges)
238
+ if (newEntry.target?.file && existing.target?.file) {
239
+ if (safelist.filesOverlap(newEntry.target.file, existing.target.file)) {
240
+ // Only conflict if line ranges overlap or are not specified
241
+ if (!newEntry.target.lines || !existing.target.lines ||
242
+ rangesOverlap(newEntry.target.lines, existing.target.lines)) {
243
+ conflicts.push({
244
+ id: existing.id,
245
+ type: "overlap",
246
+ reason: `Overlapping file scope: ${existing.target.file}`,
247
+ });
248
+ }
249
+ }
250
+ }
251
+ }
252
+
253
+ return conflicts;
254
+ }
255
+
256
+ /**
257
+ * Check if two line ranges overlap
258
+ */
259
+ function rangesOverlap(range1, range2) {
260
+ if (!range1 || !range2) return true; // No range = all lines = overlap
261
+ return range1[0] <= range2[1] && range2[0] <= range1[1];
262
+ }
263
+
264
+ function parseArgs(args) {
265
+ const opts = {
266
+ action: "list",
267
+ // Target
268
+ id: null,
269
+ pattern: null,
270
+ rule: null,
271
+ file: null,
272
+ lines: null,
273
+ // Justification
274
+ reason: null,
275
+ category: null,
276
+ ticket: null,
277
+ // Owner
278
+ owner: null,
279
+ email: null,
280
+ team: null,
281
+ // Scope & lifecycle
282
+ scope: "repo",
283
+ expires: null,
284
+ review: null,
285
+ commands: null,
286
+ // General
287
+ help: false,
288
+ json: false,
289
+ verbose: false,
290
+ dryRun: false,
291
+ strict: true,
292
+ path: process.cwd(),
293
+ };
294
+
295
+ for (let i = 0; i < args.length; i++) {
296
+ const arg = args[i];
297
+ const next = args[i + 1];
298
+
299
+ // Flags
300
+ if (arg === "--help" || arg === "-h") opts.help = true;
301
+ else if (arg === "--json") opts.json = true;
302
+ else if (arg === "--verbose" || arg === "-v") opts.verbose = true;
303
+ else if (arg === "--dry-run" || arg === "--preview") opts.dryRun = true;
304
+ else if (arg === "--no-strict") opts.strict = false;
305
+ // Target
306
+ else if (arg === "--id") opts.id = args[++i];
307
+ else if (arg === "--pattern") opts.pattern = args[++i];
308
+ else if (arg === "--rule") opts.rule = args[++i];
309
+ else if (arg === "--file" || arg === "-f") opts.file = args[++i];
310
+ else if (arg === "--lines") opts.lines = args[++i];
311
+ // Justification
312
+ else if (arg === "--reason" || arg === "-r") opts.reason = args[++i];
313
+ else if (arg === "--category" || arg === "-c") opts.category = args[++i];
314
+ else if (arg === "--ticket" || arg === "-t") opts.ticket = args[++i];
315
+ // Owner
316
+ else if (arg === "--owner" || arg === "-o") opts.owner = args[++i];
317
+ else if (arg === "--email") opts.email = args[++i];
318
+ else if (arg === "--team") opts.team = args[++i];
319
+ // Scope & lifecycle
320
+ else if (arg === "--scope" || arg === "-s") opts.scope = args[++i];
321
+ else if (arg === "--expires" || arg === "-e") opts.expires = parseInt(args[++i], 10);
322
+ else if (arg === "--review") opts.review = parseInt(args[++i], 10);
323
+ else if (arg === "--commands") opts.commands = args[++i].split(",");
324
+ else if (arg === "--path" || arg === "-p") opts.path = args[++i];
325
+ // Action (positional)
326
+ else if (!arg.startsWith("-") && !opts.action || opts.action === "list") {
327
+ if (["list", "add", "remove", "check", "report", "migrate", "validate", "clean", "preview", "search", "stats"].includes(arg)) {
328
+ opts.action = arg;
329
+ }
330
+ }
331
+ }
332
+
333
+ return opts;
334
+ }
335
+
336
+ // ═══════════════════════════════════════════════════════════════════════════════
337
+ // ACTIONS
338
+ // ═══════════════════════════════════════════════════════════════════════════════
339
+
340
+ async function actionList(projectRoot, opts) {
341
+ const { safelist: sl, warnings, errors } = safelist.loadSafelist(projectRoot);
342
+
343
+ if (opts.json) {
344
+ console.log(JSON.stringify({
345
+ entries: sl.entries,
346
+ warnings,
347
+ errors,
348
+ stats: {
349
+ total: sl.entries.length,
350
+ repo: sl.entries.filter(e => e._source === "repo").length,
351
+ local: sl.entries.filter(e => e._source === "local").length,
352
+ expired: safelist.getExpired(sl).length,
353
+ expiringSoon: safelist.getExpiringSoon(sl).length,
354
+ },
355
+ }, null, 2));
356
+ return EXIT.SUCCESS;
357
+ }
358
+
359
+ console.log(`\n${c.bold}${icons.list} Safelist Entries${c.reset}\n`);
360
+
361
+ if (errors.length > 0) {
362
+ for (const err of errors) {
363
+ console.log(` ${c.red}${icons.cross}${c.reset} ${err}`);
364
+ }
365
+ console.log();
366
+ }
367
+
368
+ if (warnings.length > 0) {
369
+ for (const warn of warnings) {
370
+ console.log(` ${c.yellow}${icons.warning}${c.reset} ${warn}`);
371
+ }
372
+ console.log();
373
+ }
374
+
375
+ // Filter out expired
376
+ const now = Date.now();
377
+ const active = sl.entries.filter(e => {
378
+ if (!e.lifecycle?.expiresAt) return true;
379
+ return new Date(e.lifecycle.expiresAt).getTime() > now;
380
+ });
381
+
382
+ if (active.length === 0) {
383
+ console.log(` ${c.dim}No active entries in safelist.${c.reset}`);
384
+ console.log(` ${c.dim}Add entries with: vibecheck safelist add --id <id> --reason "..." --category <cat> --owner <name>${c.reset}\n`);
385
+ return EXIT.SUCCESS;
386
+ }
387
+
388
+ // Group by source
389
+ const bySource = { repo: [], local: [] };
390
+ for (const entry of active) {
391
+ (bySource[entry._source] || bySource.repo).push(entry);
392
+ }
393
+
394
+ for (const [source, entries] of Object.entries(bySource)) {
395
+ if (entries.length === 0) continue;
396
+
397
+ const scopeIcon = source === "local" ? icons.unlock : icons.lock;
398
+ console.log(`${c.bold}${scopeIcon} ${source.toUpperCase()} SCOPE${c.reset} ${c.dim}(${entries.length})${c.reset}\n`);
399
+
400
+ for (const entry of entries) {
401
+ printEntry(entry, opts.verbose);
402
+ }
403
+ }
404
+
405
+ // Summary
406
+ const expired = safelist.getExpired(sl);
407
+ const expiringSoon = safelist.getExpiringSoon(sl);
408
+ const dueForReview = safelist.getDueForReview(sl);
409
+
410
+ console.log(`${c.dim}${"─".repeat(60)}${c.reset}`);
411
+ console.log(` Total: ${active.length} active, ${expired.length} expired`);
412
+ if (expiringSoon.length > 0) {
413
+ console.log(` ${c.yellow}${icons.clock}${c.reset} ${expiringSoon.length} expiring soon (7 days)`);
414
+ }
415
+ if (dueForReview.length > 0) {
416
+ console.log(` ${c.yellow}${icons.review}${c.reset} ${dueForReview.length} due for review`);
417
+ }
418
+ console.log();
419
+
420
+ return EXIT.SUCCESS;
421
+ }
422
+
423
+ function printEntry(entry, verbose = false) {
424
+ const cat = safelist.JUSTIFICATION_CATEGORIES[entry.justification?.category];
425
+ const catBadge = cat ? `${c.cyan}[${entry.justification.category}]${c.reset}` : "";
426
+
427
+ // Expiry indicator
428
+ let expiryStr = "";
429
+ if (entry.lifecycle?.expiresAt) {
430
+ const expiresAt = new Date(entry.lifecycle.expiresAt);
431
+ const daysLeft = Math.ceil((expiresAt - Date.now()) / (1000 * 60 * 60 * 24));
432
+ if (daysLeft <= 7) {
433
+ expiryStr = `${c.yellow}${icons.clock} ${daysLeft}d${c.reset}`;
434
+ } else {
435
+ expiryStr = `${c.dim}expires ${expiresAt.toLocaleDateString()}${c.reset}`;
436
+ }
437
+ }
438
+
439
+ console.log(` ${c.green}${icons.check}${c.reset} ${c.bold}${entry.id}${c.reset} ${catBadge} ${expiryStr}`);
440
+
441
+ // Target
442
+ if (entry.target?.findingId) {
443
+ console.log(` ${c.dim}Finding:${c.reset} ${entry.target.findingId}`);
444
+ }
445
+ if (entry.target?.pattern) {
446
+ console.log(` ${c.dim}Pattern:${c.reset} ${entry.target.pattern}`);
447
+ }
448
+ if (entry.target?.ruleId) {
449
+ console.log(` ${c.dim}Rule:${c.reset} ${entry.target.ruleId}`);
450
+ }
451
+ if (entry.target?.file) {
452
+ console.log(` ${c.dim}File:${c.reset} ${entry.target.file}${entry.target.lines ? `:${entry.target.lines.join("-")}` : ""}`);
453
+ }
454
+
455
+ // Justification
456
+ console.log(` ${c.dim}Reason:${c.reset} ${entry.justification?.reason || "No reason"}`);
457
+ if (entry.justification?.ticket) {
458
+ console.log(` ${c.dim}Ticket:${c.reset} ${entry.justification.ticket}`);
459
+ }
460
+
461
+ // Owner
462
+ console.log(` ${c.dim}Owner:${c.reset} ${entry.owner?.name || "Unknown"}${entry.owner?.team ? ` (${entry.owner.team})` : ""}`);
463
+
464
+ if (verbose) {
465
+ console.log(` ${c.dim}Created:${c.reset} ${entry.lifecycle?.createdAt} by ${entry.lifecycle?.createdBy || "unknown"}`);
466
+ console.log(` ${c.dim}Matches:${c.reset} ${entry.lifecycle?.matchCount || 0}`);
467
+ if (entry.lifecycle?.lastMatchedAt) {
468
+ console.log(` ${c.dim}Last match:${c.reset} ${entry.lifecycle.lastMatchedAt}`);
469
+ }
470
+ }
471
+
472
+ console.log();
473
+ }
474
+
475
+ async function actionAdd(projectRoot, opts) {
476
+ // Validate required fields
477
+ const missingFields = [];
478
+
479
+ if (!opts.id && !opts.pattern && !opts.rule && !opts.file) {
480
+ missingFields.push("target (--id, --pattern, --rule, or --file)");
481
+ }
482
+ if (!opts.reason || opts.reason.length < 10) {
483
+ missingFields.push("--reason (min 10 characters)");
484
+ }
485
+ if (!opts.category) {
486
+ missingFields.push("--category");
487
+ } else if (!safelist.JUSTIFICATION_CATEGORIES[opts.category]) {
488
+ console.error(`\n ${c.red}${icons.cross}${c.reset} Invalid category: ${opts.category}`);
489
+ console.error(` ${c.dim}Valid categories: ${Object.keys(safelist.JUSTIFICATION_CATEGORIES).join(", ")}${c.reset}\n`);
490
+ return EXIT.USER_ERROR;
491
+ }
492
+ if (!opts.owner) {
493
+ missingFields.push("--owner");
494
+ }
495
+
496
+ if (missingFields.length > 0) {
497
+ console.error(`\n ${c.red}${icons.cross}${c.reset} Missing required fields:`);
498
+ for (const field of missingFields) {
499
+ console.error(` ${c.yellow}•${c.reset} ${field}`);
500
+ }
501
+ console.error(`\n ${c.dim}Run 'vibecheck safelist --help' for usage${c.reset}\n`);
502
+ return EXIT.USER_ERROR;
503
+ }
504
+
505
+ // Check if category requires ticket
506
+ const category = safelist.JUSTIFICATION_CATEGORIES[opts.category];
507
+ if (category.requiresTicket && !opts.ticket) {
508
+ console.error(`\n ${c.red}${icons.cross}${c.reset} Category '${opts.category}' requires --ticket`);
509
+ console.error(` ${c.dim}${category.description}${c.reset}\n`);
510
+ return EXIT.USER_ERROR;
511
+ }
512
+
513
+ // Check expiry constraints
514
+ if (category.maxExpiry) {
515
+ if (!opts.expires) {
516
+ console.error(`\n ${c.red}${icons.cross}${c.reset} Category '${opts.category}' requires --expires (max ${category.maxExpiry} days)`);
517
+ return EXIT.USER_ERROR;
518
+ }
519
+ if (opts.expires > category.maxExpiry) {
520
+ console.error(`\n ${c.red}${icons.cross}${c.reset} Category '${opts.category}' max expiry is ${category.maxExpiry} days`);
521
+ return EXIT.USER_ERROR;
522
+ }
523
+ }
524
+
525
+ // Determine entry type
526
+ let type = "finding";
527
+ if (opts.pattern) type = "pattern";
528
+ else if (opts.rule) type = "rule";
529
+ else if (opts.file && !opts.id) type = "file";
530
+
531
+ // Parse lines
532
+ let lines = null;
533
+ if (opts.lines) {
534
+ const parts = opts.lines.split("-").map(n => parseInt(n, 10));
535
+ lines = [parts[0], parts[1] || parts[0]];
536
+ }
537
+
538
+ // Create entry
539
+ const entry = safelist.createEntry({
540
+ type,
541
+ findingId: opts.id,
542
+ pattern: opts.pattern,
543
+ ruleId: opts.rule,
544
+ file: opts.file,
545
+ lines,
546
+ reason: opts.reason,
547
+ category: opts.category,
548
+ ticket: opts.ticket,
549
+ ownerName: opts.owner,
550
+ ownerEmail: opts.email,
551
+ ownerTeam: opts.team,
552
+ scopeType: opts.scope,
553
+ commands: opts.commands,
554
+ expiresIn: opts.expires,
555
+ reviewIn: opts.review,
556
+ });
557
+
558
+ // Validate
559
+ const validation = safelist.validateEntry(entry);
560
+ if (!validation.valid) {
561
+ console.error(`\n ${c.red}${icons.cross}${c.reset} Validation failed:`);
562
+ for (const err of validation.errors) {
563
+ console.error(` ${c.yellow}•${c.reset} ${err}`);
564
+ }
565
+ return EXIT.USER_ERROR;
566
+ }
567
+
568
+ // Show warnings
569
+ if (validation.warnings && validation.warnings.length > 0 && !opts.json) {
570
+ console.log(`\n ${c.yellow}${icons.warning}${c.reset} Warnings:`);
571
+ for (const warn of validation.warnings) {
572
+ console.log(` ${c.dim}•${c.reset} ${warn}`);
573
+ }
574
+ }
575
+
576
+ // Check for duplicate/overlapping entries
577
+ const { safelist: existingSafelist } = safelist.loadSafelist(projectRoot);
578
+ const conflicts = checkForConflicts(entry, existingSafelist);
579
+
580
+ if (conflicts.length > 0 && !opts.json) {
581
+ console.log(`\n ${c.yellow}${icons.warning}${c.reset} Potential conflicts with existing entries:`);
582
+ for (const conflict of conflicts) {
583
+ console.log(` ${c.dim}•${c.reset} ${conflict.id}: ${conflict.reason}`);
584
+ }
585
+ console.log(` ${c.dim}Consider removing duplicates or using more specific patterns.${c.reset}`);
586
+ }
587
+
588
+ // Dry-run mode
589
+ if (opts.dryRun) {
590
+ if (opts.json) {
591
+ console.log(JSON.stringify({
592
+ dryRun: true,
593
+ entry,
594
+ validation,
595
+ conflicts,
596
+ }, null, 2));
597
+ } else {
598
+ console.log(`\n ${c.cyan}${icons.info}${c.reset} Dry-run: Would add entry:`);
599
+ printEntry(entry, true);
600
+ }
601
+ return EXIT.SUCCESS;
602
+ }
603
+
604
+ // Save
605
+ const result = safelist.saveEntry(projectRoot, entry, opts.scope);
606
+
607
+ if (!result.success) {
608
+ console.error(`\n ${c.red}${icons.cross}${c.reset} Failed to save: ${result.error}\n`);
609
+ return EXIT.INTERNAL_ERROR;
610
+ }
611
+
612
+ // Ensure local safelist is gitignored
613
+ if (opts.scope === "local") {
614
+ safelist.ensureGitIgnore(projectRoot);
615
+ }
616
+
617
+ if (opts.json) {
618
+ console.log(JSON.stringify({ success: true, entry }, null, 2));
619
+ } else {
620
+ console.log(`\n ${c.green}${icons.add}${c.reset} Added safelist entry: ${c.bold}${entry.id}${c.reset}`);
621
+ console.log(` ${c.dim}Scope: ${opts.scope} | Category: ${opts.category}${c.reset}`);
622
+ if (entry.lifecycle.expiresAt) {
623
+ console.log(` ${c.dim}Expires: ${new Date(entry.lifecycle.expiresAt).toLocaleDateString()}${c.reset}`);
624
+ }
625
+ console.log();
626
+ }
627
+
628
+ return EXIT.SUCCESS;
629
+ }
630
+
631
+ async function actionRemove(projectRoot, opts) {
632
+ if (!opts.id) {
633
+ console.error(`\n ${c.red}${icons.cross}${c.reset} --id is required for remove\n`);
634
+ return EXIT.USER_ERROR;
635
+ }
636
+
637
+ const result = safelist.removeEntry(projectRoot, opts.id);
638
+
639
+ if (!result.success) {
640
+ console.error(`\n ${c.red}${icons.cross}${c.reset} ${result.error}\n`);
641
+ return EXIT.INTERNAL_ERROR;
642
+ }
643
+
644
+ if (opts.json) {
645
+ console.log(JSON.stringify({ success: true, removed: result.removed, source: result.source }, null, 2));
646
+ } else if (result.removed) {
647
+ console.log(`\n ${c.green}${icons.remove}${c.reset} Removed entry: ${c.bold}${opts.id}${c.reset}`);
648
+ console.log(` ${c.dim}From: ${result.source} safelist${c.reset}\n`);
649
+ } else {
650
+ console.log(`\n ${c.yellow}${icons.warning}${c.reset} Entry not found: ${opts.id}\n`);
651
+ }
652
+
653
+ return result.removed ? EXIT.SUCCESS : EXIT.NOT_FOUND;
654
+ }
655
+
656
+ async function actionCheck(projectRoot, opts) {
657
+ if (!opts.id) {
658
+ console.error(`\n ${c.red}${icons.cross}${c.reset} --id is required for check\n`);
659
+ return EXIT.USER_ERROR;
660
+ }
661
+
662
+ const { safelist: sl } = safelist.loadSafelist(projectRoot);
663
+ const result = safelist.isSuppressed({ id: opts.id }, sl);
664
+
665
+ if (opts.json) {
666
+ console.log(JSON.stringify(result, null, 2));
667
+ } else if (result.suppressed) {
668
+ console.log(`\n ${c.green}${icons.check}${c.reset} ${c.bold}Suppressed${c.reset}`);
669
+ console.log(` ${c.dim}Entry:${c.reset} ${result.entry?.id}`);
670
+ console.log(` ${c.dim}Reason:${c.reset} ${result.reason}`);
671
+ console.log(` ${c.dim}Owner:${c.reset} ${result.entry?.owner?.name || "Unknown"}\n`);
672
+ } else {
673
+ console.log(`\n ${c.yellow}${icons.cross}${c.reset} ${c.bold}Not suppressed${c.reset}\n`);
674
+ }
675
+
676
+ return result.suppressed ? EXIT.SUCCESS : EXIT.BLOCKING;
677
+ }
678
+
679
+ async function actionReport(projectRoot, opts) {
680
+ const { safelist: sl, warnings, errors } = safelist.loadSafelist(projectRoot);
681
+
682
+ const expired = safelist.getExpired(sl);
683
+ const expiringSoon = safelist.getExpiringSoon(sl);
684
+ const dueForReview = safelist.getDueForReview(sl);
685
+ const unused = safelist.getUnused(sl);
686
+
687
+ // Group by category
688
+ const byCategory = {};
689
+ for (const entry of sl.entries) {
690
+ const cat = entry.justification?.category || "unknown";
691
+ if (!byCategory[cat]) byCategory[cat] = [];
692
+ byCategory[cat].push(entry);
693
+ }
694
+
695
+ // Group by owner
696
+ const byOwner = {};
697
+ for (const entry of sl.entries) {
698
+ const owner = entry.owner?.name || "Unknown";
699
+ if (!byOwner[owner]) byOwner[owner] = [];
700
+ byOwner[owner].push(entry);
701
+ }
702
+
703
+ if (opts.json) {
704
+ console.log(JSON.stringify({
705
+ summary: {
706
+ total: sl.entries.length,
707
+ repo: sl.entries.filter(e => e._source === "repo").length,
708
+ local: sl.entries.filter(e => e._source === "local").length,
709
+ },
710
+ health: {
711
+ expired: expired.length,
712
+ expiringSoon: expiringSoon.length,
713
+ dueForReview: dueForReview.length,
714
+ unused: unused.length,
715
+ },
716
+ byCategory,
717
+ byOwner,
718
+ warnings,
719
+ errors,
720
+ }, null, 2));
721
+ return EXIT.SUCCESS;
722
+ }
723
+
724
+ console.log(`
725
+ ${c.bold}╔══════════════════════════════════════════════════════════════════════════════╗
726
+ ║ ║
727
+ ║ ${icons.shield} ${c.cyan}SAFELIST HEALTH REPORT${c.reset}${c.bold} ║
728
+ ║ ║
729
+ ╚══════════════════════════════════════════════════════════════════════════════╝${c.reset}
730
+ `);
731
+
732
+ // Summary
733
+ console.log(`${c.bold}${icons.list} SUMMARY${c.reset}`);
734
+ console.log(`${"─".repeat(60)}`);
735
+ console.log(` Total entries: ${c.bold}${sl.entries.length}${c.reset}`);
736
+ console.log(` Repo-wide: ${sl.entries.filter(e => e._source === "repo").length}`);
737
+ console.log(` Local-only: ${sl.entries.filter(e => e._source === "local").length}`);
738
+ console.log();
739
+
740
+ // Health
741
+ console.log(`${c.bold}${icons.warning} HEALTH${c.reset}`);
742
+ console.log(`${"─".repeat(60)}`);
743
+
744
+ const healthyColor = (count, threshold) => count > threshold ? c.red : count > 0 ? c.yellow : c.green;
745
+
746
+ console.log(` ${healthyColor(expired.length, 0)}${icons.expired} Expired:${c.reset} ${expired.length}`);
747
+ console.log(` ${healthyColor(expiringSoon.length, 5)}${icons.clock} Expiring soon:${c.reset} ${expiringSoon.length}`);
748
+ console.log(` ${healthyColor(dueForReview.length, 3)}${icons.review} Due for review:${c.reset} ${dueForReview.length}`);
749
+ console.log(` ${healthyColor(unused.length, 10)}${icons.info} Unused (30d):${c.reset} ${unused.length}`);
750
+ console.log();
751
+
752
+ // By category
753
+ console.log(`${c.bold}${icons.info} BY CATEGORY${c.reset}`);
754
+ console.log(`${"─".repeat(60)}`);
755
+ for (const [cat, entries] of Object.entries(byCategory)) {
756
+ const catInfo = safelist.JUSTIFICATION_CATEGORIES[cat];
757
+ console.log(` ${cat.padEnd(20)} ${entries.length} ${c.dim}(${catInfo?.name || "Unknown"})${c.reset}`);
758
+ }
759
+ console.log();
760
+
761
+ // By owner
762
+ console.log(`${c.bold}${icons.person} BY OWNER${c.reset}`);
763
+ console.log(`${"─".repeat(60)}`);
764
+ for (const [owner, entries] of Object.entries(byOwner)) {
765
+ console.log(` ${owner.padEnd(20)} ${entries.length}`);
766
+ }
767
+ console.log();
768
+
769
+ // Action items
770
+ if (expired.length > 0 || dueForReview.length > 0 || unused.length > 5) {
771
+ console.log(`${c.bold}${icons.warning} ACTION ITEMS${c.reset}`);
772
+ console.log(`${"─".repeat(60)}`);
773
+
774
+ if (expired.length > 0) {
775
+ console.log(` ${c.red}•${c.reset} Clean up ${expired.length} expired entries: ${c.cyan}vibecheck safelist clean${c.reset}`);
776
+ }
777
+ if (dueForReview.length > 0) {
778
+ console.log(` ${c.yellow}•${c.reset} Review ${dueForReview.length} entries due for review`);
779
+ }
780
+ if (unused.length > 5) {
781
+ console.log(` ${c.yellow}•${c.reset} Consider removing ${unused.length} unused entries`);
782
+ }
783
+ console.log();
784
+ }
785
+
786
+ return EXIT.SUCCESS;
787
+ }
788
+
789
+ async function actionMigrate(projectRoot, opts) {
790
+ const result = safelist.migrateLegacyAllowlist(projectRoot);
791
+
792
+ if (opts.json) {
793
+ console.log(JSON.stringify(result, null, 2));
794
+ } else if (result.success) {
795
+ if (result.migrated > 0) {
796
+ console.log(`\n ${c.green}${icons.check}${c.reset} Migrated ${result.migrated} entries from legacy allowlist`);
797
+ console.log(` ${c.dim}Original file renamed to allowlist.json.migrated${c.reset}\n`);
798
+ } else {
799
+ console.log(`\n ${c.dim}No legacy allowlist.json found${c.reset}\n`);
800
+ }
801
+ } else {
802
+ console.error(`\n ${c.red}${icons.cross}${c.reset} Migration failed: ${result.error}\n`);
803
+ }
804
+
805
+ return result.success ? EXIT.SUCCESS : EXIT.INTERNAL_ERROR;
806
+ }
807
+
808
+ async function actionValidate(projectRoot, opts) {
809
+ const { safelist: sl, warnings, errors } = safelist.loadSafelist(projectRoot);
810
+
811
+ const validation = safelist.validateSafelist(sl);
812
+ const allErrors = [...errors, ...validation.errors];
813
+ const allWarnings = [...warnings, ...validation.warnings];
814
+
815
+ if (opts.json) {
816
+ console.log(JSON.stringify({
817
+ valid: allErrors.length === 0,
818
+ errors: allErrors,
819
+ warnings: allWarnings,
820
+ }, null, 2));
821
+ } else {
822
+ if (allErrors.length === 0 && allWarnings.length === 0) {
823
+ console.log(`\n ${c.green}${icons.check}${c.reset} Safelist is valid\n`);
824
+ } else {
825
+ if (allErrors.length > 0) {
826
+ console.log(`\n ${c.red}${icons.cross} ERRORS${c.reset}`);
827
+ for (const err of allErrors) {
828
+ console.log(` ${c.red}•${c.reset} ${err}`);
829
+ }
830
+ }
831
+ if (allWarnings.length > 0) {
832
+ console.log(`\n ${c.yellow}${icons.warning} WARNINGS${c.reset}`);
833
+ for (const warn of allWarnings) {
834
+ console.log(` ${c.yellow}•${c.reset} ${warn}`);
835
+ }
836
+ }
837
+ console.log();
838
+ }
839
+ }
840
+
841
+ return allErrors.length === 0 ? EXIT.SUCCESS : EXIT.BLOCKING;
842
+ }
843
+
844
+ async function actionClean(projectRoot, opts) {
845
+ const { safelist: sl, paths } = safelist.loadSafelist(projectRoot);
846
+ const expired = safelist.getExpired(sl);
847
+
848
+ if (expired.length === 0) {
849
+ if (!opts.json) {
850
+ console.log(`\n ${c.dim}No expired entries to clean${c.reset}\n`);
851
+ } else {
852
+ console.log(JSON.stringify({ cleaned: 0 }));
853
+ }
854
+ return EXIT.SUCCESS;
855
+ }
856
+
857
+ // Dry-run mode
858
+ if (opts.dryRun) {
859
+ if (opts.json) {
860
+ console.log(JSON.stringify({
861
+ dryRun: true,
862
+ wouldClean: expired.map(e => ({ id: e.id, expiresAt: e.lifecycle?.expiresAt })),
863
+ }, null, 2));
864
+ } else {
865
+ console.log(`\n ${c.cyan}${icons.info}${c.reset} Dry-run: Would clean ${expired.length} entries:`);
866
+ for (const entry of expired) {
867
+ console.log(` ${c.dim}•${c.reset} ${entry.id} (expired ${new Date(entry.lifecycle?.expiresAt).toLocaleDateString()})`);
868
+ }
869
+ console.log();
870
+ }
871
+ return EXIT.SUCCESS;
872
+ }
873
+
874
+ // Remove expired entries
875
+ let cleaned = 0;
876
+ for (const entry of expired) {
877
+ const result = safelist.removeEntry(projectRoot, entry.id);
878
+ if (result.removed) cleaned++;
879
+ }
880
+
881
+ if (opts.json) {
882
+ console.log(JSON.stringify({ cleaned }));
883
+ } else {
884
+ console.log(`\n ${c.green}${icons.remove}${c.reset} Cleaned ${cleaned} expired entries\n`);
885
+ }
886
+
887
+ return EXIT.SUCCESS;
888
+ }
889
+
890
+ // ═══════════════════════════════════════════════════════════════════════════════
891
+ // MAIN
892
+ // ═══════════════════════════════════════════════════════════════════════════════
893
+
894
+ async function runSafelist(args = [], context = {}) {
895
+ const { flags: globalFlags } = parseGlobalFlags(args);
896
+ const opts = parseArgs(args);
897
+
898
+ if (opts.help || globalFlags.help) {
899
+ printHelp();
900
+ return EXIT.SUCCESS;
901
+ }
902
+
903
+ opts.json = opts.json || isJsonMode(globalFlags);
904
+ const projectRoot = path.resolve(opts.path || context.repoRoot || process.cwd());
905
+
906
+ // Validate project exists
907
+ if (!fs.existsSync(projectRoot)) {
908
+ if (opts.json) {
909
+ console.log(JSON.stringify({ error: `Path not found: ${projectRoot}` }));
910
+ } else {
911
+ console.error(`\n ${c.red}${icons.cross}${c.reset} Path not found: ${projectRoot}\n`);
912
+ }
913
+ return EXIT.NOT_FOUND;
914
+ }
915
+
916
+ switch (opts.action) {
917
+ case "list":
918
+ return actionList(projectRoot, opts);
919
+ case "add":
920
+ return actionAdd(projectRoot, opts);
921
+ case "remove":
922
+ return actionRemove(projectRoot, opts);
923
+ case "check":
924
+ return actionCheck(projectRoot, opts);
925
+ case "report":
926
+ return actionReport(projectRoot, opts);
927
+ case "migrate":
928
+ return actionMigrate(projectRoot, opts);
929
+ case "validate":
930
+ return actionValidate(projectRoot, opts);
931
+ case "clean":
932
+ return actionClean(projectRoot, opts);
933
+ case "search":
934
+ return actionSearch(projectRoot, opts);
935
+ case "stats":
936
+ return actionStats(projectRoot, opts);
937
+ default:
938
+ console.error(`\n ${c.red}${icons.cross}${c.reset} Unknown action: ${opts.action}\n`);
939
+ return EXIT.USER_ERROR;
940
+ }
941
+ }
942
+
943
+ // ═══════════════════════════════════════════════════════════════════════════════
944
+ // ADDITIONAL ACTIONS
945
+ // ═══════════════════════════════════════════════════════════════════════════════
946
+
947
+ async function actionSearch(projectRoot, opts) {
948
+ const { safelist: sl } = safelist.loadSafelist(projectRoot);
949
+
950
+ // Search criteria
951
+ const query = opts.pattern || opts.id || opts.file || opts.owner;
952
+ if (!query) {
953
+ console.error(`\n ${c.red}${icons.cross}${c.reset} Search requires --pattern, --id, --file, or --owner\n`);
954
+ return EXIT.USER_ERROR;
955
+ }
956
+
957
+ const results = sl.entries.filter(entry => {
958
+ // Search by ID
959
+ if (opts.id && entry.id.toLowerCase().includes(opts.id.toLowerCase())) {
960
+ return true;
961
+ }
962
+
963
+ // Search by pattern
964
+ if (opts.pattern) {
965
+ const targets = [
966
+ entry.target?.findingId,
967
+ entry.target?.pattern,
968
+ entry.target?.ruleId,
969
+ entry.target?.file,
970
+ entry.justification?.reason,
971
+ ].filter(Boolean);
972
+
973
+ const regex = safelist.getCachedRegex(opts.pattern, "i");
974
+ if (regex && targets.some(t => regex.test(t))) {
975
+ return true;
976
+ }
977
+ }
978
+
979
+ // Search by file
980
+ if (opts.file && entry.target?.file) {
981
+ if (entry.target.file.toLowerCase().includes(opts.file.toLowerCase())) {
982
+ return true;
983
+ }
984
+ }
985
+
986
+ // Search by owner
987
+ if (opts.owner) {
988
+ const ownerStr = `${entry.owner?.name || ""} ${entry.owner?.team || ""} ${entry.owner?.email || ""}`.toLowerCase();
989
+ if (ownerStr.includes(opts.owner.toLowerCase())) {
990
+ return true;
991
+ }
992
+ }
993
+
994
+ // Search by category
995
+ if (opts.category && entry.justification?.category === opts.category) {
996
+ return true;
997
+ }
998
+
999
+ return false;
1000
+ });
1001
+
1002
+ if (opts.json) {
1003
+ console.log(JSON.stringify({ results, count: results.length }, null, 2));
1004
+ return EXIT.SUCCESS;
1005
+ }
1006
+
1007
+ console.log(`\n${c.bold}${icons.list} Search Results${c.reset} ${c.dim}(${results.length} found)${c.reset}\n`);
1008
+
1009
+ if (results.length === 0) {
1010
+ console.log(` ${c.dim}No entries match your search.${c.reset}\n`);
1011
+ return EXIT.SUCCESS;
1012
+ }
1013
+
1014
+ for (const entry of results) {
1015
+ printEntry(entry, opts.verbose);
1016
+ }
1017
+
1018
+ return EXIT.SUCCESS;
1019
+ }
1020
+
1021
+ async function actionStats(projectRoot, opts) {
1022
+ const { safelist: sl, warnings, errors } = safelist.loadSafelist(projectRoot);
1023
+
1024
+ // Compute detailed statistics
1025
+ const now = Date.now();
1026
+ const stats = {
1027
+ total: sl.entries.length,
1028
+ byScope: {
1029
+ repo: sl.entries.filter(e => e._source === "repo").length,
1030
+ local: sl.entries.filter(e => e._source === "local").length,
1031
+ },
1032
+ byType: {},
1033
+ byCategory: {},
1034
+ byOwner: {},
1035
+ lifecycle: {
1036
+ expired: 0,
1037
+ expiringSoon: 0,
1038
+ permanent: 0,
1039
+ withExpiry: 0,
1040
+ dueForReview: 0,
1041
+ },
1042
+ activity: {
1043
+ totalMatches: 0,
1044
+ neverMatched: 0,
1045
+ lastWeek: 0,
1046
+ lastMonth: 0,
1047
+ },
1048
+ age: {
1049
+ today: 0,
1050
+ thisWeek: 0,
1051
+ thisMonth: 0,
1052
+ older: 0,
1053
+ },
1054
+ };
1055
+
1056
+ const oneDay = 24 * 60 * 60 * 1000;
1057
+ const oneWeek = 7 * oneDay;
1058
+ const oneMonth = 30 * oneDay;
1059
+
1060
+ for (const entry of sl.entries) {
1061
+ // By type
1062
+ stats.byType[entry.type] = (stats.byType[entry.type] || 0) + 1;
1063
+
1064
+ // By category
1065
+ const cat = entry.justification?.category || "unknown";
1066
+ stats.byCategory[cat] = (stats.byCategory[cat] || 0) + 1;
1067
+
1068
+ // By owner
1069
+ const owner = entry.owner?.name || "Unknown";
1070
+ stats.byOwner[owner] = (stats.byOwner[owner] || 0) + 1;
1071
+
1072
+ // Lifecycle
1073
+ if (entry.lifecycle?.expiresAt) {
1074
+ const expiresAt = new Date(entry.lifecycle.expiresAt).getTime();
1075
+ if (expiresAt < now) {
1076
+ stats.lifecycle.expired++;
1077
+ } else if (expiresAt < now + oneWeek) {
1078
+ stats.lifecycle.expiringSoon++;
1079
+ }
1080
+ stats.lifecycle.withExpiry++;
1081
+ } else {
1082
+ stats.lifecycle.permanent++;
1083
+ }
1084
+
1085
+ if (entry.lifecycle?.reviewAt) {
1086
+ const reviewAt = new Date(entry.lifecycle.reviewAt).getTime();
1087
+ if (reviewAt < now) {
1088
+ stats.lifecycle.dueForReview++;
1089
+ }
1090
+ }
1091
+
1092
+ // Activity
1093
+ const matchCount = entry.lifecycle?.matchCount || 0;
1094
+ stats.activity.totalMatches += matchCount;
1095
+ if (matchCount === 0) {
1096
+ stats.activity.neverMatched++;
1097
+ }
1098
+
1099
+ if (entry.lifecycle?.lastMatchedAt) {
1100
+ const lastMatched = new Date(entry.lifecycle.lastMatchedAt).getTime();
1101
+ if (lastMatched > now - oneWeek) {
1102
+ stats.activity.lastWeek++;
1103
+ } else if (lastMatched > now - oneMonth) {
1104
+ stats.activity.lastMonth++;
1105
+ }
1106
+ }
1107
+
1108
+ // Age
1109
+ if (entry.lifecycle?.createdAt) {
1110
+ const created = new Date(entry.lifecycle.createdAt).getTime();
1111
+ if (created > now - oneDay) {
1112
+ stats.age.today++;
1113
+ } else if (created > now - oneWeek) {
1114
+ stats.age.thisWeek++;
1115
+ } else if (created > now - oneMonth) {
1116
+ stats.age.thisMonth++;
1117
+ } else {
1118
+ stats.age.older++;
1119
+ }
1120
+ }
1121
+ }
1122
+
1123
+ if (opts.json) {
1124
+ console.log(JSON.stringify(stats, null, 2));
1125
+ return EXIT.SUCCESS;
1126
+ }
1127
+
1128
+ console.log(`
1129
+ ${c.bold}╔══════════════════════════════════════════════════════════════════════════════╗
1130
+ ║ ${icons.shield} ${c.cyan}SAFELIST STATISTICS${c.reset}${c.bold} ║
1131
+ ╚══════════════════════════════════════════════════════════════════════════════╝${c.reset}
1132
+ `);
1133
+
1134
+ // Overview
1135
+ console.log(`${c.bold}OVERVIEW${c.reset}`);
1136
+ console.log(`${"─".repeat(60)}`);
1137
+ console.log(` Total entries: ${c.bold}${stats.total}${c.reset}`);
1138
+ console.log(` Repo-wide: ${stats.byScope.repo}`);
1139
+ console.log(` Local-only: ${stats.byScope.local}`);
1140
+ console.log();
1141
+
1142
+ // By type
1143
+ console.log(`${c.bold}BY TYPE${c.reset}`);
1144
+ console.log(`${"─".repeat(60)}`);
1145
+ for (const [type, count] of Object.entries(stats.byType)) {
1146
+ const bar = "█".repeat(Math.min(20, Math.round(count / stats.total * 20)));
1147
+ console.log(` ${type.padEnd(12)} ${bar.padEnd(20)} ${count}`);
1148
+ }
1149
+ console.log();
1150
+
1151
+ // Lifecycle
1152
+ console.log(`${c.bold}LIFECYCLE${c.reset}`);
1153
+ console.log(`${"─".repeat(60)}`);
1154
+ console.log(` ${stats.lifecycle.expired > 0 ? c.red : c.green}Expired:${c.reset} ${stats.lifecycle.expired}`);
1155
+ console.log(` ${stats.lifecycle.expiringSoon > 0 ? c.yellow : c.green}Expiring soon:${c.reset} ${stats.lifecycle.expiringSoon}`);
1156
+ console.log(` With expiry: ${stats.lifecycle.withExpiry}`);
1157
+ console.log(` Permanent: ${stats.lifecycle.permanent}`);
1158
+ console.log(` ${stats.lifecycle.dueForReview > 0 ? c.yellow : c.green}Due for review:${c.reset} ${stats.lifecycle.dueForReview}`);
1159
+ console.log();
1160
+
1161
+ // Activity
1162
+ console.log(`${c.bold}ACTIVITY${c.reset}`);
1163
+ console.log(`${"─".repeat(60)}`);
1164
+ console.log(` Total matches: ${stats.activity.totalMatches}`);
1165
+ console.log(` ${stats.activity.neverMatched > 5 ? c.yellow : c.green}Never matched:${c.reset} ${stats.activity.neverMatched}`);
1166
+ console.log(` Active last week: ${stats.activity.lastWeek}`);
1167
+ console.log(` Active last month: ${stats.activity.lastMonth}`);
1168
+ console.log();
1169
+
1170
+ // Age
1171
+ console.log(`${c.bold}AGE DISTRIBUTION${c.reset}`);
1172
+ console.log(`${"─".repeat(60)}`);
1173
+ console.log(` Today: ${stats.age.today}`);
1174
+ console.log(` This week: ${stats.age.thisWeek}`);
1175
+ console.log(` This month: ${stats.age.thisMonth}`);
1176
+ console.log(` Older: ${stats.age.older}`);
1177
+ console.log();
1178
+
1179
+ return EXIT.SUCCESS;
1180
+ }
1181
+
1182
+ // ═══════════════════════════════════════════════════════════════════════════════
1183
+ // EXPORTS
1184
+ // ═══════════════════════════════════════════════════════════════════════════════
1185
+
1186
+ module.exports = {
1187
+ runSafelist,
1188
+ // Re-export safelist module for integration
1189
+ ...safelist,
1190
+ };