@nerviq/cli 1.23.0 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/package.json +1 -1
- package/src/aider/techniques.js +4 -0
- package/src/audit/layers.js +179 -0
- package/src/audit/recommendations.js +2 -1
- package/src/audit.js +22 -3
- package/src/codex/techniques.js +4 -0
- package/src/copilot/techniques.js +4 -0
- package/src/cursor/techniques.js +4 -0
- package/src/formatters/csv.js +6 -1
- package/src/formatters/junit.js +6 -3
- package/src/formatters/markdown.js +8 -3
- package/src/gemini/techniques.js +4 -0
- package/src/opencode/techniques.js +31 -27
- package/src/plugins.js +5 -0
- package/src/techniques/automation.js +42 -5
- package/src/techniques/instructions.js +28 -5
- package/src/techniques/tools.js +24 -3
- package/src/techniques.js +23 -0
- package/src/windsurf/techniques.js +4 -0
package/README.md
CHANGED
|
@@ -70,6 +70,17 @@ When your repo has 2+ platforms configured, `nerviq audit` leads with the Harmon
|
|
|
70
70
|
|
|
71
71
|
Single-platform repos still work the same way — the Harmony Score only appears when 2+ platforms are detected. Use `--no-harmony-first` to suppress it even on multi-platform repos.
|
|
72
72
|
|
|
73
|
+
### Scope — the 4 layers
|
|
74
|
+
|
|
75
|
+
Every Nerviq check is tagged with one of four explicit layers so you know exactly what the tool claims (and does not claim) to cover:
|
|
76
|
+
|
|
77
|
+
- **governance** — agent configuration posture: presence, content, and quality of agent-instruction files and platform settings.
|
|
78
|
+
- **drift** — cross-platform consistency: do your configured platforms agree, and does declared state match repo reality?
|
|
79
|
+
- **hygiene** — repo-level cleanliness adjacent to agents (gitignore, CHANGELOG, SECURITY.md, LICENSE, Node version pinning, etc.).
|
|
80
|
+
- **shallow-risk** — reserved for obvious agent-config ↔ codebase boundary issues (CTO-06, not yet populated).
|
|
81
|
+
|
|
82
|
+
There is deliberately no "deep-review" or general-security-scanning layer — Nerviq is an agent-configuration audit tool, not a code-review tool. The full taxonomy and disambiguation rules live in `docs/integration-contracts.md §8`, and the `layer` field is surfaced in every output format (JSON, CSV, JUnit, Markdown, text).
|
|
83
|
+
|
|
73
84
|
## Quick Start
|
|
74
85
|
|
|
75
86
|
```bash
|
|
@@ -223,8 +234,8 @@ All successful operational responses are wrapped in a JSON envelope:
|
|
|
223
234
|
{
|
|
224
235
|
"data": {},
|
|
225
236
|
"meta": {
|
|
226
|
-
"version": "1.
|
|
227
|
-
"timestamp": "2026-04-
|
|
237
|
+
"version": "1.25.0",
|
|
238
|
+
"timestamp": "2026-04-15T10:00:00.000Z"
|
|
228
239
|
}
|
|
229
240
|
}
|
|
230
241
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerviq/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"description": "The intelligent nervous system for AI coding agents — 2,441 checks (8 platforms × ~300 governance rules), 10 languages, 62 domain packs. Audit, align, and amplify.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/aider/techniques.js
CHANGED
|
@@ -3456,6 +3456,10 @@ Object.assign(AIDER_TECHNIQUES, buildStackChecks({
|
|
|
3456
3456
|
|
|
3457
3457
|
attachSourceUrls('aider', AIDER_TECHNIQUES);
|
|
3458
3458
|
|
|
3459
|
+
// CTO-08 — tag every check with a scope layer.
|
|
3460
|
+
const { LAYERS: AIDER_LAYERS, assignLayers: aiderAssignLayers } = require('../audit/layers');
|
|
3461
|
+
aiderAssignLayers(AIDER_TECHNIQUES, AIDER_LAYERS.GOVERNANCE);
|
|
3462
|
+
|
|
3459
3463
|
module.exports = {
|
|
3460
3464
|
AIDER_TECHNIQUES,
|
|
3461
3465
|
};
|
|
@@ -0,0 +1,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 — 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
|
+
};
|
|
@@ -346,7 +346,7 @@ function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, option
|
|
|
346
346
|
return scoreB - scoreA;
|
|
347
347
|
})
|
|
348
348
|
.slice(0, limit)
|
|
349
|
-
.map(({ key, id, name, impact, fix, category, sourceUrl }) => {
|
|
349
|
+
.map(({ key, id, name, impact, fix, category, sourceUrl, layer }) => {
|
|
350
350
|
const feedback = outcomeSummaryByKey[key] || null;
|
|
351
351
|
const rankingAdjustment = getRecommendationAdjustment(outcomeSummaryByKey, key);
|
|
352
352
|
const signals = [
|
|
@@ -376,6 +376,7 @@ function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, option
|
|
|
376
376
|
name,
|
|
377
377
|
impact,
|
|
378
378
|
category,
|
|
379
|
+
layer, // CTO-08: surface scope layer on every next-action
|
|
379
380
|
sourceUrl,
|
|
380
381
|
module: CATEGORY_MODULES[category] || category,
|
|
381
382
|
fix,
|
package/src/audit.js
CHANGED
|
@@ -36,6 +36,7 @@ const { loadPlugins, mergePluginChecks } = require('./plugins');
|
|
|
36
36
|
const { detectDeprecationWarnings } = require('./deprecation');
|
|
37
37
|
const { buildWorkspaceHint, formatCount, guardSkippedInstructionFiles, inspectInstructionFiles } = require('./audit/instruction-files');
|
|
38
38
|
const { resolveEvidence } = require('./audit/evidence');
|
|
39
|
+
const { LAYERS, summarizeLayers } = require('./audit/layers');
|
|
39
40
|
const {
|
|
40
41
|
WEIGHTS,
|
|
41
42
|
buildScoreCoaching,
|
|
@@ -441,6 +442,7 @@ async function audit(options) {
|
|
|
441
442
|
id: null,
|
|
442
443
|
name: 'Large instruction file warning',
|
|
443
444
|
category: 'performance',
|
|
445
|
+
layer: LAYERS.GOVERNANCE,
|
|
444
446
|
impact: 'medium',
|
|
445
447
|
rating: null,
|
|
446
448
|
fix: 'Split oversized instruction files so they stay under ~12,000 tokens, and keep any single instruction file below ~240,000 tokens.',
|
|
@@ -640,6 +642,8 @@ async function audit(options) {
|
|
|
640
642
|
platformScopeNote,
|
|
641
643
|
platformCaveats,
|
|
642
644
|
recommendedDomainPacks,
|
|
645
|
+
// CTO-08: per-layer coverage summary (governance/drift/hygiene/shallow-risk).
|
|
646
|
+
layerSummary: summarizeLayers(activeResults),
|
|
643
647
|
};
|
|
644
648
|
// Detect which AI config files are present
|
|
645
649
|
const configFiles = [];
|
|
@@ -788,6 +792,18 @@ async function audit(options) {
|
|
|
788
792
|
}
|
|
789
793
|
console.log('');
|
|
790
794
|
|
|
795
|
+
// CTO-08: Coverage by layer — explicit map of what NERVIQ covers.
|
|
796
|
+
const layerSummary = result.layerSummary || summarizeLayers(activeResults);
|
|
797
|
+
console.log(colorize(' Coverage by layer:', 'bold'));
|
|
798
|
+
const layerOrder = [LAYERS.GOVERNANCE, LAYERS.DRIFT, LAYERS.HYGIENE, LAYERS.SHALLOW_RISK];
|
|
799
|
+
for (const layer of layerOrder) {
|
|
800
|
+
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'));
|
|
804
|
+
}
|
|
805
|
+
console.log('');
|
|
806
|
+
|
|
791
807
|
// Passed
|
|
792
808
|
if (passed.length > 0) {
|
|
793
809
|
console.log(colorize(' ✅ Passing', 'green'));
|
|
@@ -813,7 +829,8 @@ async function audit(options) {
|
|
|
813
829
|
console.log(colorize(' 🔴 Critical (fix immediately)', 'red'));
|
|
814
830
|
for (const r of critical) {
|
|
815
831
|
const conf = r.confidence ? ` [${confidenceLabel(r.confidence)}]` : '';
|
|
816
|
-
|
|
832
|
+
const layerPrefix = r.layer ? colorize(`[${r.layer}] `, 'dim') : '';
|
|
833
|
+
console.log(` ${layerPrefix}${colorize(r.name, 'bold')}${colorize(conf, 'dim')}`);
|
|
817
834
|
if (r.file) {
|
|
818
835
|
console.log(colorize(` at ${formatLocation(r.file, r.line)}`, 'dim'));
|
|
819
836
|
}
|
|
@@ -826,7 +843,8 @@ async function audit(options) {
|
|
|
826
843
|
console.log(colorize(' 🟡 High Impact', 'yellow'));
|
|
827
844
|
for (const r of high) {
|
|
828
845
|
const conf = r.confidence ? ` [${confidenceLabel(r.confidence)}]` : '';
|
|
829
|
-
|
|
846
|
+
const layerPrefix = r.layer ? colorize(`[${r.layer}] `, 'dim') : '';
|
|
847
|
+
console.log(` ${layerPrefix}${colorize(r.name, 'bold')}${colorize(conf, 'dim')}`);
|
|
830
848
|
if (r.file) {
|
|
831
849
|
console.log(colorize(` at ${formatLocation(r.file, r.line)}`, 'dim'));
|
|
832
850
|
}
|
|
@@ -839,7 +857,8 @@ async function audit(options) {
|
|
|
839
857
|
console.log(colorize(' 🔵 Recommended', 'blue'));
|
|
840
858
|
for (const r of medium) {
|
|
841
859
|
const conf = r.confidence ? ` [${confidenceLabel(r.confidence)}]` : '';
|
|
842
|
-
|
|
860
|
+
const layerPrefix = r.layer ? colorize(`[${r.layer}] `, 'dim') : '';
|
|
861
|
+
console.log(` ${layerPrefix}${colorize(r.name, 'bold')}${colorize(conf, 'dim')}`);
|
|
843
862
|
if (r.file) {
|
|
844
863
|
console.log(colorize(` at ${formatLocation(r.file, r.line)}`, 'dim'));
|
|
845
864
|
}
|
package/src/codex/techniques.js
CHANGED
|
@@ -4886,6 +4886,10 @@ Object.assign(CODEX_TECHNIQUES, buildSupplementalChecks({
|
|
|
4886
4886
|
|
|
4887
4887
|
attachSourceUrls('codex', CODEX_TECHNIQUES);
|
|
4888
4888
|
|
|
4889
|
+
// CTO-08 — tag every check with a scope layer.
|
|
4890
|
+
const { LAYERS: CODEX_LAYERS, assignLayers: codexAssignLayers } = require('../audit/layers');
|
|
4891
|
+
codexAssignLayers(CODEX_TECHNIQUES, CODEX_LAYERS.GOVERNANCE);
|
|
4892
|
+
|
|
4889
4893
|
module.exports = {
|
|
4890
4894
|
CODEX_TECHNIQUES,
|
|
4891
4895
|
};
|
|
@@ -3569,6 +3569,10 @@ Object.assign(COPILOT_TECHNIQUES, buildStackChecks({
|
|
|
3569
3569
|
|
|
3570
3570
|
attachSourceUrls('copilot', COPILOT_TECHNIQUES);
|
|
3571
3571
|
|
|
3572
|
+
// CTO-08 — tag every check with a scope layer.
|
|
3573
|
+
const { LAYERS: COPILOT_LAYERS, assignLayers: copilotAssignLayers } = require('../audit/layers');
|
|
3574
|
+
copilotAssignLayers(COPILOT_TECHNIQUES, COPILOT_LAYERS.GOVERNANCE);
|
|
3575
|
+
|
|
3572
3576
|
module.exports = {
|
|
3573
3577
|
COPILOT_TECHNIQUES,
|
|
3574
3578
|
};
|
package/src/cursor/techniques.js
CHANGED
|
@@ -3726,6 +3726,10 @@ Object.assign(CURSOR_TECHNIQUES, buildStackChecks({
|
|
|
3726
3726
|
|
|
3727
3727
|
attachSourceUrls('cursor', CURSOR_TECHNIQUES);
|
|
3728
3728
|
|
|
3729
|
+
// CTO-08 — tag every check with a scope layer.
|
|
3730
|
+
const { LAYERS: CURSOR_LAYERS, assignLayers: cursorAssignLayers } = require('../audit/layers');
|
|
3731
|
+
cursorAssignLayers(CURSOR_TECHNIQUES, CURSOR_LAYERS.GOVERNANCE);
|
|
3732
|
+
|
|
3729
3733
|
module.exports = {
|
|
3730
3734
|
CURSOR_TECHNIQUES,
|
|
3731
3735
|
};
|
package/src/formatters/csv.js
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
* CSV Formatter (RFC 4180)
|
|
3
3
|
*
|
|
4
4
|
* One row per check in a nerviq audit result.
|
|
5
|
-
* Columns: key,id,name,category,rating,severity,passed,file,line,sourceUrl,fix
|
|
5
|
+
* Columns: key,id,name,category,layer,rating,severity,passed,file,line,sourceUrl,fix
|
|
6
|
+
*
|
|
7
|
+
* The `layer` column (added in CTO-08) is one of 'governance',
|
|
8
|
+
* 'drift', 'hygiene', or 'shallow-risk' — see docs/integration-contracts.md §8.
|
|
6
9
|
*
|
|
7
10
|
* Quoting rules (RFC 4180):
|
|
8
11
|
* - Fields containing comma, double-quote, CR, or LF are wrapped in
|
|
@@ -21,6 +24,7 @@ const COLUMNS = [
|
|
|
21
24
|
'id',
|
|
22
25
|
'name',
|
|
23
26
|
'category',
|
|
27
|
+
'layer',
|
|
24
28
|
'rating',
|
|
25
29
|
'severity',
|
|
26
30
|
'passed',
|
|
@@ -49,6 +53,7 @@ function rowFor(r, projections = null) {
|
|
|
49
53
|
r.id ?? '',
|
|
50
54
|
r.name ?? '',
|
|
51
55
|
r.category ?? '',
|
|
56
|
+
r.layer ?? '',
|
|
52
57
|
r.rating ?? '',
|
|
53
58
|
severity,
|
|
54
59
|
r.passed === null || r.passed === undefined ? '' : String(r.passed),
|
package/src/formatters/junit.js
CHANGED
|
@@ -70,6 +70,9 @@ function formatJUnit(auditResult) {
|
|
|
70
70
|
for (const r of checks) {
|
|
71
71
|
const classname = escapeXml(r.category || 'uncategorized');
|
|
72
72
|
const name = escapeXml(r.key || r.id || r.name || 'unknown');
|
|
73
|
+
// CTO-08: surface scope layer as a testcase attribute so JUnit
|
|
74
|
+
// consumers (GitHub Actions, Jenkins, GitLab) can filter/group.
|
|
75
|
+
const layerAttr = r.layer ? ` layer="${escapeXml(r.layer)}"` : '';
|
|
73
76
|
if (r.passed === false) {
|
|
74
77
|
const msg = escapeXml(r.fix || r.name || r.key || 'check failed');
|
|
75
78
|
const type = escapeXml(severityFor(r));
|
|
@@ -77,15 +80,15 @@ function formatJUnit(auditResult) {
|
|
|
77
80
|
if (r.file) body += ` at ${r.file}${r.line ? ':' + r.line : ''}`;
|
|
78
81
|
if (r.sourceUrl) body += ` (${r.sourceUrl})`;
|
|
79
82
|
if (r.snippet) body += `\n---\n${r.snippet}`;
|
|
80
|
-
lines.push(` <testcase classname="${classname}" name="${name}" time="0">`);
|
|
83
|
+
lines.push(` <testcase classname="${classname}" name="${name}"${layerAttr} time="0">`);
|
|
81
84
|
lines.push(` <failure message="${msg}" type="${type}">${escapeXml(body)}</failure>`);
|
|
82
85
|
lines.push(` </testcase>`);
|
|
83
86
|
} else if (r.passed === null || r.skipped === true) {
|
|
84
|
-
lines.push(` <testcase classname="${classname}" name="${name}" time="0">`);
|
|
87
|
+
lines.push(` <testcase classname="${classname}" name="${name}"${layerAttr} time="0">`);
|
|
85
88
|
lines.push(` <skipped/>`);
|
|
86
89
|
lines.push(` </testcase>`);
|
|
87
90
|
} else {
|
|
88
|
-
lines.push(` <testcase classname="${classname}" name="${name}" time="0"/>`);
|
|
91
|
+
lines.push(` <testcase classname="${classname}" name="${name}"${layerAttr} time="0"/>`);
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
94
|
|
|
@@ -78,7 +78,10 @@ function formatMarkdown(auditResult, options = {}) {
|
|
|
78
78
|
if (Number.isFinite(item.projectedScoreDelta) && item.projectedScoreDelta > 0) {
|
|
79
79
|
delta = ` (+${item.projectedScoreDelta} pts → ${item.projectedScoreAfter}/100)`;
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
// CTO-08: include scope layer as a small suffix after the key so
|
|
82
|
+
// evaluators see which layer each next-action belongs to.
|
|
83
|
+
const layerSuffix = item.layer ? ` · _layer: ${escapeInline(item.layer)}_` : '';
|
|
84
|
+
lines.push(`- [ ] **[${sev}] ${title}** (\`${key}\`)${loc}${delta}${layerSuffix}`);
|
|
82
85
|
const hint = item.fix || item.hint || '';
|
|
83
86
|
if (hint) {
|
|
84
87
|
lines.push(` - ${escapeInline(hint)}`);
|
|
@@ -103,13 +106,15 @@ function formatMarkdown(auditResult, options = {}) {
|
|
|
103
106
|
lines.push('<details>');
|
|
104
107
|
lines.push(`<summary>All failed checks (${failedResults.length})</summary>`);
|
|
105
108
|
lines.push('');
|
|
106
|
-
|
|
107
|
-
lines.push('|
|
|
109
|
+
// CTO-08: add layer column between category and rating.
|
|
110
|
+
lines.push('| key | name | category | layer | rating | file | line |');
|
|
111
|
+
lines.push('| --- | --- | --- | --- | --- | --- | --- |');
|
|
108
112
|
for (const r of failedResults) {
|
|
109
113
|
const row = [
|
|
110
114
|
escapeCell(r.key),
|
|
111
115
|
escapeCell(r.name),
|
|
112
116
|
escapeCell(r.category),
|
|
117
|
+
escapeCell(r.layer || ''),
|
|
113
118
|
escapeCell(r.rating ?? ''),
|
|
114
119
|
escapeCell(r.file || ''),
|
|
115
120
|
escapeCell(r.line || ''),
|
package/src/gemini/techniques.js
CHANGED
|
@@ -3802,6 +3802,10 @@ Object.assign(GEMINI_TECHNIQUES, buildStackChecks({
|
|
|
3802
3802
|
|
|
3803
3803
|
attachSourceUrls('gemini', GEMINI_TECHNIQUES);
|
|
3804
3804
|
|
|
3805
|
+
// CTO-08 — tag every check with a scope layer.
|
|
3806
|
+
const { LAYERS: GEMINI_LAYERS, assignLayers: geminiAssignLayers } = require('../audit/layers');
|
|
3807
|
+
geminiAssignLayers(GEMINI_TECHNIQUES, GEMINI_LAYERS.GOVERNANCE);
|
|
3808
|
+
|
|
3805
3809
|
module.exports = {
|
|
3806
3810
|
GEMINI_TECHNIQUES,
|
|
3807
3811
|
};
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
const os = require('os');
|
|
24
|
-
const path = require('path');
|
|
25
|
-
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
26
|
-
const { attachSourceUrls } = require('../source-urls');
|
|
27
|
-
const { buildStackChecks } = require('../stack-checks');
|
|
28
|
-
const { isApiProject, isDatabaseProject, isAuthProject, isMonitoringRelevant } = require('../supplemental-checks');
|
|
29
|
-
const { hasCostBudgetOrUsageTracking } = require('../cost-tracking');
|
|
30
|
-
const { resolveProjectStateReadPath } = require('../state-paths');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
26
|
+
const { attachSourceUrls } = require('../source-urls');
|
|
27
|
+
const { buildStackChecks } = require('../stack-checks');
|
|
28
|
+
const { isApiProject, isDatabaseProject, isAuthProject, isMonitoringRelevant } = require('../supplemental-checks');
|
|
29
|
+
const { hasCostBudgetOrUsageTracking } = require('../cost-tracking');
|
|
30
|
+
const { resolveProjectStateReadPath } = require('../state-paths');
|
|
31
31
|
|
|
32
32
|
const DEFAULT_PROJECT_DOC_MAX_BYTES = 32768;
|
|
33
33
|
|
|
@@ -1460,15 +1460,15 @@ const OPENCODE_TECHNIQUES = {
|
|
|
1460
1460
|
line: () => null,
|
|
1461
1461
|
},
|
|
1462
1462
|
|
|
1463
|
-
opencodePilotEvidence: {
|
|
1464
|
-
id: 'OC-M03',
|
|
1465
|
-
name: 'OpenCode setup has been audited at least once',
|
|
1466
|
-
check: (ctx) => {
|
|
1467
|
-
// Check for nerviq activity artifacts
|
|
1468
|
-
const fs = require('fs');
|
|
1469
|
-
const hasArtifacts = fs.existsSync(resolveProjectStateReadPath(ctx.dir));
|
|
1470
|
-
return hasArtifacts ? true : null;
|
|
1471
|
-
},
|
|
1463
|
+
opencodePilotEvidence: {
|
|
1464
|
+
id: 'OC-M03',
|
|
1465
|
+
name: 'OpenCode setup has been audited at least once',
|
|
1466
|
+
check: (ctx) => {
|
|
1467
|
+
// Check for nerviq activity artifacts
|
|
1468
|
+
const fs = require('fs');
|
|
1469
|
+
const hasArtifacts = fs.existsSync(resolveProjectStateReadPath(ctx.dir));
|
|
1470
|
+
return hasArtifacts ? true : null;
|
|
1471
|
+
},
|
|
1472
1472
|
impact: 'low',
|
|
1473
1473
|
rating: 2,
|
|
1474
1474
|
category: 'governance',
|
|
@@ -2017,17 +2017,17 @@ const OPENCODE_TECHNIQUES = {
|
|
|
2017
2017
|
fix: 'Document prompt caching in opencode.json or AGENTS.md.',
|
|
2018
2018
|
template: null, file: () => 'opencode.json', line: () => null,
|
|
2019
2019
|
},
|
|
2020
|
-
ocCostBudgetDefined: {
|
|
2021
|
-
id: 'OC-T48', name: 'AI cost budget or per-run usage tracking documented',
|
|
2022
|
-
check: (ctx) => {
|
|
2023
|
-
const docs = docsBundle(ctx) + (ctx.fileContent('README.md') || '');
|
|
2024
|
-
if (!docs.trim() && !hasCostBudgetOrUsageTracking('', ctx)) return null;
|
|
2025
|
-
return hasCostBudgetOrUsageTracking(docs, ctx);
|
|
2026
|
-
},
|
|
2027
|
-
impact: 'low', rating: 2, category: 'cost-optimization',
|
|
2028
|
-
fix: 'Document AI cost guardrails or per-run usage tracking so OpenCode usage is measurable over time.',
|
|
2029
|
-
template: null, file: () => 'README.md', line: () => null,
|
|
2030
|
-
},
|
|
2020
|
+
ocCostBudgetDefined: {
|
|
2021
|
+
id: 'OC-T48', name: 'AI cost budget or per-run usage tracking documented',
|
|
2022
|
+
check: (ctx) => {
|
|
2023
|
+
const docs = docsBundle(ctx) + (ctx.fileContent('README.md') || '');
|
|
2024
|
+
if (!docs.trim() && !hasCostBudgetOrUsageTracking('', ctx)) return null;
|
|
2025
|
+
return hasCostBudgetOrUsageTracking(docs, ctx);
|
|
2026
|
+
},
|
|
2027
|
+
impact: 'low', rating: 2, category: 'cost-optimization',
|
|
2028
|
+
fix: 'Document AI cost guardrails or per-run usage tracking so OpenCode usage is measurable over time.',
|
|
2029
|
+
template: null, file: () => 'README.md', line: () => null,
|
|
2030
|
+
},
|
|
2031
2031
|
|
|
2032
2032
|
// ============================================================
|
|
2033
2033
|
// === PYTHON STACK CHECKS (category: 'python') ===============
|
|
@@ -3503,6 +3503,10 @@ Object.assign(OPENCODE_TECHNIQUES, buildStackChecks({
|
|
|
3503
3503
|
|
|
3504
3504
|
attachSourceUrls('opencode', OPENCODE_TECHNIQUES);
|
|
3505
3505
|
|
|
3506
|
+
// CTO-08 — tag every check with a scope layer.
|
|
3507
|
+
const { LAYERS: OC_LAYERS, assignLayers: ocAssignLayers } = require('../audit/layers');
|
|
3508
|
+
ocAssignLayers(OPENCODE_TECHNIQUES, OC_LAYERS.GOVERNANCE);
|
|
3509
|
+
|
|
3506
3510
|
module.exports = {
|
|
3507
3511
|
OPENCODE_TECHNIQUES,
|
|
3508
3512
|
};
|
package/src/plugins.js
CHANGED
|
@@ -90,6 +90,7 @@ function loadPlugins(dir) {
|
|
|
90
90
|
* Returns a new merged techniques object (does not mutate the original).
|
|
91
91
|
*/
|
|
92
92
|
function mergePluginChecks(techniques, plugins) {
|
|
93
|
+
const { LAYERS, assignLayers } = require('./audit/layers');
|
|
93
94
|
const merged = { ...techniques };
|
|
94
95
|
|
|
95
96
|
for (const plugin of plugins) {
|
|
@@ -104,6 +105,10 @@ function mergePluginChecks(techniques, plugins) {
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
// CTO-08 — plugins may not set a layer. Default their checks to
|
|
109
|
+
// governance (drift/hygiene heuristics still apply via name).
|
|
110
|
+
assignLayers(merged, LAYERS.GOVERNANCE);
|
|
111
|
+
|
|
107
112
|
return merged;
|
|
108
113
|
}
|
|
109
114
|
|
|
@@ -8,6 +8,27 @@ const {
|
|
|
8
8
|
readProjectFiles,
|
|
9
9
|
} = require('./shared');
|
|
10
10
|
|
|
11
|
+
// PP-06 recalibration helpers: opt-in signals. Repos with no infra/hooks
|
|
12
|
+
// signal at all get N/A instead of a hard fail on opt-in advisories.
|
|
13
|
+
function _repoHasInfraSignal(ctx) {
|
|
14
|
+
return ctx.files.some(f => /^Dockerfile/i.test(f))
|
|
15
|
+
|| ctx.files.some(f => /^docker-compose\.(yml|yaml)$/i.test(f))
|
|
16
|
+
|| ctx.files.some(f => /\.tf$/.test(f))
|
|
17
|
+
|| ctx.files.includes('main.tf')
|
|
18
|
+
|| ctx.hasDir('k8s')
|
|
19
|
+
|| ctx.hasDir('kubernetes')
|
|
20
|
+
|| ctx.hasDir('infra')
|
|
21
|
+
|| ctx.hasDir('terraform')
|
|
22
|
+
|| ctx.hasDir('deploy');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function _repoHasHooksBlock(ctx) {
|
|
26
|
+
const shared = ctx.jsonFile('.claude/settings.json') || {};
|
|
27
|
+
const local = ctx.jsonFile('.claude/settings.local.json') || {};
|
|
28
|
+
return !!((shared.hooks && Object.keys(shared.hooks).length > 0)
|
|
29
|
+
|| (local.hooks && Object.keys(local.hooks).length > 0));
|
|
30
|
+
}
|
|
31
|
+
|
|
11
32
|
module.exports = {
|
|
12
33
|
hooks: {
|
|
13
34
|
id: 19,
|
|
@@ -91,7 +112,11 @@ module.exports = {
|
|
|
91
112
|
dockerfile: {
|
|
92
113
|
id: 399,
|
|
93
114
|
name: 'Has Dockerfile',
|
|
94
|
-
check: (ctx) =>
|
|
115
|
+
check: (ctx) => {
|
|
116
|
+
if (ctx.files.some(f => /^Dockerfile/i.test(f))) return true;
|
|
117
|
+
// PP-06 recalibration: N/A on repos with no infra signal at all.
|
|
118
|
+
return _repoHasInfraSignal(ctx) ? false : null;
|
|
119
|
+
},
|
|
95
120
|
impact: 'medium',
|
|
96
121
|
rating: 3,
|
|
97
122
|
category: 'devops',
|
|
@@ -102,7 +127,11 @@ module.exports = {
|
|
|
102
127
|
dockerCompose: {
|
|
103
128
|
id: 39901,
|
|
104
129
|
name: 'Has docker-compose.yml',
|
|
105
|
-
check: (ctx) =>
|
|
130
|
+
check: (ctx) => {
|
|
131
|
+
if (ctx.files.some(f => /^docker-compose\.(yml|yaml)$/i.test(f))) return true;
|
|
132
|
+
// PP-06 recalibration: N/A on repos with no infra signal at all.
|
|
133
|
+
return _repoHasInfraSignal(ctx) ? false : null;
|
|
134
|
+
},
|
|
106
135
|
impact: 'medium',
|
|
107
136
|
rating: 3,
|
|
108
137
|
category: 'devops',
|
|
@@ -126,7 +155,11 @@ module.exports = {
|
|
|
126
155
|
terraformFiles: {
|
|
127
156
|
id: 397,
|
|
128
157
|
name: 'Infrastructure as Code (Terraform)',
|
|
129
|
-
check: (ctx) =>
|
|
158
|
+
check: (ctx) => {
|
|
159
|
+
if (ctx.files.some(f => /\.tf$/.test(f)) || ctx.files.includes('main.tf')) return true;
|
|
160
|
+
// PP-06 recalibration: N/A on repos with no infra signal at all.
|
|
161
|
+
return _repoHasInfraSignal(ctx) ? false : null;
|
|
162
|
+
},
|
|
130
163
|
impact: 'medium',
|
|
131
164
|
rating: 3,
|
|
132
165
|
category: 'devops',
|
|
@@ -290,7 +323,9 @@ module.exports = {
|
|
|
290
323
|
check: (ctx) => {
|
|
291
324
|
const shared = ctx.jsonFile('.claude/settings.json') || {};
|
|
292
325
|
const local = ctx.jsonFile('.claude/settings.local.json') || {};
|
|
293
|
-
|
|
326
|
+
if (shared.hooks?.Notification || local.hooks?.Notification) return true;
|
|
327
|
+
// PP-06 recalibration: N/A unless settings define a hooks block at all.
|
|
328
|
+
return _repoHasHooksBlock(ctx) ? false : null;
|
|
294
329
|
},
|
|
295
330
|
impact: 'low',
|
|
296
331
|
rating: 2,
|
|
@@ -305,7 +340,9 @@ module.exports = {
|
|
|
305
340
|
check: (ctx) => {
|
|
306
341
|
const shared = ctx.jsonFile('.claude/settings.json') || {};
|
|
307
342
|
const local = ctx.jsonFile('.claude/settings.local.json') || {};
|
|
308
|
-
|
|
343
|
+
if (shared.hooks?.SubagentStop || local.hooks?.SubagentStop) return true;
|
|
344
|
+
// PP-06 recalibration: N/A unless settings define a hooks block at all.
|
|
345
|
+
return _repoHasHooksBlock(ctx) ? false : null;
|
|
309
346
|
},
|
|
310
347
|
impact: 'low',
|
|
311
348
|
rating: 2,
|
|
@@ -50,8 +50,17 @@ module.exports = {
|
|
|
50
50
|
name: 'CLAUDE.md uses @path imports for modularity',
|
|
51
51
|
check: (ctx) => {
|
|
52
52
|
const md = ctx.claudeMdContent() || '';
|
|
53
|
-
//
|
|
54
|
-
|
|
53
|
+
// Positive-signal check (PP-06 recalibration): N/A when no CLAUDE.md
|
|
54
|
+
// surface exists, so we don't fail every repo that happens to have a
|
|
55
|
+
// short CLAUDE.md. Only fire as an advisory on long CLAUDE.md files
|
|
56
|
+
// where modular @-imports would genuinely help.
|
|
57
|
+
if (!md) return null;
|
|
58
|
+
const hasImport = /@\S+\.(md|txt|json|yml|yaml|toml)/i.test(md) || /@\w+\//.test(md);
|
|
59
|
+
if (hasImport) return true;
|
|
60
|
+
// Only advise splitting when the CLAUDE.md is long enough to warrant it.
|
|
61
|
+
const lineCount = md.split('\n').length;
|
|
62
|
+
if (lineCount < 80) return null;
|
|
63
|
+
return false;
|
|
55
64
|
},
|
|
56
65
|
impact: 'medium',
|
|
57
66
|
rating: 4,
|
|
@@ -140,8 +149,17 @@ module.exports = {
|
|
|
140
149
|
id: 2002,
|
|
141
150
|
name: 'CLAUDE.local.md for personal overrides',
|
|
142
151
|
check: (ctx) => {
|
|
143
|
-
// CLAUDE.local.md is for personal, non-committed overrides
|
|
144
|
-
|
|
152
|
+
// CLAUDE.local.md is for personal, non-committed overrides.
|
|
153
|
+
const hasLocal = ctx.files.includes('CLAUDE.local.md') || ctx.files.includes('.claude/CLAUDE.local.md');
|
|
154
|
+
if (hasLocal) return true;
|
|
155
|
+
// PP-06 recalibration: N/A when the repo has no personal-overrides
|
|
156
|
+
// convention at all. Only advise creating CLAUDE.local.md when the
|
|
157
|
+
// repo explicitly opts in to that convention (references it in
|
|
158
|
+
// .gitignore or in CLAUDE.md).
|
|
159
|
+
const gitignore = ctx.fileContent('.gitignore') || '';
|
|
160
|
+
const md = ctx.claudeMdContent() || '';
|
|
161
|
+
const mentioned = /CLAUDE\.local\.md/i.test(gitignore) || /CLAUDE\.local\.md/i.test(md);
|
|
162
|
+
return mentioned ? false : null;
|
|
145
163
|
},
|
|
146
164
|
impact: 'low',
|
|
147
165
|
rating: 2,
|
|
@@ -155,7 +173,12 @@ module.exports = {
|
|
|
155
173
|
name: 'Auto-memory or memory management mentioned',
|
|
156
174
|
check: (ctx) => {
|
|
157
175
|
const md = ctx.claudeMdContent() || '';
|
|
158
|
-
|
|
176
|
+
if (/auto.?memory|memory.*manage|remember|persistent.*context/i.test(md)) return true;
|
|
177
|
+
// PP-06 recalibration: N/A on repos that don't use Claude Code memory
|
|
178
|
+
// at all. Only fire the advisory when the repo opts in (mentions memory
|
|
179
|
+
// or has a memory directory under .claude/).
|
|
180
|
+
const opts_in = /\bmemory\b/i.test(md) || ctx.hasDir('.claude/memory');
|
|
181
|
+
return opts_in ? false : null;
|
|
159
182
|
},
|
|
160
183
|
impact: 'low', rating: 3, category: 'memory',
|
|
161
184
|
fix: 'Claude Code supports auto-memory for cross-session learning. Mention your memory strategy if relevant.',
|
package/src/techniques/tools.js
CHANGED
|
@@ -6,6 +6,20 @@
|
|
|
6
6
|
const {
|
|
7
7
|
} = require('./shared');
|
|
8
8
|
|
|
9
|
+
// PP-06 recalibration: opt-in signal for MCP. A repo "opts in" to MCP checks if
|
|
10
|
+
// it has any MCP file (even empty/partial) or mentions MCP in its Claude
|
|
11
|
+
// instructions. Repos with no MCP signal at all get N/A on MCP advisories
|
|
12
|
+
// instead of a hard fail.
|
|
13
|
+
function _repoOptsInToMcp(ctx) {
|
|
14
|
+
if (ctx.files.includes('.mcp.json')) return true;
|
|
15
|
+
const shared = ctx.jsonFile('.claude/settings.json') || {};
|
|
16
|
+
const local = ctx.jsonFile('.claude/settings.local.json') || {};
|
|
17
|
+
if (shared.mcpServers || local.mcpServers) return true;
|
|
18
|
+
const md = ctx.claudeMdContent() || '';
|
|
19
|
+
if (/\bMCP\b|mcpServers|model[\s-]?context[\s-]?protocol/i.test(md)) return true;
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
module.exports = {
|
|
10
24
|
mcpServers: {
|
|
11
25
|
id: 18,
|
|
@@ -16,7 +30,9 @@ module.exports = {
|
|
|
16
30
|
if (mcpJson && mcpJson.mcpServers && Object.keys(mcpJson.mcpServers).length > 0) return true;
|
|
17
31
|
// Fallback: check settings for legacy format
|
|
18
32
|
const settings = ctx.jsonFile('.claude/settings.local.json') || ctx.jsonFile('.claude/settings.json');
|
|
19
|
-
|
|
33
|
+
if (settings && settings.mcpServers && Object.keys(settings.mcpServers).length > 0) return true;
|
|
34
|
+
// PP-06 recalibration: N/A on repos that don't reference MCP at all.
|
|
35
|
+
return _repoOptsInToMcp(ctx) ? false : null;
|
|
20
36
|
},
|
|
21
37
|
impact: 'medium',
|
|
22
38
|
rating: 3,
|
|
@@ -34,7 +50,9 @@ module.exports = {
|
|
|
34
50
|
if (mcpJson && mcpJson.mcpServers) count += Object.keys(mcpJson.mcpServers).length;
|
|
35
51
|
const settings = ctx.jsonFile('.claude/settings.local.json') || ctx.jsonFile('.claude/settings.json');
|
|
36
52
|
if (settings && settings.mcpServers) count += Object.keys(settings.mcpServers).length;
|
|
37
|
-
|
|
53
|
+
if (count >= 2) return true;
|
|
54
|
+
// PP-06 recalibration: N/A on repos that don't reference MCP at all.
|
|
55
|
+
return _repoOptsInToMcp(ctx) ? false : null;
|
|
38
56
|
},
|
|
39
57
|
impact: 'medium',
|
|
40
58
|
rating: 4,
|
|
@@ -51,7 +69,10 @@ module.exports = {
|
|
|
51
69
|
const local = ctx.jsonFile('.claude/settings.local.json') || {};
|
|
52
70
|
const mcp = ctx.jsonFile('.mcp.json') || {};
|
|
53
71
|
const all = { ...(shared.mcpServers || {}), ...(local.mcpServers || {}), ...(mcp.mcpServers || {}) };
|
|
54
|
-
if (Object.keys(all).length === 0)
|
|
72
|
+
if (Object.keys(all).length === 0) {
|
|
73
|
+
// PP-06 recalibration: N/A on repos that don't reference MCP at all.
|
|
74
|
+
return _repoOptsInToMcp(ctx) ? false : null;
|
|
75
|
+
}
|
|
55
76
|
return Object.keys(all).some(k => /context7/i.test(k));
|
|
56
77
|
},
|
|
57
78
|
impact: 'medium',
|
package/src/techniques.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { attachSourceUrls, buildSupplementalChecks, CLAUDE_SUPPLEMENTAL_SOURCE_URLS, STACK_CATEGORY_DETECTORS, containsEmbeddedSecret } = require('./techniques/shared');
|
|
8
|
+
const { LAYERS, assignLayers } = require('./audit/layers');
|
|
8
9
|
const instructionTechniques = require('./techniques/instructions');
|
|
9
10
|
const qualityTechniques = require('./techniques/quality');
|
|
10
11
|
const apiTechniques = require('./techniques/api');
|
|
@@ -18,6 +19,24 @@ const complianceTechniques = require('./techniques/compliance');
|
|
|
18
19
|
const optimizationTechniques = require('./techniques/optimization');
|
|
19
20
|
const stackTechniques = require('./techniques/stacks');
|
|
20
21
|
|
|
22
|
+
// CTO-08 — tag every check with a layer. Source-file-level defaults map
|
|
23
|
+
// directly onto the taxonomy: hygiene.js → hygiene; everything else →
|
|
24
|
+
// governance. The assignLayers helper then upgrades drift-like checks
|
|
25
|
+
// (Harmony, propagation, cross-platform consistency) to the drift layer
|
|
26
|
+
// regardless of their source bag.
|
|
27
|
+
assignLayers(instructionTechniques, LAYERS.GOVERNANCE);
|
|
28
|
+
assignLayers(qualityTechniques, LAYERS.GOVERNANCE);
|
|
29
|
+
assignLayers(apiTechniques, LAYERS.GOVERNANCE);
|
|
30
|
+
assignLayers(automationTechniques, LAYERS.GOVERNANCE);
|
|
31
|
+
assignLayers(hygieneTechniques, LAYERS.HYGIENE);
|
|
32
|
+
assignLayers(observabilityTechniques, LAYERS.GOVERNANCE);
|
|
33
|
+
assignLayers(workflowTechniques, LAYERS.GOVERNANCE);
|
|
34
|
+
assignLayers(toolTechniques, LAYERS.GOVERNANCE);
|
|
35
|
+
assignLayers(securityTechniques, LAYERS.GOVERNANCE);
|
|
36
|
+
assignLayers(complianceTechniques, LAYERS.GOVERNANCE);
|
|
37
|
+
assignLayers(optimizationTechniques, LAYERS.GOVERNANCE);
|
|
38
|
+
assignLayers(stackTechniques, LAYERS.GOVERNANCE);
|
|
39
|
+
|
|
21
40
|
const TECHNIQUES = Object.assign({},
|
|
22
41
|
instructionTechniques,
|
|
23
42
|
qualityTechniques,
|
|
@@ -78,4 +97,8 @@ const STACKS = {
|
|
|
78
97
|
|
|
79
98
|
attachSourceUrls('claude', TECHNIQUES);
|
|
80
99
|
|
|
100
|
+
// CTO-08 — final sweep so supplemental checks added via buildSupplementalChecks
|
|
101
|
+
// also carry a layer tag (defaults to governance; drift heuristic still runs).
|
|
102
|
+
assignLayers(TECHNIQUES, LAYERS.GOVERNANCE);
|
|
103
|
+
|
|
81
104
|
module.exports = { TECHNIQUES, STACKS, STACK_CATEGORY_DETECTORS, containsEmbeddedSecret };
|
|
@@ -3770,6 +3770,10 @@ Object.assign(WINDSURF_TECHNIQUES, buildStackChecks({
|
|
|
3770
3770
|
|
|
3771
3771
|
attachSourceUrls('windsurf', WINDSURF_TECHNIQUES);
|
|
3772
3772
|
|
|
3773
|
+
// CTO-08 — tag every check with a scope layer.
|
|
3774
|
+
const { LAYERS: WS_LAYERS, assignLayers: wsAssignLayers } = require('../audit/layers');
|
|
3775
|
+
wsAssignLayers(WINDSURF_TECHNIQUES, WS_LAYERS.GOVERNANCE);
|
|
3776
|
+
|
|
3773
3777
|
module.exports = {
|
|
3774
3778
|
WINDSURF_TECHNIQUES,
|
|
3775
3779
|
};
|