@nerviq/cli 1.24.0 → 1.26.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/instruction-surfaces.js +61 -5
- package/src/opencode/techniques.js +31 -27
- package/src/plugins.js +5 -0
- package/src/techniques/shared.js +110 -0
- 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.26.0",
|
|
238
|
+
"timestamp": "2026-04-15T14: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.26.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
|
};
|
|
@@ -6,21 +6,37 @@ const TEST_COMMAND_PATTERNS = [
|
|
|
6
6
|
/\bpython\s+manage\.py\s+test\b/i,
|
|
7
7
|
/\bdjango-admin\s+test\b/i,
|
|
8
8
|
/\bpython\s+-m\s+unittest\b/i,
|
|
9
|
+
/\bpoetry\s+run\s+(?:pytest|test)\b/i,
|
|
10
|
+
/\buv\s+run\s+(?:pytest|test)\b/i,
|
|
11
|
+
/\bpdm\s+run\s+(?:pytest|test)\b/i,
|
|
12
|
+
/\bhatch\s+run\s+(?:test|pytest)\b/i,
|
|
13
|
+
/\brye\s+run\s+(?:test|pytest)\b/i,
|
|
14
|
+
/\btox(?:\s|$)/i,
|
|
15
|
+
/\bnox(?:\s|$)/i,
|
|
9
16
|
/\bgo\s+test(?:\s|$)/i,
|
|
10
17
|
/\bcargo\s+test\b/i,
|
|
11
|
-
/\bmake\s+test\b/i,
|
|
18
|
+
/\bmake\s+(?:test|check|ci)\b/i,
|
|
19
|
+
/\bjust\s+test\b/i,
|
|
12
20
|
/\bmix\s+test\b/i,
|
|
13
21
|
/\bbundle\s+exec\s+rspec\b/i,
|
|
14
22
|
/\brspec\b/i,
|
|
15
23
|
/\bphpunit\b/i,
|
|
16
24
|
/\bdotnet\s+test(?:\s|$)/i,
|
|
17
25
|
/\bflutter\s+test\b/i,
|
|
26
|
+
/\bfvm\s+flutter\s+test\b/i,
|
|
18
27
|
/\bswift\s+test\b/i,
|
|
19
28
|
/\bxcodebuild\b[^\n\r`]{0,200}\btest\b/i,
|
|
20
|
-
/\
|
|
29
|
+
/\bfastlane\s+(?:test|scan)\b/i,
|
|
30
|
+
/\bxctest\b/i,
|
|
31
|
+
/\bgradlew?\s+(?:test|check|connectedAndroidTest)\b/i,
|
|
21
32
|
/\bmvn(?:w)?\s+test\b/i,
|
|
22
33
|
/\bplaywright\s+test\b/i,
|
|
23
34
|
/\bcypress\s+run\b/i,
|
|
35
|
+
// pyproject.toml / setup.cfg tool configuration signals
|
|
36
|
+
/\[tool\.pytest\.ini_options\]/i,
|
|
37
|
+
/\[tool:pytest\]/i,
|
|
38
|
+
// Manifest / config signals that testing is wired up
|
|
39
|
+
/\bpytest\s*[=:]/i,
|
|
24
40
|
];
|
|
25
41
|
|
|
26
42
|
const LINT_COMMAND_PATTERNS = [
|
|
@@ -33,6 +49,7 @@ const LINT_COMMAND_PATTERNS = [
|
|
|
33
49
|
/\bpylint\b/i,
|
|
34
50
|
/\bmypy\b/i,
|
|
35
51
|
/\bpyright\b/i,
|
|
52
|
+
/\bpre-commit\s+run\b/i,
|
|
36
53
|
/\bgo\s+vet\b/i,
|
|
37
54
|
/\bgofmt\b/i,
|
|
38
55
|
/\bgofumpt\b/i,
|
|
@@ -41,11 +58,19 @@ const LINT_COMMAND_PATTERNS = [
|
|
|
41
58
|
/\bcargo\s+clippy\b/i,
|
|
42
59
|
/\bflutter\s+analyze\b/i,
|
|
43
60
|
/\bdart\s+analyze\b/i,
|
|
61
|
+
/\bdart\s+format\b/i,
|
|
44
62
|
/\bswiftlint\b/i,
|
|
45
63
|
/\bswift(?:-|\s+)format\b/i,
|
|
46
64
|
/\bdotnet\s+format(?:\s|$)/i,
|
|
47
|
-
/\bgradlew?\s+lint\b/i,
|
|
65
|
+
/\bgradlew?\s+(?:lint|ktlintCheck|detekt|spotless(?:Check|Apply)?)\b/i,
|
|
48
66
|
/\bmvn(?:w)?\s+(?:checkstyle:check|spotbugs:check|verify)\b/i,
|
|
67
|
+
// pyproject.toml / config signals
|
|
68
|
+
/\[tool\.ruff\]/i,
|
|
69
|
+
/\[tool\.black\]/i,
|
|
70
|
+
/\[tool\.mypy\]/i,
|
|
71
|
+
/\[tool\.pyright\]/i,
|
|
72
|
+
/\[tool\.flake8\]/i,
|
|
73
|
+
/\[tool\.pylint\b/i,
|
|
49
74
|
];
|
|
50
75
|
|
|
51
76
|
const BUILD_COMMAND_PATTERNS = [
|
|
@@ -53,16 +78,20 @@ const BUILD_COMMAND_PATTERNS = [
|
|
|
53
78
|
/\btsc(?:\s|$)/i,
|
|
54
79
|
/\bgo\s+build(?:\s|$)/i,
|
|
55
80
|
/\bcargo\s+(?:build|check)\b/i,
|
|
56
|
-
/\bmake\s+build\b/i,
|
|
81
|
+
/\bmake\s+(?:build|all)\b/i,
|
|
82
|
+
/\bjust\s+build\b/i,
|
|
57
83
|
/\bdotnet\s+(?:build|publish)(?:\s|$)/i,
|
|
58
84
|
/\bmsbuild\b/i,
|
|
59
85
|
/\bflutter\s+build(?:\s|$)/i,
|
|
60
86
|
/\bswift\s+build\b/i,
|
|
61
87
|
/\bxcodebuild\b/i,
|
|
62
|
-
/\bgradlew?\s+(?:build|assemble)\b/i,
|
|
88
|
+
/\bgradlew?\s+(?:build|assemble|assembleDebug|assembleRelease)\b/i,
|
|
63
89
|
/\bmvn(?:w)?\s+(?:compile|package|verify|install)\b/i,
|
|
64
90
|
/\bpython\s+-m\s+build\b/i,
|
|
65
91
|
/\bpoetry\s+build\b/i,
|
|
92
|
+
/\buv\s+build\b/i,
|
|
93
|
+
/\bhatch\s+build\b/i,
|
|
94
|
+
/\bpdm\s+build\b/i,
|
|
66
95
|
];
|
|
67
96
|
|
|
68
97
|
function normalizePath(filePath) {
|
|
@@ -118,6 +147,33 @@ function buildSurfaceList(ctx, scope) {
|
|
|
118
147
|
if (includeReadme) {
|
|
119
148
|
addSurface(ctx, surfaces, seen, 'README.md');
|
|
120
149
|
addSurface(ctx, surfaces, seen, 'CONTRIBUTING.md');
|
|
150
|
+
// CTO-07: framework-native verification surfaces. When a repo has
|
|
151
|
+
// `flutter test` in CONTRIBUTING.md, pytest configured in
|
|
152
|
+
// pyproject.toml, xcodebuild wired in a workflow, or gradle test in
|
|
153
|
+
// build.gradle, that is legitimate evidence of verification — an
|
|
154
|
+
// agent working in this repo can observe these files directly.
|
|
155
|
+
addSurface(ctx, surfaces, seen, 'pyproject.toml');
|
|
156
|
+
addSurface(ctx, surfaces, seen, 'setup.cfg');
|
|
157
|
+
addSurface(ctx, surfaces, seen, 'tox.ini');
|
|
158
|
+
addSurface(ctx, surfaces, seen, 'noxfile.py');
|
|
159
|
+
addSurface(ctx, surfaces, seen, 'Pipfile');
|
|
160
|
+
addSurface(ctx, surfaces, seen, 'Makefile');
|
|
161
|
+
addSurface(ctx, surfaces, seen, 'justfile');
|
|
162
|
+
addSurface(ctx, surfaces, seen, 'Justfile');
|
|
163
|
+
addSurface(ctx, surfaces, seen, 'Rakefile');
|
|
164
|
+
addSurface(ctx, surfaces, seen, 'pubspec.yaml');
|
|
165
|
+
addSurface(ctx, surfaces, seen, 'analysis_options.yaml');
|
|
166
|
+
addSurface(ctx, surfaces, seen, 'Package.swift');
|
|
167
|
+
addSurface(ctx, surfaces, seen, 'Podfile');
|
|
168
|
+
addSurface(ctx, surfaces, seen, 'Cartfile');
|
|
169
|
+
addSurface(ctx, surfaces, seen, 'fastlane/Fastfile');
|
|
170
|
+
addSurface(ctx, surfaces, seen, 'build.gradle');
|
|
171
|
+
addSurface(ctx, surfaces, seen, 'build.gradle.kts');
|
|
172
|
+
addSurface(ctx, surfaces, seen, 'settings.gradle');
|
|
173
|
+
addSurface(ctx, surfaces, seen, 'settings.gradle.kts');
|
|
174
|
+
addSurface(ctx, surfaces, seen, '.pre-commit-config.yaml');
|
|
175
|
+
addSurface(ctx, surfaces, seen, '.pre-commit-config.yml');
|
|
176
|
+
addDirSurfaces(ctx, surfaces, seen, '.github/workflows', /\.ya?ml$/i);
|
|
121
177
|
}
|
|
122
178
|
|
|
123
179
|
return surfaces;
|
|
@@ -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
|
|
package/src/techniques/shared.js
CHANGED
|
@@ -202,6 +202,106 @@ function isDotnetProject(ctx) {
|
|
|
202
202
|
return ctx.__nerviqIsDotnet;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
// ─── CTO-07 Framework-native verification signals ───────────────────────
|
|
206
|
+
// Memoized on ctx. These are "this stack has verification wired up"
|
|
207
|
+
// signals that augment documentation-surface detection.
|
|
208
|
+
|
|
209
|
+
function hasIosXcodeProject(ctx) {
|
|
210
|
+
if (ctx.__nerviqHasIosXcode !== undefined) return ctx.__nerviqHasIosXcode;
|
|
211
|
+
ctx.__nerviqHasIosXcode =
|
|
212
|
+
hasCoreProjectFile(ctx, /\.xcodeproj\//i) ||
|
|
213
|
+
hasCoreProjectFile(ctx, /\.xcworkspace\//i) ||
|
|
214
|
+
hasCoreRootFile(ctx, /(^|\/)Package\.swift$/i);
|
|
215
|
+
return ctx.__nerviqHasIosXcode;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function hasAndroidGradle(ctx) {
|
|
219
|
+
if (ctx.__nerviqHasAndroidGradle !== undefined) return ctx.__nerviqHasAndroidGradle;
|
|
220
|
+
ctx.__nerviqHasAndroidGradle =
|
|
221
|
+
hasCoreRootFile(ctx, /(^|\/)build\.gradle(\.kts)?$/i) ||
|
|
222
|
+
hasCoreRootFile(ctx, /(^|\/)settings\.gradle(\.kts)?$/i);
|
|
223
|
+
return ctx.__nerviqHasAndroidGradle;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function hasFlutterProject(ctx) {
|
|
227
|
+
if (ctx.__nerviqHasFlutter !== undefined) return ctx.__nerviqHasFlutter;
|
|
228
|
+
const pubspec = ctx.fileContent('pubspec.yaml') || '';
|
|
229
|
+
ctx.__nerviqHasFlutter = /\bflutter:\s*\n/i.test(pubspec) || /\bsdk:\s*flutter\b/i.test(pubspec);
|
|
230
|
+
return ctx.__nerviqHasFlutter;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function _pyProjectText(ctx) {
|
|
234
|
+
return getPythonProjectText(ctx);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function hasPythonPoetry(ctx) {
|
|
238
|
+
if (ctx.__nerviqHasPoetry !== undefined) return ctx.__nerviqHasPoetry;
|
|
239
|
+
const text = _pyProjectText(ctx);
|
|
240
|
+
ctx.__nerviqHasPoetry = /\[tool\.poetry\]/i.test(text) || !!ctx.fileContent('poetry.lock');
|
|
241
|
+
return ctx.__nerviqHasPoetry;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function hasPythonUv(ctx) {
|
|
245
|
+
if (ctx.__nerviqHasUv !== undefined) return ctx.__nerviqHasUv;
|
|
246
|
+
const text = _pyProjectText(ctx);
|
|
247
|
+
ctx.__nerviqHasUv = /\[tool\.uv\]/i.test(text) || !!ctx.fileContent('uv.lock');
|
|
248
|
+
return ctx.__nerviqHasUv;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function hasPythonPdm(ctx) {
|
|
252
|
+
if (ctx.__nerviqHasPdm !== undefined) return ctx.__nerviqHasPdm;
|
|
253
|
+
const text = _pyProjectText(ctx);
|
|
254
|
+
ctx.__nerviqHasPdm = /\[tool\.pdm\b/i.test(text) || !!ctx.fileContent('pdm.lock');
|
|
255
|
+
return ctx.__nerviqHasPdm;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function hasPythonHatch(ctx) {
|
|
259
|
+
if (ctx.__nerviqHasHatch !== undefined) return ctx.__nerviqHasHatch;
|
|
260
|
+
const text = _pyProjectText(ctx);
|
|
261
|
+
ctx.__nerviqHasHatch = /\[tool\.hatch\b/i.test(text);
|
|
262
|
+
return ctx.__nerviqHasHatch;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function hasFastApiProject(ctx) {
|
|
266
|
+
if (ctx.__nerviqHasFastApi !== undefined) return ctx.__nerviqHasFastApi;
|
|
267
|
+
const text = _pyProjectText(ctx);
|
|
268
|
+
ctx.__nerviqHasFastApi = /\bfastapi\b/i.test(text);
|
|
269
|
+
return ctx.__nerviqHasFastApi;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const ML_DEP_PATTERN = /\b(pytorch|torch|tensorflow|keras|scikit-learn|sklearn|jax|transformers|datasets|huggingface|accelerate|xgboost|lightgbm)\b/i;
|
|
273
|
+
|
|
274
|
+
function hasMlScaffolding(ctx) {
|
|
275
|
+
if (ctx.__nerviqHasMl !== undefined) return ctx.__nerviqHasMl;
|
|
276
|
+
const text = _pyProjectText(ctx);
|
|
277
|
+
if (ML_DEP_PATTERN.test(text)) {
|
|
278
|
+
ctx.__nerviqHasMl = true;
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
// Heuristic: notebooks/ or experiments/ dir with actual .ipynb files
|
|
282
|
+
const hasNotebooks = findProjectFiles(ctx, /\.ipynb$/i).length > 0;
|
|
283
|
+
ctx.__nerviqHasMl = hasNotebooks;
|
|
284
|
+
return ctx.__nerviqHasMl;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Checks whether a Python tool is actively configured in pyproject.toml /
|
|
289
|
+
* setup.cfg (e.g., `[tool.ruff]`, `[tool.pytest.ini_options]`,
|
|
290
|
+
* `[tool.mypy]`). When configured, any verification-surface check for that
|
|
291
|
+
* tool should pass: an agent working in this repo can run the tool.
|
|
292
|
+
*/
|
|
293
|
+
function hasConfiguredTooling(ctx, toolName) {
|
|
294
|
+
const text = _pyProjectText(ctx);
|
|
295
|
+
if (!text) return false;
|
|
296
|
+
const name = String(toolName || '').toLowerCase();
|
|
297
|
+
const sectionRe = new RegExp(`\\[tool\\.${name.replace(/[-_.]/g, '[-_.]')}(?:[.\\]]|\\s|$)`, 'i');
|
|
298
|
+
if (sectionRe.test(text)) return true;
|
|
299
|
+
if (name === 'pytest') {
|
|
300
|
+
return /\[tool\.pytest\.ini_options\]/i.test(text) || /\[tool:pytest\]/i.test(text);
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
205
305
|
/**
|
|
206
306
|
* Map category names to their project detection function.
|
|
207
307
|
* Used by the audit to skip entire categories when the stack isn't detected.
|
|
@@ -418,6 +518,16 @@ module.exports = {
|
|
|
418
518
|
isPhpProject,
|
|
419
519
|
isDotnetProject,
|
|
420
520
|
STACK_CATEGORY_DETECTORS,
|
|
521
|
+
hasIosXcodeProject,
|
|
522
|
+
hasAndroidGradle,
|
|
523
|
+
hasFlutterProject,
|
|
524
|
+
hasPythonPoetry,
|
|
525
|
+
hasPythonUv,
|
|
526
|
+
hasPythonPdm,
|
|
527
|
+
hasPythonHatch,
|
|
528
|
+
hasFastApiProject,
|
|
529
|
+
hasMlScaffolding,
|
|
530
|
+
hasConfiguredTooling,
|
|
421
531
|
getPythonFiles,
|
|
422
532
|
getMainPythonFiles,
|
|
423
533
|
getPythonProjectText,
|
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
|
};
|