claudex-setup 1.6.0 → 1.8.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/analyze.js ADDED
@@ -0,0 +1,423 @@
1
+ /**
2
+ * Project scanner + recommendation layer for augment and suggest-only modes.
3
+ * Produces a structured repo-aware analysis without writing files.
4
+ */
5
+
6
+ const path = require('path');
7
+ const { audit } = require('./audit');
8
+ const { ProjectContext } = require('./context');
9
+ const { STACKS } = require('./techniques');
10
+ const { detectDomainPacks } = require('./domain-packs');
11
+ const { recommendMcpPacks } = require('./mcp-packs');
12
+
13
+ const COLORS = {
14
+ reset: '\x1b[0m',
15
+ bold: '\x1b[1m',
16
+ dim: '\x1b[2m',
17
+ red: '\x1b[31m',
18
+ green: '\x1b[32m',
19
+ yellow: '\x1b[33m',
20
+ blue: '\x1b[36m',
21
+ magenta: '\x1b[35m',
22
+ };
23
+
24
+ function c(text, color) {
25
+ return `${COLORS[color] || ''}${text}${COLORS.reset}`;
26
+ }
27
+
28
+ function escapeRegex(value) {
29
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
30
+ }
31
+
32
+ function extractTomlSection(content, sectionName) {
33
+ const pattern = new RegExp(`\\[${escapeRegex(sectionName)}\\]([\\s\\S]*?)(?:\\n\\s*\\[|$)`);
34
+ const match = content.match(pattern);
35
+ return match ? match[1] : null;
36
+ }
37
+
38
+ function extractTomlValue(sectionContent, key) {
39
+ if (!sectionContent) return null;
40
+ const pattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=\\s*["']([^"']+)["']`, 'm');
41
+ const match = sectionContent.match(pattern);
42
+ return match ? match[1].trim() : null;
43
+ }
44
+
45
+ function detectProjectMetadata(ctx) {
46
+ const pkg = ctx.jsonFile('package.json');
47
+ if (pkg && (pkg.name || pkg.description)) {
48
+ return {
49
+ name: pkg.name || path.basename(ctx.dir),
50
+ description: pkg.description || '',
51
+ };
52
+ }
53
+
54
+ const pyproject = ctx.fileContent('pyproject.toml') || '';
55
+ if (pyproject) {
56
+ const projectSection = extractTomlSection(pyproject, 'project');
57
+ const poetrySection = extractTomlSection(pyproject, 'tool.poetry');
58
+ const name = extractTomlValue(projectSection, 'name') ||
59
+ extractTomlValue(poetrySection, 'name');
60
+ const description = extractTomlValue(projectSection, 'description') ||
61
+ extractTomlValue(poetrySection, 'description');
62
+
63
+ if (name || description) {
64
+ return {
65
+ name: name || path.basename(ctx.dir),
66
+ description: description || '',
67
+ };
68
+ }
69
+ }
70
+
71
+ return {
72
+ name: path.basename(ctx.dir),
73
+ description: '',
74
+ };
75
+ }
76
+
77
+ function detectMainDirs(ctx) {
78
+ const candidates = [
79
+ 'src', 'lib', 'app', 'pages', 'components', 'api', 'routes', 'utils', 'helpers',
80
+ 'services', 'models', 'controllers', 'views', 'public', 'assets', 'config', 'tests',
81
+ 'test', '__tests__', 'spec', 'scripts', 'prisma', 'db', 'middleware', 'hooks',
82
+ 'agents', 'chains', 'workers', 'jobs', 'dags', 'macros', 'migrations',
83
+ 'src/components', 'src/app', 'src/pages', 'src/api', 'src/lib', 'src/hooks',
84
+ 'src/utils', 'src/services', 'src/models', 'src/middleware', 'src/agents',
85
+ 'src/chains', 'src/workers', 'src/jobs', 'src/app/api', 'app/api',
86
+ 'models/staging', 'models/marts'
87
+ ];
88
+
89
+ const dirs = [];
90
+ for (const dir of candidates) {
91
+ if (ctx.hasDir(dir)) {
92
+ dirs.push(dir);
93
+ }
94
+ }
95
+ return dirs;
96
+ }
97
+
98
+ function collectClaudeAssets(ctx) {
99
+ const sharedSettings = ctx.jsonFile('.claude/settings.json');
100
+ const localSettings = ctx.jsonFile('.claude/settings.local.json');
101
+ const settings = sharedSettings || localSettings || null;
102
+
103
+ const assetFiles = {
104
+ claudeMd: ctx.fileContent('CLAUDE.md') ? 'CLAUDE.md' : (ctx.fileContent('.claude/CLAUDE.md') ? '.claude/CLAUDE.md' : null),
105
+ settings: sharedSettings ? '.claude/settings.json' : (localSettings ? '.claude/settings.local.json' : null),
106
+ commands: ctx.hasDir('.claude/commands') ? ctx.dirFiles('.claude/commands') : [],
107
+ rules: ctx.hasDir('.claude/rules') ? ctx.dirFiles('.claude/rules') : [],
108
+ hooks: ctx.hasDir('.claude/hooks') ? ctx.dirFiles('.claude/hooks') : [],
109
+ agents: ctx.hasDir('.claude/agents') ? ctx.dirFiles('.claude/agents') : [],
110
+ skills: ctx.hasDir('.claude/skills') ? ctx.dirFiles('.claude/skills') : [],
111
+ };
112
+
113
+ return {
114
+ files: assetFiles,
115
+ counts: {
116
+ commands: assetFiles.commands.length,
117
+ rules: assetFiles.rules.length,
118
+ hooks: assetFiles.hooks.length,
119
+ agents: assetFiles.agents.length,
120
+ skills: assetFiles.skills.length,
121
+ mcpServers: settings && settings.mcpServers ? Object.keys(settings.mcpServers).length : 0,
122
+ },
123
+ permissions: settings && settings.permissions ? {
124
+ defaultMode: settings.permissions.defaultMode || null,
125
+ hasDenyRules: Array.isArray(settings.permissions.deny) && settings.permissions.deny.length > 0,
126
+ } : null,
127
+ settingsSource: assetFiles.settings,
128
+ };
129
+ }
130
+
131
+ function detectMaturity(assets) {
132
+ let score = 0;
133
+ if (assets.files.claudeMd) score += 2;
134
+ if (assets.files.settings) score += 1;
135
+ if (assets.counts.rules > 0) score += 1;
136
+ if (assets.counts.commands > 0) score += 1;
137
+ if (assets.counts.hooks > 0) score += 1;
138
+ if (assets.counts.agents > 0) score += 1;
139
+ if (assets.counts.skills > 0) score += 1;
140
+
141
+ if (score === 0) return 'none';
142
+ if (score <= 2) return 'starter';
143
+ if (score <= 5) return 'developing';
144
+ return 'mature';
145
+ }
146
+
147
+ function riskFromImpact(impact) {
148
+ if (impact === 'critical') return 'high';
149
+ if (impact === 'high') return 'medium';
150
+ return 'low';
151
+ }
152
+
153
+ function moduleFromCategory(category) {
154
+ const map = {
155
+ memory: 'CLAUDE.md',
156
+ quality: 'verification',
157
+ git: 'safety',
158
+ workflow: 'commands-agents-skills',
159
+ security: 'permissions',
160
+ automation: 'hooks',
161
+ design: 'design-rules',
162
+ devops: 'ci-devops',
163
+ hygiene: 'project-hygiene',
164
+ performance: 'context-management',
165
+ tools: 'mcp-tools',
166
+ prompting: 'prompt-structure',
167
+ features: 'modern-claude-features',
168
+ 'quality-deep': 'quality-deep',
169
+ };
170
+ return map[category] || category;
171
+ }
172
+
173
+ function toStrengths(results) {
174
+ return results
175
+ .filter(r => r.passed === true)
176
+ .sort((a, b) => {
177
+ const order = { critical: 3, high: 2, medium: 1, low: 0 };
178
+ return (order[b.impact] || 0) - (order[a.impact] || 0);
179
+ })
180
+ .slice(0, 6)
181
+ .map(r => ({
182
+ key: r.key,
183
+ name: r.name,
184
+ category: r.category,
185
+ note: `Already present and worth preserving: ${r.name}.`,
186
+ }));
187
+ }
188
+
189
+ function toGaps(results) {
190
+ return results
191
+ .filter(r => r.passed === false)
192
+ .sort((a, b) => {
193
+ const order = { critical: 3, high: 2, medium: 1, low: 0 };
194
+ return (order[b.impact] || 0) - (order[a.impact] || 0);
195
+ })
196
+ .slice(0, 8)
197
+ .map(r => ({
198
+ key: r.key,
199
+ name: r.name,
200
+ impact: r.impact,
201
+ category: r.category,
202
+ fix: r.fix,
203
+ }));
204
+ }
205
+
206
+ function toRecommendations(auditResult) {
207
+ const failed = auditResult.results
208
+ .filter(r => r.passed === false)
209
+ .sort((a, b) => {
210
+ const order = { critical: 3, high: 2, medium: 1, low: 0 };
211
+ return (order[b.impact] || 0) - (order[a.impact] || 0);
212
+ });
213
+
214
+ return failed.slice(0, 10).map((r, index) => ({
215
+ priority: index + 1,
216
+ key: r.key,
217
+ name: r.name,
218
+ impact: r.impact,
219
+ module: moduleFromCategory(r.category),
220
+ risk: riskFromImpact(r.impact),
221
+ why: r.fix,
222
+ }));
223
+ }
224
+
225
+ function buildOptionalModules(stacks, assets) {
226
+ const stackKeys = stacks.map(s => s.key);
227
+ const modules = [];
228
+
229
+ if (!assets.files.claudeMd) modules.push('CLAUDE.md baseline');
230
+ if (assets.counts.commands === 0) modules.push('Slash commands');
231
+ if (assets.counts.hooks === 0) modules.push('Hooks automation');
232
+ if (!assets.permissions || !assets.permissions.hasDenyRules) modules.push('Permission safety profile');
233
+ if (assets.counts.rules === 0) modules.push('Path-specific rules');
234
+ if (stackKeys.some(k => ['react', 'nextjs', 'vue', 'angular', 'svelte'].includes(k))) modules.push('Frontend pack');
235
+ if (stackKeys.some(k => ['node', 'python', 'django', 'fastapi', 'go', 'rust', 'java'].includes(k))) modules.push('Backend pack');
236
+ if (stackKeys.some(k => ['docker', 'terraform', 'kubernetes'].includes(k))) modules.push('DevOps pack');
237
+ if (assets.counts.agents === 0) modules.push('Specialized agents');
238
+
239
+ return [...new Set(modules)].slice(0, 8);
240
+ }
241
+
242
+ function buildRiskNotes(auditResult, assets, maturity) {
243
+ const notes = [];
244
+ if (!assets.files.claudeMd) notes.push('No CLAUDE.md exists yet, so Claude has no persistent project-specific guidance.');
245
+ if (assets.permissions && assets.permissions.defaultMode === 'bypassPermissions') {
246
+ notes.push('Current settings use bypassPermissions, which is risky for broader team adoption.');
247
+ }
248
+ if (!assets.permissions || !assets.permissions.hasDenyRules) {
249
+ notes.push('Permissions lack deny rules, so secret access and destructive commands are not strongly guarded.');
250
+ }
251
+ if (maturity === 'mature') {
252
+ notes.push('This repo already has meaningful Claude assets, so augment mode should preserve existing structure instead of overwriting it.');
253
+ }
254
+ if (auditResult.results.some(r => r.key === 'ciPipeline' && r.passed === false)) {
255
+ notes.push('Without CI enforcement, readiness can drift after setup.');
256
+ }
257
+ return notes.slice(0, 5);
258
+ }
259
+
260
+ function buildRolloutOrder(report) {
261
+ const steps = [];
262
+ if (!report.existingClaudeAssets.claudeMd) steps.push('Create a project-specific CLAUDE.md baseline');
263
+ if (report.gapsIdentified.some(g => g.category === 'security')) steps.push('Add safe settings and deny rules');
264
+ if (report.gapsIdentified.some(g => g.category === 'automation')) steps.push('Add hooks and automate verification');
265
+ if (report.gapsIdentified.some(g => g.category === 'workflow')) steps.push('Add commands, rules, and specialization modules');
266
+ if (report.gapsIdentified.some(g => g.category === 'devops')) steps.push('Connect CI threshold enforcement');
267
+ if (steps.length === 0) steps.push('Tighten quality-deep items and preserve the current setup');
268
+ return steps;
269
+ }
270
+
271
+ async function analyzeProject(options) {
272
+ const mode = options.mode || 'augment';
273
+ const ctx = new ProjectContext(options.dir);
274
+ const stacks = ctx.detectStacks(STACKS);
275
+ const auditResult = await audit({ ...options, silent: true });
276
+ const assets = collectClaudeAssets(ctx);
277
+ const metadata = detectProjectMetadata(ctx);
278
+ const maturity = detectMaturity(assets);
279
+ const mainDirs = detectMainDirs(ctx);
280
+ const recommendedDomainPacks = detectDomainPacks(ctx, stacks, assets);
281
+ const recommendedMcpPacks = recommendMcpPacks(stacks, recommendedDomainPacks);
282
+
283
+ const report = {
284
+ mode,
285
+ writeBehavior: 'No files are written in this mode.',
286
+ projectSummary: {
287
+ name: metadata.name,
288
+ description: metadata.description,
289
+ directory: options.dir,
290
+ stacks: stacks.map(s => s.label),
291
+ domains: recommendedDomainPacks.map(pack => pack.label),
292
+ maturity,
293
+ score: auditResult.score,
294
+ organicScore: auditResult.organicScore,
295
+ checkCount: auditResult.checkCount,
296
+ },
297
+ detectedArchitecture: {
298
+ repoType: stacks.length > 0 ? 'stack-detected repo' : 'generic repo',
299
+ mainDirectories: mainDirs,
300
+ stackSignals: stacks.map(s => s.key),
301
+ },
302
+ existingClaudeAssets: {
303
+ claudeMd: assets.files.claudeMd,
304
+ settings: assets.settingsSource,
305
+ commands: assets.files.commands,
306
+ rules: assets.files.rules,
307
+ hooks: assets.files.hooks,
308
+ agents: assets.files.agents,
309
+ skills: assets.files.skills,
310
+ mcpServers: assets.counts.mcpServers,
311
+ },
312
+ strengthsPreserved: toStrengths(auditResult.results),
313
+ gapsIdentified: toGaps(auditResult.results),
314
+ topNextActions: auditResult.quickWins,
315
+ recommendedImprovements: toRecommendations(auditResult),
316
+ recommendedDomainPacks,
317
+ recommendedMcpPacks,
318
+ riskNotes: buildRiskNotes(auditResult, assets, maturity),
319
+ optionalModules: buildOptionalModules(stacks, assets),
320
+ };
321
+
322
+ report.suggestedRolloutOrder = buildRolloutOrder(report);
323
+ return report;
324
+ }
325
+
326
+ function printAnalysis(report, options = {}) {
327
+ if (options.json) {
328
+ console.log(JSON.stringify(report, null, 2));
329
+ return;
330
+ }
331
+
332
+ const modeLabel = report.mode === 'suggest-only' ? 'suggest-only' : report.mode;
333
+ console.log('');
334
+ console.log(c(` claudex-setup ${modeLabel}`, 'bold'));
335
+ console.log(c(' ═══════════════════════════════════════', 'dim'));
336
+ console.log(c(` ${report.writeBehavior}`, 'dim'));
337
+ console.log('');
338
+
339
+ console.log(c(' Project Summary', 'blue'));
340
+ console.log(` ${report.projectSummary.name}${report.projectSummary.description ? ` — ${report.projectSummary.description}` : ''}`);
341
+ console.log(c(` Stack: ${report.projectSummary.stacks.join(', ') || 'Unknown'}`, 'dim'));
342
+ console.log(c(` Domain packs: ${report.projectSummary.domains.join(', ') || 'Baseline General'}`, 'dim'));
343
+ console.log(c(` Maturity: ${report.projectSummary.maturity} | Score: ${report.projectSummary.score}/100 | Organic: ${report.projectSummary.organicScore}/100`, 'dim'));
344
+ console.log('');
345
+
346
+ console.log(c(' Detected Architecture', 'blue'));
347
+ console.log(c(` Main directories: ${report.detectedArchitecture.mainDirectories.join(', ') || 'No strong structure detected yet'}`, 'dim'));
348
+ console.log('');
349
+
350
+ console.log(c(' Existing Claude Assets', 'blue'));
351
+ console.log(c(` CLAUDE.md: ${report.existingClaudeAssets.claudeMd || 'missing'}`, 'dim'));
352
+ console.log(c(` Settings: ${report.existingClaudeAssets.settings || 'missing'}`, 'dim'));
353
+ console.log(c(` Commands: ${report.existingClaudeAssets.commands.length} | Rules: ${report.existingClaudeAssets.rules.length} | Hooks: ${report.existingClaudeAssets.hooks.length} | Agents: ${report.existingClaudeAssets.agents.length} | Skills: ${report.existingClaudeAssets.skills.length}`, 'dim'));
354
+ console.log('');
355
+
356
+ if (report.strengthsPreserved.length > 0) {
357
+ console.log(c(' Strengths Preserved', 'green'));
358
+ for (const item of report.strengthsPreserved) {
359
+ console.log(` - ${item.name}`);
360
+ }
361
+ console.log('');
362
+ }
363
+
364
+ if (report.gapsIdentified.length > 0) {
365
+ console.log(c(' Gaps Identified', 'yellow'));
366
+ for (const item of report.gapsIdentified.slice(0, 5)) {
367
+ console.log(` - [${item.impact}] ${item.name}`);
368
+ console.log(c(` ${item.fix}`, 'dim'));
369
+ }
370
+ console.log('');
371
+ }
372
+
373
+ if (report.topNextActions.length > 0) {
374
+ console.log(c(' Top 5 Next Actions', 'magenta'));
375
+ report.topNextActions.slice(0, 5).forEach((item, index) => {
376
+ console.log(` ${index + 1}. ${item.name}`);
377
+ console.log(c(` ${item.fix}`, 'dim'));
378
+ });
379
+ console.log('');
380
+ }
381
+
382
+ if (report.recommendedDomainPacks.length > 0) {
383
+ console.log(c(' Recommended Domain Packs', 'blue'));
384
+ for (const pack of report.recommendedDomainPacks) {
385
+ console.log(` - ${pack.label}`);
386
+ console.log(c(` ${pack.useWhen}`, 'dim'));
387
+ }
388
+ console.log('');
389
+ }
390
+
391
+ if (report.recommendedMcpPacks.length > 0) {
392
+ console.log(c(' Recommended MCP Packs', 'blue'));
393
+ for (const pack of report.recommendedMcpPacks) {
394
+ console.log(` - ${pack.label}`);
395
+ console.log(c(` ${pack.adoption}`, 'dim'));
396
+ }
397
+ console.log('');
398
+ }
399
+
400
+ if (report.riskNotes.length > 0) {
401
+ console.log(c(' Risk Notes', 'red'));
402
+ for (const note of report.riskNotes) {
403
+ console.log(` - ${note}`);
404
+ }
405
+ console.log('');
406
+ }
407
+
408
+ if (report.optionalModules.length > 0) {
409
+ console.log(c(' Optional Modules', 'blue'));
410
+ console.log(c(` ${report.optionalModules.join(' | ')}`, 'dim'));
411
+ console.log('');
412
+ }
413
+
414
+ if (report.suggestedRolloutOrder.length > 0) {
415
+ console.log(c(' Suggested Rollout Order', 'blue'));
416
+ report.suggestedRolloutOrder.forEach((item, index) => {
417
+ console.log(` ${index + 1}. ${item}`);
418
+ });
419
+ console.log('');
420
+ }
421
+ }
422
+
423
+ module.exports = { analyzeProject, printAnalysis };
package/src/audit.js CHANGED
@@ -29,17 +29,17 @@ function progressBar(score, max = 100, width = 20) {
29
29
  return colorize('█'.repeat(filled), color) + colorize('░'.repeat(empty), 'dim');
30
30
  }
31
31
 
32
- const EFFORT_ORDER = { critical: 0, high: 1, medium: 2 };
32
+ const IMPACT_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
33
33
 
34
34
  function getQuickWins(failed) {
35
- // Quick wins = medium impact items first (easiest), then high, sorted by name length (shorter = simpler)
36
- return [...failed]
35
+ const prioritized = failed.filter(r => !(r.category === 'hygiene' && r.impact === 'low'));
36
+ const pool = prioritized.length > 0 ? prioritized : failed;
37
+
38
+ return [...pool]
37
39
  .sort((a, b) => {
38
- const effortA = EFFORT_ORDER[a.impact] ?? 3;
39
- const effortB = EFFORT_ORDER[b.impact] ?? 3;
40
- // Prefer medium (easiest to fix), then high, then critical
41
- if (effortA !== effortB) return effortB - effortA;
42
- // Tie-break by fix length (shorter fix description = likely simpler)
40
+ const impactA = IMPACT_ORDER[a.impact] ?? 0;
41
+ const impactB = IMPACT_ORDER[b.impact] ?? 0;
42
+ if (impactA !== impactB) return impactB - impactA;
43
43
  return (a.fix || '').length - (b.fix || '').length;
44
44
  })
45
45
  .slice(0, 3);
@@ -90,10 +90,23 @@ async function audit(options) {
90
90
  const scaffoldedPassed = passed.filter(r => scaffoldedKeys.has(r.key));
91
91
  const organicEarned = organicPassed.reduce((sum, r) => sum + (weights[r.impact] || 5), 0);
92
92
  const organicScore = maxScore > 0 ? Math.round((organicEarned / maxScore) * 100) : 0;
93
+ const quickWins = getQuickWins(failed);
94
+ const result = {
95
+ score,
96
+ organicScore,
97
+ isScaffolded,
98
+ passed: passed.length,
99
+ failed: failed.length,
100
+ skipped: skipped.length,
101
+ checkCount: applicable.length,
102
+ stacks,
103
+ results,
104
+ quickWins: quickWins.map(({ key, name, impact, fix, category }) => ({ key, name, impact, category, fix })),
105
+ };
93
106
 
94
107
  // Silent mode: skip all output, just return result
95
108
  if (silent) {
96
- return { score, passed: passed.length, failed: failed.length, stacks, results };
109
+ return result;
97
110
  }
98
111
 
99
112
  if (options.json) {
@@ -101,15 +114,9 @@ async function audit(options) {
101
114
  console.log(JSON.stringify({
102
115
  version,
103
116
  timestamp: new Date().toISOString(),
104
- score,
105
- stacks,
106
- passed: passed.length,
107
- failed: failed.length,
108
- skipped: skipped.length,
109
- checkCount: applicable.length,
110
- results
117
+ ...result
111
118
  }, null, 2));
112
- return { score, passed: passed.length, failed: failed.length, stacks, results };
119
+ return result;
113
120
  }
114
121
 
115
122
  // Display results
@@ -173,8 +180,7 @@ async function audit(options) {
173
180
 
174
181
  // Quick wins
175
182
  if (failed.length > 0) {
176
- const quickWins = getQuickWins(failed);
177
- console.log(colorize(' ⚡ Quick wins (easiest fixes first)', 'magenta'));
183
+ console.log(colorize(' ⚡ Best next fixes', 'magenta'));
178
184
  for (let i = 0; i < quickWins.length; i++) {
179
185
  const r = quickWins[i];
180
186
  console.log(` ${i + 1}. ${colorize(r.name, 'bold')}`);
@@ -188,7 +194,7 @@ async function audit(options) {
188
194
  console.log(` ${colorize(`${passed.length}/${applicable.length}`, 'bold')} checks passing${skipped.length > 0 ? colorize(` (${skipped.length} not applicable)`, 'dim') : ''}`);
189
195
 
190
196
  if (failed.length > 0) {
191
- console.log(` Run ${colorize('npx claudex-setup setup', 'bold')} to fix automatically`);
197
+ console.log(` Run ${colorize('npx claudex-setup setup', 'bold')} to create starter-safe defaults`);
192
198
  }
193
199
 
194
200
  console.log('');
@@ -206,12 +212,11 @@ async function audit(options) {
206
212
  console.log('');
207
213
  }
208
214
 
209
- console.log(colorize(' Powered by CLAUDEX - 1,107 verified Claude Code techniques', 'dim'));
215
+ console.log(colorize(' Backed by CLAUDEX research and evidence', 'dim'));
210
216
  console.log(colorize(' https://github.com/DnaFin/claudex-setup', 'dim'));
211
217
  console.log('');
212
218
 
213
219
  // Send anonymous insights (opt-in, privacy-first, fire-and-forget)
214
- const result = { score, passed: passed.length, failed: failed.length, stacks, results };
215
220
  sendInsights(result);
216
221
 
217
222
  return result;