@nerviq/cli 1.9.0 → 1.11.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 +106 -60
- package/bin/cli.js +382 -208
- package/package.json +1 -1
- package/src/activity.js +245 -63
- package/src/aider/freshness.js +149 -146
- package/src/analyze.js +3 -1
- package/src/anti-patterns.js +17 -13
- package/src/audit.js +106 -79
- package/src/auto-suggest.js +62 -9
- package/src/benchmark.js +67 -51
- package/src/dashboard.js +36 -14
- package/src/governance.js +13 -7
- package/src/index.js +2 -1
- package/src/instruction-surfaces.js +185 -0
- package/src/integrations.js +102 -55
- package/src/locales/en.json +1 -1
- package/src/locales/es.json +1 -1
- package/src/permission-rules.js +218 -0
- package/src/secret-patterns.js +9 -0
- package/src/server.js +398 -3
- package/src/setup.js +2 -2
- package/src/stack-checks.js +1 -1
- package/src/synergy/report.js +1 -0
- package/src/techniques.js +102 -103
- package/src/terminology.js +73 -0
- package/src/token-estimate.js +35 -0
- package/src/workspace.js +155 -13
package/src/techniques.js
CHANGED
|
@@ -4,8 +4,16 @@
|
|
|
4
4
|
* Each technique includes: what to check, how to fix, impact level.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { collectClaudeDenyRules } = require('./permission-rules');
|
|
10
|
+
const {
|
|
11
|
+
getClaudeInstructionBundle,
|
|
12
|
+
hasDocumentedVerificationGuidance,
|
|
13
|
+
hasDocumentedTestCommand,
|
|
14
|
+
hasDocumentedLintCommand,
|
|
15
|
+
hasDocumentedBuildCommand,
|
|
16
|
+
} = require('./instruction-surfaces');
|
|
9
17
|
|
|
10
18
|
function hasFrontendSignals(ctx) {
|
|
11
19
|
const pkg = ctx.fileContent('package.json') || '';
|
|
@@ -444,62 +452,58 @@ const TECHNIQUES = {
|
|
|
444
452
|
// === QUALITY & TESTING (category: 'quality') ================
|
|
445
453
|
// ============================================================
|
|
446
454
|
|
|
447
|
-
verificationLoop: {
|
|
448
|
-
id: 93,
|
|
449
|
-
name: '
|
|
450
|
-
check: (ctx) => {
|
|
451
|
-
const
|
|
452
|
-
return
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
category: 'quality',
|
|
500
|
-
fix: 'Add a build command to CLAUDE.md so Claude can verify compilation before committing.',
|
|
501
|
-
template: null
|
|
502
|
-
},
|
|
455
|
+
verificationLoop: {
|
|
456
|
+
id: 93,
|
|
457
|
+
name: 'Claude instruction surfaces include verification criteria',
|
|
458
|
+
check: (ctx) => {
|
|
459
|
+
const docs = getClaudeInstructionBundle(ctx);
|
|
460
|
+
return hasDocumentedVerificationGuidance(docs);
|
|
461
|
+
},
|
|
462
|
+
impact: 'critical',
|
|
463
|
+
rating: 5,
|
|
464
|
+
category: 'quality',
|
|
465
|
+
fix: 'Add canonical test/lint/build commands to your Claude instruction surfaces (CLAUDE.md, imported docs, or .claude/commands) so Claude can verify its own work.',
|
|
466
|
+
template: null
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
testCommand: {
|
|
470
|
+
id: 93001,
|
|
471
|
+
name: 'Claude instruction surfaces include a test command',
|
|
472
|
+
check: (ctx) => {
|
|
473
|
+
return hasDocumentedTestCommand(getClaudeInstructionBundle(ctx));
|
|
474
|
+
},
|
|
475
|
+
impact: 'high',
|
|
476
|
+
rating: 5,
|
|
477
|
+
category: 'quality',
|
|
478
|
+
fix: 'Add an explicit test command to your Claude instruction surfaces (for example "Run `npm test` before committing").',
|
|
479
|
+
template: null
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
lintCommand: {
|
|
483
|
+
id: 93002,
|
|
484
|
+
name: 'Claude instruction surfaces include a lint command',
|
|
485
|
+
check: (ctx) => {
|
|
486
|
+
return hasDocumentedLintCommand(getClaudeInstructionBundle(ctx));
|
|
487
|
+
},
|
|
488
|
+
impact: 'high',
|
|
489
|
+
rating: 4,
|
|
490
|
+
category: 'quality',
|
|
491
|
+
fix: 'Add a lint command to your Claude instruction surfaces so Claude can check style and static quality automatically.',
|
|
492
|
+
template: null
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
buildCommand: {
|
|
496
|
+
id: 93003,
|
|
497
|
+
name: 'Claude instruction surfaces include a build command',
|
|
498
|
+
check: (ctx) => {
|
|
499
|
+
return hasDocumentedBuildCommand(getClaudeInstructionBundle(ctx));
|
|
500
|
+
},
|
|
501
|
+
impact: 'medium',
|
|
502
|
+
rating: 4,
|
|
503
|
+
category: 'quality',
|
|
504
|
+
fix: 'Add a build command to your Claude instruction surfaces so Claude can verify compilation before committing.',
|
|
505
|
+
template: null
|
|
506
|
+
},
|
|
503
507
|
|
|
504
508
|
// ============================================================
|
|
505
509
|
// === GIT SAFETY (category: 'git') ===========================
|
|
@@ -557,19 +561,19 @@ const TECHNIQUES = {
|
|
|
557
561
|
template: null
|
|
558
562
|
},
|
|
559
563
|
|
|
560
|
-
noSecretsInClaude: {
|
|
561
|
-
id: 1039,
|
|
562
|
-
name: 'CLAUDE.md has no embedded
|
|
563
|
-
check: (ctx) => {
|
|
564
|
-
const md = ctx.claudeMdContent() || '';
|
|
565
|
-
return !containsEmbeddedSecret(md);
|
|
566
|
-
},
|
|
567
|
-
impact: 'critical',
|
|
568
|
-
rating: 5,
|
|
569
|
-
category: 'git',
|
|
570
|
-
fix: 'Remove
|
|
571
|
-
template: null
|
|
572
|
-
},
|
|
564
|
+
noSecretsInClaude: {
|
|
565
|
+
id: 1039,
|
|
566
|
+
name: 'CLAUDE.md has no embedded secrets',
|
|
567
|
+
check: (ctx) => {
|
|
568
|
+
const md = ctx.claudeMdContent() || '';
|
|
569
|
+
return !containsEmbeddedSecret(md);
|
|
570
|
+
},
|
|
571
|
+
impact: 'critical',
|
|
572
|
+
rating: 5,
|
|
573
|
+
category: 'git',
|
|
574
|
+
fix: 'Remove hardcoded secrets, tokens, private keys, and connection strings from CLAUDE.md. Use environment variables or external secret stores instead.',
|
|
575
|
+
template: null
|
|
576
|
+
},
|
|
573
577
|
|
|
574
578
|
// ============================================================
|
|
575
579
|
// === WORKFLOW (category: 'workflow') =========================
|
|
@@ -714,16 +718,13 @@ const TECHNIQUES = {
|
|
|
714
718
|
template: null
|
|
715
719
|
},
|
|
716
720
|
|
|
717
|
-
permissionDeny: {
|
|
718
|
-
id: 2401,
|
|
719
|
-
name: 'Deny rules configured in permissions',
|
|
720
|
-
check: (ctx) => {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
return Array.isArray(deny) && deny.length > 0;
|
|
725
|
-
},
|
|
726
|
-
impact: 'high',
|
|
721
|
+
permissionDeny: {
|
|
722
|
+
id: 2401,
|
|
723
|
+
name: 'Deny rules configured in permissions',
|
|
724
|
+
check: (ctx) => {
|
|
725
|
+
return collectClaudeDenyRules(ctx).length > 0;
|
|
726
|
+
},
|
|
727
|
+
impact: 'high',
|
|
727
728
|
rating: 5,
|
|
728
729
|
category: 'security',
|
|
729
730
|
fix: 'Add permissions.deny rules to block dangerous operations (e.g. rm -rf, dropping databases).',
|
|
@@ -751,18 +752,19 @@ const TECHNIQUES = {
|
|
|
751
752
|
template: null
|
|
752
753
|
},
|
|
753
754
|
|
|
754
|
-
secretsProtection: {
|
|
755
|
-
id: 1096,
|
|
756
|
-
name: 'Secrets protection configured',
|
|
757
|
-
check: (ctx) => {
|
|
758
|
-
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
755
|
+
secretsProtection: {
|
|
756
|
+
id: 1096,
|
|
757
|
+
name: 'Secrets protection configured',
|
|
758
|
+
check: (ctx) => {
|
|
759
|
+
const shared = ctx.jsonFile('.claude/settings.json');
|
|
760
|
+
const local = ctx.jsonFile('.claude/settings.local.json');
|
|
761
|
+
const settings = shared || local;
|
|
762
|
+
if (!settings || !settings.permissions) return false;
|
|
763
|
+
const denyRules = collectClaudeDenyRules(ctx);
|
|
764
|
+
const hasDeny = denyRules.some((rule) => rule.protectsSecrets);
|
|
765
|
+
// Fail if allow includes "*" (overly broad — bypasses deny rules)
|
|
766
|
+
const allow = settings.permissions.allow || [];
|
|
767
|
+
if (Array.isArray(allow) && allow.includes('*')) return false;
|
|
766
768
|
return hasDeny;
|
|
767
769
|
},
|
|
768
770
|
impact: 'critical',
|
|
@@ -1524,16 +1526,13 @@ const TECHNIQUES = {
|
|
|
1524
1526
|
template: null
|
|
1525
1527
|
},
|
|
1526
1528
|
|
|
1527
|
-
denyRulesDepth: {
|
|
1528
|
-
id: 2014,
|
|
1529
|
-
name: 'Deny rules cover 3+ patterns',
|
|
1530
|
-
check: (ctx) => {
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
return deny.length >= 3;
|
|
1535
|
-
},
|
|
1536
|
-
impact: 'high', rating: 4, category: 'security',
|
|
1529
|
+
denyRulesDepth: {
|
|
1530
|
+
id: 2014,
|
|
1531
|
+
name: 'Deny rules cover 3+ patterns',
|
|
1532
|
+
check: (ctx) => {
|
|
1533
|
+
return collectClaudeDenyRules(ctx).length >= 3;
|
|
1534
|
+
},
|
|
1535
|
+
impact: 'high', rating: 4, category: 'security',
|
|
1537
1536
|
fix: 'Add at least 3 deny rules: rm -rf, force-push, and .env reads. More patterns = safer Claude.',
|
|
1538
1537
|
template: null
|
|
1539
1538
|
},
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const TERMINOLOGY = {
|
|
4
|
+
governance: {
|
|
5
|
+
label: 'governance',
|
|
6
|
+
description: 'the rollout safety layer: permissions, hooks, profiles, and policy packs',
|
|
7
|
+
},
|
|
8
|
+
hooks: {
|
|
9
|
+
label: 'hooks',
|
|
10
|
+
description: 'auto-run checks or scripts triggered before or after agent tool actions',
|
|
11
|
+
},
|
|
12
|
+
denyRules: {
|
|
13
|
+
label: 'deny rules',
|
|
14
|
+
description: 'explicit blocks for risky reads or commands like .env access or rm -rf',
|
|
15
|
+
},
|
|
16
|
+
mcp: {
|
|
17
|
+
label: 'MCP',
|
|
18
|
+
description: 'live external tool connectors for docs, APIs, databases, and other systems',
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const TERM_ORDER = ['governance', 'hooks', 'denyRules', 'mcp'];
|
|
23
|
+
|
|
24
|
+
function normalizeTermKeys(keys = []) {
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
for (const key of keys) {
|
|
27
|
+
if (!TERMINOLOGY[key]) continue;
|
|
28
|
+
seen.add(key);
|
|
29
|
+
}
|
|
30
|
+
return TERM_ORDER.filter((key) => seen.has(key));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function collectAuditTerminology(result = {}) {
|
|
34
|
+
const terms = new Set();
|
|
35
|
+
const texts = [];
|
|
36
|
+
|
|
37
|
+
for (const item of result.topNextActions || []) {
|
|
38
|
+
texts.push(item.name || '', item.fix || '', item.why || '', item.module || '', ...(item.signals || []));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const item of result.results || []) {
|
|
42
|
+
if (item.passed === false) {
|
|
43
|
+
texts.push(item.key || '', item.name || '', item.fix || '', item.category || '');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const blob = texts.join('\n');
|
|
48
|
+
if (/\bhook/i.test(blob)) terms.add('hooks');
|
|
49
|
+
if (/\bdeny rules?\b|permissions?\.deny|bypasspermissions|\.env access|rm -rf/i.test(blob)) terms.add('denyRules');
|
|
50
|
+
if (/\bmcp\b|context7|external tool/i.test(blob)) terms.add('mcp');
|
|
51
|
+
if (/\bgovernance\b|policy pack|permission profile/i.test(blob) || terms.size > 0) terms.add('governance');
|
|
52
|
+
|
|
53
|
+
return normalizeTermKeys([...terms]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatTerminologyLines(keys, options = {}) {
|
|
57
|
+
const normalized = normalizeTermKeys(keys);
|
|
58
|
+
if (normalized.length === 0) return [];
|
|
59
|
+
const title = options.title || ' Terms used here:';
|
|
60
|
+
const indent = options.indent || ' ';
|
|
61
|
+
const bullet = options.bullet || '-';
|
|
62
|
+
|
|
63
|
+
return [
|
|
64
|
+
title,
|
|
65
|
+
...normalized.map((key) => `${indent}${bullet} ${TERMINOLOGY[key].label}: ${TERMINOLOGY[key].description}`),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
TERMINOLOGY,
|
|
71
|
+
collectAuditTerminology,
|
|
72
|
+
formatTerminologyLines,
|
|
73
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
function splitIdentifierSegments(value) {
|
|
2
|
+
return String(value || '')
|
|
3
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
4
|
+
.replace(/[_./:-]+/g, ' ')
|
|
5
|
+
.split(/\s+/)
|
|
6
|
+
.filter(Boolean);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function estimateSegmentTokens(segment) {
|
|
10
|
+
const charCount = [...String(segment || '')].length;
|
|
11
|
+
return Math.max(1, Math.ceil(charCount / 4));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function estimateTokenCount(text) {
|
|
15
|
+
if (typeof text !== 'string' || !text) return 0;
|
|
16
|
+
|
|
17
|
+
const parts = text.match(/[\p{L}\p{N}_]+|[^\s]/gu) || [];
|
|
18
|
+
let total = 0;
|
|
19
|
+
|
|
20
|
+
for (const part of parts) {
|
|
21
|
+
if (/^[\p{L}\p{N}_]+$/u.test(part)) {
|
|
22
|
+
const segments = splitIdentifierSegments(part);
|
|
23
|
+
total += segments.reduce((sum, segment) => sum + estimateSegmentTokens(segment), 0);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
total += 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return total;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
estimateTokenCount,
|
|
35
|
+
};
|
package/src/workspace.js
CHANGED
|
@@ -166,6 +166,115 @@ function parseWorkspaceSelection(value) {
|
|
|
166
166
|
return unique(String(value).split(',').map((item) => item.trim()).filter(Boolean));
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
function summarizeAuditResult(result, scoreType, scope) {
|
|
170
|
+
return {
|
|
171
|
+
scope,
|
|
172
|
+
scoreType,
|
|
173
|
+
score: typeof result?.score === 'number' ? result.score : null,
|
|
174
|
+
passed: typeof result?.passed === 'number' ? result.passed : 0,
|
|
175
|
+
total: typeof result?.checkCount === 'number' ? result.checkCount : 0,
|
|
176
|
+
topAction: result?.topNextActions?.[0]?.name || null,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function summarizeWorkspaceEntry(result, workspacePath, absPath, platform) {
|
|
181
|
+
const stackKeys = (result.stacks || []).map((item) => item.key);
|
|
182
|
+
const stackLabels = (result.stacks || []).map((item) => item.label);
|
|
183
|
+
return {
|
|
184
|
+
name: path.basename(workspacePath),
|
|
185
|
+
workspace: workspacePath,
|
|
186
|
+
dir: absPath,
|
|
187
|
+
platform,
|
|
188
|
+
stackKeys,
|
|
189
|
+
stackLabels,
|
|
190
|
+
workspaceProfile: classifyWorkspaceProfile(stackKeys),
|
|
191
|
+
...summarizeAuditResult(result, 'workspace-live-audit', 'workspace-package'),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function classifyWorkspaceProfile(stackKeys) {
|
|
196
|
+
const keys = new Set(Array.isArray(stackKeys) ? stackKeys : []);
|
|
197
|
+
const matchAny = (candidates) => candidates.some((candidate) => keys.has(candidate));
|
|
198
|
+
|
|
199
|
+
if (matchAny(['go'])) {
|
|
200
|
+
return { key: 'go-workspace', label: 'Go workspace' };
|
|
201
|
+
}
|
|
202
|
+
if (matchAny(['python', 'django', 'fastapi'])) {
|
|
203
|
+
return { key: 'python-workspace', label: 'Python workspace' };
|
|
204
|
+
}
|
|
205
|
+
if (matchAny(['dotnet'])) {
|
|
206
|
+
return { key: 'dotnet-workspace', label: '.NET workspace' };
|
|
207
|
+
}
|
|
208
|
+
if (matchAny(['java', 'spring'])) {
|
|
209
|
+
return { key: 'java-workspace', label: 'Java workspace' };
|
|
210
|
+
}
|
|
211
|
+
if (matchAny(['flutter', 'dart'])) {
|
|
212
|
+
return { key: 'flutter-workspace', label: 'Flutter workspace' };
|
|
213
|
+
}
|
|
214
|
+
if (matchAny(['swift'])) {
|
|
215
|
+
return { key: 'swift-workspace', label: 'Swift workspace' };
|
|
216
|
+
}
|
|
217
|
+
if (matchAny(['kotlin'])) {
|
|
218
|
+
return { key: 'kotlin-workspace', label: 'Kotlin workspace' };
|
|
219
|
+
}
|
|
220
|
+
if (matchAny(['react', 'nextjs', 'node', 'typescript', 'javascript', 'nestjs', 'vue', 'angular', 'svelte'])) {
|
|
221
|
+
return { key: 'node-workspace', label: 'Node / JS workspace' };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { key: 'general-workspace', label: 'General workspace' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function buildProfileBreakdown(results) {
|
|
228
|
+
const grouped = new Map();
|
|
229
|
+
|
|
230
|
+
for (const item of results) {
|
|
231
|
+
const profileKey = item.workspaceProfile?.key || 'general-workspace';
|
|
232
|
+
const profileLabel = item.workspaceProfile?.label || 'General workspace';
|
|
233
|
+
if (!grouped.has(profileKey)) {
|
|
234
|
+
grouped.set(profileKey, {
|
|
235
|
+
profileKey,
|
|
236
|
+
profileLabel,
|
|
237
|
+
scoreType: 'workspace-live-audit',
|
|
238
|
+
workspaceCount: 0,
|
|
239
|
+
workspaces: [],
|
|
240
|
+
stackLabels: new Set(),
|
|
241
|
+
scores: [],
|
|
242
|
+
totals: [],
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const entry = grouped.get(profileKey);
|
|
247
|
+
entry.workspaceCount += 1;
|
|
248
|
+
entry.workspaces.push(item.workspace);
|
|
249
|
+
for (const label of item.stackLabels || []) {
|
|
250
|
+
entry.stackLabels.add(label);
|
|
251
|
+
}
|
|
252
|
+
if (typeof item.score === 'number') {
|
|
253
|
+
entry.scores.push(item.score);
|
|
254
|
+
}
|
|
255
|
+
if (typeof item.total === 'number') {
|
|
256
|
+
entry.totals.push(item.total);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return [...grouped.values()]
|
|
261
|
+
.map((entry) => ({
|
|
262
|
+
profileKey: entry.profileKey,
|
|
263
|
+
profileLabel: entry.profileLabel,
|
|
264
|
+
scoreType: 'workspace-live-audit',
|
|
265
|
+
workspaceCount: entry.workspaceCount,
|
|
266
|
+
averageScore: entry.scores.length > 0
|
|
267
|
+
? Math.round(entry.scores.reduce((sum, value) => sum + value, 0) / entry.scores.length)
|
|
268
|
+
: 0,
|
|
269
|
+
averageTotal: entry.totals.length > 0
|
|
270
|
+
? Math.round(entry.totals.reduce((sum, value) => sum + value, 0) / entry.totals.length)
|
|
271
|
+
: 0,
|
|
272
|
+
stackLabels: [...entry.stackLabels].sort(),
|
|
273
|
+
workspaces: entry.workspaces.sort(),
|
|
274
|
+
}))
|
|
275
|
+
.sort((left, right) => left.profileLabel.localeCompare(right.profileLabel));
|
|
276
|
+
}
|
|
277
|
+
|
|
169
278
|
async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
|
|
170
279
|
const { audit } = require('./audit');
|
|
171
280
|
const rootDir = path.resolve(dir);
|
|
@@ -175,32 +284,43 @@ async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
|
|
|
175
284
|
? expandWorkspacePatterns(rootDir, selectedPatterns)
|
|
176
285
|
: detectWorkspaces(rootDir);
|
|
177
286
|
const results = [];
|
|
287
|
+
let rootGovernance;
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const rootResult = await audit({ dir: rootDir, platform, silent: true });
|
|
291
|
+
rootGovernance = summarizeAuditResult(rootResult, 'root-live-audit', 'root-governance');
|
|
292
|
+
} catch (error) {
|
|
293
|
+
rootGovernance = {
|
|
294
|
+
scope: 'root-governance',
|
|
295
|
+
scoreType: 'root-live-audit',
|
|
296
|
+
score: null,
|
|
297
|
+
passed: 0,
|
|
298
|
+
total: 0,
|
|
299
|
+
topAction: null,
|
|
300
|
+
error: error.message,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
178
303
|
|
|
179
304
|
for (const workspacePath of workspacePaths) {
|
|
180
305
|
const absPath = path.join(rootDir, workspacePath);
|
|
181
306
|
try {
|
|
182
307
|
const result = await audit({ dir: absPath, platform, silent: true });
|
|
183
|
-
results.push(
|
|
184
|
-
name: path.basename(workspacePath),
|
|
185
|
-
workspace: workspacePath,
|
|
186
|
-
dir: absPath,
|
|
187
|
-
platform,
|
|
188
|
-
score: result.score,
|
|
189
|
-
passed: result.passed,
|
|
190
|
-
total: result.checkCount,
|
|
191
|
-
topAction: result.topNextActions?.[0]?.name || null,
|
|
192
|
-
result,
|
|
193
|
-
});
|
|
308
|
+
results.push(summarizeWorkspaceEntry(result, workspacePath, absPath, platform));
|
|
194
309
|
} catch (error) {
|
|
195
310
|
results.push({
|
|
196
311
|
name: path.basename(workspacePath),
|
|
197
312
|
workspace: workspacePath,
|
|
198
313
|
dir: absPath,
|
|
199
314
|
platform,
|
|
315
|
+
scope: 'workspace-package',
|
|
316
|
+
scoreType: 'workspace-live-audit',
|
|
200
317
|
score: null,
|
|
201
318
|
passed: 0,
|
|
202
319
|
total: 0,
|
|
203
320
|
topAction: null,
|
|
321
|
+
stackKeys: [],
|
|
322
|
+
stackLabels: [],
|
|
323
|
+
workspaceProfile: { key: 'general-workspace', label: 'General workspace' },
|
|
204
324
|
error: error.message,
|
|
205
325
|
});
|
|
206
326
|
}
|
|
@@ -210,17 +330,39 @@ async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
|
|
|
210
330
|
const averageScore = validScores.length > 0
|
|
211
331
|
? Math.round(validScores.reduce((sum, value) => sum + value, 0) / validScores.length)
|
|
212
332
|
: 0;
|
|
333
|
+
const maxScore = validScores.length > 0 ? Math.max(...validScores) : 0;
|
|
334
|
+
const minScore = validScores.length > 0 ? Math.min(...validScores) : 0;
|
|
335
|
+
const profileBreakdown = buildProfileBreakdown(results);
|
|
213
336
|
|
|
214
337
|
return {
|
|
338
|
+
summaryType: 'monorepo-workspace-audit',
|
|
215
339
|
rootDir,
|
|
216
340
|
platform,
|
|
341
|
+
selectionMode: selectedPatterns.length > 0 ? 'explicit-patterns' : 'detected-workspaces',
|
|
217
342
|
patterns: sourcePatterns,
|
|
343
|
+
rootGovernance,
|
|
344
|
+
workspaceAggregate: {
|
|
345
|
+
scope: 'workspace-aggregate',
|
|
346
|
+
scoreType: 'workspace-average-live-audit',
|
|
347
|
+
score: averageScore,
|
|
348
|
+
workspaceCount: workspacePaths.length,
|
|
349
|
+
maxScore,
|
|
350
|
+
minScore,
|
|
351
|
+
},
|
|
352
|
+
profileBreakdown,
|
|
353
|
+
scoreSemantics: {
|
|
354
|
+
rootGovernance: 'Root repo live audit for shared instructions, hooks, permissions, and top-level governance files.',
|
|
355
|
+
workspaceAggregate: 'Average of the selected workspace live audit scores. This is a package coverage rollup, not the root repo score.',
|
|
356
|
+
workspaceEntries: 'Each workspace row is a package-level live audit. Package scores can differ from the root governance score for legitimate reasons.',
|
|
357
|
+
workspaceProfiles: 'Workspace totals can differ because each package uses a stack-specific check profile based on detected languages and frameworks.',
|
|
358
|
+
},
|
|
218
359
|
workspaces: results,
|
|
219
360
|
detectedWorkspaces: workspacePaths,
|
|
220
361
|
workspaceCount: workspacePaths.length,
|
|
362
|
+
averageScoreType: 'workspace-average-live-audit',
|
|
221
363
|
averageScore,
|
|
222
|
-
maxScore
|
|
223
|
-
minScore
|
|
364
|
+
maxScore,
|
|
365
|
+
minScore,
|
|
224
366
|
};
|
|
225
367
|
}
|
|
226
368
|
|