diff --git a/src/arbiter/certify.py b/src/arbiter/certify.py index 19cc002..70d043d 100644 --- a/src/arbiter/certify.py +++ b/src/arbiter/certify.py @@ -10,7 +10,6 @@ from __future__ import annotations from dataclasses import dataclass, field -from pathlib import Path from arbiter.analyzers.base import Finding from arbiter.scoring import RepoScore @@ -68,27 +67,43 @@ def certify( """Run full certification assessment. Weights: code quality 50%, governance 30%, dependencies 20%. + When code is unscorable (no Python LOC, analyzers not installed), + reweight to governance 60% + dependencies 40% instead of penalizing. """ - code = code_score.overall if code_score.is_scorable else 0 + code = code_score.overall if code_score.is_scorable else None gov = governance_report.score deps = dep_report.score - overall = code * 0.50 + gov * 0.30 + deps * 0.20 + if code is not None: + overall = code * 0.50 + gov * 0.30 + deps * 0.20 + else: + # Reweight: skip code dimension entirely + overall = gov * 0.60 + deps * 0.40 + code = 0 # for display purposes + overall = round(overall, 1) reasons = [] + code_scorable = code_score.is_scorable # Determine decision cert = CERT_THRESHOLDS["certified"] prov = CERT_THRESHOLDS["provisional"] - if (overall >= cert["overall"] and code >= cert["code"] + # Skip code threshold checks when code is unscorable + code_meets_cert = code >= cert["code"] if code_scorable else True + code_meets_prov = code >= prov["code"] if code_scorable else True + + if not code_scorable: + reasons.append("Code quality not scored (no supported analyzers for primary language)") + + if (overall >= cert["overall"] and code_meets_cert and gov >= cert["governance"] and deps >= cert["deps"]): decision = "CERTIFIED" - elif (overall >= prov["overall"] and code >= prov["code"] + elif (overall >= prov["overall"] and code_meets_prov and gov >= prov["governance"] and deps >= prov["deps"]): decision = "PROVISIONAL" - if code < cert["code"]: + if code_scorable and code < cert["code"]: reasons.append(f"Code score {code:.1f} below certified threshold ({cert['code']})") if gov < cert["governance"]: reasons.append(f"Governance score {gov:.1f} below certified threshold ({cert['governance']})") @@ -96,7 +111,7 @@ def certify( reasons.append(f"Dependency score {deps:.1f} below certified threshold ({cert['deps']})") else: decision = "FAILED" - if code < prov["code"]: + if code_scorable and code < prov["code"]: reasons.append(f"Code score {code:.1f} below minimum threshold ({prov['code']})") if gov < prov["governance"]: reasons.append(f"Governance score {gov:.1f} below minimum threshold ({prov['governance']})") diff --git a/tests/test_certify.py b/tests/test_certify.py index 90766f5..f231486 100644 --- a/tests/test_certify.py +++ b/tests/test_certify.py @@ -61,9 +61,17 @@ def test_governance_checks_counted(self): assert result.governance_checks_passed == 7 assert result.governance_checks_total == 10 - def test_non_scorable_code(self): + def test_non_scorable_code_reweights(self): + """Non-scorable code should reweight to governance 60% + deps 40%, not fail.""" unscorable = RepoScore(overall=0, lint_score=0, security_score=0, complexity_score=0, total_findings=0, is_scorable=False) result = certify(unscorable, _FakeGovReport(80), _FakeDepReport(85), []) - assert result.code_score == 0 - assert result.decision == "FAILED" + # Reweighted: 80*0.6 + 85*0.4 = 48+34 = 82 + assert result.overall >= 80 + assert result.decision == "CERTIFIED" # governance + deps are strong enough + + def test_non_scorable_notes_reason(self): + unscorable = RepoScore(overall=0, lint_score=0, security_score=0, + complexity_score=0, total_findings=0, is_scorable=False) + result = certify(unscorable, _FakeGovReport(80), _FakeDepReport(85), []) + assert any("not scored" in r for r in result.reasons)