@nerviq/cli 1.10.0 → 1.12.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.
Files changed (57) hide show
  1. package/README.md +176 -47
  2. package/bin/cli.js +842 -287
  3. package/package.json +2 -2
  4. package/src/activity.js +225 -59
  5. package/src/adoption-advisor.js +299 -0
  6. package/src/aider/freshness.js +28 -25
  7. package/src/aider/techniques.js +16 -11
  8. package/src/analyze.js +131 -1
  9. package/src/anti-patterns.js +17 -2
  10. package/src/audit.js +197 -96
  11. package/src/behavioral-drift.js +801 -0
  12. package/src/benchmark.js +15 -10
  13. package/src/continuous-ops.js +681 -0
  14. package/src/cost-tracking.js +61 -0
  15. package/src/cursor/techniques.js +17 -12
  16. package/src/deep-review.js +83 -0
  17. package/src/diff-only.js +280 -0
  18. package/src/doctor.js +118 -55
  19. package/src/governance.js +72 -50
  20. package/src/hook-validation.js +342 -0
  21. package/src/index.js +7 -1
  22. package/src/integrations.js +144 -60
  23. package/src/mcp-validation.js +337 -0
  24. package/src/opencode/techniques.js +12 -7
  25. package/src/operating-profile.js +574 -0
  26. package/src/org.js +97 -13
  27. package/src/permission-rules.js +218 -0
  28. package/src/plans.js +192 -8
  29. package/src/platform-change-manifest.js +86 -0
  30. package/src/policy-layers.js +210 -0
  31. package/src/profiles.js +4 -1
  32. package/src/prompt-injection.js +74 -0
  33. package/src/repo-archetype.js +386 -0
  34. package/src/secret-patterns.js +9 -0
  35. package/src/server.js +398 -3
  36. package/src/setup.js +36 -2
  37. package/src/source-urls.js +132 -132
  38. package/src/supplemental-checks.js +13 -12
  39. package/src/techniques/api.js +407 -0
  40. package/src/techniques/automation.js +316 -0
  41. package/src/techniques/compliance.js +257 -0
  42. package/src/techniques/hygiene.js +294 -0
  43. package/src/techniques/instructions.js +243 -0
  44. package/src/techniques/observability.js +226 -0
  45. package/src/techniques/optimization.js +142 -0
  46. package/src/techniques/quality.js +317 -0
  47. package/src/techniques/security.js +237 -0
  48. package/src/techniques/shared.js +443 -0
  49. package/src/techniques/stacks.js +2294 -0
  50. package/src/techniques/tools.js +106 -0
  51. package/src/techniques/workflow.js +413 -0
  52. package/src/techniques.js +78 -5611
  53. package/src/terminology.js +73 -0
  54. package/src/token-estimate.js +35 -0
  55. package/src/watch.js +18 -0
  56. package/src/windsurf/techniques.js +17 -12
  57. package/src/workspace.js +105 -8
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',
@@ -62,10 +64,11 @@ function formatLocation(file, line) {
62
64
  return line ? `${file}:${line}` : file;
63
65
  }
64
66
 
65
- const IMPACT_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
66
- 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;
67
+ const IMPACT_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
68
+ const WEIGHTS = { critical: 15, high: 10, medium: 5, low: 2 };
69
+ const SCORE_MILESTONES = [50, 70, 90, 100];
70
+ const LARGE_INSTRUCTION_WARN_TOKENS = 12000;
71
+ const LARGE_INSTRUCTION_SKIP_TOKENS = 240000;
69
72
  const CATEGORY_MODULES = {
70
73
  memory: 'CLAUDE.md',
71
74
  quality: 'verification',
@@ -358,9 +361,13 @@ function getAuditSpec(platform = 'claude') {
358
361
  };
359
362
  }
360
363
 
361
- function normalizeRelativePath(filePath) {
362
- return String(filePath || '').replace(/\\/g, '/').replace(/^\.\//, '');
363
- }
364
+ function normalizeRelativePath(filePath) {
365
+ return String(filePath || '').replace(/\\/g, '/').replace(/^\.\//, '');
366
+ }
367
+
368
+ function formatCount(value) {
369
+ return Number(value || 0).toLocaleString('en-US');
370
+ }
364
371
 
365
372
  function addPath(target, filePath) {
366
373
  if (!filePath || typeof filePath !== 'string') return;
@@ -454,25 +461,27 @@ function instructionFileCandidates(spec, ctx) {
454
461
  return [...candidates];
455
462
  }
456
463
 
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
- }
464
+ function inspectInstructionFiles(spec, ctx) {
465
+ const warnings = [];
466
+
467
+ for (const filePath of instructionFileCandidates(spec, ctx)) {
468
+ const content = typeof ctx.fileContent === 'function' ? ctx.fileContent(filePath) : null;
469
+ const byteCount = typeof ctx.fileSizeBytes === 'function' ? ctx.fileSizeBytes(filePath) : null;
470
+ const tokenCount = typeof content === 'string' ? estimateTokenCount(content) : null;
471
+ if (!Number.isFinite(tokenCount) || tokenCount <= LARGE_INSTRUCTION_WARN_TOKENS) continue;
472
+
473
+ warnings.push({
474
+ file: normalizeRelativePath(filePath),
475
+ byteCount,
476
+ tokenCount,
477
+ lineCount: typeof content === 'string' ? content.split(/\r?\n/).length : null,
478
+ skipped: tokenCount > LARGE_INSTRUCTION_SKIP_TOKENS,
479
+ severity: tokenCount > LARGE_INSTRUCTION_SKIP_TOKENS ? 'critical' : 'warning',
480
+ message: tokenCount > LARGE_INSTRUCTION_SKIP_TOKENS
481
+ ? 'Instruction file exceeds ~240,000 tokens and will be skipped during audit.'
482
+ : 'Instruction file exceeds ~12,000 tokens. Audit will continue, but this file may reduce runtime clarity.',
483
+ });
484
+ }
476
485
 
477
486
  return warnings;
478
487
  }
@@ -651,7 +660,7 @@ function getRecommendationPriorityScore(item, outcomeSummaryByKey = {}, fpFeedba
651
660
  return raw * getFpFeedbackMultiplier(fpFeedbackByKey, item.key);
652
661
  }
653
662
 
654
- function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, options = {}) {
663
+ function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, options = {}) {
655
664
  const pool = getPrioritizedFailed(failed);
656
665
  const fpByKey = options.fpFeedbackByKey || null;
657
666
 
@@ -716,11 +725,65 @@ function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, option
716
725
  avgScoreDelta: feedback.avgScoreDelta,
717
726
  } : null,
718
727
  });
719
- });
720
- }
721
-
722
- function computeCategoryScores(applicable, passed) {
723
- const grouped = {};
728
+ });
729
+ }
730
+
731
+ function getNextScoreMilestone(score) {
732
+ return SCORE_MILESTONES.find((milestone) => score < milestone) || null;
733
+ }
734
+
735
+ function buildScoreCoaching({ score, earnedPoints, maxPoints, failed, outcomeSummaryByKey = {}, platform, fpFeedbackByKey = null }) {
736
+ if (!Array.isArray(failed) || failed.length === 0 || !Number.isFinite(maxPoints) || maxPoints <= 0) {
737
+ return null;
738
+ }
739
+
740
+ const nextMilestone = getNextScoreMilestone(score);
741
+ if (!nextMilestone) return null;
742
+
743
+ const targetEarnedPoints = Math.ceil((nextMilestone / 100) * maxPoints);
744
+ const pointsNeeded = Math.max(0, targetEarnedPoints - earnedPoints);
745
+ if (pointsNeeded <= 0) return null;
746
+
747
+ const rankedActions = buildTopNextActions(failed, failed.length, outcomeSummaryByKey, { platform, fpFeedbackByKey });
748
+ if (rankedActions.length === 0) return null;
749
+
750
+ const failedByKey = new Map(failed.map((item) => [item.key, item]));
751
+ const selected = [];
752
+ let recoveredPoints = 0;
753
+
754
+ for (const action of rankedActions) {
755
+ const source = failedByKey.get(action.key);
756
+ if (!source) continue;
757
+ selected.push({
758
+ key: source.key,
759
+ name: source.name,
760
+ impact: source.impact,
761
+ weight: WEIGHTS[source.impact] || 0,
762
+ });
763
+ recoveredPoints += WEIGHTS[source.impact] || 0;
764
+ if (recoveredPoints >= pointsNeeded) break;
765
+ }
766
+
767
+ if (selected.length === 0) return null;
768
+
769
+ const fixesNeeded = selected.length;
770
+ const projectedScore = Math.round(((earnedPoints + recoveredPoints) / maxPoints) * 100);
771
+ const summary = `You're ${fixesNeeded} ${fixesNeeded === 1 ? 'fix' : 'fixes'} away from ${nextMilestone}/100.`;
772
+
773
+ return {
774
+ currentScore: score,
775
+ nextMilestone,
776
+ pointsNeeded,
777
+ fixesNeeded,
778
+ projectedScore: Math.min(100, projectedScore),
779
+ summary,
780
+ recommendedKeys: selected.map((item) => item.key),
781
+ recommendedNames: selected.map((item) => item.name),
782
+ };
783
+ }
784
+
785
+ function computeCategoryScores(applicable, passed) {
786
+ const grouped = {};
724
787
 
725
788
  for (const item of applicable) {
726
789
  const category = item.category || 'unknown';
@@ -907,6 +970,9 @@ function printLiteAudit(result, dir) {
907
970
  scoreExplanation = t('audit.early');
908
971
  }
909
972
  console.log(colorize(` ${scoreExplanation}`, 'dim'));
973
+ if (result.scoreCoaching) {
974
+ console.log(colorize(` Milestone: ${result.scoreCoaching.summary}`, 'magenta'));
975
+ }
910
976
  console.log(colorize(' Score type: live repo audit (current files only, not snapshot history or benchmark projection).', 'dim'));
911
977
 
912
978
  if (result.platformScopeNote) {
@@ -921,11 +987,11 @@ function printLiteAudit(result, dir) {
921
987
  console.log(colorize(` - ${item.title}: ${item.message}`, 'dim'));
922
988
  });
923
989
  }
924
- if (result.largeInstructionFiles && result.largeInstructionFiles.length > 0) {
925
- result.largeInstructionFiles.slice(0, 2).forEach((item) => {
926
- console.log(colorize(` Large file: ${item.file} (${Math.round(item.byteCount / 1024)}KB)`, 'yellow'));
927
- });
928
- }
990
+ if (result.largeInstructionFiles && result.largeInstructionFiles.length > 0) {
991
+ result.largeInstructionFiles.slice(0, 2).forEach((item) => {
992
+ console.log(colorize(` Large file: ${item.file} (~${formatCount(item.tokenCount)} tokens)`, 'yellow'));
993
+ });
994
+ }
929
995
  console.log('');
930
996
 
931
997
  if (result.failed === 0) {
@@ -957,15 +1023,23 @@ function printLiteAudit(result, dir) {
957
1023
  console.log('');
958
1024
  let usagePatterns;
959
1025
  try { usagePatterns = require('./usage-patterns'); } catch { usagePatterns = null; }
960
- result.liteSummary.topNextActions.forEach((item, index) => {
961
- const tier = item.impact === 'critical' ? '🔴' : item.impact === 'high' ? '🟡' : '🔵';
962
- const suppressed = usagePatterns && usagePatterns.getPriorityAdjustment(dir, item.key) === 'suppress';
963
- const suffix = suppressed ? colorize(' (suppressed)', 'dim') : '';
964
- console.log(` ${index + 1}. ${tier} ${colorize(item.name, 'bold')}${suffix}`);
965
- console.log(colorize(` ${item.fix}`, 'dim'));
966
- });
967
- console.log('');
968
- console.log(` Ready? Run: ${colorize(result.suggestedNextCommand, 'bold')}`);
1026
+ result.liteSummary.topNextActions.forEach((item, index) => {
1027
+ const tier = item.impact === 'critical' ? '🔴' : item.impact === 'high' ? '🟡' : '🔵';
1028
+ const suppressed = usagePatterns && usagePatterns.getPriorityAdjustment(dir, item.key) === 'suppress';
1029
+ const suffix = suppressed ? colorize(' (suppressed)', 'dim') : '';
1030
+ console.log(` ${index + 1}. ${tier} ${colorize(item.name, 'bold')}${suffix}`);
1031
+ console.log(colorize(` ${item.fix}`, 'dim'));
1032
+ });
1033
+ console.log('');
1034
+ const liteTerminology = formatTerminologyLines(collectAuditTerminology(result));
1035
+ if (liteTerminology.length > 0) {
1036
+ liteTerminology.forEach((line) => {
1037
+ const color = line.startsWith(' Terms used here:') ? 'blue' : 'dim';
1038
+ console.log(colorize(line, color));
1039
+ });
1040
+ console.log('');
1041
+ }
1042
+ console.log(` Ready? Run: ${colorize(result.suggestedNextCommand, 'bold')}`);
969
1043
  if (result.platform === 'codex') {
970
1044
  console.log(colorize(' Note: Codex now supports no-write advisory flows via augment and suggest-only before setup/apply.', 'dim'));
971
1045
  }
@@ -1054,12 +1128,12 @@ async function audit(options) {
1054
1128
  key: 'largeInstructionFile',
1055
1129
  id: null,
1056
1130
  name: 'Large instruction file warning',
1057
- category: 'performance',
1058
- impact: 'medium',
1059
- rating: null,
1060
- fix: 'Split oversized instruction files so they stay under 50KB, and keep any single instruction file below 1MB.',
1061
- sourceUrl: null,
1062
- confidence: 'high',
1131
+ category: 'performance',
1132
+ impact: 'medium',
1133
+ rating: null,
1134
+ fix: 'Split oversized instruction files so they stay under ~12,000 tokens, and keep any single instruction file below ~240,000 tokens.',
1135
+ sourceUrl: null,
1136
+ confidence: 'high',
1063
1137
  file: largeInstructionFiles[0].file,
1064
1138
  line: null,
1065
1139
  passed: null,
@@ -1127,12 +1201,13 @@ async function audit(options) {
1127
1201
  ...largeInstructionFiles.map((item) => ({
1128
1202
  kind: 'large-instruction-file',
1129
1203
  severity: item.severity,
1130
- message: item.message,
1131
- file: item.file,
1132
- lineCount: item.lineCount,
1133
- byteCount: item.byteCount,
1134
- skipped: item.skipped,
1135
- })),
1204
+ message: item.message,
1205
+ file: item.file,
1206
+ lineCount: item.lineCount,
1207
+ byteCount: item.byteCount,
1208
+ tokenCount: item.tokenCount,
1209
+ skipped: item.skipped,
1210
+ })),
1136
1211
  ...deprecationWarnings.map((item) => ({
1137
1212
  kind: 'deprecated-feature',
1138
1213
  severity: 'warning',
@@ -1142,7 +1217,7 @@ async function audit(options) {
1142
1217
  const recommendedDomainPacks = spec.platform === 'codex'
1143
1218
  ? detectCodexDomainPacks(ctx, stacks, getCodexDomainPackSignals(ctx))
1144
1219
  : [];
1145
- const result = {
1220
+ const result = {
1146
1221
  platform: spec.platform,
1147
1222
  platformLabel: spec.platformLabel,
1148
1223
  platformVersion: spec.platformVersion,
@@ -1165,9 +1240,18 @@ async function audit(options) {
1165
1240
  deprecatedReason: r.deprecatedReason || null,
1166
1241
  sunsetDate: r.sunsetDate || null,
1167
1242
  })),
1168
- categoryScores,
1169
- quickWins: quickWins.map(({ key, name, impact, fix, category, sourceUrl }) => ({ key, name, impact, category, fix, sourceUrl })),
1170
- topNextActions,
1243
+ categoryScores,
1244
+ scoreCoaching: buildScoreCoaching({
1245
+ score,
1246
+ earnedPoints: earnedScore,
1247
+ maxPoints: maxScore,
1248
+ failed,
1249
+ outcomeSummaryByKey: outcomeSummary.byKey,
1250
+ platform: spec.platform,
1251
+ fpFeedbackByKey: fpFeedback.byKey,
1252
+ }),
1253
+ quickWins: quickWins.map(({ key, name, impact, fix, category, sourceUrl }) => ({ key, name, impact, category, fix, sourceUrl })),
1254
+ topNextActions,
1171
1255
  recommendationOutcomes: {
1172
1256
  totalEntries: outcomeSummary.totalEntries,
1173
1257
  keysTracked: outcomeSummary.keys,
@@ -1197,11 +1281,12 @@ async function audit(options) {
1197
1281
  result.detectedConfigFiles = configFiles;
1198
1282
 
1199
1283
  result.suggestedNextCommand = inferSuggestedNextCommand(result);
1200
- result.liteSummary = {
1201
- topNextActions: topNextActions.slice(0, 3),
1202
- nextCommand: result.suggestedNextCommand,
1203
- platformCaveats: platformCaveats.slice(0, 2),
1204
- };
1284
+ result.liteSummary = {
1285
+ topNextActions: topNextActions.slice(0, 3),
1286
+ nextCommand: result.suggestedNextCommand,
1287
+ platformCaveats: platformCaveats.slice(0, 2),
1288
+ scoreCoaching: result.scoreCoaching,
1289
+ };
1205
1290
 
1206
1291
  // Silent mode: skip all output, just return result
1207
1292
  if (silent) {
@@ -1260,13 +1345,13 @@ async function audit(options) {
1260
1345
  console.log('');
1261
1346
  }
1262
1347
 
1263
- if (largeInstructionFiles.length > 0) {
1264
- console.log(colorize(' Large instruction files', 'yellow'));
1265
- for (const item of largeInstructionFiles) {
1266
- const sizeKb = Math.round(item.byteCount / 1024);
1267
- console.log(colorize(` ${item.file} (${sizeKb}KB, ${item.lineCount || '?'} lines)`, 'bold'));
1268
- console.log(colorize(` → ${item.message}`, 'dim'));
1269
- }
1348
+ if (largeInstructionFiles.length > 0) {
1349
+ console.log(colorize(' Large instruction files', 'yellow'));
1350
+ for (const item of largeInstructionFiles) {
1351
+ const sizeKb = Number.isFinite(item.byteCount) ? Math.round(item.byteCount / 1024) : '?';
1352
+ console.log(colorize(` ${item.file} (~${formatCount(item.tokenCount)} tokens, ${item.lineCount || '?'} lines, ${sizeKb}KB)`, 'bold'));
1353
+ console.log(colorize(` → ${item.message}`, 'dim'));
1354
+ }
1270
1355
  console.log('');
1271
1356
  }
1272
1357
 
@@ -1296,11 +1381,18 @@ async function audit(options) {
1296
1381
  console.log('');
1297
1382
 
1298
1383
  // Score
1299
- console.log(` ${progressBar(score)} ${colorize(`${score}/100`, 'bold')}`);
1300
- if (isScaffolded && scaffoldedPassed.length > 0) {
1301
- console.log(colorize(` Organic: ${organicScore}/100 (without nerviq generated files)`, 'dim'));
1302
- }
1303
- console.log('');
1384
+ console.log(` ${progressBar(score)} ${colorize(`${score}/100`, 'bold')}`);
1385
+ if (isScaffolded && scaffoldedPassed.length > 0) {
1386
+ console.log(colorize(` Organic: ${organicScore}/100 (without nerviq generated files)`, 'dim'));
1387
+ }
1388
+ if (result.scoreCoaching) {
1389
+ const fastestPath = result.scoreCoaching.recommendedNames.slice(0, 3).join(', ');
1390
+ console.log(colorize(` Milestone: ${result.scoreCoaching.summary}`, 'magenta'));
1391
+ if (fastestPath) {
1392
+ console.log(colorize(` Fastest path: ${fastestPath}`, 'dim'));
1393
+ }
1394
+ }
1395
+ console.log('');
1304
1396
 
1305
1397
  // Passed
1306
1398
  if (passed.length > 0) {
@@ -1366,8 +1458,8 @@ async function audit(options) {
1366
1458
  }
1367
1459
 
1368
1460
  // Top next actions
1369
- if (topNextActions.length > 0) {
1370
- console.log(colorize(' ⚡ Top 5 Next Actions', 'magenta'));
1461
+ if (topNextActions.length > 0) {
1462
+ console.log(colorize(' ⚡ Top 5 Next Actions', 'magenta'));
1371
1463
  for (let i = 0; i < topNextActions.length; i++) {
1372
1464
  const item = topNextActions[i];
1373
1465
  console.log(` ${i + 1}. ${colorize(item.name, 'bold')}`);
@@ -1383,11 +1475,20 @@ async function audit(options) {
1383
1475
  console.log(colorize(` Feedback: accepted ${item.feedback.accepted}, rejected ${item.feedback.rejected}, positive ${item.feedback.positive}, negative ${item.feedback.negative}${avgDelta}`, 'dim'));
1384
1476
  }
1385
1477
  console.log(colorize(` Fix: ${item.fix}`, 'dim'));
1386
- }
1387
- console.log('');
1388
- }
1389
-
1390
- // Summary
1478
+ }
1479
+ console.log('');
1480
+ }
1481
+
1482
+ const terminology = formatTerminologyLines(collectAuditTerminology(result));
1483
+ if (terminology.length > 0) {
1484
+ terminology.forEach((line) => {
1485
+ const color = line.startsWith(' Terms used here:') ? 'blue' : 'dim';
1486
+ console.log(colorize(line, color));
1487
+ });
1488
+ console.log('');
1489
+ }
1490
+
1491
+ // Summary
1391
1492
  console.log(colorize(' ─────────────────────────────────────', 'dim'));
1392
1493
  const deprecatedNote = deprecated.length > 0 ? colorize(`, ${deprecated.length} deprecated`, 'dim') : '';
1393
1494
  console.log(` ${colorize(`${passed.length}/${applicable.length}`, 'bold')} checks passing${skipped.length > 0 ? colorize(` (${skipped.length} not applicable${deprecatedNote})`, 'dim') : (deprecatedNote ? colorize(` (${deprecatedNote})`, 'dim') : '')}`);