wogiflow 2.7.1 → 2.9.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.
@@ -0,0 +1,766 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Audit Gate 0: Pre-Agent Baseline Checks
5
+ *
6
+ * Runs BEFORE the 7 analysis agents to establish a project health baseline.
7
+ * These are hard, verifiable checks — not AI judgment. They produce quantitative
8
+ * metrics that cap the final audit score.
9
+ *
10
+ * A project that can't build should never score higher than D, regardless
11
+ * of how elegant its architecture is.
12
+ *
13
+ * Commands:
14
+ * flow-audit-gates.js run — Run all Gate 0 checks
15
+ * flow-audit-gates.js build — Build check only
16
+ * flow-audit-gates.js typecheck — Typecheck only
17
+ * flow-audit-gates.js lint — Lint check only
18
+ * flow-audit-gates.js lint-config — Lint config integrity
19
+ * flow-audit-gates.js tests — Test check only
20
+ * flow-audit-gates.js scripts — Package.json script completeness
21
+ * flow-audit-gates.js dead-exports — Find exported functions with no importers
22
+ * flow-audit-gates.js eslint-disable — Count eslint-disable comments
23
+ * flow-audit-gates.js dep-health — Dependency health (outdated, deprecated)
24
+ * flow-audit-gates.js git-health — Git history health indicators
25
+ * flow-audit-gates.js env-hygiene — Environment/config hygiene
26
+ * flow-audit-gates.js test-coverage — Test coverage metrics
27
+ * flow-audit-gates.js framework — Auto-detect framework
28
+ */
29
+
30
+ 'use strict';
31
+
32
+ const { execFileSync } = require('node:child_process');
33
+ const fs = require('node:fs');
34
+ const path = require('node:path');
35
+
36
+ const { PATHS, getConfig, safeJsonParse, color } = require('./flow-utils');
37
+
38
+ // ============================================================
39
+ // Score Cap Thresholds
40
+ // ============================================================
41
+
42
+ /** Grade values for comparison (higher = better) */
43
+ const GRADE_VALUES = { 'A+': 97, 'A': 93, 'A-': 90, 'B+': 87, 'B': 83, 'B-': 80, 'C+': 77, 'C': 73, 'C-': 70, 'D+': 67, 'D': 63, 'D-': 60, 'F': 50 };
44
+
45
+ /**
46
+ * Convert a numeric score to a letter grade.
47
+ * @param {number} score — 0-100
48
+ * @returns {string} letter grade
49
+ */
50
+ function scoreToGrade(score) {
51
+ if (score >= 97) return 'A+';
52
+ if (score >= 93) return 'A';
53
+ if (score >= 90) return 'A-';
54
+ if (score >= 87) return 'B+';
55
+ if (score >= 83) return 'B';
56
+ if (score >= 80) return 'B-';
57
+ if (score >= 77) return 'C+';
58
+ if (score >= 73) return 'C';
59
+ if (score >= 70) return 'C-';
60
+ if (score >= 67) return 'D+';
61
+ if (score >= 63) return 'D';
62
+ if (score >= 60) return 'D-';
63
+ return 'F';
64
+ }
65
+
66
+ // ============================================================
67
+ // Gate 0 Checks
68
+ // ============================================================
69
+
70
+ /**
71
+ * Run a project script and capture results.
72
+ * @param {string} scriptName — npm script name (e.g., 'build', 'typecheck')
73
+ * @param {number} [timeout=60000] — timeout in ms
74
+ * @returns {{ exists: boolean, passed: boolean, output: string, errorCount: number }}
75
+ */
76
+ function runProjectScript(scriptName, timeout = 60000) {
77
+ const pkgPath = path.join(PATHS.root, 'package.json');
78
+ const pkg = safeJsonParse(pkgPath, {});
79
+ const scripts = pkg.scripts || {};
80
+
81
+ if (!scripts[scriptName]) {
82
+ return { exists: false, passed: false, output: `Script "${scriptName}" not defined in package.json`, errorCount: 0 };
83
+ }
84
+
85
+ try {
86
+ const output = execFileSync('npm', ['run', scriptName], {
87
+ cwd: PATHS.root,
88
+ encoding: 'utf-8',
89
+ timeout,
90
+ stdio: ['pipe', 'pipe', 'pipe'],
91
+ env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' }
92
+ });
93
+ return { exists: true, passed: true, output: output.substring(0, 5000), errorCount: 0 };
94
+ } catch (err) {
95
+ const output = (err.stdout || '') + (err.stderr || '');
96
+ // Count error lines
97
+ const errorCount = (output.match(/error TS\d+|Error:|ERROR/gi) || []).length;
98
+ return { exists: true, passed: false, output: output.substring(0, 5000), errorCount };
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Gate: Build check — does the project compile?
104
+ */
105
+ function checkBuild() {
106
+ const result = runProjectScript('build', 120000);
107
+ let scoreCap = 100;
108
+ if (result.exists && !result.passed) scoreCap = 63; // D max
109
+
110
+ return {
111
+ gate: 'build',
112
+ ...result,
113
+ scoreCap,
114
+ severity: result.passed ? 'pass' : 'critical',
115
+ message: !result.exists ? 'No build script defined' :
116
+ result.passed ? 'Build succeeds' :
117
+ `Build FAILS — project cannot be deployed (cap: D)`
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Gate: Typecheck — how many type errors?
123
+ */
124
+ function checkTypecheck() {
125
+ const result = runProjectScript('typecheck', 120000);
126
+
127
+ // Parse error count from output
128
+ let errorCount = result.errorCount;
129
+ if (!errorCount && result.output) {
130
+ // Try to parse TS error count from "Found N errors" pattern
131
+ const match = result.output.match(/Found (\d+) errors?/i);
132
+ if (match) errorCount = parseInt(match[1], 10);
133
+ // Or count "error TS" lines
134
+ if (!errorCount) {
135
+ errorCount = (result.output.match(/error TS\d+/g) || []).length;
136
+ }
137
+ }
138
+
139
+ let scoreCap = 100;
140
+ if (errorCount > 500) scoreCap = 67; // D+ max
141
+ else if (errorCount > 100) scoreCap = 73; // C max
142
+ else if (errorCount > 50) scoreCap = 77; // C+ max
143
+
144
+ return {
145
+ gate: 'typecheck',
146
+ ...result,
147
+ errorCount,
148
+ scoreCap,
149
+ severity: !result.exists ? 'info' :
150
+ result.passed ? 'pass' :
151
+ errorCount > 100 ? 'critical' : 'high',
152
+ message: !result.exists ? 'No typecheck script defined' :
153
+ result.passed ? 'Typecheck passes (0 errors)' :
154
+ `Typecheck FAILS: ${errorCount} error(s) (cap: ${scoreToGrade(scoreCap)})`
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Gate: Lint check — count errors vs warnings.
160
+ */
161
+ function checkLint() {
162
+ const result = runProjectScript('lint', 60000);
163
+
164
+ // Parse error/warning counts
165
+ let errorCount = 0;
166
+ let warningCount = 0;
167
+ if (result.output) {
168
+ // ESLint format: "N problems (X errors, Y warnings)"
169
+ const match = result.output.match(/(\d+) problems?\s*\((\d+) errors?,\s*(\d+) warnings?\)/);
170
+ if (match) {
171
+ errorCount = parseInt(match[2], 10);
172
+ warningCount = parseInt(match[3], 10);
173
+ }
174
+ }
175
+
176
+ let scoreCap = 100;
177
+ if (errorCount > 50) scoreCap = 73; // C max
178
+
179
+ return {
180
+ gate: 'lint',
181
+ ...result,
182
+ errorCount,
183
+ warningCount,
184
+ scoreCap,
185
+ severity: !result.exists ? 'info' :
186
+ errorCount > 50 ? 'critical' :
187
+ errorCount > 0 ? 'high' :
188
+ warningCount > 50 ? 'medium' : 'pass',
189
+ message: !result.exists ? 'No lint script defined' :
190
+ errorCount === 0 && warningCount === 0 ? 'Lint passes (0 errors, 0 warnings)' :
191
+ `Lint: ${errorCount} error(s), ${warningCount} warning(s)${scoreCap < 100 ? ` (cap: ${scoreToGrade(scoreCap)})` : ''}`
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Gate: Lint config integrity — detect downgraded rules.
197
+ * Checks if rules that should be 'error' (per recommended presets) are set to 'warn' or 'off'.
198
+ */
199
+ function checkLintConfigIntegrity() {
200
+ const result = {
201
+ gate: 'lint-config',
202
+ exists: false,
203
+ downgradedRules: [],
204
+ scorePenalty: 0,
205
+ severity: 'pass',
206
+ message: 'No lint config found'
207
+ };
208
+
209
+ // Find ESLint config
210
+ const configFiles = [
211
+ 'eslint.config.js', 'eslint.config.ts', 'eslint.config.mjs',
212
+ '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml', '.eslintrc'
213
+ ];
214
+
215
+ let configPath = null;
216
+ for (const f of configFiles) {
217
+ const p = path.join(PATHS.root, f);
218
+ if (fs.existsSync(p)) { configPath = p; break; }
219
+ }
220
+ if (!configPath) return result;
221
+
222
+ result.exists = true;
223
+ let content;
224
+ try {
225
+ content = fs.readFileSync(configPath, 'utf-8');
226
+ } catch (_err) {
227
+ result.message = 'Lint config found but unreadable';
228
+ return result;
229
+ }
230
+
231
+ // Rules that should be 'error' per recommended presets
232
+ const SHOULD_BE_ERROR = [
233
+ 'react-hooks/rules-of-hooks',
234
+ 'react-hooks/exhaustive-deps',
235
+ 'no-undef',
236
+ 'no-unused-vars',
237
+ '@typescript-eslint/no-unused-vars',
238
+ 'no-dupe-keys',
239
+ 'no-duplicate-case',
240
+ 'no-unreachable',
241
+ 'no-constant-condition',
242
+ 'no-empty-pattern',
243
+ 'no-self-assign',
244
+ 'no-unsafe-negation',
245
+ 'no-loss-of-precision',
246
+ 'no-import-assign'
247
+ ];
248
+
249
+ for (const rule of SHOULD_BE_ERROR) {
250
+ // Check for patterns like: 'rule-name': 'warn' or "rule-name": "warn" or 'rule-name': 'off'
251
+ const escaped = rule.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
252
+ const warnPattern = new RegExp(`['"]${escaped}['"]\\s*:\\s*['"](?:warn|off|0)['"]`);
253
+ if (warnPattern.test(content)) {
254
+ result.downgradedRules.push(rule);
255
+ }
256
+ }
257
+
258
+ if (result.downgradedRules.length > 0) {
259
+ result.scorePenalty = Math.min(15, result.downgradedRules.length * 3);
260
+ result.severity = result.downgradedRules.length > 3 ? 'critical' : 'high';
261
+ result.message = `Lint config: ${result.downgradedRules.length} rule(s) downgraded from error to warn/off (-${result.scorePenalty} pts): ${result.downgradedRules.join(', ')}`;
262
+ } else {
263
+ result.message = 'Lint config integrity: no downgraded rules detected';
264
+ }
265
+
266
+ return result;
267
+ }
268
+
269
+ /**
270
+ * Gate: Tests — do tests pass?
271
+ */
272
+ function checkTests() {
273
+ const result = runProjectScript('test', 120000);
274
+ return {
275
+ gate: 'tests',
276
+ ...result,
277
+ scoreCap: 100, // Test failure doesn't cap, but is a HIGH finding
278
+ severity: !result.exists ? 'info' :
279
+ result.passed ? 'pass' : 'high',
280
+ message: !result.exists ? 'No test script defined' :
281
+ result.passed ? 'Tests pass' : 'Tests FAIL'
282
+ };
283
+ }
284
+
285
+ /**
286
+ * Gate: Package.json script completeness.
287
+ */
288
+ function checkScriptCompleteness() {
289
+ const pkgPath = path.join(PATHS.root, 'package.json');
290
+ const pkg = safeJsonParse(pkgPath, {});
291
+ const scripts = pkg.scripts || {};
292
+
293
+ const EXPECTED = ['build', 'test', 'lint', 'typecheck', 'dev'];
294
+ const missing = EXPECTED.filter(s => !scripts[s] && !scripts[`${s}:check`]);
295
+
296
+ return {
297
+ gate: 'scripts',
298
+ missing,
299
+ present: EXPECTED.filter(s => scripts[s] || scripts[`${s}:check`]),
300
+ severity: missing.length > 2 ? 'high' : missing.length > 0 ? 'medium' : 'pass',
301
+ message: missing.length === 0 ? 'All expected scripts present' :
302
+ `Missing scripts: ${missing.join(', ')} (${missing.length}/${EXPECTED.length})`
303
+ };
304
+ }
305
+
306
+ // ============================================================
307
+ // Extended Checks (run alongside agents)
308
+ // ============================================================
309
+
310
+ /**
311
+ * Count eslint-disable comments across the project.
312
+ */
313
+ function countEslintDisables() {
314
+ try {
315
+ const output = execFileSync('grep', [
316
+ '-r', 'eslint-disable',
317
+ '--include=*.ts', '--include=*.tsx', '--include=*.js', '--include=*.jsx',
318
+ '-c', '.'
319
+ ], { cwd: PATHS.root, encoding: 'utf-8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] }).toString();
320
+ const lines = output.trim().split('\n').filter(Boolean);
321
+ let total = 0;
322
+ const byFile = [];
323
+ for (const line of lines) {
324
+ const match = line.match(/:(\d+)$/);
325
+ if (match) {
326
+ const count = parseInt(match[1], 10);
327
+ total += count;
328
+ if (count > 3) byFile.push({ file: line.replace(/:(\d+)$/, ''), count });
329
+ }
330
+ }
331
+ return {
332
+ total,
333
+ byFile: byFile.sort((a, b) => b.count - a.count).slice(0, 10),
334
+ severity: total > 50 ? 'high' : total > 20 ? 'medium' : total > 0 ? 'low' : 'pass'
335
+ };
336
+ } catch (_err) {
337
+ return { total: 0, byFile: [], severity: 'pass' };
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Detect project framework from package.json.
343
+ */
344
+ function detectFramework() {
345
+ const pkgPath = path.join(PATHS.root, 'package.json');
346
+ const pkg = safeJsonParse(pkgPath, {});
347
+ const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
348
+
349
+ const frameworks = [];
350
+ if (allDeps['next']) frameworks.push({ name: 'nextjs', version: allDeps['next'] });
351
+ if (allDeps['react']) frameworks.push({ name: 'react', version: allDeps['react'] });
352
+ if (allDeps['vue']) frameworks.push({ name: 'vue', version: allDeps['vue'] });
353
+ if (allDeps['svelte'] || allDeps['@sveltejs/kit']) frameworks.push({ name: 'svelte', version: allDeps['svelte'] || allDeps['@sveltejs/kit'] });
354
+ if (allDeps['@angular/core']) frameworks.push({ name: 'angular', version: allDeps['@angular/core'] });
355
+ if (allDeps['@nestjs/core']) frameworks.push({ name: 'nestjs', version: allDeps['@nestjs/core'] });
356
+ if (allDeps['express']) frameworks.push({ name: 'express', version: allDeps['express'] });
357
+ if (allDeps['fastify']) frameworks.push({ name: 'fastify', version: allDeps['fastify'] });
358
+ if (allDeps['hono']) frameworks.push({ name: 'hono', version: allDeps['hono'] });
359
+
360
+ const hasTypescript = !!allDeps['typescript'];
361
+ const hasMonorepo = fs.existsSync(path.join(PATHS.root, 'packages')) ||
362
+ fs.existsSync(path.join(PATHS.root, 'apps')) ||
363
+ (pkg.workspaces && pkg.workspaces.length > 0);
364
+
365
+ return { frameworks, hasTypescript, hasMonorepo, language: hasTypescript ? 'typescript' : 'javascript' };
366
+ }
367
+
368
+ /**
369
+ * Check git health indicators.
370
+ */
371
+ function checkGitHealth() {
372
+ const result = {
373
+ commitFrequency: 'unknown',
374
+ staleBranches: 0,
375
+ uncommittedFiles: 0,
376
+ conventionalCommits: false,
377
+ recentCommits: 0
378
+ };
379
+
380
+ try {
381
+ // Commits in last 30 days
382
+ const recent = execSync('git log --oneline --since="30 days ago" 2>/dev/null | wc -l', {
383
+ cwd: PATHS.root, encoding: 'utf-8', timeout: 5000
384
+ }).trim();
385
+ result.recentCommits = parseInt(recent, 10) || 0;
386
+ result.commitFrequency = result.recentCommits > 60 ? 'high' :
387
+ result.recentCommits > 20 ? 'medium' :
388
+ result.recentCommits > 0 ? 'low' : 'inactive';
389
+
390
+ // Stale branches (>30 days, not merged)
391
+ try {
392
+ const branches = execSync('git branch -r --no-merged 2>/dev/null | wc -l', {
393
+ cwd: PATHS.root, encoding: 'utf-8', timeout: 5000
394
+ }).trim();
395
+ result.staleBranches = parseInt(branches, 10) || 0;
396
+ } catch (_err) { /* non-critical */ }
397
+
398
+ // Uncommitted changes
399
+ try {
400
+ const status = execSync('git status --porcelain 2>/dev/null | wc -l', {
401
+ cwd: PATHS.root, encoding: 'utf-8', timeout: 5000
402
+ }).trim();
403
+ result.uncommittedFiles = parseInt(status, 10) || 0;
404
+ } catch (_err) { /* non-critical */ }
405
+
406
+ // Check if last 10 commits use conventional format
407
+ try {
408
+ const msgs = execSync('git log --oneline -10 2>/dev/null', {
409
+ cwd: PATHS.root, encoding: 'utf-8', timeout: 5000
410
+ }).trim();
411
+ const lines = msgs.split('\n').filter(Boolean);
412
+ const conventional = lines.filter(l => /^\w+ (feat|fix|docs|style|refactor|perf|test|chore|ci|build)\(?/.test(l));
413
+ result.conventionalCommits = conventional.length >= lines.length * 0.5;
414
+ } catch (_err) { /* non-critical */ }
415
+ } catch (_err) {
416
+ // Git not available
417
+ }
418
+
419
+ return result;
420
+ }
421
+
422
+ /**
423
+ * Check environment/config hygiene.
424
+ */
425
+ function checkEnvHygiene() {
426
+ const checks = [];
427
+
428
+ // .env.example exists?
429
+ const hasEnvExample = fs.existsSync(path.join(PATHS.root, '.env.example'));
430
+ const hasEnv = fs.existsSync(path.join(PATHS.root, '.env'));
431
+ const hasGitignore = fs.existsSync(path.join(PATHS.root, '.gitignore'));
432
+
433
+ checks.push({
434
+ check: '.env.example',
435
+ status: hasEnvExample ? 'pass' : (hasEnv ? 'fail' : 'na'),
436
+ message: hasEnvExample ? '.env.example exists' :
437
+ hasEnv ? '.env exists but no .env.example — other devs can\'t set up' : 'No .env files'
438
+ });
439
+
440
+ // .env in .gitignore?
441
+ if (hasGitignore && hasEnv) {
442
+ let gitignore = '';
443
+ try {
444
+ gitignore = fs.readFileSync(path.join(PATHS.root, '.gitignore'), 'utf-8');
445
+ } catch (_err) {
446
+ // Unreadable — skip this check
447
+ }
448
+ const envIgnored = gitignore.split('\n').some(l => l.trim() === '.env' || l.trim() === '.env*');
449
+ checks.push({
450
+ check: '.env in .gitignore',
451
+ status: envIgnored ? 'pass' : 'fail',
452
+ message: envIgnored ? '.env is gitignored' : 'WARNING: .env is NOT in .gitignore — secrets may be committed'
453
+ });
454
+ }
455
+
456
+ // CI config exists?
457
+ const ciConfigs = [
458
+ '.github/workflows',
459
+ '.gitlab-ci.yml',
460
+ 'Jenkinsfile',
461
+ '.circleci/config.yml',
462
+ 'bitbucket-pipelines.yml'
463
+ ];
464
+ const hasCI = ciConfigs.some(c => fs.existsSync(path.join(PATHS.root, c)));
465
+ checks.push({
466
+ check: 'CI configuration',
467
+ status: hasCI ? 'pass' : 'fail',
468
+ message: hasCI ? 'CI pipeline configured' : 'No CI configuration found — no automated quality enforcement'
469
+ });
470
+
471
+ // Docker?
472
+ const hasDocker = fs.existsSync(path.join(PATHS.root, 'Dockerfile')) ||
473
+ fs.existsSync(path.join(PATHS.root, 'docker-compose.yml'));
474
+ if (hasDocker) {
475
+ checks.push({ check: 'Docker', status: 'info', message: 'Dockerfile/docker-compose found' });
476
+ }
477
+
478
+ return checks;
479
+ }
480
+
481
+ // ============================================================
482
+ // Score Capping
483
+ // ============================================================
484
+
485
+ /**
486
+ * Calculate the Gate 0 score cap from all gate results.
487
+ * The final audit score = min(gate0_cap, weighted_agent_score)
488
+ *
489
+ * @param {Array<Object>} gateResults — results from all gates
490
+ * @returns {{ scoreCap: number, grade: string, penalties: number, reasons: string[] }}
491
+ */
492
+ function calculateScoreCap(gateResults) {
493
+ let cap = 100;
494
+ let penalties = 0;
495
+ const reasons = [];
496
+
497
+ for (const gate of gateResults) {
498
+ if (gate.scoreCap !== undefined && gate.scoreCap < cap) {
499
+ cap = gate.scoreCap;
500
+ reasons.push(`${gate.gate}: ${gate.message}`);
501
+ }
502
+ if (gate.scorePenalty) {
503
+ penalties += gate.scorePenalty;
504
+ reasons.push(`${gate.gate}: -${gate.scorePenalty} pts (${gate.message})`);
505
+ }
506
+ }
507
+
508
+ const effectiveCap = Math.max(50, cap - penalties);
509
+
510
+ return {
511
+ scoreCap: effectiveCap,
512
+ grade: scoreToGrade(effectiveCap),
513
+ penalties,
514
+ reasons
515
+ };
516
+ }
517
+
518
+ // ============================================================
519
+ // Trend Comparison
520
+ // ============================================================
521
+
522
+ /**
523
+ * Compare current gate results with previous audit.
524
+ * @param {Object} currentResults — from runAllGates()
525
+ * @param {Object} [previousAudit] — from last-audit.json
526
+ * @returns {Array<Object>} deltas
527
+ */
528
+ function compareTrend(currentResults, previousAudit) {
529
+ if (!previousAudit || !previousAudit.gate0) return [];
530
+
531
+ const deltas = [];
532
+ const prev = previousAudit.gate0;
533
+
534
+ for (const gate of currentResults.gates) {
535
+ const prevGate = prev.gates?.find(g => g.gate === gate.gate);
536
+ if (!prevGate) continue;
537
+
538
+ if (gate.errorCount !== undefined && prevGate.errorCount !== undefined) {
539
+ const delta = gate.errorCount - prevGate.errorCount;
540
+ if (delta !== 0) {
541
+ deltas.push({
542
+ gate: gate.gate,
543
+ metric: 'errorCount',
544
+ previous: prevGate.errorCount,
545
+ current: gate.errorCount,
546
+ delta,
547
+ improved: delta < 0
548
+ });
549
+ }
550
+ }
551
+ }
552
+
553
+ return deltas;
554
+ }
555
+
556
+ // ============================================================
557
+ // Main: Run All Gates
558
+ // ============================================================
559
+
560
+ /**
561
+ * Run all Gate 0 checks and return consolidated results.
562
+ * @returns {Object} gate results with score cap
563
+ */
564
+ function runAllGates() {
565
+ const gates = [];
566
+
567
+ gates.push(checkBuild());
568
+ gates.push(checkTypecheck());
569
+ gates.push(checkLint());
570
+ gates.push(checkLintConfigIntegrity());
571
+ gates.push(checkTests());
572
+ gates.push(checkScriptCompleteness());
573
+
574
+ const cap = calculateScoreCap(gates);
575
+ const framework = detectFramework();
576
+
577
+ // Load previous audit for trend comparison
578
+ let trend = [];
579
+ const lastAuditPath = path.join(PATHS.state, 'last-audit.json');
580
+ const previousAudit = safeJsonParse(lastAuditPath, null);
581
+ if (previousAudit) {
582
+ trend = compareTrend({ gates }, previousAudit);
583
+ }
584
+
585
+ return {
586
+ gates,
587
+ cap,
588
+ framework,
589
+ eslintDisables: countEslintDisables(),
590
+ gitHealth: checkGitHealth(),
591
+ envHygiene: checkEnvHygiene(),
592
+ trend,
593
+ timestamp: new Date().toISOString()
594
+ };
595
+ }
596
+
597
+ // ============================================================
598
+ // CLI
599
+ // ============================================================
600
+
601
+ function main() {
602
+ const command = process.argv[2] || 'run';
603
+
604
+ switch (command) {
605
+ case 'run': {
606
+ const results = runAllGates();
607
+ console.log(JSON.stringify(results, null, 2));
608
+ break;
609
+ }
610
+
611
+ case 'build':
612
+ console.log(JSON.stringify(checkBuild(), null, 2));
613
+ break;
614
+
615
+ case 'typecheck':
616
+ console.log(JSON.stringify(checkTypecheck(), null, 2));
617
+ break;
618
+
619
+ case 'lint':
620
+ console.log(JSON.stringify(checkLint(), null, 2));
621
+ break;
622
+
623
+ case 'lint-config':
624
+ console.log(JSON.stringify(checkLintConfigIntegrity(), null, 2));
625
+ break;
626
+
627
+ case 'tests':
628
+ console.log(JSON.stringify(checkTests(), null, 2));
629
+ break;
630
+
631
+ case 'scripts':
632
+ console.log(JSON.stringify(checkScriptCompleteness(), null, 2));
633
+ break;
634
+
635
+ case 'eslint-disable':
636
+ console.log(JSON.stringify(countEslintDisables(), null, 2));
637
+ break;
638
+
639
+ case 'framework':
640
+ console.log(JSON.stringify(detectFramework(), null, 2));
641
+ break;
642
+
643
+ case 'git-health':
644
+ console.log(JSON.stringify(checkGitHealth(), null, 2));
645
+ break;
646
+
647
+ case 'env-hygiene':
648
+ console.log(JSON.stringify(checkEnvHygiene(), null, 2));
649
+ break;
650
+
651
+ case 'dead-exports': {
652
+ // Delegated to the AI agent — this is a hint for the agent prompt
653
+ console.log('Dead export detection requires AI analysis. Use the audit agent prompt.');
654
+ break;
655
+ }
656
+
657
+ case 'dep-health': {
658
+ // Uses existing flow-audit.js outdated + audit
659
+ try {
660
+ const { getOutdatedDeps, getAuditResults } = require('./flow-audit');
661
+ console.log(JSON.stringify({
662
+ outdated: getOutdatedDeps(),
663
+ vulnerabilities: getAuditResults()
664
+ }, null, 2));
665
+ } catch (err) {
666
+ console.log(JSON.stringify({ error: err.message }, null, 2));
667
+ }
668
+ break;
669
+ }
670
+
671
+ case 'test-coverage': {
672
+ // Try to run coverage command
673
+ const pkgPath = path.join(PATHS.root, 'package.json');
674
+ const pkg = safeJsonParse(pkgPath, {});
675
+ const scripts = pkg.scripts || {};
676
+ const scriptName = scripts['test:coverage'] ? 'test:coverage' : (scripts['coverage'] ? 'coverage' : null);
677
+ if (scriptName) {
678
+ try {
679
+ const output = execFileSync('npm', ['run', scriptName], {
680
+ cwd: PATHS.root, encoding: 'utf-8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe']
681
+ });
682
+ console.log(JSON.stringify({ available: true, output: output.substring(0, 3000) }, null, 2));
683
+ } catch (err) {
684
+ console.log(JSON.stringify({ available: true, error: (err.stdout || err.stderr || '').substring(0, 2000) }, null, 2));
685
+ }
686
+ } else {
687
+ // Check test file ratio
688
+ try {
689
+ const testFiles = execFileSync('sh', ['-c', 'find . -name "*.test.*" -o -name "*.spec.*" 2>/dev/null | grep -v node_modules | wc -l'], {
690
+ cwd: PATHS.root, encoding: 'utf-8', timeout: 5000
691
+ }).trim();
692
+ const srcFiles = execFileSync('sh', ['-c', 'find . \\( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \\) 2>/dev/null | grep -v node_modules | grep -vE "\\.(test|spec)\\." | wc -l'], {
693
+ cwd: PATHS.root, encoding: 'utf-8', timeout: 5000
694
+ }).trim();
695
+ console.log(JSON.stringify({
696
+ available: false,
697
+ testFiles: parseInt(testFiles, 10) || 0,
698
+ sourceFiles: parseInt(srcFiles, 10) || 0,
699
+ ratio: parseInt(srcFiles, 10) > 0 ? ((parseInt(testFiles, 10) / parseInt(srcFiles, 10)) * 100).toFixed(1) + '%' : '0%'
700
+ }, null, 2));
701
+ } catch (_err) {
702
+ console.log(JSON.stringify({ available: false, testFiles: 0, sourceFiles: 0, ratio: '0%' }, null, 2));
703
+ }
704
+ }
705
+ break;
706
+ }
707
+
708
+ default:
709
+ console.log(`
710
+ Wogi Flow - Audit Gate 0: Pre-Agent Baseline Checks
711
+
712
+ Usage: flow-audit-gates.js <command>
713
+
714
+ Commands:
715
+ run Run all Gate 0 checks (default)
716
+ build Build check only
717
+ typecheck Typecheck only
718
+ lint Lint check only
719
+ lint-config Lint config integrity (downgraded rules)
720
+ tests Test check only
721
+ scripts Package.json script completeness
722
+ eslint-disable Count eslint-disable comments
723
+ framework Auto-detect project framework
724
+ git-health Git history health indicators
725
+ env-hygiene Environment/config hygiene
726
+ dep-health Dependency health (outdated + vulnerabilities)
727
+ test-coverage Test coverage metrics
728
+ dead-exports Dead export detection (requires AI agent)
729
+ `);
730
+ }
731
+ }
732
+
733
+ // ============================================================
734
+ // Exports
735
+ // ============================================================
736
+
737
+ module.exports = {
738
+ // Gate 0 checks
739
+ checkBuild,
740
+ checkTypecheck,
741
+ checkLint,
742
+ checkLintConfigIntegrity,
743
+ checkTests,
744
+ checkScriptCompleteness,
745
+
746
+ // Extended checks
747
+ countEslintDisables,
748
+ detectFramework,
749
+ checkGitHealth,
750
+ checkEnvHygiene,
751
+
752
+ // Score
753
+ calculateScoreCap,
754
+ scoreToGrade,
755
+ GRADE_VALUES,
756
+
757
+ // Trend
758
+ compareTrend,
759
+
760
+ // Main
761
+ runAllGates
762
+ };
763
+
764
+ if (require.main === module) {
765
+ main();
766
+ }