Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions src/arbiter/certify.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -68,35 +67,51 @@ 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']})")
if deps < cert["deps"]:
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']})")
Expand Down
14 changes: 11 additions & 3 deletions tests/test_certify.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading