claudex-setup 1.16.0 → 1.16.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claudex-setup",
3
- "version": "1.16.0",
4
- "description": "Score your repo's Claude Code setup against 84 checks. See gaps, apply fixes selectively with rollback, govern hooks and permissions, and benchmark impact — without breaking existing config.",
3
+ "version": "1.16.1",
4
+ "description": "Score your repo's Claude Code setup against 85 checks. See gaps, apply fixes selectively with rollback, govern hooks and permissions, and benchmark impact — without breaking existing config.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "claudex-setup": "bin/cli.js"
package/src/activity.js CHANGED
@@ -21,10 +21,12 @@ function ensureArtifactDirs(dir) {
21
21
  const activityDir = path.join(root, 'activity');
22
22
  const rollbackDir = path.join(root, 'rollbacks');
23
23
  const snapshotDir = path.join(root, 'snapshots');
24
+ const outcomesDir = path.join(root, 'outcomes');
24
25
  fs.mkdirSync(activityDir, { recursive: true });
25
26
  fs.mkdirSync(rollbackDir, { recursive: true });
26
27
  fs.mkdirSync(snapshotDir, { recursive: true });
27
- return { root, activityDir, rollbackDir, snapshotDir };
28
+ fs.mkdirSync(outcomesDir, { recursive: true });
29
+ return { root, activityDir, rollbackDir, snapshotDir, outcomesDir };
28
30
  }
29
31
 
30
32
  function writeJson(filePath, payload) {
@@ -322,6 +324,192 @@ function exportTrendReport(dir) {
322
324
  return lines.join('\n');
323
325
  }
324
326
 
327
+ function readOutcomeIndex(dir) {
328
+ const indexPath = path.join(dir, '.claude', 'claudex-setup', 'outcomes', 'index.json');
329
+ if (!fs.existsSync(indexPath)) return [];
330
+ try {
331
+ const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
332
+ return Array.isArray(entries) ? entries : [];
333
+ } catch {
334
+ return [];
335
+ }
336
+ }
337
+
338
+ function updateOutcomeIndex(outcomesDir, record) {
339
+ const indexPath = path.join(outcomesDir, 'index.json');
340
+ let entries = [];
341
+
342
+ if (fs.existsSync(indexPath)) {
343
+ try {
344
+ entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
345
+ if (!Array.isArray(entries)) entries = [];
346
+ } catch {
347
+ entries = [];
348
+ }
349
+ }
350
+
351
+ entries.push(record);
352
+ const MAX_INDEX_ENTRIES = 500;
353
+ if (entries.length > MAX_INDEX_ENTRIES) {
354
+ entries = entries.slice(entries.length - MAX_INDEX_ENTRIES);
355
+ }
356
+ fs.writeFileSync(indexPath, JSON.stringify(entries, null, 2), 'utf8');
357
+ }
358
+
359
+ function normalizeOutcomeStatus(value) {
360
+ const normalized = `${value || ''}`.trim().toLowerCase();
361
+ if (!['accepted', 'rejected', 'deferred'].includes(normalized)) {
362
+ throw new Error('feedback status must be one of: accepted, rejected, deferred');
363
+ }
364
+ return normalized;
365
+ }
366
+
367
+ function normalizeOutcomeEffect(value) {
368
+ const normalized = `${value || ''}`.trim().toLowerCase();
369
+ if (!['positive', 'neutral', 'negative'].includes(normalized)) {
370
+ throw new Error('feedback effect must be one of: positive, neutral, negative');
371
+ }
372
+ return normalized;
373
+ }
374
+
375
+ function recordRecommendationOutcome(dir, payload) {
376
+ const key = `${payload.key || ''}`.trim();
377
+ if (!key) {
378
+ throw new Error('feedback requires a recommendation key');
379
+ }
380
+
381
+ const status = normalizeOutcomeStatus(payload.status);
382
+ const effect = normalizeOutcomeEffect(payload.effect || 'neutral');
383
+ const scoreDelta = Number.isFinite(payload.scoreDelta) ? payload.scoreDelta : (
384
+ payload.scoreDelta === null || payload.scoreDelta === undefined || payload.scoreDelta === ''
385
+ ? null
386
+ : Number(payload.scoreDelta)
387
+ );
388
+
389
+ if (scoreDelta !== null && !Number.isFinite(scoreDelta)) {
390
+ throw new Error('feedback scoreDelta must be a number when provided');
391
+ }
392
+
393
+ const id = timestampId();
394
+ const { outcomesDir } = ensureArtifactDirs(dir);
395
+ const filePath = path.join(outcomesDir, `${id}.json`);
396
+ const record = {
397
+ id,
398
+ createdAt: new Date().toISOString(),
399
+ key,
400
+ status,
401
+ effect,
402
+ source: `${payload.source || 'manual-cli'}`.trim() || 'manual-cli',
403
+ notes: `${payload.notes || ''}`.trim(),
404
+ scoreDelta,
405
+ };
406
+
407
+ writeJson(filePath, record);
408
+ updateOutcomeIndex(outcomesDir, {
409
+ ...record,
410
+ relativePath: path.relative(dir, filePath),
411
+ });
412
+
413
+ return {
414
+ id,
415
+ filePath,
416
+ relativePath: path.relative(dir, filePath),
417
+ record,
418
+ };
419
+ }
420
+
421
+ function summarizeOutcomeEntries(entries = []) {
422
+ const byKey = {};
423
+
424
+ for (const entry of entries) {
425
+ if (!entry || !entry.key) continue;
426
+ const bucket = byKey[entry.key] || {
427
+ key: entry.key,
428
+ total: 0,
429
+ accepted: 0,
430
+ rejected: 0,
431
+ deferred: 0,
432
+ positive: 0,
433
+ neutral: 0,
434
+ negative: 0,
435
+ scoreDeltaTotal: 0,
436
+ scoreDeltaCount: 0,
437
+ latestAt: null,
438
+ };
439
+
440
+ bucket.total += 1;
441
+ if (bucket[entry.status] !== undefined) bucket[entry.status] += 1;
442
+ if (bucket[entry.effect] !== undefined) bucket[entry.effect] += 1;
443
+ if (Number.isFinite(entry.scoreDelta)) {
444
+ bucket.scoreDeltaTotal += entry.scoreDelta;
445
+ bucket.scoreDeltaCount += 1;
446
+ }
447
+ if (!bucket.latestAt || new Date(entry.createdAt) > new Date(bucket.latestAt)) {
448
+ bucket.latestAt = entry.createdAt;
449
+ }
450
+
451
+ byKey[entry.key] = bucket;
452
+ }
453
+
454
+ for (const bucket of Object.values(byKey)) {
455
+ bucket.avgScoreDelta = bucket.scoreDeltaCount > 0
456
+ ? Number((bucket.scoreDeltaTotal / bucket.scoreDeltaCount).toFixed(2))
457
+ : null;
458
+ bucket.evidenceClass = bucket.total > 0 ? 'measured' : 'estimated';
459
+ }
460
+
461
+ return {
462
+ totalEntries: entries.length,
463
+ byKey,
464
+ keys: Object.keys(byKey).sort(),
465
+ };
466
+ }
467
+
468
+ function getRecommendationOutcomeSummary(dir) {
469
+ return summarizeOutcomeEntries(readOutcomeIndex(dir));
470
+ }
471
+
472
+ function getRecommendationAdjustment(summaryByKey, key) {
473
+ const bucket = summaryByKey && summaryByKey[key];
474
+ if (!bucket) return 0;
475
+
476
+ let adjustment = 0;
477
+ adjustment += bucket.accepted * 2;
478
+ adjustment += bucket.positive * 3;
479
+ adjustment -= bucket.rejected * 3;
480
+ adjustment -= bucket.negative * 4;
481
+
482
+ if (Number.isFinite(bucket.avgScoreDelta)) {
483
+ if (bucket.avgScoreDelta > 0) adjustment += Math.min(4, Math.round(bucket.avgScoreDelta / 4));
484
+ if (bucket.avgScoreDelta < 0) adjustment -= Math.min(4, Math.round(Math.abs(bucket.avgScoreDelta) / 4));
485
+ }
486
+
487
+ if (adjustment > 8) return 8;
488
+ if (adjustment < -8) return -8;
489
+ return adjustment;
490
+ }
491
+
492
+ function formatRecommendationOutcomeSummary(dir) {
493
+ const summary = getRecommendationOutcomeSummary(dir);
494
+ if (summary.totalEntries === 0) {
495
+ return 'No recommendation outcomes recorded yet. Use `npx claudex-setup feedback --key permissionDeny --status accepted --effect positive` after a real run.';
496
+ }
497
+
498
+ const lines = [
499
+ 'Recommendation outcome summary:',
500
+ '',
501
+ ];
502
+
503
+ for (const key of summary.keys) {
504
+ const bucket = summary.byKey[key];
505
+ const avg = Number.isFinite(bucket.avgScoreDelta) ? ` | avg score delta ${bucket.avgScoreDelta >= 0 ? '+' : ''}${bucket.avgScoreDelta}` : '';
506
+ const adjustment = getRecommendationAdjustment(summary.byKey, key);
507
+ lines.push(` ${key}: total ${bucket.total} | accepted ${bucket.accepted} | rejected ${bucket.rejected} | deferred ${bucket.deferred} | positive ${bucket.positive} | negative ${bucket.negative}${avg} | ranking ${adjustment >= 0 ? '+' : ''}${adjustment}`);
508
+ }
509
+
510
+ return lines.join('\n');
511
+ }
512
+
325
513
  module.exports = {
326
514
  ensureArtifactDirs,
327
515
  writeActivityArtifact,
@@ -332,4 +520,10 @@ module.exports = {
332
520
  compareLatest,
333
521
  formatHistory,
334
522
  exportTrendReport,
523
+ readOutcomeIndex,
524
+ recordRecommendationOutcome,
525
+ summarizeOutcomeEntries,
526
+ getRecommendationOutcomeSummary,
527
+ getRecommendationAdjustment,
528
+ formatRecommendationOutcomeSummary,
335
529
  };
package/src/analyze.js CHANGED
@@ -244,12 +244,15 @@ function toGaps(results) {
244
244
  }
245
245
 
246
246
  function toRecommendations(auditResult) {
247
- const failed = auditResult.results
248
- .filter(r => r.passed === false)
249
- .sort((a, b) => {
250
- const order = { critical: 3, high: 2, medium: 1, low: 0 };
251
- return (order[b.impact] || 0) - (order[a.impact] || 0);
252
- });
247
+ const failed = auditResult.results.filter(r => r.passed === false);
248
+ const topActionOrder = new Map((auditResult.topNextActions || []).map((item, index) => [item.key, index]));
249
+ failed.sort((a, b) => {
250
+ const rankedA = topActionOrder.has(a.key) ? topActionOrder.get(a.key) : Number.MAX_SAFE_INTEGER;
251
+ const rankedB = topActionOrder.has(b.key) ? topActionOrder.get(b.key) : Number.MAX_SAFE_INTEGER;
252
+ if (rankedA !== rankedB) return rankedA - rankedB;
253
+ const order = { critical: 3, high: 2, medium: 1, low: 0 };
254
+ return (order[b.impact] || 0) - (order[a.impact] || 0);
255
+ });
253
256
 
254
257
  return failed.slice(0, 10).map((r, index) => ({
255
258
  priority: index + 1,
@@ -259,6 +262,8 @@ function toRecommendations(auditResult) {
259
262
  module: moduleFromCategory(r.category),
260
263
  risk: riskFromImpact(r.impact),
261
264
  why: r.fix,
265
+ evidenceClass: (auditResult.topNextActions || []).find(item => item.key === r.key)?.evidenceClass || 'estimated',
266
+ rankingAdjustment: (auditResult.topNextActions || []).find(item => item.key === r.key)?.rankingAdjustment || 0,
262
267
  }));
263
268
  }
264
269
 
package/src/audit.js CHANGED
@@ -6,6 +6,7 @@ const { TECHNIQUES, STACKS } = require('./techniques');
6
6
  const { ProjectContext } = require('./context');
7
7
  const { getBadgeMarkdown } = require('./badge');
8
8
  const { sendInsights, getLocalInsights } = require('./insights');
9
+ const { getRecommendationOutcomeSummary, getRecommendationAdjustment } = require('./activity');
9
10
 
10
11
  const COLORS = {
11
12
  reset: '\x1b[0m',
@@ -98,18 +99,35 @@ function getQuickWins(failed) {
98
99
  .slice(0, 3);
99
100
  }
100
101
 
101
- function buildTopNextActions(failed, limit = 5) {
102
+ function getRecommendationPriorityScore(item, outcomeSummaryByKey = {}) {
103
+ const impactScore = (IMPACT_ORDER[item.impact] ?? 0) * 100;
104
+ const feedbackAdjustment = getRecommendationAdjustment(outcomeSummaryByKey, item.key);
105
+ const brevityPenalty = Math.min((item.fix || '').length, 240) / 20;
106
+ return impactScore + (feedbackAdjustment * 10) - brevityPenalty;
107
+ }
108
+
109
+ function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}) {
102
110
  const pool = getPrioritizedFailed(failed);
103
111
 
104
112
  return [...pool]
105
113
  .sort((a, b) => {
106
- const impactA = IMPACT_ORDER[a.impact] ?? 0;
107
- const impactB = IMPACT_ORDER[b.impact] ?? 0;
108
- if (impactA !== impactB) return impactB - impactA;
109
- return (a.fix || '').length - (b.fix || '').length;
114
+ return getRecommendationPriorityScore(b, outcomeSummaryByKey) - getRecommendationPriorityScore(a, outcomeSummaryByKey);
110
115
  })
111
116
  .slice(0, limit)
112
- .map(({ key, name, impact, fix, category }) => ({
117
+ .map(({ key, name, impact, fix, category }) => {
118
+ const feedback = outcomeSummaryByKey[key] || null;
119
+ const rankingAdjustment = getRecommendationAdjustment(outcomeSummaryByKey, key);
120
+ const signals = [
121
+ `failed-check:${key}`,
122
+ `impact:${impact}`,
123
+ `category:${category}`,
124
+ ];
125
+ if (feedback) {
126
+ signals.push(`feedback:${feedback.total}`);
127
+ signals.push(`ranking-adjustment:${rankingAdjustment >= 0 ? '+' : ''}${rankingAdjustment}`);
128
+ }
129
+
130
+ return ({
113
131
  key,
114
132
  name,
115
133
  impact,
@@ -119,12 +137,20 @@ function buildTopNextActions(failed, limit = 5) {
119
137
  why: ACTION_RATIONALES[key] || fix,
120
138
  risk: riskFromImpact(impact),
121
139
  confidence: confidenceFromImpact(impact),
122
- signals: [
123
- `failed-check:${key}`,
124
- `impact:${impact}`,
125
- `category:${category}`,
126
- ],
127
- }));
140
+ signals,
141
+ evidenceClass: feedback ? 'measured' : 'estimated',
142
+ rankingAdjustment,
143
+ feedback: feedback ? {
144
+ total: feedback.total,
145
+ accepted: feedback.accepted,
146
+ rejected: feedback.rejected,
147
+ deferred: feedback.deferred,
148
+ positive: feedback.positive,
149
+ negative: feedback.negative,
150
+ avgScoreDelta: feedback.avgScoreDelta,
151
+ } : null,
152
+ });
153
+ });
128
154
  }
129
155
 
130
156
  function inferSuggestedNextCommand(result) {
@@ -194,6 +220,7 @@ async function audit(options) {
194
220
  const ctx = new ProjectContext(options.dir);
195
221
  const stacks = ctx.detectStacks(STACKS);
196
222
  const results = [];
223
+ const outcomeSummary = getRecommendationOutcomeSummary(options.dir);
197
224
 
198
225
  // Run all technique checks
199
226
  for (const [key, technique] of Object.entries(TECHNIQUES)) {
@@ -235,7 +262,7 @@ async function audit(options) {
235
262
  const organicEarned = organicPassed.reduce((sum, r) => sum + (weights[r.impact] || 5), 0);
236
263
  const organicScore = maxScore > 0 ? Math.round((organicEarned / maxScore) * 100) : 0;
237
264
  const quickWins = getQuickWins(failed);
238
- const topNextActions = buildTopNextActions(failed, 5);
265
+ const topNextActions = buildTopNextActions(failed, 5, outcomeSummary.byKey);
239
266
  const result = {
240
267
  score,
241
268
  organicScore,
@@ -248,6 +275,10 @@ async function audit(options) {
248
275
  results,
249
276
  quickWins: quickWins.map(({ key, name, impact, fix, category }) => ({ key, name, impact, category, fix })),
250
277
  topNextActions,
278
+ recommendationOutcomes: {
279
+ totalEntries: outcomeSummary.totalEntries,
280
+ keysTracked: outcomeSummary.keys,
281
+ },
251
282
  };
252
283
  result.suggestedNextCommand = inferSuggestedNextCommand(result);
253
284
  result.liteSummary = {
@@ -344,6 +375,10 @@ async function audit(options) {
344
375
  console.log(colorize(` Why: ${item.why}`, 'dim'));
345
376
  console.log(colorize(` Trace: ${item.signals.join(' | ')}`, 'dim'));
346
377
  console.log(colorize(` Risk: ${item.risk} | Confidence: ${item.confidence}`, 'dim'));
378
+ if (item.feedback) {
379
+ const avgDelta = Number.isFinite(item.feedback.avgScoreDelta) ? ` | Avg score delta: ${item.feedback.avgScoreDelta >= 0 ? '+' : ''}${item.feedback.avgScoreDelta}` : '';
380
+ console.log(colorize(` Feedback: accepted ${item.feedback.accepted}, rejected ${item.feedback.rejected}, positive ${item.feedback.positive}, negative ${item.feedback.negative}${avgDelta}`, 'dim'));
381
+ }
347
382
  console.log(colorize(` Fix: ${item.fix}`, 'dim'));
348
383
  }
349
384
  console.log('');
@@ -382,4 +417,4 @@ async function audit(options) {
382
417
  return result;
383
418
  }
384
419
 
385
- module.exports = { audit };
420
+ module.exports = { audit, buildTopNextActions };
package/src/context.js CHANGED
@@ -14,6 +14,7 @@ class ProjectContext {
14
14
  this.dir = dir;
15
15
  this.files = [];
16
16
  this._cache = {};
17
+ this._dependencyCache = null;
17
18
  this._scan();
18
19
  }
19
20
 
@@ -107,6 +108,52 @@ class ProjectContext {
107
108
  }
108
109
  }
109
110
 
111
+ projectDependencies() {
112
+ if (this._dependencyCache) return this._dependencyCache;
113
+
114
+ const deps = {};
115
+ const addDependency = (name, source) => {
116
+ if (!name) return;
117
+ const normalized = `${name}`.trim().toLowerCase().replace(/\[.*\]$/, '');
118
+ if (!normalized || normalized === 'python') return;
119
+ if (!deps[normalized]) {
120
+ deps[normalized] = source || true;
121
+ }
122
+ };
123
+
124
+ const pkg = this.jsonFile('package.json') || {};
125
+ for (const source of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
126
+ for (const name of Object.keys(pkg[source] || {})) {
127
+ addDependency(name, 'package.json');
128
+ }
129
+ }
130
+
131
+ const pyproject = this.fileContent('pyproject.toml') || '';
132
+ for (const name of extractPyprojectDependencies(pyproject)) {
133
+ addDependency(name, 'pyproject.toml');
134
+ }
135
+
136
+ const requirementFiles = [
137
+ 'requirements.txt',
138
+ 'requirements-dev.txt',
139
+ 'requirements-dev.in',
140
+ 'requirements-prod.txt',
141
+ 'requirements/base.txt',
142
+ 'requirements/dev.txt',
143
+ 'requirements/test.txt',
144
+ ];
145
+ for (const filePath of requirementFiles) {
146
+ const content = this.fileContent(filePath);
147
+ if (!content) continue;
148
+ for (const name of extractRequirementsDependencies(content)) {
149
+ addDependency(name, filePath);
150
+ }
151
+ }
152
+
153
+ this._dependencyCache = deps;
154
+ return deps;
155
+ }
156
+
110
157
  detectStacks(STACKS) {
111
158
  const detected = [];
112
159
  for (const [key, stack] of Object.entries(STACKS)) {
@@ -132,4 +179,63 @@ class ProjectContext {
132
179
  }
133
180
  }
134
181
 
182
+ function extractPyprojectDependencies(content) {
183
+ if (!content) return [];
184
+
185
+ const deps = new Set();
186
+ const add = (value) => {
187
+ if (!value) return;
188
+ deps.add(value.trim().toLowerCase().replace(/\[.*\]$/, ''));
189
+ };
190
+
191
+ const extractSection = (sectionName) => {
192
+ const escaped = sectionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
193
+ const pattern = new RegExp(`\\[${escaped}\\]([\\s\\S]*?)(?:\\n\\s*\\[|$)`);
194
+ const match = content.match(pattern);
195
+ return match ? match[1] : '';
196
+ };
197
+
198
+ const poetryDeps = extractSection('tool.poetry.dependencies');
199
+ for (const match of poetryDeps.matchAll(/^\s*([A-Za-z0-9_.-]+)\s*=/gm)) {
200
+ add(match[1]);
201
+ }
202
+
203
+ const projectDeps = extractSection('project');
204
+ const projectDepsArrayMatch = projectDeps.match(/dependencies\s*=\s*\[([\s\S]*?)\]/m);
205
+ if (projectDepsArrayMatch) {
206
+ for (const item of projectDepsArrayMatch[1].matchAll(/["']([^"']+)["']/g)) {
207
+ const name = item[1].split(/[<>=!~ ]/)[0];
208
+ add(name);
209
+ }
210
+ }
211
+
212
+ const optionalDepsSection = extractSection('project.optional-dependencies');
213
+ for (const item of optionalDepsSection.matchAll(/["']([^"']+)["']/g)) {
214
+ const name = item[1].split(/[<>=!~ ]/)[0];
215
+ add(name);
216
+ }
217
+
218
+ const dependencyGroupsSection = extractSection('dependency-groups');
219
+ for (const item of dependencyGroupsSection.matchAll(/["']([^"']+)["']/g)) {
220
+ const name = item[1].split(/[<>=!~ ]/)[0];
221
+ add(name);
222
+ }
223
+
224
+ return [...deps].filter(Boolean);
225
+ }
226
+
227
+ function extractRequirementsDependencies(content) {
228
+ if (!content) return [];
229
+
230
+ const deps = new Set();
231
+ for (const rawLine of content.split(/\r?\n/)) {
232
+ const line = rawLine.replace(/#.*$/, '').trim();
233
+ if (!line || line.startsWith('-')) continue;
234
+ const match = line.match(/^([A-Za-z0-9_.-]+)/);
235
+ if (!match) continue;
236
+ deps.add(match[1].toLowerCase().replace(/\[.*\]$/, ''));
237
+ }
238
+ return [...deps];
239
+ }
240
+
135
241
  module.exports = { ProjectContext };