@nerviq/cli 1.8.0 → 1.8.3

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/bin/cli.js CHANGED
@@ -6,8 +6,9 @@ const { analyzeProject, printAnalysis, exportMarkdown } = require('../src/analyz
6
6
  const { buildProposalBundle, printProposalBundle, writePlanFile, applyProposalBundle, printApplyResult } = require('../src/plans');
7
7
  const { getGovernanceSummary, printGovernanceSummary, ensureWritableProfile, renderGovernanceMarkdown } = require('../src/governance');
8
8
  const { runBenchmark, printBenchmark, writeBenchmarkReport } = require('../src/benchmark');
9
- const { writeSnapshotArtifact, recordRecommendationOutcome, formatRecommendationOutcomeSummary, getRecommendationOutcomeSummary } = require('../src/activity');
9
+ const { writeSnapshotArtifact, writeRollbackArtifact, recordRecommendationOutcome, formatRecommendationOutcomeSummary, getRecommendationOutcomeSummary } = require('../src/activity');
10
10
  const { collectFeedback } = require('../src/feedback');
11
+ const { recordPattern, getPriorityAdjustment, formatUsageSummary } = require('../src/usage-patterns');
11
12
  const { startServer } = require('../src/server');
12
13
  const { auditWorkspaces } = require('../src/workspace');
13
14
  const { scanOrg } = require('../src/org');
@@ -26,7 +27,7 @@ const COMMAND_ALIASES = {
26
27
  gov: 'governance',
27
28
  outcome: 'feedback',
28
29
  };
29
- const KNOWN_COMMANDS = ['audit', 'org', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'fix', 'rollback', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'check-health', 'dashboard', 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise', 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export', 'freshness', 'help', 'version'];
30
+ const KNOWN_COMMANDS = ['audit', 'org', 'setup', 'init', 'augment', 'suggest-only', 'plan', 'apply', 'fix', 'rollback', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'check-health', 'dashboard', 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise', 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export', 'freshness', 'help', 'version'];
30
31
 
31
32
  function levenshtein(a, b) {
32
33
  const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
@@ -328,8 +329,10 @@ const HELP = `
328
329
  FIX
329
330
  nerviq fix Show fixable checks and manual-fix guidance
330
331
  nerviq fix <key> Auto-fix a specific check (with score impact)
332
+ nerviq fix <key> --prompt Show AI agent prompt for a check (no auto-fix)
331
333
  nerviq fix --all-critical Fix all critical issues at once
332
334
  nerviq fix --dry-run Preview fixes without writing
335
+ nerviq fix --auto Apply fixes without confirmation prompt
333
336
  nerviq rollback Undo the most recent apply (delete created files)
334
337
  nerviq rollback --list Show available rollback points
335
338
  nerviq rollback --dry-run Preview what would be deleted
@@ -744,6 +747,17 @@ async function main() {
744
747
  });
745
748
  return; // keep process alive for http
746
749
  } else if (normalizedCommand === 'feedback') {
750
+ if (flags.includes('--patterns')) {
751
+ if (options.json) {
752
+ const { getUsageSummary } = require('../src/usage-patterns');
753
+ console.log(JSON.stringify(getUsageSummary(options.dir), null, 2));
754
+ } else {
755
+ console.log('');
756
+ console.log(formatUsageSummary(options.dir));
757
+ console.log('');
758
+ }
759
+ process.exit(0);
760
+ }
747
761
  if (parsed.feedbackKey) {
748
762
  if (!parsed.feedbackStatus) {
749
763
  console.error('\n Error: feedback logging requires --status when --key is provided.\n');
@@ -1283,9 +1297,11 @@ async function main() {
1283
1297
  console.log(output);
1284
1298
  process.exit(0);
1285
1299
  } else if (normalizedCommand === 'fix') {
1286
- // nerviq fix [key] [--all-critical] [--dry-run]
1300
+ // nerviq fix [key] [--all-critical] [--dry-run] [--auto] [--prompt]
1287
1301
  const fixKey = parsed.extraArgs[0] || null;
1288
1302
  const allCritical = flags.includes('--all-critical');
1303
+ const promptOnly = flags.includes('--prompt');
1304
+ const autoApply = options.auto || options.dryRun;
1289
1305
 
1290
1306
  // Step 1: Run silent audit to find failed checks (only actual failures, not skipped/null)
1291
1307
  const auditResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
@@ -1298,6 +1314,7 @@ async function main() {
1298
1314
 
1299
1315
  // Step 2: Determine which checks to fix
1300
1316
  const { TECHNIQUES } = require('../src/techniques');
1317
+ const { FIX_PROMPTS, formatFixPrompt } = require('../src/fix-prompts');
1301
1318
  const fs = require('fs');
1302
1319
  const pathMod = require('path');
1303
1320
 
@@ -1346,6 +1363,18 @@ async function main() {
1346
1363
  }
1347
1364
  process.exit(1);
1348
1365
  }
1366
+ // --prompt flag: show AI prompt and exit without attempting fix
1367
+ if (promptOnly) {
1368
+ const prompt = FIX_PROMPTS[fixKey];
1369
+ if (prompt) {
1370
+ console.log(formatFixPrompt(fixKey, prompt));
1371
+ } else {
1372
+ const failedCheck = failedResults.find(r => r.key === fixKey);
1373
+ console.log(`\n No AI prompt available for '${fixKey}'.`);
1374
+ console.log(` Manual fix: ${failedCheck ? failedCheck.fix : 'See nerviq audit --full.'}\n`);
1375
+ }
1376
+ process.exit(0);
1377
+ }
1349
1378
  targetKeys = [fixKey];
1350
1379
  } else if (allCritical) {
1351
1380
  targetKeys = failedResults.filter(r => r.impact === 'critical').map(r => r.key);
@@ -1369,15 +1398,30 @@ async function main() {
1369
1398
  console.log('');
1370
1399
  }
1371
1400
  if (nonFixable.length > 0) {
1372
- console.log(` Manual fix needed (${nonFixable.length}):`);
1373
- for (const r of nonFixable.slice(0, 5)) {
1374
- const tier = r.impact === 'critical' ? '🔴' : r.impact === 'high' ? '🟡' : '🔵';
1375
- console.log(` ${tier} ${r.key}: ${r.fix}`);
1401
+ const withPrompt = nonFixable.filter(r => FIX_PROMPTS[r.key]);
1402
+ const withoutPrompt = nonFixable.filter(r => !FIX_PROMPTS[r.key]);
1403
+ if (withPrompt.length > 0) {
1404
+ console.log(` AI prompt available (${withPrompt.length}):`);
1405
+ for (const r of withPrompt.slice(0, 5)) {
1406
+ const tier = r.impact === 'critical' ? '🔴' : r.impact === 'high' ? '🟡' : '🔵';
1407
+ console.log(` ${tier} nerviq fix ${r.key} --prompt`);
1408
+ }
1409
+ if (withPrompt.length > 5) {
1410
+ console.log(` ... and ${withPrompt.length - 5} more`);
1411
+ }
1412
+ console.log('');
1376
1413
  }
1377
- if (nonFixable.length > 5) {
1378
- console.log(` ... and ${nonFixable.length - 5} more (use --full to see all)`);
1414
+ if (withoutPrompt.length > 0) {
1415
+ console.log(` Manual fix needed (${withoutPrompt.length}):`);
1416
+ for (const r of withoutPrompt.slice(0, 5)) {
1417
+ const tier = r.impact === 'critical' ? '🔴' : r.impact === 'high' ? '🟡' : '🔵';
1418
+ console.log(` ${tier} ${r.key}: ${r.fix}`);
1419
+ }
1420
+ if (withoutPrompt.length > 5) {
1421
+ console.log(` ... and ${withoutPrompt.length - 5} more (use --full to see all)`);
1422
+ }
1423
+ console.log('');
1379
1424
  }
1380
- console.log('');
1381
1425
  }
1382
1426
  if (fixable.length > 0) {
1383
1427
  console.log(` Quick actions:`);
@@ -1388,55 +1432,241 @@ async function main() {
1388
1432
  process.exit(0);
1389
1433
  }
1390
1434
 
1391
- // Step 3: For each target, either use template, inline fix, or show manual instructions
1435
+ // Step 2.5: Predict impact and show preview before applying
1436
+ const IMPACT_WEIGHTS = { critical: 15, high: 10, medium: 5, low: 2 };
1392
1437
  const preScore = auditResult.score;
1438
+ const applicableResults = (auditResult.results || []).filter(r => r.passed !== null);
1439
+ const maxScore = applicableResults.reduce((sum, r) => sum + (IMPACT_WEIGHTS[r.impact] || 5), 0);
1440
+
1441
+ // Compute predicted score by simulating target fixes as passing
1442
+ const targetKeySet = new Set(targetKeys);
1443
+ const INLINE_FIX_KEYS = new Set(Object.keys(INLINE_FIXERS));
1444
+ const fixableTargets = targetKeys.filter(k => {
1445
+ const tech = TECHNIQUES[k];
1446
+ return (tech && tech.template) || INLINE_FIX_KEYS.has(k);
1447
+ });
1448
+ const fixableTargetSet = new Set(fixableTargets);
1449
+ const simulatedEarned = applicableResults.reduce((sum, r) => {
1450
+ const w = IMPACT_WEIGHTS[r.impact] || 5;
1451
+ if (r.passed) return sum + w;
1452
+ if (fixableTargetSet.has(r.key)) return sum + w;
1453
+ return sum;
1454
+ }, 0);
1455
+ const predictedScore = maxScore > 0 ? Math.round((simulatedEarned / maxScore) * 100) : 0;
1456
+ const predictedDelta = predictedScore - preScore;
1457
+
1458
+ if (!autoApply) {
1459
+ console.log('');
1460
+ if (allCritical && fixableTargets.length > 1) {
1461
+ // Multi-fix summary
1462
+ console.log(` ${fixableTargets.length} critical fixes available:`);
1463
+ let runningEarned = applicableResults.reduce((s, r) => s + (r.passed ? (IMPACT_WEIGHTS[r.impact] || 5) : 0), 0);
1464
+ let runningScore = maxScore > 0 ? Math.round((runningEarned / maxScore) * 100) : 0;
1465
+ fixableTargets.forEach((k, idx) => {
1466
+ const r = failedResults.find(fr => fr.key === k);
1467
+ const w = IMPACT_WEIGHTS[r.impact] || 5;
1468
+ const nextEarned = runningEarned + w;
1469
+ const nextScore = maxScore > 0 ? Math.round((nextEarned / maxScore) * 100) : 0;
1470
+ const d = nextScore - runningScore;
1471
+ console.log(` ${idx + 1}. ${(r.key).padEnd(18)} ${runningScore} → ${nextScore} (+${d})`);
1472
+ runningEarned = nextEarned;
1473
+ runningScore = nextScore;
1474
+ });
1475
+ console.log('');
1476
+ console.log(` Total: ${preScore} → ${predictedScore} (+${predictedDelta})`);
1477
+ } else {
1478
+ // Single fix preview
1479
+ const targetCheck = failedResults.find(r => r.key === fixableTargets[0]) || failedResults.find(r => r.key === targetKeys[0]);
1480
+ if (targetCheck) {
1481
+ console.log(` Predicted impact: ${preScore} → ${predictedScore} (+${predictedDelta})`);
1482
+ }
1483
+ }
1484
+
1485
+ // Prompt for confirmation
1486
+ const readline = require('readline');
1487
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1488
+ const answer = await new Promise(resolve => {
1489
+ rl.question(' Apply? (Y/n) ', resolve);
1490
+ });
1491
+ rl.close();
1492
+ if (answer && answer.trim().toLowerCase() === 'n') {
1493
+ for (const key of targetKeys) {
1494
+ recordPattern(options.dir, key, 'rejected');
1495
+ }
1496
+ console.log('\n Aborted.\n');
1497
+ process.exit(0);
1498
+ }
1499
+ }
1500
+
1501
+ // Step 3: Create rollback snapshot before applying fixes
1502
+ const isBatch = allCritical && targetKeys.length > 1;
1503
+ let rollbackId = null;
1504
+ const allCreatedFiles = [];
1505
+ const fixResults = []; // { key, name, status, delta }
1506
+
1507
+ if (!options.dryRun && targetKeys.length > 0) {
1508
+ // Snapshot existing files for rollback
1509
+ const snapshotFiles = {};
1510
+ for (const key of targetKeys) {
1511
+ const technique = TECHNIQUES[key];
1512
+ if (technique && technique.template && technique.template.path) {
1513
+ const tplPath = pathMod.join(options.dir, technique.template.path);
1514
+ if (fs.existsSync(tplPath)) {
1515
+ snapshotFiles[technique.template.path] = fs.readFileSync(tplPath, 'utf8');
1516
+ }
1517
+ }
1518
+ }
1519
+ const rollbackArtifact = writeRollbackArtifact(options.dir, {
1520
+ sourcePlan: 'fix-batch',
1521
+ preSnapshot: snapshotFiles,
1522
+ createdFiles: [],
1523
+ patchedFiles: Object.keys(snapshotFiles),
1524
+ rollbackInstructions: ['Use nerviq rollback to undo these fixes'],
1525
+ });
1526
+ rollbackId = rollbackArtifact.id;
1527
+ }
1528
+
1529
+ // Step 3b: Apply fixes sequentially with progress
1393
1530
  let fixed = 0;
1394
1531
  let manual = 0;
1532
+ let runningScore = preScore;
1395
1533
 
1396
- for (const key of targetKeys) {
1534
+ for (let i = 0; i < targetKeys.length; i++) {
1535
+ const key = targetKeys[i];
1397
1536
  const technique = TECHNIQUES[key];
1398
1537
  const failedCheck = failedResults.find(r => r.key === key);
1538
+ const progress = isBatch ? `${i + 1}/${targetKeys.length}: ` : '';
1399
1539
 
1400
1540
  if (technique && technique.template) {
1401
1541
  if (options.dryRun) {
1402
- console.log(`\n [dry-run] Would fix: ${failedCheck.name} (${key})`);
1542
+ console.log(` [dry-run] Would fix: ${progress}${failedCheck.name} (${key})`);
1543
+ fixResults.push({ key, name: failedCheck.name, status: 'dry-run', delta: 0 });
1403
1544
  fixed++;
1404
1545
  } else {
1405
- // Use setup with only this key
1406
- await setup({ ...options, only: [key], silent: true });
1407
- console.log(` ✅ Fixed: ${failedCheck.name}`);
1408
- fixed++;
1546
+ try {
1547
+ if (isBatch) console.log(` Fixing ${progress}${key}...`);
1548
+ const setupResult = await setup({ ...options, only: [key], silent: true });
1549
+ if (setupResult && setupResult.writtenFiles) {
1550
+ allCreatedFiles.push(...setupResult.writtenFiles);
1551
+ }
1552
+ const midResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
1553
+ const delta = midResult.score - runningScore;
1554
+ fixResults.push({ key, name: failedCheck.name, status: 'fixed', delta });
1555
+ runningScore = midResult.score;
1556
+ if (!isBatch) console.log(` ✅ Fixed: ${failedCheck.name}`);
1557
+ fixed++;
1558
+ } catch (err) {
1559
+ fixResults.push({ key, name: failedCheck.name, status: 'failed', delta: 0 });
1560
+ if (isBatch) {
1561
+ console.log(` ❌ Failed: ${key} — ${err.message}`);
1562
+ console.log(` Stopping batch. ${fixed} fixes applied so far.`);
1563
+ console.log(` Rollback: nerviq rollback --id ${rollbackId}`);
1564
+ break;
1565
+ } else {
1566
+ console.log(` ❌ Failed: ${failedCheck.name} — ${err.message}`);
1567
+ }
1568
+ }
1409
1569
  }
1410
1570
  } else if (INLINE_FIXERS[key]) {
1411
1571
  if (options.dryRun) {
1412
- console.log(` [dry-run] Would fix: ${failedCheck.name} (${key})`);
1572
+ console.log(` [dry-run] Would fix: ${progress}${failedCheck.name} (${key})`);
1573
+ fixResults.push({ key, name: failedCheck.name, status: 'dry-run', delta: 0 });
1413
1574
  fixed++;
1414
1575
  } else {
1415
- const didFix = INLINE_FIXERS[key](options.dir);
1416
- if (didFix) {
1417
- console.log(` ✅ Fixed: ${failedCheck.name}`);
1418
- fixed++;
1419
- } else {
1420
- console.log(` ⏭️ Already fixed: ${failedCheck.name}`);
1576
+ try {
1577
+ if (isBatch) console.log(` Fixing ${progress}${key}...`);
1578
+ const didFix = INLINE_FIXERS[key](options.dir);
1579
+ if (didFix) {
1580
+ const midResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
1581
+ const delta = midResult.score - runningScore;
1582
+ fixResults.push({ key, name: failedCheck.name, status: 'fixed', delta });
1583
+ runningScore = midResult.score;
1584
+ if (!isBatch) console.log(` ✅ Fixed: ${failedCheck.name}`);
1585
+ fixed++;
1586
+ } else {
1587
+ fixResults.push({ key, name: failedCheck.name, status: 'skipped', delta: 0 });
1588
+ if (!isBatch) console.log(` ⏭️ Already fixed: ${failedCheck.name}`);
1589
+ }
1590
+ } catch (err) {
1591
+ fixResults.push({ key, name: failedCheck.name, status: 'failed', delta: 0 });
1592
+ if (isBatch) {
1593
+ console.log(` ❌ Failed: ${key} — ${err.message}`);
1594
+ console.log(` Stopping batch. ${fixed} fixes applied so far.`);
1595
+ console.log(` Rollback: nerviq rollback --id ${rollbackId}`);
1596
+ break;
1597
+ }
1421
1598
  }
1422
1599
  }
1423
1600
  } else {
1424
- console.log(` 📋 ${failedCheck.name} (manual fix needed)`);
1425
- console.log(` ${failedCheck.fix}`);
1601
+ if (!isBatch) {
1602
+ const aiPrompt = FIX_PROMPTS[key];
1603
+ if (aiPrompt) {
1604
+ console.log(formatFixPrompt(key, aiPrompt));
1605
+ } else {
1606
+ console.log(` 📋 ${failedCheck.name} (manual fix needed)`);
1607
+ console.log(` ${failedCheck.fix}`);
1608
+ }
1609
+ }
1610
+ fixResults.push({ key, name: failedCheck.name, status: 'skipped', delta: 0 });
1426
1611
  manual++;
1427
1612
  }
1428
1613
  }
1429
1614
 
1430
- // Step 4: Show score impact
1431
- if (fixed > 0 && !options.dryRun) {
1615
+ // Record accepted patterns for successfully fixed checks
1616
+ if (!options.dryRun) {
1617
+ for (const key of targetKeys) {
1618
+ const fr = fixResults.find(r => r.key === key);
1619
+ recordPattern(options.dir, key, fr && fr.status === 'fixed' ? 'accepted' : 'rejected');
1620
+ }
1621
+ }
1622
+
1623
+ // Update rollback artifact with actual created files
1624
+ if (!options.dryRun && rollbackId && allCreatedFiles.length > 0) {
1625
+ const { ensureArtifactDirs } = require('../src/activity');
1626
+ const { rollbackDir } = ensureArtifactDirs(options.dir);
1627
+ const rbFiles = fs.readdirSync(rollbackDir).filter(f => f.includes(rollbackId));
1628
+ if (rbFiles.length > 0) {
1629
+ const rbPath = pathMod.join(rollbackDir, rbFiles[0]);
1630
+ try {
1631
+ const rbData = JSON.parse(fs.readFileSync(rbPath, 'utf8'));
1632
+ rbData.createdFiles = allCreatedFiles;
1633
+ fs.writeFileSync(rbPath, JSON.stringify(rbData, null, 2), 'utf8');
1634
+ } catch { /* best effort */ }
1635
+ }
1636
+ }
1637
+
1638
+ // Step 4: Show batch summary or simple score impact
1639
+ if (isBatch && fixResults.length > 0) {
1640
+ console.log('');
1641
+ console.log(' Batch fix complete:');
1642
+ for (let i = 0; i < fixResults.length; i++) {
1643
+ const r = fixResults[i];
1644
+ const icon = r.status === 'fixed' ? '✅' : r.status === 'failed' ? '❌' : '⚠ ';
1645
+ const deltaStr = r.status === 'fixed' ? ` (+${r.delta})` : r.status === 'skipped' ? ' (skipped — no auto-fix)' : r.status === 'failed' ? ' (failed)' : ' (dry-run)';
1646
+ console.log(` ${icon} ${i + 1}. ${r.key.padEnd(20)}${deltaStr}`);
1647
+ }
1648
+ const totalDelta = runningScore - preScore;
1649
+ console.log('');
1650
+ console.log(` Score: ${preScore} → ${runningScore} (${totalDelta >= 0 ? '+' : ''}${totalDelta})`);
1651
+ if (rollbackId && !options.dryRun) {
1652
+ console.log(` Rollback available: nerviq rollback --id ${rollbackId}`);
1653
+ }
1654
+ } else if (fixed > 0 && !options.dryRun) {
1432
1655
  const postResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
1433
1656
  const delta = postResult.score - preScore;
1434
1657
  console.log('');
1435
1658
  console.log(` Score: ${preScore} → ${postResult.score} (${delta >= 0 ? '+' : ''}${delta})`);
1659
+ if (rollbackId) {
1660
+ console.log(` Rollback available: nerviq rollback --id ${rollbackId}`);
1661
+ }
1436
1662
  }
1437
1663
 
1438
1664
  console.log(`\n ${fixed} fixed, ${manual} need manual action.\n`);
1439
1665
 
1666
+ } else if (normalizedCommand === 'init') {
1667
+ const { runInit } = require('../src/init');
1668
+ await runInit(options.dir, flags);
1669
+ process.exit(0);
1440
1670
  } else if (normalizedCommand === 'setup') {
1441
1671
  await setup(options);
1442
1672
  if (options.snapshot) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "1.8.0",
3
+ "version": "1.8.3",
4
4
  "description": "The intelligent nervous system for AI coding agents — 2,431 checks across 8 platforms, 10 languages, and 62 domain packs. Audit, align, and amplify.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/activity.js CHANGED
@@ -557,12 +557,13 @@ function loadSnapshotPayload(dir, indexEntry) {
557
557
  * Analyze check health by comparing the two most recent audit snapshots.
558
558
  * Detects checks that regressed (passed → failed), improved (failed → passed),
559
559
  * and flags sudden drops that may indicate platform format changes.
560
+ * When more than 2 snapshots exist, also computes per-check pass rates.
560
561
  *
561
562
  * @param {string} dir - Project root directory.
562
563
  * @returns {Object|null} Health report, or null if fewer than 2 audit snapshots exist.
563
564
  */
564
565
  function checkHealth(dir) {
565
- const history = getHistory(dir, 2);
566
+ const history = getHistory(dir, 20);
566
567
  if (history.length < 2) return null;
567
568
 
568
569
  const currentPayload = loadSnapshotPayload(dir, history[0]);
@@ -627,15 +628,20 @@ function checkHealth(dir) {
627
628
  }
628
629
  }
629
630
 
631
+ // Per-check pass rates across all snapshots
632
+ const passRates = computePassRates(dir, history);
633
+
630
634
  return {
631
635
  currentDate: history[0].createdAt,
632
636
  previousDate: history[1].createdAt,
637
+ snapshotsAnalyzed: history.length,
633
638
  scoreDelta: (currentPayload.score || 0) - (previousPayload.score || 0),
634
639
  regressions,
635
640
  improvements,
636
641
  newChecks,
637
642
  removedChecks,
638
643
  platformAlerts,
644
+ passRates,
639
645
  summary: {
640
646
  regressionsCount: regressions.length,
641
647
  improvementsCount: improvements.length,
@@ -646,6 +652,66 @@ function checkHealth(dir) {
646
652
  };
647
653
  }
648
654
 
655
+ /**
656
+ * Compute per-check pass rates across all snapshots.
657
+ * Returns { declining, consistentlyFailing, consistentlyPassing, overallHealth }.
658
+ */
659
+ function computePassRates(dir, history) {
660
+ // key → { passes, total, recentResults: [bool...] (newest first) }
661
+ const stats = {};
662
+ for (const entry of history) {
663
+ const payload = loadSnapshotPayload(dir, entry);
664
+ if (!payload || !payload.results) continue;
665
+ for (const r of payload.results) {
666
+ if (!r.key || r.passed === null || r.passed === undefined) continue;
667
+ if (!stats[r.key]) stats[r.key] = { name: r.name, passes: 0, total: 0, recentResults: [] };
668
+ stats[r.key].total++;
669
+ if (r.passed) stats[r.key].passes++;
670
+ stats[r.key].recentResults.push(!!r.passed);
671
+ }
672
+ }
673
+
674
+ const declining = [];
675
+ const consistentlyFailing = [];
676
+ let consistentlyPassingCount = 0;
677
+ let totalChecks = 0;
678
+ let totalPasses = 0;
679
+ let totalAppearances = 0;
680
+
681
+ for (const [key, s] of Object.entries(stats)) {
682
+ const rate = s.total > 0 ? s.passes / s.total : 0;
683
+ totalChecks++;
684
+ totalPasses += s.passes;
685
+ totalAppearances += s.total;
686
+
687
+ if (s.total >= 2 && rate === 0) {
688
+ consistentlyFailing.push({ key, name: s.name, runs: s.total });
689
+ } else if (rate === 1) {
690
+ consistentlyPassingCount++;
691
+ } else if (s.total >= 2) {
692
+ // Check if declining: earlier results passed, recent ones failed
693
+ const half = Math.ceil(s.recentResults.length / 2);
694
+ const recentHalf = s.recentResults.slice(0, half);
695
+ const olderHalf = s.recentResults.slice(half);
696
+ const recentRate = recentHalf.filter(Boolean).length / recentHalf.length;
697
+ const olderRate = olderHalf.length > 0 ? olderHalf.filter(Boolean).length / olderHalf.length : recentRate;
698
+ if (olderRate > recentRate) {
699
+ const failStreak = s.recentResults.findIndex(v => v === true);
700
+ declining.push({
701
+ key, name: s.name,
702
+ oldRate: Math.round(olderRate * 100),
703
+ newRate: Math.round(recentRate * 100),
704
+ failingRuns: failStreak === -1 ? s.recentResults.length : failStreak,
705
+ });
706
+ }
707
+ }
708
+ }
709
+
710
+ const overallHealth = totalAppearances > 0 ? Math.round((totalPasses / totalAppearances) * 100) : 100;
711
+
712
+ return { declining, consistentlyFailing, consistentlyPassingCount, overallHealth };
713
+ }
714
+
649
715
  /**
650
716
  * Format check-health report for CLI display.
651
717
  */
@@ -653,11 +719,12 @@ function formatCheckHealth(healthReport) {
653
719
  if (!healthReport) return 'Need at least 2 audit snapshots. Run `nerviq audit --snapshot` twice.';
654
720
 
655
721
  const lines = [];
656
- const { scoreDelta, regressions, improvements, platformAlerts, newChecks, summary } = healthReport;
722
+ const { scoreDelta, regressions, improvements, platformAlerts, newChecks, passRates } = healthReport;
657
723
  const sign = scoreDelta >= 0 ? '+' : '';
658
724
 
659
725
  lines.push(` Check Health Report`);
660
- lines.push(` ─────────────────────────────────────`);
726
+ lines.push(` ═══════════════════════════════════════`);
727
+ lines.push(` Snapshots analyzed: ${healthReport.snapshotsAnalyzed}`);
661
728
  lines.push(` Period: ${healthReport.previousDate?.split('T')[0]} → ${healthReport.currentDate?.split('T')[0]}`);
662
729
  lines.push(` Score delta: ${sign}${scoreDelta}`);
663
730
  lines.push('');
@@ -671,6 +738,23 @@ function formatCheckHealth(healthReport) {
671
738
  lines.push('');
672
739
  }
673
740
 
741
+ if (passRates && passRates.declining.length > 0) {
742
+ lines.push(` Checks with declining pass rate:`);
743
+ for (const d of passRates.declining) {
744
+ const detail = d.failingRuns > 0 ? `(failing in last ${d.failingRuns} runs)` : '';
745
+ lines.push(` ⚠ ${d.key.padEnd(22)} ${d.oldRate}% → ${d.newRate}% ${detail}`);
746
+ }
747
+ lines.push('');
748
+ }
749
+
750
+ if (passRates && passRates.consistentlyFailing.length > 0) {
751
+ lines.push(` Consistently failing (0% pass rate):`);
752
+ for (const f of passRates.consistentlyFailing) {
753
+ lines.push(` ✗ ${f.key.padEnd(22)} 0/${f.runs} runs`);
754
+ }
755
+ lines.push('');
756
+ }
757
+
674
758
  if (regressions.length > 0) {
675
759
  lines.push(` 🔴 Regressions (${regressions.length} checks now failing)`);
676
760
  for (const r of regressions) {
@@ -692,11 +776,21 @@ function formatCheckHealth(healthReport) {
692
776
  lines.push('');
693
777
  }
694
778
 
779
+ if (passRates && passRates.consistentlyPassingCount > 0) {
780
+ lines.push(` Consistently passing (100%):`);
781
+ lines.push(` ✓ ${passRates.consistentlyPassingCount} checks at 100% pass rate`);
782
+ lines.push('');
783
+ }
784
+
695
785
  if (regressions.length === 0 && platformAlerts.length === 0) {
696
786
  lines.push(` ✅ All checks stable. No regressions detected.`);
697
787
  lines.push('');
698
788
  }
699
789
 
790
+ if (passRates) {
791
+ lines.push(` Overall health: ${passRates.overallHealth}%`);
792
+ }
793
+
700
794
  return lines.join('\n');
701
795
  }
702
796
 
package/src/audit.js CHANGED
@@ -953,9 +953,13 @@ function printLiteAudit(result, dir) {
953
953
 
954
954
  console.log(colorize(' Top 3 things to fix right now:', 'magenta'));
955
955
  console.log('');
956
+ let usagePatterns;
957
+ try { usagePatterns = require('./usage-patterns'); } catch { usagePatterns = null; }
956
958
  result.liteSummary.topNextActions.forEach((item, index) => {
957
959
  const tier = item.impact === 'critical' ? '🔴' : item.impact === 'high' ? '🟡' : '🔵';
958
- console.log(` ${index + 1}. ${tier} ${colorize(item.name, 'bold')}`);
960
+ const suppressed = usagePatterns && usagePatterns.getPriorityAdjustment(dir, item.key) === 'suppress';
961
+ const suffix = suppressed ? colorize(' (suppressed)', 'dim') : '';
962
+ console.log(` ${index + 1}. ${tier} ${colorize(item.name, 'bold')}${suffix}`);
959
963
  console.log(colorize(` ${item.fix}`, 'dim'));
960
964
  });
961
965
  console.log('');
@@ -0,0 +1,122 @@
1
+ /**
2
+ * AI-generated fix prompts for checks without template auto-fixes.
3
+ * Each key maps to a check key from techniques.js.
4
+ * These prompts are designed to be copy-pasted into an AI coding agent.
5
+ */
6
+
7
+ const FIX_PROMPTS = {
8
+ importSyntax:
9
+ 'Refactor CLAUDE.md to use @path imports for modularity. Split large sections into separate files (e.g. @docs/coding-style.md, @docs/architecture.md) and reference them with @path syntax. Also consider using .claude/rules/ for path-specific rules.',
10
+
11
+ underlines200:
12
+ 'Refactor CLAUDE.md to be under 200 lines. Move detailed sections into separate files using @import or .claude/rules/ for path-specific rules. Keep only essential project overview, build commands, and key conventions in the main file.',
13
+
14
+ verificationLoop:
15
+ 'Add a verification section to CLAUDE.md with commands Claude should run after making changes. Include test, lint, and build commands. Example:\n\n## Verification\nAfter every change, run:\n- `npm test` to verify tests pass\n- `npm run lint` to check code style\n- `npm run build` to verify compilation',
16
+
17
+ testCommand:
18
+ 'Add an explicit test command to CLAUDE.md. Example: "Run `npm test` before committing." or "Run `pytest` to verify changes." Place it in a ## Commands or ## Verification section.',
19
+
20
+ lintCommand:
21
+ 'Add a lint command to CLAUDE.md so the AI agent auto-checks code style. Example: "Run `npm run lint` or `eslint .` before committing." Place it in a ## Commands section.',
22
+
23
+ buildCommand:
24
+ 'Add a build command to CLAUDE.md so the AI agent can verify compilation. Example: "Run `npm run build` or `tsc` to verify the project compiles." Place it in a ## Commands section.',
25
+
26
+ settingsPermissions:
27
+ 'Create or update .claude/settings.json with permission configuration. Add "permissions": { "allow": ["Read", "Write src/**"], "deny": ["Write .env", "Write **/secrets/**"] } to control which tools and paths the AI agent can access.',
28
+
29
+ permissionDeny:
30
+ 'Add deny rules to .claude/settings.json under permissions.deny to block dangerous operations. Example entries: "rm -rf /", "DROP TABLE", "Write .env", "Write **/*.pem", "Write **/secrets/**".',
31
+
32
+ noBypassPermissions:
33
+ 'Remove bypassPermissions from your .claude/settings.json defaultMode. Instead, use explicit allow rules in permissions.allow to grant only the access needed.',
34
+
35
+ secretsProtection:
36
+ 'Add permissions.deny rules in .claude/settings.json to block reading sensitive files. Add entries like: ".env", ".env.*", "**/.env", "**/*.pem", "**/secrets/**" to the deny array.',
37
+
38
+ securityReview:
39
+ 'Add a /security-review command or mention security review in CLAUDE.md. Create .claude/commands/security-review.md with: "Review the codebase for OWASP Top 10 vulnerabilities. Check for: SQL injection, XSS, CSRF, insecure dependencies, hardcoded secrets, and misconfigured permissions."',
40
+
41
+ preToolUseHook:
42
+ 'Add a PreToolUse hook in .claude/settings.json to validate tool calls before execution. Example: add a hook that blocks writes to protected files or validates file paths. See hooks documentation for the event schema.',
43
+
44
+ postToolUseHook:
45
+ 'Add a PostToolUse hook in .claude/settings.json for automated actions after tool calls. Example: auto-run linting after file writes, or validate output format after code generation.',
46
+
47
+ sessionStartHook:
48
+ 'Add a SessionStart hook in .claude/settings.json for initialization tasks. Example: load project state, rotate logs, or display a welcome message with project status at the start of each session.',
49
+
50
+ deployCommand:
51
+ 'Create .claude/commands/deploy.md with deployment instructions. Include: pre-deploy checks (tests, lint, build), deployment steps for your platform (Vercel, AWS, etc.), and post-deploy verification.',
52
+
53
+ reviewCommand:
54
+ 'Create .claude/commands/review.md with code review instructions. Include: check for security issues, verify test coverage, review naming conventions, check for code duplication, and validate error handling.',
55
+
56
+ compactionAwareness:
57
+ 'Add compaction guidance to CLAUDE.md. Add a line like: "Run /compact when context gets heavy or before large operations." This helps the AI agent manage its context window effectively.',
58
+
59
+ contextManagement:
60
+ 'Add context management tips to CLAUDE.md. Include: "Use /compact proactively at 70% capacity. Prefer targeted file reads over broad searches. Keep conversation focused on one task at a time."',
61
+
62
+ mcpServers:
63
+ 'Create .mcp.json at the project root to configure MCP servers. Example:\n{\n "mcpServers": {\n "memory": { "command": "npx", "args": ["-y", "@anthropic/mcp-memory"] }\n }\n}\nUse `claude mcp add <name>` to add servers interactively.',
64
+
65
+ context7Mcp:
66
+ 'Add the Context7 MCP server for real-time documentation lookup. Add to .mcp.json:\n"context7": { "command": "npx", "args": ["-y", "@anthropic/context7-mcp"] }\nThis provides always-up-to-date library documentation.',
67
+
68
+ xmlTags:
69
+ 'Add XML-tagged sections to CLAUDE.md for structured rules. Wrap critical rules in tags like <constraints>, <validation>, or <rules>. Example:\n<constraints>\n- Never modify package-lock.json manually\n- Always run tests before committing\n</constraints>',
70
+
71
+ fewShotExamples:
72
+ 'Add code examples to CLAUDE.md showing preferred patterns. Include 2-3 examples of your coding style: naming conventions, error handling patterns, file structure. Use fenced code blocks with the appropriate language tag.',
73
+
74
+ roleDefinition:
75
+ 'Add a role definition to the top of CLAUDE.md. Example: "You are a senior backend engineer working on a Node.js microservices platform. Prioritize type safety, comprehensive error handling, and test coverage."',
76
+
77
+ constraintBlocks:
78
+ 'Add XML constraint blocks to CLAUDE.md for critical rules. Wrap must-follow rules in <constraints> tags for ~40% better adherence. Example:\n<constraints>\n- Never delete database migrations\n- Always use parameterized queries\n- Run the full test suite before committing\n</constraints>',
79
+
80
+ readme:
81
+ 'Create a README.md with: project name and description, installation/setup instructions, usage examples, configuration options, and contribution guidelines.',
82
+
83
+ changelog:
84
+ 'Create a CHANGELOG.md following Keep a Changelog format. Include sections: Added, Changed, Deprecated, Removed, Fixed, Security. Start with your current version.',
85
+
86
+ contributing:
87
+ 'Create a CONTRIBUTING.md with: how to set up the dev environment, coding standards and style guide, pull request process, issue reporting guidelines, and code of conduct reference.',
88
+
89
+ editorconfig:
90
+ 'Create a .editorconfig file at the project root with consistent formatting rules:\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true',
91
+
92
+ ciPipeline:
93
+ 'Add a CI pipeline for automated testing. For GitHub Actions, create .github/workflows/ci.yml with steps: checkout, setup Node/Python, install dependencies, run lint, run tests, run build.',
94
+
95
+ dockerfile:
96
+ 'Create a Dockerfile for the project. Use a multi-stage build: stage 1 installs dependencies and builds, stage 2 copies only production artifacts. Use a slim base image and set a non-root user.',
97
+
98
+ noSecretsInClaude:
99
+ 'Remove any API keys, tokens, or secrets from CLAUDE.md. Replace them with environment variable references (e.g. $API_KEY or process.env.API_KEY). Store actual values in .env files that are gitignored.',
100
+ };
101
+
102
+ /**
103
+ * Format a fix prompt for display in the terminal.
104
+ */
105
+ function formatFixPrompt(key, prompt) {
106
+ const divider = '\u2500'.repeat(38);
107
+ const lines = [
108
+ '',
109
+ ` No auto-fix for '${key}'. Here's a prompt for your AI agent:`,
110
+ '',
111
+ ` ${divider}`,
112
+ ];
113
+ for (const line of prompt.split('\n')) {
114
+ lines.push(` ${line}`);
115
+ }
116
+ lines.push(` ${divider}`);
117
+ lines.push('');
118
+ lines.push(' Copy and paste this into Claude Code, Cursor, or your preferred AI agent.');
119
+ return lines.join('\n');
120
+ }
121
+
122
+ module.exports = { FIX_PROMPTS, formatFixPrompt };
package/src/init.js ADDED
@@ -0,0 +1,184 @@
1
+ 'use strict';
2
+
3
+ const readline = require('readline');
4
+ const { detectPlatforms } = require('./public-api');
5
+ const { audit } = require('./audit');
6
+ const { setup } = require('./setup');
7
+ const { ProjectContext } = require('./context');
8
+ const { STACKS } = require('./techniques');
9
+
10
+ const PLATFORM_LABELS = {
11
+ claude: 'Claude Code',
12
+ codex: 'Codex',
13
+ gemini: 'Gemini CLI',
14
+ copilot: 'GitHub Copilot',
15
+ cursor: 'Cursor',
16
+ windsurf: 'Windsurf',
17
+ aider: 'Aider',
18
+ opencode: 'OpenCode',
19
+ };
20
+
21
+ const ALL_PLATFORMS = Object.keys(PLATFORM_LABELS);
22
+
23
+ const TEAM_SIZES = ['solo', 'small', 'team', 'enterprise'];
24
+ const TEAM_LABELS = {
25
+ solo: 'Solo developer',
26
+ small: 'Small team (2-5)',
27
+ team: 'Team (6-20)',
28
+ enterprise: 'Enterprise (20+)',
29
+ };
30
+
31
+ function prompt(rl, question) {
32
+ return new Promise((resolve) => {
33
+ rl.question(question, (answer) => resolve(answer.trim()));
34
+ });
35
+ }
36
+
37
+ function detectStacks(dir) {
38
+ const ctx = new ProjectContext(dir);
39
+ return ctx.detectStacks(STACKS);
40
+ }
41
+
42
+ function labelPlatforms(platforms) {
43
+ return platforms.map((p) => PLATFORM_LABELS[p] || p).join(', ');
44
+ }
45
+
46
+ function parsePlatforms(input) {
47
+ return input
48
+ .split(/[,\s]+/)
49
+ .map((s) => s.trim().toLowerCase())
50
+ .filter((s) => ALL_PLATFORMS.includes(s));
51
+ }
52
+
53
+ async function runInit(dir, flags) {
54
+ const rl = readline.createInterface({
55
+ input: process.stdin,
56
+ output: process.stdout,
57
+ });
58
+
59
+ const dim = '\x1b[2m';
60
+ const bold = '\x1b[1m';
61
+ const cyan = '\x1b[36m';
62
+ const green = '\x1b[32m';
63
+ const reset = '\x1b[0m';
64
+
65
+ console.log('');
66
+ console.log(`${bold} Welcome to Nerviq${reset} — let's set up your AI coding agent governance.`);
67
+ console.log('');
68
+
69
+ // --- Question 1: Platforms ---
70
+ const detected = detectPlatforms(dir);
71
+ const detectedLabel = detected.length > 0
72
+ ? `${dim}[auto-detected: ${labelPlatforms(detected)}]${reset}`
73
+ : `${dim}[no platforms detected]${reset}`;
74
+ console.log(` ${bold}1.${reset} Which platform(s) do you use?`);
75
+ console.log(` ${detectedLabel}`);
76
+ const platformInput = await prompt(
77
+ rl,
78
+ ` ${dim}> Press Enter to confirm, or type: ${ALL_PLATFORMS.join(',')}${reset}\n > `,
79
+ );
80
+ const platforms = platformInput === ''
81
+ ? (detected.length > 0 ? detected : ['claude'])
82
+ : parsePlatforms(platformInput);
83
+ if (platforms.length === 0) platforms.push('claude');
84
+
85
+ console.log('');
86
+
87
+ // --- Question 2: Stack ---
88
+ const stacks = detectStacks(dir);
89
+ const stackLabels = stacks.map((s) => s.label);
90
+ const stackDetectedLabel = stackLabels.length > 0
91
+ ? `${dim}[auto-detected: ${stackLabels.join(', ')}]${reset}`
92
+ : `${dim}[no stack detected]${reset}`;
93
+ console.log(` ${bold}2.${reset} What's your primary stack?`);
94
+ console.log(` ${stackDetectedLabel}`);
95
+ const stackInput = await prompt(
96
+ rl,
97
+ ` ${dim}> Press Enter to confirm, or type your stack${reset}\n > `,
98
+ );
99
+ const stackDisplay = stackInput === ''
100
+ ? (stackLabels.length > 0 ? stackLabels.join(', ') : 'General')
101
+ : stackInput;
102
+
103
+ console.log('');
104
+
105
+ // --- Question 3: Team size ---
106
+ console.log(` ${bold}3.${reset} What's your team size?`);
107
+ const teamInput = await prompt(
108
+ rl,
109
+ ` ${dim}> solo / small (2-5) / team (6-20) / enterprise (20+)${reset}\n > `,
110
+ );
111
+ const teamKey = TEAM_SIZES.find((t) => teamInput.toLowerCase().startsWith(t)) || 'solo';
112
+ const teamLabel = TEAM_LABELS[teamKey];
113
+
114
+ rl.close();
115
+
116
+ console.log('');
117
+ console.log(` ${cyan}Setting up for: ${labelPlatforms(platforms)} | ${stackDisplay} | ${teamLabel}${reset}`);
118
+ console.log('');
119
+
120
+ // --- Run audit (before) ---
121
+ const primaryPlatform = platforms[0];
122
+ console.log(` ${dim}Running audit...${reset}`);
123
+ const preResult = await audit({ dir, silent: true, platform: primaryPlatform });
124
+ const preScore = preResult.score;
125
+ console.log(` Score: ${bold}${preScore}/100${reset}`);
126
+ console.log('');
127
+
128
+ // --- Run setup ---
129
+ console.log(` ${dim}Running setup...${reset}`);
130
+ const setupResult = await setup({
131
+ dir,
132
+ platform: primaryPlatform,
133
+ silent: true,
134
+ profile: 'safe-write',
135
+ mcpPacks: [],
136
+ });
137
+
138
+ for (const f of setupResult.writtenFiles) {
139
+ console.log(` ${green}✅${reset} Created ${f}`);
140
+ }
141
+ for (const f of setupResult.preservedFiles) {
142
+ console.log(` ${dim}⏭️ Kept ${f} (already exists)${reset}`);
143
+ }
144
+
145
+ // --- Run additional platform setups ---
146
+ for (const plat of platforms.slice(1)) {
147
+ try {
148
+ const extraResult = await setup({
149
+ dir,
150
+ platform: plat,
151
+ silent: true,
152
+ profile: 'safe-write',
153
+ mcpPacks: [],
154
+ });
155
+ for (const f of extraResult.writtenFiles) {
156
+ console.log(` ${green}✅${reset} Created ${f}`);
157
+ }
158
+ } catch {
159
+ // Platform setup not available, skip
160
+ }
161
+ }
162
+
163
+ // --- Run audit (after) ---
164
+ const postResult = await audit({ dir, silent: true, platform: primaryPlatform });
165
+ const postScore = postResult.score;
166
+ const delta = postScore - preScore;
167
+
168
+ if (delta > 0) {
169
+ console.log(` Score: ${bold}${postScore}/100${reset} (${green}+${delta}${reset})`);
170
+ } else {
171
+ console.log(` Score: ${bold}${postScore}/100${reset}`);
172
+ }
173
+
174
+ console.log('');
175
+ console.log(` ${bold}Next steps:${reset}`);
176
+ console.log(` - Review: ${cyan}nerviq audit --full${reset}`);
177
+ if (platforms.length > 1) {
178
+ console.log(` - Cross-platform: ${cyan}nerviq harmony-audit${reset}`);
179
+ }
180
+ console.log(` - Customize: ${cyan}nerviq augment${reset}`);
181
+ console.log('');
182
+ }
183
+
184
+ module.exports = { runInit };
@@ -0,0 +1,99 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { ensureProjectStateDir, resolveProjectStateReadPath } = require('./state-paths');
4
+
5
+ const PATTERNS_FILE = 'patterns.json';
6
+ const SUPPRESS_THRESHOLD = 3;
7
+
8
+ function patternsPath(dir, writable) {
9
+ if (writable) {
10
+ const feedbackDir = ensureProjectStateDir(dir, 'feedback');
11
+ return path.join(feedbackDir, PATTERNS_FILE);
12
+ }
13
+ const feedbackDir = resolveProjectStateReadPath(dir, 'feedback');
14
+ return path.join(feedbackDir, PATTERNS_FILE);
15
+ }
16
+
17
+ function loadPatterns(dir) {
18
+ const filePath = patternsPath(dir, false);
19
+ if (!fs.existsSync(filePath)) return {};
20
+ try {
21
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+
27
+ function savePatterns(dir, patterns) {
28
+ const filePath = patternsPath(dir, true);
29
+ fs.writeFileSync(filePath, JSON.stringify(patterns, null, 2), 'utf8');
30
+ }
31
+
32
+ function recordPattern(dir, checkKey, action) {
33
+ if (!['accepted', 'rejected', 'skipped'].includes(action)) return;
34
+ const patterns = loadPatterns(dir);
35
+ if (!patterns[checkKey]) {
36
+ patterns[checkKey] = { accepted: 0, rejected: 0, skipped: 0, lastAction: null, lastDate: null };
37
+ }
38
+ patterns[checkKey][action] += 1;
39
+ patterns[checkKey].lastAction = action;
40
+ patterns[checkKey].lastDate = new Date().toISOString();
41
+ savePatterns(dir, patterns);
42
+ }
43
+
44
+ function getPriorityAdjustment(dir, checkKey) {
45
+ const patterns = loadPatterns(dir);
46
+ const entry = patterns[checkKey];
47
+ if (!entry) return null;
48
+ const total = entry.accepted + entry.rejected;
49
+ if (total < 2) return null;
50
+ if (entry.accepted > 0 && entry.rejected === 0) return 'boost';
51
+ if (entry.rejected >= SUPPRESS_THRESHOLD && entry.accepted === 0) return 'suppress';
52
+ return null;
53
+ }
54
+
55
+ function getUsageSummary(dir) {
56
+ const patterns = loadPatterns(dir);
57
+ const keys = Object.keys(patterns);
58
+ const totalEvents = keys.reduce((sum, k) => {
59
+ const e = patterns[k];
60
+ return sum + e.accepted + e.rejected + e.skipped;
61
+ }, 0);
62
+
63
+ const withRates = keys
64
+ .filter(k => (patterns[k].accepted + patterns[k].rejected) > 0)
65
+ .map(k => {
66
+ const e = patterns[k];
67
+ const total = e.accepted + e.rejected;
68
+ return { key: k, accepted: e.accepted, rejected: e.rejected, total, rate: total > 0 ? e.accepted / total : 0 };
69
+ });
70
+
71
+ withRates.sort((a, b) => b.rate - a.rate || b.total - a.total);
72
+ const topAccepted = withRates.filter(e => e.rate > 0).slice(0, 5);
73
+ const topRejected = withRates.filter(e => e.rate < 1).sort((a, b) => a.rate - b.rate || b.total - a.total).slice(0, 5);
74
+
75
+ return { totalEvents, topAccepted, topRejected, patterns };
76
+ }
77
+
78
+ function formatUsageSummary(dir) {
79
+ const summary = getUsageSummary(dir);
80
+ if (summary.totalEvents === 0) return ' No usage patterns recorded yet.\n Patterns are tracked when you run nerviq fix.';
81
+
82
+ const lines = [` Usage Patterns (${summary.totalEvents} events recorded):`];
83
+ if (summary.topAccepted.length > 0) {
84
+ lines.push('', ' Most accepted:');
85
+ summary.topAccepted.forEach((e, i) => {
86
+ lines.push(` ${i + 1}. ${e.key.padEnd(20)} ${e.accepted}/${e.total} (${Math.round(e.rate * 100)}%)`);
87
+ });
88
+ }
89
+ if (summary.topRejected.length > 0) {
90
+ lines.push('', ' Most rejected:');
91
+ summary.topRejected.forEach((e, i) => {
92
+ const hint = e.rate === 0 && e.total >= SUPPRESS_THRESHOLD ? ' -- consider suppressing' : '';
93
+ lines.push(` ${i + 1}. ${e.key.padEnd(20)} ${e.accepted}/${e.total} (${Math.round(e.rate * 100)}%)${hint}`);
94
+ });
95
+ }
96
+ return lines.join('\n');
97
+ }
98
+
99
+ module.exports = { loadPatterns, recordPattern, getPriorityAdjustment, getUsageSummary, formatUsageSummary };