@nerviq/cli 1.26.0 → 1.27.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 (59) hide show
  1. package/CHANGELOG.md +1407 -0
  2. package/README.md +4 -4
  3. package/SECURITY.md +82 -0
  4. package/bin/cli.js +13 -1
  5. package/contracts/audit-webhook-event.schema.json +138 -0
  6. package/contracts/pack-contract.schema.json +15 -0
  7. package/contracts/technique-contract.schema.json +18 -0
  8. package/docs/ARCHITECTURE.md +74 -0
  9. package/docs/api-reference.md +356 -0
  10. package/docs/autofix.md +64 -0
  11. package/docs/bitbucket-pipe.yml +57 -0
  12. package/docs/case-studies.md +149 -0
  13. package/docs/category-definition-kit.md +56 -0
  14. package/docs/ci-integration.md +127 -0
  15. package/docs/claude-code-style.md +24 -0
  16. package/docs/claude-maintainer-ops.md +19 -0
  17. package/docs/external-validation.md +78 -0
  18. package/docs/first-tier-integration-gate.md +59 -0
  19. package/docs/getting-started.md +119 -0
  20. package/docs/gitlab-ci-template.yml +54 -0
  21. package/docs/index.html +597 -0
  22. package/docs/integration-contracts.md +287 -0
  23. package/docs/license-faq.md +53 -0
  24. package/docs/maintenance.md +155 -0
  25. package/docs/methodology.md +236 -0
  26. package/docs/new-platform-guide.md +202 -0
  27. package/docs/open-vsx-publishing.md +46 -0
  28. package/docs/platform-change-ingestion.md +46 -0
  29. package/docs/plugins.md +101 -0
  30. package/docs/pre-commit.md +58 -0
  31. package/docs/security-model.md +63 -0
  32. package/docs/shallow-risk.md +246 -0
  33. package/docs/versioning-policy.md +63 -0
  34. package/docs/why-nerviq.md +82 -0
  35. package/package.json +7 -2
  36. package/sdk/README.md +190 -0
  37. package/src/audit/layers.js +180 -179
  38. package/src/audit.js +118 -48
  39. package/src/codex/setup.js +3 -2
  40. package/src/formatters/csv.js +86 -85
  41. package/src/formatters/junit.js +123 -103
  42. package/src/formatters/markdown.js +164 -135
  43. package/src/gemini/setup.js +3 -2
  44. package/src/init.js +4 -3
  45. package/src/opencode/context.js +42 -3
  46. package/src/opencode/techniques.js +198 -142
  47. package/src/output-icons.js +44 -0
  48. package/src/setup/runtime.js +6 -5
  49. package/src/setup.js +4 -3
  50. package/src/shallow-risk/index.js +56 -0
  51. package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +50 -0
  52. package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +46 -0
  53. package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +46 -0
  54. package/src/shallow-risk/patterns/agent-config-missing-file.js +72 -0
  55. package/src/shallow-risk/patterns/agent-config-secret-literal.js +49 -0
  56. package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +34 -0
  57. package/src/shallow-risk/patterns/hook-script-missing.js +70 -0
  58. package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +52 -0
  59. package/src/shallow-risk/shared.js +520 -0
@@ -1,179 +1,180 @@
1
- /**
2
- * CTO-08 — 5-layer scope clarity.
3
- *
4
- * Every check in the NERVIQ audit is tagged with exactly one layer so
5
- * customers and evaluators get an explicit map of what NERVIQ covers and
6
- * what it does not. The 4 positive layers below intentionally exclude any
7
- * "deep-review" / general-security-scanning lane: NERVIQ is an
8
- * agent-configuration audit tool, not a code-review tool.
9
- *
10
- * Taxonomy (canonical — mirrored in docs/integration-contracts.md §8):
11
- *
12
- * governance — Agent configuration posture: presence, content, and
13
- * quality of agent-instruction files and platform
14
- * settings. Answers "does my agent know X?".
15
- *
16
- * drift — Cross-platform consistency: do multiple platform
17
- * configs agree? Does the declared state match the
18
- * repo reality? Answers "do two places agree on X?".
19
- *
20
- * hygiene — Repo-level cleanliness and operational basics
21
- * adjacent to agents (gitignore, CHANGELOG, SECURITY.md,
22
- * CI, Dependabot, license, editorconfig, Node version
23
- * pinning, etc.). Answers "does the repo have standard
24
- * engineering hygiene that makes the agent's job
25
- * easier?".
26
- *
27
- * shallow-risk — Reserved for CTO-06. No checks currently live in
28
- * this layer; the constant exists so formatters and
29
- * types know about it.
30
- *
31
- * Disambiguation rule-of-thumb when a check could plausibly belong to
32
- * more than one layer: prefer the most specific layer (drift > hygiene
33
- * > governance). If in doubt, default to hygiene a mild
34
- * misclassification is recoverable; a missing tag breaks the coverage
35
- * test.
36
- */
37
-
38
- 'use strict';
39
-
40
- const LAYERS = Object.freeze({
41
- GOVERNANCE: 'governance',
42
- DRIFT: 'drift',
43
- HYGIENE: 'hygiene',
44
- SHALLOW_RISK: 'shallow-risk',
45
- });
46
-
47
- const LAYER_DEFINITIONS = Object.freeze({
48
- [LAYERS.GOVERNANCE]: 'Agent configuration posture: presence, content, and quality of agent-instruction files and platform settings.',
49
- [LAYERS.DRIFT]: 'Cross-platform consistency: do multiple platform configs agree, and does the declared state match repo reality?',
50
- [LAYERS.HYGIENE]: 'Repo-level cleanliness and operational basics adjacent to agents (gitignore, CHANGELOG, SECURITY.md, CI, license, etc.).',
51
- [LAYERS.SHALLOW_RISK]: 'Reserved for shallow-risk boundary checks (CTO-06). No checks currently populate this layer.',
52
- });
53
-
54
- const VALID_LAYER_VALUES = new Set(Object.values(LAYERS));
55
-
56
- function isValidLayer(value) {
57
- return typeof value === 'string' && VALID_LAYER_VALUES.has(value);
58
- }
59
-
60
- /**
61
- * Name/id patterns that strongly indicate a drift check. Applied only as
62
- * a heuristic when tagging existing check bags (see assignLayers).
63
- */
64
- const DRIFT_PATTERNS = [
65
- /drift/i,
66
- /harmony/i,
67
- /\bpropagation\b/i,
68
- /consisten(t|cy)/i,
69
- /cross[- ]?platform/i,
70
- /across (surfaces|platforms|all .* surfaces)/i,
71
- /\bpacks are consistent\b/i,
72
- /propagation (checklist|completeness|delay)/i,
73
- ];
74
-
75
- /**
76
- * Hygiene name patterns — used to upgrade a check from a default
77
- * governance bag into hygiene when the check is clearly about repo
78
- * engineering hygiene rather than agent config.
79
- */
80
- const HYGIENE_PATTERNS = [
81
- /\.gitignore/i,
82
- /\bCHANGELOG\b/i,
83
- /\bCONTRIBUTING\b/i,
84
- /\bLICENSE\b/i,
85
- /\.editorconfig/i,
86
- /\bEditorConfig\b/i,
87
- /\bSECURITY\.md\b/i,
88
- /\bCODE_OF_CONDUCT\b/i,
89
- /\bDependabot\b/i,
90
- /\bNode version pinned\b/i,
91
- /\bREADME\b.*\b(install|usage|contributing|sections|section)\b/i,
92
- /\blockfile\b/i,
93
- /\bcargo-audit\b/i,
94
- /\bDockerfile\b/i,
95
- /\bCI (is configured|configured|pipeline|workflow)/i,
96
- /\bGitHub Actions\b/i,
97
- /\b\.github\/workflows\b/i,
98
- /\bpre-commit\b/i,
99
- /\b(poetry|uv|pipenv|npm|pnpm|yarn|bun)\.lock/i,
100
- /\brenovate\b/i,
101
- /\bsemver\b/i,
102
- /\brelease automation\b/i,
103
- ];
104
-
105
- /**
106
- * Check categories that strongly indicate repo-hygiene rather than
107
- * agent-configuration. These cover the stack-specific engineering
108
- * baselines (Python lockfile, Rust target/ in .gitignore, etc.) that
109
- * ship via the stacks checks.
110
- */
111
- const HYGIENE_CATEGORIES = new Set([
112
- 'dependency-management', 'supply-chain', 'release-freshness',
113
- 'docker', 'ci', 'ci-cd',
114
- 'git', // the cross-platform hygiene.js checks live here
115
- ]);
116
-
117
- function inferLayerForCheck(check, defaultLayer) {
118
- const probe = `${check.name || ''} ${check.id || ''} ${check.key || ''}`;
119
- if (DRIFT_PATTERNS.some((re) => re.test(probe))) return LAYERS.DRIFT;
120
- if (defaultLayer === LAYERS.GOVERNANCE) {
121
- if (HYGIENE_PATTERNS.some((re) => re.test(probe))) return LAYERS.HYGIENE;
122
- if (check.category && HYGIENE_CATEGORIES.has(check.category)) return LAYERS.HYGIENE;
123
- }
124
- return defaultLayer;
125
- }
126
-
127
- /**
128
- * Mutates `bag` (a technique dictionary of { key: { name, id, ... } })
129
- * so every entry has a `layer` field. Existing `layer` values on
130
- * individual checks are respected.
131
- *
132
- * @param {Object} bag technique dictionary
133
- * @param {string} defaultLayer one of LAYERS.*, used when heuristics don't fire
134
- * @returns {Object} the same bag, for chaining
135
- */
136
- function assignLayers(bag, defaultLayer = LAYERS.GOVERNANCE) {
137
- if (!bag || typeof bag !== 'object') return bag;
138
- if (!isValidLayer(defaultLayer)) {
139
- throw new Error(`assignLayers: invalid defaultLayer "${defaultLayer}"`);
140
- }
141
- for (const [key, check] of Object.entries(bag)) {
142
- if (!check || typeof check !== 'object') continue;
143
- if (isValidLayer(check.layer)) continue;
144
- const withKey = { ...check, key };
145
- check.layer = inferLayerForCheck(withKey, defaultLayer);
146
- }
147
- return bag;
148
- }
149
-
150
- /**
151
- * Summary helper — counts checks per layer in a results array. Used by
152
- * the audit text renderer and by the coverage test.
153
- */
154
- function summarizeLayers(results) {
155
- const summary = {
156
- [LAYERS.GOVERNANCE]: { total: 0, passed: 0, failed: 0, skipped: 0 },
157
- [LAYERS.DRIFT]: { total: 0, passed: 0, failed: 0, skipped: 0 },
158
- [LAYERS.HYGIENE]: { total: 0, passed: 0, failed: 0, skipped: 0 },
159
- [LAYERS.SHALLOW_RISK]: { total: 0, passed: 0, failed: 0, skipped: 0 },
160
- };
161
- for (const r of results || []) {
162
- const layer = isValidLayer(r.layer) ? r.layer : LAYERS.HYGIENE;
163
- const bucket = summary[layer];
164
- bucket.total += 1;
165
- if (r.passed === true) bucket.passed += 1;
166
- else if (r.passed === false) bucket.failed += 1;
167
- else bucket.skipped += 1;
168
- }
169
- return summary;
170
- }
171
-
172
- module.exports = {
173
- LAYERS,
174
- LAYER_DEFINITIONS,
175
- isValidLayer,
176
- assignLayers,
177
- summarizeLayers,
178
- inferLayerForCheck,
179
- };
1
+ /**
2
+ * CTO-08 — 5-layer scope clarity.
3
+ *
4
+ * Every check in the NERVIQ audit is tagged with exactly one layer so
5
+ * customers and evaluators get an explicit map of what NERVIQ covers and
6
+ * what it does not. The 4 positive layers below intentionally exclude any
7
+ * "deep-review" / general-security-scanning lane: NERVIQ is an
8
+ * agent-configuration audit tool, not a code-review tool.
9
+ *
10
+ * Taxonomy (canonical — mirrored in docs/integration-contracts.md §8):
11
+ *
12
+ * governance — Agent configuration posture: presence, content, and
13
+ * quality of agent-instruction files and platform
14
+ * settings. Answers "does my agent know X?".
15
+ *
16
+ * drift — Cross-platform consistency: do multiple platform
17
+ * configs agree? Does the declared state match the
18
+ * repo reality? Answers "do two places agree on X?".
19
+ *
20
+ * hygiene — Repo-level cleanliness and operational basics
21
+ * adjacent to agents (gitignore, CHANGELOG, SECURITY.md,
22
+ * CI, Dependabot, license, editorconfig, Node version
23
+ * pinning, etc.). Answers "does the repo have standard
24
+ * engineering hygiene that makes the agent's job
25
+ * easier?".
26
+ *
27
+ * shallow-risk — Parallel, opt-in boundary checks that sit at the
28
+ * agent-config <-> codebase edge. Findings are emitted
29
+ * through `auditResult.shallowRiskHints[]` and are not
30
+ * folded into governance scoring.
31
+ *
32
+ * Disambiguation rule-of-thumb when a check could plausibly belong to
33
+ * more than one layer: prefer the most specific layer (drift > hygiene
34
+ * > governance). If in doubt, default to hygiene — a mild
35
+ * misclassification is recoverable; a missing tag breaks the coverage
36
+ * test.
37
+ */
38
+
39
+ 'use strict';
40
+
41
+ const LAYERS = Object.freeze({
42
+ GOVERNANCE: 'governance',
43
+ DRIFT: 'drift',
44
+ HYGIENE: 'hygiene',
45
+ SHALLOW_RISK: 'shallow-risk',
46
+ });
47
+
48
+ const LAYER_DEFINITIONS = Object.freeze({
49
+ [LAYERS.GOVERNANCE]: 'Agent configuration posture: presence, content, and quality of agent-instruction files and platform settings.',
50
+ [LAYERS.DRIFT]: 'Cross-platform consistency: do multiple platform configs agree, and does the declared state match repo reality?',
51
+ [LAYERS.HYGIENE]: 'Repo-level cleanliness and operational basics adjacent to agents (gitignore, CHANGELOG, SECURITY.md, CI, license, etc.).',
52
+ [LAYERS.SHALLOW_RISK]: 'Parallel, opt-in boundary checks emitted via auditResult.shallowRiskHints[]; shown separately and excluded from governance scoring.',
53
+ });
54
+
55
+ const VALID_LAYER_VALUES = new Set(Object.values(LAYERS));
56
+
57
+ function isValidLayer(value) {
58
+ return typeof value === 'string' && VALID_LAYER_VALUES.has(value);
59
+ }
60
+
61
+ /**
62
+ * Name/id patterns that strongly indicate a drift check. Applied only as
63
+ * a heuristic when tagging existing check bags (see assignLayers).
64
+ */
65
+ const DRIFT_PATTERNS = [
66
+ /drift/i,
67
+ /harmony/i,
68
+ /\bpropagation\b/i,
69
+ /consisten(t|cy)/i,
70
+ /cross[- ]?platform/i,
71
+ /across (surfaces|platforms|all .* surfaces)/i,
72
+ /\bpacks are consistent\b/i,
73
+ /propagation (checklist|completeness|delay)/i,
74
+ ];
75
+
76
+ /**
77
+ * Hygiene name patterns used to upgrade a check from a default
78
+ * governance bag into hygiene when the check is clearly about repo
79
+ * engineering hygiene rather than agent config.
80
+ */
81
+ const HYGIENE_PATTERNS = [
82
+ /\.gitignore/i,
83
+ /\bCHANGELOG\b/i,
84
+ /\bCONTRIBUTING\b/i,
85
+ /\bLICENSE\b/i,
86
+ /\.editorconfig/i,
87
+ /\bEditorConfig\b/i,
88
+ /\bSECURITY\.md\b/i,
89
+ /\bCODE_OF_CONDUCT\b/i,
90
+ /\bDependabot\b/i,
91
+ /\bNode version pinned\b/i,
92
+ /\bREADME\b.*\b(install|usage|contributing|sections|section)\b/i,
93
+ /\blockfile\b/i,
94
+ /\bcargo-audit\b/i,
95
+ /\bDockerfile\b/i,
96
+ /\bCI (is configured|configured|pipeline|workflow)/i,
97
+ /\bGitHub Actions\b/i,
98
+ /\b\.github\/workflows\b/i,
99
+ /\bpre-commit\b/i,
100
+ /\b(poetry|uv|pipenv|npm|pnpm|yarn|bun)\.lock/i,
101
+ /\brenovate\b/i,
102
+ /\bsemver\b/i,
103
+ /\brelease automation\b/i,
104
+ ];
105
+
106
+ /**
107
+ * Check categories that strongly indicate repo-hygiene rather than
108
+ * agent-configuration. These cover the stack-specific engineering
109
+ * baselines (Python lockfile, Rust target/ in .gitignore, etc.) that
110
+ * ship via the stacks checks.
111
+ */
112
+ const HYGIENE_CATEGORIES = new Set([
113
+ 'dependency-management', 'supply-chain', 'release-freshness',
114
+ 'docker', 'ci', 'ci-cd',
115
+ 'git', // the cross-platform hygiene.js checks live here
116
+ ]);
117
+
118
+ function inferLayerForCheck(check, defaultLayer) {
119
+ const probe = `${check.name || ''} ${check.id || ''} ${check.key || ''}`;
120
+ if (DRIFT_PATTERNS.some((re) => re.test(probe))) return LAYERS.DRIFT;
121
+ if (defaultLayer === LAYERS.GOVERNANCE) {
122
+ if (HYGIENE_PATTERNS.some((re) => re.test(probe))) return LAYERS.HYGIENE;
123
+ if (check.category && HYGIENE_CATEGORIES.has(check.category)) return LAYERS.HYGIENE;
124
+ }
125
+ return defaultLayer;
126
+ }
127
+
128
+ /**
129
+ * Mutates `bag` (a technique dictionary of { key: { name, id, ... } })
130
+ * so every entry has a `layer` field. Existing `layer` values on
131
+ * individual checks are respected.
132
+ *
133
+ * @param {Object} bag technique dictionary
134
+ * @param {string} defaultLayer one of LAYERS.*, used when heuristics don't fire
135
+ * @returns {Object} the same bag, for chaining
136
+ */
137
+ function assignLayers(bag, defaultLayer = LAYERS.GOVERNANCE) {
138
+ if (!bag || typeof bag !== 'object') return bag;
139
+ if (!isValidLayer(defaultLayer)) {
140
+ throw new Error(`assignLayers: invalid defaultLayer "${defaultLayer}"`);
141
+ }
142
+ for (const [key, check] of Object.entries(bag)) {
143
+ if (!check || typeof check !== 'object') continue;
144
+ if (isValidLayer(check.layer)) continue;
145
+ const withKey = { ...check, key };
146
+ check.layer = inferLayerForCheck(withKey, defaultLayer);
147
+ }
148
+ return bag;
149
+ }
150
+
151
+ /**
152
+ * Summary helper counts checks per layer in a results array. Used by
153
+ * the audit text renderer and by the coverage test.
154
+ */
155
+ function summarizeLayers(results) {
156
+ const summary = {
157
+ [LAYERS.GOVERNANCE]: { total: 0, passed: 0, failed: 0, skipped: 0 },
158
+ [LAYERS.DRIFT]: { total: 0, passed: 0, failed: 0, skipped: 0 },
159
+ [LAYERS.HYGIENE]: { total: 0, passed: 0, failed: 0, skipped: 0 },
160
+ [LAYERS.SHALLOW_RISK]: { total: 0, passed: 0, failed: 0, skipped: 0 },
161
+ };
162
+ for (const r of results || []) {
163
+ const layer = isValidLayer(r.layer) ? r.layer : LAYERS.HYGIENE;
164
+ const bucket = summary[layer];
165
+ bucket.total += 1;
166
+ if (r.passed === true) bucket.passed += 1;
167
+ else if (r.passed === false) bucket.failed += 1;
168
+ else bucket.skipped += 1;
169
+ }
170
+ return summary;
171
+ }
172
+
173
+ module.exports = {
174
+ LAYERS,
175
+ LAYER_DEFINITIONS,
176
+ isValidLayer,
177
+ assignLayers,
178
+ summarizeLayers,
179
+ inferLayerForCheck,
180
+ };
package/src/audit.js CHANGED
@@ -37,6 +37,7 @@ const { detectDeprecationWarnings } = require('./deprecation');
37
37
  const { buildWorkspaceHint, formatCount, guardSkippedInstructionFiles, inspectInstructionFiles } = require('./audit/instruction-files');
38
38
  const { resolveEvidence } = require('./audit/evidence');
39
39
  const { LAYERS, summarizeLayers } = require('./audit/layers');
40
+ const { runShallowRisk, SHALLOW_RISK_BANNER_LINES } = require('./shallow-risk');
40
41
  const {
41
42
  WEIGHTS,
42
43
  buildScoreCoaching,
@@ -78,6 +79,54 @@ function formatLocation(file, line) {
78
79
  return line ? `${file}:${line}` : file;
79
80
  }
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
+
81
130
  function getAuditSpec(platform = 'claude') {
82
131
  if (platform === 'codex') {
83
132
  return {
@@ -299,6 +348,7 @@ function printLiteAudit(result, dir) {
299
348
  if (result.failed === 0) {
300
349
  const platformLabel = result.platform === 'codex' ? 'Codex' : 'Claude';
301
350
  console.log(colorize(` Your ${platformLabel} setup looks solid.`, 'green'));
351
+ printShallowRiskSection(result);
302
352
  console.log(` Next: ${colorize(result.suggestedNextCommand, 'bold')}`);
303
353
  if (result.platform === 'codex') {
304
354
  console.log(colorize(' Note: Codex now supports no-write advisory flows via augment and suggest-only before setup/apply.', 'dim'));
@@ -333,6 +383,7 @@ function printLiteAudit(result, dir) {
333
383
  console.log(colorize(` ${item.fix}`, 'dim'));
334
384
  });
335
385
  console.log('');
386
+ printShallowRiskSection(result);
336
387
  const liteTerminology = formatTerminologyLines(collectAuditTerminology(result));
337
388
  if (liteTerminology.length > 0) {
338
389
  liteTerminology.forEach((line) => {
@@ -365,8 +416,12 @@ async function audit(options) {
365
416
  const spec = getAuditSpec(options.platform || 'claude');
366
417
  const silent = options.silent || false;
367
418
  const ctx = new spec.ContextClass(options.dir);
368
- const largeInstructionFiles = inspectInstructionFiles(spec, ctx);
369
- guardSkippedInstructionFiles(ctx, largeInstructionFiles);
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
+ }
370
425
  const stacks = ctx.detectStacks(STACKS);
371
426
  const results = [];
372
427
  const outcomeSummary = getRecommendationOutcomeSummary(options.dir);
@@ -397,46 +452,48 @@ async function audit(options) {
397
452
  const includeGenericQuality = options.verbose;
398
453
 
399
454
  // Run all technique checks
400
- for (const [key, technique] of Object.entries(techniques)) {
401
- // Skip entire stack category if the stack is not detected at a core location
402
- // Skip generic quality categories unless --verbose is set
403
- const cat = technique.category;
404
- if ((!includeGenericQuality && GENERIC_QUALITY_CATEGORIES.has(cat)) ||
405
- (STACK_CATEGORY_DETECTORS[cat] && !activeStackCategories.has(cat))) {
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
+ }
406
485
  results.push({
407
486
  key,
408
487
  ...technique,
409
- file: null,
410
- line: null,
411
- passed: null, // not applicable
488
+ file,
489
+ line: Number.isFinite(line) ? line : null,
490
+ snippet,
491
+ passed,
412
492
  });
413
- continue;
414
493
  }
415
-
416
- const passed = technique.check(ctx);
417
- let file = typeof technique.file === 'function' ? (technique.file(ctx) ?? null) : (technique.file ?? null);
418
- let line = typeof technique.line === 'function' ? (technique.line(ctx) ?? null) : (technique.line ?? null);
419
- let snippet = null;
420
- // CTO-04: only compute evidence on failed checks (cheap, and only where it adds trust).
421
- if (passed === false) {
422
- const evidence = resolveEvidence(key, ctx, { file, line });
423
- if (evidence) {
424
- file = evidence.file;
425
- line = evidence.line;
426
- snippet = evidence.snippet;
427
- }
428
- }
429
- results.push({
430
- key,
431
- ...technique,
432
- file,
433
- line: Number.isFinite(line) ? line : null,
434
- snippet,
435
- passed,
436
- });
437
494
  }
438
495
 
439
- if (largeInstructionFiles.length > 0) {
496
+ if (!shallowRiskOnly && largeInstructionFiles.length > 0) {
440
497
  results.push({
441
498
  key: 'largeInstructionFile',
442
499
  id: null,
@@ -505,8 +562,10 @@ async function audit(options) {
505
562
  const scaffoldedPassed = passed.filter(r => scaffoldedKeys.has(r.key));
506
563
  const organicEarned = organicPassed.reduce((sum, r) => sum + (WEIGHTS[r.impact] || 5), 0);
507
564
  const organicScore = maxScore > 0 ? Math.round((organicEarned / maxScore) * 100) : 0;
508
- const quickWins = getQuickWins(failed, { platform: spec.platform });
509
- const topNextActions = buildTopNextActions(failed, 5, outcomeSummary.byKey, { platform: spec.platform, fpFeedbackByKey: fpFeedback.byKey });
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 });
510
569
 
511
570
  // CTO-04: enrich top actions with file/line/snippet from the corresponding
512
571
  // result record (evidence was resolved above during the check loop).
@@ -536,10 +595,10 @@ async function audit(options) {
536
595
  action.projectedOrganicScoreDelta = 0;
537
596
  }
538
597
  }
539
- const categoryScores = computeCategoryScores(applicable, passed);
540
- const platformScopeNote = getPlatformScopeNote(spec, ctx);
541
- const platformCaveats = getPlatformCaveats(spec, ctx);
542
- const deprecationWarnings = detectDeprecationWarnings(failed, packageVersion);
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);
543
602
  const warnings = [
544
603
  ...largeInstructionFiles.map((item) => ({
545
604
  kind: 'large-instruction-file',
@@ -557,7 +616,7 @@ async function audit(options) {
557
616
  ...item,
558
617
  })),
559
618
  ];
560
- const recommendedDomainPacks = spec.platform === 'codex'
619
+ const recommendedDomainPacks = !shallowRiskOnly && spec.platform === 'codex'
561
620
  ? detectCodexDomainPacks(ctx, stacks, getCodexDomainPackSignals(ctx))
562
621
  : [];
563
622
 
@@ -568,7 +627,7 @@ async function audit(options) {
568
627
  stackKeys.has('nextjs') || stackKeys.has('angular') || stackKeys.has('svelte') ||
569
628
  stackKeys.has('nestjs') || stackKeys.has('remix') || stackKeys.has('astro') ||
570
629
  stackKeys.has('typescript') || stackKeys.has('deno') || stackKeys.has('bun');
571
- if (!hasNodeStack) {
630
+ if (!shallowRiskOnly && !hasNodeStack) {
572
631
  let preferredTest = null;
573
632
  let preferredInstall = null;
574
633
  if (stackKeys.has('python') || stackKeys.has('django') || stackKeys.has('fastapi')) {
@@ -620,7 +679,7 @@ async function audit(options) {
620
679
  sunsetDate: r.sunsetDate || null,
621
680
  })),
622
681
  categoryScores,
623
- scoreCoaching: buildScoreCoaching({
682
+ scoreCoaching: shallowRiskOnly ? null : buildScoreCoaching({
624
683
  score,
625
684
  earnedPoints: earnedScore,
626
685
  maxPoints: maxScore,
@@ -645,6 +704,9 @@ async function audit(options) {
645
704
  // CTO-08: per-layer coverage summary (governance/drift/hygiene/shallow-risk).
646
705
  layerSummary: summarizeLayers(activeResults),
647
706
  };
707
+ if (shallowRiskEnabled) {
708
+ result.shallowRiskHints = runShallowRisk(ctx);
709
+ }
648
710
  // Detect which AI config files are present
649
711
  const configFiles = [];
650
712
  const configChecks = [
@@ -661,7 +723,9 @@ async function audit(options) {
661
723
  }
662
724
  result.detectedConfigFiles = configFiles;
663
725
 
664
- result.suggestedNextCommand = inferSuggestedNextCommand(result);
726
+ result.suggestedNextCommand = shallowRiskOnly
727
+ ? 'nerviq audit --shallow-risk --full'
728
+ : inferSuggestedNextCommand(result);
665
729
  result.liteSummary = {
666
730
  topNextActions: topNextActions.slice(0, 3),
667
731
  nextCommand: result.suggestedNextCommand,
@@ -710,6 +774,11 @@ async function audit(options) {
710
774
  return result;
711
775
  }
712
776
 
777
+ if (shallowRiskOnly) {
778
+ printShallowRiskOnly(result, options.dir);
779
+ return result;
780
+ }
781
+
713
782
  if (options.lite) {
714
783
  printLiteAudit(result, options.dir);
715
784
  sendInsights(result);
@@ -798,9 +867,8 @@ async function audit(options) {
798
867
  const layerOrder = [LAYERS.GOVERNANCE, LAYERS.DRIFT, LAYERS.HYGIENE, LAYERS.SHALLOW_RISK];
799
868
  for (const layer of layerOrder) {
800
869
  const b = layerSummary[layer] || { total: 0, passed: 0, failed: 0, skipped: 0 };
801
- const reservedNote = layer === LAYERS.SHALLOW_RISK && b.total === 0
802
- ? ' (reserved for --shallow-risk)' : '';
803
- console.log(colorize(` ${layer}: ${b.total} checks (${b.passed} passed, ${b.failed} failed)${reservedNote}`, 'dim'));
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'));
804
872
  }
805
873
  console.log('');
806
874
 
@@ -895,6 +963,8 @@ async function audit(options) {
895
963
  console.log('');
896
964
  }
897
965
 
966
+ printShallowRiskSection(result);
967
+
898
968
  const terminology = formatTerminologyLines(collectAuditTerminology(result));
899
969
  if (terminology.length > 0) {
900
970
  terminology.forEach((line) => {