Skip to content

Commit e9b6da0

Browse files
Claude (agent)claude
andcommitted
fix(certify): reweight when code is unscorable instead of auto-failing
Previously, repos with no Python LOC got code_score=0, causing automatic FAILED certification even for well-governed Go/Rust/TS repos. Now reweights to governance 60% + deps 40% when code scoring is unavailable, with a transparent note in reasons. Before: prometheus/prometheus → FAILED (47.0) After: prometheus/prometheus → CERTIFIED (94.0) Found by scanning 10 Go repos: all failed despite excellent governance because code scoring was Python-only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 581d68e commit e9b6da0

2 files changed

Lines changed: 33 additions & 10 deletions

File tree

src/arbiter/certify.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from __future__ import annotations
1111

1212
from dataclasses import dataclass, field
13-
from pathlib import Path
1413

1514
from arbiter.analyzers.base import Finding
1615
from arbiter.scoring import RepoScore
@@ -68,35 +67,51 @@ def certify(
6867
"""Run full certification assessment.
6968
7069
Weights: code quality 50%, governance 30%, dependencies 20%.
70+
When code is unscorable (no Python LOC, analyzers not installed),
71+
reweight to governance 60% + dependencies 40% instead of penalizing.
7172
"""
72-
code = code_score.overall if code_score.is_scorable else 0
73+
code = code_score.overall if code_score.is_scorable else None
7374
gov = governance_report.score
7475
deps = dep_report.score
7576

76-
overall = code * 0.50 + gov * 0.30 + deps * 0.20
77+
if code is not None:
78+
overall = code * 0.50 + gov * 0.30 + deps * 0.20
79+
else:
80+
# Reweight: skip code dimension entirely
81+
overall = gov * 0.60 + deps * 0.40
82+
code = 0 # for display purposes
83+
7784
overall = round(overall, 1)
7885

7986
reasons = []
87+
code_scorable = code_score.is_scorable
8088

8189
# Determine decision
8290
cert = CERT_THRESHOLDS["certified"]
8391
prov = CERT_THRESHOLDS["provisional"]
8492

85-
if (overall >= cert["overall"] and code >= cert["code"]
93+
# Skip code threshold checks when code is unscorable
94+
code_meets_cert = code >= cert["code"] if code_scorable else True
95+
code_meets_prov = code >= prov["code"] if code_scorable else True
96+
97+
if not code_scorable:
98+
reasons.append("Code quality not scored (no supported analyzers for primary language)")
99+
100+
if (overall >= cert["overall"] and code_meets_cert
86101
and gov >= cert["governance"] and deps >= cert["deps"]):
87102
decision = "CERTIFIED"
88-
elif (overall >= prov["overall"] and code >= prov["code"]
103+
elif (overall >= prov["overall"] and code_meets_prov
89104
and gov >= prov["governance"] and deps >= prov["deps"]):
90105
decision = "PROVISIONAL"
91-
if code < cert["code"]:
106+
if code_scorable and code < cert["code"]:
92107
reasons.append(f"Code score {code:.1f} below certified threshold ({cert['code']})")
93108
if gov < cert["governance"]:
94109
reasons.append(f"Governance score {gov:.1f} below certified threshold ({cert['governance']})")
95110
if deps < cert["deps"]:
96111
reasons.append(f"Dependency score {deps:.1f} below certified threshold ({cert['deps']})")
97112
else:
98113
decision = "FAILED"
99-
if code < prov["code"]:
114+
if code_scorable and code < prov["code"]:
100115
reasons.append(f"Code score {code:.1f} below minimum threshold ({prov['code']})")
101116
if gov < prov["governance"]:
102117
reasons.append(f"Governance score {gov:.1f} below minimum threshold ({prov['governance']})")

tests/test_certify.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,17 @@ def test_governance_checks_counted(self):
6161
assert result.governance_checks_passed == 7
6262
assert result.governance_checks_total == 10
6363

64-
def test_non_scorable_code(self):
64+
def test_non_scorable_code_reweights(self):
65+
"""Non-scorable code should reweight to governance 60% + deps 40%, not fail."""
6566
unscorable = RepoScore(overall=0, lint_score=0, security_score=0,
6667
complexity_score=0, total_findings=0, is_scorable=False)
6768
result = certify(unscorable, _FakeGovReport(80), _FakeDepReport(85), [])
68-
assert result.code_score == 0
69-
assert result.decision == "FAILED"
69+
# Reweighted: 80*0.6 + 85*0.4 = 48+34 = 82
70+
assert result.overall >= 80
71+
assert result.decision == "CERTIFIED" # governance + deps are strong enough
72+
73+
def test_non_scorable_notes_reason(self):
74+
unscorable = RepoScore(overall=0, lint_score=0, security_score=0,
75+
complexity_score=0, total_findings=0, is_scorable=False)
76+
result = certify(unscorable, _FakeGovReport(80), _FakeDepReport(85), [])
77+
assert any("not scored" in r for r in result.reasons)

0 commit comments

Comments
 (0)