@rigour-labs/core 4.2.1 → 4.2.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/dist/deep/verifier.js +526 -17
- package/dist/deep/verifier.test.js +144 -23
- package/package.json +6 -6
package/dist/deep/verifier.js
CHANGED
|
@@ -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
|
-
// ──
|
|
98
|
-
|
|
99
|
-
case '
|
|
100
|
-
|
|
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 '
|
|
114
|
-
|
|
106
|
+
case 'api_design':
|
|
107
|
+
return verifyEntityNameRequired(finding, fileFacts);
|
|
108
|
+
// ── Structural precondition categories (Tier 2) ──
|
|
115
109
|
case 'dead_code':
|
|
116
|
-
|
|
117
|
-
case '
|
|
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.
|
|
122
|
-
verificationNotes: finding.confidence < 0.
|
|
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
|
*/
|
|
@@ -441,26 +441,147 @@ describe('Verifier', () => {
|
|
|
441
441
|
expect(result).toHaveLength(0);
|
|
442
442
|
});
|
|
443
443
|
});
|
|
444
|
-
// ──
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
444
|
+
// ── Structurally-verified categories ──
|
|
445
|
+
// After verifier hardening, these categories use entity names, cross-file
|
|
446
|
+
// checks, or raised confidence floors — not just confidence >= 0.3.
|
|
447
|
+
describe('structurally-verified categories', () => {
|
|
448
|
+
// Tier 1: Entity-name required — accepts when entity name matches
|
|
449
|
+
it('should accept feature_envy when entity name exists in file', () => {
|
|
450
|
+
const findings = [makeFinding({
|
|
451
|
+
category: 'feature_envy',
|
|
452
|
+
description: 'The processData function accesses too many external modules',
|
|
453
|
+
confidence: 0.5,
|
|
454
|
+
})];
|
|
455
|
+
const facts = [makeFileFacts()];
|
|
456
|
+
const result = verifyFindings(findings, facts);
|
|
457
|
+
expect(result).toHaveLength(1);
|
|
458
|
+
});
|
|
459
|
+
it('should reject feature_envy with no entity name and low confidence', () => {
|
|
460
|
+
const findings = [makeFinding({
|
|
461
|
+
category: 'feature_envy',
|
|
462
|
+
description: 'Some function accesses external modules',
|
|
463
|
+
confidence: 0.3,
|
|
464
|
+
})];
|
|
465
|
+
const facts = [makeFileFacts()];
|
|
466
|
+
const result = verifyFindings(findings, facts);
|
|
467
|
+
expect(result).toHaveLength(0); // No entity name → needs confidence >= 0.6
|
|
468
|
+
});
|
|
469
|
+
// Tier 2: dead_code — needs entity unreferenced externally
|
|
470
|
+
it('should accept dead_code when entity is unreferenced', () => {
|
|
471
|
+
const findings = [makeFinding({
|
|
472
|
+
category: 'dead_code',
|
|
473
|
+
description: 'processData is never called',
|
|
474
|
+
confidence: 0.5,
|
|
475
|
+
})];
|
|
476
|
+
// Only one file — processData can't be imported by other files
|
|
477
|
+
const facts = [makeFileFacts()];
|
|
478
|
+
const result = verifyFindings(findings, facts);
|
|
479
|
+
expect(result).toHaveLength(1);
|
|
480
|
+
});
|
|
481
|
+
it('should reject dead_code when entity is imported by another file', () => {
|
|
482
|
+
const findings = [makeFinding({
|
|
483
|
+
category: 'dead_code',
|
|
484
|
+
description: 'processData is never called',
|
|
485
|
+
confidence: 0.9,
|
|
486
|
+
})];
|
|
487
|
+
const facts = [
|
|
488
|
+
makeFileFacts(),
|
|
489
|
+
makeFileFacts({
|
|
490
|
+
path: 'src/consumer.ts',
|
|
491
|
+
imports: ['./service', 'processData'], // References the entity
|
|
492
|
+
}),
|
|
493
|
+
];
|
|
494
|
+
const result = verifyFindings(findings, facts);
|
|
495
|
+
expect(result).toHaveLength(0); // Entity is referenced — not dead code
|
|
496
|
+
});
|
|
497
|
+
// Tier 2: naming_convention — checks actual language rules
|
|
498
|
+
it('should accept naming_convention when name violates rules', () => {
|
|
499
|
+
const findings = [makeFinding({
|
|
500
|
+
category: 'naming_convention',
|
|
501
|
+
file: 'pkg/utils.go',
|
|
502
|
+
description: 'get_user_name uses snake_case instead of MixedCaps',
|
|
503
|
+
confidence: 0.5,
|
|
504
|
+
})];
|
|
505
|
+
const facts = [makeFileFacts({
|
|
506
|
+
path: 'pkg/utils.go',
|
|
507
|
+
language: 'go',
|
|
508
|
+
functions: [{
|
|
509
|
+
name: 'get_user_name',
|
|
510
|
+
lineStart: 10, lineEnd: 30, lineCount: 20,
|
|
511
|
+
paramCount: 1, params: ['id'],
|
|
512
|
+
maxNesting: 1, hasReturn: true, isAsync: false, isExported: false,
|
|
513
|
+
}],
|
|
514
|
+
})];
|
|
515
|
+
const result = verifyFindings(findings, facts);
|
|
516
|
+
expect(result).toHaveLength(1);
|
|
517
|
+
expect(result[0].verified).toBe(true);
|
|
518
|
+
});
|
|
519
|
+
it('should reject naming_convention when name follows rules', () => {
|
|
520
|
+
const findings = [makeFinding({
|
|
521
|
+
category: 'naming_convention',
|
|
522
|
+
description: 'processData naming issue',
|
|
523
|
+
confidence: 0.8,
|
|
524
|
+
})];
|
|
525
|
+
const facts = [makeFileFacts()]; // processData is camelCase — correct for TS
|
|
526
|
+
const result = verifyFindings(findings, facts);
|
|
527
|
+
expect(result).toHaveLength(0); // Name follows conventions → FP
|
|
528
|
+
});
|
|
529
|
+
// Tier 2: performance — needs non-trivial function
|
|
530
|
+
it('should accept performance when function is substantial', () => {
|
|
531
|
+
const findings = [makeFinding({
|
|
532
|
+
category: 'performance',
|
|
533
|
+
description: 'processData has O(n²) complexity',
|
|
534
|
+
confidence: 0.5,
|
|
535
|
+
})];
|
|
536
|
+
const facts = [makeFileFacts()]; // processData is 70 lines
|
|
537
|
+
const result = verifyFindings(findings, facts);
|
|
538
|
+
expect(result).toHaveLength(1);
|
|
539
|
+
});
|
|
540
|
+
// Tier 3: dry_violation — needs cross-file similarity
|
|
541
|
+
it('should accept dry_violation when similar function exists in another file', () => {
|
|
542
|
+
const findings = [makeFinding({
|
|
543
|
+
category: 'dry_violation',
|
|
544
|
+
description: 'processData is duplicated across files',
|
|
545
|
+
confidence: 0.5,
|
|
546
|
+
})];
|
|
547
|
+
const facts = [
|
|
548
|
+
makeFileFacts(),
|
|
549
|
+
makeFileFacts({
|
|
550
|
+
path: 'src/other.ts',
|
|
551
|
+
functions: [{
|
|
552
|
+
name: 'processData', // Same name = similar
|
|
553
|
+
lineStart: 10, lineEnd: 80, lineCount: 70,
|
|
554
|
+
paramCount: 5, params: ['a', 'b', 'c', 'd', 'e'],
|
|
555
|
+
maxNesting: 3, hasReturn: true, isAsync: true, isExported: true,
|
|
556
|
+
}],
|
|
557
|
+
}),
|
|
558
|
+
];
|
|
559
|
+
const result = verifyFindings(findings, facts);
|
|
560
|
+
expect(result).toHaveLength(1);
|
|
561
|
+
});
|
|
562
|
+
it('should reject dry_violation when no similar function exists', () => {
|
|
563
|
+
const findings = [makeFinding({
|
|
564
|
+
category: 'dry_violation',
|
|
565
|
+
description: 'processData is duplicated',
|
|
566
|
+
confidence: 0.5,
|
|
567
|
+
})];
|
|
568
|
+
const facts = [makeFileFacts()]; // Only one file — no cross-file match
|
|
569
|
+
const result = verifyFindings(findings, facts);
|
|
570
|
+
expect(result).toHaveLength(0);
|
|
571
|
+
});
|
|
572
|
+
// Tier 4: Confidence floor raised to 0.5
|
|
573
|
+
it('should accept architecture with confidence >= 0.5', () => {
|
|
574
|
+
const findings = [makeFinding({ category: 'architecture', confidence: 0.6 })];
|
|
575
|
+
const facts = [makeFileFacts()];
|
|
576
|
+
const result = verifyFindings(findings, facts);
|
|
577
|
+
expect(result).toHaveLength(1);
|
|
578
|
+
});
|
|
579
|
+
it('should reject architecture with confidence < 0.5', () => {
|
|
580
|
+
const findings = [makeFinding({ category: 'architecture', confidence: 0.4 })];
|
|
581
|
+
const facts = [makeFileFacts()];
|
|
582
|
+
const result = verifyFindings(findings, facts);
|
|
583
|
+
expect(result).toHaveLength(0);
|
|
584
|
+
});
|
|
464
585
|
});
|
|
465
586
|
// ── Resource leak (Go-specific) ──
|
|
466
587
|
describe('resource leak verification', () => {
|
|
@@ -502,13 +623,13 @@ describe('Verifier', () => {
|
|
|
502
623
|
makeFinding({ category: 'god_function', file: 'nonexistent.ts' }), // Should fail (no file)
|
|
503
624
|
makeFinding({ category: 'long_file' }), // Should fail (300 lines, need >300)
|
|
504
625
|
makeFinding({ category: 'magic_number' }), // Should fail (no magicNumbers set)
|
|
505
|
-
makeFinding({ category: '
|
|
506
|
-
makeFinding({ category: '
|
|
626
|
+
makeFinding({ category: 'architecture', confidence: 0.1 }), // Should fail (below 0.5 floor)
|
|
627
|
+
makeFinding({ category: 'architecture', confidence: 0.6 }), // Should pass (above 0.5 floor)
|
|
507
628
|
];
|
|
508
629
|
const facts = [makeFileFacts()];
|
|
509
630
|
const result = verifyFindings(findings, facts);
|
|
510
631
|
const verified = result.filter(r => r.verified);
|
|
511
|
-
expect(verified.length).toBeGreaterThanOrEqual(2); // god_class,
|
|
632
|
+
expect(verified.length).toBeGreaterThanOrEqual(2); // god_class, architecture(0.6)
|
|
512
633
|
});
|
|
513
634
|
});
|
|
514
635
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.3",
|
|
4
4
|
"description": "Deterministic quality gate engine for AI-generated code. AST analysis, drift detection, and Fix Packet generation across TypeScript, JavaScript, Python, Go, Ruby, and C#.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://rigour.run",
|
|
@@ -59,11 +59,11 @@
|
|
|
59
59
|
"@xenova/transformers": "^2.17.2",
|
|
60
60
|
"better-sqlite3": "^11.0.0",
|
|
61
61
|
"openai": "^4.104.0",
|
|
62
|
-
"@rigour-labs/brain-darwin-
|
|
63
|
-
"@rigour-labs/brain-
|
|
64
|
-
"@rigour-labs/brain-linux-
|
|
65
|
-
"@rigour-labs/brain-
|
|
66
|
-
"@rigour-labs/brain-win-x64": "4.2.
|
|
62
|
+
"@rigour-labs/brain-darwin-arm64": "4.2.3",
|
|
63
|
+
"@rigour-labs/brain-darwin-x64": "4.2.3",
|
|
64
|
+
"@rigour-labs/brain-linux-arm64": "4.2.3",
|
|
65
|
+
"@rigour-labs/brain-linux-x64": "4.2.3",
|
|
66
|
+
"@rigour-labs/brain-win-x64": "4.2.3"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@types/better-sqlite3": "^7.6.12",
|