@nerviq/cli 1.29.0 → 1.29.1

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 (80) hide show
  1. package/CHANGELOG.md +1527 -1493
  2. package/README.md +550 -538
  3. package/SECURITY.md +82 -82
  4. package/bin/cli.js +2562 -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 +67 -67
  17. package/src/aider/activity.js +226 -226
  18. package/src/aider/context.js +162 -162
  19. package/src/aider/freshness.js +123 -123
  20. package/src/aider/techniques.js +3465 -3465
  21. package/src/audit/layers.js +180 -180
  22. package/src/audit.js +1032 -1032
  23. package/src/benchmark.js +299 -299
  24. package/src/codex/activity.js +324 -324
  25. package/src/codex/freshness.js +142 -142
  26. package/src/codex/techniques.js +4895 -4895
  27. package/src/context.js +326 -326
  28. package/src/continuous-ops.js +11 -1
  29. package/src/convert.js +340 -340
  30. package/src/copilot/config-parser.js +280 -280
  31. package/src/copilot/context.js +218 -218
  32. package/src/copilot/freshness.js +177 -177
  33. package/src/copilot/patch.js +238 -238
  34. package/src/copilot/techniques.js +3578 -3578
  35. package/src/cursor/freshness.js +194 -194
  36. package/src/cursor/patch.js +243 -243
  37. package/src/cursor/techniques.js +3735 -3735
  38. package/src/doctor.js +201 -201
  39. package/src/fix-engine.js +511 -8
  40. package/src/formatters/csv.js +86 -86
  41. package/src/formatters/junit.js +123 -123
  42. package/src/formatters/markdown.js +164 -164
  43. package/src/formatters/otel.js +151 -151
  44. package/src/freshness.js +156 -156
  45. package/src/gemini/activity.js +402 -402
  46. package/src/gemini/context.js +290 -290
  47. package/src/gemini/freshness.js +183 -183
  48. package/src/gemini/patch.js +229 -229
  49. package/src/gemini/techniques.js +3811 -3811
  50. package/src/governance.js +533 -533
  51. package/src/harmony/audit.js +306 -306
  52. package/src/i18n.js +63 -63
  53. package/src/insights.js +119 -119
  54. package/src/integrations.js +134 -134
  55. package/src/locales/en.json +33 -33
  56. package/src/locales/es.json +33 -33
  57. package/src/migrate.js +354 -354
  58. package/src/opencode/activity.js +286 -286
  59. package/src/opencode/freshness.js +137 -137
  60. package/src/opencode/techniques.js +3450 -3450
  61. package/src/setup/analysis.js +12 -12
  62. package/src/setup.js +7 -6
  63. package/src/shallow-risk/index.js +56 -56
  64. package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +50 -50
  65. package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +46 -46
  66. package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +46 -46
  67. package/src/shallow-risk/patterns/agent-config-missing-file.js +317 -317
  68. package/src/shallow-risk/patterns/agent-config-secret-literal.js +49 -49
  69. package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +34 -34
  70. package/src/shallow-risk/patterns/hook-script-missing.js +70 -70
  71. package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +52 -52
  72. package/src/shallow-risk/shared.js +648 -648
  73. package/src/source-urls.js +295 -295
  74. package/src/state-paths.js +85 -85
  75. package/src/supplemental-checks.js +805 -805
  76. package/src/telemetry.js +160 -160
  77. package/src/windsurf/context.js +359 -359
  78. package/src/windsurf/freshness.js +194 -194
  79. package/src/windsurf/patch.js +231 -231
  80. package/src/windsurf/techniques.js +3779 -3779
package/src/audit.js CHANGED
@@ -1,1032 +1,1032 @@
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
+ 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 };