@nerviq/cli 1.9.0 → 1.10.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 +28 -33
- package/bin/cli.js +161 -106
- package/package.json +1 -1
- package/src/activity.js +68 -12
- package/src/aider/freshness.js +168 -168
- package/src/anti-patterns.js +13 -11
- package/src/audit.js +6 -5
- package/src/auto-suggest.js +62 -9
- package/src/benchmark.js +52 -41
- package/src/dashboard.js +36 -14
- package/src/instruction-surfaces.js +185 -0
- package/src/locales/en.json +1 -1
- package/src/locales/es.json +1 -1
- package/src/stack-checks.js +1 -1
- package/src/synergy/report.js +1 -0
- package/src/techniques.js +61 -58
- package/src/workspace.js +51 -6
package/src/dashboard.js
CHANGED
|
@@ -68,7 +68,7 @@ function buildScoreOverTimeSvg(history) {
|
|
|
68
68
|
|
|
69
69
|
return `
|
|
70
70
|
<div class="card">
|
|
71
|
-
<h2>Score Over Time</h2>
|
|
71
|
+
<h2>Audit Snapshot Score Over Time</h2>
|
|
72
72
|
<svg viewBox="0 0 ${w} ${h}" width="100%" style="max-width:${w}px">
|
|
73
73
|
${yLabels}
|
|
74
74
|
<polyline points="${polyline}" fill="none" stroke="${COLORS.blue}" stroke-width="2"/>
|
|
@@ -119,11 +119,28 @@ function buildCategoryBreakdownSvg(results) {
|
|
|
119
119
|
</div>`;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
function getDashboardScoreMeta(history) {
|
|
123
|
+
if (history && history.length > 0) {
|
|
124
|
+
return {
|
|
125
|
+
label: 'Latest audit snapshot score',
|
|
126
|
+
note: 'Dashboard is anchored to the most recent saved audit snapshot. Trend and drift sections use audit snapshots only.',
|
|
127
|
+
consoleSource: 'latest audit snapshot',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
label: 'Live audit score',
|
|
133
|
+
note: 'No saved audit snapshots found, so this dashboard ran a live audit of the current repo. Run `nerviq audit --snapshot` to build history.',
|
|
134
|
+
consoleSource: 'live audit (no snapshots yet)',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
122
138
|
function buildHtml(projectName, auditPayload, history) {
|
|
123
139
|
const score = auditPayload.score ?? 0;
|
|
124
140
|
const platform = auditPayload.platform || 'unknown';
|
|
125
141
|
const results = auditPayload.results || [];
|
|
126
142
|
const timestamp = new Date().toISOString();
|
|
143
|
+
const scoreMeta = getDashboardScoreMeta(history);
|
|
127
144
|
|
|
128
145
|
// Top 5 failed checks sorted by impact severity
|
|
129
146
|
const impactOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
@@ -184,7 +201,8 @@ function buildHtml(projectName, auditPayload, history) {
|
|
|
184
201
|
|
|
185
202
|
<div class="card score-card">
|
|
186
203
|
<div class="score-number" style="color:${scoreColor(score)}">${score}</div>
|
|
187
|
-
<div class="score-label"
|
|
204
|
+
<div class="score-label">${escapeHtml(scoreMeta.label)}</div>
|
|
205
|
+
<div style="color:${COLORS.textDim};font-size:.85rem;margin-top:.75rem;max-width:520px;margin-left:auto;margin-right:auto">${escapeHtml(scoreMeta.note)}</div>
|
|
188
206
|
</div>
|
|
189
207
|
|
|
190
208
|
<div class="card">
|
|
@@ -238,6 +256,7 @@ async function generateDashboard(dir, flags = {}) {
|
|
|
238
256
|
auditPayload = await audit({ dir, silent: true, platform: flags.platform || 'claude' });
|
|
239
257
|
}
|
|
240
258
|
|
|
259
|
+
const scoreMeta = getDashboardScoreMeta(history);
|
|
241
260
|
const html = buildHtml(projectName, auditPayload, history);
|
|
242
261
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
243
262
|
fs.writeFileSync(outputPath, html, 'utf8');
|
|
@@ -247,8 +266,9 @@ async function generateDashboard(dir, flags = {}) {
|
|
|
247
266
|
console.log('');
|
|
248
267
|
console.log(' nerviq dashboard');
|
|
249
268
|
console.log(' \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550');
|
|
250
|
-
console.log(`
|
|
251
|
-
console.log(`
|
|
269
|
+
console.log(` Dashboard score: ${auditPayload.score ?? '?'}/100`);
|
|
270
|
+
console.log(` Score source: ${scoreMeta.consoleSource}`);
|
|
271
|
+
console.log(` Audit snapshots: ${history.length}`);
|
|
252
272
|
console.log(` Output: ${relPath}`);
|
|
253
273
|
console.log('');
|
|
254
274
|
}
|
|
@@ -261,7 +281,7 @@ async function generateDashboard(dir, flags = {}) {
|
|
|
261
281
|
exec(cmd);
|
|
262
282
|
}
|
|
263
283
|
|
|
264
|
-
return { outputPath, relativePath: relPath, score: auditPayload.score };
|
|
284
|
+
return { outputPath, relativePath: relPath, score: auditPayload.score, scoreSource: scoreMeta.consoleSource };
|
|
265
285
|
}
|
|
266
286
|
|
|
267
287
|
/**
|
|
@@ -274,13 +294,15 @@ function detectDrifts(history, threshold = 5) {
|
|
|
274
294
|
for (let i = 0; i < history.length - 1; i++) {
|
|
275
295
|
const current = history[i];
|
|
276
296
|
const previous = history[i + 1];
|
|
277
|
-
|
|
278
|
-
|
|
297
|
+
const currentScore = current.summary?.score;
|
|
298
|
+
const previousScore = previous.summary?.score;
|
|
299
|
+
if (currentScore != null && previousScore != null) {
|
|
300
|
+
const delta = currentScore - previousScore;
|
|
279
301
|
if (Math.abs(delta) >= threshold) {
|
|
280
302
|
drifts.push({
|
|
281
|
-
date: current.date || current.timestamp,
|
|
282
|
-
from:
|
|
283
|
-
to:
|
|
303
|
+
date: current.createdAt || current.date || current.timestamp,
|
|
304
|
+
from: previousScore,
|
|
305
|
+
to: currentScore,
|
|
284
306
|
delta,
|
|
285
307
|
});
|
|
286
308
|
}
|
|
@@ -307,8 +329,8 @@ function buildDriftAlertsHtml(drifts) {
|
|
|
307
329
|
|
|
308
330
|
return `
|
|
309
331
|
<div style="margin-top:32px">
|
|
310
|
-
<h2 style="color:${COLORS.text};font-size:18px;margin-bottom:12px">⚠
|
|
311
|
-
<p style="color:${COLORS.textDim};font-size:13px;margin-bottom:12px">Changes of 5+ points between consecutive snapshots</p>
|
|
332
|
+
<h2 style="color:${COLORS.text};font-size:18px;margin-bottom:12px">⚠ Audit Snapshot Drift Alerts</h2>
|
|
333
|
+
<p style="color:${COLORS.textDim};font-size:13px;margin-bottom:12px">Changes of 5+ points between consecutive audit snapshots</p>
|
|
312
334
|
<table style="width:100%;border-collapse:collapse;background:${COLORS.surface};border-radius:8px;overflow:hidden">
|
|
313
335
|
<thead><tr style="background:${COLORS.border}">
|
|
314
336
|
<th style="padding:8px 12px;text-align:left;color:${COLORS.textDim};font-size:12px">Date</th>
|
|
@@ -375,7 +397,7 @@ function buildPortfolioHtml(repoResults) {
|
|
|
375
397
|
|
|
376
398
|
<div class="card score-card">
|
|
377
399
|
<div class="score-number" style="color:${scoreColor(avgScore)}">${avgScore}</div>
|
|
378
|
-
<div class="score-label">average score across ${repoResults.length} repos</div>
|
|
400
|
+
<div class="score-label">average live audit score across ${repoResults.length} repos</div>
|
|
379
401
|
</div>
|
|
380
402
|
|
|
381
403
|
<div class="highlights">
|
|
@@ -452,7 +474,7 @@ async function generatePortfolioDashboard(repoPaths, flags = {}) {
|
|
|
452
474
|
console.log(' nerviq portfolio dashboard');
|
|
453
475
|
console.log(' \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550');
|
|
454
476
|
console.log(` Repos: ${repoResults.length}`);
|
|
455
|
-
console.log(` Average score: ${avgScore}/100`);
|
|
477
|
+
console.log(` Average live audit score: ${avgScore}/100`);
|
|
456
478
|
console.log(` Output: ${outputPath}`);
|
|
457
479
|
console.log('');
|
|
458
480
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
const TEST_COMMAND_PATTERNS = [
|
|
4
|
+
/\b(?:npm|pnpm|yarn|bun)(?:\s+run)?\s+test\b/i,
|
|
5
|
+
/\b(?:python\s+-m\s+)?pytest\b/i,
|
|
6
|
+
/\bpython\s+manage\.py\s+test\b/i,
|
|
7
|
+
/\bdjango-admin\s+test\b/i,
|
|
8
|
+
/\bpython\s+-m\s+unittest\b/i,
|
|
9
|
+
/\bgo\s+test(?:\s|$)/i,
|
|
10
|
+
/\bcargo\s+test\b/i,
|
|
11
|
+
/\bmake\s+test\b/i,
|
|
12
|
+
/\bmix\s+test\b/i,
|
|
13
|
+
/\bbundle\s+exec\s+rspec\b/i,
|
|
14
|
+
/\brspec\b/i,
|
|
15
|
+
/\bphpunit\b/i,
|
|
16
|
+
/\bdotnet\s+test(?:\s|$)/i,
|
|
17
|
+
/\bflutter\s+test\b/i,
|
|
18
|
+
/\bswift\s+test\b/i,
|
|
19
|
+
/\bxcodebuild\b[^\n\r`]{0,200}\btest\b/i,
|
|
20
|
+
/\bgradlew?\s+test\b/i,
|
|
21
|
+
/\bmvn(?:w)?\s+test\b/i,
|
|
22
|
+
/\bplaywright\s+test\b/i,
|
|
23
|
+
/\bcypress\s+run\b/i,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const LINT_COMMAND_PATTERNS = [
|
|
27
|
+
/\b(?:npm|pnpm|yarn|bun)(?:\s+run)?\s+lint\b/i,
|
|
28
|
+
/\beslint\b/i,
|
|
29
|
+
/\bprettier\b/i,
|
|
30
|
+
/\bruff(?:\s+(?:check|format))?\b/i,
|
|
31
|
+
/\bblack\b/i,
|
|
32
|
+
/\bflake8\b/i,
|
|
33
|
+
/\bpylint\b/i,
|
|
34
|
+
/\bmypy\b/i,
|
|
35
|
+
/\bpyright\b/i,
|
|
36
|
+
/\bgo\s+vet\b/i,
|
|
37
|
+
/\bgofmt\b/i,
|
|
38
|
+
/\bgofumpt\b/i,
|
|
39
|
+
/\bstaticcheck\b/i,
|
|
40
|
+
/\bgolangci-lint\b/i,
|
|
41
|
+
/\bcargo\s+clippy\b/i,
|
|
42
|
+
/\bflutter\s+analyze\b/i,
|
|
43
|
+
/\bdart\s+analyze\b/i,
|
|
44
|
+
/\bswiftlint\b/i,
|
|
45
|
+
/\bswift(?:-|\s+)format\b/i,
|
|
46
|
+
/\bdotnet\s+format(?:\s|$)/i,
|
|
47
|
+
/\bgradlew?\s+lint\b/i,
|
|
48
|
+
/\bmvn(?:w)?\s+(?:checkstyle:check|spotbugs:check|verify)\b/i,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const BUILD_COMMAND_PATTERNS = [
|
|
52
|
+
/\b(?:npm|pnpm|yarn|bun)(?:\s+run)?\s+build\b/i,
|
|
53
|
+
/\btsc(?:\s|$)/i,
|
|
54
|
+
/\bgo\s+build(?:\s|$)/i,
|
|
55
|
+
/\bcargo\s+(?:build|check)\b/i,
|
|
56
|
+
/\bmake\s+build\b/i,
|
|
57
|
+
/\bdotnet\s+(?:build|publish)(?:\s|$)/i,
|
|
58
|
+
/\bmsbuild\b/i,
|
|
59
|
+
/\bflutter\s+build(?:\s|$)/i,
|
|
60
|
+
/\bswift\s+build\b/i,
|
|
61
|
+
/\bxcodebuild\b/i,
|
|
62
|
+
/\bgradlew?\s+(?:build|assemble)\b/i,
|
|
63
|
+
/\bmvn(?:w)?\s+(?:compile|package|verify|install)\b/i,
|
|
64
|
+
/\bpython\s+-m\s+build\b/i,
|
|
65
|
+
/\bpoetry\s+build\b/i,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
function normalizePath(filePath) {
|
|
69
|
+
return String(filePath || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function addSurface(ctx, surfaces, seen, filePath) {
|
|
73
|
+
const normalized = normalizePath(filePath);
|
|
74
|
+
if (!normalized || seen.has(normalized)) return;
|
|
75
|
+
const content = typeof ctx.fileContent === 'function' ? (ctx.fileContent(normalized) || '') : '';
|
|
76
|
+
if (!content.trim()) return;
|
|
77
|
+
seen.add(normalized);
|
|
78
|
+
surfaces.push({ path: normalized, content });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function addDirSurfaces(ctx, surfaces, seen, dirPath, filter) {
|
|
82
|
+
if (typeof ctx.dirFiles !== 'function') return;
|
|
83
|
+
for (const entry of ctx.dirFiles(dirPath) || []) {
|
|
84
|
+
if (filter && !filter.test(entry)) continue;
|
|
85
|
+
addSurface(ctx, surfaces, seen, path.join(dirPath, entry));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildSurfaceList(ctx, scope) {
|
|
90
|
+
const surfaces = [];
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
const includeReadme = scope === 'repo';
|
|
93
|
+
|
|
94
|
+
addSurface(ctx, surfaces, seen, 'CLAUDE.md');
|
|
95
|
+
addSurface(ctx, surfaces, seen, '.claude/CLAUDE.md');
|
|
96
|
+
addDirSurfaces(ctx, surfaces, seen, '.claude/rules', /\.md$/i);
|
|
97
|
+
addDirSurfaces(ctx, surfaces, seen, '.claude/commands', /\.md$/i);
|
|
98
|
+
addDirSurfaces(ctx, surfaces, seen, '.claude/agents', /\.md$/i);
|
|
99
|
+
|
|
100
|
+
if (scope === 'repo') {
|
|
101
|
+
addSurface(ctx, surfaces, seen, 'AGENTS.md');
|
|
102
|
+
addSurface(ctx, surfaces, seen, 'AGENTS.override.md');
|
|
103
|
+
addSurface(ctx, surfaces, seen, '.cursorrules');
|
|
104
|
+
addSurface(ctx, surfaces, seen, '.windsurfrules');
|
|
105
|
+
addSurface(ctx, surfaces, seen, 'GEMINI.md');
|
|
106
|
+
addSurface(ctx, surfaces, seen, '.gemini/GEMINI.md');
|
|
107
|
+
addSurface(ctx, surfaces, seen, '.github/copilot-instructions.md');
|
|
108
|
+
addDirSurfaces(ctx, surfaces, seen, '.cursor/rules', /\.(md|mdc)$/i);
|
|
109
|
+
addDirSurfaces(ctx, surfaces, seen, '.cursor/commands', /\.md$/i);
|
|
110
|
+
addDirSurfaces(ctx, surfaces, seen, '.windsurf/rules', /\.md$/i);
|
|
111
|
+
addDirSurfaces(ctx, surfaces, seen, '.windsurf/workflows', /\.md$/i);
|
|
112
|
+
addDirSurfaces(ctx, surfaces, seen, '.github/instructions', /\.instructions\.md$/i);
|
|
113
|
+
addDirSurfaces(ctx, surfaces, seen, '.github/prompts', /\.prompt\.md$/i);
|
|
114
|
+
addDirSurfaces(ctx, surfaces, seen, '.opencode/commands', /\.(md|markdown|ya?ml)$/i);
|
|
115
|
+
addDirSurfaces(ctx, surfaces, seen, '.gemini/agents', /\.md$/i);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (includeReadme) {
|
|
119
|
+
addSurface(ctx, surfaces, seen, 'README.md');
|
|
120
|
+
addSurface(ctx, surfaces, seen, 'CONTRIBUTING.md');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return surfaces;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getSurfaceBundle(ctx, scope) {
|
|
127
|
+
const cacheKey = scope === 'repo' ? '__nerviqRepoInstructionBundle' : '__nerviqClaudeInstructionBundle';
|
|
128
|
+
if (ctx && ctx[cacheKey] !== undefined) return ctx[cacheKey];
|
|
129
|
+
const bundle = buildSurfaceList(ctx, scope)
|
|
130
|
+
.map((surface) => surface.content)
|
|
131
|
+
.join('\n\n');
|
|
132
|
+
if (ctx) ctx[cacheKey] = bundle;
|
|
133
|
+
return bundle;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function matchesAny(text, patterns) {
|
|
137
|
+
const normalized = String(text || '');
|
|
138
|
+
return patterns.some((pattern) => {
|
|
139
|
+
pattern.lastIndex = 0;
|
|
140
|
+
return pattern.test(normalized);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function hasDocumentedTestCommand(text) {
|
|
145
|
+
return matchesAny(text, TEST_COMMAND_PATTERNS);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function hasDocumentedLintCommand(text) {
|
|
149
|
+
return matchesAny(text, LINT_COMMAND_PATTERNS);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function hasDocumentedBuildCommand(text) {
|
|
153
|
+
return matchesAny(text, BUILD_COMMAND_PATTERNS);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function hasDocumentedVerificationGuidance(text) {
|
|
157
|
+
const normalized = String(text || '');
|
|
158
|
+
if (!normalized.trim()) return false;
|
|
159
|
+
return hasDocumentedTestCommand(normalized) ||
|
|
160
|
+
hasDocumentedLintCommand(normalized) ||
|
|
161
|
+
hasDocumentedBuildCommand(normalized) ||
|
|
162
|
+
(/\b(?:verification|verify|self-check|quality gate)\b/i.test(normalized) &&
|
|
163
|
+
matchesAny(normalized, [
|
|
164
|
+
...TEST_COMMAND_PATTERNS,
|
|
165
|
+
...LINT_COMMAND_PATTERNS,
|
|
166
|
+
...BUILD_COMMAND_PATTERNS,
|
|
167
|
+
]));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getClaudeInstructionBundle(ctx) {
|
|
171
|
+
return getSurfaceBundle(ctx, 'claude');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getRepoInstructionBundle(ctx) {
|
|
175
|
+
return getSurfaceBundle(ctx, 'repo');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = {
|
|
179
|
+
getClaudeInstructionBundle,
|
|
180
|
+
getRepoInstructionBundle,
|
|
181
|
+
hasDocumentedVerificationGuidance,
|
|
182
|
+
hasDocumentedTestCommand,
|
|
183
|
+
hasDocumentedLintCommand,
|
|
184
|
+
hasDocumentedBuildCommand,
|
|
185
|
+
};
|
package/src/locales/en.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"audit.platform": "Platform: {platform} ({version})",
|
|
8
8
|
"audit.domainPacks": "Domain packs: {packs}",
|
|
9
9
|
"audit.scope": "Scope: {message}",
|
|
10
|
-
"audit.score": "
|
|
10
|
+
"audit.score": "Live audit score: {score} ({passed}/{total} checks passing)",
|
|
11
11
|
"audit.found": "Found: {files}",
|
|
12
12
|
"audit.excellent": "Excellent setup — production-ready governance",
|
|
13
13
|
"audit.strong": "Strong setup — {count} critical items to address",
|
package/src/locales/es.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"audit.platform": "Plataforma: {platform} ({version})",
|
|
8
8
|
"audit.domainPacks": "Paquetes de dominio: {packs}",
|
|
9
9
|
"audit.scope": "Alcance: {message}",
|
|
10
|
-
"audit.score": "Puntuación: {score}
|
|
10
|
+
"audit.score": "Puntuación de auditoría en vivo: {score} ({passed}/{total} verificaciones aprobadas)",
|
|
11
11
|
"audit.found": "Encontrado: {files}",
|
|
12
12
|
"audit.excellent": "Configuración excelente — gobernanza lista para producción",
|
|
13
13
|
"audit.strong": "Configuración sólida — {count} elementos críticos por resolver",
|
package/src/stack-checks.js
CHANGED
|
@@ -358,7 +358,7 @@ function buildStackChecks({ platform, objectPrefix, idPrefix, docs }) {
|
|
|
358
358
|
impact: 'high',
|
|
359
359
|
fix: 'Document the XCTest or `xcodebuild test` workflow so iOS verification is part of the default path.',
|
|
360
360
|
check: (ctx) => hasSwiftSurface(ctx)
|
|
361
|
-
? /xctest|swift test|xcodebuild
|
|
361
|
+
? /xctest|swift test|xcodebuild[^\n\r]{0,200}\btest\b|test target/i.test(projectText(ctx, docs)) ||
|
|
362
362
|
hasMatchingFile(ctx, /(^|[\\/])Tests([\\/]|$)|XCTestCase/i)
|
|
363
363
|
: null,
|
|
364
364
|
}),
|
package/src/synergy/report.js
CHANGED
|
@@ -54,6 +54,7 @@ function formatSynergyReport(options) {
|
|
|
54
54
|
lines.push(c(' ║ SYNERGY DASHBOARD [EXPERIMENTAL] ║', 'blue'));
|
|
55
55
|
lines.push(c(' ╚══════════════════════════════════════════════════╝', 'blue'));
|
|
56
56
|
lines.push(c(' Static routing rules. Learned routing planned for v2.0.', 'dim'));
|
|
57
|
+
lines.push(c(' Harmony is the GA cross-platform surface. Treat Synergy as advisory research output.', 'dim'));
|
|
57
58
|
lines.push('');
|
|
58
59
|
|
|
59
60
|
// Compound audit
|
package/src/techniques.js
CHANGED
|
@@ -4,8 +4,15 @@
|
|
|
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 {
|
|
10
|
+
getClaudeInstructionBundle,
|
|
11
|
+
hasDocumentedVerificationGuidance,
|
|
12
|
+
hasDocumentedTestCommand,
|
|
13
|
+
hasDocumentedLintCommand,
|
|
14
|
+
hasDocumentedBuildCommand,
|
|
15
|
+
} = require('./instruction-surfaces');
|
|
9
16
|
|
|
10
17
|
function hasFrontendSignals(ctx) {
|
|
11
18
|
const pkg = ctx.fileContent('package.json') || '';
|
|
@@ -444,62 +451,58 @@ const TECHNIQUES = {
|
|
|
444
451
|
// === QUALITY & TESTING (category: 'quality') ================
|
|
445
452
|
// ============================================================
|
|
446
453
|
|
|
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
|
-
},
|
|
454
|
+
verificationLoop: {
|
|
455
|
+
id: 93,
|
|
456
|
+
name: 'Claude instruction surfaces include verification criteria',
|
|
457
|
+
check: (ctx) => {
|
|
458
|
+
const docs = getClaudeInstructionBundle(ctx);
|
|
459
|
+
return hasDocumentedVerificationGuidance(docs);
|
|
460
|
+
},
|
|
461
|
+
impact: 'critical',
|
|
462
|
+
rating: 5,
|
|
463
|
+
category: 'quality',
|
|
464
|
+
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.',
|
|
465
|
+
template: null
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
testCommand: {
|
|
469
|
+
id: 93001,
|
|
470
|
+
name: 'Claude instruction surfaces include a test command',
|
|
471
|
+
check: (ctx) => {
|
|
472
|
+
return hasDocumentedTestCommand(getClaudeInstructionBundle(ctx));
|
|
473
|
+
},
|
|
474
|
+
impact: 'high',
|
|
475
|
+
rating: 5,
|
|
476
|
+
category: 'quality',
|
|
477
|
+
fix: 'Add an explicit test command to your Claude instruction surfaces (for example "Run `npm test` before committing").',
|
|
478
|
+
template: null
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
lintCommand: {
|
|
482
|
+
id: 93002,
|
|
483
|
+
name: 'Claude instruction surfaces include a lint command',
|
|
484
|
+
check: (ctx) => {
|
|
485
|
+
return hasDocumentedLintCommand(getClaudeInstructionBundle(ctx));
|
|
486
|
+
},
|
|
487
|
+
impact: 'high',
|
|
488
|
+
rating: 4,
|
|
489
|
+
category: 'quality',
|
|
490
|
+
fix: 'Add a lint command to your Claude instruction surfaces so Claude can check style and static quality automatically.',
|
|
491
|
+
template: null
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
buildCommand: {
|
|
495
|
+
id: 93003,
|
|
496
|
+
name: 'Claude instruction surfaces include a build command',
|
|
497
|
+
check: (ctx) => {
|
|
498
|
+
return hasDocumentedBuildCommand(getClaudeInstructionBundle(ctx));
|
|
499
|
+
},
|
|
500
|
+
impact: 'medium',
|
|
501
|
+
rating: 4,
|
|
502
|
+
category: 'quality',
|
|
503
|
+
fix: 'Add a build command to your Claude instruction surfaces so Claude can verify compilation before committing.',
|
|
504
|
+
template: null
|
|
505
|
+
},
|
|
503
506
|
|
|
504
507
|
// ============================================================
|
|
505
508
|
// === GIT SAFETY (category: 'git') ===========================
|
package/src/workspace.js
CHANGED
|
@@ -166,6 +166,17 @@ 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
|
+
|
|
169
180
|
async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
|
|
170
181
|
const { audit } = require('./audit');
|
|
171
182
|
const rootDir = path.resolve(dir);
|
|
@@ -175,6 +186,22 @@ async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
|
|
|
175
186
|
? expandWorkspacePatterns(rootDir, selectedPatterns)
|
|
176
187
|
: detectWorkspaces(rootDir);
|
|
177
188
|
const results = [];
|
|
189
|
+
let rootGovernance;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const rootResult = await audit({ dir: rootDir, platform, silent: true });
|
|
193
|
+
rootGovernance = summarizeAuditResult(rootResult, 'root-live-audit', 'root-governance');
|
|
194
|
+
} catch (error) {
|
|
195
|
+
rootGovernance = {
|
|
196
|
+
scope: 'root-governance',
|
|
197
|
+
scoreType: 'root-live-audit',
|
|
198
|
+
score: null,
|
|
199
|
+
passed: 0,
|
|
200
|
+
total: 0,
|
|
201
|
+
topAction: null,
|
|
202
|
+
error: error.message,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
178
205
|
|
|
179
206
|
for (const workspacePath of workspacePaths) {
|
|
180
207
|
const absPath = path.join(rootDir, workspacePath);
|
|
@@ -185,10 +212,7 @@ async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
|
|
|
185
212
|
workspace: workspacePath,
|
|
186
213
|
dir: absPath,
|
|
187
214
|
platform,
|
|
188
|
-
|
|
189
|
-
passed: result.passed,
|
|
190
|
-
total: result.checkCount,
|
|
191
|
-
topAction: result.topNextActions?.[0]?.name || null,
|
|
215
|
+
...summarizeAuditResult(result, 'workspace-live-audit', 'workspace-package'),
|
|
192
216
|
result,
|
|
193
217
|
});
|
|
194
218
|
} catch (error) {
|
|
@@ -197,6 +221,8 @@ async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
|
|
|
197
221
|
workspace: workspacePath,
|
|
198
222
|
dir: absPath,
|
|
199
223
|
platform,
|
|
224
|
+
scope: 'workspace-package',
|
|
225
|
+
scoreType: 'workspace-live-audit',
|
|
200
226
|
score: null,
|
|
201
227
|
passed: 0,
|
|
202
228
|
total: 0,
|
|
@@ -210,17 +236,36 @@ async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
|
|
|
210
236
|
const averageScore = validScores.length > 0
|
|
211
237
|
? Math.round(validScores.reduce((sum, value) => sum + value, 0) / validScores.length)
|
|
212
238
|
: 0;
|
|
239
|
+
const maxScore = validScores.length > 0 ? Math.max(...validScores) : 0;
|
|
240
|
+
const minScore = validScores.length > 0 ? Math.min(...validScores) : 0;
|
|
213
241
|
|
|
214
242
|
return {
|
|
243
|
+
summaryType: 'monorepo-workspace-audit',
|
|
215
244
|
rootDir,
|
|
216
245
|
platform,
|
|
246
|
+
selectionMode: selectedPatterns.length > 0 ? 'explicit-patterns' : 'detected-workspaces',
|
|
217
247
|
patterns: sourcePatterns,
|
|
248
|
+
rootGovernance,
|
|
249
|
+
workspaceAggregate: {
|
|
250
|
+
scope: 'workspace-aggregate',
|
|
251
|
+
scoreType: 'workspace-average-live-audit',
|
|
252
|
+
score: averageScore,
|
|
253
|
+
workspaceCount: workspacePaths.length,
|
|
254
|
+
maxScore,
|
|
255
|
+
minScore,
|
|
256
|
+
},
|
|
257
|
+
scoreSemantics: {
|
|
258
|
+
rootGovernance: 'Root repo live audit for shared instructions, hooks, permissions, and top-level governance files.',
|
|
259
|
+
workspaceAggregate: 'Average of the selected workspace live audit scores. This is a package coverage rollup, not the root repo score.',
|
|
260
|
+
workspaceEntries: 'Each workspace row is a package-level live audit. Package scores can differ from the root governance score for legitimate reasons.',
|
|
261
|
+
},
|
|
218
262
|
workspaces: results,
|
|
219
263
|
detectedWorkspaces: workspacePaths,
|
|
220
264
|
workspaceCount: workspacePaths.length,
|
|
265
|
+
averageScoreType: 'workspace-average-live-audit',
|
|
221
266
|
averageScore,
|
|
222
|
-
maxScore
|
|
223
|
-
minScore
|
|
267
|
+
maxScore,
|
|
268
|
+
minScore,
|
|
224
269
|
};
|
|
225
270
|
}
|
|
226
271
|
|