@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/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">out of 100</div>
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(` Score: ${auditPayload.score ?? '?'}/100`);
251
- console.log(` Snapshots: ${history.length}`);
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
- if (current.score != null && previous.score != null) {
278
- const delta = current.score - previous.score;
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: previous.score,
283
- to: current.score,
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">⚠ Score Drift Alerts</h2>
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
+ };
@@ -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": "Score: {score}/100 ({passed}/{total} checks passing)",
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",
@@ -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}/100 ({passed}/{total} verificaciones aprobadas)",
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",
@@ -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 test|test target/i.test(projectText(ctx, docs)) ||
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
  }),
@@ -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: 'Verification criteria in CLAUDE.md',
450
- check: (ctx) => {
451
- const md = ctx.claudeMdContent() || '';
452
- return /\b(npm test|yarn test|pnpm test|pytest|go test|make test|npm run lint|yarn lint|npx |ruff |eslint)\b/i.test(md) ||
453
- /\b(test command|lint command|build command|verify|run tests|run lint)\b/i.test(md);
454
- },
455
- impact: 'critical',
456
- rating: 5,
457
- category: 'quality',
458
- fix: 'Add test/lint/build commands to CLAUDE.md so Claude can verify its own work.',
459
- template: null
460
- },
461
-
462
- testCommand: {
463
- id: 93001,
464
- name: 'CLAUDE.md contains a test command',
465
- check: (ctx) => {
466
- const md = ctx.claudeMdContent() || '';
467
- return /npm test|pytest|jest|vitest|cargo test|go test|mix test|rspec/.test(md);
468
- },
469
- impact: 'high',
470
- rating: 5,
471
- category: 'quality',
472
- fix: 'Add an explicit test command to CLAUDE.md (e.g. "Run `npm test` before committing").',
473
- template: null
474
- },
475
-
476
- lintCommand: {
477
- id: 93002,
478
- name: 'CLAUDE.md contains a lint command',
479
- check: (ctx) => {
480
- const md = ctx.claudeMdContent() || '';
481
- return /eslint|prettier|ruff|black|clippy|golangci-lint|rubocop|npm run lint|yarn lint|pnpm lint|bun lint/.test(md);
482
- },
483
- impact: 'high',
484
- rating: 4,
485
- category: 'quality',
486
- fix: 'Add a lint command to CLAUDE.md so Claude auto-formats and checks code style.',
487
- template: null
488
- },
489
-
490
- buildCommand: {
491
- id: 93003,
492
- name: 'CLAUDE.md contains a build command',
493
- check: (ctx) => {
494
- const md = ctx.claudeMdContent() || '';
495
- return /npm run build|cargo build|go build|make|tsc|gradle build|mvn compile/.test(md);
496
- },
497
- impact: 'medium',
498
- rating: 4,
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
- score: result.score,
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: validScores.length > 0 ? Math.max(...validScores) : 0,
223
- minScore: validScores.length > 0 ? Math.min(...validScores) : 0,
267
+ maxScore,
268
+ minScore,
224
269
  };
225
270
  }
226
271