@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/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 { loadPlugins, mergePluginChecks } = require('./plugins');
33
- const { hasWorkspaceConfig, detectWorkspaceGlobs, detectWorkspaces } = require('./workspace');
34
- const { detectDeprecationWarnings } = require('./deprecation');
35
- const { version: packageVersion } = require('../package.json');
36
- const { t } = require('./i18n');
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 LARGE_INSTRUCTION_WARN_BYTES = 50 * 1024;
68
- const LARGE_INSTRUCTION_SKIP_BYTES = 1024 * 1024;
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 byteCount = typeof ctx.fileSizeBytes === 'function' ? ctx.fileSizeBytes(filePath) : null;
462
- if (!Number.isFinite(byteCount) || byteCount <= LARGE_INSTRUCTION_WARN_BYTES) continue;
463
-
464
- const content = typeof ctx.fileContent === 'function' ? ctx.fileContent(filePath) : null;
465
- warnings.push({
466
- file: normalizeRelativePath(filePath),
467
- byteCount,
468
- lineCount: typeof content === 'string' ? content.split(/\r?\n/).length : null,
469
- skipped: byteCount > LARGE_INSTRUCTION_SKIP_BYTES,
470
- severity: byteCount > LARGE_INSTRUCTION_SKIP_BYTES ? 'critical' : 'warning',
471
- message: byteCount > LARGE_INSTRUCTION_SKIP_BYTES
472
- ? 'Instruction file exceeds 1MB and will be skipped during audit.'
473
- : 'Instruction file exceeds 50KB. Audit will continue, but this file may reduce runtime clarity.',
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
- if (result.platformScopeNote) {
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} (${Math.round(item.byteCount / 1024)}KB)`, 'yellow'));
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
- console.log(` Ready? Run: ${colorize(result.suggestedNextCommand, 'bold')}`);
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 50KB, and keep any single instruction file below 1MB.',
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
- skipped: item.skipped,
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} (${sizeKb}KB, ${item.lineCount || '?'} lines)`, 'bold'));
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
- // Summary
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') : '')}`);
@@ -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
- return { totalEvents, auditCount: auditSnapshots.length, suggestedRules, suggestedSuppressions, suggestedPriorities };
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 = [` Auto-Suggested Rules (based on ${sources.join(', ')}):`];
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('', ' No strong patterns detected yet. Keep using nerviq fix and audit to build history.');
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
- '## Methodology',
213
- ...report.methodology.map(item => `- ${item}`),
214
- '',
215
- '## Before',
216
- `- Score: ${report.before.score}/100`,
217
- `- Organic score: ${report.before.organicScore}/100`,
218
- `- Passing checks: ${report.before.passed}/${report.before.checkCount}`,
219
- '',
220
- '## After',
221
- `- Score: ${report.after.score}/100`,
222
- `- Organic score: ${report.after.organicScore}/100`,
223
- `- Passing checks: ${report.after.passed}/${report.after.checkCount}`,
224
- '',
225
- '## Delta',
226
- `- Score delta: ${report.delta.score}`,
227
- `- Organic score delta: ${report.delta.organicScore}`,
228
- `- Passed checks delta: ${report.delta.passed}`,
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
- methodology: [
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
- const orgDeltaSign = report.delta.organicScore >= 0 ? '+' : '';
325
- const totalDeltaSign = report.delta.score >= 0 ? '+' : '';
326
- console.log(` Organic improvement: \x1b[1m${orgDeltaSign}${report.delta.organicScore} points\x1b[0m (your actual config quality)`);
327
- console.log(` Total with nerviq setup: ${totalDeltaSign}${report.delta.score} points`);
328
- console.log('');
329
- console.log(` Before: organic ${report.before.organicScore}/100, total ${report.before.score}/100`);
330
- console.log(` After: organic ${report.after.organicScore}/100, total ${report.after.score}/100`);
331
- console.log('');
332
- console.log(` ${report.executiveSummary.headline}`);
333
- console.log(` Recommendation: ${report.executiveSummary.decisionGuidance}`);
334
- console.log(` Workflow evidence: ${report.workflowEvidence.summary.passed}/${report.workflowEvidence.summary.total} tasks (${report.workflowEvidence.summary.coverageScore}%)`);
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">out of 100</div>
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(` Score: ${auditPayload.score ?? '?'}/100`);
251
- console.log(` Snapshots: ${history.length}`);
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
- if (current.score != null && previous.score != null) {
278
- const delta = current.score - previous.score;
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: previous.score,
283
- to: current.score,
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">⚠ Score Drift Alerts</h2>
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
  }