@nerviq/cli 1.29.0 → 1.30.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 (93) hide show
  1. package/CHANGELOG.md +1764 -1493
  2. package/README.md +568 -538
  3. package/SECURITY.md +78 -82
  4. package/bin/cli.js +2838 -2558
  5. package/docs/api-reference.md +356 -356
  6. package/docs/audit-fix.md +109 -0
  7. package/docs/autofix.md +3 -62
  8. package/docs/getting-started.md +1 -1
  9. package/docs/index.html +592 -592
  10. package/docs/integration-contracts.md +287 -287
  11. package/docs/maintenance.md +128 -128
  12. package/docs/new-platform-guide.md +202 -202
  13. package/docs/release-process.md +63 -0
  14. package/docs/shallow-risk.md +244 -244
  15. package/docs/why-nerviq.md +82 -82
  16. package/package.json +75 -67
  17. package/sdk/README.md +12 -3
  18. package/sdk/examples/langchain-integration.md +128 -0
  19. package/sdk/examples/self-governing-agent.js +135 -0
  20. package/sdk/index.d.ts +115 -0
  21. package/sdk/index.js +94 -0
  22. package/sdk/package.json +11 -0
  23. package/src/activity.js +13 -0
  24. package/src/aider/activity.js +226 -226
  25. package/src/aider/context.js +162 -162
  26. package/src/aider/freshness.js +123 -123
  27. package/src/aider/techniques.js +3465 -3465
  28. package/src/audit/layers.js +180 -180
  29. package/src/audit.js +1133 -1032
  30. package/src/auto-suggest.js +9 -2
  31. package/src/behavioral-drift.js +37 -2
  32. package/src/benchmark.js +299 -299
  33. package/src/codex/activity.js +324 -324
  34. package/src/codex/freshness.js +149 -142
  35. package/src/codex/techniques.js +4895 -4895
  36. package/src/context.js +326 -326
  37. package/src/continuous-ops.js +11 -1
  38. package/src/convert.js +340 -340
  39. package/src/copilot/config-parser.js +280 -280
  40. package/src/copilot/context.js +218 -218
  41. package/src/copilot/freshness.js +184 -177
  42. package/src/copilot/patch.js +238 -238
  43. package/src/copilot/techniques.js +3578 -3578
  44. package/src/cursor/freshness.js +194 -194
  45. package/src/cursor/patch.js +243 -243
  46. package/src/cursor/techniques.js +3735 -3735
  47. package/src/doctor.js +201 -201
  48. package/src/fix-engine.js +511 -8
  49. package/src/formatters/csv.js +86 -86
  50. package/src/formatters/junit.js +123 -123
  51. package/src/formatters/markdown.js +164 -164
  52. package/src/formatters/otel.js +151 -151
  53. package/src/freshness.js +163 -156
  54. package/src/gemini/activity.js +402 -402
  55. package/src/gemini/context.js +290 -290
  56. package/src/gemini/freshness.js +188 -188
  57. package/src/gemini/patch.js +229 -229
  58. package/src/gemini/techniques.js +3811 -3811
  59. package/src/governance.js +533 -533
  60. package/src/harmony/audit.js +306 -306
  61. package/src/i18n.js +63 -63
  62. package/src/insights.js +119 -119
  63. package/src/integrations.js +134 -134
  64. package/src/locales/en.json +33 -33
  65. package/src/locales/es.json +33 -33
  66. package/src/migrate.js +354 -354
  67. package/src/opencode/activity.js +286 -286
  68. package/src/opencode/freshness.js +137 -137
  69. package/src/opencode/techniques.js +3450 -3450
  70. package/src/safe-glyph.js +97 -0
  71. package/src/setup/analysis.js +12 -12
  72. package/src/setup.js +13 -6
  73. package/src/shallow-risk/index.js +113 -56
  74. package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +51 -50
  75. package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +47 -46
  76. package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +47 -46
  77. package/src/shallow-risk/patterns/agent-config-framework-version-mismatch.js +138 -0
  78. package/src/shallow-risk/patterns/agent-config-missing-file.js +318 -317
  79. package/src/shallow-risk/patterns/agent-config-script-not-in-package-json.js +108 -0
  80. package/src/shallow-risk/patterns/agent-config-secret-literal.js +52 -49
  81. package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +35 -34
  82. package/src/shallow-risk/patterns/hook-script-missing.js +71 -70
  83. package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +53 -52
  84. package/src/shallow-risk/shared.js +653 -648
  85. package/src/source-urls.js +295 -295
  86. package/src/state-paths.js +85 -85
  87. package/src/supplemental-checks.js +805 -805
  88. package/src/telemetry.js +160 -160
  89. package/src/watch.js +46 -0
  90. package/src/windsurf/context.js +359 -359
  91. package/src/windsurf/freshness.js +194 -194
  92. package/src/windsurf/patch.js +231 -231
  93. package/src/windsurf/techniques.js +3779 -3779
package/src/audit.js CHANGED
@@ -1,1032 +1,1133 @@
1
- /**
2
- * Audit engine - evaluates project against NERVIQ technique database.
3
- */
4
-
5
- const { TECHNIQUES: CLAUDE_TECHNIQUES, STACKS, STACK_CATEGORY_DETECTORS } = require('./techniques');
6
- const { ProjectContext } = require('./context');
7
- const { CODEX_TECHNIQUES } = require('./codex/techniques');
8
- const { detectCodexDomainPacks } = require('./codex/domain-packs');
9
- const { CodexProjectContext, detectCodexVersion } = require('./codex/context');
10
- const { GEMINI_TECHNIQUES } = require('./gemini/techniques');
11
- const { detectGeminiDomainPacks } = require('./gemini/domain-packs');
12
- const { GeminiProjectContext, detectGeminiVersion } = require('./gemini/context');
13
- const { COPILOT_TECHNIQUES } = require('./copilot/techniques');
14
- const { detectCopilotDomainPacks } = require('./copilot/domain-packs');
15
- const { CopilotProjectContext } = require('./copilot/context');
16
- const { CURSOR_TECHNIQUES } = require('./cursor/techniques');
17
- const { detectCursorDomainPacks } = require('./cursor/domain-packs');
18
- const { CursorProjectContext } = require('./cursor/context');
19
- const { WINDSURF_TECHNIQUES } = require('./windsurf/techniques');
20
- const { WindsurfProjectContext } = require('./windsurf/context');
21
- const { AIDER_TECHNIQUES } = require('./aider/techniques');
22
- const { AiderProjectContext } = require('./aider/context');
23
- const { OPENCODE_TECHNIQUES } = require('./opencode/techniques');
24
- const { OpenCodeProjectContext } = require('./opencode/context');
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 { formatMarkdown } = require('./formatters/markdown');
32
- const { formatJUnit } = require('./formatters/junit');
33
- const { formatCsv } = require('./formatters/csv');
34
- const { collectAuditTerminology, formatTerminologyLines } = require('./terminology');
35
- const { loadPlugins, mergePluginChecks } = require('./plugins');
36
- const { detectDeprecationWarnings } = require('./deprecation');
37
- const { buildWorkspaceHint, formatCount, guardSkippedInstructionFiles, inspectInstructionFiles } = require('./audit/instruction-files');
38
- const { resolveEvidence } = require('./audit/evidence');
39
- const { LAYERS, summarizeLayers } = require('./audit/layers');
40
- const { runShallowRisk, SHALLOW_RISK_BANNER_LINES } = require('./shallow-risk');
41
- const {
42
- WEIGHTS,
43
- buildScoreCoaching,
44
- buildTopNextActions,
45
- confidenceLabel,
46
- computeCategoryScores,
47
- getFpFeedbackMultiplier,
48
- getQuickWins,
49
- getRecommendationPriorityScore,
50
- inferSuggestedNextCommand,
51
- } = require('./audit/recommendations');
52
- const { version: packageVersion } = require('../package.json');
53
- const { t } = require('./i18n');
54
-
55
- const COLORS = {
56
- reset: '\x1b[0m',
57
- bold: '\x1b[1m',
58
- dim: '\x1b[2m',
59
- red: '\x1b[31m',
60
- green: '\x1b[32m',
61
- yellow: '\x1b[33m',
62
- blue: '\x1b[36m',
63
- magenta: '\x1b[35m',
64
- };
65
-
66
- function colorize(text, color) {
67
- return `${COLORS[color] || ''}${text}${COLORS.reset}`;
68
- }
69
-
70
- function progressBar(score, max = 100, width = 20) {
71
- const filled = Math.round((score / max) * width);
72
- const empty = width - filled;
73
- const color = score >= 70 ? 'green' : score >= 40 ? 'yellow' : 'red';
74
- return colorize('█'.repeat(filled), color) + colorize('░'.repeat(empty), 'dim');
75
- }
76
-
77
- function formatLocation(file, line) {
78
- if (!file) return null;
79
- return line ? `${file}:${line}` : file;
80
- }
81
-
82
- function hasShallowRiskData(result) {
83
- return Boolean(result) && Object.prototype.hasOwnProperty.call(result, 'shallowRiskHints');
84
- }
85
-
86
- function printShallowRiskSection(result) {
87
- if (!hasShallowRiskData(result)) return;
88
-
89
- const hints = Array.isArray(result.shallowRiskHints) ? result.shallowRiskHints : [];
90
- console.log(colorize(' Shallow Risk Hints (experimental, opt-in)', 'yellow'));
91
- for (const line of SHALLOW_RISK_BANNER_LINES) {
92
- console.log(colorize(` ${line}`, 'dim'));
93
- }
94
- console.log('');
95
-
96
- if (hints.length === 0) {
97
- console.log(colorize(' No shallow-risk hints found.', 'green'));
98
- console.log('');
99
- return;
100
- }
101
-
102
- for (const hint of hints) {
103
- const severity = (hint.severity || 'medium').toUpperCase();
104
- console.log(` ${colorize(`[${severity}]`, 'bold')} ${hint.name}`);
105
- if (hint.file) {
106
- console.log(colorize(` at ${formatLocation(hint.file, hint.line)}`, 'dim'));
107
- }
108
- if (hint.fix) {
109
- console.log(colorize(` -> ${hint.fix}`, 'dim'));
110
- }
111
- }
112
- console.log('');
113
- }
114
-
115
- function printShallowRiskOnly(result, dir) {
116
- console.log('');
117
- console.log(colorize(' Nerviq Shallow Risk', 'bold'));
118
- console.log(colorize(' ═══════════════════════════════════════', 'dim'));
119
- console.log(colorize(` ${t('audit.scanning', { dir })}`, 'dim'));
120
- console.log('');
121
- if (result.detectedConfigFiles && result.detectedConfigFiles.length > 0) {
122
- console.log(colorize(` Found: ${result.detectedConfigFiles.join(', ')}`, 'dim'));
123
- console.log('');
124
- }
125
- printShallowRiskSection(result);
126
- console.log(` Next: ${colorize('nerviq audit --shallow-risk --full', 'bold')}`);
127
- console.log('');
128
- }
129
-
130
- function getAuditSpec(platform = 'claude') {
131
- if (platform === 'codex') {
132
- return {
133
- platform: 'codex',
134
- platformLabel: 'Codex',
135
- techniques: CODEX_TECHNIQUES,
136
- ContextClass: CodexProjectContext,
137
- platformVersion: detectCodexVersion(),
138
- };
139
- }
140
-
141
- if (platform === 'gemini') {
142
- return {
143
- platform: 'gemini',
144
- platformLabel: 'Gemini CLI',
145
- techniques: GEMINI_TECHNIQUES,
146
- ContextClass: GeminiProjectContext,
147
- platformVersion: detectGeminiVersion(),
148
- };
149
- }
150
-
151
- if (platform === 'copilot') {
152
- return {
153
- platform: 'copilot',
154
- platformLabel: 'GitHub Copilot',
155
- techniques: COPILOT_TECHNIQUES,
156
- ContextClass: CopilotProjectContext,
157
- platformVersion: null,
158
- };
159
- }
160
-
161
- if (platform === 'cursor') {
162
- return {
163
- platform: 'cursor',
164
- platformLabel: 'Cursor',
165
- techniques: CURSOR_TECHNIQUES,
166
- ContextClass: CursorProjectContext,
167
- platformVersion: null,
168
- };
169
- }
170
-
171
- if (platform === 'windsurf') {
172
- return {
173
- platform: 'windsurf',
174
- platformLabel: 'Windsurf',
175
- techniques: WINDSURF_TECHNIQUES,
176
- ContextClass: WindsurfProjectContext,
177
- platformVersion: null,
178
- };
179
- }
180
-
181
- if (platform === 'aider') {
182
- return {
183
- platform: 'aider',
184
- platformLabel: 'Aider',
185
- techniques: AIDER_TECHNIQUES,
186
- ContextClass: AiderProjectContext,
187
- platformVersion: null,
188
- };
189
- }
190
-
191
- if (platform === 'opencode') {
192
- return {
193
- platform: 'opencode',
194
- platformLabel: 'OpenCode',
195
- techniques: OPENCODE_TECHNIQUES,
196
- ContextClass: OpenCodeProjectContext,
197
- platformVersion: null,
198
- };
199
- }
200
-
201
- return {
202
- platform: 'claude',
203
- platformLabel: 'Claude',
204
- techniques: CLAUDE_TECHNIQUES,
205
- ContextClass: ProjectContext,
206
- platformVersion: null,
207
- };
208
- }
209
-
210
- function getPlatformScopeNote(spec, ctx) {
211
- if (spec.platform !== 'codex') {
212
- return null;
213
- }
214
-
215
- const hasClaudeSurface = Boolean(
216
- (typeof ctx.fileContent === 'function' && ctx.fileContent('CLAUDE.md')) ||
217
- (typeof ctx.hasDir === 'function' && ctx.hasDir('.claude'))
218
- );
219
-
220
- if (!hasClaudeSurface) {
221
- return null;
222
- }
223
-
224
- return {
225
- kind: 'codex-only-pass',
226
- message: 'This is a Codex-only pass. Claude Code surfaces were also detected and should be audited separately with `npx nerviq`.',
227
- };
228
- }
229
-
230
- function getPlatformCaveats(spec, ctx) {
231
- if (spec.platform !== 'codex') {
232
- return [];
233
- }
234
-
235
- const caveats = [];
236
- const hooksJson = typeof ctx.hooksJsonContent === 'function' ? (ctx.hooksJsonContent() || '') : '';
237
- const agentsContent = typeof ctx.agentsMdContent === 'function' ? (ctx.agentsMdContent() || '') : '';
238
- const hooksClaimed = Boolean(
239
- hooksJson ||
240
- (typeof ctx.hasDir === 'function' && ctx.hasDir('.codex/hooks')) ||
241
- /\bhooks?\b|\bSessionStart\b|\bPreToolUse\b|\bPostToolUse\b|\bUserPromptSubmit\b|\bStop\b/i.test(agentsContent)
242
- );
243
-
244
- if (process.platform === 'win32') {
245
- caveats.push({
246
- key: 'codex-windows-hooks',
247
- severity: hooksClaimed ? 'critical' : 'info',
248
- title: 'Codex hooks are not available on Windows',
249
- message: hooksClaimed
250
- ? 'This repo claims Codex hooks, but native Windows sessions do not execute them. Keep enforcement in rules, CI, or another documented fallback.'
251
- : 'Native Windows sessions do not execute Codex hooks. If you add hooks later, treat them as non-enforcing on Windows and keep critical enforcement in rules or CI.',
252
- file: hooksJson ? '.codex/hooks.json' : null,
253
- line: hooksJson ? 1 : null,
254
- });
255
- }
256
-
257
- const maxThreads = typeof ctx.configValue === 'function' ? ctx.configValue('agents.max_threads') : undefined;
258
- caveats.push({
259
- key: 'codex-max-threads-default',
260
- severity: typeof maxThreads === 'number' && maxThreads > 6 ? 'warning' : 'info',
261
- title: 'Codex agent thread concurrency defaults to 6 when unset',
262
- message: typeof maxThreads === 'number'
263
- ? `This repo sets agents.max_threads = ${maxThreads}. Codex defaults to 6 when unset, so any higher concurrency assumption should be validated in the runtime you actually use.`
264
- : 'Codex defaults agents.max_threads to 6 when unset. If your workflow depends on heavy parallel subagent usage, set it intentionally and validate the behavior in your real runtime.',
265
- file: typeof ctx.fileContent === 'function' && ctx.fileContent('.codex/config.toml') ? '.codex/config.toml' : null,
266
- line: typeof ctx.lineNumber === 'function' ? (ctx.lineNumber('.codex/config.toml', /\bagents\.max_threads\b|\bmax_threads\b/i) || null) : null,
267
- });
268
-
269
- return caveats;
270
- }
271
-
272
- function getCodexDomainPackSignals(ctx) {
273
- return {
274
- instructionPath: typeof ctx.agentsMdPath === 'function' ? ctx.agentsMdPath() : null,
275
- trust: {
276
- approvalPolicy: typeof ctx.configValue === 'function' ? (ctx.configValue('approval_policy') || null) : null,
277
- sandboxMode: typeof ctx.configValue === 'function' ? (ctx.configValue('sandbox_mode') || null) : null,
278
- isTrustedProject: typeof ctx.isProjectTrusted === 'function' ? ctx.isProjectTrusted() : false,
279
- },
280
- counts: {
281
- rules: typeof ctx.ruleFiles === 'function' ? ctx.ruleFiles().length : 0,
282
- workflows: typeof ctx.workflowFiles === 'function' ? ctx.workflowFiles().length : 0,
283
- mcpServers: typeof ctx.mcpServers === 'function' ? Object.keys(ctx.mcpServers() || {}).length : 0,
284
- },
285
- };
286
- }
287
-
288
- function printLiteAudit(result, dir) {
289
- console.log('');
290
- const productLabel = result.platform === 'codex' ? t('audit.codexQuickScan') : t('audit.quickScan');
291
- console.log(colorize(` ${productLabel}`, 'bold'));
292
- console.log(colorize(' ═══════════════════════════════════════', 'dim'));
293
- console.log(colorize(` ${t('audit.scanning', { dir })}`, 'dim'));
294
- console.log('');
295
- if (result.detectedConfigFiles && result.detectedConfigFiles.length > 0) {
296
- console.log(colorize(` Found: ${result.detectedConfigFiles.join(', ')}`, 'dim'));
297
- }
298
- console.log('');
299
- console.log(` ${t('audit.score', { score: colorize(`${result.score}/100`, 'bold'), passed: result.passed, total: result.passed + result.failed })}`);
300
-
301
- // Score explanation line (lite mode only)
302
- const _critCount = (result.results || []).filter(r => r.passed === false && r.impact === 'critical').length;
303
- const _highCount = (result.results || []).filter(r => r.passed === false && r.impact === 'high').length;
304
- let scoreExplanation;
305
- if (result.score >= 90) {
306
- scoreExplanation = t('audit.excellent');
307
- } else if (result.score >= 70) {
308
- scoreExplanation = t('audit.strong', { count: _critCount });
309
- } else if (result.score >= 50) {
310
- scoreExplanation = t('audit.good', { count: _critCount + _highCount });
311
- } else if (result.score >= 30) {
312
- // Find weakest category (most failures)
313
- const catFailures = {};
314
- (result.results || []).filter(r => r.passed === false).forEach(r => {
315
- const cat = r.category || 'unknown';
316
- catFailures[cat] = (catFailures[cat] || 0) + 1;
317
- });
318
- const weakestCategory = Object.keys(catFailures).sort((a, b) => catFailures[b] - catFailures[a])[0] || 'config';
319
- scoreExplanation = t('audit.basic', { category: weakestCategory });
320
- } else {
321
- scoreExplanation = t('audit.early');
322
- }
323
- console.log(colorize(` ${scoreExplanation}`, 'dim'));
324
- if (result.scoreCoaching) {
325
- console.log(colorize(` Milestone: ${result.scoreCoaching.summary}`, 'magenta'));
326
- }
327
- console.log(colorize(' Score type: live repo audit (current files only, not snapshot history or benchmark projection).', 'dim'));
328
-
329
- if (result.platformScopeNote) {
330
- console.log(colorize(` Scope: ${result.platformScopeNote.message}`, 'dim'));
331
- }
332
- if (result.workspaceHint && result.workspaceHint.workspaces.length > 0) {
333
- console.log(colorize(` Workspaces: ${result.workspaceHint.workspaces.join(', ')}`, 'dim'));
334
- }
335
- if (result.platformCaveats && result.platformCaveats.length > 0) {
336
- console.log(colorize(' Platform caveats:', 'yellow'));
337
- result.platformCaveats.slice(0, 2).forEach((item) => {
338
- console.log(colorize(` - ${item.title}: ${item.message}`, 'dim'));
339
- });
340
- }
341
- if (result.largeInstructionFiles && result.largeInstructionFiles.length > 0) {
342
- result.largeInstructionFiles.slice(0, 2).forEach((item) => {
343
- console.log(colorize(` Large file: ${item.file} (~${formatCount(item.tokenCount)} tokens)`, 'yellow'));
344
- });
345
- }
346
- console.log('');
347
-
348
- if (result.failed === 0) {
349
- const platformLabel = result.platform === 'codex' ? 'Codex' : 'Claude';
350
- console.log(colorize(` Your ${platformLabel} setup looks solid.`, 'green'));
351
- printShallowRiskSection(result);
352
- console.log(` Next: ${colorize(result.suggestedNextCommand, 'bold')}`);
353
- if (result.platform === 'codex') {
354
- console.log(colorize(' Note: Codex now supports no-write advisory flows via augment and suggest-only before setup/apply.', 'dim'));
355
- }
356
- console.log(colorize(' Star: github.com/nerviq/nerviq | Discord: discord.gg/nerviq', 'dim'));
357
- console.log('');
358
- return;
359
- }
360
-
361
- // Urgency summary line (only count actual failures, not skipped/null)
362
- const criticalCount = (result.results || []).filter(r => r.passed === false && r.impact === 'critical').length;
363
- const highCount = (result.results || []).filter(r => r.passed === false && r.impact === 'high').length;
364
- const mediumCount = result.failed - criticalCount - highCount;
365
- const urgencyParts = [];
366
- if (criticalCount > 0) urgencyParts.push(colorize(`🔴 ${criticalCount} critical`, 'red'));
367
- if (highCount > 0) urgencyParts.push(colorize(`🟡 ${highCount} high`, 'yellow'));
368
- if (mediumCount > 0) urgencyParts.push(colorize(`🔵 ${mediumCount} recommended`, 'blue'));
369
- if (urgencyParts.length > 0) {
370
- console.log(` ${urgencyParts.join(' ')}`);
371
- console.log('');
372
- }
373
-
374
- console.log(colorize(' Top 3 things to fix right now:', 'magenta'));
375
- console.log('');
376
- let usagePatterns;
377
- try { usagePatterns = require('./usage-patterns'); } catch { usagePatterns = null; }
378
- result.liteSummary.topNextActions.forEach((item, index) => {
379
- const tier = item.impact === 'critical' ? '🔴' : item.impact === 'high' ? '🟡' : '🔵';
380
- const suppressed = usagePatterns && usagePatterns.getPriorityAdjustment(dir, item.key) === 'suppress';
381
- const suffix = suppressed ? colorize(' (suppressed)', 'dim') : '';
382
- console.log(` ${index + 1}. ${tier} ${colorize(item.name, 'bold')}${suffix}`);
383
- console.log(colorize(` ${item.fix}`, 'dim'));
384
- });
385
- console.log('');
386
- printShallowRiskSection(result);
387
- const liteTerminology = formatTerminologyLines(collectAuditTerminology(result));
388
- if (liteTerminology.length > 0) {
389
- liteTerminology.forEach((line) => {
390
- const color = line.startsWith(' Terms used here:') ? 'blue' : 'dim';
391
- console.log(colorize(line, color));
392
- });
393
- console.log('');
394
- }
395
- console.log(` Ready? Run: ${colorize(result.suggestedNextCommand, 'bold')}`);
396
- if (result.platform === 'codex') {
397
- console.log(colorize(' Note: Codex now supports no-write advisory flows via augment and suggest-only before setup/apply.', 'dim'));
398
- }
399
- console.log(colorize(` See all ${result.failed} failed checks: ${colorize('nerviq audit --full', 'bold')}`, 'dim'));
400
- console.log(colorize(' Star: github.com/nerviq/nerviq | Discord: discord.gg/nerviq', 'dim'));
401
- console.log('');
402
- }
403
-
404
- /**
405
- * Run a full audit of a project's Claude Code setup against the NERVIQ technique database.
406
- * @param {Object} options - Audit options.
407
- * @param {string} options.dir - Project directory to audit.
408
- * @param {boolean} [options.silent] - Skip all console output, return result only.
409
- * @param {boolean} [options.json] - Output result as JSON.
410
- * @param {boolean} [options.lite] - Show short top-3 quick scan.
411
- * @param {boolean} [options.verbose] - Show all recommendations including medium-impact.
412
- * @param {boolean} [options.showDeprecated] - Include deprecated checks in output.
413
- * @returns {Promise<Object>} Audit result with score, passed/failed counts, quickWins, and topNextActions.
414
- */
415
- async function audit(options) {
416
- const spec = getAuditSpec(options.platform || 'claude');
417
- const silent = options.silent || false;
418
- const ctx = new spec.ContextClass(options.dir);
419
- const shallowRiskEnabled = Boolean(options.shallowRisk) && process.env.NERVIQ_SHALLOW_RISK !== 'off';
420
- const shallowRiskOnly = Boolean(options.shallowRiskOnly) && shallowRiskEnabled;
421
- const largeInstructionFiles = shallowRiskOnly ? [] : inspectInstructionFiles(spec, ctx);
422
- if (!shallowRiskOnly) {
423
- guardSkippedInstructionFiles(ctx, largeInstructionFiles);
424
- }
425
- const stacks = ctx.detectStacks(STACKS);
426
- const results = [];
427
- const outcomeSummary = getRecommendationOutcomeSummary(options.dir);
428
- const fpFeedback = getFeedbackSummary(options.dir);
429
- const workspaceHint = buildWorkspaceHint(options.dir);
430
-
431
- // Load and merge plugin checks
432
- const plugins = loadPlugins(options.dir);
433
- const techniques = plugins.length > 0
434
- ? mergePluginChecks(spec.techniques, plugins)
435
- : spec.techniques;
436
-
437
- // Pre-compute which stack categories are active for this project
438
- const activeStackCategories = new Set();
439
- for (const [category, detector] of Object.entries(STACK_CATEGORY_DETECTORS)) {
440
- if (detector(ctx)) activeStackCategories.add(category);
441
- }
442
-
443
- // Generic quality categories that are NOT about AI agent configuration.
444
- // These are only included with --verbose or --full --verbose (deep quality mode).
445
- const GENERIC_QUALITY_CATEGORIES = new Set([
446
- 'observability', 'accessibility', 'i18n', 'privacy', 'error-tracking',
447
- 'supply-chain', 'api-versioning', 'caching', 'rate-limiting', 'feature-flags',
448
- 'docs-quality', 'monorepo', 'performance-budget', 'realtime', 'graphql',
449
- 'testing-strategy', 'code-quality', 'api-design', 'database', 'authentication',
450
- 'monitoring', 'dependency-management', 'cost-optimization', 'devops',
451
- ]);
452
- const includeGenericQuality = options.verbose;
453
-
454
- // Run all technique checks
455
- if (!shallowRiskOnly) {
456
- for (const [key, technique] of Object.entries(techniques)) {
457
- // Skip entire stack category if the stack is not detected at a core location
458
- // Skip generic quality categories unless --verbose is set
459
- const cat = technique.category;
460
- if ((!includeGenericQuality && GENERIC_QUALITY_CATEGORIES.has(cat)) ||
461
- (STACK_CATEGORY_DETECTORS[cat] && !activeStackCategories.has(cat))) {
462
- results.push({
463
- key,
464
- ...technique,
465
- file: null,
466
- line: null,
467
- passed: null, // not applicable
468
- });
469
- continue;
470
- }
471
-
472
- const passed = technique.check(ctx);
473
- let file = typeof technique.file === 'function' ? (technique.file(ctx) ?? null) : (technique.file ?? null);
474
- let line = typeof technique.line === 'function' ? (technique.line(ctx) ?? null) : (technique.line ?? null);
475
- let snippet = null;
476
- // CTO-04: only compute evidence on failed checks (cheap, and only where it adds trust).
477
- if (passed === false) {
478
- const evidence = resolveEvidence(key, ctx, { file, line });
479
- if (evidence) {
480
- file = evidence.file;
481
- line = evidence.line;
482
- snippet = evidence.snippet;
483
- }
484
- }
485
- results.push({
486
- key,
487
- ...technique,
488
- file,
489
- line: Number.isFinite(line) ? line : null,
490
- snippet,
491
- passed,
492
- });
493
- }
494
- }
495
-
496
- if (!shallowRiskOnly && largeInstructionFiles.length > 0) {
497
- results.push({
498
- key: 'largeInstructionFile',
499
- id: null,
500
- name: 'Large instruction file warning',
501
- category: 'performance',
502
- layer: LAYERS.GOVERNANCE,
503
- impact: 'medium',
504
- rating: null,
505
- fix: 'Split oversized instruction files so they stay under ~12,000 tokens, and keep any single instruction file below ~240,000 tokens.',
506
- sourceUrl: null,
507
- confidence: 'high',
508
- file: largeInstructionFiles[0].file,
509
- line: null,
510
- passed: null,
511
- details: largeInstructionFiles,
512
- });
513
- }
514
-
515
- // Separate deprecated checks from active checks.
516
- // Deprecated checks are excluded from scoring but preserved for display.
517
- const deprecated = results.filter(r => r.deprecated === true);
518
- const activeResults = results.filter(r => r.deprecated !== true);
519
-
520
- // null = not applicable (skip), true = pass, false = fail
521
- const applicable = activeResults.filter(r => r.passed !== null);
522
- const skipped = activeResults.filter(r => r.passed === null);
523
- const passed = applicable.filter(r => r.passed);
524
- const failed = applicable.filter(r => !r.passed);
525
- const critical = failed.filter(r => r.impact === 'critical');
526
- const high = failed.filter(r => r.impact === 'high');
527
- const medium = failed.filter(r => r.impact === 'medium');
528
-
529
- // Calculate score only from applicable checks
530
- const maxScore = applicable.reduce((sum, r) => sum + (WEIGHTS[r.impact] || 5), 0);
531
- const earnedScore = passed.reduce((sum, r) => sum + (WEIGHTS[r.impact] || 5), 0);
532
- const score = maxScore > 0 ? Math.round((earnedScore / maxScore) * 100) : 0;
533
-
534
- // Detect scaffolded vs organic: if CLAUDE.md contains our version stamp, some checks
535
- // are passing because WE generated them, not the user
536
- const instructionSource = spec.platform === 'codex'
537
- ? (ctx.agentsMdContent ? (ctx.agentsMdContent() || '') : '')
538
- : (ctx.claudeMdContent() || '');
539
- const isScaffolded = instructionSource.includes('Generated by nerviq') ||
540
- instructionSource.includes('nerviq');
541
- // Scaffolded checks: things our setup creates (CLAUDE.md / AGENTS.md, hooks, commands, agents, rules, skills)
542
- const scaffoldedKeys = spec.platform === 'codex'
543
- ? new Set([
544
- 'codexAgentsMd',
545
- 'codexAgentsMdSubstantive',
546
- 'codexAgentsVerificationCommands',
547
- 'codexAgentsArchitecture',
548
- 'codexConfigExists',
549
- 'codexModelExplicit',
550
- 'codexReasoningEffortExplicit',
551
- 'codexWeakModelExplicit',
552
- 'codexSandboxModeExplicit',
553
- 'codexApprovalPolicyExplicit',
554
- 'codexFullAutoErrorModeExplicit',
555
- 'codexHistorySendToServerExplicit',
556
- ])
557
- : new Set(['claudeMd', 'mermaidArchitecture', 'verificationLoop',
558
- 'hooks', 'customCommands', 'multipleCommands', 'agents', 'pathRules', 'multipleRules',
559
- 'skills', 'hooksConfigured', 'preToolUseHook', 'postToolUseHook', 'fewShotExamples',
560
- 'constraintBlocks', 'xmlTags']);
561
- const organicPassed = passed.filter(r => !scaffoldedKeys.has(r.key));
562
- const scaffoldedPassed = passed.filter(r => scaffoldedKeys.has(r.key));
563
- const organicEarned = organicPassed.reduce((sum, r) => sum + (WEIGHTS[r.impact] || 5), 0);
564
- const organicScore = maxScore > 0 ? Math.round((organicEarned / maxScore) * 100) : 0;
565
- const quickWins = shallowRiskOnly ? [] : getQuickWins(failed, { platform: spec.platform });
566
- const topNextActions = shallowRiskOnly
567
- ? []
568
- : buildTopNextActions(failed, 5, outcomeSummary.byKey, { platform: spec.platform, fpFeedbackByKey: fpFeedback.byKey });
569
-
570
- // CTO-04: enrich top actions with file/line/snippet from the corresponding
571
- // result record (evidence was resolved above during the check loop).
572
- // CTO-05: project score-after-fix per action.
573
- const resultByKey = new Map(results.map((r) => [r.key, r]));
574
- for (const action of topNextActions) {
575
- const source = resultByKey.get(action.key);
576
- if (source) {
577
- if (source.file && !action.file) action.file = source.file;
578
- if (source.line && !action.line) action.line = source.line;
579
- if (source.snippet) action.snippet = source.snippet;
580
- }
581
- // Projected score delta: if this single failed check flipped to passed.
582
- const weight = WEIGHTS[action.impact] || 0;
583
- if (maxScore > 0 && weight > 0) {
584
- const projectedScoreAfter = Math.round(((earnedScore + weight) / maxScore) * 100);
585
- action.projectedScoreDelta = projectedScoreAfter - score;
586
- action.projectedScoreAfter = projectedScoreAfter;
587
- const isScaffolded = scaffoldedKeys.has(action.key);
588
- const projectedOrganicAfter = isScaffolded
589
- ? organicScore
590
- : Math.round(((organicEarned + weight) / maxScore) * 100);
591
- action.projectedOrganicScoreDelta = projectedOrganicAfter - organicScore;
592
- } else {
593
- action.projectedScoreDelta = 0;
594
- action.projectedScoreAfter = score;
595
- action.projectedOrganicScoreDelta = 0;
596
- }
597
- }
598
- const categoryScores = shallowRiskOnly ? {} : computeCategoryScores(applicable, passed);
599
- const platformScopeNote = shallowRiskOnly ? null : getPlatformScopeNote(spec, ctx);
600
- const platformCaveats = shallowRiskOnly ? [] : getPlatformCaveats(spec, ctx);
601
- const deprecationWarnings = shallowRiskOnly ? [] : detectDeprecationWarnings(failed, packageVersion);
602
- const warnings = [
603
- ...largeInstructionFiles.map((item) => ({
604
- kind: 'large-instruction-file',
605
- severity: item.severity,
606
- message: item.message,
607
- file: item.file,
608
- lineCount: item.lineCount,
609
- byteCount: item.byteCount,
610
- tokenCount: item.tokenCount,
611
- skipped: item.skipped,
612
- })),
613
- ...deprecationWarnings.map((item) => ({
614
- kind: 'deprecated-feature',
615
- severity: 'warning',
616
- ...item,
617
- })),
618
- ];
619
- const recommendedDomainPacks = !shallowRiskOnly && spec.platform === 'codex'
620
- ? detectCodexDomainPacks(ctx, stacks, getCodexDomainPackSignals(ctx))
621
- : [];
622
-
623
- // FB-05: framework-aware fix rewriting don't recommend `npm test` on a
624
- // Python/Go/Rust-only repo. Only rewrites when Node/JS stacks are absent.
625
- const stackKeys = new Set(stacks.map(s => s.key));
626
- const hasNodeStack = stackKeys.has('node') || stackKeys.has('react') || stackKeys.has('vue') ||
627
- stackKeys.has('nextjs') || stackKeys.has('angular') || stackKeys.has('svelte') ||
628
- stackKeys.has('nestjs') || stackKeys.has('remix') || stackKeys.has('astro') ||
629
- stackKeys.has('typescript') || stackKeys.has('deno') || stackKeys.has('bun');
630
- if (!shallowRiskOnly && !hasNodeStack) {
631
- let preferredTest = null;
632
- let preferredInstall = null;
633
- if (stackKeys.has('python') || stackKeys.has('django') || stackKeys.has('fastapi')) {
634
- preferredTest = 'pytest'; preferredInstall = 'pip install -r requirements.txt';
635
- } else if (stackKeys.has('go')) {
636
- preferredTest = 'go test ./...'; preferredInstall = 'go mod download';
637
- } else if (stackKeys.has('rust')) {
638
- preferredTest = 'cargo test'; preferredInstall = 'cargo fetch';
639
- } else if (stackKeys.has('ruby')) {
640
- preferredTest = 'bundle exec rspec'; preferredInstall = 'bundle install';
641
- } else if (stackKeys.has('java') || stackKeys.has('kotlin')) {
642
- preferredTest = './gradlew test'; preferredInstall = './gradlew build';
643
- } else if (stackKeys.has('elixir')) {
644
- preferredTest = 'mix test'; preferredInstall = 'mix deps.get';
645
- } else if (stackKeys.has('dotnet')) {
646
- preferredTest = 'dotnet test'; preferredInstall = 'dotnet restore';
647
- }
648
- if (preferredTest) {
649
- for (const r of results) {
650
- if (typeof r.fix !== 'string') continue;
651
- if (/\bnpm\s+test\b/i.test(r.fix)) r.fix = r.fix.replace(/`npm\s+test`/gi, '`' + preferredTest + '`').replace(/\bnpm\s+test\b/gi, preferredTest);
652
- if (/\bnpm\s+ci\b/i.test(r.fix) && preferredInstall) r.fix = r.fix.replace(/`npm\s+ci`/gi, '`' + preferredInstall + '`').replace(/\bnpm\s+ci\b/gi, preferredInstall);
653
- if (/\bnpm\s+install\b/i.test(r.fix) && preferredInstall) r.fix = r.fix.replace(/`npm\s+install`/gi, '`' + preferredInstall + '`').replace(/\bnpm\s+install\b/gi, preferredInstall);
654
- }
655
- }
656
- }
657
-
658
- const result = {
659
- platform: spec.platform,
660
- platformLabel: spec.platformLabel,
661
- platformVersion: spec.platformVersion,
662
- score,
663
- organicScore,
664
- earnedPoints: earnedScore,
665
- maxPoints: maxScore,
666
- isScaffolded,
667
- passed: passed.length,
668
- failed: failed.length,
669
- skipped: skipped.length,
670
- deprecated: deprecated.length,
671
- checkCount: applicable.length,
672
- stacks,
673
- results,
674
- deprecatedChecks: deprecated.map(r => ({
675
- key: r.key,
676
- name: r.name,
677
- category: r.category,
678
- deprecatedReason: r.deprecatedReason || null,
679
- sunsetDate: r.sunsetDate || null,
680
- })),
681
- categoryScores,
682
- scoreCoaching: shallowRiskOnly ? null : buildScoreCoaching({
683
- score,
684
- earnedPoints: earnedScore,
685
- maxPoints: maxScore,
686
- failed,
687
- outcomeSummaryByKey: outcomeSummary.byKey,
688
- platform: spec.platform,
689
- fpFeedbackByKey: fpFeedback.byKey,
690
- }),
691
- quickWins: quickWins.map(({ key, name, impact, fix, category, sourceUrl }) => ({ key, name, impact, category, fix, sourceUrl })),
692
- topNextActions,
693
- recommendationOutcomes: {
694
- totalEntries: outcomeSummary.totalEntries,
695
- keysTracked: outcomeSummary.keys,
696
- },
697
- largeInstructionFiles,
698
- deprecationWarnings,
699
- warnings,
700
- workspaceHint,
701
- platformScopeNote,
702
- platformCaveats,
703
- recommendedDomainPacks,
704
- // CTO-08: per-layer coverage summary (governance/drift/hygiene/shallow-risk).
705
- layerSummary: summarizeLayers(activeResults),
706
- };
707
- if (shallowRiskEnabled) {
708
- result.shallowRiskHints = runShallowRisk(ctx);
709
- }
710
- // Detect which AI config files are present
711
- const configFiles = [];
712
- const configChecks = [
713
- ['CLAUDE.md', 'CLAUDE.md'], ['.claude/settings.json', '.claude/settings.json'],
714
- ['AGENTS.md', 'AGENTS.md'], ['.cursorrules', '.cursorrules'],
715
- ['.cursor/rules', '.cursor/rules/'], ['GEMINI.md', 'GEMINI.md'],
716
- ['.windsurfrules', '.windsurfrules'], ['.aider.conf.yml', '.aider.conf.yml'],
717
- ['opencode.json', 'opencode.json'], ['.mcp.json', '.mcp.json'],
718
- ];
719
- for (const [file, label] of configChecks) {
720
- try {
721
- if (require('fs').existsSync(require('path').join(options.dir, file))) configFiles.push(label);
722
- } catch {}
723
- }
724
- result.detectedConfigFiles = configFiles;
725
-
726
- result.suggestedNextCommand = shallowRiskOnly
727
- ? 'nerviq audit --shallow-risk --full'
728
- : inferSuggestedNextCommand(result);
729
- result.liteSummary = {
730
- topNextActions: topNextActions.slice(0, 3),
731
- nextCommand: result.suggestedNextCommand,
732
- platformCaveats: platformCaveats.slice(0, 2),
733
- scoreCoaching: result.scoreCoaching,
734
- };
735
-
736
- // Silent mode: skip all output, just return result
737
- if (silent) {
738
- return result;
739
- }
740
-
741
- if (options.json) {
742
- console.log(JSON.stringify({
743
- version: packageVersion,
744
- timestamp: new Date().toISOString(),
745
- ...result
746
- }, null, 2));
747
- return result;
748
- }
749
-
750
- if (options.format === 'sarif') {
751
- console.log(JSON.stringify(formatSarif(result, { dir: options.dir }), null, 2));
752
- return result;
753
- }
754
-
755
- if (options.format === 'otel') {
756
- console.log(JSON.stringify(formatOtelMetrics(result), null, 2));
757
- return result;
758
- }
759
-
760
- if (options.format === 'markdown') {
761
- const enriched = { version: packageVersion, timestamp: new Date().toISOString(), ...result };
762
- console.log(formatMarkdown(enriched, { dir: options.dir }));
763
- return result;
764
- }
765
-
766
- if (options.format === 'junit') {
767
- const enriched = { version: packageVersion, timestamp: new Date().toISOString(), ...result };
768
- console.log(formatJUnit(enriched));
769
- return result;
770
- }
771
-
772
- if (options.format === 'csv') {
773
- console.log(formatCsv(result));
774
- return result;
775
- }
776
-
777
- if (shallowRiskOnly) {
778
- printShallowRiskOnly(result, options.dir);
779
- return result;
780
- }
781
-
782
- if (options.lite) {
783
- printLiteAudit(result, options.dir);
784
- sendInsights(result);
785
- return result;
786
- }
787
-
788
- // Display results
789
- console.log('');
790
- const auditTitle = spec.platform === 'codex' ? t('audit.codexTitle') : t('audit.title');
791
- console.log(colorize(` ${auditTitle}`, 'bold'));
792
- console.log(colorize(' ═══════════════════════════════════════', 'dim'));
793
- console.log(colorize(` ${t('audit.scanning', { dir: options.dir })}`, 'dim'));
794
- if (spec.platformVersion) {
795
- console.log(colorize(` Platform: ${spec.platformLabel} (${spec.platformVersion})`, 'blue'));
796
- }
797
- if (spec.platform === 'codex' && recommendedDomainPacks.length > 0) {
798
- console.log(colorize(` Domain packs: ${recommendedDomainPacks.map((pack) => pack.label).join(', ')}`, 'dim'));
799
- }
800
- if (platformScopeNote) {
801
- console.log(colorize(` Scope: ${platformScopeNote.message}`, 'dim'));
802
- }
803
- if (platformCaveats.length > 0) {
804
- console.log(colorize(' Platform caveats', 'yellow'));
805
- for (const caveat of platformCaveats) {
806
- console.log(colorize(` ${caveat.title}`, 'bold'));
807
- console.log(colorize(` → ${caveat.message}`, 'dim'));
808
- if (caveat.file) {
809
- console.log(colorize(` at ${formatLocation(caveat.file, caveat.line)}`, 'dim'));
810
- }
811
- }
812
- console.log('');
813
- }
814
-
815
- if (largeInstructionFiles.length > 0) {
816
- console.log(colorize(' Large instruction files', 'yellow'));
817
- for (const item of largeInstructionFiles) {
818
- const sizeKb = Number.isFinite(item.byteCount) ? Math.round(item.byteCount / 1024) : '?';
819
- console.log(colorize(` ${item.file} (~${formatCount(item.tokenCount)} tokens, ${item.lineCount || '?'} lines, ${sizeKb}KB)`, 'bold'));
820
- console.log(colorize(` → ${item.message}`, 'dim'));
821
- }
822
- console.log('');
823
- }
824
-
825
- if (deprecationWarnings.length > 0) {
826
- console.log(colorize(' Deprecated feature warnings', 'yellow'));
827
- for (const item of deprecationWarnings) {
828
- console.log(colorize(` ${item.feature}`, 'bold'));
829
- console.log(colorize(` → ${item.message}`, 'dim'));
830
- console.log(colorize(` Alternative: ${item.alternative}`, 'dim'));
831
- }
832
- console.log('');
833
- }
834
-
835
- if (workspaceHint && !options.workspace) {
836
- console.log(colorize(' Monorepo detected', 'blue'));
837
- if (workspaceHint.workspaces.length > 0) {
838
- console.log(colorize(` Workspaces: ${workspaceHint.workspaces.join(', ')}`, 'dim'));
839
- }
840
- console.log(colorize(` Tip: ${workspaceHint.suggestedCommand}`, 'dim'));
841
- console.log('');
842
- }
843
-
844
- if (stacks.length > 0) {
845
- console.log(colorize(` Detected: ${stacks.map(s => s.label).join(', ')}`, 'blue'));
846
- }
847
-
848
- console.log('');
849
-
850
- // Score
851
- console.log(` ${progressBar(score)} ${colorize(`${score}/100`, 'bold')}`);
852
- if (isScaffolded && scaffoldedPassed.length > 0) {
853
- console.log(colorize(` Organic: ${organicScore}/100 (without nerviq generated files)`, 'dim'));
854
- }
855
- if (result.scoreCoaching) {
856
- const fastestPath = result.scoreCoaching.recommendedNames.slice(0, 3).join(', ');
857
- console.log(colorize(` Milestone: ${result.scoreCoaching.summary}`, 'magenta'));
858
- if (fastestPath) {
859
- console.log(colorize(` Fastest path: ${fastestPath}`, 'dim'));
860
- }
861
- }
862
- console.log('');
863
-
864
- // CTO-08: Coverage by layer — explicit map of what NERVIQ covers.
865
- const layerSummary = result.layerSummary || summarizeLayers(activeResults);
866
- console.log(colorize(' Coverage by layer:', 'bold'));
867
- const layerOrder = [LAYERS.GOVERNANCE, LAYERS.DRIFT, LAYERS.HYGIENE, LAYERS.SHALLOW_RISK];
868
- for (const layer of layerOrder) {
869
- const b = layerSummary[layer] || { total: 0, passed: 0, failed: 0, skipped: 0 };
870
- const layerNote = layer === LAYERS.SHALLOW_RISK ? ' (parallel, opt-in, not scored)' : '';
871
- console.log(colorize(` ${layer}: ${b.total} checks (${b.passed} passed, ${b.failed} failed)${layerNote}`, 'dim'));
872
- }
873
- console.log('');
874
-
875
- // Passed
876
- if (passed.length > 0) {
877
- console.log(colorize(' ✅ Passing', 'green'));
878
- for (const r of passed) {
879
- console.log(colorize(` ${r.name}`, 'dim'));
880
- }
881
- console.log('');
882
- }
883
-
884
- // Deprecated checks (shown with --show-deprecated or --full)
885
- if (deprecated.length > 0 && (options.showDeprecated || options.full)) {
886
- console.log(colorize(` ⏳ Deprecated (${deprecated.length} checks excluded from scoring)`, 'dim'));
887
- for (const r of deprecated) {
888
- const reason = r.deprecatedReason ? ` — ${r.deprecatedReason}` : '';
889
- const sunset = r.sunsetDate ? ` (sunset: ${r.sunsetDate})` : '';
890
- console.log(colorize(` [DEPRECATED] ${r.name}${reason}${sunset}`, 'dim'));
891
- }
892
- console.log('');
893
- }
894
-
895
- // Failed - by priority
896
- if (critical.length > 0) {
897
- console.log(colorize(' 🔴 Critical (fix immediately)', 'red'));
898
- for (const r of critical) {
899
- const conf = r.confidence ? ` [${confidenceLabel(r.confidence)}]` : '';
900
- const layerPrefix = r.layer ? colorize(`[${r.layer}] `, 'dim') : '';
901
- console.log(` ${layerPrefix}${colorize(r.name, 'bold')}${colorize(conf, 'dim')}`);
902
- if (r.file) {
903
- console.log(colorize(` at ${formatLocation(r.file, r.line)}`, 'dim'));
904
- }
905
- console.log(colorize(` → ${r.fix}`, 'dim'));
906
- }
907
- console.log('');
908
- }
909
-
910
- if (high.length > 0) {
911
- console.log(colorize(' 🟡 High Impact', 'yellow'));
912
- for (const r of high) {
913
- const conf = r.confidence ? ` [${confidenceLabel(r.confidence)}]` : '';
914
- const layerPrefix = r.layer ? colorize(`[${r.layer}] `, 'dim') : '';
915
- console.log(` ${layerPrefix}${colorize(r.name, 'bold')}${colorize(conf, 'dim')}`);
916
- if (r.file) {
917
- console.log(colorize(` at ${formatLocation(r.file, r.line)}`, 'dim'));
918
- }
919
- console.log(colorize(` → ${r.fix}`, 'dim'));
920
- }
921
- console.log('');
922
- }
923
-
924
- if (medium.length > 0 && options.verbose) {
925
- console.log(colorize(' 🔵 Recommended', 'blue'));
926
- for (const r of medium) {
927
- const conf = r.confidence ? ` [${confidenceLabel(r.confidence)}]` : '';
928
- const layerPrefix = r.layer ? colorize(`[${r.layer}] `, 'dim') : '';
929
- console.log(` ${layerPrefix}${colorize(r.name, 'bold')}${colorize(conf, 'dim')}`);
930
- if (r.file) {
931
- console.log(colorize(` at ${formatLocation(r.file, r.line)}`, 'dim'));
932
- }
933
- console.log(colorize(` → ${r.fix}`, 'dim'));
934
- }
935
- console.log('');
936
- } else if (medium.length > 0) {
937
- console.log(colorize(` 🔵 ${medium.length} more recommendations (use --verbose)`, 'blue'));
938
- console.log('');
939
- }
940
-
941
- // Top next actions
942
- if (topNextActions.length > 0) {
943
- console.log(colorize(' ⚡ Top 5 Next Actions', 'magenta'));
944
- for (let i = 0; i < topNextActions.length; i++) {
945
- const item = topNextActions[i];
946
- const delta = Number.isFinite(item.projectedScoreDelta) && item.projectedScoreDelta > 0
947
- ? colorize(` (+${item.projectedScoreDelta} pts → ${item.projectedScoreAfter}/100)`, 'green')
948
- : '';
949
- console.log(` ${i + 1}. ${colorize(item.name, 'bold')}${delta}`);
950
- console.log(colorize(` Why: ${item.why}`, 'dim'));
951
- console.log(colorize(` Trace: ${item.signals.join(' | ')}`, 'dim'));
952
- console.log(colorize(` Risk: ${item.risk} | Confidence: ${item.confidence}`, 'dim'));
953
- const sourceResult = result.results.find(r => r.key === item.key);
954
- if (sourceResult && sourceResult.file) {
955
- console.log(colorize(` Evidence: ${formatLocation(sourceResult.file, sourceResult.line)}`, 'dim'));
956
- }
957
- if (item.feedback) {
958
- const avgDelta = Number.isFinite(item.feedback.avgScoreDelta) ? ` | Avg score delta: ${item.feedback.avgScoreDelta >= 0 ? '+' : ''}${item.feedback.avgScoreDelta}` : '';
959
- console.log(colorize(` Feedback: accepted ${item.feedback.accepted}, rejected ${item.feedback.rejected}, positive ${item.feedback.positive}, negative ${item.feedback.negative}${avgDelta}`, 'dim'));
960
- }
961
- console.log(colorize(` Fix: ${item.fix}`, 'dim'));
962
- }
963
- console.log('');
964
- }
965
-
966
- printShallowRiskSection(result);
967
-
968
- const terminology = formatTerminologyLines(collectAuditTerminology(result));
969
- if (terminology.length > 0) {
970
- terminology.forEach((line) => {
971
- const color = line.startsWith(' Terms used here:') ? 'blue' : 'dim';
972
- console.log(colorize(line, color));
973
- });
974
- console.log('');
975
- }
976
-
977
- // Summary
978
- console.log(colorize(' ─────────────────────────────────────', 'dim'));
979
- const deprecatedNote = deprecated.length > 0 ? colorize(`, ${deprecated.length} deprecated`, 'dim') : '';
980
- console.log(` ${colorize(`${passed.length}/${applicable.length}`, 'bold')} checks passing${skipped.length > 0 ? colorize(` (${skipped.length} not applicable${deprecatedNote})`, 'dim') : (deprecatedNote ? colorize(` (${deprecatedNote})`, 'dim') : '')}`);
981
-
982
- if (failed.length > 0) {
983
- console.log(` Next command: ${colorize(result.suggestedNextCommand, 'bold')}`);
984
- if (result.platform === 'codex') {
985
- console.log(colorize(' Codex now supports advisory no-write flows through augment and suggest-only before setup/apply.', 'dim'));
986
- }
987
- }
988
-
989
- console.log('');
990
- console.log(` Add to README: ${getBadgeMarkdown(score)}`);
991
- console.log('');
992
-
993
- // Weakest categories insight
994
- const insights = getLocalInsights({ score, results });
995
- if (insights.weakest.length > 0) {
996
- console.log(colorize(' Weakest areas:', 'dim'));
997
- for (const w of insights.weakest) {
998
- const bar = w.score === 0 ? colorize('none', 'red') : `${w.score}%`;
999
- console.log(colorize(` ${w.name}: ${bar} (${w.passed}/${w.total})`, 'dim'));
1000
- }
1001
- console.log('');
1002
- }
1003
-
1004
- // Cross-platform synergy hint
1005
- try {
1006
- const { detectActivePlatforms } = require('./harmony/canon');
1007
- const { analyzeCompensation } = require('./synergy/compensation');
1008
- const { calculateSynergyScore } = require('./synergy/ranking');
1009
- const detected = detectActivePlatforms(options.dir);
1010
- const activePlatforms = (detected || []).filter(p => p.detected).map(p => p.platform);
1011
- if (activePlatforms.length >= 2) {
1012
- const comp = analyzeCompensation(activePlatforms);
1013
- const synergyScore = calculateSynergyScore(activePlatforms);
1014
- console.log(colorize(` Cross-platform synergy [EXPERIMENTAL]: ${activePlatforms.length} platforms detected`, 'blue'));
1015
- console.log(colorize(` Platforms: ${activePlatforms.join(', ')}`, 'dim'));
1016
- console.log(colorize(` Compensations: ${comp.compensations.length} | Gaps: ${comp.uncoveredGaps.length}`, 'dim'));
1017
- console.log(colorize(` Run: npx nerviq harmony-audit for full cross-platform analysis`, 'dim'));
1018
- console.log('');
1019
- }
1020
- } catch { /* synergy display is optional */ }
1021
-
1022
- console.log(colorize(` Backed by NERVIQ research and evidence for ${spec.platformLabel}`, 'dim'));
1023
- console.log(colorize(' https://github.com/nerviq/nerviq', 'dim'));
1024
- console.log('');
1025
-
1026
- // Send anonymous insights (opt-in, privacy-first, fire-and-forget)
1027
- sendInsights(result);
1028
-
1029
- return result;
1030
- }
1031
-
1032
- module.exports = { audit, buildTopNextActions, getFpFeedbackMultiplier, getRecommendationPriorityScore };
1
+ /**
2
+ * Audit engine - evaluates project against NERVIQ technique database.
3
+ */
4
+
5
+ const { TECHNIQUES: CLAUDE_TECHNIQUES, STACKS, STACK_CATEGORY_DETECTORS } = require('./techniques');
6
+ const { ProjectContext } = require('./context');
7
+ const { CODEX_TECHNIQUES } = require('./codex/techniques');
8
+ const { detectCodexDomainPacks } = require('./codex/domain-packs');
9
+ const { CodexProjectContext, detectCodexVersion } = require('./codex/context');
10
+ const { GEMINI_TECHNIQUES } = require('./gemini/techniques');
11
+ const { detectGeminiDomainPacks } = require('./gemini/domain-packs');
12
+ const { GeminiProjectContext, detectGeminiVersion } = require('./gemini/context');
13
+ const { COPILOT_TECHNIQUES } = require('./copilot/techniques');
14
+ const { detectCopilotDomainPacks } = require('./copilot/domain-packs');
15
+ const { CopilotProjectContext } = require('./copilot/context');
16
+ const { CURSOR_TECHNIQUES } = require('./cursor/techniques');
17
+ const { detectCursorDomainPacks } = require('./cursor/domain-packs');
18
+ const { CursorProjectContext } = require('./cursor/context');
19
+ const { WINDSURF_TECHNIQUES } = require('./windsurf/techniques');
20
+ const { WindsurfProjectContext } = require('./windsurf/context');
21
+ const { AIDER_TECHNIQUES } = require('./aider/techniques');
22
+ const { AiderProjectContext } = require('./aider/context');
23
+ const { OPENCODE_TECHNIQUES } = require('./opencode/techniques');
24
+ const { OpenCodeProjectContext } = require('./opencode/context');
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 { formatMarkdown } = require('./formatters/markdown');
32
+ const { formatJUnit } = require('./formatters/junit');
33
+ const { formatCsv } = require('./formatters/csv');
34
+ const { collectAuditTerminology, formatTerminologyLines } = require('./terminology');
35
+ const { loadPlugins, mergePluginChecks } = require('./plugins');
36
+ const { detectDeprecationWarnings } = require('./deprecation');
37
+ const { buildWorkspaceHint, formatCount, guardSkippedInstructionFiles, inspectInstructionFiles } = require('./audit/instruction-files');
38
+ const { resolveEvidence } = require('./audit/evidence');
39
+ const { LAYERS, summarizeLayers } = require('./audit/layers');
40
+ const { runShallowRisk, SHALLOW_RISK_BANNER_LINES } = require('./shallow-risk');
41
+ const {
42
+ WEIGHTS,
43
+ buildScoreCoaching,
44
+ buildTopNextActions,
45
+ confidenceLabel,
46
+ computeCategoryScores,
47
+ getFpFeedbackMultiplier,
48
+ getQuickWins,
49
+ getRecommendationPriorityScore,
50
+ inferSuggestedNextCommand,
51
+ } = require('./audit/recommendations');
52
+ const { version: packageVersion } = require('../package.json');
53
+ const { t } = require('./i18n');
54
+
55
+ const COLORS = {
56
+ reset: '\x1b[0m',
57
+ bold: '\x1b[1m',
58
+ dim: '\x1b[2m',
59
+ red: '\x1b[31m',
60
+ green: '\x1b[32m',
61
+ yellow: '\x1b[33m',
62
+ blue: '\x1b[36m',
63
+ magenta: '\x1b[35m',
64
+ };
65
+
66
+ // MEMO-16: route every CLI string through safe-glyph before colorize so
67
+ // Windows consoles without UTF-8 codepage 65001 get ASCII-safe fallbacks
68
+ // (`[OK]`, `[X]`, `[!]`, etc.) instead of mojibake. No-op on UTF-8 capable
69
+ // terminals (modern macOS / Linux / Windows Terminal / VS Code / WSL).
70
+ const { safeText } = require('./safe-glyph');
71
+
72
+ function colorize(text, color) {
73
+ return `${COLORS[color] || ''}${safeText(text)}${COLORS.reset}`;
74
+ }
75
+
76
+ function progressBar(score, max = 100, width = 20) {
77
+ const filled = Math.round((score / max) * width);
78
+ const empty = width - filled;
79
+ const color = score >= 70 ? 'green' : score >= 40 ? 'yellow' : 'red';
80
+ return colorize('█'.repeat(filled), color) + colorize('░'.repeat(empty), 'dim');
81
+ }
82
+
83
+ function formatLocation(file, line) {
84
+ if (!file) return null;
85
+ return line ? `${file}:${line}` : file;
86
+ }
87
+
88
+ function hasShallowRiskData(result) {
89
+ return Boolean(result) && Object.prototype.hasOwnProperty.call(result, 'shallowRiskHints');
90
+ }
91
+
92
+ function printShallowRiskSection(result) {
93
+ if (!hasShallowRiskData(result)) return;
94
+
95
+ const hints = Array.isArray(result.shallowRiskHints) ? result.shallowRiskHints : [];
96
+ console.log(colorize(' Shallow Risk Hints (experimental, opt-in)', 'yellow'));
97
+ for (const line of SHALLOW_RISK_BANNER_LINES) {
98
+ console.log(colorize(` ${line}`, 'dim'));
99
+ }
100
+ console.log('');
101
+
102
+ if (hints.length === 0) {
103
+ console.log(colorize(' No shallow-risk hints found.', 'green'));
104
+ console.log('');
105
+ return;
106
+ }
107
+
108
+ for (const hint of hints) {
109
+ const severity = (hint.severity || 'medium').toUpperCase();
110
+ console.log(` ${colorize(`[${severity}]`, 'bold')} ${hint.name}`);
111
+ if (hint.file) {
112
+ console.log(colorize(` at ${formatLocation(hint.file, hint.line)}`, 'dim'));
113
+ }
114
+ if (hint.fix) {
115
+ console.log(colorize(` -> ${hint.fix}`, 'dim'));
116
+ }
117
+ }
118
+ console.log('');
119
+ }
120
+
121
+ function printShallowRiskOnly(result, dir) {
122
+ console.log('');
123
+ console.log(colorize(' Nerviq Shallow Risk', 'bold'));
124
+ console.log(colorize(' ═══════════════════════════════════════', 'dim'));
125
+ console.log(colorize(` ${t('audit.scanning', { dir })}`, 'dim'));
126
+ console.log('');
127
+ if (result.detectedConfigFiles && result.detectedConfigFiles.length > 0) {
128
+ console.log(colorize(` Found: ${result.detectedConfigFiles.join(', ')}`, 'dim'));
129
+ console.log('');
130
+ }
131
+ printShallowRiskSection(result);
132
+ console.log(` Next: ${colorize('nerviq audit --shallow-risk --full', 'bold')}`);
133
+ console.log('');
134
+ }
135
+
136
+ function getAuditSpec(platform = 'claude') {
137
+ if (platform === 'codex') {
138
+ return {
139
+ platform: 'codex',
140
+ platformLabel: 'Codex',
141
+ techniques: CODEX_TECHNIQUES,
142
+ ContextClass: CodexProjectContext,
143
+ platformVersion: detectCodexVersion(),
144
+ };
145
+ }
146
+
147
+ if (platform === 'gemini') {
148
+ return {
149
+ platform: 'gemini',
150
+ platformLabel: 'Gemini CLI',
151
+ techniques: GEMINI_TECHNIQUES,
152
+ ContextClass: GeminiProjectContext,
153
+ platformVersion: detectGeminiVersion(),
154
+ };
155
+ }
156
+
157
+ if (platform === 'copilot') {
158
+ return {
159
+ platform: 'copilot',
160
+ platformLabel: 'GitHub Copilot',
161
+ techniques: COPILOT_TECHNIQUES,
162
+ ContextClass: CopilotProjectContext,
163
+ platformVersion: null,
164
+ };
165
+ }
166
+
167
+ if (platform === 'cursor') {
168
+ return {
169
+ platform: 'cursor',
170
+ platformLabel: 'Cursor',
171
+ techniques: CURSOR_TECHNIQUES,
172
+ ContextClass: CursorProjectContext,
173
+ platformVersion: null,
174
+ };
175
+ }
176
+
177
+ if (platform === 'windsurf') {
178
+ return {
179
+ platform: 'windsurf',
180
+ platformLabel: 'Windsurf',
181
+ techniques: WINDSURF_TECHNIQUES,
182
+ ContextClass: WindsurfProjectContext,
183
+ platformVersion: null,
184
+ };
185
+ }
186
+
187
+ if (platform === 'aider') {
188
+ return {
189
+ platform: 'aider',
190
+ platformLabel: 'Aider',
191
+ techniques: AIDER_TECHNIQUES,
192
+ ContextClass: AiderProjectContext,
193
+ platformVersion: null,
194
+ };
195
+ }
196
+
197
+ if (platform === 'opencode') {
198
+ return {
199
+ platform: 'opencode',
200
+ platformLabel: 'OpenCode',
201
+ techniques: OPENCODE_TECHNIQUES,
202
+ ContextClass: OpenCodeProjectContext,
203
+ platformVersion: null,
204
+ };
205
+ }
206
+
207
+ return {
208
+ platform: 'claude',
209
+ platformLabel: 'Claude',
210
+ techniques: CLAUDE_TECHNIQUES,
211
+ ContextClass: ProjectContext,
212
+ platformVersion: null,
213
+ };
214
+ }
215
+
216
+ function getPlatformScopeNote(spec, ctx) {
217
+ if (spec.platform !== 'codex') {
218
+ return null;
219
+ }
220
+
221
+ const hasClaudeSurface = Boolean(
222
+ (typeof ctx.fileContent === 'function' && ctx.fileContent('CLAUDE.md')) ||
223
+ (typeof ctx.hasDir === 'function' && ctx.hasDir('.claude'))
224
+ );
225
+
226
+ if (!hasClaudeSurface) {
227
+ return null;
228
+ }
229
+
230
+ return {
231
+ kind: 'codex-only-pass',
232
+ message: 'This is a Codex-only pass. Claude Code surfaces were also detected and should be audited separately with `npx nerviq`.',
233
+ };
234
+ }
235
+
236
+ function getPlatformCaveats(spec, ctx) {
237
+ if (spec.platform !== 'codex') {
238
+ return [];
239
+ }
240
+
241
+ const caveats = [];
242
+ const hooksJson = typeof ctx.hooksJsonContent === 'function' ? (ctx.hooksJsonContent() || '') : '';
243
+ const agentsContent = typeof ctx.agentsMdContent === 'function' ? (ctx.agentsMdContent() || '') : '';
244
+ const hooksClaimed = Boolean(
245
+ hooksJson ||
246
+ (typeof ctx.hasDir === 'function' && ctx.hasDir('.codex/hooks')) ||
247
+ /\bhooks?\b|\bSessionStart\b|\bPreToolUse\b|\bPostToolUse\b|\bUserPromptSubmit\b|\bStop\b/i.test(agentsContent)
248
+ );
249
+
250
+ if (process.platform === 'win32') {
251
+ caveats.push({
252
+ key: 'codex-windows-hooks',
253
+ severity: hooksClaimed ? 'critical' : 'info',
254
+ title: 'Codex hooks are not available on Windows',
255
+ message: hooksClaimed
256
+ ? 'This repo claims Codex hooks, but native Windows sessions do not execute them. Keep enforcement in rules, CI, or another documented fallback.'
257
+ : 'Native Windows sessions do not execute Codex hooks. If you add hooks later, treat them as non-enforcing on Windows and keep critical enforcement in rules or CI.',
258
+ file: hooksJson ? '.codex/hooks.json' : null,
259
+ line: hooksJson ? 1 : null,
260
+ });
261
+ }
262
+
263
+ const maxThreads = typeof ctx.configValue === 'function' ? ctx.configValue('agents.max_threads') : undefined;
264
+ caveats.push({
265
+ key: 'codex-max-threads-default',
266
+ severity: typeof maxThreads === 'number' && maxThreads > 6 ? 'warning' : 'info',
267
+ title: 'Codex agent thread concurrency defaults to 6 when unset',
268
+ message: typeof maxThreads === 'number'
269
+ ? `This repo sets agents.max_threads = ${maxThreads}. Codex defaults to 6 when unset, so any higher concurrency assumption should be validated in the runtime you actually use.`
270
+ : 'Codex defaults agents.max_threads to 6 when unset. If your workflow depends on heavy parallel subagent usage, set it intentionally and validate the behavior in your real runtime.',
271
+ file: typeof ctx.fileContent === 'function' && ctx.fileContent('.codex/config.toml') ? '.codex/config.toml' : null,
272
+ line: typeof ctx.lineNumber === 'function' ? (ctx.lineNumber('.codex/config.toml', /\bagents\.max_threads\b|\bmax_threads\b/i) || null) : null,
273
+ });
274
+
275
+ return caveats;
276
+ }
277
+
278
+ function getCodexDomainPackSignals(ctx) {
279
+ return {
280
+ instructionPath: typeof ctx.agentsMdPath === 'function' ? ctx.agentsMdPath() : null,
281
+ trust: {
282
+ approvalPolicy: typeof ctx.configValue === 'function' ? (ctx.configValue('approval_policy') || null) : null,
283
+ sandboxMode: typeof ctx.configValue === 'function' ? (ctx.configValue('sandbox_mode') || null) : null,
284
+ isTrustedProject: typeof ctx.isProjectTrusted === 'function' ? ctx.isProjectTrusted() : false,
285
+ },
286
+ counts: {
287
+ rules: typeof ctx.ruleFiles === 'function' ? ctx.ruleFiles().length : 0,
288
+ workflows: typeof ctx.workflowFiles === 'function' ? ctx.workflowFiles().length : 0,
289
+ mcpServers: typeof ctx.mcpServers === 'function' ? Object.keys(ctx.mcpServers() || {}).length : 0,
290
+ },
291
+ };
292
+ }
293
+
294
+ function printLiteAudit(result, dir) {
295
+ console.log('');
296
+ const productLabel = result.platform === 'codex' ? t('audit.codexQuickScan') : t('audit.quickScan');
297
+ console.log(colorize(` ${productLabel}`, 'bold'));
298
+ console.log(colorize(' ═══════════════════════════════════════', 'dim'));
299
+ console.log(colorize(` ${t('audit.scanning', { dir })}`, 'dim'));
300
+ console.log('');
301
+ if (result.detectedConfigFiles && result.detectedConfigFiles.length > 0) {
302
+ console.log(colorize(` Found: ${result.detectedConfigFiles.join(', ')}`, 'dim'));
303
+ }
304
+ console.log('');
305
+ console.log(` ${t('audit.score', { score: colorize(`${result.score}/100`, 'bold'), passed: result.passed, total: result.passed + result.failed })}`);
306
+
307
+ // Score explanation line (lite mode only)
308
+ const _critCount = (result.results || []).filter(r => r.passed === false && r.impact === 'critical').length;
309
+ const _highCount = (result.results || []).filter(r => r.passed === false && r.impact === 'high').length;
310
+ let scoreExplanation;
311
+ if (result.score >= 90) {
312
+ scoreExplanation = t('audit.excellent');
313
+ } else if (result.score >= 70) {
314
+ scoreExplanation = t('audit.strong', { count: _critCount });
315
+ } else if (result.score >= 50) {
316
+ scoreExplanation = t('audit.good', { count: _critCount + _highCount });
317
+ } else if (result.score >= 30) {
318
+ // Find weakest category (most failures)
319
+ const catFailures = {};
320
+ (result.results || []).filter(r => r.passed === false).forEach(r => {
321
+ const cat = r.category || 'unknown';
322
+ catFailures[cat] = (catFailures[cat] || 0) + 1;
323
+ });
324
+ const weakestCategory = Object.keys(catFailures).sort((a, b) => catFailures[b] - catFailures[a])[0] || 'config';
325
+ scoreExplanation = t('audit.basic', { category: weakestCategory });
326
+ } else {
327
+ scoreExplanation = t('audit.early');
328
+ }
329
+ console.log(colorize(` ${scoreExplanation}`, 'dim'));
330
+ if (result.scoreCoaching) {
331
+ console.log(colorize(` Milestone: ${result.scoreCoaching.summary}`, 'magenta'));
332
+ }
333
+ console.log(colorize(' Score type: live repo audit (current files only, not snapshot history or benchmark projection).', 'dim'));
334
+
335
+ if (result.platformScopeNote) {
336
+ console.log(colorize(` Scope: ${result.platformScopeNote.message}`, 'dim'));
337
+ }
338
+ if (result.workspaceHint && result.workspaceHint.workspaces.length > 0) {
339
+ console.log(colorize(` Workspaces: ${result.workspaceHint.workspaces.join(', ')}`, 'dim'));
340
+ }
341
+ if (result.platformCaveats && result.platformCaveats.length > 0) {
342
+ console.log(colorize(' Platform caveats:', 'yellow'));
343
+ result.platformCaveats.slice(0, 2).forEach((item) => {
344
+ console.log(colorize(` - ${item.title}: ${item.message}`, 'dim'));
345
+ });
346
+ }
347
+ if (result.largeInstructionFiles && result.largeInstructionFiles.length > 0) {
348
+ result.largeInstructionFiles.slice(0, 2).forEach((item) => {
349
+ console.log(colorize(` Large file: ${item.file} (~${formatCount(item.tokenCount)} tokens)`, 'yellow'));
350
+ });
351
+ }
352
+ console.log('');
353
+
354
+ if (result.failed === 0) {
355
+ const platformLabel = result.platform === 'codex' ? 'Codex' : 'Claude';
356
+ console.log(colorize(` Your ${platformLabel} setup looks solid.`, 'green'));
357
+ printShallowRiskSection(result);
358
+ console.log(` Next: ${colorize(result.suggestedNextCommand, 'bold')}`);
359
+ if (result.platform === 'codex') {
360
+ console.log(colorize(' Note: Codex now supports no-write advisory flows via augment and suggest-only before setup/apply.', 'dim'));
361
+ }
362
+ console.log(colorize(' Star: github.com/nerviq/nerviq | Discord: discord.gg/nerviq', 'dim'));
363
+ console.log('');
364
+ return;
365
+ }
366
+
367
+ // Urgency summary line (only count actual failures, not skipped/null)
368
+ const criticalCount = (result.results || []).filter(r => r.passed === false && r.impact === 'critical').length;
369
+ const highCount = (result.results || []).filter(r => r.passed === false && r.impact === 'high').length;
370
+ const mediumCount = result.failed - criticalCount - highCount;
371
+ const urgencyParts = [];
372
+ if (criticalCount > 0) urgencyParts.push(colorize(`🔴 ${criticalCount} critical`, 'red'));
373
+ if (highCount > 0) urgencyParts.push(colorize(`🟡 ${highCount} high`, 'yellow'));
374
+ if (mediumCount > 0) urgencyParts.push(colorize(`🔵 ${mediumCount} recommended`, 'blue'));
375
+ if (urgencyParts.length > 0) {
376
+ console.log(` ${urgencyParts.join(' ')}`);
377
+ console.log('');
378
+ }
379
+
380
+ // PROD-03: stale-reference headline. When the new BUG-04 patterns fire,
381
+ // surface them BEFORE "Top 3 things to fix" because:
382
+ // 1. They are deterministic (file X says Y, package.json says Z) and
383
+ // a buyer can verify them in 30 seconds.
384
+ // 2. cursor-doctor + AgentLinter market signals show this is the
385
+ // highest-leverage user-visible value in this category.
386
+ // 3. The user-lab found these get buried among 70+ failed checks.
387
+ if (result.staleReferences && result.staleReferences.count > 0) {
388
+ console.log(colorize(` 📌 Stale references in agent docs: ${result.staleReferences.count}`, 'yellow'));
389
+ for (const sample of result.staleReferences.topSample) {
390
+ const labelTag = sample.key.replace('agent-config-', '').replace(/-/g, ' ');
391
+ console.log(colorize(` [${labelTag}] ${sample.file || '(unknown)'}:${sample.line || '?'}`, 'dim'));
392
+ console.log(colorize(` → ${sample.fix}`, 'dim'));
393
+ }
394
+ if (result.staleReferences.count > result.staleReferences.topSample.length) {
395
+ const remaining = result.staleReferences.count - result.staleReferences.topSample.length;
396
+ console.log(colorize(` ... and ${remaining} more (run with --shallow-risk to see all)`, 'dim'));
397
+ }
398
+ console.log('');
399
+ }
400
+
401
+ console.log(colorize(' Top 3 things to fix right now:', 'magenta'));
402
+ console.log('');
403
+ let usagePatterns;
404
+ try { usagePatterns = require('./usage-patterns'); } catch { usagePatterns = null; }
405
+ result.liteSummary.topNextActions.forEach((item, index) => {
406
+ const tier = item.impact === 'critical' ? '🔴' : item.impact === 'high' ? '🟡' : '🔵';
407
+ const suppressed = usagePatterns && usagePatterns.getPriorityAdjustment(dir, item.key) === 'suppress';
408
+ const suffix = suppressed ? colorize(' (suppressed)', 'dim') : '';
409
+ console.log(` ${index + 1}. ${tier} ${colorize(item.name, 'bold')}${suffix}`);
410
+ console.log(colorize(` ${item.fix}`, 'dim'));
411
+ });
412
+ console.log('');
413
+ printShallowRiskSection(result);
414
+ const liteTerminology = formatTerminologyLines(collectAuditTerminology(result));
415
+ if (liteTerminology.length > 0) {
416
+ liteTerminology.forEach((line) => {
417
+ const color = line.startsWith(' Terms used here:') ? 'blue' : 'dim';
418
+ console.log(colorize(line, color));
419
+ });
420
+ console.log('');
421
+ }
422
+ console.log(` Ready? Run: ${colorize(result.suggestedNextCommand, 'bold')}`);
423
+ if (result.platform === 'codex') {
424
+ console.log(colorize(' Note: Codex now supports no-write advisory flows via augment and suggest-only before setup/apply.', 'dim'));
425
+ }
426
+ console.log(colorize(` See all ${result.failed} failed checks: ${colorize('nerviq audit --full', 'bold')}`, 'dim'));
427
+ console.log(colorize(' Star: github.com/nerviq/nerviq | Discord: discord.gg/nerviq', 'dim'));
428
+ console.log('');
429
+ }
430
+
431
+ /**
432
+ * Run a full audit of a project's Claude Code setup against the NERVIQ technique database.
433
+ * @param {Object} options - Audit options.
434
+ * @param {string} options.dir - Project directory to audit.
435
+ * @param {boolean} [options.silent] - Skip all console output, return result only.
436
+ * @param {boolean} [options.json] - Output result as JSON.
437
+ * @param {boolean} [options.lite] - Show short top-3 quick scan.
438
+ * @param {boolean} [options.verbose] - Show all recommendations including medium-impact.
439
+ * @param {boolean} [options.showDeprecated] - Include deprecated checks in output.
440
+ * @returns {Promise<Object>} Audit result with score, passed/failed counts, quickWins, and topNextActions.
441
+ */
442
+ async function audit(options) {
443
+ const spec = getAuditSpec(options.platform || 'claude');
444
+ const silent = options.silent || false;
445
+ const ctx = new spec.ContextClass(options.dir);
446
+ const shallowRiskEnabled = Boolean(options.shallowRisk) && process.env.NERVIQ_SHALLOW_RISK !== 'off';
447
+ const shallowRiskOnly = Boolean(options.shallowRiskOnly) && shallowRiskEnabled;
448
+ const largeInstructionFiles = shallowRiskOnly ? [] : inspectInstructionFiles(spec, ctx);
449
+ if (!shallowRiskOnly) {
450
+ guardSkippedInstructionFiles(ctx, largeInstructionFiles);
451
+ }
452
+ const stacks = ctx.detectStacks(STACKS);
453
+ const results = [];
454
+ const outcomeSummary = getRecommendationOutcomeSummary(options.dir);
455
+ const fpFeedback = getFeedbackSummary(options.dir);
456
+ const workspaceHint = buildWorkspaceHint(options.dir);
457
+
458
+ // Load and merge plugin checks
459
+ const plugins = loadPlugins(options.dir);
460
+ const techniques = plugins.length > 0
461
+ ? mergePluginChecks(spec.techniques, plugins)
462
+ : spec.techniques;
463
+
464
+ // Pre-compute which stack categories are active for this project
465
+ const activeStackCategories = new Set();
466
+ for (const [category, detector] of Object.entries(STACK_CATEGORY_DETECTORS)) {
467
+ if (detector(ctx)) activeStackCategories.add(category);
468
+ }
469
+
470
+ // Generic quality categories that are NOT about AI agent configuration.
471
+ // These are only included with --verbose or --full --verbose (deep quality mode).
472
+ const GENERIC_QUALITY_CATEGORIES = new Set([
473
+ 'observability', 'accessibility', 'i18n', 'privacy', 'error-tracking',
474
+ 'supply-chain', 'api-versioning', 'caching', 'rate-limiting', 'feature-flags',
475
+ 'docs-quality', 'monorepo', 'performance-budget', 'realtime', 'graphql',
476
+ 'testing-strategy', 'code-quality', 'api-design', 'database', 'authentication',
477
+ 'monitoring', 'dependency-management', 'cost-optimization', 'devops',
478
+ ]);
479
+ const includeGenericQuality = options.verbose;
480
+
481
+ // Run all technique checks.
482
+ //
483
+ // AI-12a optimization (2026-04-29): partition techniques into applicable
484
+ // vs not-applicable BEFORE the hot loop. Previously the loop iterated all
485
+ // ~2,441 techniques and skipped ~85% via the in-loop guard, which still
486
+ // paid the Object.entries iteration + spread + push cost per skipped
487
+ // technique. After partition the hot loop only runs `check(ctx)` on
488
+ // genuinely applicable techniques; not-applicable ones are batched into
489
+ // a single fast push at the end. Target: ≥40% cut on first-run audit
490
+ // for repos > 200 files. See ai-12-governance-budget-tracking memo for
491
+ // the budget context.
492
+ if (!shallowRiskOnly) {
493
+ const techniqueEntries = Object.entries(techniques);
494
+ const applicable = [];
495
+ const notApplicable = [];
496
+ for (const entry of techniqueEntries) {
497
+ const cat = entry[1].category;
498
+ if ((!includeGenericQuality && GENERIC_QUALITY_CATEGORIES.has(cat)) ||
499
+ (STACK_CATEGORY_DETECTORS[cat] && !activeStackCategories.has(cat))) {
500
+ notApplicable.push(entry);
501
+ } else {
502
+ applicable.push(entry);
503
+ }
504
+ }
505
+ // Fast push for not-applicable techniques (no check() call needed).
506
+ for (let i = 0; i < notApplicable.length; i++) {
507
+ const [key, technique] = notApplicable[i];
508
+ results.push({
509
+ key,
510
+ ...technique,
511
+ file: null,
512
+ line: null,
513
+ passed: null,
514
+ });
515
+ }
516
+ // Hot loop: only applicable techniques.
517
+ for (const [key, technique] of applicable) {
518
+ const passed = technique.check(ctx);
519
+ let file = typeof technique.file === 'function' ? (technique.file(ctx) ?? null) : (technique.file ?? null);
520
+ let line = typeof technique.line === 'function' ? (technique.line(ctx) ?? null) : (technique.line ?? null);
521
+ let snippet = null;
522
+ // CTO-04: only compute evidence on failed checks (cheap, and only where it adds trust).
523
+ if (passed === false) {
524
+ const evidence = resolveEvidence(key, ctx, { file, line });
525
+ if (evidence) {
526
+ file = evidence.file;
527
+ line = evidence.line;
528
+ snippet = evidence.snippet;
529
+ }
530
+ }
531
+ results.push({
532
+ key,
533
+ ...technique,
534
+ file,
535
+ line: Number.isFinite(line) ? line : null,
536
+ snippet,
537
+ passed,
538
+ });
539
+ }
540
+ }
541
+
542
+ if (!shallowRiskOnly && largeInstructionFiles.length > 0) {
543
+ results.push({
544
+ key: 'largeInstructionFile',
545
+ id: null,
546
+ name: 'Large instruction file warning',
547
+ category: 'performance',
548
+ layer: LAYERS.GOVERNANCE,
549
+ impact: 'medium',
550
+ rating: null,
551
+ fix: 'Split oversized instruction files so they stay under ~12,000 tokens, and keep any single instruction file below ~240,000 tokens.',
552
+ sourceUrl: null,
553
+ confidence: 'high',
554
+ file: largeInstructionFiles[0].file,
555
+ line: null,
556
+ passed: null,
557
+ details: largeInstructionFiles,
558
+ });
559
+ }
560
+
561
+ // Separate deprecated checks from active checks.
562
+ // Deprecated checks are excluded from scoring but preserved for display.
563
+ const deprecated = results.filter(r => r.deprecated === true);
564
+ const activeResults = results.filter(r => r.deprecated !== true);
565
+
566
+ // null = not applicable (skip), true = pass, false = fail
567
+ const applicable = activeResults.filter(r => r.passed !== null);
568
+ const skipped = activeResults.filter(r => r.passed === null);
569
+ const passed = applicable.filter(r => r.passed);
570
+ const failed = applicable.filter(r => !r.passed);
571
+ const critical = failed.filter(r => r.impact === 'critical');
572
+ const high = failed.filter(r => r.impact === 'high');
573
+ const medium = failed.filter(r => r.impact === 'medium');
574
+
575
+ // Calculate score only from applicable checks
576
+ const maxScore = applicable.reduce((sum, r) => sum + (WEIGHTS[r.impact] || 5), 0);
577
+ const earnedScore = passed.reduce((sum, r) => sum + (WEIGHTS[r.impact] || 5), 0);
578
+ const score = maxScore > 0 ? Math.round((earnedScore / maxScore) * 100) : 0;
579
+
580
+ // Detect scaffolded vs organic: if CLAUDE.md contains our version stamp, some checks
581
+ // are passing because WE generated them, not the user
582
+ const instructionSource = spec.platform === 'codex'
583
+ ? (ctx.agentsMdContent ? (ctx.agentsMdContent() || '') : '')
584
+ : (ctx.claudeMdContent() || '');
585
+ const isScaffolded = instructionSource.includes('Generated by nerviq') ||
586
+ instructionSource.includes('nerviq');
587
+ // Scaffolded checks: things our setup creates (CLAUDE.md / AGENTS.md, hooks, commands, agents, rules, skills)
588
+ const scaffoldedKeys = spec.platform === 'codex'
589
+ ? new Set([
590
+ 'codexAgentsMd',
591
+ 'codexAgentsMdSubstantive',
592
+ 'codexAgentsVerificationCommands',
593
+ 'codexAgentsArchitecture',
594
+ 'codexConfigExists',
595
+ 'codexModelExplicit',
596
+ 'codexReasoningEffortExplicit',
597
+ 'codexWeakModelExplicit',
598
+ 'codexSandboxModeExplicit',
599
+ 'codexApprovalPolicyExplicit',
600
+ 'codexFullAutoErrorModeExplicit',
601
+ 'codexHistorySendToServerExplicit',
602
+ ])
603
+ : new Set(['claudeMd', 'mermaidArchitecture', 'verificationLoop',
604
+ 'hooks', 'customCommands', 'multipleCommands', 'agents', 'pathRules', 'multipleRules',
605
+ 'skills', 'hooksConfigured', 'preToolUseHook', 'postToolUseHook', 'fewShotExamples',
606
+ 'constraintBlocks', 'xmlTags']);
607
+ const organicPassed = passed.filter(r => !scaffoldedKeys.has(r.key));
608
+ const scaffoldedPassed = passed.filter(r => scaffoldedKeys.has(r.key));
609
+ const organicEarned = organicPassed.reduce((sum, r) => sum + (WEIGHTS[r.impact] || 5), 0);
610
+ const organicScore = maxScore > 0 ? Math.round((organicEarned / maxScore) * 100) : 0;
611
+ const quickWins = shallowRiskOnly ? [] : getQuickWins(failed, { platform: spec.platform });
612
+ const topNextActions = shallowRiskOnly
613
+ ? []
614
+ : buildTopNextActions(failed, 5, outcomeSummary.byKey, { platform: spec.platform, fpFeedbackByKey: fpFeedback.byKey });
615
+
616
+ // CTO-04: enrich top actions with file/line/snippet from the corresponding
617
+ // result record (evidence was resolved above during the check loop).
618
+ // CTO-05: project score-after-fix per action.
619
+ const resultByKey = new Map(results.map((r) => [r.key, r]));
620
+ for (const action of topNextActions) {
621
+ const source = resultByKey.get(action.key);
622
+ if (source) {
623
+ if (source.file && !action.file) action.file = source.file;
624
+ if (source.line && !action.line) action.line = source.line;
625
+ if (source.snippet) action.snippet = source.snippet;
626
+ }
627
+ // Projected score delta: if this single failed check flipped to passed.
628
+ const weight = WEIGHTS[action.impact] || 0;
629
+ if (maxScore > 0 && weight > 0) {
630
+ const projectedScoreAfter = Math.round(((earnedScore + weight) / maxScore) * 100);
631
+ action.projectedScoreDelta = projectedScoreAfter - score;
632
+ action.projectedScoreAfter = projectedScoreAfter;
633
+ const isScaffolded = scaffoldedKeys.has(action.key);
634
+ const projectedOrganicAfter = isScaffolded
635
+ ? organicScore
636
+ : Math.round(((organicEarned + weight) / maxScore) * 100);
637
+ action.projectedOrganicScoreDelta = projectedOrganicAfter - organicScore;
638
+ } else {
639
+ action.projectedScoreDelta = 0;
640
+ action.projectedScoreAfter = score;
641
+ action.projectedOrganicScoreDelta = 0;
642
+ }
643
+ }
644
+ const categoryScores = shallowRiskOnly ? {} : computeCategoryScores(applicable, passed);
645
+ const platformScopeNote = shallowRiskOnly ? null : getPlatformScopeNote(spec, ctx);
646
+ const platformCaveats = shallowRiskOnly ? [] : getPlatformCaveats(spec, ctx);
647
+ const deprecationWarnings = shallowRiskOnly ? [] : detectDeprecationWarnings(failed, packageVersion);
648
+ const warnings = [
649
+ ...largeInstructionFiles.map((item) => ({
650
+ kind: 'large-instruction-file',
651
+ severity: item.severity,
652
+ message: item.message,
653
+ file: item.file,
654
+ lineCount: item.lineCount,
655
+ byteCount: item.byteCount,
656
+ tokenCount: item.tokenCount,
657
+ skipped: item.skipped,
658
+ })),
659
+ ...deprecationWarnings.map((item) => ({
660
+ kind: 'deprecated-feature',
661
+ severity: 'warning',
662
+ ...item,
663
+ })),
664
+ ];
665
+ const recommendedDomainPacks = !shallowRiskOnly && spec.platform === 'codex'
666
+ ? detectCodexDomainPacks(ctx, stacks, getCodexDomainPackSignals(ctx))
667
+ : [];
668
+
669
+ // FB-05: framework-aware fix rewriting — don't recommend `npm test` on a
670
+ // Python/Go/Rust-only repo. Only rewrites when Node/JS stacks are absent.
671
+ const stackKeys = new Set(stacks.map(s => s.key));
672
+ const hasNodeStack = stackKeys.has('node') || stackKeys.has('react') || stackKeys.has('vue') ||
673
+ stackKeys.has('nextjs') || stackKeys.has('angular') || stackKeys.has('svelte') ||
674
+ stackKeys.has('nestjs') || stackKeys.has('remix') || stackKeys.has('astro') ||
675
+ stackKeys.has('typescript') || stackKeys.has('deno') || stackKeys.has('bun');
676
+ if (!shallowRiskOnly && !hasNodeStack) {
677
+ let preferredTest = null;
678
+ let preferredInstall = null;
679
+ if (stackKeys.has('python') || stackKeys.has('django') || stackKeys.has('fastapi')) {
680
+ preferredTest = 'pytest'; preferredInstall = 'pip install -r requirements.txt';
681
+ } else if (stackKeys.has('go')) {
682
+ preferredTest = 'go test ./...'; preferredInstall = 'go mod download';
683
+ } else if (stackKeys.has('rust')) {
684
+ preferredTest = 'cargo test'; preferredInstall = 'cargo fetch';
685
+ } else if (stackKeys.has('ruby')) {
686
+ preferredTest = 'bundle exec rspec'; preferredInstall = 'bundle install';
687
+ } else if (stackKeys.has('java') || stackKeys.has('kotlin')) {
688
+ preferredTest = './gradlew test'; preferredInstall = './gradlew build';
689
+ } else if (stackKeys.has('elixir')) {
690
+ preferredTest = 'mix test'; preferredInstall = 'mix deps.get';
691
+ } else if (stackKeys.has('dotnet')) {
692
+ preferredTest = 'dotnet test'; preferredInstall = 'dotnet restore';
693
+ }
694
+ if (preferredTest) {
695
+ for (const r of results) {
696
+ if (typeof r.fix !== 'string') continue;
697
+ if (/\bnpm\s+test\b/i.test(r.fix)) r.fix = r.fix.replace(/`npm\s+test`/gi, '`' + preferredTest + '`').replace(/\bnpm\s+test\b/gi, preferredTest);
698
+ if (/\bnpm\s+ci\b/i.test(r.fix) && preferredInstall) r.fix = r.fix.replace(/`npm\s+ci`/gi, '`' + preferredInstall + '`').replace(/\bnpm\s+ci\b/gi, preferredInstall);
699
+ if (/\bnpm\s+install\b/i.test(r.fix) && preferredInstall) r.fix = r.fix.replace(/`npm\s+install`/gi, '`' + preferredInstall + '`').replace(/\bnpm\s+install\b/gi, preferredInstall);
700
+ }
701
+ }
702
+ }
703
+
704
+ const result = {
705
+ platform: spec.platform,
706
+ platformLabel: spec.platformLabel,
707
+ platformVersion: spec.platformVersion,
708
+ score,
709
+ organicScore,
710
+ earnedPoints: earnedScore,
711
+ maxPoints: maxScore,
712
+ isScaffolded,
713
+ passed: passed.length,
714
+ failed: failed.length,
715
+ skipped: skipped.length,
716
+ deprecated: deprecated.length,
717
+ checkCount: applicable.length,
718
+ stacks,
719
+ results,
720
+ deprecatedChecks: deprecated.map(r => ({
721
+ key: r.key,
722
+ name: r.name,
723
+ category: r.category,
724
+ deprecatedReason: r.deprecatedReason || null,
725
+ sunsetDate: r.sunsetDate || null,
726
+ })),
727
+ categoryScores,
728
+ scoreCoaching: shallowRiskOnly ? null : buildScoreCoaching({
729
+ score,
730
+ earnedPoints: earnedScore,
731
+ maxPoints: maxScore,
732
+ failed,
733
+ outcomeSummaryByKey: outcomeSummary.byKey,
734
+ platform: spec.platform,
735
+ fpFeedbackByKey: fpFeedback.byKey,
736
+ }),
737
+ quickWins: quickWins.map(({ key, name, impact, fix, category, sourceUrl }) => ({ key, name, impact, category, fix, sourceUrl })),
738
+ topNextActions,
739
+ recommendationOutcomes: {
740
+ totalEntries: outcomeSummary.totalEntries,
741
+ keysTracked: outcomeSummary.keys,
742
+ },
743
+ largeInstructionFiles,
744
+ deprecationWarnings,
745
+ warnings,
746
+ workspaceHint,
747
+ platformScopeNote,
748
+ platformCaveats,
749
+ recommendedDomainPacks,
750
+ // CTO-08: per-layer coverage summary (governance/drift/hygiene/shallow-risk).
751
+ layerSummary: summarizeLayers(activeResults),
752
+ };
753
+ if (shallowRiskEnabled) {
754
+ result.shallowRiskHints = runShallowRisk(ctx);
755
+ }
756
+ // PROD-03: stale-reference HEADLINE — runs default-on (separately from
757
+ // the full shallow-risk pipeline) because the two new BUG-04 patterns
758
+ // (`agent-config-script-not-in-package-json` and
759
+ // `agent-config-framework-version-mismatch`) are deterministic, fast,
760
+ // and have near-zero FP. The user-lab found these are the highest-
761
+ // leverage user-visible value in this category (cursor-doctor /
762
+ // AgentLinter market signal); they shouldn't be gated behind a flag.
763
+ // Reuse the broader hint list when shallow-risk is already enabled.
764
+ if (shallowRiskOnly !== true) {
765
+ const STALE_REFERENCE_KEYS = new Set([
766
+ 'agent-config-script-not-in-package-json',
767
+ 'agent-config-framework-version-mismatch',
768
+ ]);
769
+ let staleHints;
770
+ if (shallowRiskEnabled && Array.isArray(result.shallowRiskHints)) {
771
+ staleHints = result.shallowRiskHints.filter((h) => STALE_REFERENCE_KEYS.has(h.key));
772
+ } else {
773
+ // Default-on mini-scan: run only the 2 stale-reference patterns.
774
+ try {
775
+ const minimalPatterns = [
776
+ require('./shallow-risk/patterns/agent-config-script-not-in-package-json'),
777
+ require('./shallow-risk/patterns/agent-config-framework-version-mismatch'),
778
+ ];
779
+ const { buildFinding } = require('./shallow-risk/shared');
780
+ staleHints = [];
781
+ for (const p of minimalPatterns) {
782
+ let raw = [];
783
+ try { raw = p.run(ctx) || []; } catch { raw = []; }
784
+ for (const f of raw) {
785
+ staleHints.push(buildFinding(p, ctx, f));
786
+ }
787
+ }
788
+ } catch {
789
+ staleHints = [];
790
+ }
791
+ }
792
+ if (staleHints && staleHints.length > 0) {
793
+ result.staleReferences = {
794
+ count: staleHints.length,
795
+ byKey: staleHints.reduce((acc, h) => {
796
+ acc[h.key] = (acc[h.key] || 0) + 1;
797
+ return acc;
798
+ }, {}),
799
+ topSample: staleHints.slice(0, 3).map((h) => ({
800
+ key: h.key,
801
+ file: h.file,
802
+ line: h.line,
803
+ fix: h.fix,
804
+ })),
805
+ headline: staleHints.length === 1
806
+ ? '1 stale reference found in agent docs.'
807
+ : `${staleHints.length} stale references found in agent docs.`,
808
+ };
809
+ }
810
+ }
811
+ // Detect which AI config files are present
812
+ const configFiles = [];
813
+ const configChecks = [
814
+ ['CLAUDE.md', 'CLAUDE.md'], ['.claude/settings.json', '.claude/settings.json'],
815
+ ['AGENTS.md', 'AGENTS.md'], ['.cursorrules', '.cursorrules'],
816
+ ['.cursor/rules', '.cursor/rules/'], ['GEMINI.md', 'GEMINI.md'],
817
+ ['.windsurfrules', '.windsurfrules'], ['.aider.conf.yml', '.aider.conf.yml'],
818
+ ['opencode.json', 'opencode.json'], ['.mcp.json', '.mcp.json'],
819
+ ];
820
+ for (const [file, label] of configChecks) {
821
+ try {
822
+ if (require('fs').existsSync(require('path').join(options.dir, file))) configFiles.push(label);
823
+ } catch {}
824
+ }
825
+ result.detectedConfigFiles = configFiles;
826
+
827
+ result.suggestedNextCommand = shallowRiskOnly
828
+ ? 'nerviq audit --shallow-risk --full'
829
+ : inferSuggestedNextCommand(result);
830
+ result.liteSummary = {
831
+ topNextActions: topNextActions.slice(0, 3),
832
+ nextCommand: result.suggestedNextCommand,
833
+ platformCaveats: platformCaveats.slice(0, 2),
834
+ scoreCoaching: result.scoreCoaching,
835
+ };
836
+
837
+ // Silent mode: skip all output, just return result
838
+ if (silent) {
839
+ return result;
840
+ }
841
+
842
+ if (options.json) {
843
+ console.log(JSON.stringify({
844
+ version: packageVersion,
845
+ timestamp: new Date().toISOString(),
846
+ ...result
847
+ }, null, 2));
848
+ return result;
849
+ }
850
+
851
+ if (options.format === 'sarif') {
852
+ console.log(JSON.stringify(formatSarif(result, { dir: options.dir }), null, 2));
853
+ return result;
854
+ }
855
+
856
+ if (options.format === 'otel') {
857
+ console.log(JSON.stringify(formatOtelMetrics(result), null, 2));
858
+ return result;
859
+ }
860
+
861
+ if (options.format === 'markdown') {
862
+ const enriched = { version: packageVersion, timestamp: new Date().toISOString(), ...result };
863
+ console.log(formatMarkdown(enriched, { dir: options.dir }));
864
+ return result;
865
+ }
866
+
867
+ if (options.format === 'junit') {
868
+ const enriched = { version: packageVersion, timestamp: new Date().toISOString(), ...result };
869
+ console.log(formatJUnit(enriched));
870
+ return result;
871
+ }
872
+
873
+ if (options.format === 'csv') {
874
+ console.log(formatCsv(result));
875
+ return result;
876
+ }
877
+
878
+ if (shallowRiskOnly) {
879
+ printShallowRiskOnly(result, options.dir);
880
+ return result;
881
+ }
882
+
883
+ if (options.lite) {
884
+ printLiteAudit(result, options.dir);
885
+ sendInsights(result);
886
+ return result;
887
+ }
888
+
889
+ // Display results
890
+ console.log('');
891
+ const auditTitle = spec.platform === 'codex' ? t('audit.codexTitle') : t('audit.title');
892
+ console.log(colorize(` ${auditTitle}`, 'bold'));
893
+ console.log(colorize(' ═══════════════════════════════════════', 'dim'));
894
+ console.log(colorize(` ${t('audit.scanning', { dir: options.dir })}`, 'dim'));
895
+ if (spec.platformVersion) {
896
+ console.log(colorize(` Platform: ${spec.platformLabel} (${spec.platformVersion})`, 'blue'));
897
+ }
898
+ if (spec.platform === 'codex' && recommendedDomainPacks.length > 0) {
899
+ console.log(colorize(` Domain packs: ${recommendedDomainPacks.map((pack) => pack.label).join(', ')}`, 'dim'));
900
+ }
901
+ if (platformScopeNote) {
902
+ console.log(colorize(` Scope: ${platformScopeNote.message}`, 'dim'));
903
+ }
904
+ if (platformCaveats.length > 0) {
905
+ console.log(colorize(' Platform caveats', 'yellow'));
906
+ for (const caveat of platformCaveats) {
907
+ console.log(colorize(` ${caveat.title}`, 'bold'));
908
+ console.log(colorize(` → ${caveat.message}`, 'dim'));
909
+ if (caveat.file) {
910
+ console.log(colorize(` at ${formatLocation(caveat.file, caveat.line)}`, 'dim'));
911
+ }
912
+ }
913
+ console.log('');
914
+ }
915
+
916
+ if (largeInstructionFiles.length > 0) {
917
+ console.log(colorize(' Large instruction files', 'yellow'));
918
+ for (const item of largeInstructionFiles) {
919
+ const sizeKb = Number.isFinite(item.byteCount) ? Math.round(item.byteCount / 1024) : '?';
920
+ console.log(colorize(` ${item.file} (~${formatCount(item.tokenCount)} tokens, ${item.lineCount || '?'} lines, ${sizeKb}KB)`, 'bold'));
921
+ console.log(colorize(` → ${item.message}`, 'dim'));
922
+ }
923
+ console.log('');
924
+ }
925
+
926
+ if (deprecationWarnings.length > 0) {
927
+ console.log(colorize(' Deprecated feature warnings', 'yellow'));
928
+ for (const item of deprecationWarnings) {
929
+ console.log(colorize(` ${item.feature}`, 'bold'));
930
+ console.log(colorize(` → ${item.message}`, 'dim'));
931
+ console.log(colorize(` Alternative: ${item.alternative}`, 'dim'));
932
+ }
933
+ console.log('');
934
+ }
935
+
936
+ if (workspaceHint && !options.workspace) {
937
+ console.log(colorize(' Monorepo detected', 'blue'));
938
+ if (workspaceHint.workspaces.length > 0) {
939
+ console.log(colorize(` Workspaces: ${workspaceHint.workspaces.join(', ')}`, 'dim'));
940
+ }
941
+ console.log(colorize(` Tip: ${workspaceHint.suggestedCommand}`, 'dim'));
942
+ console.log('');
943
+ }
944
+
945
+ if (stacks.length > 0) {
946
+ console.log(colorize(` Detected: ${stacks.map(s => s.label).join(', ')}`, 'blue'));
947
+ }
948
+
949
+ console.log('');
950
+
951
+ // Score
952
+ console.log(` ${progressBar(score)} ${colorize(`${score}/100`, 'bold')}`);
953
+ if (isScaffolded && scaffoldedPassed.length > 0) {
954
+ console.log(colorize(` Organic: ${organicScore}/100 (without nerviq generated files)`, 'dim'));
955
+ }
956
+ if (result.scoreCoaching) {
957
+ const fastestPath = result.scoreCoaching.recommendedNames.slice(0, 3).join(', ');
958
+ console.log(colorize(` Milestone: ${result.scoreCoaching.summary}`, 'magenta'));
959
+ if (fastestPath) {
960
+ console.log(colorize(` Fastest path: ${fastestPath}`, 'dim'));
961
+ }
962
+ }
963
+ console.log('');
964
+
965
+ // CTO-08: Coverage by layer — explicit map of what NERVIQ covers.
966
+ const layerSummary = result.layerSummary || summarizeLayers(activeResults);
967
+ console.log(colorize(' Coverage by layer:', 'bold'));
968
+ const layerOrder = [LAYERS.GOVERNANCE, LAYERS.DRIFT, LAYERS.HYGIENE, LAYERS.SHALLOW_RISK];
969
+ for (const layer of layerOrder) {
970
+ const b = layerSummary[layer] || { total: 0, passed: 0, failed: 0, skipped: 0 };
971
+ const layerNote = layer === LAYERS.SHALLOW_RISK ? ' (parallel, opt-in, not scored)' : '';
972
+ console.log(colorize(` ${layer}: ${b.total} checks (${b.passed} passed, ${b.failed} failed)${layerNote}`, 'dim'));
973
+ }
974
+ console.log('');
975
+
976
+ // Passed
977
+ if (passed.length > 0) {
978
+ console.log(colorize(' ✅ Passing', 'green'));
979
+ for (const r of passed) {
980
+ console.log(colorize(` ${r.name}`, 'dim'));
981
+ }
982
+ console.log('');
983
+ }
984
+
985
+ // Deprecated checks (shown with --show-deprecated or --full)
986
+ if (deprecated.length > 0 && (options.showDeprecated || options.full)) {
987
+ console.log(colorize(` ⏳ Deprecated (${deprecated.length} checks excluded from scoring)`, 'dim'));
988
+ for (const r of deprecated) {
989
+ const reason = r.deprecatedReason ? ` — ${r.deprecatedReason}` : '';
990
+ const sunset = r.sunsetDate ? ` (sunset: ${r.sunsetDate})` : '';
991
+ console.log(colorize(` [DEPRECATED] ${r.name}${reason}${sunset}`, 'dim'));
992
+ }
993
+ console.log('');
994
+ }
995
+
996
+ // Failed - by priority
997
+ if (critical.length > 0) {
998
+ console.log(colorize(' 🔴 Critical (fix immediately)', 'red'));
999
+ for (const r of critical) {
1000
+ const conf = r.confidence ? ` [${confidenceLabel(r.confidence)}]` : '';
1001
+ const layerPrefix = r.layer ? colorize(`[${r.layer}] `, 'dim') : '';
1002
+ console.log(` ${layerPrefix}${colorize(r.name, 'bold')}${colorize(conf, 'dim')}`);
1003
+ if (r.file) {
1004
+ console.log(colorize(` at ${formatLocation(r.file, r.line)}`, 'dim'));
1005
+ }
1006
+ console.log(colorize(` → ${r.fix}`, 'dim'));
1007
+ }
1008
+ console.log('');
1009
+ }
1010
+
1011
+ if (high.length > 0) {
1012
+ console.log(colorize(' 🟡 High Impact', 'yellow'));
1013
+ for (const r of high) {
1014
+ const conf = r.confidence ? ` [${confidenceLabel(r.confidence)}]` : '';
1015
+ const layerPrefix = r.layer ? colorize(`[${r.layer}] `, 'dim') : '';
1016
+ console.log(` ${layerPrefix}${colorize(r.name, 'bold')}${colorize(conf, 'dim')}`);
1017
+ if (r.file) {
1018
+ console.log(colorize(` at ${formatLocation(r.file, r.line)}`, 'dim'));
1019
+ }
1020
+ console.log(colorize(` → ${r.fix}`, 'dim'));
1021
+ }
1022
+ console.log('');
1023
+ }
1024
+
1025
+ if (medium.length > 0 && options.verbose) {
1026
+ console.log(colorize(' 🔵 Recommended', 'blue'));
1027
+ for (const r of medium) {
1028
+ const conf = r.confidence ? ` [${confidenceLabel(r.confidence)}]` : '';
1029
+ const layerPrefix = r.layer ? colorize(`[${r.layer}] `, 'dim') : '';
1030
+ console.log(` ${layerPrefix}${colorize(r.name, 'bold')}${colorize(conf, 'dim')}`);
1031
+ if (r.file) {
1032
+ console.log(colorize(` at ${formatLocation(r.file, r.line)}`, 'dim'));
1033
+ }
1034
+ console.log(colorize(` → ${r.fix}`, 'dim'));
1035
+ }
1036
+ console.log('');
1037
+ } else if (medium.length > 0) {
1038
+ console.log(colorize(` 🔵 ${medium.length} more recommendations (use --verbose)`, 'blue'));
1039
+ console.log('');
1040
+ }
1041
+
1042
+ // Top next actions
1043
+ if (topNextActions.length > 0) {
1044
+ console.log(colorize(' ⚡ Top 5 Next Actions', 'magenta'));
1045
+ for (let i = 0; i < topNextActions.length; i++) {
1046
+ const item = topNextActions[i];
1047
+ const delta = Number.isFinite(item.projectedScoreDelta) && item.projectedScoreDelta > 0
1048
+ ? colorize(` (+${item.projectedScoreDelta} pts → ${item.projectedScoreAfter}/100)`, 'green')
1049
+ : '';
1050
+ console.log(` ${i + 1}. ${colorize(item.name, 'bold')}${delta}`);
1051
+ console.log(colorize(` Why: ${item.why}`, 'dim'));
1052
+ console.log(colorize(` Trace: ${item.signals.join(' | ')}`, 'dim'));
1053
+ console.log(colorize(` Risk: ${item.risk} | Confidence: ${item.confidence}`, 'dim'));
1054
+ const sourceResult = result.results.find(r => r.key === item.key);
1055
+ if (sourceResult && sourceResult.file) {
1056
+ console.log(colorize(` Evidence: ${formatLocation(sourceResult.file, sourceResult.line)}`, 'dim'));
1057
+ }
1058
+ if (item.feedback) {
1059
+ const avgDelta = Number.isFinite(item.feedback.avgScoreDelta) ? ` | Avg score delta: ${item.feedback.avgScoreDelta >= 0 ? '+' : ''}${item.feedback.avgScoreDelta}` : '';
1060
+ console.log(colorize(` Feedback: accepted ${item.feedback.accepted}, rejected ${item.feedback.rejected}, positive ${item.feedback.positive}, negative ${item.feedback.negative}${avgDelta}`, 'dim'));
1061
+ }
1062
+ console.log(colorize(` Fix: ${item.fix}`, 'dim'));
1063
+ }
1064
+ console.log('');
1065
+ }
1066
+
1067
+ printShallowRiskSection(result);
1068
+
1069
+ const terminology = formatTerminologyLines(collectAuditTerminology(result));
1070
+ if (terminology.length > 0) {
1071
+ terminology.forEach((line) => {
1072
+ const color = line.startsWith(' Terms used here:') ? 'blue' : 'dim';
1073
+ console.log(colorize(line, color));
1074
+ });
1075
+ console.log('');
1076
+ }
1077
+
1078
+ // Summary
1079
+ console.log(colorize(' ─────────────────────────────────────', 'dim'));
1080
+ const deprecatedNote = deprecated.length > 0 ? colorize(`, ${deprecated.length} deprecated`, 'dim') : '';
1081
+ console.log(` ${colorize(`${passed.length}/${applicable.length}`, 'bold')} checks passing${skipped.length > 0 ? colorize(` (${skipped.length} not applicable${deprecatedNote})`, 'dim') : (deprecatedNote ? colorize(` (${deprecatedNote})`, 'dim') : '')}`);
1082
+
1083
+ if (failed.length > 0) {
1084
+ console.log(` Next command: ${colorize(result.suggestedNextCommand, 'bold')}`);
1085
+ if (result.platform === 'codex') {
1086
+ console.log(colorize(' Codex now supports advisory no-write flows through augment and suggest-only before setup/apply.', 'dim'));
1087
+ }
1088
+ }
1089
+
1090
+ console.log('');
1091
+ console.log(` Add to README: ${getBadgeMarkdown(score)}`);
1092
+ console.log('');
1093
+
1094
+ // Weakest categories insight
1095
+ const insights = getLocalInsights({ score, results });
1096
+ if (insights.weakest.length > 0) {
1097
+ console.log(colorize(' Weakest areas:', 'dim'));
1098
+ for (const w of insights.weakest) {
1099
+ const bar = w.score === 0 ? colorize('none', 'red') : `${w.score}%`;
1100
+ console.log(colorize(` ${w.name}: ${bar} (${w.passed}/${w.total})`, 'dim'));
1101
+ }
1102
+ console.log('');
1103
+ }
1104
+
1105
+ // Cross-platform synergy hint
1106
+ try {
1107
+ const { detectActivePlatforms } = require('./harmony/canon');
1108
+ const { analyzeCompensation } = require('./synergy/compensation');
1109
+ const { calculateSynergyScore } = require('./synergy/ranking');
1110
+ const detected = detectActivePlatforms(options.dir);
1111
+ const activePlatforms = (detected || []).filter(p => p.detected).map(p => p.platform);
1112
+ if (activePlatforms.length >= 2) {
1113
+ const comp = analyzeCompensation(activePlatforms);
1114
+ const synergyScore = calculateSynergyScore(activePlatforms);
1115
+ console.log(colorize(` Cross-platform synergy [EXPERIMENTAL]: ${activePlatforms.length} platforms detected`, 'blue'));
1116
+ console.log(colorize(` Platforms: ${activePlatforms.join(', ')}`, 'dim'));
1117
+ console.log(colorize(` Compensations: ${comp.compensations.length} | Gaps: ${comp.uncoveredGaps.length}`, 'dim'));
1118
+ console.log(colorize(` Run: npx nerviq harmony-audit for full cross-platform analysis`, 'dim'));
1119
+ console.log('');
1120
+ }
1121
+ } catch { /* synergy display is optional */ }
1122
+
1123
+ console.log(colorize(` Backed by NERVIQ research and evidence for ${spec.platformLabel}`, 'dim'));
1124
+ console.log(colorize(' https://github.com/nerviq/nerviq', 'dim'));
1125
+ console.log('');
1126
+
1127
+ // Send anonymous insights (opt-in, privacy-first, fire-and-forget)
1128
+ sendInsights(result);
1129
+
1130
+ return result;
1131
+ }
1132
+
1133
+ module.exports = { audit, buildTopNextActions, getFpFeedbackMultiplier, getRecommendationPriorityScore };