@nerviq/cli 1.8.8 → 1.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.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@nerviq/cli)](https://www.npmjs.com/package/@nerviq/cli)
6
6
  [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE)
7
- [![Checks: 2431](https://img.shields.io/badge/checks-2431-brightgreen)](https://github.com/nerviq/nerviq)
7
+ [![Checks: 2438](https://img.shields.io/badge/checks-2438-brightgreen)](https://github.com/nerviq/nerviq)
8
8
 
9
9
  ---
10
10
 
@@ -14,7 +14,7 @@ Nerviq audits, sets up, and governs AI coding agent configurations for **8 platf
14
14
 
15
15
  | Platform | Checks | Status |
16
16
  |----------|--------|--------|
17
- | Claude Code | 393 | Full |
17
+ | Claude Code | 400 | Full |
18
18
  | Codex (OpenAI) | 272 | Full |
19
19
  | Gemini CLI (Google) | 300 | Full |
20
20
  | GitHub Copilot | 299 | Full |
@@ -85,7 +85,7 @@ No install required. Zero dependencies.
85
85
  | **Team lead / DevEx** | `nerviq governance` → `nerviq audit --json` | CI threshold + `nerviq watch` |
86
86
  | **Enterprise / Platform** | `nerviq harmony-audit` → `nerviq harmony-drift` | Policy packs + `nerviq certify` |
87
87
 
88
- ## 2,431 Checks Across 96 Categories (8 Platforms × ~300 Governance Rules)
88
+ ## 2,438 Checks Across 96 Categories (8 Platforms × ~300 Governance Rules)
89
89
 
90
90
  | Category Group | Checks | Examples |
91
91
  |----------------|--------|---------|
@@ -290,7 +290,7 @@ Nerviq is built on the NERVIQ knowledge engine — the largest verified catalog
290
290
 
291
291
  - **448+ research documents** covering all 8 platforms
292
292
  - **332+ experiments** with tested, rated results
293
- - **2,431 checks** across 8 platforms (~300 unique governance rules × 8 platform adaptations), each with `sourceUrl` and `confidence` level (0.0-1.0)
293
+ - **2,438 checks** across 8 platforms (~300 unique governance rules × 8 platform adaptations), each with `sourceUrl` and `confidence` level (0.0-1.0)
294
294
  - Every check is traceable to primary documentation or verified experiment
295
295
  - 90-day freshness cycle: stale findings are re-verified or pruned
296
296
 
@@ -347,3 +347,16 @@ If Nerviq helped you, consider giving it a ⭐ on [GitHub](https://github.com/ne
347
347
  | `BETA` | Works but has limited real-world testing. API may change |
348
348
  | `EXPERIMENTAL` | Early stage, static rules, results may vary |
349
349
 
350
+ ## Previously nerviq-cli
351
+
352
+ Nerviq was previously published as `nerviq-cli`. If you were using it:
353
+
354
+ ```bash
355
+ # Old
356
+ npx nerviq-cli
357
+
358
+ # New
359
+ npx @nerviq/cli audit
360
+ ```
361
+
362
+ All features are preserved and expanded.
package/bin/cli.js CHANGED
@@ -14,6 +14,7 @@ const { auditWorkspaces } = require('../src/workspace');
14
14
  const { scanOrg } = require('../src/org');
15
15
  const { detectAntiPatterns, printAntiPatterns, printAntiPatternCatalog } = require('../src/anti-patterns');
16
16
  const { VERIFICATION_DATES, getVerificationDate, getVerificationStats } = require('../src/verification-metadata');
17
+ const { init: initI18n, t } = require('../src/i18n');
17
18
  const { version } = require('../package.json');
18
19
 
19
20
  const args = process.argv.slice(2);
@@ -91,11 +92,12 @@ function parseArgs(rawArgs) {
91
92
  let external = null;
92
93
  let repos = [];
93
94
  let teamProfile = null;
95
+ let lang = null;
94
96
 
95
97
  for (let i = 0; i < rawArgs.length; i++) {
96
98
  const arg = rawArgs[i];
97
99
 
98
- if (arg === '--threshold' || arg === '--out' || arg === '--plan' || arg === '--only' || arg === '--profile' || arg === '--mcp-pack' || arg === '--require' || arg === '--key' || arg === '--status' || arg === '--effect' || arg === '--notes' || arg === '--source' || arg === '--score-delta' || arg === '--platform' || arg === '--format' || arg === '--from' || arg === '--to' || arg === '--port' || arg === '--workspace' || arg === '--check-version' || arg === '--webhook' || arg === '--external' || arg === '--team-profile') {
100
+ if (arg === '--threshold' || arg === '--out' || arg === '--plan' || arg === '--only' || arg === '--profile' || arg === '--mcp-pack' || arg === '--require' || arg === '--key' || arg === '--status' || arg === '--effect' || arg === '--notes' || arg === '--source' || arg === '--score-delta' || arg === '--platform' || arg === '--format' || arg === '--from' || arg === '--to' || arg === '--port' || arg === '--workspace' || arg === '--check-version' || arg === '--webhook' || arg === '--external' || arg === '--team-profile' || arg === '--lang') {
99
101
  const value = rawArgs[i + 1];
100
102
  if (!value || value.startsWith('--')) {
101
103
  throw new Error(`${arg} requires a value`);
@@ -123,10 +125,16 @@ function parseArgs(rawArgs) {
123
125
  if (arg === '--webhook') webhookUrl = value.trim();
124
126
  if (arg === '--external') external = value.trim();
125
127
  if (arg === '--team-profile') teamProfile = value.trim();
128
+ if (arg === '--lang') lang = value.trim().toLowerCase();
126
129
  i++;
127
130
  continue;
128
131
  }
129
132
 
133
+ if (arg.startsWith('--lang=')) {
134
+ lang = arg.split('=').slice(1).join('=').trim().toLowerCase();
135
+ continue;
136
+ }
137
+
130
138
  if (arg.startsWith('--team-profile=')) {
131
139
  teamProfile = arg.split('=').slice(1).join('=').trim();
132
140
  continue;
@@ -258,7 +266,7 @@ function parseArgs(rawArgs) {
258
266
 
259
267
  const normalizedCommand = COMMAND_ALIASES[command] || command;
260
268
 
261
- return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, format, port, workspace, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo, checkVersion, webhookUrl, external, repos, teamProfile };
269
+ return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, format, port, workspace, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo, checkVersion, webhookUrl, external, repos, teamProfile, lang };
262
270
  }
263
271
 
264
272
  function printWorkspaceSummary(summary, options) {
@@ -308,6 +316,7 @@ function printScanDetail(summary, options) {
308
316
 
309
317
  // Show per-category breakdown if result is available
310
318
  if (item.result && item.result.results) {
319
+ const STACK_LANGUAGES = new Set(['python', 'go', 'rust', 'java', 'ruby', 'dotnet', 'php', 'flutter', 'swift', 'kotlin']);
311
320
  const categories = {};
312
321
  for (const r of item.result.results) {
313
322
  const cat = r.category || 'other';
@@ -315,7 +324,9 @@ function printScanDetail(summary, options) {
315
324
  categories[cat].total++;
316
325
  if (r.passed) categories[cat].passed++;
317
326
  }
318
- const catEntries = Object.entries(categories).sort((a, b) => (a[1].passed / a[1].total) - (b[1].passed / b[1].total));
327
+ const catEntries = Object.entries(categories)
328
+ .filter(([cat, v]) => v.passed > 0 || !STACK_LANGUAGES.has(cat))
329
+ .sort((a, b) => (a[1].passed / a[1].total) - (b[1].passed / b[1].total));
319
330
  const catLine = catEntries.map(([cat, v]) => `${cat}: ${v.passed}/${v.total}`).join(' ');
320
331
  console.log(` \x1b[2m${catLine}\x1b[0m`);
321
332
  }
@@ -509,6 +520,11 @@ async function main() {
509
520
 
510
521
  const { flags, command, normalizedCommand } = parsed;
511
522
 
523
+ // Initialize i18n with --lang flag or NERVIQ_LANG env var
524
+ if (parsed.lang) {
525
+ initI18n(parsed.lang);
526
+ }
527
+
512
528
  if (flags.includes('--help') || command === 'help') {
513
529
  console.log(HELP);
514
530
  process.exit(0);
@@ -544,6 +560,7 @@ async function main() {
544
560
  port: parsed.port !== null ? Number(parsed.port) : null,
545
561
  workspace: parsed.workspace || null,
546
562
  webhookUrl: parsed.webhookUrl || null,
563
+ lang: parsed.lang || null,
547
564
  external: parsed.external || null,
548
565
  dir: process.cwd()
549
566
  };
@@ -1402,7 +1419,21 @@ async function main() {
1402
1419
  console.error('\n Error: Profile name required. Usage: nerviq profile load <name>\n');
1403
1420
  process.exit(1);
1404
1421
  }
1405
- const profile = loadProfile(options.dir, profileArg);
1422
+ let profile;
1423
+ try {
1424
+ profile = loadProfile(options.dir, profileArg);
1425
+ } catch {
1426
+ // Not found as a user-saved profile — try built-in governance profiles
1427
+ const { getPermissionProfile } = require('../src/governance');
1428
+ const builtIn = getPermissionProfile(profileArg);
1429
+ if (builtIn && builtIn.key === profileArg) {
1430
+ profile = { name: builtIn.label, platforms: ['claude'], threshold: builtIn.threshold || 0, ...builtIn };
1431
+ }
1432
+ }
1433
+ if (!profile) {
1434
+ console.error(`\n Error: Profile '${profileArg}' not found. Run 'nerviq profile list' to see available profiles.\n`);
1435
+ process.exit(1);
1436
+ }
1406
1437
 
1407
1438
  // Apply profile settings to .claude/settings.json
1408
1439
  const fs = require('fs');
@@ -1458,8 +1489,35 @@ async function main() {
1458
1489
  process.exit(1);
1459
1490
  }
1460
1491
  } else if (normalizedCommand === 'synergy-report') {
1461
- // Placeholder synergy report is referenced but may not be implemented yet
1462
- console.log('\n Synergy report: coming soon.\n');
1492
+ const { formatSynergyReport } = require('../src/synergy/report');
1493
+ const { detectActivePlatforms: detectSynergyPlatforms } = require('../src/harmony/canon');
1494
+ const presentPlatforms = detectSynergyPlatforms(options.dir).map(p => p.platform);
1495
+ if (presentPlatforms.length === 0) {
1496
+ console.log('\n No platform configurations detected.');
1497
+ console.log(' Run "nerviq harmony-audit" first, or "nerviq setup" to bootstrap a platform.\n');
1498
+ process.exit(0);
1499
+ }
1500
+ const platformAudits = {};
1501
+ const activePlatforms = [];
1502
+ for (const plat of presentPlatforms) {
1503
+ try {
1504
+ const result = await audit({ dir: options.dir, silent: true, platform: plat });
1505
+ if (result && typeof result.score === 'number') {
1506
+ platformAudits[plat] = result;
1507
+ activePlatforms.push(plat);
1508
+ }
1509
+ } catch (_e) { /* platform not available */ }
1510
+ }
1511
+ if (activePlatforms.length === 0) {
1512
+ console.log('\n No auditable platforms found. Run "nerviq harmony-audit" first.\n');
1513
+ process.exit(0);
1514
+ }
1515
+ const report = formatSynergyReport({ platformAudits, activePlatforms });
1516
+ if (options.json) {
1517
+ console.log(JSON.stringify({ activePlatforms, platformAudits }, null, 2));
1518
+ } else {
1519
+ console.log(report);
1520
+ }
1463
1521
  process.exit(0);
1464
1522
  } else if (normalizedCommand === 'doctor') {
1465
1523
  const { runDoctor } = require('../src/doctor');
@@ -1888,6 +1946,16 @@ async function main() {
1888
1946
  process.exit(0);
1889
1947
  }
1890
1948
  const result = await audit(options);
1949
+ if (options.out) {
1950
+ const fs = require('fs');
1951
+ const path = require('path');
1952
+ const outPath = path.resolve(options.out);
1953
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
1954
+ fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf8');
1955
+ if (!options.json) {
1956
+ console.log(`\n Audit report written to ${options.out}\n`);
1957
+ }
1958
+ }
1891
1959
  if (options.webhookUrl) {
1892
1960
  try {
1893
1961
  const { sendWebhook, formatSlackMessage } = require('../src/integrations');
package/package.json CHANGED
@@ -1,59 +1,59 @@
1
- {
2
- "name": "@nerviq/cli",
3
- "version": "1.8.8",
4
- "description": "The intelligent nervous system for AI coding agents — 2,431 checks (8 platforms × ~300 governance rules), 10 languages, 62 domain packs. Audit, align, and amplify.",
5
- "main": "src/index.js",
6
- "bin": {
7
- "nerviq": "bin/cli.js",
8
- "@nerviq/cli": "bin/cli.js",
9
- "nerviq-mcp": "src/mcp-server.js"
10
- },
11
- "files": [
12
- "bin",
13
- "src",
14
- "README.md"
15
- ],
16
- "scripts": {
17
- "start": "node bin/cli.js",
18
- "build": "npm pack --dry-run",
19
- "test": "node test/run.js",
20
- "test:jest": "jest",
21
- "test:coverage": "jest --coverage",
22
- "test:all": "npm test && npx jest && node test/check-matrix.js && node test/codex-check-matrix.js && node test/gemini-check-matrix.js && node test/copilot-check-matrix.js && node test/cursor-check-matrix.js && node test/windsurf-check-matrix.js && node test/aider-check-matrix.js && node test/opencode-check-matrix.js && node test/golden-matrix.js && node test/codex-golden-matrix.js && node test/gemini-golden-matrix.js && node test/copilot-golden-matrix.js && node test/cursor-golden-matrix.js && node test/windsurf-golden-matrix.js && node test/aider-golden-matrix.js && node test/opencode-golden-matrix.js",
23
- "benchmark:perf": "node tools/benchmark.js",
24
- "catalog": "node -e \"const {generateCatalog}=require('./src/catalog');console.log(JSON.stringify(generateCatalog(),null,2))\""
25
- },
26
- "keywords": [
27
- "nerviq",
28
- "ai-agents",
29
- "agent-governance",
30
- "agent-config",
31
- "harmony",
32
- "synergy",
33
- "audit",
34
- "claude",
35
- "codex",
36
- "gemini",
37
- "copilot",
38
- "cursor",
39
- "windsurf",
40
- "aider",
41
- "developer-tools",
42
- "cli",
43
- "mcp",
44
- "multi-agent"
45
- ],
46
- "author": "Nerviq <hello@nerviq.net>",
47
- "license": "AGPL-3.0",
48
- "repository": {
49
- "type": "git",
50
- "url": "git+https://github.com/nerviq/nerviq.git"
51
- },
52
- "homepage": "https://nerviq.net",
53
- "engines": {
54
- "node": ">=18.0.0"
55
- },
56
- "devDependencies": {
57
- "jest": "^30.3.0"
58
- }
59
- }
1
+ {
2
+ "name": "@nerviq/cli",
3
+ "version": "1.9.0",
4
+ "description": "The intelligent nervous system for AI coding agents — 2,438 checks (8 platforms × ~300 governance rules), 10 languages, 62 domain packs. Audit, align, and amplify.",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "nerviq": "bin/cli.js",
8
+ "@nerviq/cli": "bin/cli.js",
9
+ "nerviq-mcp": "src/mcp-server.js"
10
+ },
11
+ "files": [
12
+ "bin",
13
+ "src",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "start": "node bin/cli.js",
18
+ "build": "npm pack --dry-run",
19
+ "test": "node test/run.js",
20
+ "test:jest": "jest",
21
+ "test:coverage": "jest --coverage",
22
+ "test:all": "npm test && npx jest && node test/check-matrix.js && node test/codex-check-matrix.js && node test/gemini-check-matrix.js && node test/copilot-check-matrix.js && node test/cursor-check-matrix.js && node test/windsurf-check-matrix.js && node test/aider-check-matrix.js && node test/opencode-check-matrix.js && node test/golden-matrix.js && node test/codex-golden-matrix.js && node test/gemini-golden-matrix.js && node test/copilot-golden-matrix.js && node test/cursor-golden-matrix.js && node test/windsurf-golden-matrix.js && node test/aider-golden-matrix.js && node test/opencode-golden-matrix.js",
23
+ "benchmark:perf": "node tools/benchmark.js",
24
+ "catalog": "node -e \"const {generateCatalog}=require('./src/catalog');console.log(JSON.stringify(generateCatalog(),null,2))\""
25
+ },
26
+ "keywords": [
27
+ "nerviq",
28
+ "ai-agents",
29
+ "agent-governance",
30
+ "agent-config",
31
+ "harmony",
32
+ "synergy",
33
+ "audit",
34
+ "claude",
35
+ "codex",
36
+ "gemini",
37
+ "copilot",
38
+ "cursor",
39
+ "windsurf",
40
+ "aider",
41
+ "developer-tools",
42
+ "cli",
43
+ "mcp",
44
+ "multi-agent"
45
+ ],
46
+ "author": "Nerviq <hello@nerviq.net>",
47
+ "license": "AGPL-3.0",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/nerviq/nerviq.git"
51
+ },
52
+ "homepage": "https://nerviq.net",
53
+ "engines": {
54
+ "node": ">=18.0.0"
55
+ },
56
+ "devDependencies": {
57
+ "jest": "^30.3.0"
58
+ }
59
+ }
package/src/audit.js CHANGED
@@ -33,6 +33,7 @@ const { loadPlugins, mergePluginChecks } = require('./plugins');
33
33
  const { hasWorkspaceConfig, detectWorkspaceGlobs, detectWorkspaces } = require('./workspace');
34
34
  const { detectDeprecationWarnings } = require('./deprecation');
35
35
  const { version: packageVersion } = require('../package.json');
36
+ const { t } = require('./i18n');
36
37
 
37
38
  const COLORS = {
38
39
  reset: '\x1b[0m',
@@ -872,27 +873,27 @@ function getCodexDomainPackSignals(ctx) {
872
873
 
873
874
  function printLiteAudit(result, dir) {
874
875
  console.log('');
875
- const productLabel = result.platform === 'codex' ? 'nerviq codex quick scan' : 'nerviq quick scan';
876
+ const productLabel = result.platform === 'codex' ? t('audit.codexQuickScan') : t('audit.quickScan');
876
877
  console.log(colorize(` ${productLabel}`, 'bold'));
877
878
  console.log(colorize(' ═══════════════════════════════════════', 'dim'));
878
- console.log(colorize(` Scanning: ${dir}`, 'dim'));
879
+ console.log(colorize(` ${t('audit.scanning', { dir })}`, 'dim'));
879
880
  console.log('');
880
881
  if (result.detectedConfigFiles && result.detectedConfigFiles.length > 0) {
881
882
  console.log(colorize(` Found: ${result.detectedConfigFiles.join(', ')}`, 'dim'));
882
883
  }
883
884
  console.log('');
884
- console.log(` Score: ${colorize(`${result.score}/100`, 'bold')} (${result.passed}/${result.passed + result.failed} checks passing)`);
885
+ console.log(` ${t('audit.score', { score: colorize(`${result.score}/100`, 'bold'), passed: result.passed, total: result.passed + result.failed })}`);
885
886
 
886
887
  // Score explanation line (lite mode only)
887
888
  const _critCount = (result.results || []).filter(r => r.passed === false && r.impact === 'critical').length;
888
889
  const _highCount = (result.results || []).filter(r => r.passed === false && r.impact === 'high').length;
889
890
  let scoreExplanation;
890
891
  if (result.score >= 90) {
891
- scoreExplanation = 'Excellent setup — production-ready governance';
892
+ scoreExplanation = t('audit.excellent');
892
893
  } else if (result.score >= 70) {
893
- scoreExplanation = `Strong setup ${_critCount} critical items to address`;
894
+ scoreExplanation = t('audit.strong', { count: _critCount });
894
895
  } else if (result.score >= 50) {
895
- scoreExplanation = `Good foundation ${_critCount + _highCount} items need attention`;
896
+ scoreExplanation = t('audit.good', { count: _critCount + _highCount });
896
897
  } else if (result.score >= 30) {
897
898
  // Find weakest category (most failures)
898
899
  const catFailures = {};
@@ -901,9 +902,9 @@ function printLiteAudit(result, dir) {
901
902
  catFailures[cat] = (catFailures[cat] || 0) + 1;
902
903
  });
903
904
  const weakestCategory = Object.keys(catFailures).sort((a, b) => catFailures[b] - catFailures[a])[0] || 'config';
904
- scoreExplanation = `Basic setup significant gaps in ${weakestCategory}`;
905
+ scoreExplanation = t('audit.basic', { category: weakestCategory });
905
906
  } else {
906
- scoreExplanation = 'Early stage — run `nerviq setup` to bootstrap your config';
907
+ scoreExplanation = t('audit.early');
907
908
  }
908
909
  console.log(colorize(` ${scoreExplanation}`, 'dim'));
909
910
 
@@ -1233,10 +1234,10 @@ async function audit(options) {
1233
1234
 
1234
1235
  // Display results
1235
1236
  console.log('');
1236
- const auditTitle = spec.platform === 'codex' ? 'nerviq codex audit' : 'nerviq audit';
1237
+ const auditTitle = spec.platform === 'codex' ? t('audit.codexTitle') : t('audit.title');
1237
1238
  console.log(colorize(` ${auditTitle}`, 'bold'));
1238
1239
  console.log(colorize(' ═══════════════════════════════════════', 'dim'));
1239
- console.log(colorize(` Scanning: ${options.dir}`, 'dim'));
1240
+ console.log(colorize(` ${t('audit.scanning', { dir: options.dir })}`, 'dim'));
1240
1241
  if (spec.platformVersion) {
1241
1242
  console.log(colorize(` Platform: ${spec.platformLabel} (${spec.platformVersion})`, 'blue'));
1242
1243
  }
package/src/benchmark.js CHANGED
@@ -321,9 +321,13 @@ function printBenchmark(report, options = {}) {
321
321
  console.log(' ═══════════════════════════════════════');
322
322
  console.log(' Runs in an isolated temp copy. Your current repo is not modified.');
323
323
  console.log('');
324
- console.log(` Before: ${report.before.score}/100 (organic ${report.before.organicScore}/100)`);
325
- console.log(` After: ${report.after.score}/100 (organic ${report.after.organicScore}/100)`);
326
- console.log(` Delta: score ${report.delta.score >= 0 ? '+' : ''}${report.delta.score}, organic ${report.delta.organicScore >= 0 ? '+' : ''}${report.delta.organicScore}`);
324
+ const orgDeltaSign = report.delta.organicScore >= 0 ? '+' : '';
325
+ const totalDeltaSign = report.delta.score >= 0 ? '+' : '';
326
+ console.log(` Organic improvement: \x1b[1m${orgDeltaSign}${report.delta.organicScore} points\x1b[0m (your actual config quality)`);
327
+ console.log(` Total with nerviq setup: ${totalDeltaSign}${report.delta.score} points`);
328
+ console.log('');
329
+ console.log(` Before: organic ${report.before.organicScore}/100, total ${report.before.score}/100`);
330
+ console.log(` After: organic ${report.after.organicScore}/100, total ${report.after.score}/100`);
327
331
  console.log('');
328
332
  console.log(` ${report.executiveSummary.headline}`);
329
333
  console.log(` Recommendation: ${report.executiveSummary.decisionGuidance}`);
@@ -37,10 +37,14 @@ async function certifyProject(dir) {
37
37
 
38
38
  // Run per-platform audits
39
39
  const platformScores = {};
40
+ const allAuditResults = [];
40
41
  for (const platform of platforms) {
41
42
  try {
42
43
  const result = await audit({ dir: resolvedDir, platform, silent: true });
43
44
  platformScores[platform] = result.score;
45
+ if (Array.isArray(result.results)) {
46
+ allAuditResults.push(...result.results);
47
+ }
44
48
  } catch {
45
49
  platformScores[platform] = 0;
46
50
  }
@@ -55,18 +59,36 @@ async function certifyProject(dir) {
55
59
  harmonyScore = 0;
56
60
  }
57
61
 
58
- // Determine certification level
62
+ // Determine certification level with security gates
59
63
  const scores = Object.values(platformScores);
60
64
  const allAbove70 = scores.length > 0 && scores.every(s => s >= 70);
61
65
  const allAbove50 = scores.length > 0 && scores.every(s => s >= 50);
62
66
  const anyAbove40 = scores.some(s => s >= 40);
63
67
 
68
+ // Security gate helpers — check whether specific audit checks passed
69
+ const checkPassed = (key) => {
70
+ const match = allAuditResults.find(r => r.key === key);
71
+ return match ? match.passed === true : false;
72
+ };
73
+
74
+ const gitIgnoreOk = checkPassed('gitIgnoreEnv');
75
+ const secretsOk = checkPassed('secretsProtection');
76
+ const criticalAntiPatterns = allAuditResults.filter(
77
+ r => r.passed === false && r.impact === 'critical'
78
+ );
79
+ const noCriticalAntiPatterns = criticalAntiPatterns.length === 0;
80
+
81
+ // Bronze gate: score >= 40 AND basic security (gitignore + secrets protection)
82
+ const bronzeSecurityGate = gitIgnoreOk && secretsOk;
83
+ // Silver gate: Bronze requirements AND no critical anti-patterns
84
+ const silverSecurityGate = bronzeSecurityGate && noCriticalAntiPatterns;
85
+
64
86
  let level;
65
- if (harmonyScore >= 80 && allAbove70) {
87
+ if (harmonyScore >= 80 && allAbove70 && silverSecurityGate) {
66
88
  level = LEVELS.GOLD;
67
- } else if (harmonyScore >= 60 && allAbove50) {
89
+ } else if (harmonyScore >= 60 && allAbove50 && silverSecurityGate) {
68
90
  level = LEVELS.SILVER;
69
- } else if (anyAbove40) {
91
+ } else if (anyAbove40 && bronzeSecurityGate) {
70
92
  level = LEVELS.BRONZE;
71
93
  } else {
72
94
  level = LEVELS.NONE;
@@ -80,6 +102,12 @@ async function certifyProject(dir) {
80
102
  platformScores,
81
103
  platforms,
82
104
  badge,
105
+ securityGates: {
106
+ gitIgnoreEnv: gitIgnoreOk,
107
+ secretsProtection: secretsOk,
108
+ noCriticalAntiPatterns,
109
+ criticalAntiPatternCount: criticalAntiPatterns.length,
110
+ },
83
111
  };
84
112
  }
85
113