@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 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.24.0",
227
- "timestamp": "2026-04-15T06:00:00.000Z"
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.24.0",
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": {
@@ -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
- console.log(` ${colorize(r.name, 'bold')}${colorize(conf, 'dim')}`);
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
- console.log(` ${colorize(r.name, 'bold')}${colorize(conf, 'dim')}`);
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
- console.log(` ${colorize(r.name, 'bold')}${colorize(conf, 'dim')}`);
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
  }
@@ -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
  };
@@ -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
  };
@@ -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),
@@ -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
- lines.push(`- [ ] **[${sev}] ${title}** (\`${key}\`)${loc}${delta}`);
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
- lines.push('| key | name | category | rating | file | line |');
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 || ''),
@@ -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
- /\bgradlew?\s+test\b/i,
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
 
@@ -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
  };