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
28 changes: 25 additions & 3 deletions src/excel_all_repos_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from openpyxl.utils import get_column_letter
from openpyxl.worksheet.datavalidation import DataValidation
from src.scoring_dimensions import display_dimension
from src.scorer import PARTIAL_RUN_BASIS_THRESHOLD, WEIGHTS

ALL_REPOS_HEADERS = [
"Repo",
Expand Down Expand Up @@ -52,6 +53,8 @@
"Size (KB)",
"Trend",
"Risk Tier",
"Scored Dimensions",
"Scored Weight Sum",
]

ALL_REPOS_LONG_TEXT_HEADERS = ("Description", "Topics", "Badges")
Expand All @@ -60,7 +63,7 @@
ALL_REPOS_INTEREST_COLUMN = 4
ALL_REPOS_TIER_COLUMN = 7
ALL_REPOS_PATTERN_COLUMN = 12
ALL_REPOS_TREND_COLUMN_OFFSET = 1
ALL_REPOS_TREND_COLUMN_OFFSET = 3
ALL_REPOS_LONG_TEXT_WIDTH = 60


Expand Down Expand Up @@ -101,7 +104,7 @@ def build_all_repo_rows(
"commit_pattern": activity.get("commit_pattern", ""),
"values": [
repo_name,
audit.get("grade", "F"),
_build_display_grade(audit),
round(audit.get("overall_score", 0), 3),
round(audit.get("interest_score", 0), 3),
audit.get("interest_grade", "—"),
Expand Down Expand Up @@ -144,6 +147,8 @@ def build_all_repo_rows(
metadata.get("size_kb", 0),
render_sparkline(trend_scores),
(risk_lookup or {}).get(str(repo_name), ""),
", ".join(audit.get("scored_dimensions", [])),
round(audit.get("scored_weight_sum", 0), 3),
],
}
)
Expand All @@ -162,13 +167,30 @@ def _build_biggest_drag(audit: dict[str, Any]) -> str:
return f"{display_dimension(worst_dimension)} ({dimension_scores[worst_dimension]:.1f})"


def _build_display_grade(audit: dict[str, Any]) -> str:
"""Render partial-run basis disclosure without changing the model grade."""
grade = str(audit.get("grade", "F"))
if " on " in grade:
return grade

scored_dimensions = audit.get("scored_dimensions") or []
scored_weight_sum = audit.get("scored_weight_sum")
if (
scored_dimensions
and isinstance(scored_weight_sum, (int, float))
and scored_weight_sum < sum(WEIGHTS.values()) * PARTIAL_RUN_BASIS_THRESHOLD
):
return f"{grade} on {len(scored_dimensions)}/{len(WEIGHTS)} dimensions"
return grade


def _build_grade_reason(audit: dict[str, Any]) -> str:
dimension_scores = {
result["dimension"]: result["score"]
for result in audit.get("analyzer_results", [])
if result["dimension"] != "interest"
}
grade = audit.get("grade", "F")
grade = _build_display_grade(audit)
if not dimension_scores:
return grade
weakest_dimensions = sorted(dimension_scores.items(), key=lambda item: item[1])[:2]
Expand Down
2 changes: 1 addition & 1 deletion src/excel_dashboard_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,7 @@ def _write_dashboard_distribution_charts(

grade_dist = {}
for audit in audits:
grade = audit.get("grade", "F")
grade = audit.get("grade", "F").split(" ", 1)[0]
grade_dist[grade] = grade_dist.get(grade, 0) + 1
grade_label_col = 27
grade_value_col = 28
Expand Down
5 changes: 3 additions & 2 deletions src/excel_styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,9 @@ def write_kpi_card(

def color_grade_cell(cell, grade: str) -> None:
"""Apply grade-specific coloring to a cell."""
if grade in GRADE_FILLS:
cell.fill = GRADE_FILLS[grade]
grade_letter = grade.split(" ", 1)[0]
if grade_letter in GRADE_FILLS:
cell.fill = GRADE_FILLS[grade_letter]
cell.font = Font("Calibri", 10, bold=True, color=WHITE)


Expand Down
4 changes: 4 additions & 0 deletions src/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ class RepoAudit:
interest_score: float = 0.0
interest_tier: str = "mundane"
grade: str = "F"
scored_dimensions: list[str] = field(default_factory=list)
scored_weight_sum: float = 0.0
interest_grade: str = "F"
badges: list[str] = field(default_factory=list)
next_badges: list[dict] = field(default_factory=list)
Expand All @@ -122,6 +124,8 @@ def to_dict(self) -> dict:
"completeness_tier": self.completeness_tier,
"interest_tier": self.interest_tier,
"grade": self.grade,
"scored_dimensions": self.scored_dimensions,
"scored_weight_sum": round(self.scored_weight_sum, 3),
"interest_grade": self.interest_grade,
"badges": self.badges,
"next_badges": self.next_badges,
Expand Down
2 changes: 2 additions & 0 deletions src/report_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ def audit_from_dict(data: dict) -> RepoAudit:
interest_score=data.get("interest_score", 0),
interest_tier=data.get("interest_tier", "mundane"),
grade=data.get("grade", "F"), interest_grade=data.get("interest_grade", "F"),
scored_dimensions=data.get("scored_dimensions", []),
scored_weight_sum=data.get("scored_weight_sum", 0),
badges=data.get("badges", []), next_badges=data.get("next_badges", []),
flags=data.get("flags", []), lenses=data.get("lenses", {}), hotspots=data.get("hotspots", []),
action_candidates=data.get("action_candidates", []), security_posture=data.get("security_posture", {}),
Expand Down
8 changes: 7 additions & 1 deletion src/scorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
STALE_THRESHOLD_DAYS = 730 # 2 years

GRADE_THRESHOLDS = [(0.80, "A"), (0.70, "B"), (0.55, "C"), (0.35, "D"), (0.0, "F")]
PARTIAL_RUN_BASIS_THRESHOLD = 0.85

def letter_grade(
score: float, grade_thresholds: list[tuple[float, str]] | None = None
Expand Down Expand Up @@ -105,6 +106,7 @@ def score_repo(
weight_sum += weight

overall_score = weighted_sum / weight_sum if weight_sum > 0 else 0.0
scored_dimensions = [dimension for dimension in weights if dimension in score_map]

# Compute interest score (separate axis, from interest analyzer)
interest_score = score_map.get("interest", 0.0)
Expand Down Expand Up @@ -166,14 +168,18 @@ def score_repo(
tier = "skeleton"
flags.append("readme-only")

grade = letter_grade(overall_score, grade_thresholds)

audit = RepoAudit(
metadata=metadata,
analyzer_results=results,
overall_score=overall_score,
interest_score=interest_score,
completeness_tier=tier,
interest_tier=interest_tier,
grade=letter_grade(overall_score, grade_thresholds),
grade=grade,
scored_dimensions=scored_dimensions,
scored_weight_sum=weight_sum,
interest_grade=letter_grade(interest_score, grade_thresholds),
flags=flags,
)
Expand Down
30 changes: 26 additions & 4 deletions src/web_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
no_baseline_summary,
no_linked_artifact_summary,
)
from src.scorer import PARTIAL_RUN_BASIS_THRESHOLD, WEIGHTS
from src.sparkline import sparkline as render_sparkline
from src.terminology import ACTION_SYNC_CANONICAL_LABELS
from src.weekly_scheduling_overlay import resolve_weekly_story_value
Expand Down Expand Up @@ -180,6 +181,8 @@ def _render_html(
{
"name": entry["name"],
"grade": a.get("grade", "F"),
"scored_dimensions": a.get("scored_dimensions", []),
"scored_weight_sum": a.get("scored_weight_sum", 0),
"score": round(a.get("overall_score", 0), 3),
"interest": round(a.get("interest_score", 0), 3),
"tier": a.get("completeness_tier", ""),
Expand Down Expand Up @@ -243,14 +246,31 @@ def _render_html(

# ── KPI and header sections ───────────────────────────────────────────
def _header_section(username: str, date: str, repos: int, grade: str) -> str:
color = GRADE_COLORS_CSS.get(grade, "#6B7280")
color = GRADE_COLORS_CSS.get(grade.split(" ", 1)[0], "#6B7280")
return f"""
<header>
<h1>Portfolio Dashboard: {escape(username)}</h1>
<p>Generated {escape(date)} | {repos} repos audited | Grade <span style="color:{color};font-weight:bold">{escape(grade)}</span></p>
</header>"""


def _build_display_grade(audit: dict) -> str:
"""Render partial-run basis disclosure without changing the model grade."""
grade = str(audit.get("grade", "F"))
if " on " in grade:
return grade

scored_dimensions = audit.get("scored_dimensions") or []
scored_weight_sum = audit.get("scored_weight_sum")
if (
scored_dimensions
and isinstance(scored_weight_sum, (int, float))
and scored_weight_sum < sum(WEIGHTS.values()) * PARTIAL_RUN_BASIS_THRESHOLD
):
return f"{grade} on {len(scored_dimensions)}/{len(WEIGHTS)} dimensions"
return grade


def _kpi_section(data: dict) -> str:
tiers = data.get("tier_distribution", {})
avg = data.get("average_score", 0)
Expand Down Expand Up @@ -923,14 +943,16 @@ def _repo_table(
explanation = a.get("score_explanation") or build_score_explanation(a) or {}
name = m.get("name", "")
grade = a.get("grade", "F")
grade_letter = grade.split(" ", 1)[0]
display_grade = _build_display_grade(a)
score = a.get("overall_score", 0)
interest = a.get("interest_score", 0)
profile_score = entry["profile_score"]
tier = a.get("completeness_tier", "")
lang = m.get("language") or ""
desc = (m.get("description") or "")[:60]
collections = ", ".join(entry["collections"])
gc = GRADE_COLORS_CSS.get(grade, "#6B7280")
gc = GRADE_COLORS_CSS.get(grade_letter, "#6B7280")
tc = TIER_COLORS_CSS.get(tier, "#6B7280")

spark = ""
Expand Down Expand Up @@ -974,14 +996,14 @@ def _repo_table(
rows.append(
f'<tr data-tier="{escape(tier, quote=True)}" '
f'data-risk="{escape(risk_tier, quote=True)}" '
f'data-grade="{escape(grade, quote=True)}" '
f'data-grade="{escape(grade_letter, quote=True)}" '
f'data-name="{escape(name, quote=True)}" '
f'data-collections="{escape(collections.lower(), quote=True)}" '
f'data-overall="{score:.3f}" '
f'data-profile="{profile_score:.3f}">'
f"<td>{link}</td>"
f'<td class="num">{profile_score:.3f}</td>'
f'<td style="color:{gc};font-weight:bold;text-align:center">{escape(grade)}</td>'
f'<td style="color:{gc};font-weight:bold;text-align:center">{escape(display_grade)}</td>'
f'<td class="num">{score:.3f}</td>'
f'<td class="num">{interest:.3f}</td>'
f'<td style="color:{tc};font-weight:bold">{safe_tier}</td>'
Expand Down
62 changes: 61 additions & 1 deletion tests/test_cli_hardening.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
from src.baseline_context import build_baseline_context
from src.models import AnalyzerResult, AuditReport, RepoAudit
from src.report_scorecards import apply_scorecards
from src.report_state import report_from_dict
from src.report_state import audit_from_dict, report_from_dict
from src.scorer import WEIGHTS


def _make_args(**overrides) -> Namespace:
Expand Down Expand Up @@ -1738,3 +1739,62 @@ def _record_score_repo(*a, **kwargs):

assert captured[0]["scorecard_enabled"] is True
assert captured[0]["security_offline"] is True


def test_analyze_repos_forwards_real_scoring_profile_overrides(monkeypatch, sample_metadata):
args = _make_args()
captured: list[dict[str, object]] = []

class _CloneContext:
def __enter__(self):
return {sample_metadata.name: Path("/tmp/repo")}

def __exit__(self, exc_type, exc, tb):
return False

monkeypatch.setattr(run_audit, "clone_workspace", lambda *a, **k: _CloneContext())
monkeypatch.setattr(run_audit, "create_progress", lambda: None)
monkeypatch.setattr(run_audit, "run_all_analyzers", lambda *a, **k: [])

def _record_score_repo(*a, **kwargs):
captured.append(kwargs)
return RepoAudit(
metadata=sample_metadata,
analyzer_results=[],
overall_score=0.5,
completeness_tier="functional",
)

monkeypatch.setattr(run_audit, "score_repo", _record_score_repo)
profile = run_audit.ScoringProfile(
dict(WEIGHTS), overrides={"stale_threshold_days": 1}
)

run_audit._analyze_repos(
[sample_metadata],
args=args,
client=run_audit.GitHubClient(token=None, cache=None),
portfolio_lang_freq={},
custom_weights=profile,
)

assert captured[0]["scoring_profile"] == profile.overrides


def test_partial_run_basis_survives_report_readback(sample_metadata):
audit = RepoAudit(
metadata=sample_metadata,
analyzer_results=[],
overall_score=0.72,
completeness_tier="functional",
grade="B",
scored_dimensions=["readme", "testing"],
scored_weight_sum=0.30,
)

restored = audit_from_dict(audit.to_dict())

assert restored.grade == "B"
assert restored.to_dict()["grade"] == "B"
assert restored.scored_dimensions == ["readme", "testing"]
assert restored.scored_weight_sum == 0.30
38 changes: 38 additions & 0 deletions tests/test_excel_enhanced.py
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,44 @@ def test_headers_include_interest_breakdown(self):
for label in ("Tech Novelty", "Burst", "Ambition", "Storytelling"):
assert label in header_values, f"Missing header: {label}"

def test_partial_run_basis_is_exported(self):
report = _make_report()
report["audits"][0].update(
{
"grade": "B",
"scored_dimensions": ["readme", "testing"],
"scored_weight_sum": 0.3,
}
)
wb = Workbook()
_build_all_repos(wb, report)
ws = wb["All Repos"]
headers = {ws.cell(row=1, column=column).value: column for column in range(1, 50)}
repo_row = next(row for row in range(2, 10) if ws.cell(row=row, column=1).value == "RepoA")

assert "Scored Dimensions" in headers
assert "Scored Weight Sum" in headers
assert ws.cell(row=repo_row, column=headers["Grade"]).value == "B on 2/10 dimensions"
assert ws.cell(row=repo_row, column=headers["Scored Dimensions"]).value == "readme, testing"
assert ws.cell(row=repo_row, column=headers["Scored Weight Sum"]).value == 0.3

def test_dashboard_grade_distribution_uses_qualified_grade_letter(self):
report = _make_report()
report["audits"][0].update(
{
"grade": "B",
"scored_dimensions": ["readme", "testing"],
"scored_weight_sum": 0.3,
}
)

wb = Workbook()
_build_dashboard(wb, report)
ws = wb["Dashboard"]

assert ws.cell(row=11, column=27).value == "B"
assert ws.cell(row=11, column=28).value == 1

def test_trend_is_last_column(self):
ws = self._build()
# Trend is dynamically the last header column (currently 36)
Expand Down
22 changes: 22 additions & 0 deletions tests/test_scoring_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,28 @@ def test_custom_weights_none_uses_default(self):
audit_default = score_repo(_make_metadata(), results)
assert audit_none.overall_score == audit_default.overall_score

def test_partial_run_grade_discloses_scored_basis(self):
scored_dimensions = list(WEIGHTS)[:6] + ["documentation"]
results = _make_results({dim: 0.72 for dim in scored_dimensions})

audit = score_repo(_make_metadata(), results)

assert audit.grade == "B"
assert audit.scored_dimensions == scored_dimensions
assert audit.scored_weight_sum == 0.75
assert audit.to_dict()["scored_dimensions"] == scored_dimensions
assert audit.to_dict()["scored_weight_sum"] == 0.75
assert audit.to_dict()["grade"] == "B"

def test_full_run_grade_remains_unqualified(self):
results = _make_results({dim: 0.72 for dim in WEIGHTS})

audit = score_repo(_make_metadata(), results)

assert audit.grade == "B"
assert audit.scored_dimensions == list(WEIGHTS)
assert audit.scored_weight_sum == 1.0

def test_profile_json_valid(self, tmp_path):
"""Verify profile JSONs sum to ~1.0."""
from pathlib import Path
Expand Down
Loading