@rigour-labs/core 4.2.0 → 4.2.2

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.
@@ -94,32 +94,49 @@ function verifyFinding(finding, factsByPath) {
94
94
  };
95
95
  }
96
96
  return { ...finding, verified: finding.confidence >= 0.4, verificationNotes: 'Accepted on confidence' };
97
- // ── Categories verified by file existence + reasonable confidence ──
98
- case 'dry_violation':
99
- case 'copy_paste_code':
100
- case 'data_clump':
97
+ // ── Entity-name-verified categories (Tier 1) ──
98
+ // LLM must reference a real class/struct/function name — drops hallucinated entities
99
+ case 'lazy_class':
100
+ return verifyLazyClass(finding, fileFacts);
101
101
  case 'feature_envy':
102
- case 'shotgun_surgery':
103
- case 'inappropriate_intimacy':
104
102
  case 'primitive_obsession':
105
- case 'lazy_class':
106
103
  case 'speculative_generality':
107
104
  case 'refused_bequest':
108
- case 'architecture':
109
- case 'circular_dependency':
110
- case 'package_cohesion':
111
- case 'api_design':
112
105
  case 'missing_abstraction':
113
- case 'language_idiom':
114
- case 'naming_convention':
106
+ case 'api_design':
107
+ return verifyEntityNameRequired(finding, fileFacts);
108
+ // ── Structural precondition categories (Tier 2) ──
115
109
  case 'dead_code':
116
- case 'code_smell':
117
- case 'performance':
110
+ return verifyDeadCode(finding, fileFacts, factsByPath);
111
+ case 'naming_convention':
112
+ return verifyNamingConvention(finding, fileFacts);
118
113
  case 'hardcoded_config':
114
+ return verifyHardcodedConfig(finding, fileFacts);
115
+ case 'data_clump':
116
+ return verifyDataClump(finding, fileFacts);
117
+ case 'performance':
118
+ return verifyPerformance(finding, fileFacts);
119
+ // ── Cross-file graph categories (Tier 3) ──
120
+ case 'circular_dependency':
121
+ return verifyCircularDependency(finding, fileFacts, factsByPath);
122
+ case 'dry_violation':
123
+ case 'copy_paste_code':
124
+ return verifyDryViolation(finding, fileFacts, factsByPath);
125
+ case 'shotgun_surgery':
126
+ return verifyShotgunSurgery(finding, fileFacts, factsByPath);
127
+ case 'inappropriate_intimacy':
128
+ return verifyInappropriateIntimacy(finding, fileFacts, factsByPath);
129
+ // ── Confidence-floor categories (Tier 4) — raised from 0.3 → 0.5 ──
130
+ case 'architecture':
131
+ case 'package_cohesion':
132
+ case 'code_smell':
133
+ case 'language_idiom':
119
134
  return {
120
135
  ...finding,
121
- verified: finding.confidence >= 0.3,
122
- verificationNotes: finding.confidence < 0.3 ? 'Low confidence' : 'File exists, accepted',
136
+ verified: finding.confidence >= 0.5,
137
+ verificationNotes: finding.confidence < 0.5
138
+ ? `Low confidence (${finding.confidence}) — requires >= 0.5 for ${finding.category}`
139
+ : `Accepted at confidence ${finding.confidence} (threshold: 0.5)`,
123
140
  };
124
141
  default:
125
142
  // Unknown category — accept if file exists and confidence is reasonable
@@ -344,6 +361,498 @@ function verifyTestFinding(finding, facts) {
344
361
  verificationNotes: 'Accepted on confidence',
345
362
  };
346
363
  }
364
+ // ════════════════════════════════════════════════════════════════════════
365
+ // Tier 1: Entity-name-verified categories
366
+ // LLM must reference a real class/struct/function — hallucinated names are dropped
367
+ // ════════════════════════════════════════════════════════════════════════
368
+ /**
369
+ * Verify lazy_class: class/struct must exist AND have few methods.
370
+ * A "lazy class" that doesn't exist is a hallucination.
371
+ */
372
+ function verifyLazyClass(finding, facts) {
373
+ const entities = [
374
+ ...facts.classes.map(c => ({ name: c.name, methodCount: c.methodCount, lineCount: c.lineCount })),
375
+ ...(facts.structs || []).map(s => ({ name: s.name, methodCount: s.methodCount, lineCount: s.lineCount })),
376
+ ];
377
+ if (entities.length === 0) {
378
+ return { ...finding, verified: false, verificationNotes: 'No classes or structs found — cannot verify lazy class' };
379
+ }
380
+ const entityName = extractEntityName(finding.description, entities.map(e => e.name));
381
+ if (!entityName) {
382
+ // LLM didn't reference a specific name — only accept if high confidence
383
+ return {
384
+ ...finding,
385
+ verified: finding.confidence >= 0.6,
386
+ verificationNotes: 'No entity name found in description — requires high confidence',
387
+ };
388
+ }
389
+ const entity = entities.find(e => e.name === entityName);
390
+ if (!entity) {
391
+ return { ...finding, verified: false, verificationNotes: `Entity "${entityName}" not found in ${facts.path}` };
392
+ }
393
+ // Lazy class = too few methods for its existence
394
+ if (entity.methodCount >= 4 && entity.lineCount >= 50) {
395
+ return {
396
+ ...finding,
397
+ verified: false,
398
+ verificationNotes: `"${entityName}" has ${entity.methodCount} methods, ${entity.lineCount} lines — not lazy`,
399
+ };
400
+ }
401
+ return {
402
+ ...finding,
403
+ verified: true,
404
+ verificationNotes: `"${entityName}" has only ${entity.methodCount} methods, ${entity.lineCount} lines — lazy class confirmed`,
405
+ };
406
+ }
407
+ /**
408
+ * Generic entity-name verification for design smell categories.
409
+ * Requires the LLM to reference a real function/class/struct name.
410
+ */
411
+ function verifyEntityNameRequired(finding, facts) {
412
+ const allNames = [
413
+ ...facts.classes.map(c => c.name),
414
+ ...(facts.structs || []).map(s => s.name),
415
+ ...facts.functions.map(f => f.name),
416
+ ];
417
+ if (allNames.length === 0) {
418
+ return { ...finding, verified: false, verificationNotes: 'No entities found in file' };
419
+ }
420
+ const entityName = extractEntityName(finding.description, allNames);
421
+ if (entityName) {
422
+ return {
423
+ ...finding,
424
+ verified: finding.confidence >= 0.3,
425
+ verificationNotes: `Entity "${entityName}" exists — ${finding.category} accepted`,
426
+ };
427
+ }
428
+ // LLM didn't name a specific entity — require higher confidence
429
+ return {
430
+ ...finding,
431
+ verified: finding.confidence >= 0.6,
432
+ verificationNotes: `No entity name matched in description — requires confidence >= 0.6 for ${finding.category}`,
433
+ };
434
+ }
435
+ // ════════════════════════════════════════════════════════════════════════
436
+ // Tier 2: Structural precondition categories
437
+ // ════════════════════════════════════════════════════════════════════════
438
+ /**
439
+ * Verify dead_code: function/export must exist AND not be referenced by other files.
440
+ */
441
+ function verifyDeadCode(finding, facts, factsByPath) {
442
+ const allNames = [
443
+ ...facts.functions.map(f => f.name),
444
+ ...facts.exports,
445
+ ...facts.classes.map(c => c.name),
446
+ ];
447
+ const entityName = extractEntityName(finding.description, allNames);
448
+ if (!entityName) {
449
+ return {
450
+ ...finding,
451
+ verified: finding.confidence >= 0.6,
452
+ verificationNotes: 'No entity name found — requires high confidence for dead code',
453
+ };
454
+ }
455
+ // Check if the entity is referenced in any other file's imports
456
+ let referencedExternally = false;
457
+ for (const [otherPath, otherFacts] of factsByPath) {
458
+ if (otherPath === facts.path)
459
+ continue;
460
+ // Check if any import in other files references this entity or this file
461
+ for (const imp of otherFacts.imports) {
462
+ if (imp.includes(entityName)) {
463
+ referencedExternally = true;
464
+ break;
465
+ }
466
+ }
467
+ if (referencedExternally)
468
+ break;
469
+ }
470
+ if (referencedExternally) {
471
+ return {
472
+ ...finding,
473
+ verified: false,
474
+ verificationNotes: `"${entityName}" is imported by other files — not dead code`,
475
+ };
476
+ }
477
+ // Check if the entity is exported (could be used externally)
478
+ const func = facts.functions.find(f => f.name === entityName);
479
+ if (func?.isExported) {
480
+ // Exported but no internal references — might be a public API
481
+ return {
482
+ ...finding,
483
+ verified: finding.confidence >= 0.5,
484
+ verificationNotes: `"${entityName}" is exported but unreferenced — possible dead public API`,
485
+ };
486
+ }
487
+ return {
488
+ ...finding,
489
+ verified: true,
490
+ verificationNotes: `"${entityName}" exists and is not referenced externally — dead code confirmed`,
491
+ };
492
+ }
493
+ /**
494
+ * Verify naming_convention: check the referenced name against language-specific patterns.
495
+ */
496
+ function verifyNamingConvention(finding, facts) {
497
+ const allNames = [
498
+ ...facts.classes.map(c => c.name),
499
+ ...(facts.structs || []).map(s => s.name),
500
+ ...facts.functions.map(f => f.name),
501
+ ];
502
+ const entityName = extractEntityName(finding.description, allNames);
503
+ if (!entityName) {
504
+ return {
505
+ ...finding,
506
+ verified: finding.confidence >= 0.6,
507
+ verificationNotes: 'No entity name found — requires high confidence for naming convention',
508
+ };
509
+ }
510
+ // Language-specific naming convention checks
511
+ let violatesConvention = false;
512
+ let reason = '';
513
+ switch (facts.language) {
514
+ case 'go':
515
+ // Go: exported = PascalCase, unexported = camelCase. Common violation: snake_case
516
+ if (entityName.includes('_') && !entityName.startsWith('_') && entityName !== entityName.toUpperCase()) {
517
+ violatesConvention = true;
518
+ reason = 'Go names should use MixedCaps, not snake_case';
519
+ }
520
+ break;
521
+ case 'python':
522
+ // Python: functions/variables = snake_case, classes = PascalCase
523
+ if (facts.functions.some(f => f.name === entityName)) {
524
+ // Function — should be snake_case
525
+ if (entityName !== entityName.toLowerCase() && /[A-Z]/.test(entityName) && !entityName.startsWith('_')) {
526
+ violatesConvention = true;
527
+ reason = 'Python functions should be snake_case';
528
+ }
529
+ }
530
+ else if (facts.classes.some(c => c.name === entityName)) {
531
+ // Class — should be PascalCase
532
+ if (entityName.includes('_') && entityName !== entityName.toUpperCase()) {
533
+ violatesConvention = true;
534
+ reason = 'Python classes should be PascalCase';
535
+ }
536
+ }
537
+ break;
538
+ case 'typescript':
539
+ case 'javascript':
540
+ // JS/TS: functions = camelCase, classes = PascalCase
541
+ if (facts.functions.some(f => f.name === entityName)) {
542
+ if (entityName.includes('_') && entityName !== entityName.toUpperCase()) {
543
+ violatesConvention = true;
544
+ reason = 'JS/TS functions should be camelCase';
545
+ }
546
+ }
547
+ break;
548
+ default:
549
+ // No language-specific check — fall back to confidence
550
+ return {
551
+ ...finding,
552
+ verified: finding.confidence >= 0.5,
553
+ verificationNotes: `No naming rules for ${facts.language} — accepted on confidence`,
554
+ };
555
+ }
556
+ if (!violatesConvention) {
557
+ return {
558
+ ...finding,
559
+ verified: false,
560
+ verificationNotes: `"${entityName}" follows ${facts.language} naming conventions — false positive`,
561
+ };
562
+ }
563
+ return {
564
+ ...finding,
565
+ verified: true,
566
+ verificationNotes: `"${entityName}" — ${reason}`,
567
+ };
568
+ }
569
+ /**
570
+ * Verify hardcoded_config: file must have sufficient magic numbers or string constants.
571
+ */
572
+ function verifyHardcodedConfig(finding, facts) {
573
+ const hasMagicNumbers = (facts.magicNumbers || 0) > 2;
574
+ const isTestFile = facts.hasTests;
575
+ // Don't flag test files for hardcoded config — test data is expected
576
+ if (isTestFile) {
577
+ return {
578
+ ...finding,
579
+ verified: false,
580
+ verificationNotes: 'Test files are expected to have hardcoded values',
581
+ };
582
+ }
583
+ if (hasMagicNumbers) {
584
+ return {
585
+ ...finding,
586
+ verified: true,
587
+ verificationNotes: `File has ${facts.magicNumbers} magic numbers — hardcoded config likely`,
588
+ };
589
+ }
590
+ // No magic numbers detected by AST — require higher LLM confidence
591
+ return {
592
+ ...finding,
593
+ verified: finding.confidence >= 0.6,
594
+ verificationNotes: `No magic numbers detected by AST (${facts.magicNumbers || 0}) — requires confidence >= 0.6`,
595
+ };
596
+ }
597
+ /**
598
+ * Verify data_clump: multiple functions must share 3+ similar parameter names.
599
+ */
600
+ function verifyDataClump(finding, facts) {
601
+ if (facts.functions.length < 2) {
602
+ return { ...finding, verified: false, verificationNotes: 'Need multiple functions to detect data clump' };
603
+ }
604
+ // Check if any pair of functions shares 3+ parameter names
605
+ let maxSharedParams = 0;
606
+ let clumpPair = '';
607
+ for (let i = 0; i < facts.functions.length; i++) {
608
+ for (let j = i + 1; j < facts.functions.length; j++) {
609
+ const paramsA = new Set(facts.functions[i].params.map(p => p.replace(/[:\s].*/, '').trim().toLowerCase()));
610
+ const paramsB = new Set(facts.functions[j].params.map(p => p.replace(/[:\s].*/, '').trim().toLowerCase()));
611
+ const shared = [...paramsA].filter(p => paramsB.has(p) && p.length > 1).length;
612
+ if (shared > maxSharedParams) {
613
+ maxSharedParams = shared;
614
+ clumpPair = `${facts.functions[i].name} & ${facts.functions[j].name}`;
615
+ }
616
+ }
617
+ }
618
+ if (maxSharedParams >= 3) {
619
+ return {
620
+ ...finding,
621
+ verified: true,
622
+ verificationNotes: `${clumpPair} share ${maxSharedParams} parameters — data clump confirmed`,
623
+ };
624
+ }
625
+ return {
626
+ ...finding,
627
+ verified: false,
628
+ verificationNotes: `Max shared params between any function pair: ${maxSharedParams} (need >= 3)`,
629
+ };
630
+ }
631
+ /**
632
+ * Verify performance: function must be non-trivial (> 20 lines or has deep nesting).
633
+ */
634
+ function verifyPerformance(finding, facts) {
635
+ const entityName = extractEntityName(finding.description, facts.functions.map(f => f.name));
636
+ if (entityName) {
637
+ const func = facts.functions.find(f => f.name === entityName);
638
+ if (!func) {
639
+ return { ...finding, verified: false, verificationNotes: `Function "${entityName}" not found` };
640
+ }
641
+ if (func.lineCount < 10) {
642
+ return {
643
+ ...finding,
644
+ verified: false,
645
+ verificationNotes: `Function "${entityName}" is only ${func.lineCount} lines — unlikely performance issue`,
646
+ };
647
+ }
648
+ return {
649
+ ...finding,
650
+ verified: true,
651
+ verificationNotes: `Function "${entityName}" is ${func.lineCount} lines — performance review accepted`,
652
+ };
653
+ }
654
+ // No specific function referenced — check file-level
655
+ if (facts.lineCount < 50) {
656
+ return {
657
+ ...finding,
658
+ verified: false,
659
+ verificationNotes: `File is only ${facts.lineCount} lines — unlikely performance hotspot`,
660
+ };
661
+ }
662
+ return {
663
+ ...finding,
664
+ verified: finding.confidence >= 0.5,
665
+ verificationNotes: `No specific function referenced — accepted at confidence >= 0.5`,
666
+ };
667
+ }
668
+ // ════════════════════════════════════════════════════════════════════════
669
+ // Tier 3: Cross-file graph verification
670
+ // ════════════════════════════════════════════════════════════════════════
671
+ /**
672
+ * Verify circular_dependency: build import graph and check if a cycle actually exists.
673
+ */
674
+ function verifyCircularDependency(finding, facts, factsByPath) {
675
+ // Extract the other file(s) mentioned in the finding description
676
+ const mentionedPaths = [];
677
+ for (const [p] of factsByPath) {
678
+ const baseName = p.split('/').pop() || '';
679
+ const dirName = p.split('/').slice(-2).join('/');
680
+ if (finding.description.includes(baseName) || finding.description.includes(dirName) || finding.description.includes(p)) {
681
+ if (p !== facts.path)
682
+ mentionedPaths.push(p);
683
+ }
684
+ }
685
+ if (mentionedPaths.length === 0) {
686
+ // LLM didn't reference a specific file — require high confidence
687
+ return {
688
+ ...finding,
689
+ verified: finding.confidence >= 0.6,
690
+ verificationNotes: 'No specific file path mentioned in description — requires confidence >= 0.6',
691
+ };
692
+ }
693
+ // Check for actual bidirectional imports
694
+ for (const otherPath of mentionedPaths) {
695
+ const otherFacts = factsByPath.get(otherPath);
696
+ if (!otherFacts)
697
+ continue;
698
+ const thisImportsOther = facts.imports.some(imp => otherPath.includes(imp.replace(/\./g, '/')) || imp.includes(otherPath.replace(/\//g, '.').replace(/\.\w+$/, '')));
699
+ const otherImportsThis = otherFacts.imports.some(imp => facts.path.includes(imp.replace(/\./g, '/')) || imp.includes(facts.path.replace(/\//g, '.').replace(/\.\w+$/, '')));
700
+ if (thisImportsOther && otherImportsThis) {
701
+ return {
702
+ ...finding,
703
+ verified: true,
704
+ verificationNotes: `Circular import confirmed: ${facts.path} ↔ ${otherPath}`,
705
+ };
706
+ }
707
+ }
708
+ // Check generic: does the current file import something that imports it back?
709
+ const thisFileModules = new Set(facts.imports);
710
+ for (const [otherPath, otherFacts] of factsByPath) {
711
+ if (otherPath === facts.path)
712
+ continue;
713
+ const otherImportsThis = otherFacts.imports.some(imp => {
714
+ const normalized = facts.path.replace(/\.\w+$/, '').replace(/\//g, '/');
715
+ return normalized.endsWith(imp.replace(/\./g, '/')) || imp.endsWith(normalized);
716
+ });
717
+ if (otherImportsThis) {
718
+ const thisImportsOther = facts.imports.some(imp => {
719
+ const normalized = otherPath.replace(/\.\w+$/, '').replace(/\//g, '/');
720
+ return normalized.endsWith(imp.replace(/\./g, '/')) || imp.endsWith(normalized);
721
+ });
722
+ if (thisImportsOther) {
723
+ return {
724
+ ...finding,
725
+ verified: true,
726
+ verificationNotes: `Circular import found: ${facts.path} ↔ ${otherPath}`,
727
+ };
728
+ }
729
+ }
730
+ }
731
+ return {
732
+ ...finding,
733
+ verified: false,
734
+ verificationNotes: 'No circular dependency found in import graph',
735
+ };
736
+ }
737
+ /**
738
+ * Verify dry_violation / copy_paste_code: check for functions with similar signatures across files.
739
+ */
740
+ function verifyDryViolation(finding, facts, factsByPath) {
741
+ // Extract referenced function name
742
+ const entityName = extractEntityName(finding.description, facts.functions.map(f => f.name));
743
+ if (entityName) {
744
+ const func = facts.functions.find(f => f.name === entityName);
745
+ if (!func) {
746
+ return { ...finding, verified: false, verificationNotes: `Function "${entityName}" not found` };
747
+ }
748
+ // Check if a similar function exists in another file
749
+ for (const [otherPath, otherFacts] of factsByPath) {
750
+ if (otherPath === facts.path)
751
+ continue;
752
+ for (const otherFunc of otherFacts.functions) {
753
+ // Similar if: same name, or similar param count and line count
754
+ const nameSimilar = otherFunc.name === func.name
755
+ || otherFunc.name.toLowerCase() === func.name.toLowerCase();
756
+ const structSimilar = Math.abs(otherFunc.paramCount - func.paramCount) <= 1
757
+ && Math.abs(otherFunc.lineCount - func.lineCount) <= 5
758
+ && func.lineCount > 5;
759
+ if (nameSimilar || structSimilar) {
760
+ return {
761
+ ...finding,
762
+ verified: true,
763
+ verificationNotes: `"${entityName}" (${func.lineCount} lines) similar to "${otherFunc.name}" in ${otherPath} (${otherFunc.lineCount} lines)`,
764
+ };
765
+ }
766
+ }
767
+ }
768
+ return {
769
+ ...finding,
770
+ verified: false,
771
+ verificationNotes: `No similar function found across files for "${entityName}"`,
772
+ };
773
+ }
774
+ // No function name — require high confidence
775
+ return {
776
+ ...finding,
777
+ verified: finding.confidence >= 0.6,
778
+ verificationNotes: 'No entity name found in DRY violation — requires confidence >= 0.6',
779
+ };
780
+ }
781
+ /**
782
+ * Verify shotgun_surgery: an entity should be referenced/imported by many files (>= 4).
783
+ */
784
+ function verifyShotgunSurgery(finding, facts, factsByPath) {
785
+ const allNames = [
786
+ ...facts.classes.map(c => c.name),
787
+ ...(facts.structs || []).map(s => s.name),
788
+ ...facts.functions.filter(f => f.isExported).map(f => f.name),
789
+ ];
790
+ const entityName = extractEntityName(finding.description, allNames);
791
+ if (!entityName) {
792
+ return {
793
+ ...finding,
794
+ verified: finding.confidence >= 0.6,
795
+ verificationNotes: 'No entity name found — requires high confidence for shotgun surgery',
796
+ };
797
+ }
798
+ // Count how many other files reference this entity
799
+ let importerCount = 0;
800
+ for (const [otherPath, otherFacts] of factsByPath) {
801
+ if (otherPath === facts.path)
802
+ continue;
803
+ if (otherFacts.imports.some(imp => imp.includes(entityName))) {
804
+ importerCount++;
805
+ }
806
+ }
807
+ if (importerCount >= 4) {
808
+ return {
809
+ ...finding,
810
+ verified: true,
811
+ verificationNotes: `"${entityName}" is imported by ${importerCount} files — shotgun surgery confirmed`,
812
+ };
813
+ }
814
+ return {
815
+ ...finding,
816
+ verified: false,
817
+ verificationNotes: `"${entityName}" only imported by ${importerCount} files (need >= 4 for shotgun surgery)`,
818
+ };
819
+ }
820
+ /**
821
+ * Verify inappropriate_intimacy: two modules must have bidirectional imports.
822
+ */
823
+ function verifyInappropriateIntimacy(finding, facts, factsByPath) {
824
+ // Look for bidirectional import relationships from this file
825
+ let biDirectionalCount = 0;
826
+ let biDirectionalPartner = '';
827
+ for (const [otherPath, otherFacts] of factsByPath) {
828
+ if (otherPath === facts.path)
829
+ continue;
830
+ const thisImportsOther = facts.imports.some(imp => {
831
+ const otherModule = otherPath.replace(/\.\w+$/, '');
832
+ return otherModule.endsWith(imp.replace(/\./g, '/')) || imp.endsWith(otherModule.split('/').pop() || '');
833
+ });
834
+ const otherImportsThis = otherFacts.imports.some(imp => {
835
+ const thisModule = facts.path.replace(/\.\w+$/, '');
836
+ return thisModule.endsWith(imp.replace(/\./g, '/')) || imp.endsWith(thisModule.split('/').pop() || '');
837
+ });
838
+ if (thisImportsOther && otherImportsThis) {
839
+ biDirectionalCount++;
840
+ biDirectionalPartner = otherPath;
841
+ }
842
+ }
843
+ if (biDirectionalCount > 0) {
844
+ return {
845
+ ...finding,
846
+ verified: true,
847
+ verificationNotes: `Bidirectional import with ${biDirectionalPartner} — inappropriate intimacy confirmed`,
848
+ };
849
+ }
850
+ return {
851
+ ...finding,
852
+ verified: false,
853
+ verificationNotes: 'No bidirectional import relationships found',
854
+ };
855
+ }
347
856
  /**
348
857
  * Try to find a known entity name referenced in a description string.
349
858
  */