@nerviq/cli 1.13.0 → 1.14.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "The intelligent nervous system for AI coding agents — 2,441 checks (8 platforms × ~300 governance rules), 10 languages, 62 domain packs. Audit, align, and amplify.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/audit.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Audit engine - evaluates project against NERVIQ technique database.
3
3
  */
4
4
 
5
- const { TECHNIQUES: CLAUDE_TECHNIQUES, STACKS, STACK_CATEGORY_DETECTORS } = require('./techniques');
5
+ const { TECHNIQUES: CLAUDE_TECHNIQUES, STACKS, STACK_CATEGORY_DETECTORS } = require('./techniques');
6
6
  const { ProjectContext } = require('./context');
7
7
  const { CODEX_TECHNIQUES } = require('./codex/techniques');
8
8
  const { detectCodexDomainPacks } = require('./codex/domain-packs');
@@ -23,28 +23,28 @@ const { AiderProjectContext } = require('./aider/context');
23
23
  const { OPENCODE_TECHNIQUES } = require('./opencode/techniques');
24
24
  const { OpenCodeProjectContext } = require('./opencode/context');
25
25
  const { getBadgeMarkdown } = require('./badge');
26
- const { sendInsights, getLocalInsights } = require('./insights');
27
- const { getRecommendationOutcomeSummary } = require('./activity');
28
- const { getFeedbackSummary } = require('./feedback');
29
- const { formatSarif } = require('./formatters/sarif');
30
- const { formatOtelMetrics } = require('./formatters/otel');
31
- const { collectAuditTerminology, formatTerminologyLines } = require('./terminology');
32
- const { loadPlugins, mergePluginChecks } = require('./plugins');
33
- const { detectDeprecationWarnings } = require('./deprecation');
34
- const { buildWorkspaceHint, formatCount, guardSkippedInstructionFiles, inspectInstructionFiles } = require('./audit/instruction-files');
35
- const {
36
- WEIGHTS,
37
- buildScoreCoaching,
38
- buildTopNextActions,
39
- confidenceLabel,
40
- computeCategoryScores,
41
- getFpFeedbackMultiplier,
42
- getQuickWins,
43
- getRecommendationPriorityScore,
44
- inferSuggestedNextCommand,
45
- } = require('./audit/recommendations');
46
- const { version: packageVersion } = require('../package.json');
47
- const { t } = require('./i18n');
26
+ const { sendInsights, getLocalInsights } = require('./insights');
27
+ const { getRecommendationOutcomeSummary } = require('./activity');
28
+ const { getFeedbackSummary } = require('./feedback');
29
+ const { formatSarif } = require('./formatters/sarif');
30
+ const { formatOtelMetrics } = require('./formatters/otel');
31
+ const { collectAuditTerminology, formatTerminologyLines } = require('./terminology');
32
+ const { loadPlugins, mergePluginChecks } = require('./plugins');
33
+ const { detectDeprecationWarnings } = require('./deprecation');
34
+ const { buildWorkspaceHint, formatCount, guardSkippedInstructionFiles, inspectInstructionFiles } = require('./audit/instruction-files');
35
+ const {
36
+ WEIGHTS,
37
+ buildScoreCoaching,
38
+ buildTopNextActions,
39
+ confidenceLabel,
40
+ computeCategoryScores,
41
+ getFpFeedbackMultiplier,
42
+ getQuickWins,
43
+ getRecommendationPriorityScore,
44
+ inferSuggestedNextCommand,
45
+ } = require('./audit/recommendations');
46
+ const { version: packageVersion } = require('../package.json');
47
+ const { t } = require('./i18n');
48
48
 
49
49
  const COLORS = {
50
50
  reset: '\x1b[0m',
@@ -68,10 +68,10 @@ function progressBar(score, max = 100, width = 20) {
68
68
  return colorize('█'.repeat(filled), color) + colorize('░'.repeat(empty), 'dim');
69
69
  }
70
70
 
71
- function formatLocation(file, line) {
72
- if (!file) return null;
73
- return line ? `${file}:${line}` : file;
74
- }
71
+ function formatLocation(file, line) {
72
+ if (!file) return null;
73
+ return line ? `${file}:${line}` : file;
74
+ }
75
75
 
76
76
  function getAuditSpec(platform = 'claude') {
77
77
  if (platform === 'codex') {
@@ -153,7 +153,7 @@ function getAuditSpec(platform = 'claude') {
153
153
  };
154
154
  }
155
155
 
156
- function getPlatformScopeNote(spec, ctx) {
156
+ function getPlatformScopeNote(spec, ctx) {
157
157
  if (spec.platform !== 'codex') {
158
158
  return null;
159
159
  }
@@ -242,7 +242,7 @@ function printLiteAudit(result, dir) {
242
242
  console.log(colorize(` Found: ${result.detectedConfigFiles.join(', ')}`, 'dim'));
243
243
  }
244
244
  console.log('');
245
- console.log(` ${t('audit.score', { score: colorize(`${result.score}/100`, 'bold'), passed: result.passed, total: result.passed + result.failed })}`);
245
+ console.log(` ${t('audit.score', { score: colorize(`${result.score}/100`, 'bold'), passed: result.passed, total: result.passed + result.failed })}`);
246
246
 
247
247
  // Score explanation line (lite mode only)
248
248
  const _critCount = (result.results || []).filter(r => r.passed === false && r.impact === 'critical').length;
@@ -265,14 +265,14 @@ function printLiteAudit(result, dir) {
265
265
  scoreExplanation = t('audit.basic', { category: weakestCategory });
266
266
  } else {
267
267
  scoreExplanation = t('audit.early');
268
- }
269
- console.log(colorize(` ${scoreExplanation}`, 'dim'));
270
- if (result.scoreCoaching) {
271
- console.log(colorize(` Milestone: ${result.scoreCoaching.summary}`, 'magenta'));
272
- }
273
- console.log(colorize(' Score type: live repo audit (current files only, not snapshot history or benchmark projection).', 'dim'));
274
-
275
- if (result.platformScopeNote) {
268
+ }
269
+ console.log(colorize(` ${scoreExplanation}`, 'dim'));
270
+ if (result.scoreCoaching) {
271
+ console.log(colorize(` Milestone: ${result.scoreCoaching.summary}`, 'magenta'));
272
+ }
273
+ console.log(colorize(' Score type: live repo audit (current files only, not snapshot history or benchmark projection).', 'dim'));
274
+
275
+ if (result.platformScopeNote) {
276
276
  console.log(colorize(` Scope: ${result.platformScopeNote.message}`, 'dim'));
277
277
  }
278
278
  if (result.workspaceHint && result.workspaceHint.workspaces.length > 0) {
@@ -284,11 +284,11 @@ function printLiteAudit(result, dir) {
284
284
  console.log(colorize(` - ${item.title}: ${item.message}`, 'dim'));
285
285
  });
286
286
  }
287
- if (result.largeInstructionFiles && result.largeInstructionFiles.length > 0) {
288
- result.largeInstructionFiles.slice(0, 2).forEach((item) => {
289
- console.log(colorize(` Large file: ${item.file} (~${formatCount(item.tokenCount)} tokens)`, 'yellow'));
290
- });
291
- }
287
+ if (result.largeInstructionFiles && result.largeInstructionFiles.length > 0) {
288
+ result.largeInstructionFiles.slice(0, 2).forEach((item) => {
289
+ console.log(colorize(` Large file: ${item.file} (~${formatCount(item.tokenCount)} tokens)`, 'yellow'));
290
+ });
291
+ }
292
292
  console.log('');
293
293
 
294
294
  if (result.failed === 0) {
@@ -320,23 +320,23 @@ function printLiteAudit(result, dir) {
320
320
  console.log('');
321
321
  let usagePatterns;
322
322
  try { usagePatterns = require('./usage-patterns'); } catch { usagePatterns = null; }
323
- result.liteSummary.topNextActions.forEach((item, index) => {
324
- const tier = item.impact === 'critical' ? '🔴' : item.impact === 'high' ? '🟡' : '🔵';
325
- const suppressed = usagePatterns && usagePatterns.getPriorityAdjustment(dir, item.key) === 'suppress';
326
- const suffix = suppressed ? colorize(' (suppressed)', 'dim') : '';
327
- console.log(` ${index + 1}. ${tier} ${colorize(item.name, 'bold')}${suffix}`);
328
- console.log(colorize(` ${item.fix}`, 'dim'));
329
- });
330
- console.log('');
331
- const liteTerminology = formatTerminologyLines(collectAuditTerminology(result));
332
- if (liteTerminology.length > 0) {
333
- liteTerminology.forEach((line) => {
334
- const color = line.startsWith(' Terms used here:') ? 'blue' : 'dim';
335
- console.log(colorize(line, color));
336
- });
337
- console.log('');
338
- }
339
- console.log(` Ready? Run: ${colorize(result.suggestedNextCommand, 'bold')}`);
323
+ result.liteSummary.topNextActions.forEach((item, index) => {
324
+ const tier = item.impact === 'critical' ? '🔴' : item.impact === 'high' ? '🟡' : '🔵';
325
+ const suppressed = usagePatterns && usagePatterns.getPriorityAdjustment(dir, item.key) === 'suppress';
326
+ const suffix = suppressed ? colorize(' (suppressed)', 'dim') : '';
327
+ console.log(` ${index + 1}. ${tier} ${colorize(item.name, 'bold')}${suffix}`);
328
+ console.log(colorize(` ${item.fix}`, 'dim'));
329
+ });
330
+ console.log('');
331
+ const liteTerminology = formatTerminologyLines(collectAuditTerminology(result));
332
+ if (liteTerminology.length > 0) {
333
+ liteTerminology.forEach((line) => {
334
+ const color = line.startsWith(' Terms used here:') ? 'blue' : 'dim';
335
+ console.log(colorize(line, color));
336
+ });
337
+ console.log('');
338
+ }
339
+ console.log(` Ready? Run: ${colorize(result.suggestedNextCommand, 'bold')}`);
340
340
  if (result.platform === 'codex') {
341
341
  console.log(colorize(' Note: Codex now supports no-write advisory flows via augment and suggest-only before setup/apply.', 'dim'));
342
342
  }
@@ -387,7 +387,7 @@ async function audit(options) {
387
387
  'supply-chain', 'api-versioning', 'caching', 'rate-limiting', 'feature-flags',
388
388
  'docs-quality', 'monorepo', 'performance-budget', 'realtime', 'graphql',
389
389
  'testing-strategy', 'code-quality', 'api-design', 'database', 'authentication',
390
- 'monitoring', 'dependency-management', 'cost-optimization',
390
+ 'monitoring', 'dependency-management', 'cost-optimization', 'devops',
391
391
  ]);
392
392
  const includeGenericQuality = options.verbose;
393
393
 
@@ -425,12 +425,12 @@ async function audit(options) {
425
425
  key: 'largeInstructionFile',
426
426
  id: null,
427
427
  name: 'Large instruction file warning',
428
- category: 'performance',
429
- impact: 'medium',
430
- rating: null,
431
- fix: 'Split oversized instruction files so they stay under ~12,000 tokens, and keep any single instruction file below ~240,000 tokens.',
432
- sourceUrl: null,
433
- confidence: 'high',
428
+ category: 'performance',
429
+ impact: 'medium',
430
+ rating: null,
431
+ fix: 'Split oversized instruction files so they stay under ~12,000 tokens, and keep any single instruction file below ~240,000 tokens.',
432
+ sourceUrl: null,
433
+ confidence: 'high',
434
434
  file: largeInstructionFiles[0].file,
435
435
  line: null,
436
436
  passed: null,
@@ -498,13 +498,13 @@ async function audit(options) {
498
498
  ...largeInstructionFiles.map((item) => ({
499
499
  kind: 'large-instruction-file',
500
500
  severity: item.severity,
501
- message: item.message,
502
- file: item.file,
503
- lineCount: item.lineCount,
504
- byteCount: item.byteCount,
505
- tokenCount: item.tokenCount,
506
- skipped: item.skipped,
507
- })),
501
+ message: item.message,
502
+ file: item.file,
503
+ lineCount: item.lineCount,
504
+ byteCount: item.byteCount,
505
+ tokenCount: item.tokenCount,
506
+ skipped: item.skipped,
507
+ })),
508
508
  ...deprecationWarnings.map((item) => ({
509
509
  kind: 'deprecated-feature',
510
510
  severity: 'warning',
@@ -514,7 +514,7 @@ async function audit(options) {
514
514
  const recommendedDomainPacks = spec.platform === 'codex'
515
515
  ? detectCodexDomainPacks(ctx, stacks, getCodexDomainPackSignals(ctx))
516
516
  : [];
517
- const result = {
517
+ const result = {
518
518
  platform: spec.platform,
519
519
  platformLabel: spec.platformLabel,
520
520
  platformVersion: spec.platformVersion,
@@ -537,18 +537,18 @@ async function audit(options) {
537
537
  deprecatedReason: r.deprecatedReason || null,
538
538
  sunsetDate: r.sunsetDate || null,
539
539
  })),
540
- categoryScores,
541
- scoreCoaching: buildScoreCoaching({
542
- score,
543
- earnedPoints: earnedScore,
544
- maxPoints: maxScore,
545
- failed,
546
- outcomeSummaryByKey: outcomeSummary.byKey,
547
- platform: spec.platform,
548
- fpFeedbackByKey: fpFeedback.byKey,
549
- }),
550
- quickWins: quickWins.map(({ key, name, impact, fix, category, sourceUrl }) => ({ key, name, impact, category, fix, sourceUrl })),
551
- topNextActions,
540
+ categoryScores,
541
+ scoreCoaching: buildScoreCoaching({
542
+ score,
543
+ earnedPoints: earnedScore,
544
+ maxPoints: maxScore,
545
+ failed,
546
+ outcomeSummaryByKey: outcomeSummary.byKey,
547
+ platform: spec.platform,
548
+ fpFeedbackByKey: fpFeedback.byKey,
549
+ }),
550
+ quickWins: quickWins.map(({ key, name, impact, fix, category, sourceUrl }) => ({ key, name, impact, category, fix, sourceUrl })),
551
+ topNextActions,
552
552
  recommendationOutcomes: {
553
553
  totalEntries: outcomeSummary.totalEntries,
554
554
  keysTracked: outcomeSummary.keys,
@@ -578,12 +578,12 @@ async function audit(options) {
578
578
  result.detectedConfigFiles = configFiles;
579
579
 
580
580
  result.suggestedNextCommand = inferSuggestedNextCommand(result);
581
- result.liteSummary = {
582
- topNextActions: topNextActions.slice(0, 3),
583
- nextCommand: result.suggestedNextCommand,
584
- platformCaveats: platformCaveats.slice(0, 2),
585
- scoreCoaching: result.scoreCoaching,
586
- };
581
+ result.liteSummary = {
582
+ topNextActions: topNextActions.slice(0, 3),
583
+ nextCommand: result.suggestedNextCommand,
584
+ platformCaveats: platformCaveats.slice(0, 2),
585
+ scoreCoaching: result.scoreCoaching,
586
+ };
587
587
 
588
588
  // Silent mode: skip all output, just return result
589
589
  if (silent) {
@@ -642,13 +642,13 @@ async function audit(options) {
642
642
  console.log('');
643
643
  }
644
644
 
645
- if (largeInstructionFiles.length > 0) {
646
- console.log(colorize(' Large instruction files', 'yellow'));
647
- for (const item of largeInstructionFiles) {
648
- const sizeKb = Number.isFinite(item.byteCount) ? Math.round(item.byteCount / 1024) : '?';
649
- console.log(colorize(` ${item.file} (~${formatCount(item.tokenCount)} tokens, ${item.lineCount || '?'} lines, ${sizeKb}KB)`, 'bold'));
650
- console.log(colorize(` → ${item.message}`, 'dim'));
651
- }
645
+ if (largeInstructionFiles.length > 0) {
646
+ console.log(colorize(' Large instruction files', 'yellow'));
647
+ for (const item of largeInstructionFiles) {
648
+ const sizeKb = Number.isFinite(item.byteCount) ? Math.round(item.byteCount / 1024) : '?';
649
+ console.log(colorize(` ${item.file} (~${formatCount(item.tokenCount)} tokens, ${item.lineCount || '?'} lines, ${sizeKb}KB)`, 'bold'));
650
+ console.log(colorize(` → ${item.message}`, 'dim'));
651
+ }
652
652
  console.log('');
653
653
  }
654
654
 
@@ -678,18 +678,18 @@ async function audit(options) {
678
678
  console.log('');
679
679
 
680
680
  // Score
681
- console.log(` ${progressBar(score)} ${colorize(`${score}/100`, 'bold')}`);
682
- if (isScaffolded && scaffoldedPassed.length > 0) {
683
- console.log(colorize(` Organic: ${organicScore}/100 (without nerviq generated files)`, 'dim'));
684
- }
685
- if (result.scoreCoaching) {
686
- const fastestPath = result.scoreCoaching.recommendedNames.slice(0, 3).join(', ');
687
- console.log(colorize(` Milestone: ${result.scoreCoaching.summary}`, 'magenta'));
688
- if (fastestPath) {
689
- console.log(colorize(` Fastest path: ${fastestPath}`, 'dim'));
690
- }
691
- }
692
- console.log('');
681
+ console.log(` ${progressBar(score)} ${colorize(`${score}/100`, 'bold')}`);
682
+ if (isScaffolded && scaffoldedPassed.length > 0) {
683
+ console.log(colorize(` Organic: ${organicScore}/100 (without nerviq generated files)`, 'dim'));
684
+ }
685
+ if (result.scoreCoaching) {
686
+ const fastestPath = result.scoreCoaching.recommendedNames.slice(0, 3).join(', ');
687
+ console.log(colorize(` Milestone: ${result.scoreCoaching.summary}`, 'magenta'));
688
+ if (fastestPath) {
689
+ console.log(colorize(` Fastest path: ${fastestPath}`, 'dim'));
690
+ }
691
+ }
692
+ console.log('');
693
693
 
694
694
  // Passed
695
695
  if (passed.length > 0) {
@@ -755,8 +755,8 @@ async function audit(options) {
755
755
  }
756
756
 
757
757
  // Top next actions
758
- if (topNextActions.length > 0) {
759
- console.log(colorize(' ⚡ Top 5 Next Actions', 'magenta'));
758
+ if (topNextActions.length > 0) {
759
+ console.log(colorize(' ⚡ Top 5 Next Actions', 'magenta'));
760
760
  for (let i = 0; i < topNextActions.length; i++) {
761
761
  const item = topNextActions[i];
762
762
  console.log(` ${i + 1}. ${colorize(item.name, 'bold')}`);
@@ -772,20 +772,20 @@ async function audit(options) {
772
772
  console.log(colorize(` Feedback: accepted ${item.feedback.accepted}, rejected ${item.feedback.rejected}, positive ${item.feedback.positive}, negative ${item.feedback.negative}${avgDelta}`, 'dim'));
773
773
  }
774
774
  console.log(colorize(` Fix: ${item.fix}`, 'dim'));
775
- }
776
- console.log('');
777
- }
778
-
779
- const terminology = formatTerminologyLines(collectAuditTerminology(result));
780
- if (terminology.length > 0) {
781
- terminology.forEach((line) => {
782
- const color = line.startsWith(' Terms used here:') ? 'blue' : 'dim';
783
- console.log(colorize(line, color));
784
- });
785
- console.log('');
786
- }
787
-
788
- // Summary
775
+ }
776
+ console.log('');
777
+ }
778
+
779
+ const terminology = formatTerminologyLines(collectAuditTerminology(result));
780
+ if (terminology.length > 0) {
781
+ terminology.forEach((line) => {
782
+ const color = line.startsWith(' Terms used here:') ? 'blue' : 'dim';
783
+ console.log(colorize(line, color));
784
+ });
785
+ console.log('');
786
+ }
787
+
788
+ // Summary
789
789
  console.log(colorize(' ─────────────────────────────────────', 'dim'));
790
790
  const deprecatedNote = deprecated.length > 0 ? colorize(`, ${deprecated.length} deprecated`, 'dim') : '';
791
791
  console.log(` ${colorize(`${passed.length}/${applicable.length}`, 'bold')} checks passing${skipped.length > 0 ? colorize(` (${skipped.length} not applicable${deprecatedNote})`, 'dim') : (deprecatedNote ? colorize(` (${deprecatedNote})`, 'dim') : '')}`);
@@ -346,6 +346,237 @@ async function runHarmonyGovernance(options) {
346
346
  return summary;
347
347
  }
348
348
 
349
+ // ─── Command: harmony score ──────────────────────────────────────────────────
350
+
351
+ /**
352
+ * Output a standalone Harmony Score (0-100) with optional badge and CI threshold.
353
+ *
354
+ * Options:
355
+ * --json JSON output
356
+ * --badge Print shields.io badge markdown
357
+ * --threshold N Exit with code 1 if score < N (for CI gates)
358
+ * --quiet Score number only (for piping)
359
+ */
360
+ async function runHarmonyScore(options) {
361
+ const dir = resolveDir(options);
362
+ const { harmonyAudit } = require('./audit');
363
+ const result = await harmonyAudit({ dir, silent: true });
364
+
365
+ const score = result.harmonyScore;
366
+ const threshold = parseInt(options.threshold, 10) || 0;
367
+ const pass = score >= threshold;
368
+
369
+ if (options.json) {
370
+ const output = {
371
+ harmonyScore: score,
372
+ platforms: result.platformScores,
373
+ activePlatforms: result.activePlatforms.map(p => p.platform),
374
+ driftCount: result.drift.drifts.length,
375
+ threshold: threshold || null,
376
+ pass,
377
+ };
378
+ if (options.badge) {
379
+ output.badge = getHarmonyBadgeMarkdown(score);
380
+ output.badgeUrl = getHarmonyBadgeUrl(score);
381
+ }
382
+ console.log(JSON.stringify(output, null, 2));
383
+ return output;
384
+ }
385
+
386
+ if (options.quiet) {
387
+ console.log(score);
388
+ return { score, pass };
389
+ }
390
+
391
+ console.log('');
392
+ console.log(c(' Harmony Score', 'bold'));
393
+ console.log(c(' ═══════════════════════════════════════', 'dim'));
394
+ console.log('');
395
+
396
+ // Score with color bar
397
+ const barWidth = 30;
398
+ const filled = Math.round((score / 100) * barWidth);
399
+ const empty = barWidth - filled;
400
+ const scoreColor = score >= 80 ? 'green' : score >= 50 ? 'yellow' : 'red';
401
+ const bar = c('\u2588'.repeat(filled), scoreColor) + c('\u2591'.repeat(empty), 'dim');
402
+ console.log(` ${bar} ${c(`${score}/100`, scoreColor)}`);
403
+ console.log('');
404
+
405
+ // Per-platform breakdown
406
+ for (const ap of result.activePlatforms) {
407
+ const ps = result.platformScores[ap.platform];
408
+ const psColor = ps >= 70 ? 'green' : ps >= 40 ? 'yellow' : 'red';
409
+ console.log(` ${ap.platform.padEnd(12)} ${ps !== null ? c(`${ps}/100`, psColor) : c('n/a', 'dim')}`);
410
+ }
411
+ console.log('');
412
+
413
+ // Drift summary
414
+ const driftCount = result.drift.drifts.length;
415
+ if (driftCount > 0) {
416
+ const critical = result.drift.drifts.filter(d => d.severity === 'critical').length;
417
+ const high = result.drift.drifts.filter(d => d.severity === 'high').length;
418
+ let driftMsg = ` ${driftCount} drift issue${driftCount !== 1 ? 's' : ''}`;
419
+ if (critical > 0) driftMsg += c(` (${critical} critical)`, 'red');
420
+ else if (high > 0) driftMsg += c(` (${high} high)`, 'yellow');
421
+ console.log(driftMsg);
422
+ console.log(c(' Run "nerviq harmony-audit" for details.', 'dim'));
423
+ console.log('');
424
+ }
425
+
426
+ // Badge output
427
+ if (options.badge) {
428
+ console.log(c(' Badge:', 'bold'));
429
+ console.log(` ${getHarmonyBadgeMarkdown(score)}`);
430
+ console.log('');
431
+ }
432
+
433
+ // Threshold check
434
+ if (threshold > 0) {
435
+ if (pass) {
436
+ console.log(c(` Threshold: ${score} >= ${threshold} PASS`, 'green'));
437
+ } else {
438
+ console.log(c(` Threshold: ${score} < ${threshold} FAIL`, 'red'));
439
+ }
440
+ console.log('');
441
+ }
442
+
443
+ return { score, pass, platforms: result.platformScores };
444
+ }
445
+
446
+ // ─── Harmony Badge helpers ───────────────────────────────────────────────────
447
+
448
+ function getHarmonyBadgeUrl(score) {
449
+ const color = score >= 80 ? 'brightgreen' : score >= 60 ? 'yellow' : score >= 40 ? 'orange' : 'red';
450
+ const label = encodeURIComponent('Harmony Score');
451
+ const message = encodeURIComponent(`${score}/100`);
452
+ return `https://img.shields.io/badge/${label}-${message}-${color}`;
453
+ }
454
+
455
+ function getHarmonyBadgeMarkdown(score) {
456
+ const url = getHarmonyBadgeUrl(score);
457
+ return `[![Harmony Score](${url})](https://github.com/nerviq/nerviq)`;
458
+ }
459
+
460
+ // ─── Command: harmony demo ──────────────────────────────────────────────────
461
+
462
+ /**
463
+ * Zero-setup demo: creates a temporary multi-platform project, runs harmony
464
+ * audit on it, and shows how Nerviq detects cross-platform drift.
465
+ *
466
+ * This lets new users see Harmony's value instantly without configuring anything.
467
+ */
468
+ async function runHarmonyDemo(options) {
469
+ const fs = require('fs');
470
+ const os = require('os');
471
+ const { harmonyAudit } = require('./audit');
472
+
473
+ console.log('');
474
+ console.log(c(' Harmony Demo — Zero-Setup Cross-Platform Drift Detection', 'bold'));
475
+ console.log(c(' ═══════════════════════════════════════════════════════', 'dim'));
476
+ console.log('');
477
+ console.log(c(' Creating a sample multi-platform project...', 'dim'));
478
+ console.log('');
479
+
480
+ // Create temp directory with realistic multi-platform configs
481
+ const demoDir = path.join(os.tmpdir(), `nerviq-harmony-demo-${Date.now()}`);
482
+ fs.mkdirSync(demoDir, { recursive: true });
483
+ fs.mkdirSync(path.join(demoDir, '.claude'), { recursive: true });
484
+ fs.mkdirSync(path.join(demoDir, '.cursor'), { recursive: true });
485
+ fs.mkdirSync(path.join(demoDir, '.github'), { recursive: true });
486
+
487
+ // Claude config — well-configured
488
+ fs.writeFileSync(path.join(demoDir, 'CLAUDE.md'), [
489
+ '# Project Instructions',
490
+ '',
491
+ '## Architecture',
492
+ 'This is a Node.js API with PostgreSQL. Use Express for routing.',
493
+ '',
494
+ '## Testing',
495
+ 'Run tests with `npm test`. All PRs require passing tests.',
496
+ '',
497
+ '## Security',
498
+ '- Never commit .env files',
499
+ '- Use parameterized queries for all database access',
500
+ '- Validate all user input',
501
+ '',
502
+ '## Code Style',
503
+ '- Use ESLint with the project config',
504
+ '- Prefer async/await over callbacks',
505
+ '- Add JSDoc comments for public functions',
506
+ ].join('\n'));
507
+
508
+ fs.writeFileSync(path.join(demoDir, '.claude', 'settings.json'), JSON.stringify({
509
+ permissions: {
510
+ allow: ['Read', 'Glob', 'Grep'],
511
+ deny: ['Bash(rm -rf *)'],
512
+ },
513
+ model: 'claude-sonnet-4-6',
514
+ }, null, 2));
515
+
516
+ // Cursor config — intentionally drifted (different rules, less security)
517
+ fs.writeFileSync(path.join(demoDir, '.cursorrules'), [
518
+ 'You are a helpful coding assistant.',
519
+ 'This is a Node.js project using Express.',
520
+ 'Write clean, readable code.',
521
+ // Missing: security rules, testing rules, architecture details
522
+ ].join('\n'));
523
+
524
+ // Copilot config — partial coverage
525
+ fs.writeFileSync(path.join(demoDir, '.github', 'copilot-instructions.md'), [
526
+ '# Copilot Instructions',
527
+ '',
528
+ 'This is a Node.js Express API project.',
529
+ 'Use TypeScript-style JSDoc annotations.',
530
+ 'Follow RESTful conventions for API endpoints.',
531
+ // Missing: security, testing, architecture details
532
+ ].join('\n'));
533
+
534
+ // Add a package.json for realism
535
+ fs.writeFileSync(path.join(demoDir, 'package.json'), JSON.stringify({
536
+ name: 'harmony-demo-project',
537
+ version: '1.0.0',
538
+ scripts: { test: 'jest' },
539
+ }, null, 2));
540
+
541
+ console.log(c(' Demo project created with 3 platforms:', 'bold'));
542
+ console.log(` ${c('Claude', 'green')} — Well-configured (CLAUDE.md + settings.json)`);
543
+ console.log(` ${c('Cursor', 'yellow')} — Basic rules only (.cursorrules)`);
544
+ console.log(` ${c('Copilot', 'yellow')} — Partial coverage (copilot-instructions.md)`);
545
+ console.log('');
546
+ console.log(c(' Intentional drift injected:', 'bold'));
547
+ console.log(` ${c('\u2718', 'red')} Security rules only in Claude, missing from Cursor & Copilot`);
548
+ console.log(` ${c('\u2718', 'red')} Testing instructions only in Claude`);
549
+ console.log(` ${c('\u2718', 'red')} Architecture details inconsistent across platforms`);
550
+ console.log(` ${c('\u2718', 'red')} Trust posture differs (Claude has explicit permissions)`);
551
+ console.log('');
552
+ console.log(c(' Running Harmony Audit...', 'dim'));
553
+ console.log('');
554
+
555
+ // Run the actual harmony audit on the demo project
556
+ const result = await harmonyAudit({ dir: demoDir, silent: false, verbose: !!options.verbose });
557
+
558
+ console.log('');
559
+ console.log(c(' ═══════════════════════════════════════════════════════', 'dim'));
560
+ console.log(c(' What you just saw:', 'bold'));
561
+ console.log('');
562
+ console.log(' Nerviq Harmony detected real configuration drift between');
563
+ console.log(' 3 AI coding platforms in your project — differences in');
564
+ console.log(' instructions, security posture, and tool coverage that');
565
+ console.log(' cause inconsistent AI behavior.');
566
+ console.log('');
567
+ console.log(c(' Try it on your own project:', 'bold'));
568
+ console.log(` ${c('npx @nerviq/cli harmony-audit', 'blue')}`);
569
+ console.log(` ${c('npx @nerviq/cli harmony-score --threshold 70', 'blue')}`);
570
+ console.log('');
571
+
572
+ // Clean up
573
+ try {
574
+ fs.rmSync(demoDir, { recursive: true, force: true });
575
+ } catch (_e) { /* cleanup optional */ }
576
+
577
+ return result;
578
+ }
579
+
349
580
  module.exports = {
350
581
  runHarmonyAudit,
351
582
  runHarmonySync,
@@ -353,4 +584,8 @@ module.exports = {
353
584
  runHarmonyAdvise,
354
585
  runHarmonyWatch,
355
586
  runHarmonyGovernance,
587
+ runHarmonyScore,
588
+ runHarmonyDemo,
589
+ getHarmonyBadgeUrl,
590
+ getHarmonyBadgeMarkdown,
356
591
  };
package/src/index.js CHANGED
@@ -240,6 +240,7 @@ module.exports = {
240
240
  const { startHarmonyWatch } = require('./harmony/watch');
241
241
  const { saveHarmonyState, loadHarmonyState, getHarmonyHistory } = require('./harmony/memory');
242
242
  const { getHarmonyGovernanceSummary, formatHarmonyGovernanceReport } = require('./harmony/governance');
243
+ const { getHarmonyBadgeUrl, getHarmonyBadgeMarkdown } = require('./harmony/cli');
243
244
  return {
244
245
  buildCanonicalModel, detectActivePlatforms, detectDrift, formatDriftReport,
245
246
  harmonyAudit, formatHarmonyAuditReport,
@@ -247,6 +248,7 @@ module.exports = {
247
248
  generateStrategicAdvice, PLATFORM_STRENGTHS,
248
249
  startHarmonyWatch, saveHarmonyState, loadHarmonyState, getHarmonyHistory,
249
250
  getHarmonyGovernanceSummary, formatHarmonyGovernanceReport,
251
+ getHarmonyBadgeUrl, getHarmonyBadgeMarkdown,
250
252
  };
251
253
  })(),
252
254
  // Synergy (cross-platform amplification)