diff --git a/src/excel_all_repos_helpers.py b/src/excel_all_repos_helpers.py
index 2e57917..d9bd09f 100644
--- a/src/excel_all_repos_helpers.py
+++ b/src/excel_all_repos_helpers.py
@@ -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",
@@ -52,6 +53,8 @@
"Size (KB)",
"Trend",
"Risk Tier",
+ "Scored Dimensions",
+ "Scored Weight Sum",
]
ALL_REPOS_LONG_TEXT_HEADERS = ("Description", "Topics", "Badges")
@@ -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
@@ -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", "—"),
@@ -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),
],
}
)
@@ -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]
diff --git a/src/excel_dashboard_helpers.py b/src/excel_dashboard_helpers.py
index 53cb3c7..b5b58da 100644
--- a/src/excel_dashboard_helpers.py
+++ b/src/excel_dashboard_helpers.py
@@ -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
diff --git a/src/excel_styles.py b/src/excel_styles.py
index ea4ca43..0384f06 100644
--- a/src/excel_styles.py
+++ b/src/excel_styles.py
@@ -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)
diff --git a/src/models.py b/src/models.py
index 762f499..46b81f2 100644
--- a/src/models.py
+++ b/src/models.py
@@ -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)
@@ -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,
diff --git a/src/report_state.py b/src/report_state.py
index 9757c80..fbb43e9 100644
--- a/src/report_state.py
+++ b/src/report_state.py
@@ -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", {}),
diff --git a/src/scorer.py b/src/scorer.py
index 78c5a64..649f1d9 100644
--- a/src/scorer.py
+++ b/src/scorer.py
@@ -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
@@ -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)
@@ -166,6 +168,8 @@ def score_repo(
tier = "skeleton"
flags.append("readme-only")
+ grade = letter_grade(overall_score, grade_thresholds)
+
audit = RepoAudit(
metadata=metadata,
analyzer_results=results,
@@ -173,7 +177,9 @@ def score_repo(
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,
)
diff --git a/src/web_export.py b/src/web_export.py
index e00c562..3cacce8 100644
--- a/src/web_export.py
+++ b/src/web_export.py
@@ -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
@@ -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", ""),
@@ -243,7 +246,7 @@ 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"""
Portfolio Dashboard: {escape(username)}
@@ -251,6 +254,23 @@ def _header_section(username: str, date: str, repos: int, grade: str) -> str: