@nerviq/cli 1.9.0 → 1.11.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 +106 -60
- package/bin/cli.js +382 -208
- package/package.json +1 -1
- package/src/activity.js +245 -63
- package/src/aider/freshness.js +149 -146
- package/src/analyze.js +3 -1
- package/src/anti-patterns.js +17 -13
- package/src/audit.js +106 -79
- package/src/auto-suggest.js +62 -9
- package/src/benchmark.js +67 -51
- package/src/dashboard.js +36 -14
- package/src/governance.js +13 -7
- package/src/index.js +2 -1
- package/src/instruction-surfaces.js +185 -0
- package/src/integrations.js +102 -55
- package/src/locales/en.json +1 -1
- package/src/locales/es.json +1 -1
- package/src/permission-rules.js +218 -0
- package/src/secret-patterns.js +9 -0
- package/src/server.js +398 -3
- package/src/setup.js +2 -2
- package/src/stack-checks.js +1 -1
- package/src/synergy/report.js +1 -0
- package/src/techniques.js +102 -103
- package/src/terminology.js +73 -0
- package/src/token-estimate.js +35 -0
- package/src/workspace.js +155 -13
package/src/audit.js
CHANGED
|
@@ -24,16 +24,18 @@ const { AiderProjectContext } = require('./aider/context');
|
|
|
24
24
|
const { OPENCODE_TECHNIQUES } = require('./opencode/techniques');
|
|
25
25
|
const { OpenCodeProjectContext } = require('./opencode/context');
|
|
26
26
|
const { getBadgeMarkdown } = require('./badge');
|
|
27
|
-
const { sendInsights, getLocalInsights } = require('./insights');
|
|
28
|
-
const { getRecommendationOutcomeSummary, getRecommendationAdjustment } = require('./activity');
|
|
29
|
-
const { getFeedbackSummary } = require('./feedback');
|
|
30
|
-
const { formatSarif } = require('./formatters/sarif');
|
|
31
|
-
const { formatOtelMetrics } = require('./formatters/otel');
|
|
32
|
-
const {
|
|
33
|
-
const {
|
|
34
|
-
const {
|
|
35
|
-
const {
|
|
36
|
-
const {
|
|
27
|
+
const { sendInsights, getLocalInsights } = require('./insights');
|
|
28
|
+
const { getRecommendationOutcomeSummary, getRecommendationAdjustment } = require('./activity');
|
|
29
|
+
const { getFeedbackSummary } = require('./feedback');
|
|
30
|
+
const { formatSarif } = require('./formatters/sarif');
|
|
31
|
+
const { formatOtelMetrics } = require('./formatters/otel');
|
|
32
|
+
const { collectAuditTerminology, formatTerminologyLines } = require('./terminology');
|
|
33
|
+
const { loadPlugins, mergePluginChecks } = require('./plugins');
|
|
34
|
+
const { hasWorkspaceConfig, detectWorkspaceGlobs, detectWorkspaces } = require('./workspace');
|
|
35
|
+
const { detectDeprecationWarnings } = require('./deprecation');
|
|
36
|
+
const { estimateTokenCount } = require('./token-estimate');
|
|
37
|
+
const { version: packageVersion } = require('../package.json');
|
|
38
|
+
const { t } = require('./i18n');
|
|
37
39
|
|
|
38
40
|
const COLORS = {
|
|
39
41
|
reset: '\x1b[0m',
|
|
@@ -64,8 +66,8 @@ function formatLocation(file, line) {
|
|
|
64
66
|
|
|
65
67
|
const IMPACT_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
|
|
66
68
|
const WEIGHTS = { critical: 15, high: 10, medium: 5, low: 2 };
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
+
const LARGE_INSTRUCTION_WARN_TOKENS = 12000;
|
|
70
|
+
const LARGE_INSTRUCTION_SKIP_TOKENS = 240000;
|
|
69
71
|
const CATEGORY_MODULES = {
|
|
70
72
|
memory: 'CLAUDE.md',
|
|
71
73
|
quality: 'verification',
|
|
@@ -358,9 +360,13 @@ function getAuditSpec(platform = 'claude') {
|
|
|
358
360
|
};
|
|
359
361
|
}
|
|
360
362
|
|
|
361
|
-
function normalizeRelativePath(filePath) {
|
|
362
|
-
return String(filePath || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
363
|
-
}
|
|
363
|
+
function normalizeRelativePath(filePath) {
|
|
364
|
+
return String(filePath || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function formatCount(value) {
|
|
368
|
+
return Number(value || 0).toLocaleString('en-US');
|
|
369
|
+
}
|
|
364
370
|
|
|
365
371
|
function addPath(target, filePath) {
|
|
366
372
|
if (!filePath || typeof filePath !== 'string') return;
|
|
@@ -454,25 +460,27 @@ function instructionFileCandidates(spec, ctx) {
|
|
|
454
460
|
return [...candidates];
|
|
455
461
|
}
|
|
456
462
|
|
|
457
|
-
function inspectInstructionFiles(spec, ctx) {
|
|
458
|
-
const warnings = [];
|
|
459
|
-
|
|
460
|
-
for (const filePath of instructionFileCandidates(spec, ctx)) {
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
463
|
+
function inspectInstructionFiles(spec, ctx) {
|
|
464
|
+
const warnings = [];
|
|
465
|
+
|
|
466
|
+
for (const filePath of instructionFileCandidates(spec, ctx)) {
|
|
467
|
+
const content = typeof ctx.fileContent === 'function' ? ctx.fileContent(filePath) : null;
|
|
468
|
+
const byteCount = typeof ctx.fileSizeBytes === 'function' ? ctx.fileSizeBytes(filePath) : null;
|
|
469
|
+
const tokenCount = typeof content === 'string' ? estimateTokenCount(content) : null;
|
|
470
|
+
if (!Number.isFinite(tokenCount) || tokenCount <= LARGE_INSTRUCTION_WARN_TOKENS) continue;
|
|
471
|
+
|
|
472
|
+
warnings.push({
|
|
473
|
+
file: normalizeRelativePath(filePath),
|
|
474
|
+
byteCount,
|
|
475
|
+
tokenCount,
|
|
476
|
+
lineCount: typeof content === 'string' ? content.split(/\r?\n/).length : null,
|
|
477
|
+
skipped: tokenCount > LARGE_INSTRUCTION_SKIP_TOKENS,
|
|
478
|
+
severity: tokenCount > LARGE_INSTRUCTION_SKIP_TOKENS ? 'critical' : 'warning',
|
|
479
|
+
message: tokenCount > LARGE_INSTRUCTION_SKIP_TOKENS
|
|
480
|
+
? 'Instruction file exceeds ~240,000 tokens and will be skipped during audit.'
|
|
481
|
+
: 'Instruction file exceeds ~12,000 tokens. Audit will continue, but this file may reduce runtime clarity.',
|
|
482
|
+
});
|
|
483
|
+
}
|
|
476
484
|
|
|
477
485
|
return warnings;
|
|
478
486
|
}
|
|
@@ -882,7 +890,7 @@ function printLiteAudit(result, dir) {
|
|
|
882
890
|
console.log(colorize(` Found: ${result.detectedConfigFiles.join(', ')}`, 'dim'));
|
|
883
891
|
}
|
|
884
892
|
console.log('');
|
|
885
|
-
console.log(` ${t('audit.score', { score: colorize(`${result.score}/100`, 'bold'), passed: result.passed, total: result.passed + result.failed })}`);
|
|
893
|
+
console.log(` ${t('audit.score', { score: colorize(`${result.score}/100`, 'bold'), passed: result.passed, total: result.passed + result.failed })}`);
|
|
886
894
|
|
|
887
895
|
// Score explanation line (lite mode only)
|
|
888
896
|
const _critCount = (result.results || []).filter(r => r.passed === false && r.impact === 'critical').length;
|
|
@@ -905,10 +913,11 @@ function printLiteAudit(result, dir) {
|
|
|
905
913
|
scoreExplanation = t('audit.basic', { category: weakestCategory });
|
|
906
914
|
} else {
|
|
907
915
|
scoreExplanation = t('audit.early');
|
|
908
|
-
}
|
|
909
|
-
console.log(colorize(` ${scoreExplanation}`, 'dim'));
|
|
910
|
-
|
|
911
|
-
|
|
916
|
+
}
|
|
917
|
+
console.log(colorize(` ${scoreExplanation}`, 'dim'));
|
|
918
|
+
console.log(colorize(' Score type: live repo audit (current files only, not snapshot history or benchmark projection).', 'dim'));
|
|
919
|
+
|
|
920
|
+
if (result.platformScopeNote) {
|
|
912
921
|
console.log(colorize(` Scope: ${result.platformScopeNote.message}`, 'dim'));
|
|
913
922
|
}
|
|
914
923
|
if (result.workspaceHint && result.workspaceHint.workspaces.length > 0) {
|
|
@@ -920,11 +929,11 @@ function printLiteAudit(result, dir) {
|
|
|
920
929
|
console.log(colorize(` - ${item.title}: ${item.message}`, 'dim'));
|
|
921
930
|
});
|
|
922
931
|
}
|
|
923
|
-
if (result.largeInstructionFiles && result.largeInstructionFiles.length > 0) {
|
|
924
|
-
result.largeInstructionFiles.slice(0, 2).forEach((item) => {
|
|
925
|
-
console.log(colorize(` Large file: ${item.file} (
|
|
926
|
-
});
|
|
927
|
-
}
|
|
932
|
+
if (result.largeInstructionFiles && result.largeInstructionFiles.length > 0) {
|
|
933
|
+
result.largeInstructionFiles.slice(0, 2).forEach((item) => {
|
|
934
|
+
console.log(colorize(` Large file: ${item.file} (~${formatCount(item.tokenCount)} tokens)`, 'yellow'));
|
|
935
|
+
});
|
|
936
|
+
}
|
|
928
937
|
console.log('');
|
|
929
938
|
|
|
930
939
|
if (result.failed === 0) {
|
|
@@ -956,15 +965,23 @@ function printLiteAudit(result, dir) {
|
|
|
956
965
|
console.log('');
|
|
957
966
|
let usagePatterns;
|
|
958
967
|
try { usagePatterns = require('./usage-patterns'); } catch { usagePatterns = null; }
|
|
959
|
-
result.liteSummary.topNextActions.forEach((item, index) => {
|
|
960
|
-
const tier = item.impact === 'critical' ? '🔴' : item.impact === 'high' ? '🟡' : '🔵';
|
|
961
|
-
const suppressed = usagePatterns && usagePatterns.getPriorityAdjustment(dir, item.key) === 'suppress';
|
|
962
|
-
const suffix = suppressed ? colorize(' (suppressed)', 'dim') : '';
|
|
963
|
-
console.log(` ${index + 1}. ${tier} ${colorize(item.name, 'bold')}${suffix}`);
|
|
964
|
-
console.log(colorize(` ${item.fix}`, 'dim'));
|
|
965
|
-
});
|
|
966
|
-
console.log('');
|
|
967
|
-
|
|
968
|
+
result.liteSummary.topNextActions.forEach((item, index) => {
|
|
969
|
+
const tier = item.impact === 'critical' ? '🔴' : item.impact === 'high' ? '🟡' : '🔵';
|
|
970
|
+
const suppressed = usagePatterns && usagePatterns.getPriorityAdjustment(dir, item.key) === 'suppress';
|
|
971
|
+
const suffix = suppressed ? colorize(' (suppressed)', 'dim') : '';
|
|
972
|
+
console.log(` ${index + 1}. ${tier} ${colorize(item.name, 'bold')}${suffix}`);
|
|
973
|
+
console.log(colorize(` ${item.fix}`, 'dim'));
|
|
974
|
+
});
|
|
975
|
+
console.log('');
|
|
976
|
+
const liteTerminology = formatTerminologyLines(collectAuditTerminology(result));
|
|
977
|
+
if (liteTerminology.length > 0) {
|
|
978
|
+
liteTerminology.forEach((line) => {
|
|
979
|
+
const color = line.startsWith(' Terms used here:') ? 'blue' : 'dim';
|
|
980
|
+
console.log(colorize(line, color));
|
|
981
|
+
});
|
|
982
|
+
console.log('');
|
|
983
|
+
}
|
|
984
|
+
console.log(` Ready? Run: ${colorize(result.suggestedNextCommand, 'bold')}`);
|
|
968
985
|
if (result.platform === 'codex') {
|
|
969
986
|
console.log(colorize(' Note: Codex now supports no-write advisory flows via augment and suggest-only before setup/apply.', 'dim'));
|
|
970
987
|
}
|
|
@@ -1053,12 +1070,12 @@ async function audit(options) {
|
|
|
1053
1070
|
key: 'largeInstructionFile',
|
|
1054
1071
|
id: null,
|
|
1055
1072
|
name: 'Large instruction file warning',
|
|
1056
|
-
category: 'performance',
|
|
1057
|
-
impact: 'medium',
|
|
1058
|
-
rating: null,
|
|
1059
|
-
fix: 'Split oversized instruction files so they stay under
|
|
1060
|
-
sourceUrl: null,
|
|
1061
|
-
confidence: 'high',
|
|
1073
|
+
category: 'performance',
|
|
1074
|
+
impact: 'medium',
|
|
1075
|
+
rating: null,
|
|
1076
|
+
fix: 'Split oversized instruction files so they stay under ~12,000 tokens, and keep any single instruction file below ~240,000 tokens.',
|
|
1077
|
+
sourceUrl: null,
|
|
1078
|
+
confidence: 'high',
|
|
1062
1079
|
file: largeInstructionFiles[0].file,
|
|
1063
1080
|
line: null,
|
|
1064
1081
|
passed: null,
|
|
@@ -1126,12 +1143,13 @@ async function audit(options) {
|
|
|
1126
1143
|
...largeInstructionFiles.map((item) => ({
|
|
1127
1144
|
kind: 'large-instruction-file',
|
|
1128
1145
|
severity: item.severity,
|
|
1129
|
-
message: item.message,
|
|
1130
|
-
file: item.file,
|
|
1131
|
-
lineCount: item.lineCount,
|
|
1132
|
-
byteCount: item.byteCount,
|
|
1133
|
-
|
|
1134
|
-
|
|
1146
|
+
message: item.message,
|
|
1147
|
+
file: item.file,
|
|
1148
|
+
lineCount: item.lineCount,
|
|
1149
|
+
byteCount: item.byteCount,
|
|
1150
|
+
tokenCount: item.tokenCount,
|
|
1151
|
+
skipped: item.skipped,
|
|
1152
|
+
})),
|
|
1135
1153
|
...deprecationWarnings.map((item) => ({
|
|
1136
1154
|
kind: 'deprecated-feature',
|
|
1137
1155
|
severity: 'warning',
|
|
@@ -1259,13 +1277,13 @@ async function audit(options) {
|
|
|
1259
1277
|
console.log('');
|
|
1260
1278
|
}
|
|
1261
1279
|
|
|
1262
|
-
if (largeInstructionFiles.length > 0) {
|
|
1263
|
-
console.log(colorize(' Large instruction files', 'yellow'));
|
|
1264
|
-
for (const item of largeInstructionFiles) {
|
|
1265
|
-
const sizeKb = Math.round(item.byteCount / 1024);
|
|
1266
|
-
console.log(colorize(` ${item.file} (
|
|
1267
|
-
console.log(colorize(` → ${item.message}`, 'dim'));
|
|
1268
|
-
}
|
|
1280
|
+
if (largeInstructionFiles.length > 0) {
|
|
1281
|
+
console.log(colorize(' Large instruction files', 'yellow'));
|
|
1282
|
+
for (const item of largeInstructionFiles) {
|
|
1283
|
+
const sizeKb = Number.isFinite(item.byteCount) ? Math.round(item.byteCount / 1024) : '?';
|
|
1284
|
+
console.log(colorize(` ${item.file} (~${formatCount(item.tokenCount)} tokens, ${item.lineCount || '?'} lines, ${sizeKb}KB)`, 'bold'));
|
|
1285
|
+
console.log(colorize(` → ${item.message}`, 'dim'));
|
|
1286
|
+
}
|
|
1269
1287
|
console.log('');
|
|
1270
1288
|
}
|
|
1271
1289
|
|
|
@@ -1365,8 +1383,8 @@ async function audit(options) {
|
|
|
1365
1383
|
}
|
|
1366
1384
|
|
|
1367
1385
|
// Top next actions
|
|
1368
|
-
if (topNextActions.length > 0) {
|
|
1369
|
-
console.log(colorize(' ⚡ Top 5 Next Actions', 'magenta'));
|
|
1386
|
+
if (topNextActions.length > 0) {
|
|
1387
|
+
console.log(colorize(' ⚡ Top 5 Next Actions', 'magenta'));
|
|
1370
1388
|
for (let i = 0; i < topNextActions.length; i++) {
|
|
1371
1389
|
const item = topNextActions[i];
|
|
1372
1390
|
console.log(` ${i + 1}. ${colorize(item.name, 'bold')}`);
|
|
@@ -1382,11 +1400,20 @@ async function audit(options) {
|
|
|
1382
1400
|
console.log(colorize(` Feedback: accepted ${item.feedback.accepted}, rejected ${item.feedback.rejected}, positive ${item.feedback.positive}, negative ${item.feedback.negative}${avgDelta}`, 'dim'));
|
|
1383
1401
|
}
|
|
1384
1402
|
console.log(colorize(` Fix: ${item.fix}`, 'dim'));
|
|
1385
|
-
}
|
|
1386
|
-
console.log('');
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1403
|
+
}
|
|
1404
|
+
console.log('');
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
const terminology = formatTerminologyLines(collectAuditTerminology(result));
|
|
1408
|
+
if (terminology.length > 0) {
|
|
1409
|
+
terminology.forEach((line) => {
|
|
1410
|
+
const color = line.startsWith(' Terms used here:') ? 'blue' : 'dim';
|
|
1411
|
+
console.log(colorize(line, color));
|
|
1412
|
+
});
|
|
1413
|
+
console.log('');
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// Summary
|
|
1390
1417
|
console.log(colorize(' ─────────────────────────────────────', 'dim'));
|
|
1391
1418
|
const deprecatedNote = deprecated.length > 0 ? colorize(`, ${deprecated.length} deprecated`, 'dim') : '';
|
|
1392
1419
|
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/auto-suggest.js
CHANGED
|
@@ -52,23 +52,72 @@ function analyzeSuggestions(dir) {
|
|
|
52
52
|
.sort((a, b) => b[1] - a[1])
|
|
53
53
|
.map(([key, count]) => ({ key, failCount: count, auditCount: auditSnapshots.length }));
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
const hasSuggestions = suggestedRules.length > 0 || suggestedSuppressions.length > 0 || suggestedPriorities.length > 0;
|
|
56
|
+
let bootstrap = { ready: true, state: 'ready', message: null, steps: [] };
|
|
57
|
+
|
|
58
|
+
if (totalEvents === 0 && auditSnapshots.length === 0) {
|
|
59
|
+
bootstrap = {
|
|
60
|
+
ready: false,
|
|
61
|
+
state: 'empty',
|
|
62
|
+
message: 'No local usage or snapshot history exists yet.',
|
|
63
|
+
steps: [
|
|
64
|
+
'Run `nerviq audit --snapshot` to save the baseline.',
|
|
65
|
+
'Use `nerviq fix`, `nerviq fix --all-critical`, or `nerviq feedback` to record recommendation outcomes.',
|
|
66
|
+
'Run `nerviq audit --snapshot` again after a meaningful repo change.',
|
|
67
|
+
'Re-run `nerviq suggest-rules`.',
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
} else if (!hasSuggestions && totalEvents === 0 && auditSnapshots.length > 0) {
|
|
71
|
+
bootstrap = {
|
|
72
|
+
ready: false,
|
|
73
|
+
state: 'snapshots-only',
|
|
74
|
+
message: `${auditSnapshots.length} audit snapshot(s) exist, but no recommendation outcomes have been recorded yet.`,
|
|
75
|
+
steps: [
|
|
76
|
+
'Run `nerviq fix` or `nerviq feedback` so Nerviq can learn which recommendations you accept or reject.',
|
|
77
|
+
'Re-run `nerviq suggest-rules` after another fix cycle.',
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
} else if (!hasSuggestions && totalEvents > 0 && auditSnapshots.length === 0) {
|
|
81
|
+
bootstrap = {
|
|
82
|
+
ready: false,
|
|
83
|
+
state: 'patterns-only',
|
|
84
|
+
message: `${totalEvents} usage event(s) exist, but no audit snapshots have been saved yet.`,
|
|
85
|
+
steps: [
|
|
86
|
+
'Run `nerviq audit --snapshot` to save the baseline.',
|
|
87
|
+
'Run it again after changes so repeated failures can be prioritized.',
|
|
88
|
+
'Re-run `nerviq suggest-rules`.',
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
} else if (!hasSuggestions) {
|
|
92
|
+
bootstrap = {
|
|
93
|
+
ready: false,
|
|
94
|
+
state: 'warming-up',
|
|
95
|
+
message: `Nerviq has some local history (${totalEvents} pattern events, ${auditSnapshots.length} audit snapshots), but not enough repeated signals yet.`,
|
|
96
|
+
steps: [
|
|
97
|
+
'Keep saving snapshots with `nerviq audit --snapshot`.',
|
|
98
|
+
'Keep recording outcomes with `nerviq fix` or `nerviq feedback`.',
|
|
99
|
+
'Re-run `nerviq suggest-rules` after another change cycle.',
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { totalEvents, auditCount: auditSnapshots.length, suggestedRules, suggestedSuppressions, suggestedPriorities, bootstrap };
|
|
56
105
|
}
|
|
57
106
|
|
|
58
107
|
/**
|
|
59
108
|
* Format suggestions for CLI output.
|
|
60
109
|
*/
|
|
61
110
|
function formatSuggestions(suggestions) {
|
|
62
|
-
const { totalEvents, auditCount, suggestedRules, suggestedSuppressions, suggestedPriorities } = suggestions;
|
|
63
|
-
|
|
64
|
-
if (totalEvents === 0 && auditCount === 0) {
|
|
65
|
-
return ' No usage data yet. Run nerviq fix or nerviq audit to build pattern history.';
|
|
66
|
-
}
|
|
111
|
+
const { totalEvents, auditCount, suggestedRules, suggestedSuppressions, suggestedPriorities, bootstrap } = suggestions;
|
|
67
112
|
|
|
68
113
|
const sources = [];
|
|
69
114
|
if (totalEvents > 0) sources.push(`${totalEvents} pattern events`);
|
|
70
115
|
if (auditCount > 0) sources.push(`${auditCount} audit snapshots`);
|
|
71
|
-
const lines = [
|
|
116
|
+
const lines = [
|
|
117
|
+
sources.length > 0
|
|
118
|
+
? ` Auto-Suggested Rules (based on ${sources.join(', ')}):`
|
|
119
|
+
: ' Auto-Suggested Rules:',
|
|
120
|
+
];
|
|
72
121
|
|
|
73
122
|
if (suggestedRules.length > 0) {
|
|
74
123
|
lines.push('', ' Suggested as required (always accepted):');
|
|
@@ -91,8 +140,12 @@ function formatSuggestions(suggestions) {
|
|
|
91
140
|
}
|
|
92
141
|
}
|
|
93
142
|
|
|
94
|
-
if (suggestedRules.length === 0 && suggestedSuppressions.length === 0 && suggestedPriorities.length === 0) {
|
|
95
|
-
lines.push('',
|
|
143
|
+
if (suggestedRules.length === 0 && suggestedSuppressions.length === 0 && suggestedPriorities.length === 0 && bootstrap && !bootstrap.ready) {
|
|
144
|
+
lines.push('', ` ${bootstrap.message}`);
|
|
145
|
+
lines.push(' Bootstrap it with:');
|
|
146
|
+
for (let i = 0; i < bootstrap.steps.length; i++) {
|
|
147
|
+
lines.push(` ${i + 1}. ${bootstrap.steps[i]}`);
|
|
148
|
+
}
|
|
96
149
|
}
|
|
97
150
|
|
|
98
151
|
return lines.join('\n');
|
package/src/benchmark.js
CHANGED
|
@@ -2,11 +2,12 @@ const fs = require('fs');
|
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
|
-
const { version } = require('../package.json');
|
|
6
|
-
const { audit } = require('./audit');
|
|
7
|
-
const { setup } = require('./setup');
|
|
8
|
-
const { analyzeProject } = require('./analyze');
|
|
9
|
-
const { getGovernanceSummary } = require('./governance');
|
|
5
|
+
const { version } = require('../package.json');
|
|
6
|
+
const { audit } = require('./audit');
|
|
7
|
+
const { setup } = require('./setup');
|
|
8
|
+
const { analyzeProject } = require('./analyze');
|
|
9
|
+
const { getGovernanceSummary } = require('./governance');
|
|
10
|
+
const { formatTerminologyLines } = require('./terminology');
|
|
10
11
|
|
|
11
12
|
function copyProject(sourceDir, targetDir) {
|
|
12
13
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
@@ -201,31 +202,36 @@ function buildCaseStudy(before, after, applyResult) {
|
|
|
201
202
|
};
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
function renderBenchmarkMarkdown(report) {
|
|
205
|
-
return [
|
|
206
|
-
'# NERVIQ CLI Benchmark Report',
|
|
207
|
-
'',
|
|
208
|
-
`- Generated by: ${report.generatedBy}`,
|
|
209
|
-
`- Created at: ${report.createdAt}`,
|
|
210
|
-
`- Source repo: ${report.directory}`,
|
|
211
|
-
'',
|
|
212
|
-
'##
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
'',
|
|
220
|
-
'##
|
|
221
|
-
`-
|
|
222
|
-
`- Organic score: ${report.
|
|
223
|
-
`- Passing checks: ${report.
|
|
224
|
-
'',
|
|
225
|
-
'##
|
|
226
|
-
`-
|
|
227
|
-
`-
|
|
228
|
-
`-
|
|
205
|
+
function renderBenchmarkMarkdown(report) {
|
|
206
|
+
return [
|
|
207
|
+
'# NERVIQ CLI Benchmark Report',
|
|
208
|
+
'',
|
|
209
|
+
`- Generated by: ${report.generatedBy}`,
|
|
210
|
+
`- Created at: ${report.createdAt}`,
|
|
211
|
+
`- Source repo: ${report.directory}`,
|
|
212
|
+
'',
|
|
213
|
+
'## Score Semantics',
|
|
214
|
+
`- Baseline live audit score: ${report.scoreSemantics.baseline}`,
|
|
215
|
+
`- Projected benchmark score: ${report.scoreSemantics.projected}`,
|
|
216
|
+
`- Organic score: ${report.scoreSemantics.organic}`,
|
|
217
|
+
'',
|
|
218
|
+
'## Methodology',
|
|
219
|
+
...report.methodology.map(item => `- ${item}`),
|
|
220
|
+
'',
|
|
221
|
+
'## Baseline (Live Repo)',
|
|
222
|
+
`- Live audit score: ${report.before.score}/100`,
|
|
223
|
+
`- Organic live score: ${report.before.organicScore}/100`,
|
|
224
|
+
`- Passing checks: ${report.before.passed}/${report.before.checkCount}`,
|
|
225
|
+
'',
|
|
226
|
+
'## Projected (Isolated Benchmark Copy)',
|
|
227
|
+
`- Projected benchmark score: ${report.after.score}/100`,
|
|
228
|
+
`- Projected organic score: ${report.after.organicScore}/100`,
|
|
229
|
+
`- Passing checks: ${report.after.passed}/${report.after.checkCount}`,
|
|
230
|
+
'',
|
|
231
|
+
'## Delta',
|
|
232
|
+
`- Projected score delta: ${report.delta.score}`,
|
|
233
|
+
`- Projected organic score delta: ${report.delta.organicScore}`,
|
|
234
|
+
`- Passed checks delta: ${report.delta.passed}`,
|
|
229
235
|
'',
|
|
230
236
|
'## Executive Summary',
|
|
231
237
|
`- ${report.executiveSummary.headline}`,
|
|
@@ -285,9 +291,14 @@ async function runBenchmark(options) {
|
|
|
285
291
|
schemaVersion: 1,
|
|
286
292
|
generatedBy: `nerviq@${version}`,
|
|
287
293
|
createdAt: new Date().toISOString(),
|
|
288
|
-
directory: sourceDir,
|
|
289
|
-
platform,
|
|
290
|
-
|
|
294
|
+
directory: sourceDir,
|
|
295
|
+
platform,
|
|
296
|
+
scoreSemantics: {
|
|
297
|
+
baseline: 'current repo state before benchmark runs',
|
|
298
|
+
projected: 'starter-safe post-setup score measured on an isolated temp copy',
|
|
299
|
+
organic: 'repo-owned config quality excluding starter-generated Nerviq assets',
|
|
300
|
+
},
|
|
301
|
+
methodology: [
|
|
291
302
|
'Run a baseline audit on the source repo.',
|
|
292
303
|
'Copy the repo into a temporary isolated workspace.',
|
|
293
304
|
`Apply starter-safe ${platform === 'codex' ? 'Codex' : 'Claude'} artifacts only on the isolated copy.`,
|
|
@@ -316,24 +327,29 @@ function printBenchmark(report, options = {}) {
|
|
|
316
327
|
return;
|
|
317
328
|
}
|
|
318
329
|
|
|
319
|
-
console.log('');
|
|
320
|
-
console.log(' nerviq benchmark');
|
|
321
|
-
console.log(' ═══════════════════════════════════════');
|
|
322
|
-
console.log(' Runs in an isolated temp copy. Your current repo is not modified.');
|
|
323
|
-
console.log('');
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
console.log(`
|
|
328
|
-
console.log(
|
|
329
|
-
console.log(
|
|
330
|
-
console.log(`
|
|
331
|
-
console.log(
|
|
332
|
-
console.log(
|
|
333
|
-
console.log(`
|
|
334
|
-
console.log(`
|
|
335
|
-
console.log(
|
|
336
|
-
|
|
330
|
+
console.log('');
|
|
331
|
+
console.log(' nerviq benchmark');
|
|
332
|
+
console.log(' ═══════════════════════════════════════');
|
|
333
|
+
console.log(' Runs in an isolated temp copy. Your current repo is not modified.');
|
|
334
|
+
console.log(' Score type: baseline = live repo audit, projected = isolated post-setup benchmark.');
|
|
335
|
+
console.log('');
|
|
336
|
+
const orgDeltaSign = report.delta.organicScore >= 0 ? '+' : '';
|
|
337
|
+
const totalDeltaSign = report.delta.score >= 0 ? '+' : '';
|
|
338
|
+
console.log(` Projected organic delta: \x1b[1m${orgDeltaSign}${report.delta.organicScore} points\x1b[0m (repo-owned config quality)`);
|
|
339
|
+
console.log(` Projected total delta with nerviq setup: ${totalDeltaSign}${report.delta.score} points`);
|
|
340
|
+
console.log('');
|
|
341
|
+
console.log(` Baseline live audit: organic ${report.before.organicScore}/100, total ${report.before.score}/100`);
|
|
342
|
+
console.log(` Projected after setup: organic ${report.after.organicScore}/100, total ${report.after.score}/100`);
|
|
343
|
+
console.log('');
|
|
344
|
+
console.log(` ${report.executiveSummary.headline}`);
|
|
345
|
+
console.log(` Recommendation: ${report.executiveSummary.decisionGuidance}`);
|
|
346
|
+
console.log(` Workflow evidence: ${report.workflowEvidence.summary.passed}/${report.workflowEvidence.summary.total} tasks (${report.workflowEvidence.summary.coverageScore}%)`);
|
|
347
|
+
console.log('');
|
|
348
|
+
for (const line of formatTerminologyLines(['governance', 'hooks', 'mcp'])) {
|
|
349
|
+
console.log(line);
|
|
350
|
+
}
|
|
351
|
+
console.log('');
|
|
352
|
+
}
|
|
337
353
|
|
|
338
354
|
function writeBenchmarkReport(report, outFile) {
|
|
339
355
|
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
package/src/dashboard.js
CHANGED
|
@@ -68,7 +68,7 @@ function buildScoreOverTimeSvg(history) {
|
|
|
68
68
|
|
|
69
69
|
return `
|
|
70
70
|
<div class="card">
|
|
71
|
-
<h2>Score Over Time</h2>
|
|
71
|
+
<h2>Audit Snapshot Score Over Time</h2>
|
|
72
72
|
<svg viewBox="0 0 ${w} ${h}" width="100%" style="max-width:${w}px">
|
|
73
73
|
${yLabels}
|
|
74
74
|
<polyline points="${polyline}" fill="none" stroke="${COLORS.blue}" stroke-width="2"/>
|
|
@@ -119,11 +119,28 @@ function buildCategoryBreakdownSvg(results) {
|
|
|
119
119
|
</div>`;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
function getDashboardScoreMeta(history) {
|
|
123
|
+
if (history && history.length > 0) {
|
|
124
|
+
return {
|
|
125
|
+
label: 'Latest audit snapshot score',
|
|
126
|
+
note: 'Dashboard is anchored to the most recent saved audit snapshot. Trend and drift sections use audit snapshots only.',
|
|
127
|
+
consoleSource: 'latest audit snapshot',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
label: 'Live audit score',
|
|
133
|
+
note: 'No saved audit snapshots found, so this dashboard ran a live audit of the current repo. Run `nerviq audit --snapshot` to build history.',
|
|
134
|
+
consoleSource: 'live audit (no snapshots yet)',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
122
138
|
function buildHtml(projectName, auditPayload, history) {
|
|
123
139
|
const score = auditPayload.score ?? 0;
|
|
124
140
|
const platform = auditPayload.platform || 'unknown';
|
|
125
141
|
const results = auditPayload.results || [];
|
|
126
142
|
const timestamp = new Date().toISOString();
|
|
143
|
+
const scoreMeta = getDashboardScoreMeta(history);
|
|
127
144
|
|
|
128
145
|
// Top 5 failed checks sorted by impact severity
|
|
129
146
|
const impactOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
@@ -184,7 +201,8 @@ function buildHtml(projectName, auditPayload, history) {
|
|
|
184
201
|
|
|
185
202
|
<div class="card score-card">
|
|
186
203
|
<div class="score-number" style="color:${scoreColor(score)}">${score}</div>
|
|
187
|
-
<div class="score-label"
|
|
204
|
+
<div class="score-label">${escapeHtml(scoreMeta.label)}</div>
|
|
205
|
+
<div style="color:${COLORS.textDim};font-size:.85rem;margin-top:.75rem;max-width:520px;margin-left:auto;margin-right:auto">${escapeHtml(scoreMeta.note)}</div>
|
|
188
206
|
</div>
|
|
189
207
|
|
|
190
208
|
<div class="card">
|
|
@@ -238,6 +256,7 @@ async function generateDashboard(dir, flags = {}) {
|
|
|
238
256
|
auditPayload = await audit({ dir, silent: true, platform: flags.platform || 'claude' });
|
|
239
257
|
}
|
|
240
258
|
|
|
259
|
+
const scoreMeta = getDashboardScoreMeta(history);
|
|
241
260
|
const html = buildHtml(projectName, auditPayload, history);
|
|
242
261
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
243
262
|
fs.writeFileSync(outputPath, html, 'utf8');
|
|
@@ -247,8 +266,9 @@ async function generateDashboard(dir, flags = {}) {
|
|
|
247
266
|
console.log('');
|
|
248
267
|
console.log(' nerviq dashboard');
|
|
249
268
|
console.log(' \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550');
|
|
250
|
-
console.log(`
|
|
251
|
-
console.log(`
|
|
269
|
+
console.log(` Dashboard score: ${auditPayload.score ?? '?'}/100`);
|
|
270
|
+
console.log(` Score source: ${scoreMeta.consoleSource}`);
|
|
271
|
+
console.log(` Audit snapshots: ${history.length}`);
|
|
252
272
|
console.log(` Output: ${relPath}`);
|
|
253
273
|
console.log('');
|
|
254
274
|
}
|
|
@@ -261,7 +281,7 @@ async function generateDashboard(dir, flags = {}) {
|
|
|
261
281
|
exec(cmd);
|
|
262
282
|
}
|
|
263
283
|
|
|
264
|
-
return { outputPath, relativePath: relPath, score: auditPayload.score };
|
|
284
|
+
return { outputPath, relativePath: relPath, score: auditPayload.score, scoreSource: scoreMeta.consoleSource };
|
|
265
285
|
}
|
|
266
286
|
|
|
267
287
|
/**
|
|
@@ -274,13 +294,15 @@ function detectDrifts(history, threshold = 5) {
|
|
|
274
294
|
for (let i = 0; i < history.length - 1; i++) {
|
|
275
295
|
const current = history[i];
|
|
276
296
|
const previous = history[i + 1];
|
|
277
|
-
|
|
278
|
-
|
|
297
|
+
const currentScore = current.summary?.score;
|
|
298
|
+
const previousScore = previous.summary?.score;
|
|
299
|
+
if (currentScore != null && previousScore != null) {
|
|
300
|
+
const delta = currentScore - previousScore;
|
|
279
301
|
if (Math.abs(delta) >= threshold) {
|
|
280
302
|
drifts.push({
|
|
281
|
-
date: current.date || current.timestamp,
|
|
282
|
-
from:
|
|
283
|
-
to:
|
|
303
|
+
date: current.createdAt || current.date || current.timestamp,
|
|
304
|
+
from: previousScore,
|
|
305
|
+
to: currentScore,
|
|
284
306
|
delta,
|
|
285
307
|
});
|
|
286
308
|
}
|
|
@@ -307,8 +329,8 @@ function buildDriftAlertsHtml(drifts) {
|
|
|
307
329
|
|
|
308
330
|
return `
|
|
309
331
|
<div style="margin-top:32px">
|
|
310
|
-
<h2 style="color:${COLORS.text};font-size:18px;margin-bottom:12px">⚠
|
|
311
|
-
<p style="color:${COLORS.textDim};font-size:13px;margin-bottom:12px">Changes of 5+ points between consecutive snapshots</p>
|
|
332
|
+
<h2 style="color:${COLORS.text};font-size:18px;margin-bottom:12px">⚠ Audit Snapshot Drift Alerts</h2>
|
|
333
|
+
<p style="color:${COLORS.textDim};font-size:13px;margin-bottom:12px">Changes of 5+ points between consecutive audit snapshots</p>
|
|
312
334
|
<table style="width:100%;border-collapse:collapse;background:${COLORS.surface};border-radius:8px;overflow:hidden">
|
|
313
335
|
<thead><tr style="background:${COLORS.border}">
|
|
314
336
|
<th style="padding:8px 12px;text-align:left;color:${COLORS.textDim};font-size:12px">Date</th>
|
|
@@ -375,7 +397,7 @@ function buildPortfolioHtml(repoResults) {
|
|
|
375
397
|
|
|
376
398
|
<div class="card score-card">
|
|
377
399
|
<div class="score-number" style="color:${scoreColor(avgScore)}">${avgScore}</div>
|
|
378
|
-
<div class="score-label">average score across ${repoResults.length} repos</div>
|
|
400
|
+
<div class="score-label">average live audit score across ${repoResults.length} repos</div>
|
|
379
401
|
</div>
|
|
380
402
|
|
|
381
403
|
<div class="highlights">
|
|
@@ -452,7 +474,7 @@ async function generatePortfolioDashboard(repoPaths, flags = {}) {
|
|
|
452
474
|
console.log(' nerviq portfolio dashboard');
|
|
453
475
|
console.log(' \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550');
|
|
454
476
|
console.log(` Repos: ${repoResults.length}`);
|
|
455
|
-
console.log(` Average score: ${avgScore}/100`);
|
|
477
|
+
console.log(` Average live audit score: ${avgScore}/100`);
|
|
456
478
|
console.log(` Output: ${outputPath}`);
|
|
457
479
|
console.log('');
|
|
458
480
|
}
|