@howlil/ez-agents 3.5.0 → 4.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 (382) hide show
  1. package/README.md +735 -537
  2. package/agents/ez-architect-agent.md +267 -0
  3. package/agents/ez-backend-agent.md +303 -0
  4. package/agents/ez-chief-strategist.md +271 -0
  5. package/agents/ez-codebase-mapper.md +770 -770
  6. package/agents/ez-context-manager.md +319 -0
  7. package/agents/ez-debugger.md +1255 -1255
  8. package/agents/ez-design-expert.md +347 -0
  9. package/agents/ez-devops-agent.md +331 -0
  10. package/agents/ez-executor.md +487 -487
  11. package/agents/ez-frontend-agent.md +322 -0
  12. package/agents/ez-phase-researcher.md +553 -553
  13. package/agents/ez-planner.md +1307 -1307
  14. package/agents/ez-product-engineer.md +435 -0
  15. package/agents/ez-project-researcher.md +629 -629
  16. package/agents/ez-qa-agent.md +320 -0
  17. package/agents/ez-release-agent.md +333 -333
  18. package/agents/ez-requirements-agent.md +377 -377
  19. package/agents/ez-roadmapper.md +650 -650
  20. package/agents/ez-technical-writer.md +551 -0
  21. package/agents/ez-ux-expert.md +393 -0
  22. package/agents/ez-verifier.md +579 -579
  23. package/bin/guards/autonomy-guard.cjs +346 -0
  24. package/bin/guards/context-budget-guard.cjs +278 -0
  25. package/bin/guards/hallucination-guard.cjs +380 -0
  26. package/bin/guards/hidden-state-guard.cjs +182 -0
  27. package/bin/guards/team-overhead-guard.cjs +266 -0
  28. package/bin/guards/tool-sprawl-guard.cjs +271 -0
  29. package/bin/lib/analytics/analytics-collector.cjs +86 -0
  30. package/bin/lib/analytics/analytics-reporter.cjs +130 -0
  31. package/bin/lib/analytics/cohort-analyzer.cjs +138 -0
  32. package/bin/lib/analytics/funnel-analyzer.cjs +147 -0
  33. package/bin/lib/analytics/nps-tracker.cjs +147 -0
  34. package/bin/lib/archetype-detector.cjs +289 -0
  35. package/bin/lib/assistant-adapter.cjs +361 -0
  36. package/bin/lib/audit-exec.cjs +175 -0
  37. package/bin/lib/auth.cjs +176 -0
  38. package/bin/lib/backup-service.cjs +422 -0
  39. package/bin/lib/bdd-validator.cjs +622 -0
  40. package/bin/lib/business-flow-mapper.cjs +429 -0
  41. package/bin/lib/circuit-breaker.cjs +276 -0
  42. package/bin/lib/code-complexity-analyzer.cjs +360 -0
  43. package/bin/lib/codebase-analyzer.cjs +241 -0
  44. package/bin/lib/commands.cjs +691 -0
  45. package/bin/lib/config.cjs +236 -0
  46. package/bin/lib/constraint-extractor.cjs +526 -0
  47. package/bin/lib/content-scanner.cjs +238 -0
  48. package/bin/lib/context-cache.cjs +154 -0
  49. package/bin/lib/context-compressor.cjs +102 -0
  50. package/bin/lib/context-deduplicator.cjs +105 -0
  51. package/bin/lib/context-errors.cjs +78 -0
  52. package/bin/lib/context-manager.cjs +338 -0
  53. package/bin/lib/context-metadata-tracker.cjs +140 -0
  54. package/bin/lib/context-relevance-scorer.cjs +99 -0
  55. package/bin/lib/core.cjs +507 -0
  56. package/bin/lib/cost-alerts.cjs +174 -0
  57. package/bin/lib/cost-tracker.cjs +275 -0
  58. package/bin/lib/crash-recovery.cjs +220 -0
  59. package/bin/lib/dependency-graph.cjs +319 -0
  60. package/bin/lib/deploy/deploy-audit-log.cjs +76 -0
  61. package/bin/lib/deploy/deploy-detector.cjs +69 -0
  62. package/bin/lib/deploy/deploy-env-manager.cjs +109 -0
  63. package/bin/lib/deploy/deploy-health-check.cjs +88 -0
  64. package/bin/lib/deploy/deploy-pre-flight.cjs +57 -0
  65. package/bin/lib/deploy/deploy-rollback.cjs +72 -0
  66. package/bin/lib/deploy/deploy-runner.cjs +97 -0
  67. package/bin/lib/deploy/deploy-status.cjs +74 -0
  68. package/bin/lib/discussion-synthesizer.cjs +439 -0
  69. package/bin/lib/error-cache.cjs +114 -0
  70. package/bin/lib/error-registry.cjs +177 -0
  71. package/bin/lib/file-access.cjs +207 -0
  72. package/bin/lib/file-lock.cjs +236 -0
  73. package/bin/lib/finops/budget-enforcer.cjs +126 -0
  74. package/bin/lib/finops/cost-reporter.cjs +132 -0
  75. package/bin/lib/finops/finops-analyzer.cjs +112 -0
  76. package/bin/lib/finops/spot-manager.cjs +118 -0
  77. package/bin/lib/framework-detector.cjs +396 -0
  78. package/bin/lib/frontmatter.cjs +313 -0
  79. package/bin/lib/fs-utils.cjs +153 -0
  80. package/bin/lib/gate-executor.cjs +272 -0
  81. package/bin/lib/gates/README.md +374 -0
  82. package/bin/lib/gates/gate-01-requirement.cjs +303 -0
  83. package/bin/lib/gates/gate-02-architecture.cjs +555 -0
  84. package/bin/lib/gates/gate-03-code.cjs +635 -0
  85. package/bin/lib/gates/gate-04-security.cjs +829 -0
  86. package/bin/lib/git-errors.cjs +83 -0
  87. package/bin/lib/git-utils.cjs +321 -0
  88. package/bin/lib/git-workflow-engine.cjs +1157 -0
  89. package/bin/lib/health-check.cjs +227 -0
  90. package/bin/lib/index.cjs +279 -0
  91. package/bin/lib/init.cjs +725 -0
  92. package/bin/lib/lock-logger.cjs +194 -0
  93. package/bin/lib/lock-state.cjs +263 -0
  94. package/bin/lib/lockfile-validator.cjs +227 -0
  95. package/bin/lib/log-rotation.cjs +71 -0
  96. package/bin/lib/logger.cjs +125 -0
  97. package/bin/lib/memory-compression.cjs +256 -0
  98. package/bin/lib/milestone.cjs +247 -0
  99. package/bin/lib/model-provider.cjs +241 -0
  100. package/bin/lib/package-manager-detector.cjs +203 -0
  101. package/bin/lib/package-manager-executor.cjs +385 -0
  102. package/bin/lib/package-manager-service.cjs +216 -0
  103. package/bin/lib/perf/api-monitor.cjs +88 -0
  104. package/bin/lib/perf/db-optimizer.cjs +78 -0
  105. package/bin/lib/perf/frontend-performance.cjs +56 -0
  106. package/bin/lib/perf/perf-analyzer.cjs +77 -0
  107. package/bin/lib/perf/perf-baseline.cjs +102 -0
  108. package/bin/lib/perf/perf-reporter.cjs +117 -0
  109. package/bin/lib/perf/regression-detector.cjs +92 -0
  110. package/bin/lib/phase.cjs +963 -0
  111. package/bin/lib/planning-write.cjs +123 -0
  112. package/bin/lib/project-reporter.cjs +565 -0
  113. package/bin/lib/quality-gate.cjs +332 -0
  114. package/bin/lib/quality-metrics.cjs +324 -0
  115. package/bin/lib/recovery-manager.cjs +98 -0
  116. package/bin/lib/release-validator.cjs +617 -0
  117. package/bin/lib/retry.cjs +119 -0
  118. package/bin/lib/roadmap.cjs +309 -0
  119. package/bin/lib/safe-exec.cjs +173 -0
  120. package/bin/lib/safe-path.cjs +130 -0
  121. package/bin/lib/security-errors.cjs +62 -0
  122. package/bin/lib/session-chain.cjs +304 -0
  123. package/bin/lib/session-errors.cjs +81 -0
  124. package/bin/lib/session-export.cjs +251 -0
  125. package/bin/lib/session-import.cjs +262 -0
  126. package/bin/lib/session-manager.cjs +280 -0
  127. package/bin/lib/skill-context.cjs +148 -0
  128. package/bin/lib/skill-matcher.cjs +236 -0
  129. package/bin/lib/skill-registry.cjs +360 -0
  130. package/bin/lib/skill-resolver.cjs +449 -0
  131. package/bin/lib/skill-triggers.cjs +90 -0
  132. package/bin/lib/skill-validator.cjs +270 -0
  133. package/bin/lib/skill-versioning.cjs +355 -0
  134. package/bin/lib/stack-detector.cjs +399 -0
  135. package/bin/lib/state.cjs +736 -0
  136. package/bin/lib/tech-debt-analyzer.cjs +309 -0
  137. package/bin/lib/temp-file.cjs +239 -0
  138. package/bin/lib/template.cjs +223 -0
  139. package/bin/lib/test-file-lock.cjs +112 -0
  140. package/bin/lib/test-graceful.cjs +93 -0
  141. package/bin/lib/test-logger.cjs +60 -0
  142. package/bin/lib/test-safe-exec.cjs +38 -0
  143. package/bin/lib/test-safe-path.cjs +33 -0
  144. package/bin/lib/test-temp-file.cjs +125 -0
  145. package/bin/lib/tier-manager.cjs +428 -0
  146. package/bin/lib/timeout-exec.cjs +63 -0
  147. package/bin/lib/tradeoff-analyzer.cjs +284 -0
  148. package/bin/lib/url-fetch.cjs +170 -0
  149. package/bin/lib/verify.cjs +863 -0
  150. package/bin/update.js +217 -214
  151. package/commands/deploy.cjs +53 -0
  152. package/commands/ez/add-tests.md +41 -41
  153. package/commands/ez/audit-milestone.md +36 -36
  154. package/commands/ez/complete-milestone.md +136 -136
  155. package/commands/ez/discuss-phase.md +90 -90
  156. package/commands/ez/execute-phase.md +52 -52
  157. package/commands/ez/help.md +22 -22
  158. package/commands/ez/map-codebase.md +71 -71
  159. package/commands/ez/new-milestone.md +44 -44
  160. package/commands/ez/new-project.md +51 -42
  161. package/commands/ez/plan-phase.md +53 -53
  162. package/commands/ez/progress.md +36 -36
  163. package/commands/ez/quick.md +45 -45
  164. package/commands/ez/resume-work.md +40 -40
  165. package/commands/ez/run-phase.md +580 -0
  166. package/commands/ez/settings.md +36 -36
  167. package/commands/ez/update.md +37 -37
  168. package/commands/ez/verify-work.md +402 -38
  169. package/commands/health-check.cjs +44 -0
  170. package/commands/rollback.cjs +47 -0
  171. package/ez-agents/bin/ez-tools.cjs +599 -2
  172. package/ez-agents/bin/guards/autonomy-guard.cjs +346 -0
  173. package/ez-agents/bin/guards/context-budget-guard.cjs +247 -0
  174. package/ez-agents/bin/guards/hallucination-guard.cjs +271 -0
  175. package/ez-agents/bin/guards/hidden-state-guard.cjs +182 -0
  176. package/ez-agents/bin/guards/team-overhead-guard.cjs +266 -0
  177. package/ez-agents/bin/guards/tool-sprawl-guard.cjs +271 -0
  178. package/ez-agents/bin/lib/analytics/analytics-collector.cjs +86 -0
  179. package/ez-agents/bin/lib/analytics/analytics-reporter.cjs +130 -0
  180. package/ez-agents/bin/lib/analytics/cohort-analyzer.cjs +138 -0
  181. package/ez-agents/bin/lib/analytics/funnel-analyzer.cjs +147 -0
  182. package/ez-agents/bin/lib/analytics/nps-tracker.cjs +147 -0
  183. package/ez-agents/bin/lib/archetype-detector.cjs +289 -0
  184. package/ez-agents/bin/lib/audit-exec.cjs +166 -167
  185. package/ez-agents/bin/lib/auth.cjs +176 -176
  186. package/ez-agents/bin/lib/backup-service.cjs +422 -0
  187. package/ez-agents/bin/lib/bdd-validator.cjs +622 -622
  188. package/ez-agents/bin/lib/business-flow-mapper.cjs +429 -0
  189. package/ez-agents/bin/lib/code-complexity-analyzer.cjs +360 -0
  190. package/ez-agents/bin/lib/codebase-analyzer.cjs +241 -0
  191. package/ez-agents/bin/lib/commands.cjs +685 -685
  192. package/ez-agents/bin/lib/config.cjs +41 -1
  193. package/ez-agents/bin/lib/constraint-extractor.cjs +526 -0
  194. package/ez-agents/bin/lib/content-scanner.cjs +238 -238
  195. package/ez-agents/bin/lib/context-cache.cjs +154 -154
  196. package/ez-agents/bin/lib/context-errors.cjs +71 -71
  197. package/ez-agents/bin/lib/context-manager.cjs +220 -220
  198. package/ez-agents/bin/lib/core.cjs +507 -512
  199. package/ez-agents/bin/lib/cost-tracker.cjs +243 -0
  200. package/ez-agents/bin/lib/crash-recovery.cjs +172 -0
  201. package/ez-agents/bin/lib/dependency-graph.cjs +319 -0
  202. package/ez-agents/bin/lib/deploy/deploy-audit-log.cjs +76 -0
  203. package/ez-agents/bin/lib/deploy/deploy-detector.cjs +69 -0
  204. package/ez-agents/bin/lib/deploy/deploy-env-manager.cjs +109 -0
  205. package/ez-agents/bin/lib/deploy/deploy-health-check.cjs +88 -0
  206. package/ez-agents/bin/lib/deploy/deploy-pre-flight.cjs +57 -0
  207. package/ez-agents/bin/lib/deploy/deploy-rollback.cjs +72 -0
  208. package/ez-agents/bin/lib/deploy/deploy-runner.cjs +97 -0
  209. package/ez-agents/bin/lib/deploy/deploy-status.cjs +74 -0
  210. package/ez-agents/bin/lib/file-access.cjs +207 -207
  211. package/ez-agents/bin/lib/finops/budget-enforcer.cjs +126 -0
  212. package/ez-agents/bin/lib/finops/cost-reporter.cjs +132 -0
  213. package/ez-agents/bin/lib/finops/finops-analyzer.cjs +112 -0
  214. package/ez-agents/bin/lib/finops/spot-manager.cjs +118 -0
  215. package/ez-agents/bin/lib/framework-detector.cjs +396 -0
  216. package/ez-agents/bin/lib/frontmatter.cjs +3 -1
  217. package/ez-agents/bin/lib/gates/README.md +374 -0
  218. package/ez-agents/bin/lib/gates/gate-01-requirement.cjs +303 -0
  219. package/ez-agents/bin/lib/gates/gate-02-architecture.cjs +555 -0
  220. package/ez-agents/bin/lib/gates/gate-03-code.cjs +635 -0
  221. package/ez-agents/bin/lib/gates/gate-04-security.cjs +829 -0
  222. package/ez-agents/bin/lib/git-errors.cjs +83 -83
  223. package/ez-agents/bin/lib/git-utils.cjs +321 -321
  224. package/ez-agents/bin/lib/git-workflow-engine.cjs +1157 -1157
  225. package/ez-agents/bin/lib/health-check.cjs +162 -162
  226. package/ez-agents/bin/lib/index.cjs +2 -8
  227. package/ez-agents/bin/lib/init.cjs +0 -2
  228. package/ez-agents/bin/lib/lockfile-validator.cjs +227 -227
  229. package/ez-agents/bin/lib/log-rotation.cjs +71 -0
  230. package/ez-agents/bin/lib/logger.cjs +22 -47
  231. package/ez-agents/bin/lib/memory-compression.cjs +256 -256
  232. package/ez-agents/bin/lib/package-manager-detector.cjs +203 -203
  233. package/ez-agents/bin/lib/package-manager-executor.cjs +385 -385
  234. package/ez-agents/bin/lib/package-manager-service.cjs +216 -216
  235. package/ez-agents/bin/lib/perf/api-monitor.cjs +88 -0
  236. package/ez-agents/bin/lib/perf/db-optimizer.cjs +78 -0
  237. package/ez-agents/bin/lib/perf/frontend-performance.cjs +56 -0
  238. package/ez-agents/bin/lib/perf/perf-analyzer.cjs +77 -0
  239. package/ez-agents/bin/lib/perf/perf-baseline.cjs +102 -0
  240. package/ez-agents/bin/lib/perf/perf-reporter.cjs +117 -0
  241. package/ez-agents/bin/lib/perf/regression-detector.cjs +92 -0
  242. package/ez-agents/bin/lib/project-reporter.cjs +502 -0
  243. package/ez-agents/bin/lib/quality-gate.cjs +332 -0
  244. package/ez-agents/bin/lib/recovery-manager.cjs +98 -0
  245. package/ez-agents/bin/lib/release-validator.cjs +617 -614
  246. package/ez-agents/bin/lib/security-errors.cjs +62 -0
  247. package/ez-agents/bin/lib/session-chain.cjs +304 -304
  248. package/ez-agents/bin/lib/session-errors.cjs +81 -81
  249. package/ez-agents/bin/lib/session-export.cjs +251 -251
  250. package/ez-agents/bin/lib/session-import.cjs +262 -262
  251. package/ez-agents/bin/lib/session-manager.cjs +280 -280
  252. package/ez-agents/bin/lib/skill-context.cjs +148 -0
  253. package/ez-agents/bin/lib/skill-matcher.cjs +236 -0
  254. package/ez-agents/bin/lib/skill-registry.cjs +341 -0
  255. package/ez-agents/bin/lib/skill-resolver.cjs +449 -0
  256. package/ez-agents/bin/lib/skill-triggers.cjs +90 -0
  257. package/ez-agents/bin/lib/skill-validator.cjs +270 -0
  258. package/ez-agents/bin/lib/skill-versioning.cjs +355 -0
  259. package/ez-agents/bin/lib/stack-detector.cjs +399 -0
  260. package/ez-agents/bin/lib/tech-debt-analyzer.cjs +309 -0
  261. package/ez-agents/bin/lib/tier-manager.cjs +428 -428
  262. package/ez-agents/bin/lib/tradeoff-analyzer.cjs +284 -0
  263. package/ez-agents/bin/lib/url-fetch.cjs +170 -170
  264. package/ez-agents/bin/lib/verify.cjs +863 -863
  265. package/ez-agents/references/decimal-phase-calculation.md +65 -65
  266. package/ez-agents/references/git-integration.md +248 -248
  267. package/ez-agents/references/git-planning-commit.md +38 -38
  268. package/ez-agents/references/metrics-schema.md +118 -118
  269. package/ez-agents/references/model-profile-resolution.md +34 -34
  270. package/ez-agents/references/model-profiles.md +93 -93
  271. package/ez-agents/references/phase-argument-parsing.md +61 -61
  272. package/ez-agents/references/planning-config.md +340 -340
  273. package/ez-agents/references/tier-strategy.md +103 -103
  274. package/ez-agents/references/ui-brand.md +160 -160
  275. package/ez-agents/references/verification-patterns.md +612 -612
  276. package/ez-agents/templates/DEBUG.md +164 -164
  277. package/ez-agents/templates/UAT.md +247 -247
  278. package/ez-agents/templates/agent-output-format.md +404 -0
  279. package/ez-agents/templates/bdd-feature.md +173 -173
  280. package/ez-agents/templates/codebase/architecture.md +255 -255
  281. package/ez-agents/templates/codebase/structure.md +285 -285
  282. package/ez-agents/templates/copilot-instructions.md +7 -7
  283. package/ez-agents/templates/debug-subagent-prompt.md +91 -91
  284. package/ez-agents/templates/discovery.md +146 -146
  285. package/ez-agents/templates/discussion.md +68 -68
  286. package/ez-agents/templates/handoff-protocol.md +294 -0
  287. package/ez-agents/templates/incident-runbook.md +205 -205
  288. package/ez-agents/templates/mode-workflow-templates.md +301 -0
  289. package/ez-agents/templates/phase-prompt.md +610 -610
  290. package/ez-agents/templates/planner-subagent-prompt.md +117 -117
  291. package/ez-agents/templates/project.md +184 -184
  292. package/ez-agents/templates/release-checklist.md +136 -133
  293. package/ez-agents/templates/research.md +552 -552
  294. package/ez-agents/templates/rollback-plan.md +201 -201
  295. package/ez-agents/templates/security-user-setup.md +244 -0
  296. package/ez-agents/templates/skill-validation-rules.md +476 -0
  297. package/ez-agents/templates/state.md +180 -176
  298. package/ez-agents/templates/summary-complex.md +59 -59
  299. package/ez-agents/tests/gates/gate-01-02.test.cjs +812 -0
  300. package/ez-agents/tests/gates/gate-03-04.test.cjs +762 -0
  301. package/ez-agents/tests/gates/gate-05-validator.test.cjs +145 -0
  302. package/ez-agents/tests/gates/gate-06-docs-validator.test.cjs +244 -0
  303. package/ez-agents/tests/gates/gate-07-release-validator.test.cjs +219 -0
  304. package/ez-agents/tests/guards/context-budget-guard.test.cjs +145 -0
  305. package/ez-agents/tests/guards/edge-case-guards.test.cjs +238 -0
  306. package/ez-agents/tests/guards/hallucination-guard.test.cjs +124 -0
  307. package/ez-agents/workflows/audit-milestone.md +1 -1
  308. package/ez-agents/workflows/autonomous.md +844 -844
  309. package/ez-agents/workflows/complete-milestone.md +1 -1
  310. package/ez-agents/workflows/discuss-phase.md +1 -1
  311. package/ez-agents/workflows/execute-phase.md +124 -3
  312. package/ez-agents/workflows/help.md +42 -181
  313. package/ez-agents/workflows/hotfix.md +291 -291
  314. package/ez-agents/workflows/new-milestone.md +713 -713
  315. package/ez-agents/workflows/new-project.md +1089 -1107
  316. package/ez-agents/workflows/plan-phase.md +0 -40
  317. package/ez-agents/workflows/release.md +253 -253
  318. package/ez-agents/workflows/resume-session.md +215 -215
  319. package/ez-agents/workflows/run-phase.md +531 -0
  320. package/ez-agents/workflows/settings.md +2 -35
  321. package/hooks/dist/ez-check-update.js +81 -81
  322. package/hooks/dist/ez-context-monitor.js +148 -141
  323. package/hooks/dist/ez-statusline.js +115 -115
  324. package/package.json +78 -71
  325. package/scripts/fix-qwen-installation.js +144 -144
  326. package/agents/ez-integration-checker.md +0 -443
  327. package/agents/ez-nyquist-auditor.md +0 -176
  328. package/agents/ez-observer-agent.md +0 -260
  329. package/agents/ez-plan-checker.md +0 -706
  330. package/agents/ez-research-synthesizer.md +0 -247
  331. package/agents/ez-scrum-master-agent.md +0 -242
  332. package/agents/ez-tech-lead-agent.md +0 -267
  333. package/agents/ez-ui-auditor.md +0 -439
  334. package/agents/ez-ui-checker.md +0 -300
  335. package/agents/ez-ui-researcher.md +0 -353
  336. package/commands/ez/add-phase.md +0 -43
  337. package/commands/ez/add-todo.md +0 -47
  338. package/commands/ez/arch-review.md +0 -102
  339. package/commands/ez/auth.md +0 -87
  340. package/commands/ez/autonomous.md +0 -41
  341. package/commands/ez/check-todos.md +0 -45
  342. package/commands/ez/cleanup.md +0 -18
  343. package/commands/ez/debug.md +0 -168
  344. package/commands/ez/export-session.md +0 -79
  345. package/commands/ez/gather-requirements.md +0 -117
  346. package/commands/ez/git-workflow.md +0 -72
  347. package/commands/ez/health.md +0 -22
  348. package/commands/ez/hotfix.md +0 -120
  349. package/commands/ez/import-session.md +0 -82
  350. package/commands/ez/insert-phase.md +0 -32
  351. package/commands/ez/join-discord.md +0 -18
  352. package/commands/ez/list-phase-assumptions.md +0 -46
  353. package/commands/ez/list-sessions.md +0 -96
  354. package/commands/ez/package-manager.md +0 -316
  355. package/commands/ez/pause-work.md +0 -38
  356. package/commands/ez/plan-milestone-gaps.md +0 -34
  357. package/commands/ez/preflight.md +0 -79
  358. package/commands/ez/reapply-patches.md +0 -124
  359. package/commands/ez/release.md +0 -153
  360. package/commands/ez/remove-phase.md +0 -31
  361. package/commands/ez/research-phase.md +0 -190
  362. package/commands/ez/resume.md +0 -107
  363. package/commands/ez/set-profile.md +0 -34
  364. package/commands/ez/standup.md +0 -85
  365. package/commands/ez/stats.md +0 -18
  366. package/commands/ez/ui-phase.md +0 -34
  367. package/commands/ez/ui-review.md +0 -32
  368. package/commands/ez/validate-phase.md +0 -35
  369. package/ez-agents/bin/lib/metrics-tracker.cjs +0 -406
  370. package/ez-agents/templates/UI-SPEC.md +0 -100
  371. package/ez-agents/templates/VALIDATION.md +0 -76
  372. package/ez-agents/templates/context.md +0 -352
  373. package/ez-agents/templates/verification-report.md +0 -322
  374. package/ez-agents/workflows/arch-review.md +0 -54
  375. package/ez-agents/workflows/export-session.md +0 -255
  376. package/ez-agents/workflows/gather-requirements.md +0 -206
  377. package/ez-agents/workflows/import-session.md +0 -303
  378. package/ez-agents/workflows/research-phase.md +0 -74
  379. package/ez-agents/workflows/standup.md +0 -64
  380. package/ez-agents/workflows/ui-phase.md +0 -290
  381. package/ez-agents/workflows/ui-review.md +0 -157
  382. package/ez-agents/workflows/validate-phase.md +0 -167
@@ -1,614 +1,617 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Release Validator — Automated release readiness validation
5
- *
6
- * Runs security gates, tier checklist validation, and produces
7
- * a Production Readiness Score (0-100) for /ez:release.
8
- */
9
-
10
- 'use strict';
11
-
12
- const fs = require('fs');
13
- const path = require('path');
14
- const { execSync } = require('child_process');
15
- const TierManager = require('./tier-manager.cjs');
16
-
17
- // ─────────────────────────────────────────────
18
- // Security Helpers
19
- // ─────────────────────────────────────────────
20
-
21
- /**
22
- * Calculate Shannon entropy for a string — used to detect high-entropy secrets
23
- * @param {string} str
24
- * @param {number} threshold
25
- * @returns {boolean}
26
- */
27
- function hasHighEntropy(str, threshold = 4.5) {
28
- const freq = {};
29
- for (const c of str) freq[c] = (freq[c] || 0) + 1;
30
- const len = str.length;
31
- let entropy = 0;
32
- for (const c in freq) {
33
- const p = freq[c] / len;
34
- entropy -= p * Math.log2(p);
35
- }
36
- return entropy > threshold;
37
- }
38
-
39
- // ─────────────────────────────────────────────
40
- // Security Gates
41
- // ─────────────────────────────────────────────
42
-
43
- /**
44
- * Run all security gates
45
- * @param {string} cwd - Working directory
46
- * @returns {{ passed: boolean, gates: object[] }}
47
- */
48
- function runSecurityGates(cwd = process.cwd()) {
49
- const gates = [];
50
-
51
- // Gate 1: Multi-pattern secret detection
52
- try {
53
- const secretPatterns = [
54
- // Original + variants
55
- '(api[_-]?key|api[_-]?k[e3]y|password|passw[0o]rd|s[e3]cr[e3]t|auth[_-]?token)',
56
- // High-value secret types
57
- '(bearer|private[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret)',
58
- // Known formats: AWS
59
- 'AKIA[0-9A-Z]{16}',
60
- // GitHub PAT
61
- 'ghp_[a-zA-Z0-9]{36}',
62
- // JWT pattern
63
- 'eyJ[a-zA-Z0-9-_=]+\\.[a-zA-Z0-9-_=]+\\.'
64
- ];
65
- const secretPattern = secretPatterns.join('|');
66
- const result = execSync(
67
- `git grep -i -E "${secretPattern}" HEAD 2>/dev/null | grep -v "example\\|placeholder\\|your-key\\|process\\.env\\|env\\.\\|config\\." | wc -l`,
68
- { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
69
- ).trim();
70
- const count = parseInt(result) || 0;
71
- gates.push({
72
- name: 'no_secrets',
73
- label: 'No secrets in committed files',
74
- passed: count === 0,
75
- blocking: true,
76
- detail: count === 0 ? 'Clean' : `${count} potential secret(s) found`
77
- });
78
- } catch {
79
- gates.push({ name: 'no_secrets', label: 'No secrets in committed files', passed: true, blocking: true, detail: 'Check skipped (git not available)' });
80
- }
81
-
82
- // Gate 2: npm audit
83
- try {
84
- execSync('npm audit --audit-level=critical 2>/dev/null', { cwd, stdio: 'pipe' });
85
- gates.push({ name: 'npm_audit', label: 'npm audit — no critical vulnerabilities', passed: true, blocking: true, detail: 'Clean' });
86
- } catch (err) {
87
- const output = err.stdout ? err.stdout.toString() : '';
88
- const criticals = (output.match(/critical/gi) || []).length;
89
- gates.push({
90
- name: 'npm_audit',
91
- label: 'npm audit — no critical vulnerabilities',
92
- passed: false,
93
- blocking: true,
94
- detail: `${criticals} critical vulnerability issue(s). Run: npm audit fix`
95
- });
96
- }
97
-
98
- // Gate 3: No production TODOs
99
- try {
100
- const srcDirs = ['src', 'lib', 'app', 'server'].filter(d => fs.existsSync(path.join(cwd, d)));
101
- if (srcDirs.length > 0) {
102
- const searchDirs = srcDirs.join(' ');
103
- const result = execSync(
104
- `grep -rn "TODO\\|FIXME\\|HACK\\|XXX" ${searchDirs} --include="*.ts" --include="*.js" --include="*.py" 2>/dev/null | grep -v "test\\|spec\\|\\.test\\." | wc -l`,
105
- { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
106
- ).trim();
107
- const count = parseInt(result) || 0;
108
- gates.push({
109
- name: 'no_prod_todos',
110
- label: 'No production TODO/FIXME in src/',
111
- passed: count === 0,
112
- blocking: false, // advisory
113
- detail: count === 0 ? 'Clean' : `${count} TODO/FIXME found in production code`
114
- });
115
- } else {
116
- gates.push({ name: 'no_prod_todos', label: 'No production TODO/FIXME in src/', passed: true, blocking: false, detail: 'No src/ directory found — skipped' });
117
- }
118
- } catch {
119
- gates.push({ name: 'no_prod_todos', label: 'No production TODO/FIXME in src/', passed: true, blocking: false, detail: 'Check skipped' });
120
- }
121
-
122
- // Gate 4: .env in .gitignore
123
- try {
124
- const gitignore = fs.readFileSync(path.join(cwd, '.gitignore'), 'utf8');
125
- const protected_ = gitignore.match(/^\.env/m) !== null;
126
- gates.push({
127
- name: 'env_protected',
128
- label: '.env files in .gitignore',
129
- passed: protected_,
130
- blocking: true,
131
- detail: protected_ ? 'Protected' : '.env not found in .gitignore — add it before releasing'
132
- });
133
- } catch {
134
- gates.push({ name: 'env_protected', label: '.env files in .gitignore', passed: false, blocking: true, detail: '.gitignore not found or .env not listed' });
135
- }
136
-
137
- // Gate 5: MoSCoW coverage advisory (non-blocking)
138
- try {
139
- const featuresDir = path.join(cwd, 'features');
140
- const testDir = path.join(cwd, 'test');
141
- const specDir = path.join(cwd, 'spec');
142
- const bddDir = [featuresDir, testDir, specDir].find(d => fs.existsSync(d));
143
- if (bddDir) {
144
- const featureFiles = fs.readdirSync(bddDir).filter(f => f.endsWith('.feature'));
145
- if (featureFiles.length > 0) {
146
- let totalScenarios = 0;
147
- let taggedScenarios = 0;
148
- for (const file of featureFiles) {
149
- const content = fs.readFileSync(path.join(bddDir, file), 'utf8');
150
- const scenarioMatches = content.match(/^\s*Scenario/gm) || [];
151
- const mustMatches = content.match(/@must|@should|@could|@wont/g) || [];
152
- totalScenarios += scenarioMatches.length;
153
- taggedScenarios += mustMatches.length;
154
- }
155
- const untaggedPct = totalScenarios > 0 ? ((totalScenarios - taggedScenarios) / totalScenarios) * 100 : 0;
156
- gates.push({
157
- name: 'moscow_coverage',
158
- label: 'MoSCoW tag coverage in BDD scenarios',
159
- passed: untaggedPct <= 20,
160
- blocking: false, // advisory
161
- detail: untaggedPct <= 20
162
- ? `${Math.round(100 - untaggedPct)}% scenarios tagged`
163
- : `${Math.round(untaggedPct)}% scenarios missing @must/@should tags`
164
- });
165
- } else {
166
- gates.push({ name: 'moscow_coverage', label: 'MoSCoW tag coverage in BDD scenarios', passed: true, blocking: false, detail: 'No .feature files found — skipped' });
167
- }
168
- } else {
169
- gates.push({ name: 'moscow_coverage', label: 'MoSCoW tag coverage in BDD scenarios', passed: true, blocking: false, detail: 'No BDD directory found — skipped' });
170
- }
171
- } catch {
172
- gates.push({ name: 'moscow_coverage', label: 'MoSCoW tag coverage in BDD scenarios', passed: true, blocking: false, detail: 'Check skipped' });
173
- }
174
-
175
- const passed = gates.filter(g => g.passed && g.blocking).length;
176
- const total = gates.filter(g => g.blocking).length;
177
- const allPassed = gates.filter(g => g.blocking).every(g => g.passed);
178
-
179
- return { passed: allPassed, gates, score: `${passed}/${total}` };
180
- }
181
-
182
- // ─────────────────────────────────────────────
183
- // Tier Checklist
184
- // ─────────────────────────────────────────────
185
-
186
- const MVP_CHECKLIST = [
187
- { id: 'bdd_must', label: 'All @must BDD scenarios passing', auto: true },
188
- { id: 'npm_audit', label: 'npm audit — no critical vulnerabilities', auto: true },
189
- { id: 'health_endpoint', label: 'Health endpoint returns 200', auto: true },
190
- { id: 'no_secrets', label: 'No secrets in committed files', auto: true },
191
- { id: 'app_starts', label: 'Application starts without errors', auto: true },
192
- { id: 'rollback_documented', label: 'Rollback procedure documented', auto: true }
193
- ];
194
-
195
- const MEDIUM_EXTRA = [
196
- { id: 'bdd_should', label: 'All @should BDD scenarios passing', auto: true },
197
- { id: 'coverage_80', label: 'Test coverage ≥ 80%', auto: true },
198
- { id: 'staging_parity', label: 'Staging environment parity verified', auto: false },
199
- { id: 'monitoring', label: 'Monitoring/alerts configured', auto: false },
200
- { id: 'structured_logging', label: 'Structured logging (no console.log in prod)', auto: true },
201
- { id: 'perf_baseline', label: 'Performance baseline documented', auto: false },
202
- { id: 'error_tracking', label: 'Error tracking configured', auto: false },
203
- { id: 'db_migrations', label: 'Database migrations tested on staging', auto: false },
204
- { id: 'api_docs', label: 'API documentation current', auto: false },
205
- { id: 'env_example', label: '.env.example up to date', auto: true },
206
- { id: 'graceful_shutdown', label: 'Graceful shutdown handled', auto: true },
207
- { id: 'rate_limiting', label: 'Rate limiting on public API endpoints', auto: true }
208
- ];
209
-
210
- const ENTERPRISE_EXTRA = [
211
- { id: 'bdd_could', label: 'All @could BDD scenarios passing', auto: true },
212
- { id: 'coverage_95', label: 'Test coverage ≥ 95%', auto: true },
213
- { id: 'security_audit', label: 'Security audit completed', auto: false },
214
- { id: 'compliance_docs', label: 'Compliance documentation updated', auto: false },
215
- { id: 'load_test', label: 'Load test results documented', auto: false },
216
- { id: 'dr_tested', label: 'Disaster recovery tested', auto: false },
217
- { id: 'data_retention', label: 'Data retention policy configured', auto: false },
218
- { id: 'audit_logging', label: 'Audit logging enabled', auto: true },
219
- { id: 'pentest', label: 'Penetration test completed or scheduled', auto: false },
220
- { id: 'soc2_gdpr', label: 'SOC2/GDPR controls validated', auto: false },
221
- { id: 'change_ticket', label: 'Change management ticket filed', auto: false },
222
- { id: 'incident_runbook', label: 'Incident runbook up to date', auto: false }
223
- ];
224
-
225
- // ─────────────────────────────────────────────
226
- // Rollback Validation
227
- // ─────────────────────────────────────────────
228
-
229
- /**
230
- * Validate rollback plan content — checks for unfilled placeholders
231
- * @param {string} cwd
232
- * @returns {{ status: string, detail: string }}
233
- */
234
- function validateRollbackContent(cwd) {
235
- const releasesDir = path.join(cwd, '.planning', 'releases');
236
- if (!fs.existsSync(releasesDir)) {
237
- return { status: 'fail', detail: 'No .planning/releases/ directory found' };
238
- }
239
-
240
- const rollbackFiles = fs.readdirSync(releasesDir)
241
- .filter(f => f.includes('ROLLBACK') && f.endsWith('.md'));
242
-
243
- if (rollbackFiles.length === 0) {
244
- return { status: 'fail', detail: 'No rollback plan found' };
245
- }
246
-
247
- const latest = rollbackFiles.sort().pop();
248
- const content = fs.readFileSync(path.join(releasesDir, latest), 'utf8');
249
-
250
- // Detect unfilled placeholders like {name}, {migration_name}, {your-domain}
251
- const placeholders = content.match(/\{[a-z_-]+\}/gi) || [];
252
- if (placeholders.length > 0) {
253
- return {
254
- status: 'fail',
255
- detail: `Rollback plan has ${placeholders.length} unfilled placeholder(s): ${placeholders.slice(0, 3).join(', ')}`
256
- };
257
- }
258
-
259
- return { status: 'pass', detail: `Rollback plan validated: ${latest}` };
260
- }
261
-
262
- // ─────────────────────────────────────────────
263
- // Manual Checklist State Persistence (Fix 11)
264
- // ─────────────────────────────────────────────
265
-
266
- /**
267
- * Load persisted manual checklist state
268
- * @param {string} cwd
269
- * @returns {object}
270
- */
271
- function loadChecklistState(cwd) {
272
- const statePath = path.join(cwd, '.planning', 'releases', 'checklist-state.json');
273
- if (!fs.existsSync(statePath)) return {};
274
- try { return JSON.parse(fs.readFileSync(statePath, 'utf8')); }
275
- catch { return {}; }
276
- }
277
-
278
- /**
279
- * Mark a manual checklist item as complete with approver and timestamp
280
- * @param {string} itemId
281
- * @param {string} approver
282
- * @param {string} cwd
283
- */
284
- function markManualItemComplete(itemId, approver, cwd = process.cwd()) {
285
- const state = loadChecklistState(cwd);
286
- state[itemId] = {
287
- approved: true,
288
- approver: approver || 'unknown',
289
- timestamp: new Date().toISOString(),
290
- expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() // 30 days
291
- };
292
- const statePath = path.join(cwd, '.planning', 'releases', 'checklist-state.json');
293
- fs.mkdirSync(path.dirname(statePath), { recursive: true });
294
- fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
295
- }
296
-
297
- /**
298
- * Get full checklist for a tier
299
- * @param {string} tier
300
- * @returns {object[]}
301
- */
302
- function getChecklist(tier) {
303
- const t = tier.toLowerCase();
304
- if (t === 'mvp') return [...MVP_CHECKLIST];
305
- if (t === 'medium') return [...MVP_CHECKLIST, ...MEDIUM_EXTRA];
306
- if (t === 'enterprise') return [...MVP_CHECKLIST, ...MEDIUM_EXTRA, ...ENTERPRISE_EXTRA];
307
- throw new Error(`Unknown tier: ${tier}`);
308
- }
309
-
310
- /**
311
- * Run automated checklist items
312
- * @param {string} tier
313
- * @param {string} cwd
314
- * @param {object} context - additional context (coverage, bddResults, etc.)
315
- * @returns {{ items: object[], passed: number, total: number, score: number }}
316
- */
317
- function runChecklist(tier, cwd = process.cwd(), context = {}) {
318
- const items = getChecklist(tier);
319
- const results = [];
320
-
321
- for (const item of items) {
322
- let result;
323
- if (!item.auto) {
324
- // Fix 11: Check persisted state for manual items
325
- const state = loadChecklistState(cwd);
326
- const saved = state[item.id];
327
- if (saved && saved.approved) {
328
- const age = Date.now() - new Date(saved.timestamp).getTime();
329
- const ageDays = Math.floor(age / (1000 * 60 * 60 * 24));
330
- if (ageDays > 30) {
331
- result = { ...item, status: 'fail', detail: `Manual check expired ${ageDays}d ago — re-verify required` };
332
- } else {
333
- result = { ...item, status: 'pass', detail: `Verified by ${saved.approver} on ${saved.timestamp.split('T')[0]}` };
334
- }
335
- } else {
336
- result = { ...item, status: 'manual', detail: 'Requires manual verification (run: ez checklist mark <id> <approver>)' };
337
- }
338
- } else {
339
- result = runChecklistItem(item, cwd, context);
340
- }
341
- results.push(result);
342
- }
343
-
344
- const autoItems = results.filter(r => r.auto);
345
- const passed = autoItems.filter(r => r.status === 'pass').length;
346
- const total = autoItems.length;
347
-
348
- // Compute readiness score: blocking failures cost 10, advisory failures cost 2
349
- let score = 100;
350
- for (const r of results) {
351
- if (r.status === 'fail') {
352
- score -= r.blocking !== false ? 10 : 2;
353
- }
354
- }
355
- score = Math.max(0, score);
356
-
357
- return { items: results, passed, total, score };
358
- }
359
-
360
- function runChecklistItem(item, cwd, context) {
361
- try {
362
- switch (item.id) {
363
- case 'npm_audit':
364
- case 'no_secrets':
365
- // Already handled in security gates — check from context
366
- return { ...item, status: 'pass', detail: 'Verified in security gates' };
367
-
368
- case 'coverage_80': {
369
- const cov = context.coverage;
370
- if (cov === undefined) return { ...item, status: 'skip', detail: 'No coverage data available' };
371
- return { ...item, status: cov >= 80 ? 'pass' : 'fail', detail: `Coverage: ${cov}%` };
372
- }
373
-
374
- case 'coverage_95': {
375
- const cov = context.coverage;
376
- if (cov === undefined) return { ...item, status: 'skip', detail: 'No coverage data available' };
377
- return { ...item, status: cov >= 95 ? 'pass' : 'fail', detail: `Coverage: ${cov}%` };
378
- }
379
-
380
- case 'bdd_must': {
381
- const { bddPassed, moscowTagged, totalScenarios } = context;
382
- // Hard gate: fail if there are too many untagged scenarios
383
- if (moscowTagged !== undefined && totalScenarios > 0) {
384
- const untaggedPct = ((totalScenarios - moscowTagged) / totalScenarios) * 100;
385
- if (untaggedPct > 20) { // > 20% untagged = blocking
386
- return { ...item, status: 'fail', detail: `${Math.round(untaggedPct)}% scenarios missing @must/@should tags BDD coverage unverifiable` };
387
- }
388
- }
389
- if (bddPassed === undefined) return { ...item, status: 'skip', detail: 'No BDD resultsrun test suite first' };
390
- return { ...item, status: bddPassed ? 'pass' : 'fail', detail: bddPassed ? 'All scenarios passing' : 'Some scenarios failing' };
391
- }
392
-
393
- case 'bdd_should':
394
- case 'bdd_could': {
395
- const bddPassed = context.bddPassed;
396
- if (bddPassed === undefined) return { ...item, status: 'skip', detail: 'No BDD test results available' };
397
- return { ...item, status: bddPassed ? 'pass' : 'fail', detail: bddPassed ? 'All scenarios passing' : 'Some scenarios failing' };
398
- }
399
-
400
- case 'rollback_documented': {
401
- const rollbackResult = validateRollbackContent(cwd);
402
- return { ...item, status: rollbackResult.status, detail: rollbackResult.detail };
403
- }
404
-
405
- case 'env_example': {
406
- const hasExample = fs.existsSync(path.join(cwd, '.env.example'));
407
- return { ...item, status: hasExample ? 'pass' : 'fail', detail: hasExample ? '.env.example found' : '.env.example missing' };
408
- }
409
-
410
- case 'structured_logging': {
411
- try {
412
- const srcDirs = ['src', 'lib', 'app'].filter(d => fs.existsSync(path.join(cwd, d)));
413
- if (srcDirs.length === 0) return { ...item, status: 'skip', detail: 'No src/ found' };
414
- const result = execSync(
415
- `grep -rn "console\\.log" ${srcDirs.join(' ')} --include="*.ts" --include="*.js" 2>/dev/null | grep -v "test\\|spec" | wc -l`,
416
- { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
417
- ).trim();
418
- const count = parseInt(result) || 0;
419
- return { ...item, status: count === 0 ? 'pass' : 'fail', detail: count === 0 ? 'No console.log in prod' : `${count} console.log found` };
420
- } catch {
421
- return { ...item, status: 'skip', detail: 'Check failed' };
422
- }
423
- }
424
-
425
- case 'health_endpoint': {
426
- // Try to detect health endpoint in source
427
- try {
428
- const srcDirs = ['src', 'app', 'server', 'pages/api'].filter(d => fs.existsSync(path.join(cwd, d)));
429
- if (srcDirs.length === 0) return { ...item, status: 'skip', detail: 'No src/ found' };
430
- const result = execSync(
431
- `grep -rn "health\\|/ping\\|/status" ${srcDirs.join(' ')} --include="*.ts" --include="*.js" 2>/dev/null | wc -l`,
432
- { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
433
- ).trim();
434
- const found = parseInt(result) > 0;
435
- return { ...item, status: found ? 'pass' : 'skip', detail: found ? 'Health endpoint found in source' : 'No health endpoint found (optional for MVP)' };
436
- } catch {
437
- return { ...item, status: 'skip', detail: 'Check failed' };
438
- }
439
- }
440
-
441
- default:
442
- return { ...item, status: 'skip', detail: 'Automated check not implemented' };
443
- }
444
- } catch (err) {
445
- return { ...item, status: 'error', detail: err.message };
446
- }
447
- }
448
-
449
- // ─────────────────────────────────────────────
450
- // Full Validation
451
- // ─────────────────────────────────────────────
452
-
453
- /**
454
- * Run full release validation
455
- * @param {string} tier
456
- * @param {string} version
457
- * @param {string} cwd
458
- * @param {object} context
459
- * @returns {{ valid: boolean, blockers: string[], warnings: string[], score: number, securityGates: object, checklist: object }}
460
- */
461
- function validateRelease(tier, version, cwd = process.cwd(), context = {}) {
462
- const securityGates = runSecurityGates(cwd);
463
- const checklist = runChecklist(tier, cwd, context);
464
-
465
- const blockers = [];
466
- const warnings = [];
467
-
468
- // Security gate failures
469
- for (const gate of securityGates.gates) {
470
- if (!gate.passed && gate.blocking) blockers.push(`Security: ${gate.label} — ${gate.detail}`);
471
- if (!gate.passed && !gate.blocking) warnings.push(`Security: ${gate.label} — ${gate.detail}`);
472
- }
473
-
474
- // Checklist failures
475
- for (const item of checklist.items) {
476
- if (item.status === 'fail') {
477
- if (item.blocking !== false) warnings.push(`Checklist: ${item.label}`);
478
- }
479
- }
480
-
481
- const valid = blockers.length === 0;
482
- const readinessScore = Math.min(checklist.score, securityGates.passed ? 100 : 50);
483
-
484
- const readinessStatus = readinessScore >= 90 ? 'READY'
485
- : readinessScore >= 70 ? 'CONDITIONAL'
486
- : 'NOT READY';
487
-
488
- return {
489
- valid,
490
- tier,
491
- version,
492
- blockers,
493
- warnings,
494
- score: readinessScore,
495
- readinessStatus,
496
- securityGates,
497
- checklist
498
- };
499
- }
500
-
501
- /**
502
- * Format validation result as markdown
503
- */
504
- function formatValidation(result) {
505
- const lines = [];
506
- const icon = result.valid ? '✓' : '✗';
507
-
508
- lines.push(`## Release Validation: v${result.version} (${result.tier})`);
509
- lines.push(`**Status:** ${icon} ${result.valid ? 'READY' : 'BLOCKED'}`);
510
- lines.push(`**Production Readiness Score:** ${result.score}/100 — ${result.readinessStatus}`);
511
- lines.push('');
512
-
513
- lines.push('### Security Gates');
514
- for (const g of result.securityGates.gates) {
515
- lines.push(`- ${g.passed ? '✓' : '✗'} ${g.label}: ${g.detail}`);
516
- }
517
- lines.push('');
518
-
519
- lines.push(`### Checklist (${result.tier})`);
520
- for (const item of result.checklist.items) {
521
- const icon2 = item.status === 'pass' ? '✓' : item.status === 'skip' ? '○' : item.status === 'manual' ? '?' : '✗';
522
- lines.push(`- ${icon2} ${item.label}${item.detail ? ` — ${item.detail}` : ''}`);
523
- }
524
- lines.push('');
525
-
526
- if (result.blockers.length > 0) {
527
- lines.push('### Blockers (must fix)');
528
- result.blockers.forEach(b => lines.push(`- 🛑 ${b}`));
529
- lines.push('');
530
- }
531
-
532
- if (result.warnings.length > 0) {
533
- lines.push('### Warnings (advisory)');
534
- result.warnings.forEach(w => lines.push(`- ⚠️ ${w}`));
535
- }
536
-
537
- return lines.join('\n');
538
- }
539
-
540
- // ─────────────────────────────────────────────
541
- // CLI Interface
542
- // ─────────────────────────────────────────────
543
-
544
- if (require.main === module) {
545
- const args = process.argv.slice(2);
546
- const cmd = args[0];
547
-
548
- try {
549
- if (cmd === 'security-gates') {
550
- const result = runSecurityGates(process.cwd());
551
- if (args.includes('--json')) {
552
- console.log(JSON.stringify(result, null, 2));
553
- } else {
554
- for (const g of result.gates) {
555
- console.log(`${g.passed ? '✓' : '✗'} ${g.label}: ${g.detail}`);
556
- }
557
- process.exit(result.passed ? 0 : 1);
558
- }
559
- } else if (cmd === 'checklist') {
560
- const tier = args[1];
561
- if (!tier) { console.error('Usage: release-validator.cjs checklist <tier>'); process.exit(1); }
562
- const result = runChecklist(tier, process.cwd());
563
- if (args.includes('--json')) {
564
- console.log(JSON.stringify(result, null, 2));
565
- } else {
566
- for (const item of result.items) {
567
- const icon = item.status === 'pass' ? '✓' : item.status === 'skip' ? '○' : '✗';
568
- console.log(`${icon} ${item.label}`);
569
- }
570
- console.log(`\nScore: ${result.score}/100`);
571
- }
572
- } else if (cmd === 'validate') {
573
- const tier = args[1];
574
- const version = args[2] || '0.0.0';
575
- if (!tier) { console.error('Usage: release-validator.cjs validate <tier> [version]'); process.exit(1); }
576
- const result = validateRelease(tier, version, process.cwd());
577
- if (args.includes('--json')) {
578
- console.log(JSON.stringify(result, null, 2));
579
- } else {
580
- console.log(formatValidation(result));
581
- process.exit(result.valid ? 0 : 1);
582
- }
583
- } else if (cmd === 'checklist-mark') {
584
- // Fix 11: ez checklist mark <id> <approver>
585
- const itemId = args[1];
586
- const approver = args[2];
587
- if (!itemId || !approver) {
588
- console.error('Usage: release-validator.cjs checklist-mark <item-id> <approver>');
589
- process.exit(1);
590
- }
591
- markManualItemComplete(itemId, approver, process.cwd());
592
- console.log(JSON.stringify({ marked: true, item: itemId, approver, timestamp: new Date().toISOString() }));
593
- } else {
594
- console.error(`Unknown command: ${cmd}`);
595
- console.error('Commands: security-gates, checklist, validate, checklist-mark');
596
- process.exit(1);
597
- }
598
- } catch (err) {
599
- console.error(`Error: ${err.message}`);
600
- process.exit(1);
601
- }
602
- }
603
-
604
- module.exports = {
605
- runSecurityGates,
606
- getChecklist,
607
- runChecklist,
608
- validateRelease,
609
- formatValidation,
610
- validateRollbackContent,
611
- loadChecklistState,
612
- markManualItemComplete,
613
- hasHighEntropy
614
- };
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Release Validator — Automated release readiness validation
5
+ *
6
+ * Runs security gates, tier checklist validation, and produces
7
+ * a Production Readiness Score (0-100) for /ez:release.
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { execSync } = require('child_process');
15
+ const TierManager = require('./tier-manager.cjs');
16
+
17
+ // ─────────────────────────────────────────────
18
+ // Security Helpers
19
+ // ─────────────────────────────────────────────
20
+
21
+ /**
22
+ * Calculate Shannon entropy for a string — used to detect high-entropy secrets
23
+ * @param {string} str
24
+ * @param {number} threshold
25
+ * @returns {boolean}
26
+ */
27
+ function hasHighEntropy(str, threshold = 4.5) {
28
+ const freq = {};
29
+ for (const c of str) freq[c] = (freq[c] || 0) + 1;
30
+ const len = str.length;
31
+ let entropy = 0;
32
+ for (const c in freq) {
33
+ const p = freq[c] / len;
34
+ entropy -= p * Math.log2(p);
35
+ }
36
+ return entropy > threshold;
37
+ }
38
+
39
+ // ─────────────────────────────────────────────
40
+ // Security Gates
41
+ // ─────────────────────────────────────────────
42
+
43
+ /**
44
+ * Run all security gates
45
+ * @param {string} cwd - Working directory
46
+ * @returns {{ passed: boolean, gates: object[] }}
47
+ */
48
+ function runSecurityGates(cwd = process.cwd()) {
49
+ const gates = [];
50
+
51
+ // Gate 1: Multi-pattern secret detection
52
+ try {
53
+ const secretPatterns = [
54
+ // Original + variants
55
+ '(api[_-]?key|api[_-]?k[e3]y|password|passw[0o]rd|s[e3]cr[e3]t|auth[_-]?token)',
56
+ // High-value secret types
57
+ '(bearer|private[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret)',
58
+ // Known formats: AWS
59
+ 'AKIA[0-9A-Z]{16}',
60
+ // GitHub PAT
61
+ 'ghp_[a-zA-Z0-9]{36}',
62
+ // JWT pattern
63
+ 'eyJ[a-zA-Z0-9-_=]+\\.[a-zA-Z0-9-_=]+\\.'
64
+ ];
65
+ const secretPattern = secretPatterns.join('|');
66
+ const result = execSync(
67
+ `git grep -i -E "${secretPattern}" HEAD 2>/dev/null | grep -v "example\\|placeholder\\|your-key\\|process\\.env\\|env\\.\\|config\\." | wc -l`,
68
+ { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
69
+ ).trim();
70
+ const count = parseInt(result) || 0;
71
+ gates.push({
72
+ name: 'no_secrets',
73
+ label: 'No secrets in committed files',
74
+ passed: count === 0,
75
+ blocking: true,
76
+ detail: count === 0 ? 'Clean' : `${count} potential secret(s) found`
77
+ });
78
+ } catch {
79
+ gates.push({ name: 'no_secrets', label: 'No secrets in committed files', passed: true, blocking: true, detail: 'Check skipped (git not available)' });
80
+ }
81
+
82
+ // Gate 2: npm audit
83
+ try {
84
+ execSync('npm audit --audit-level=critical 2>/dev/null', { cwd, stdio: 'pipe' });
85
+ gates.push({ name: 'npm_audit', label: 'npm audit — no critical vulnerabilities', passed: true, blocking: true, detail: 'Clean' });
86
+ } catch (err) {
87
+ const output = err.stdout ? err.stdout.toString() : '';
88
+ const criticals = (output.match(/critical/gi) || []).length;
89
+ gates.push({
90
+ name: 'npm_audit',
91
+ label: 'npm audit — no critical vulnerabilities',
92
+ passed: false,
93
+ blocking: true,
94
+ detail: `${criticals} critical vulnerability issue(s). Run: npm audit fix`
95
+ });
96
+ }
97
+
98
+ // Gate 3: No production TODOs
99
+ try {
100
+ const srcDirs = ['src', 'lib', 'app', 'server'].filter(d => fs.existsSync(path.join(cwd, d)));
101
+ if (srcDirs.length > 0) {
102
+ const searchDirs = srcDirs.join(' ');
103
+ const result = execSync(
104
+ `grep -rn "TODO\\|FIXME\\|HACK\\|XXX" ${searchDirs} --include="*.ts" --include="*.js" --include="*.py" 2>/dev/null | grep -v "test\\|spec\\|\\.test\\." | wc -l`,
105
+ { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
106
+ ).trim();
107
+ const count = parseInt(result) || 0;
108
+ gates.push({
109
+ name: 'no_prod_todos',
110
+ label: 'No production TODO/FIXME in src/',
111
+ passed: count === 0,
112
+ blocking: false, // advisory
113
+ detail: count === 0 ? 'Clean' : `${count} TODO/FIXME found in production code`
114
+ });
115
+ } else {
116
+ gates.push({ name: 'no_prod_todos', label: 'No production TODO/FIXME in src/', passed: true, blocking: false, detail: 'No src/ directory found — skipped' });
117
+ }
118
+ } catch {
119
+ gates.push({ name: 'no_prod_todos', label: 'No production TODO/FIXME in src/', passed: true, blocking: false, detail: 'Check skipped' });
120
+ }
121
+
122
+ // Gate 4: .env in .gitignore
123
+ try {
124
+ const gitignore = fs.readFileSync(path.join(cwd, '.gitignore'), 'utf8');
125
+ const protected_ = gitignore.match(/^\.env/m) !== null;
126
+ gates.push({
127
+ name: 'env_protected',
128
+ label: '.env files in .gitignore',
129
+ passed: protected_,
130
+ blocking: true,
131
+ detail: protected_ ? 'Protected' : '.env not found in .gitignore — add it before releasing'
132
+ });
133
+ } catch {
134
+ gates.push({ name: 'env_protected', label: '.env files in .gitignore', passed: false, blocking: true, detail: '.gitignore not found or .env not listed' });
135
+ }
136
+
137
+ // Gate 5: MoSCoW coverage advisory (non-blocking)
138
+ try {
139
+ const featuresDir = path.join(cwd, 'features');
140
+ const testDir = path.join(cwd, 'test');
141
+ const specDir = path.join(cwd, 'spec');
142
+ const bddDir = [featuresDir, testDir, specDir].find(d => fs.existsSync(d));
143
+ if (bddDir) {
144
+ const featureFiles = fs.readdirSync(bddDir).filter(f => f.endsWith('.feature'));
145
+ if (featureFiles.length > 0) {
146
+ let totalScenarios = 0;
147
+ let taggedScenarios = 0;
148
+ for (const file of featureFiles) {
149
+ const content = fs.readFileSync(path.join(bddDir, file), 'utf8');
150
+ const scenarioMatches = content.match(/^\s*Scenario/gm) || [];
151
+ const mustMatches = content.match(/@must|@should|@could|@wont/g) || [];
152
+ totalScenarios += scenarioMatches.length;
153
+ taggedScenarios += mustMatches.length;
154
+ }
155
+ const untaggedPct = totalScenarios > 0 ? ((totalScenarios - taggedScenarios) / totalScenarios) * 100 : 0;
156
+ gates.push({
157
+ name: 'moscow_coverage',
158
+ label: 'MoSCoW tag coverage in BDD scenarios',
159
+ passed: untaggedPct <= 20,
160
+ blocking: false, // advisory
161
+ detail: untaggedPct <= 20
162
+ ? `${Math.round(100 - untaggedPct)}% scenarios tagged`
163
+ : `${Math.round(untaggedPct)}% scenarios missing @must/@should tags`
164
+ });
165
+ } else {
166
+ gates.push({ name: 'moscow_coverage', label: 'MoSCoW tag coverage in BDD scenarios', passed: true, blocking: false, detail: 'No .feature files found — skipped' });
167
+ }
168
+ } else {
169
+ gates.push({ name: 'moscow_coverage', label: 'MoSCoW tag coverage in BDD scenarios', passed: true, blocking: false, detail: 'No BDD directory found — skipped' });
170
+ }
171
+ } catch {
172
+ gates.push({ name: 'moscow_coverage', label: 'MoSCoW tag coverage in BDD scenarios', passed: true, blocking: false, detail: 'Check skipped' });
173
+ }
174
+
175
+ const passed = gates.filter(g => g.passed && g.blocking).length;
176
+ const total = gates.filter(g => g.blocking).length;
177
+ const allPassed = gates.filter(g => g.blocking).every(g => g.passed);
178
+
179
+ return { passed: allPassed, gates, score: `${passed}/${total}` };
180
+ }
181
+
182
+ // ─────────────────────────────────────────────
183
+ // Tier Checklist
184
+ // ─────────────────────────────────────────────
185
+
186
+ const MVP_CHECKLIST = [
187
+ { id: 'bdd_must', label: 'All @must BDD scenarios passing', auto: true },
188
+ { id: 'npm_audit', label: 'npm audit — no critical vulnerabilities', auto: true },
189
+ { id: 'health_endpoint', label: 'Health endpoint returns 200', auto: true },
190
+ { id: 'no_secrets', label: 'No secrets in committed files', auto: true },
191
+ { id: 'app_starts', label: 'Application starts without errors', auto: true },
192
+ { id: 'rollback_documented', label: 'Rollback procedure documented', auto: true },
193
+ { id: 'security_scan', label: 'Baseline security scan completed', auto: true },
194
+ { id: 'audit_logging', label: 'Audit logging enabled for security-sensitive actions', auto: true },
195
+ { id: 'compliance_evidence', label: 'Required compliance checklist/evidence files present', auto: false }
196
+ ];
197
+
198
+ const MEDIUM_EXTRA = [
199
+ { id: 'bdd_should', label: 'All @should BDD scenarios passing', auto: true },
200
+ { id: 'coverage_80', label: 'Test coverage 80%', auto: true },
201
+ { id: 'staging_parity', label: 'Staging environment parity verified', auto: false },
202
+ { id: 'monitoring', label: 'Monitoring/alerts configured', auto: false },
203
+ { id: 'structured_logging', label: 'Structured logging (no console.log in prod)', auto: true },
204
+ { id: 'perf_baseline', label: 'Performance baseline documented', auto: false },
205
+ { id: 'error_tracking', label: 'Error tracking configured', auto: false },
206
+ { id: 'db_migrations', label: 'Database migrations tested on staging', auto: false },
207
+ { id: 'api_docs', label: 'API documentation current', auto: false },
208
+ { id: 'env_example', label: '.env.example up to date', auto: true },
209
+ { id: 'graceful_shutdown', label: 'Graceful shutdown handled', auto: true },
210
+ { id: 'rate_limiting', label: 'Rate limiting on public API endpoints', auto: true }
211
+ ];
212
+
213
+ const ENTERPRISE_EXTRA = [
214
+ { id: 'bdd_could', label: 'All @could BDD scenarios passing', auto: true },
215
+ { id: 'coverage_95', label: 'Test coverage 95%', auto: true },
216
+ { id: 'security_audit', label: 'Security audit completed', auto: false },
217
+ { id: 'compliance_docs', label: 'Compliance documentation updated', auto: false },
218
+ { id: 'load_test', label: 'Load test results documented', auto: false },
219
+ { id: 'dr_tested', label: 'Disaster recovery tested', auto: false },
220
+ { id: 'data_retention', label: 'Data retention policy configured', auto: false },
221
+ { id: 'audit_logging', label: 'Audit logging enabled', auto: true },
222
+ { id: 'pentest', label: 'Penetration test completed or scheduled', auto: false },
223
+ { id: 'soc2_gdpr', label: 'SOC2/GDPR controls validated', auto: false },
224
+ { id: 'change_ticket', label: 'Change management ticket filed', auto: false },
225
+ { id: 'incident_runbook', label: 'Incident runbook up to date', auto: false }
226
+ ];
227
+
228
+ // ─────────────────────────────────────────────
229
+ // Rollback Validation
230
+ // ─────────────────────────────────────────────
231
+
232
+ /**
233
+ * Validate rollback plan content — checks for unfilled placeholders
234
+ * @param {string} cwd
235
+ * @returns {{ status: string, detail: string }}
236
+ */
237
+ function validateRollbackContent(cwd) {
238
+ const releasesDir = path.join(cwd, '.planning', 'releases');
239
+ if (!fs.existsSync(releasesDir)) {
240
+ return { status: 'fail', detail: 'No .planning/releases/ directory found' };
241
+ }
242
+
243
+ const rollbackFiles = fs.readdirSync(releasesDir)
244
+ .filter(f => f.includes('ROLLBACK') && f.endsWith('.md'));
245
+
246
+ if (rollbackFiles.length === 0) {
247
+ return { status: 'fail', detail: 'No rollback plan found' };
248
+ }
249
+
250
+ const latest = rollbackFiles.sort().pop();
251
+ const content = fs.readFileSync(path.join(releasesDir, latest), 'utf8');
252
+
253
+ // Detect unfilled placeholders like {name}, {migration_name}, {your-domain}
254
+ const placeholders = content.match(/\{[a-z_-]+\}/gi) || [];
255
+ if (placeholders.length > 0) {
256
+ return {
257
+ status: 'fail',
258
+ detail: `Rollback plan has ${placeholders.length} unfilled placeholder(s): ${placeholders.slice(0, 3).join(', ')}`
259
+ };
260
+ }
261
+
262
+ return { status: 'pass', detail: `Rollback plan validated: ${latest}` };
263
+ }
264
+
265
+ // ─────────────────────────────────────────────
266
+ // Manual Checklist State Persistence (Fix 11)
267
+ // ─────────────────────────────────────────────
268
+
269
+ /**
270
+ * Load persisted manual checklist state
271
+ * @param {string} cwd
272
+ * @returns {object}
273
+ */
274
+ function loadChecklistState(cwd) {
275
+ const statePath = path.join(cwd, '.planning', 'releases', 'checklist-state.json');
276
+ if (!fs.existsSync(statePath)) return {};
277
+ try { return JSON.parse(fs.readFileSync(statePath, 'utf8')); }
278
+ catch { return {}; }
279
+ }
280
+
281
+ /**
282
+ * Mark a manual checklist item as complete with approver and timestamp
283
+ * @param {string} itemId
284
+ * @param {string} approver
285
+ * @param {string} cwd
286
+ */
287
+ function markManualItemComplete(itemId, approver, cwd = process.cwd()) {
288
+ const state = loadChecklistState(cwd);
289
+ state[itemId] = {
290
+ approved: true,
291
+ approver: approver || 'unknown',
292
+ timestamp: new Date().toISOString(),
293
+ expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() // 30 days
294
+ };
295
+ const statePath = path.join(cwd, '.planning', 'releases', 'checklist-state.json');
296
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
297
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
298
+ }
299
+
300
+ /**
301
+ * Get full checklist for a tier
302
+ * @param {string} tier
303
+ * @returns {object[]}
304
+ */
305
+ function getChecklist(tier) {
306
+ const t = tier.toLowerCase();
307
+ if (t === 'mvp') return [...MVP_CHECKLIST];
308
+ if (t === 'medium') return [...MVP_CHECKLIST, ...MEDIUM_EXTRA];
309
+ if (t === 'enterprise') return [...MVP_CHECKLIST, ...MEDIUM_EXTRA, ...ENTERPRISE_EXTRA];
310
+ throw new Error(`Unknown tier: ${tier}`);
311
+ }
312
+
313
+ /**
314
+ * Run automated checklist items
315
+ * @param {string} tier
316
+ * @param {string} cwd
317
+ * @param {object} context - additional context (coverage, bddResults, etc.)
318
+ * @returns {{ items: object[], passed: number, total: number, score: number }}
319
+ */
320
+ function runChecklist(tier, cwd = process.cwd(), context = {}) {
321
+ const items = getChecklist(tier);
322
+ const results = [];
323
+
324
+ for (const item of items) {
325
+ let result;
326
+ if (!item.auto) {
327
+ // Fix 11: Check persisted state for manual items
328
+ const state = loadChecklistState(cwd);
329
+ const saved = state[item.id];
330
+ if (saved && saved.approved) {
331
+ const age = Date.now() - new Date(saved.timestamp).getTime();
332
+ const ageDays = Math.floor(age / (1000 * 60 * 60 * 24));
333
+ if (ageDays > 30) {
334
+ result = { ...item, status: 'fail', detail: `Manual check expired ${ageDays}d ago — re-verify required` };
335
+ } else {
336
+ result = { ...item, status: 'pass', detail: `Verified by ${saved.approver} on ${saved.timestamp.split('T')[0]}` };
337
+ }
338
+ } else {
339
+ result = { ...item, status: 'manual', detail: 'Requires manual verification (run: ez checklist mark <id> <approver>)' };
340
+ }
341
+ } else {
342
+ result = runChecklistItem(item, cwd, context);
343
+ }
344
+ results.push(result);
345
+ }
346
+
347
+ const autoItems = results.filter(r => r.auto);
348
+ const passed = autoItems.filter(r => r.status === 'pass').length;
349
+ const total = autoItems.length;
350
+
351
+ // Compute readiness score: blocking failures cost 10, advisory failures cost 2
352
+ let score = 100;
353
+ for (const r of results) {
354
+ if (r.status === 'fail') {
355
+ score -= r.blocking !== false ? 10 : 2;
356
+ }
357
+ }
358
+ score = Math.max(0, score);
359
+
360
+ return { items: results, passed, total, score };
361
+ }
362
+
363
+ function runChecklistItem(item, cwd, context) {
364
+ try {
365
+ switch (item.id) {
366
+ case 'npm_audit':
367
+ case 'no_secrets':
368
+ // Already handled in security gates — check from context
369
+ return { ...item, status: 'pass', detail: 'Verified in security gates' };
370
+
371
+ case 'coverage_80': {
372
+ const cov = context.coverage;
373
+ if (cov === undefined) return { ...item, status: 'skip', detail: 'No coverage data available' };
374
+ return { ...item, status: cov >= 80 ? 'pass' : 'fail', detail: `Coverage: ${cov}%` };
375
+ }
376
+
377
+ case 'coverage_95': {
378
+ const cov = context.coverage;
379
+ if (cov === undefined) return { ...item, status: 'skip', detail: 'No coverage data available' };
380
+ return { ...item, status: cov >= 95 ? 'pass' : 'fail', detail: `Coverage: ${cov}%` };
381
+ }
382
+
383
+ case 'bdd_must': {
384
+ const { bddPassed, moscowTagged, totalScenarios } = context;
385
+ // Hard gate: fail if there are too many untagged scenarios
386
+ if (moscowTagged !== undefined && totalScenarios > 0) {
387
+ const untaggedPct = ((totalScenarios - moscowTagged) / totalScenarios) * 100;
388
+ if (untaggedPct > 20) { // > 20% untagged = blocking
389
+ return { ...item, status: 'fail', detail: `${Math.round(untaggedPct)}% scenarios missing @must/@should tags BDD coverage unverifiable` };
390
+ }
391
+ }
392
+ if (bddPassed === undefined) return { ...item, status: 'skip', detail: 'No BDD results — run test suite first' };
393
+ return { ...item, status: bddPassed ? 'pass' : 'fail', detail: bddPassed ? 'All scenarios passing' : 'Some scenarios failing' };
394
+ }
395
+
396
+ case 'bdd_should':
397
+ case 'bdd_could': {
398
+ const bddPassed = context.bddPassed;
399
+ if (bddPassed === undefined) return { ...item, status: 'skip', detail: 'No BDD test results available' };
400
+ return { ...item, status: bddPassed ? 'pass' : 'fail', detail: bddPassed ? 'All scenarios passing' : 'Some scenarios failing' };
401
+ }
402
+
403
+ case 'rollback_documented': {
404
+ const rollbackResult = validateRollbackContent(cwd);
405
+ return { ...item, status: rollbackResult.status, detail: rollbackResult.detail };
406
+ }
407
+
408
+ case 'env_example': {
409
+ const hasExample = fs.existsSync(path.join(cwd, '.env.example'));
410
+ return { ...item, status: hasExample ? 'pass' : 'fail', detail: hasExample ? '.env.example found' : '.env.example missing' };
411
+ }
412
+
413
+ case 'structured_logging': {
414
+ try {
415
+ const srcDirs = ['src', 'lib', 'app'].filter(d => fs.existsSync(path.join(cwd, d)));
416
+ if (srcDirs.length === 0) return { ...item, status: 'skip', detail: 'No src/ found' };
417
+ const result = execSync(
418
+ `grep -rn "console\\.log" ${srcDirs.join(' ')} --include="*.ts" --include="*.js" 2>/dev/null | grep -v "test\\|spec" | wc -l`,
419
+ { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
420
+ ).trim();
421
+ const count = parseInt(result) || 0;
422
+ return { ...item, status: count === 0 ? 'pass' : 'fail', detail: count === 0 ? 'No console.log in prod' : `${count} console.log found` };
423
+ } catch {
424
+ return { ...item, status: 'skip', detail: 'Check failed' };
425
+ }
426
+ }
427
+
428
+ case 'health_endpoint': {
429
+ // Try to detect health endpoint in source
430
+ try {
431
+ const srcDirs = ['src', 'app', 'server', 'pages/api'].filter(d => fs.existsSync(path.join(cwd, d)));
432
+ if (srcDirs.length === 0) return { ...item, status: 'skip', detail: 'No src/ found' };
433
+ const result = execSync(
434
+ `grep -rn "health\\|/ping\\|/status" ${srcDirs.join(' ')} --include="*.ts" --include="*.js" 2>/dev/null | wc -l`,
435
+ { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
436
+ ).trim();
437
+ const found = parseInt(result) > 0;
438
+ return { ...item, status: found ? 'pass' : 'skip', detail: found ? 'Health endpoint found in source' : 'No health endpoint found (optional for MVP)' };
439
+ } catch {
440
+ return { ...item, status: 'skip', detail: 'Check failed' };
441
+ }
442
+ }
443
+
444
+ default:
445
+ return { ...item, status: 'skip', detail: 'Automated check not implemented' };
446
+ }
447
+ } catch (err) {
448
+ return { ...item, status: 'error', detail: err.message };
449
+ }
450
+ }
451
+
452
+ // ─────────────────────────────────────────────
453
+ // Full Validation
454
+ // ─────────────────────────────────────────────
455
+
456
+ /**
457
+ * Run full release validation
458
+ * @param {string} tier
459
+ * @param {string} version
460
+ * @param {string} cwd
461
+ * @param {object} context
462
+ * @returns {{ valid: boolean, blockers: string[], warnings: string[], score: number, securityGates: object, checklist: object }}
463
+ */
464
+ function validateRelease(tier, version, cwd = process.cwd(), context = {}) {
465
+ const securityGates = runSecurityGates(cwd);
466
+ const checklist = runChecklist(tier, cwd, context);
467
+
468
+ const blockers = [];
469
+ const warnings = [];
470
+
471
+ // Security gate failures
472
+ for (const gate of securityGates.gates) {
473
+ if (!gate.passed && gate.blocking) blockers.push(`Security: ${gate.label} — ${gate.detail}`);
474
+ if (!gate.passed && !gate.blocking) warnings.push(`Security: ${gate.label} — ${gate.detail}`);
475
+ }
476
+
477
+ // Checklist failures
478
+ for (const item of checklist.items) {
479
+ if (item.status === 'fail') {
480
+ if (item.blocking !== false) warnings.push(`Checklist: ${item.label}`);
481
+ }
482
+ }
483
+
484
+ const valid = blockers.length === 0;
485
+ const readinessScore = Math.min(checklist.score, securityGates.passed ? 100 : 50);
486
+
487
+ const readinessStatus = readinessScore >= 90 ? 'READY'
488
+ : readinessScore >= 70 ? 'CONDITIONAL'
489
+ : 'NOT READY';
490
+
491
+ return {
492
+ valid,
493
+ tier,
494
+ version,
495
+ blockers,
496
+ warnings,
497
+ score: readinessScore,
498
+ readinessStatus,
499
+ securityGates,
500
+ checklist
501
+ };
502
+ }
503
+
504
+ /**
505
+ * Format validation result as markdown
506
+ */
507
+ function formatValidation(result) {
508
+ const lines = [];
509
+ const icon = result.valid ? '' : '';
510
+
511
+ lines.push(`## Release Validation: v${result.version} (${result.tier})`);
512
+ lines.push(`**Status:** ${icon} ${result.valid ? 'READY' : 'BLOCKED'}`);
513
+ lines.push(`**Production Readiness Score:** ${result.score}/100 — ${result.readinessStatus}`);
514
+ lines.push('');
515
+
516
+ lines.push('### Security Gates');
517
+ for (const g of result.securityGates.gates) {
518
+ lines.push(`- ${g.passed ? '✓' : '✗'} ${g.label}: ${g.detail}`);
519
+ }
520
+ lines.push('');
521
+
522
+ lines.push(`### Checklist (${result.tier})`);
523
+ for (const item of result.checklist.items) {
524
+ const icon2 = item.status === 'pass' ? '✓' : item.status === 'skip' ? '○' : item.status === 'manual' ? '?' : '✗';
525
+ lines.push(`- ${icon2} ${item.label}${item.detail ? ` — ${item.detail}` : ''}`);
526
+ }
527
+ lines.push('');
528
+
529
+ if (result.blockers.length > 0) {
530
+ lines.push('### Blockers (must fix)');
531
+ result.blockers.forEach(b => lines.push(`- 🛑 ${b}`));
532
+ lines.push('');
533
+ }
534
+
535
+ if (result.warnings.length > 0) {
536
+ lines.push('### Warnings (advisory)');
537
+ result.warnings.forEach(w => lines.push(`- ⚠️ ${w}`));
538
+ }
539
+
540
+ return lines.join('\n');
541
+ }
542
+
543
+ // ─────────────────────────────────────────────
544
+ // CLI Interface
545
+ // ─────────────────────────────────────────────
546
+
547
+ if (require.main === module) {
548
+ const args = process.argv.slice(2);
549
+ const cmd = args[0];
550
+
551
+ try {
552
+ if (cmd === 'security-gates') {
553
+ const result = runSecurityGates(process.cwd());
554
+ if (args.includes('--json')) {
555
+ console.log(JSON.stringify(result, null, 2));
556
+ } else {
557
+ for (const g of result.gates) {
558
+ console.log(`${g.passed ? '✓' : '✗'} ${g.label}: ${g.detail}`);
559
+ }
560
+ process.exit(result.passed ? 0 : 1);
561
+ }
562
+ } else if (cmd === 'checklist') {
563
+ const tier = args[1];
564
+ if (!tier) { console.error('Usage: release-validator.cjs checklist <tier>'); process.exit(1); }
565
+ const result = runChecklist(tier, process.cwd());
566
+ if (args.includes('--json')) {
567
+ console.log(JSON.stringify(result, null, 2));
568
+ } else {
569
+ for (const item of result.items) {
570
+ const icon = item.status === 'pass' ? '✓' : item.status === 'skip' ? '○' : '✗';
571
+ console.log(`${icon} ${item.label}`);
572
+ }
573
+ console.log(`\nScore: ${result.score}/100`);
574
+ }
575
+ } else if (cmd === 'validate') {
576
+ const tier = args[1];
577
+ const version = args[2] || '0.0.0';
578
+ if (!tier) { console.error('Usage: release-validator.cjs validate <tier> [version]'); process.exit(1); }
579
+ const result = validateRelease(tier, version, process.cwd());
580
+ if (args.includes('--json')) {
581
+ console.log(JSON.stringify(result, null, 2));
582
+ } else {
583
+ console.log(formatValidation(result));
584
+ process.exit(result.valid ? 0 : 1);
585
+ }
586
+ } else if (cmd === 'checklist-mark') {
587
+ // Fix 11: ez checklist mark <id> <approver>
588
+ const itemId = args[1];
589
+ const approver = args[2];
590
+ if (!itemId || !approver) {
591
+ console.error('Usage: release-validator.cjs checklist-mark <item-id> <approver>');
592
+ process.exit(1);
593
+ }
594
+ markManualItemComplete(itemId, approver, process.cwd());
595
+ console.log(JSON.stringify({ marked: true, item: itemId, approver, timestamp: new Date().toISOString() }));
596
+ } else {
597
+ console.error(`Unknown command: ${cmd}`);
598
+ console.error('Commands: security-gates, checklist, validate, checklist-mark');
599
+ process.exit(1);
600
+ }
601
+ } catch (err) {
602
+ console.error(`Error: ${err.message}`);
603
+ process.exit(1);
604
+ }
605
+ }
606
+
607
+ module.exports = {
608
+ runSecurityGates,
609
+ getChecklist,
610
+ runChecklist,
611
+ validateRelease,
612
+ formatValidation,
613
+ validateRollbackContent,
614
+ loadChecklistState,
615
+ markManualItemComplete,
616
+ hasHighEntropy
617
+ };