@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/README.md +5 -2
- package/bin/cli.js +1014 -998
- package/package.json +1 -1
- package/src/audit.js +127 -127
- package/src/harmony/cli.js +235 -0
- package/src/index.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerviq/cli",
|
|
3
|
-
"version": "1.
|
|
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') : '')}`);
|
package/src/harmony/cli.js
CHANGED
|
@@ -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 `[](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)
|