@nerviq/cli 1.0.0 → 1.2.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.
Files changed (48) hide show
  1. package/bin/cli.js +170 -73
  2. package/package.json +3 -5
  3. package/src/activity.js +20 -0
  4. package/src/aider/domain-packs.js +27 -2
  5. package/src/aider/mcp-packs.js +231 -0
  6. package/src/aider/techniques.js +3210 -1397
  7. package/src/audit.js +290 -9
  8. package/src/catalog.js +18 -2
  9. package/src/codex/domain-packs.js +23 -1
  10. package/src/codex/mcp-packs.js +254 -0
  11. package/src/codex/techniques.js +4738 -3257
  12. package/src/copilot/domain-packs.js +23 -1
  13. package/src/copilot/mcp-packs.js +254 -0
  14. package/src/copilot/techniques.js +3433 -1936
  15. package/src/cursor/domain-packs.js +23 -1
  16. package/src/cursor/mcp-packs.js +257 -0
  17. package/src/cursor/techniques.js +3697 -1869
  18. package/src/deprecation.js +98 -0
  19. package/src/domain-pack-expansion.js +571 -0
  20. package/src/domain-packs.js +25 -2
  21. package/src/formatters/otel.js +151 -0
  22. package/src/gemini/domain-packs.js +23 -1
  23. package/src/gemini/mcp-packs.js +257 -0
  24. package/src/gemini/techniques.js +3734 -2238
  25. package/src/integrations.js +194 -0
  26. package/src/mcp-packs.js +233 -0
  27. package/src/opencode/domain-packs.js +23 -1
  28. package/src/opencode/mcp-packs.js +231 -0
  29. package/src/opencode/techniques.js +3500 -1687
  30. package/src/org.js +68 -0
  31. package/src/source-urls.js +410 -260
  32. package/src/stack-checks.js +565 -0
  33. package/src/supplemental-checks.js +767 -0
  34. package/src/techniques.js +2929 -1449
  35. package/src/telemetry.js +160 -0
  36. package/src/windsurf/domain-packs.js +23 -1
  37. package/src/windsurf/mcp-packs.js +257 -0
  38. package/src/windsurf/techniques.js +3647 -1834
  39. package/src/workspace.js +233 -0
  40. package/CHANGELOG.md +0 -198
  41. package/content/case-study-template.md +0 -91
  42. package/content/claims-governance.md +0 -37
  43. package/content/claude-code/audit-repo/SKILL.md +0 -20
  44. package/content/claude-native-integration.md +0 -60
  45. package/content/devto-article.json +0 -9
  46. package/content/launch-posts.md +0 -226
  47. package/content/pilot-rollout-kit.md +0 -30
  48. package/content/release-checklist.md +0 -31
package/src/audit.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * Audit engine - evaluates project against CLAUDEX technique database.
3
3
  */
4
4
 
5
+ const path = require('path');
5
6
  const { TECHNIQUES: CLAUDE_TECHNIQUES, STACKS } = require('./techniques');
6
7
  const { ProjectContext } = require('./context');
7
8
  const { CODEX_TECHNIQUES } = require('./codex/techniques');
@@ -25,8 +26,13 @@ const { OpenCodeProjectContext } = require('./opencode/context');
25
26
  const { getBadgeMarkdown } = require('./badge');
26
27
  const { sendInsights, getLocalInsights } = require('./insights');
27
28
  const { getRecommendationOutcomeSummary, getRecommendationAdjustment } = require('./activity');
29
+ const { getFeedbackSummary } = require('./feedback');
28
30
  const { formatSarif } = require('./formatters/sarif');
31
+ const { formatOtelMetrics } = require('./formatters/otel');
29
32
  const { loadPlugins, mergePluginChecks } = require('./plugins');
33
+ const { hasWorkspaceConfig, detectWorkspaceGlobs, detectWorkspaces } = require('./workspace');
34
+ const { detectDeprecationWarnings } = require('./deprecation');
35
+ const { version: packageVersion } = require('../package.json');
30
36
 
31
37
  const COLORS = {
32
38
  reset: '\x1b[0m',
@@ -57,6 +63,8 @@ function formatLocation(file, line) {
57
63
 
58
64
  const IMPACT_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
59
65
  const WEIGHTS = { critical: 15, high: 10, medium: 5, low: 2 };
66
+ const LARGE_INSTRUCTION_WARN_BYTES = 50 * 1024;
67
+ const LARGE_INSTRUCTION_SKIP_BYTES = 1024 * 1024;
60
68
  const CATEGORY_MODULES = {
61
69
  memory: 'CLAUDE.md',
62
70
  quality: 'verification',
@@ -349,6 +357,171 @@ function getAuditSpec(platform = 'claude') {
349
357
  };
350
358
  }
351
359
 
360
+ function normalizeRelativePath(filePath) {
361
+ return String(filePath || '').replace(/\\/g, '/').replace(/^\.\//, '');
362
+ }
363
+
364
+ function addPath(target, filePath) {
365
+ if (!filePath || typeof filePath !== 'string') return;
366
+ target.add(normalizeRelativePath(filePath));
367
+ }
368
+
369
+ function addDirFiles(ctx, target, dirPath, filter) {
370
+ if (typeof ctx.dirFiles !== 'function') return;
371
+ for (const file of ctx.dirFiles(dirPath)) {
372
+ if (filter && !filter.test(file)) continue;
373
+ addPath(target, path.join(dirPath, file));
374
+ }
375
+ }
376
+
377
+ function instructionFileCandidates(spec, ctx) {
378
+ const candidates = new Set();
379
+
380
+ if (spec.platform === 'claude') {
381
+ addPath(candidates, 'CLAUDE.md');
382
+ addPath(candidates, '.claude/CLAUDE.md');
383
+ addDirFiles(ctx, candidates, '.claude/rules', /\.md$/i);
384
+ addDirFiles(ctx, candidates, '.claude/commands', /\.md$/i);
385
+ addDirFiles(ctx, candidates, '.claude/agents', /\.md$/i);
386
+ if (typeof ctx.dirFiles === 'function') {
387
+ for (const skillDir of ctx.dirFiles('.claude/skills')) {
388
+ addPath(candidates, path.join('.claude', 'skills', skillDir, 'SKILL.md'));
389
+ }
390
+ }
391
+ }
392
+
393
+ if (spec.platform === 'codex') {
394
+ addPath(candidates, 'AGENTS.md');
395
+ addPath(candidates, 'AGENTS.override.md');
396
+ addPath(candidates, typeof ctx.agentsMdPath === 'function' ? ctx.agentsMdPath() : null);
397
+ addDirFiles(ctx, candidates, 'codex/rules');
398
+ addDirFiles(ctx, candidates, '.codex/rules');
399
+ if (typeof ctx.skillDirs === 'function') {
400
+ for (const skillDir of ctx.skillDirs()) {
401
+ addPath(candidates, path.join('.agents', 'skills', skillDir, 'SKILL.md'));
402
+ }
403
+ }
404
+ }
405
+
406
+ if (spec.platform === 'gemini') {
407
+ addPath(candidates, 'GEMINI.md');
408
+ addPath(candidates, '.gemini/GEMINI.md');
409
+ addDirFiles(ctx, candidates, '.gemini/agents', /\.md$/i);
410
+ if (typeof ctx.skillDirs === 'function') {
411
+ for (const skillDir of ctx.skillDirs()) {
412
+ addPath(candidates, path.join('.gemini', 'skills', skillDir, 'SKILL.md'));
413
+ }
414
+ }
415
+ }
416
+
417
+ if (spec.platform === 'copilot') {
418
+ addPath(candidates, '.github/copilot-instructions.md');
419
+ addDirFiles(ctx, candidates, '.github/instructions', /\.instructions\.md$/i);
420
+ addDirFiles(ctx, candidates, '.github/prompts', /\.prompt\.md$/i);
421
+ }
422
+
423
+ if (spec.platform === 'cursor') {
424
+ addPath(candidates, '.cursorrules');
425
+ addDirFiles(ctx, candidates, '.cursor/rules', /\.mdc$/i);
426
+ addDirFiles(ctx, candidates, '.cursor/commands', /\.md$/i);
427
+ }
428
+
429
+ if (spec.platform === 'windsurf') {
430
+ addPath(candidates, '.windsurfrules');
431
+ addDirFiles(ctx, candidates, '.windsurf/rules', /\.md$/i);
432
+ addDirFiles(ctx, candidates, '.windsurf/workflows', /\.md$/i);
433
+ addDirFiles(ctx, candidates, '.windsurf/memories', /\.(md|json)$/i);
434
+ }
435
+
436
+ if (spec.platform === 'aider' && typeof ctx.conventionFiles === 'function') {
437
+ for (const file of ctx.conventionFiles()) {
438
+ addPath(candidates, file);
439
+ }
440
+ }
441
+
442
+ if (spec.platform === 'opencode') {
443
+ addPath(candidates, 'AGENTS.md');
444
+ addPath(candidates, 'CLAUDE.md');
445
+ addDirFiles(ctx, candidates, '.opencode/commands', /\.(md|markdown|ya?ml)$/i);
446
+ if (typeof ctx.skillDirs === 'function') {
447
+ for (const skillDir of ctx.skillDirs()) {
448
+ addPath(candidates, path.join('.opencode', 'commands', skillDir, 'SKILL.md'));
449
+ }
450
+ }
451
+ }
452
+
453
+ return [...candidates];
454
+ }
455
+
456
+ function inspectInstructionFiles(spec, ctx) {
457
+ const warnings = [];
458
+
459
+ for (const filePath of instructionFileCandidates(spec, ctx)) {
460
+ const byteCount = typeof ctx.fileSizeBytes === 'function' ? ctx.fileSizeBytes(filePath) : null;
461
+ if (!Number.isFinite(byteCount) || byteCount <= LARGE_INSTRUCTION_WARN_BYTES) continue;
462
+
463
+ const content = typeof ctx.fileContent === 'function' ? ctx.fileContent(filePath) : null;
464
+ warnings.push({
465
+ file: normalizeRelativePath(filePath),
466
+ byteCount,
467
+ lineCount: typeof content === 'string' ? content.split(/\r?\n/).length : null,
468
+ skipped: byteCount > LARGE_INSTRUCTION_SKIP_BYTES,
469
+ severity: byteCount > LARGE_INSTRUCTION_SKIP_BYTES ? 'critical' : 'warning',
470
+ message: byteCount > LARGE_INSTRUCTION_SKIP_BYTES
471
+ ? 'Instruction file exceeds 1MB and will be skipped during audit.'
472
+ : 'Instruction file exceeds 50KB. Audit will continue, but this file may reduce runtime clarity.',
473
+ });
474
+ }
475
+
476
+ return warnings;
477
+ }
478
+
479
+ function guardSkippedInstructionFiles(ctx, warnings) {
480
+ const skippedFiles = new Set(
481
+ warnings.filter((item) => item.skipped).map((item) => normalizeRelativePath(item.file))
482
+ );
483
+
484
+ if (skippedFiles.size === 0) return;
485
+
486
+ const originalFileContent = typeof ctx.fileContent === 'function' ? ctx.fileContent.bind(ctx) : null;
487
+ const originalLineNumber = typeof ctx.lineNumber === 'function' ? ctx.lineNumber.bind(ctx) : null;
488
+
489
+ if (originalFileContent) {
490
+ ctx.fileContent = (filePath) => {
491
+ if (skippedFiles.has(normalizeRelativePath(filePath))) return null;
492
+ return originalFileContent(filePath);
493
+ };
494
+ }
495
+
496
+ if (originalLineNumber) {
497
+ ctx.lineNumber = (filePath, matcher) => {
498
+ if (skippedFiles.has(normalizeRelativePath(filePath))) return null;
499
+ return originalLineNumber(filePath, matcher);
500
+ };
501
+ }
502
+ }
503
+
504
+ function buildWorkspaceHint(dir) {
505
+ if (!hasWorkspaceConfig(dir)) {
506
+ return null;
507
+ }
508
+
509
+ const patterns = detectWorkspaceGlobs(dir);
510
+ const workspaces = detectWorkspaces(dir);
511
+ if (patterns.length === 0 && workspaces.length === 0) {
512
+ return null;
513
+ }
514
+
515
+ return {
516
+ detected: true,
517
+ patterns,
518
+ workspaces,
519
+ suggestedCommand: patterns.length > 0
520
+ ? `npx nerviq audit --workspace ${patterns.join(',')}`
521
+ : `npx nerviq audit --workspace ${workspaces.join(',')}`,
522
+ };
523
+ }
524
+
352
525
  function riskFromImpact(impact) {
353
526
  if (impact === 'critical') return 'high';
354
527
  if (impact === 'high') return 'medium';
@@ -441,24 +614,48 @@ function getQuickWins(failed, options = {}) {
441
614
  .slice(0, 3);
442
615
  }
443
616
 
444
- function getRecommendationPriorityScore(item, outcomeSummaryByKey = {}) {
617
+ /**
618
+ * Compute a multiplier based on FP (helpful/not-helpful) feedback for a check key.
619
+ * - >50% "not helpful" feedback: lower priority by 30% (multiplier 0.7)
620
+ * - >80% "helpful" feedback: boost priority by 20% (multiplier 1.2)
621
+ * - Otherwise: no change (multiplier 1.0)
622
+ * @param {Object} fpFeedbackByKey - Keyed feedback summary from getFeedbackSummary().byKey
623
+ * @param {string} key - The check key to look up
624
+ * @returns {number} Multiplier to apply to priority score
625
+ */
626
+ function getFpFeedbackMultiplier(fpFeedbackByKey, key) {
627
+ if (!fpFeedbackByKey) return 1.0;
628
+ const bucket = fpFeedbackByKey[key];
629
+ if (!bucket || bucket.total === 0) return 1.0;
630
+
631
+ const unhelpfulRate = bucket.unhelpful / bucket.total;
632
+ const helpfulRate = bucket.helpful / bucket.total;
633
+
634
+ if (unhelpfulRate > 0.5) return 0.7;
635
+ if (helpfulRate > 0.8) return 1.2;
636
+ return 1.0;
637
+ }
638
+
639
+ function getRecommendationPriorityScore(item, outcomeSummaryByKey = {}, fpFeedbackByKey = null) {
445
640
  const impactScore = (IMPACT_ORDER[item.impact] ?? 0) * 100;
446
641
  const feedbackAdjustment = getRecommendationAdjustment(outcomeSummaryByKey, item.key);
447
642
  const brevityPenalty = Math.min((item.fix || '').length, 240) / 20;
448
- return impactScore + (feedbackAdjustment * 10) - brevityPenalty;
643
+ const raw = impactScore + (feedbackAdjustment * 10) - brevityPenalty;
644
+ return raw * getFpFeedbackMultiplier(fpFeedbackByKey, item.key);
449
645
  }
450
646
 
451
647
  function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, options = {}) {
452
648
  const pool = getPrioritizedFailed(failed);
649
+ const fpByKey = options.fpFeedbackByKey || null;
453
650
 
454
651
  return [...pool]
455
652
  .sort((a, b) => {
456
653
  const scoreB = options.platform === 'codex'
457
654
  ? codexPriorityScore(b, outcomeSummaryByKey)
458
- : getRecommendationPriorityScore(b, outcomeSummaryByKey);
655
+ : getRecommendationPriorityScore(b, outcomeSummaryByKey, fpByKey);
459
656
  const scoreA = options.platform === 'codex'
460
657
  ? codexPriorityScore(a, outcomeSummaryByKey)
461
- : getRecommendationPriorityScore(a, outcomeSummaryByKey);
658
+ : getRecommendationPriorityScore(a, outcomeSummaryByKey, fpByKey);
462
659
  return scoreB - scoreA;
463
660
  })
464
661
  .slice(0, limit)
@@ -479,7 +676,7 @@ function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}, option
479
676
  const evidenceClass = options.platform === 'codex' ? codexEvidenceClass(fullItem) : (feedback ? 'measured' : 'estimated');
480
677
  const priorityScore = options.platform === 'codex'
481
678
  ? codexPriorityScore(fullItem, outcomeSummaryByKey)
482
- : Math.max(0, Math.min(100, Math.round(getRecommendationPriorityScore(fullItem, outcomeSummaryByKey) / 3)));
679
+ : Math.max(0, Math.min(100, Math.round(getRecommendationPriorityScore(fullItem, outcomeSummaryByKey, fpByKey) / 3)));
483
680
 
484
681
  signals.push(`evidence:${evidenceClass}`);
485
682
  if (options.platform === 'codex' && CODEX_HARD_FAIL_KEYS.has(key)) {
@@ -677,12 +874,20 @@ function printLiteAudit(result, dir) {
677
874
  if (result.platformScopeNote) {
678
875
  console.log(colorize(` Scope: ${result.platformScopeNote.message}`, 'dim'));
679
876
  }
877
+ if (result.workspaceHint && result.workspaceHint.workspaces.length > 0) {
878
+ console.log(colorize(` Workspaces: ${result.workspaceHint.workspaces.join(', ')}`, 'dim'));
879
+ }
680
880
  if (result.platformCaveats && result.platformCaveats.length > 0) {
681
881
  console.log(colorize(' Platform caveats:', 'yellow'));
682
882
  result.platformCaveats.slice(0, 2).forEach((item) => {
683
883
  console.log(colorize(` - ${item.title}: ${item.message}`, 'dim'));
684
884
  });
685
885
  }
886
+ if (result.largeInstructionFiles && result.largeInstructionFiles.length > 0) {
887
+ result.largeInstructionFiles.slice(0, 2).forEach((item) => {
888
+ console.log(colorize(` Large file: ${item.file} (${Math.round(item.byteCount / 1024)}KB)`, 'yellow'));
889
+ });
890
+ }
686
891
  console.log('');
687
892
 
688
893
  if (result.failed === 0) {
@@ -725,9 +930,13 @@ async function audit(options) {
725
930
  const spec = getAuditSpec(options.platform || 'claude');
726
931
  const silent = options.silent || false;
727
932
  const ctx = new spec.ContextClass(options.dir);
933
+ const largeInstructionFiles = inspectInstructionFiles(spec, ctx);
934
+ guardSkippedInstructionFiles(ctx, largeInstructionFiles);
728
935
  const stacks = ctx.detectStacks(STACKS);
729
936
  const results = [];
730
937
  const outcomeSummary = getRecommendationOutcomeSummary(options.dir);
938
+ const fpFeedback = getFeedbackSummary(options.dir);
939
+ const workspaceHint = buildWorkspaceHint(options.dir);
731
940
 
732
941
  // Load and merge plugin checks
733
942
  const plugins = loadPlugins(options.dir);
@@ -749,6 +958,24 @@ async function audit(options) {
749
958
  });
750
959
  }
751
960
 
961
+ if (largeInstructionFiles.length > 0) {
962
+ results.push({
963
+ key: 'largeInstructionFile',
964
+ id: null,
965
+ name: 'Large instruction file warning',
966
+ category: 'performance',
967
+ impact: 'medium',
968
+ rating: null,
969
+ fix: 'Split oversized instruction files so they stay under 50KB, and keep any single instruction file below 1MB.',
970
+ sourceUrl: null,
971
+ confidence: 'high',
972
+ file: largeInstructionFiles[0].file,
973
+ line: null,
974
+ passed: null,
975
+ details: largeInstructionFiles,
976
+ });
977
+ }
978
+
752
979
  // null = not applicable (skip), true = pass, false = fail
753
980
  const applicable = results.filter(r => r.passed !== null);
754
981
  const skipped = results.filter(r => r.passed === null);
@@ -795,10 +1022,27 @@ async function audit(options) {
795
1022
  const organicEarned = organicPassed.reduce((sum, r) => sum + (WEIGHTS[r.impact] || 5), 0);
796
1023
  const organicScore = maxScore > 0 ? Math.round((organicEarned / maxScore) * 100) : 0;
797
1024
  const quickWins = getQuickWins(failed, { platform: spec.platform });
798
- const topNextActions = buildTopNextActions(failed, 5, outcomeSummary.byKey, { platform: spec.platform });
1025
+ const topNextActions = buildTopNextActions(failed, 5, outcomeSummary.byKey, { platform: spec.platform, fpFeedbackByKey: fpFeedback.byKey });
799
1026
  const categoryScores = computeCategoryScores(applicable, passed);
800
1027
  const platformScopeNote = getPlatformScopeNote(spec, ctx);
801
1028
  const platformCaveats = getPlatformCaveats(spec, ctx);
1029
+ const deprecationWarnings = detectDeprecationWarnings(failed, packageVersion);
1030
+ const warnings = [
1031
+ ...largeInstructionFiles.map((item) => ({
1032
+ kind: 'large-instruction-file',
1033
+ severity: item.severity,
1034
+ message: item.message,
1035
+ file: item.file,
1036
+ lineCount: item.lineCount,
1037
+ byteCount: item.byteCount,
1038
+ skipped: item.skipped,
1039
+ })),
1040
+ ...deprecationWarnings.map((item) => ({
1041
+ kind: 'deprecated-feature',
1042
+ severity: 'warning',
1043
+ ...item,
1044
+ })),
1045
+ ];
802
1046
  const recommendedDomainPacks = spec.platform === 'codex'
803
1047
  ? detectCodexDomainPacks(ctx, stacks, getCodexDomainPackSignals(ctx))
804
1048
  : [];
@@ -824,6 +1068,10 @@ async function audit(options) {
824
1068
  totalEntries: outcomeSummary.totalEntries,
825
1069
  keysTracked: outcomeSummary.keys,
826
1070
  },
1071
+ largeInstructionFiles,
1072
+ deprecationWarnings,
1073
+ warnings,
1074
+ workspaceHint,
827
1075
  platformScopeNote,
828
1076
  platformCaveats,
829
1077
  recommendedDomainPacks,
@@ -841,9 +1089,8 @@ async function audit(options) {
841
1089
  }
842
1090
 
843
1091
  if (options.json) {
844
- const { version } = require('../package.json');
845
1092
  console.log(JSON.stringify({
846
- version,
1093
+ version: packageVersion,
847
1094
  timestamp: new Date().toISOString(),
848
1095
  ...result
849
1096
  }, null, 2));
@@ -855,6 +1102,11 @@ async function audit(options) {
855
1102
  return result;
856
1103
  }
857
1104
 
1105
+ if (options.format === 'otel') {
1106
+ console.log(JSON.stringify(formatOtelMetrics(result), null, 2));
1107
+ return result;
1108
+ }
1109
+
858
1110
  if (options.lite) {
859
1111
  printLiteAudit(result, options.dir);
860
1112
  sendInsights(result);
@@ -888,6 +1140,35 @@ async function audit(options) {
888
1140
  console.log('');
889
1141
  }
890
1142
 
1143
+ if (largeInstructionFiles.length > 0) {
1144
+ console.log(colorize(' Large instruction files', 'yellow'));
1145
+ for (const item of largeInstructionFiles) {
1146
+ const sizeKb = Math.round(item.byteCount / 1024);
1147
+ console.log(colorize(` ${item.file} (${sizeKb}KB, ${item.lineCount || '?'} lines)`, 'bold'));
1148
+ console.log(colorize(` → ${item.message}`, 'dim'));
1149
+ }
1150
+ console.log('');
1151
+ }
1152
+
1153
+ if (deprecationWarnings.length > 0) {
1154
+ console.log(colorize(' Deprecated feature warnings', 'yellow'));
1155
+ for (const item of deprecationWarnings) {
1156
+ console.log(colorize(` ${item.feature}`, 'bold'));
1157
+ console.log(colorize(` → ${item.message}`, 'dim'));
1158
+ console.log(colorize(` Alternative: ${item.alternative}`, 'dim'));
1159
+ }
1160
+ console.log('');
1161
+ }
1162
+
1163
+ if (workspaceHint && !options.workspace) {
1164
+ console.log(colorize(' Monorepo detected', 'blue'));
1165
+ if (workspaceHint.workspaces.length > 0) {
1166
+ console.log(colorize(` Workspaces: ${workspaceHint.workspaces.join(', ')}`, 'dim'));
1167
+ }
1168
+ console.log(colorize(` Tip: ${workspaceHint.suggestedCommand}`, 'dim'));
1169
+ console.log('');
1170
+ }
1171
+
891
1172
  if (stacks.length > 0) {
892
1173
  console.log(colorize(` Detected: ${stacks.map(s => s.label).join(', ')}`, 'blue'));
893
1174
  }
@@ -1008,4 +1289,4 @@ async function audit(options) {
1008
1289
  return result;
1009
1290
  }
1010
1291
 
1011
- module.exports = { audit, buildTopNextActions };
1292
+ module.exports = { audit, buildTopNextActions, getFpFeedbackMultiplier, getRecommendationPriorityScore };
package/src/catalog.js CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
+ const { version } = require('../package.json');
8
9
 
9
10
  const { TECHNIQUES: CLAUDE_TECHNIQUES } = require('./techniques');
10
11
  const { CODEX_TECHNIQUES } = require('./codex/techniques');
@@ -31,7 +32,7 @@ const PLATFORM_MAP = {
31
32
  * Generate a unified catalog array from all platform technique files.
32
33
  * Each entry contains:
33
34
  * platform, id, key, name, category, impact, rating, fix, sourceUrl,
34
- * confidence, template, deprecated
35
+ * confidence, lastVerified, template, deprecated
35
36
  */
36
37
  function generateCatalog() {
37
38
  const catalog = [];
@@ -62,6 +63,7 @@ function generateCatalog() {
62
63
  fix: tech.fix ?? null,
63
64
  sourceUrl: tech.sourceUrl ?? null,
64
65
  confidence: tech.confidence ?? null,
66
+ lastVerified: tech.lastVerified ?? null,
65
67
  template: tech.template ?? null,
66
68
  deprecated: tech.deprecated ?? false,
67
69
  });
@@ -71,6 +73,20 @@ function generateCatalog() {
71
73
  return catalog;
72
74
  }
73
75
 
76
+ /**
77
+ * Generate a catalog envelope with version metadata.
78
+ * @returns {{ catalogVersion: string, generatedAt: string, totalChecks: number, checks: Object[] }}
79
+ */
80
+ function generateCatalogWithVersion() {
81
+ const checks = generateCatalog();
82
+ return {
83
+ catalogVersion: version,
84
+ generatedAt: new Date().toISOString(),
85
+ totalChecks: checks.length,
86
+ checks,
87
+ };
88
+ }
89
+
74
90
  /**
75
91
  * Write the catalog as formatted JSON to the given output path.
76
92
  * @param {string} outputPath - Absolute or relative path for the JSON file
@@ -84,4 +100,4 @@ function writeCatalogJson(outputPath) {
84
100
  return { path: resolved, count: catalog.length };
85
101
  }
86
102
 
87
- module.exports = { generateCatalog, writeCatalogJson };
103
+ module.exports = { generateCatalog, generateCatalogWithVersion, writeCatalogJson };
@@ -1,4 +1,6 @@
1
- const CODEX_DOMAIN_PACKS = [
1
+ const { buildAdditionalDomainPacks, detectAdditionalDomainPacks } = require('../domain-pack-expansion');
2
+
3
+ const BASE_CODEX_DOMAIN_PACKS = [
2
4
  {
3
5
  key: 'baseline-general',
4
6
  label: 'Baseline General',
@@ -162,6 +164,13 @@ const CODEX_DOMAIN_PACKS = [
162
164
  },
163
165
  ];
164
166
 
167
+ const CODEX_DOMAIN_PACKS = [
168
+ ...BASE_CODEX_DOMAIN_PACKS,
169
+ ...buildAdditionalDomainPacks('codex', {
170
+ existingKeys: new Set(BASE_CODEX_DOMAIN_PACKS.map((pack) => pack.key)),
171
+ }),
172
+ ];
173
+
165
174
  function uniqueByKey(items) {
166
175
  const seen = new Set();
167
176
  const result = [];
@@ -354,6 +363,19 @@ function detectCodexDomainPacks(ctx, stacks = [], assets = {}) {
354
363
  ]);
355
364
  }
356
365
 
366
+ detectAdditionalDomainPacks({
367
+ ctx,
368
+ pkg,
369
+ deps,
370
+ stackKeys,
371
+ addMatch,
372
+ hasBackend,
373
+ hasFrontend,
374
+ hasInfra,
375
+ hasCi,
376
+ isEnterpriseGoverned,
377
+ });
378
+
357
379
  if (matches.length === 0) {
358
380
  addMatch('baseline-general', [
359
381
  'No stronger platform-specific domain dominated, so a safe general Codex baseline is the best starting point.',