@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.
- package/CHANGELOG.md +1407 -0
- package/README.md +4 -4
- package/SECURITY.md +82 -0
- package/bin/cli.js +13 -1
- package/contracts/audit-webhook-event.schema.json +138 -0
- package/contracts/pack-contract.schema.json +15 -0
- package/contracts/technique-contract.schema.json +18 -0
- package/docs/ARCHITECTURE.md +74 -0
- package/docs/api-reference.md +356 -0
- package/docs/autofix.md +64 -0
- package/docs/bitbucket-pipe.yml +57 -0
- package/docs/case-studies.md +149 -0
- package/docs/category-definition-kit.md +56 -0
- package/docs/ci-integration.md +127 -0
- package/docs/claude-code-style.md +24 -0
- package/docs/claude-maintainer-ops.md +19 -0
- package/docs/external-validation.md +78 -0
- package/docs/first-tier-integration-gate.md +59 -0
- package/docs/getting-started.md +119 -0
- package/docs/gitlab-ci-template.yml +54 -0
- package/docs/index.html +597 -0
- package/docs/integration-contracts.md +287 -0
- package/docs/license-faq.md +53 -0
- package/docs/maintenance.md +155 -0
- package/docs/methodology.md +236 -0
- package/docs/new-platform-guide.md +202 -0
- package/docs/open-vsx-publishing.md +46 -0
- package/docs/platform-change-ingestion.md +46 -0
- package/docs/plugins.md +101 -0
- package/docs/pre-commit.md +58 -0
- package/docs/security-model.md +63 -0
- package/docs/shallow-risk.md +246 -0
- package/docs/versioning-policy.md +63 -0
- package/docs/why-nerviq.md +82 -0
- package/package.json +7 -2
- package/sdk/README.md +190 -0
- package/src/audit/layers.js +180 -179
- package/src/audit.js +118 -48
- package/src/codex/setup.js +3 -2
- package/src/formatters/csv.js +86 -85
- package/src/formatters/junit.js +123 -103
- package/src/formatters/markdown.js +164 -135
- package/src/gemini/setup.js +3 -2
- package/src/init.js +4 -3
- package/src/opencode/context.js +42 -3
- package/src/opencode/techniques.js +198 -142
- package/src/output-icons.js +44 -0
- package/src/setup/runtime.js +6 -5
- package/src/setup.js +4 -3
- package/src/shallow-risk/index.js +56 -0
- package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +50 -0
- package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +46 -0
- package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +46 -0
- package/src/shallow-risk/patterns/agent-config-missing-file.js +72 -0
- package/src/shallow-risk/patterns/agent-config-secret-literal.js +49 -0
- package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +34 -0
- package/src/shallow-risk/patterns/hook-script-missing.js +70 -0
- package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +52 -0
- package/src/shallow-risk/shared.js +520 -0
package/src/audit/layers.js
CHANGED
|
@@ -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 —
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
[LAYERS.
|
|
50
|
-
[LAYERS.
|
|
51
|
-
[LAYERS.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
*
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
/
|
|
67
|
-
|
|
68
|
-
/
|
|
69
|
-
/
|
|
70
|
-
/
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
/\
|
|
84
|
-
/\
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
/\
|
|
88
|
-
/\
|
|
89
|
-
/\
|
|
90
|
-
/\
|
|
91
|
-
/\
|
|
92
|
-
/\
|
|
93
|
-
/\
|
|
94
|
-
/\
|
|
95
|
-
/\
|
|
96
|
-
/\
|
|
97
|
-
/\
|
|
98
|
-
/\
|
|
99
|
-
/\b
|
|
100
|
-
/\
|
|
101
|
-
/\
|
|
102
|
-
/\
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
'
|
|
114
|
-
'
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* @param {
|
|
134
|
-
* @
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (!
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
*
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
[LAYERS.
|
|
158
|
-
[LAYERS.
|
|
159
|
-
[LAYERS.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
bucket
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
else bucket.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
369
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
|
410
|
-
line: null,
|
|
411
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
802
|
-
|
|
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) => {
|