@howlil/ez-agents 3.4.2 → 3.5.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 (74) hide show
  1. package/README.md +77 -2
  2. package/agents/ez-observer-agent.md +260 -0
  3. package/agents/ez-release-agent.md +333 -0
  4. package/agents/ez-requirements-agent.md +377 -0
  5. package/agents/ez-scrum-master-agent.md +242 -0
  6. package/agents/ez-tech-lead-agent.md +267 -0
  7. package/bin/install.js +3221 -3272
  8. package/commands/ez/arch-review.md +102 -0
  9. package/commands/ez/execute-phase.md +11 -0
  10. package/commands/ez/export-session.md +79 -0
  11. package/commands/ez/gather-requirements.md +117 -0
  12. package/commands/ez/git-workflow.md +72 -0
  13. package/commands/ez/hotfix.md +120 -0
  14. package/commands/ez/import-session.md +82 -0
  15. package/commands/ez/list-sessions.md +96 -0
  16. package/commands/ez/package-manager.md +316 -0
  17. package/commands/ez/plan-phase.md +9 -1
  18. package/commands/ez/preflight.md +79 -0
  19. package/commands/ez/progress.md +13 -1
  20. package/commands/ez/release.md +153 -0
  21. package/commands/ez/resume.md +107 -0
  22. package/commands/ez/standup.md +85 -0
  23. package/ez-agents/bin/ez-tools.cjs +1095 -716
  24. package/ez-agents/bin/lib/bdd-validator.cjs +622 -0
  25. package/ez-agents/bin/lib/content-scanner.cjs +238 -0
  26. package/ez-agents/bin/lib/context-cache.cjs +154 -0
  27. package/ez-agents/bin/lib/context-errors.cjs +71 -0
  28. package/ez-agents/bin/lib/context-manager.cjs +220 -0
  29. package/ez-agents/bin/lib/discussion-synthesizer.cjs +458 -0
  30. package/ez-agents/bin/lib/file-access.cjs +207 -0
  31. package/ez-agents/bin/lib/git-errors.cjs +83 -0
  32. package/ez-agents/bin/lib/git-utils.cjs +321 -203
  33. package/ez-agents/bin/lib/git-workflow-engine.cjs +1157 -0
  34. package/ez-agents/bin/lib/index.cjs +46 -2
  35. package/ez-agents/bin/lib/lockfile-validator.cjs +227 -0
  36. package/ez-agents/bin/lib/logger.cjs +124 -154
  37. package/ez-agents/bin/lib/memory-compression.cjs +256 -0
  38. package/ez-agents/bin/lib/metrics-tracker.cjs +406 -0
  39. package/ez-agents/bin/lib/package-manager-detector.cjs +203 -0
  40. package/ez-agents/bin/lib/package-manager-executor.cjs +385 -0
  41. package/ez-agents/bin/lib/package-manager-service.cjs +216 -0
  42. package/ez-agents/bin/lib/release-validator.cjs +614 -0
  43. package/ez-agents/bin/lib/safe-exec.cjs +128 -214
  44. package/ez-agents/bin/lib/session-chain.cjs +304 -0
  45. package/ez-agents/bin/lib/session-errors.cjs +81 -0
  46. package/ez-agents/bin/lib/session-export.cjs +251 -0
  47. package/ez-agents/bin/lib/session-import.cjs +262 -0
  48. package/ez-agents/bin/lib/session-manager.cjs +280 -0
  49. package/ez-agents/bin/lib/tier-manager.cjs +428 -0
  50. package/ez-agents/bin/lib/url-fetch.cjs +170 -0
  51. package/ez-agents/references/metrics-schema.md +118 -0
  52. package/ez-agents/references/planning-config.md +140 -0
  53. package/ez-agents/references/tier-strategy.md +103 -0
  54. package/ez-agents/templates/bdd-feature.md +173 -0
  55. package/ez-agents/templates/discussion.md +68 -0
  56. package/ez-agents/templates/incident-runbook.md +205 -0
  57. package/ez-agents/templates/release-checklist.md +133 -0
  58. package/ez-agents/templates/rollback-plan.md +201 -0
  59. package/ez-agents/workflows/arch-review.md +54 -0
  60. package/ez-agents/workflows/autonomous.md +844 -743
  61. package/ez-agents/workflows/execute-phase.md +45 -0
  62. package/ez-agents/workflows/export-session.md +255 -0
  63. package/ez-agents/workflows/gather-requirements.md +206 -0
  64. package/ez-agents/workflows/help.md +92 -0
  65. package/ez-agents/workflows/hotfix.md +291 -0
  66. package/ez-agents/workflows/import-session.md +303 -0
  67. package/ez-agents/workflows/new-milestone.md +713 -384
  68. package/ez-agents/workflows/new-project.md +1107 -1113
  69. package/ez-agents/workflows/plan-phase.md +22 -0
  70. package/ez-agents/workflows/progress.md +15 -25
  71. package/ez-agents/workflows/release.md +253 -0
  72. package/ez-agents/workflows/resume-session.md +215 -0
  73. package/ez-agents/workflows/standup.md +64 -0
  74. package/package.json +9 -2
@@ -0,0 +1,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
+ ];
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 results — run 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
+ };