diff --git a/src/analyzers/activity.py b/src/analyzers/activity.py index 356bdd8..694b7e5 100644 --- a/src/analyzers/activity.py +++ b/src/analyzers/activity.py @@ -17,7 +17,6 @@ class ActivityAnalyzer(BaseAnalyzer): name = "activity" - weight = 0.15 def analyze( self, diff --git a/src/analyzers/base.py b/src/analyzers/base.py index 149c025..e0269bf 100644 --- a/src/analyzers/base.py +++ b/src/analyzers/base.py @@ -14,7 +14,6 @@ class BaseAnalyzer(ABC): """Abstract base for all repo analyzers.""" name: str - weight: float @abstractmethod def analyze( diff --git a/src/analyzers/cicd.py b/src/analyzers/cicd.py index 1acf5fd..71a3efa 100644 --- a/src/analyzers/cicd.py +++ b/src/analyzers/cicd.py @@ -22,7 +22,6 @@ class CicdAnalyzer(BaseAnalyzer): name = "cicd" - weight = 0.10 def analyze( self, diff --git a/src/analyzers/code_quality.py b/src/analyzers/code_quality.py index 37b01be..69dff68 100644 --- a/src/analyzers/code_quality.py +++ b/src/analyzers/code_quality.py @@ -50,7 +50,6 @@ class CodeQualityAnalyzer(BaseAnalyzer): name = "code_quality" - weight = 0.15 def analyze( self, diff --git a/src/analyzers/community_profile.py b/src/analyzers/community_profile.py index 7e1b132..331871f 100644 --- a/src/analyzers/community_profile.py +++ b/src/analyzers/community_profile.py @@ -32,7 +32,6 @@ class CommunityProfileAnalyzer(BaseAnalyzer): """ name = "community_profile" - weight = 0.03 def analyze( self, diff --git a/src/analyzers/completeness.py b/src/analyzers/completeness.py index 220b974..336fb54 100644 --- a/src/analyzers/completeness.py +++ b/src/analyzers/completeness.py @@ -52,7 +52,6 @@ class DocumentationAnalyzer(BaseAnalyzer): name = "documentation" - weight = 0.05 def analyze( self, @@ -108,7 +107,6 @@ def analyze( class BuildReadinessAnalyzer(BaseAnalyzer): name = "build_readiness" - weight = 0.05 def analyze( self, diff --git a/src/analyzers/dependencies.py b/src/analyzers/dependencies.py index 5d14f0c..b54a599 100644 --- a/src/analyzers/dependencies.py +++ b/src/analyzers/dependencies.py @@ -41,7 +41,6 @@ class DependenciesAnalyzer(BaseAnalyzer): name = "dependencies" - weight = 0.10 def cache_inputs_hash( self, diff --git a/src/analyzers/interest.py b/src/analyzers/interest.py index 435db32..5dbe3ce 100644 --- a/src/analyzers/interest.py +++ b/src/analyzers/interest.py @@ -35,7 +35,6 @@ class InterestAnalyzer(BaseAnalyzer): """ name = "interest" - weight = 0.0 # Not part of completeness score — separate axis def analyze( self, diff --git a/src/analyzers/readme.py b/src/analyzers/readme.py index 3ee9ab9..0ff5370 100644 --- a/src/analyzers/readme.py +++ b/src/analyzers/readme.py @@ -36,7 +36,6 @@ class ReadmeAnalyzer(BaseAnalyzer): name = "readme" - weight = 0.15 def cache_inputs_hash( self, diff --git a/src/analyzers/security.py b/src/analyzers/security.py index ee07d62..6474789 100644 --- a/src/analyzers/security.py +++ b/src/analyzers/security.py @@ -112,7 +112,6 @@ class SecurityAnalyzer(BaseAnalyzer): """Scans for security surface issues — exposed secrets, dangerous files, missing config.""" name = "security" - weight = 0.0 # Advisory dimension, not part of completeness score def analyze( self, diff --git a/src/analyzers/structure.py b/src/analyzers/structure.py index 8065c8a..02af1a1 100644 --- a/src/analyzers/structure.py +++ b/src/analyzers/structure.py @@ -42,7 +42,6 @@ class StructureAnalyzer(BaseAnalyzer): name = "structure" - weight = 0.10 def cache_inputs_hash( self, diff --git a/src/analyzers/testing.py b/src/analyzers/testing.py index a1566a8..1556408 100644 --- a/src/analyzers/testing.py +++ b/src/analyzers/testing.py @@ -21,7 +21,6 @@ class TestingAnalyzer(BaseAnalyzer): name = "testing" - weight = 0.15 def analyze( self, diff --git a/src/app/run_audit.py b/src/app/run_audit.py index e8505b0..be0b755 100644 --- a/src/app/run_audit.py +++ b/src/app/run_audit.py @@ -53,6 +53,17 @@ DEFAULT_ANALYSIS_WORKERS = 1 MAX_ANALYSIS_WORKERS = 8 DEFAULT_PORTFOLIO_WORKSPACE = Path.home() / "Projects" +_SCORING_PROFILE_RESERVED_KEYS = frozenset( + {"stale_threshold_days", "grade_thresholds", "completeness_tiers"} +) + + +class ScoringProfile(dict[str, float]): + """Flat profile weights plus optional scoring-constant overrides.""" + + def __init__(self, weights: dict[str, float], *, overrides: dict[str, object]) -> None: + super().__init__(weights) + self.overrides = overrides CLI_MODE_GUIDE = """GitHub portfolio operating system with four product modes: First Run setup, baseline creation, first workbook, first control-center read Weekly Review normal workbook-first operator loop @@ -545,6 +556,7 @@ def _analyze_one(repo_meta: RepoMetadata, repo_path: Path) -> RepoAudit: repo_path=repo_path, portfolio_lang_freq=portfolio_lang_freq, custom_weights=custom_weights, + scoring_profile=getattr(custom_weights, "overrides", None), github_client=worker_client, scorecard_enabled=args.scorecard, security_offline=args.security_offline, @@ -1646,7 +1658,13 @@ def _load_scoring_profile(profile_name: str | None) -> tuple[dict[str, float] | profile_path = Path(f"config/scoring-profiles/{profile_name}.json") if profile_path.is_file(): print_info(f"Using scoring profile: {profile_name}") - return json.loads(profile_path.read_text()), normalized + profile = json.loads(profile_path.read_text()) + overrides = { + key: profile.pop(key) + for key in _SCORING_PROFILE_RESERVED_KEYS + if key in profile + } + return ScoringProfile(profile, overrides=overrides), normalized print_warning(f"Scoring profile not found: {profile_path}") return None, normalized diff --git a/src/excel_action_items_helpers.py b/src/excel_action_items_helpers.py index cd8aaef..025a5b5 100644 --- a/src/excel_action_items_helpers.py +++ b/src/excel_action_items_helpers.py @@ -4,6 +4,8 @@ from typing import Any +from src.scoring_dimensions import display_dimension + ACTION_ITEMS_HEADERS = ["#", "Repo", "Action", "Impact", "Effort", "Dimension"] EFFORT_MAP = { "readme": "Low", @@ -42,11 +44,13 @@ def collect_action_items(data: dict[str, Any]) -> list[dict[str, Any]]: for result in audit.get("analyzer_results", []) if result["dimension"] != "interest" } - for dimension, score in sorted(dimension_scores.items(), key=lambda item: item[1])[:2]: + for dimension, score in sorted( + dimension_scores.items(), key=lambda item: item[1] + )[:2]: actions.append( { "repo": audit["metadata"]["name"], - "action": f"Improve {dimension} (currently {score:.1f})", + "action": f"Improve {display_dimension(dimension)} (currently {score:.1f})", "impact": f"Close {gap:.3f} gap to {next_tier}", "effort": EFFORT_MAP.get(dimension, "Med"), "dimension": dimension, @@ -69,7 +73,9 @@ def collect_action_items(data: dict[str, Any]) -> list[dict[str, Any]]: ) effort_order = {"Low": 0, "Med": 1, "High": 2} - actions.sort(key=lambda action: (effort_order.get(action["effort"], 1), action["gap"])) + actions.sort( + key=lambda action: (effort_order.get(action["effort"], 1), action["gap"]) + ) seen: set[tuple[str, str]] = set() unique: list[dict[str, Any]] = [] @@ -102,7 +108,9 @@ def write_action_items_sections( ws.freeze_panes = "A5" if content["sprint_rows"]: - ws.cell(row=3, column=1, value="Weekly Sprint (Top 5 Low-Effort)").font = section_font + ws.cell( + row=3, column=1, value="Weekly Sprint (Top 5 Low-Effort)" + ).font = section_font for col, header in enumerate(ACTION_ITEMS_HEADERS, 1): ws.cell(row=4, column=col, value=header) style_header_row(ws, 4, len(ACTION_ITEMS_HEADERS)) @@ -111,7 +119,9 @@ def write_action_items_sections( style_data_cell(ws.cell(row=row_number, column=col, value=value)) full_start = len(content["sprint_rows"]) + 7 - ws.cell(row=full_start, column=1, value="All Actions (Prioritized)").font = section_font + ws.cell( + row=full_start, column=1, value="All Actions (Prioritized)" + ).font = section_font full_start += 1 for col, header in enumerate(ACTION_ITEMS_HEADERS, 1): ws.cell(row=full_start, column=col, value=header) @@ -136,7 +146,7 @@ def _build_action_rows(actions: list[dict[str, Any]]) -> list[list[Any]]: action["action"], action["impact"], action["effort"], - action["dimension"], + display_dimension(action["dimension"]), ] for index, action in enumerate(actions, start=1) ] diff --git a/src/excel_all_repos_helpers.py b/src/excel_all_repos_helpers.py index fc3f3cf..2e57917 100644 --- a/src/excel_all_repos_helpers.py +++ b/src/excel_all_repos_helpers.py @@ -9,6 +9,7 @@ from openpyxl.styles import Alignment, Font from openpyxl.utils import get_column_letter from openpyxl.worksheet.datavalidation import DataValidation +from src.scoring_dimensions import display_dimension ALL_REPOS_HEADERS = [ "Repo", @@ -158,7 +159,7 @@ def _build_biggest_drag(audit: dict[str, Any]) -> str: if not dimension_scores: return "—" worst_dimension = min(dimension_scores, key=dimension_scores.get) - return f"{worst_dimension} ({dimension_scores[worst_dimension]:.1f})" + return f"{display_dimension(worst_dimension)} ({dimension_scores[worst_dimension]:.1f})" def _build_grade_reason(audit: dict[str, Any]) -> str: @@ -174,8 +175,8 @@ def _build_grade_reason(audit: dict[str, Any]) -> str: if len(weakest_dimensions) < 2: return grade return ( - f"{grade}: {weakest_dimensions[0][0]}={weakest_dimensions[0][1]:.1f}, " - f"{weakest_dimensions[1][0]}={weakest_dimensions[1][1]:.1f}" + f"{grade}: {display_dimension(weakest_dimensions[0][0])}={weakest_dimensions[0][1]:.1f}, " + f"{display_dimension(weakest_dimensions[1][0])}={weakest_dimensions[1][1]:.1f}" ) diff --git a/src/excel_repo_data_helpers.py b/src/excel_repo_data_helpers.py index 39d3611..026d11e 100644 --- a/src/excel_repo_data_helpers.py +++ b/src/excel_repo_data_helpers.py @@ -12,6 +12,7 @@ build_score_explanation, ) from src.sparkline import sparkline as render_sparkline +from src.scoring_dimensions import display_dimension def score_explanation_for_audit(audit: dict) -> dict: @@ -300,7 +301,9 @@ def repo_detail_rows( ) for rank, (dimension, score, summary) in enumerate(ranked_dimensions, 1): lookup_key = f"{repo_name}::{rank}" - dimension_rows.append([lookup_key, repo_name, rank, dimension, score, summary]) + dimension_rows.append( + [lookup_key, repo_name, rank, display_dimension(dimension), score, summary] + ) for run_index, score in enumerate(scores, 1): history_rows.append([repo_name, run_index, score, render_sparkline(scores)]) diff --git a/src/excel_score_explainer_helpers.py b/src/excel_score_explainer_helpers.py index 25409e3..2a59360 100644 --- a/src/excel_score_explainer_helpers.py +++ b/src/excel_score_explainer_helpers.py @@ -4,6 +4,8 @@ from typing import Any +from src.scoring_dimensions import UNSCORED_DIMENSIONS, display_dimension + SCORE_EXPLAINER_HEADERS = ["Dimension", "Weight", "What It Measures", "How to Improve"] DIMENSION_INFO = { @@ -41,6 +43,10 @@ "docs/ dir, CHANGELOG, comment density", "Add docs/ folder or CHANGELOG.md", ), + "description": ( + "Repository description confidence and consistency", + "Add a concise, accurate repository description", + ), } @@ -53,12 +59,21 @@ def build_score_explainer_content( return { "dimension_rows": [ [ - dimension, + display_dimension(dimension), f"{weight:.0%}", DIMENSION_INFO.get(dimension, ("", ""))[0], DIMENSION_INFO.get(dimension, ("", ""))[1], ] for dimension, weight in sorted(weights.items(), key=lambda item: item[1], reverse=True) + ] + [ + [ + display_dimension(dimension), + "Unscored", + DIMENSION_INFO.get(dimension, ("", ""))[0], + DIMENSION_INFO.get(dimension, ("", ""))[1], + ] + for dimension in sorted(UNSCORED_DIMENSIONS) + if dimension not in weights ], "grade_rows": [[grade, f">= {threshold:.0%}"] for threshold, grade in grade_thresholds], "tier_rows": [ diff --git a/src/portfolio_risk.py b/src/portfolio_risk.py index 638aa24..50fc1dd 100644 --- a/src/portfolio_risk.py +++ b/src/portfolio_risk.py @@ -56,7 +56,6 @@ def build_risk_entry( display_name: str, operating_path: str, path_override: str, - path_confidence: str, context_quality: str, activity_status: str, registry_status: str, diff --git a/src/portfolio_truth_reconcile.py b/src/portfolio_truth_reconcile.py index 87c7f9f..0dfd523 100644 --- a/src/portfolio_truth_reconcile.py +++ b/src/portfolio_truth_reconcile.py @@ -653,7 +653,6 @@ def _build_truth_project( display_name=raw_project["name"], operating_path=path_entry.get("operating_path", ""), path_override=path_entry.get("path_override", ""), - path_confidence=path_entry.get("path_confidence", "legacy"), context_quality=context_quality, activity_status=activity_status, registry_status=registry_status, diff --git a/src/report_enrichment.py b/src/report_enrichment.py index 021d9a8..a8ab468 100644 --- a/src/report_enrichment.py +++ b/src/report_enrichment.py @@ -7,6 +7,7 @@ from src.portfolio_truth_types import truth_latest_path from src.report_contracts import RiskLookupEntry, RiskPosture, TopElevatedEntry +from src.scoring_dimensions import display_dimension from src.terminology import ACTION_SYNC_CANONICAL_LABELS from src.weekly_packaging import finalize_weekly_pack from src.weekly_scheduling_overlay import apply_weekly_scheduling_overlay @@ -614,9 +615,10 @@ def _top_dimension_labels(scores: dict[str, float], *, reverse: bool) -> list[st for dimension, score in ordered: if dimension == "interest": continue - labels.append( - f"{DIMENSION_LABELS.get(dimension, dimension.replace('_', ' ').title())} ({score:.2f})" - ) + label = DIMENSION_LABELS.get(dimension, dimension.replace("_", " ").title()) + if display_dimension(dimension) != dimension: + label = f"{label} (unscored)" + labels.append(f"{label} ({score:.2f})") if len(labels) == 3: break return labels diff --git a/src/reporter.py b/src/reporter.py index 6a58ebb..b96900d 100644 --- a/src/reporter.py +++ b/src/reporter.py @@ -49,6 +49,7 @@ build_weekly_review_pack, no_linked_artifact_summary, ) +from src.scoring_dimensions import display_dimension from src.terminology import ACTION_SYNC_CANONICAL_LABELS from src.weekly_scheduling_overlay import resolve_weekly_story_value @@ -65,7 +66,9 @@ def _truncate(text: str | None, length: int = 60) -> str: return text[:length] + "..." if len(text) > length else text -def _file_path(output_dir: Path, prefix: str, username: str, dt: datetime, ext: str) -> Path: +def _file_path( + output_dir: Path, prefix: str, username: str, dt: datetime, ext: str +) -> Path: return output_dir / f"{prefix}-{username}-{_date_str(dt)}.{ext}" @@ -88,7 +91,10 @@ def _sanitize_for_json(obj: object) -> object: def _has_preflight_issues(preflight_summary: dict) -> bool: return bool( preflight_summary - and (preflight_summary.get("blocking_errors") or preflight_summary.get("warnings")) + and ( + preflight_summary.get("blocking_errors") + or preflight_summary.get("warnings") + ) ) @@ -98,7 +104,9 @@ def _has_preflight_issues(preflight_summary: dict) -> bool: def write_json_report(report: AuditReport, output_dir: Path) -> Path: """Write the full audit report as JSON.""" output_dir.mkdir(parents=True, exist_ok=True) - path = _file_path(output_dir, "audit-report", report.username, report.generated_at, "json") + path = _file_path( + output_dir, "audit-report", report.username, report.generated_at, "json" + ) with open(path, "w") as f: json.dump(_sanitize_for_json(report.to_dict()), f, indent=2) @@ -196,7 +204,9 @@ def write_raw_metadata(report: AuditReport, output_dir: Path) -> Path: def write_pcc_export(report: AuditReport, output_dir: Path) -> Path: """Write PCC-compatible flat JSON array.""" output_dir.mkdir(parents=True, exist_ok=True) - path = _file_path(output_dir, "pcc-import", report.username, report.generated_at, "json") + path = _file_path( + output_dir, "pcc-import", report.username, report.generated_at, "json" + ) records = [] for audit in report.audits: @@ -233,7 +243,9 @@ def write_markdown_report( ) -> Path: """Write human-readable Markdown audit report.""" output_dir.mkdir(parents=True, exist_ok=True) - path = _file_path(output_dir, "audit-report", report.username, report.generated_at, "md") + path = _file_path( + output_dir, "audit-report", report.username, report.generated_at, "md" + ) lines: list[str] = [] _w = lines.append @@ -269,7 +281,9 @@ def write_markdown_report( f"Warnings: {report.preflight_summary.get('warnings', 0)}" ) for check in (report.preflight_summary.get("checks") or [])[:5]: - _w(f"- {check.get('summary', 'Issue detected')} ({check.get('category', 'setup')})") + _w( + f"- {check.get('summary', 'Issue detected')} ({check.get('category', 'setup')})" + ) _w("") report_dict = report.to_dict() @@ -326,7 +340,9 @@ def write_markdown_report( _w( f"- Run Changes: {weekly_pack.get('run_change_summary', build_run_change_summary(diff_data))}" ) - _w(f"- Queue Pressure: {weekly_pack.get('queue_pressure_summary', queue_pressure_summary)}") + _w( + f"- Queue Pressure: {weekly_pack.get('queue_pressure_summary', queue_pressure_summary)}" + ) _w( f"- Trust / Actionability: {weekly_pack.get('trust_actionability_summary', trust_actionability_summary)}" ) @@ -421,17 +437,25 @@ def write_markdown_report( _w( f" - Intent Alignment: {item.get('intent_alignment', 'missing-contract')} — {item.get('intent_alignment_summary', 'Intent alignment cannot be judged until a portfolio catalog contract exists.')}" ) - _w(f" - {item.get('scorecard_line', 'Scorecard: No maturity scorecard is recorded yet.')}") + _w( + f" - {item.get('scorecard_line', 'Scorecard: No maturity scorecard is recorded yet.')}" + ) _w( f" - Maturity Gap: {item.get('maturity_gap_summary', 'No maturity gap summary is recorded yet.')}" ) _w(" - Evidence:") for evidence in item.get("evidence_strip", [])[:5]: - command = f" [{evidence.get('command_hint')}]" if evidence.get("command_hint") else "" + command = ( + f" [{evidence.get('command_hint')}]" + if evidence.get("command_hint") + else "" + ) _w( f" - {evidence.get('label', 'Item')} — {evidence.get('summary', 'No evidence summary is recorded yet.')}{command}" ) - _w(f" - Checkpoint Timing: {item.get('follow_through_checkpoint_timing', 'Unknown')}") + _w( + f" - Checkpoint Timing: {item.get('follow_through_checkpoint_timing', 'Unknown')}" + ) _w( f" - Next Checkpoint: {item.get('follow_through_checkpoint', 'Use the next run or linked artifact to confirm whether the recommendation moved.')}" ) @@ -446,7 +470,9 @@ def write_markdown_report( if weekly_pack.get("top_below_target_scorecard_items"): _w("- Scorecard Gaps:") for item in weekly_pack.get("top_below_target_scorecard_items", [])[:5]: - _w(f" - {item.get('repo', 'Repo')} — {item.get('summary', 'Below target.')}") + _w( + f" - {item.get('repo', 'Repo')} — {item.get('summary', 'Below target.')}" + ) _w( f"- Next Checkpoint: {weekly_pack.get('follow_through_checkpoint_summary', 'Use the next run or linked artifact to confirm whether the recommendation moved.')}" ) @@ -529,7 +555,11 @@ def write_markdown_report( ) _w("- Evidence:") for evidence in briefing.get("evidence_strip", [])[:5]: - command = f" [{evidence.get('command_hint')}]" if evidence.get("command_hint") else "" + command = ( + f" [{evidence.get('command_hint')}]" + if evidence.get("command_hint") + else "" + ) _w( f" - {evidence.get('label', 'Item')} — {evidence.get('summary', 'No evidence summary is recorded yet.')}{command}" ) @@ -556,7 +586,9 @@ def write_markdown_report( if report.operator_summary.get("watch_strategy"): _w(f"- Watch Strategy: `{report.operator_summary.get('watch_strategy')}`") if report.operator_summary.get("watch_decision_summary"): - _w(f"- Watch Decision: {report.operator_summary.get('watch_decision_summary')}") + _w( + f"- Watch Decision: {report.operator_summary.get('watch_decision_summary')}" + ) if report.operator_summary.get("what_changed"): _w(f"- What Changed: {report.operator_summary.get('what_changed')}") if report.operator_summary.get("why_it_matters"): @@ -567,9 +599,13 @@ def write_markdown_report( if report.operator_summary.get("trend_summary"): _w(f"- Trend: {report.operator_summary.get('trend_summary')}") if report.operator_summary.get("accountability_summary"): - _w(f"- Accountability: {report.operator_summary.get('accountability_summary')}") + _w( + f"- Accountability: {report.operator_summary.get('accountability_summary')}" + ) if report.operator_summary.get("follow_through_summary"): - _w(f"- Follow-Through: {report.operator_summary.get('follow_through_summary')}") + _w( + f"- Follow-Through: {report.operator_summary.get('follow_through_summary')}" + ) if report.operator_summary.get("follow_through_checkpoint_summary"): _w( f"- Next Checkpoint: {report.operator_summary.get('follow_through_checkpoint_summary')}" @@ -579,8 +615,12 @@ def write_markdown_report( f"- {ACTION_SYNC_CANONICAL_LABELS['readiness']}: {(report.operator_summary.get('action_sync_summary') or {}).get('summary')}" ) if report.operator_summary.get("next_action_sync_step"): - _w(f"- Next Action Sync Step: {report.operator_summary.get('next_action_sync_step')}") - if (report.operator_summary.get("apply_readiness_summary") or {}).get("summary"): + _w( + f"- Next Action Sync Step: {report.operator_summary.get('next_action_sync_step')}" + ) + if (report.operator_summary.get("apply_readiness_summary") or {}).get( + "summary" + ): _w( f"- Apply Packet: {(report.operator_summary.get('apply_readiness_summary') or {}).get('summary')}" ) @@ -590,10 +630,14 @@ def write_markdown_report( ) command_hint = (report.operator_summary.get("next_apply_candidate") or {}).get( "apply_command" - ) or (report.operator_summary.get("next_apply_candidate") or {}).get("preview_command") + ) or (report.operator_summary.get("next_apply_candidate") or {}).get( + "preview_command" + ) if command_hint: _w(f"- Action Sync Command Hint: `{command_hint}`") - if (report.operator_summary.get("campaign_outcomes_summary") or {}).get("summary"): + if (report.operator_summary.get("campaign_outcomes_summary") or {}).get( + "summary" + ): _w( f"- {ACTION_SYNC_CANONICAL_LABELS['post_apply_monitoring']}: {(report.operator_summary.get('campaign_outcomes_summary') or {}).get('summary')}" ) @@ -601,7 +645,9 @@ def write_markdown_report( _w( f"- Next Monitoring Step: {(report.operator_summary.get('next_monitoring_step') or {}).get('summary')}" ) - if (report.operator_summary.get("intervention_ledger_summary") or {}).get("summary"): + if (report.operator_summary.get("intervention_ledger_summary") or {}).get( + "summary" + ): _w( f"- {ACTION_SYNC_CANONICAL_LABELS['historical_portfolio_intelligence']}: {(report.operator_summary.get('intervention_ledger_summary') or {}).get('summary')}" ) @@ -609,15 +655,21 @@ def write_markdown_report( _w( f"- Next Historical Focus: {(report.operator_summary.get('next_historical_focus') or {}).get('summary')}" ) - if (report.operator_summary.get("automation_guidance_summary") or {}).get("summary"): + if (report.operator_summary.get("automation_guidance_summary") or {}).get( + "summary" + ): _w( f"- {ACTION_SYNC_CANONICAL_LABELS['automation_guidance']}: {(report.operator_summary.get('automation_guidance_summary') or {}).get('summary')}" ) - if (report.operator_summary.get("next_safe_automation_step") or {}).get("summary"): + if (report.operator_summary.get("next_safe_automation_step") or {}).get( + "summary" + ): _w( f"- Next Safe Automation Step: {(report.operator_summary.get('next_safe_automation_step') or {}).get('summary')}" ) - if (report.operator_summary.get("approval_workflow_summary") or {}).get("summary"): + if (report.operator_summary.get("approval_workflow_summary") or {}).get( + "summary" + ): _w( f"- {ACTION_SYNC_CANONICAL_LABELS['approval_workflow']}: {(report.operator_summary.get('approval_workflow_summary') or {}).get('summary')}" ) @@ -649,7 +701,9 @@ def write_markdown_report( _w( f"- Follow-Through Recovery Memory Reset: {report.operator_summary.get('follow_through_recovery_memory_reset_summary')}" ) - if report.operator_summary.get("follow_through_recovery_rebuild_strength_summary"): + if report.operator_summary.get( + "follow_through_recovery_rebuild_strength_summary" + ): _w( f"- Follow-Through Recovery Rebuild Strength: {report.operator_summary.get('follow_through_recovery_rebuild_strength_summary')}" ) @@ -657,7 +711,9 @@ def write_markdown_report( _w( f"- Follow-Through Recovery Reacquisition: {report.operator_summary.get('follow_through_recovery_reacquisition_summary')}" ) - if report.operator_summary.get("follow_through_recovery_reacquisition_durability_summary"): + if report.operator_summary.get( + "follow_through_recovery_reacquisition_durability_summary" + ): _w( f"- Follow-Through Reacquisition Durability: {report.operator_summary.get('follow_through_recovery_reacquisition_durability_summary')}" ) @@ -669,8 +725,12 @@ def write_markdown_report( ) primary_target = report.operator_summary.get("primary_target") or {} if primary_target: - repo = f"{primary_target.get('repo')}: " if primary_target.get("repo") else "" - _w(f"- Primary Target: {repo}{primary_target.get('title', 'Operator target')}") + repo = ( + f"{primary_target.get('repo')}: " if primary_target.get("repo") else "" + ) + _w( + f"- Primary Target: {repo}{primary_target.get('title', 'Operator target')}" + ) if report.operator_summary.get("primary_target_reason"): _w( f"- Why This Is The Top Target: {report.operator_summary.get('primary_target_reason')}" @@ -682,13 +742,17 @@ def write_markdown_report( if report.operator_summary.get("closure_guidance"): _w(f"- Closure Guidance: {report.operator_summary.get('closure_guidance')}") if report.operator_summary.get("primary_target_last_intervention"): - intervention = report.operator_summary.get("primary_target_last_intervention") or {} + intervention = ( + report.operator_summary.get("primary_target_last_intervention") or {} + ) when = (intervention.get("recorded_at") or "")[:10] repo = f"{intervention.get('repo')}: " if intervention.get("repo") else "" title = intervention.get("title", "") event_type = intervention.get("event_type", "recorded") outcome = intervention.get("outcome", event_type) - _w(f"- What We Tried: {when} {event_type} for {repo}{title} ({outcome})".strip()) + _w( + f"- What We Tried: {when} {event_type} for {repo}{title} ({outcome})".strip() + ) if report.operator_summary.get("primary_target_resolution_evidence"): _w( f"- Resolution Evidence: {report.operator_summary.get('primary_target_resolution_evidence')}" @@ -701,7 +765,10 @@ def write_markdown_report( if report.operator_summary.get("primary_target_confidence_reasons"): _w( "- Confidence Reasons: " - + ", ".join(report.operator_summary.get("primary_target_confidence_reasons") or []) + + ", ".join( + report.operator_summary.get("primary_target_confidence_reasons") + or [] + ) ) if report.operator_summary.get("next_action_confidence_label"): _w( @@ -719,12 +786,18 @@ def write_markdown_report( _w( f"- Why This Confidence Is Actionable: {report.operator_summary.get('adaptive_confidence_summary')}" ) - if report.operator_summary.get("primary_target_exception_status") not in {None, "", "none"}: + if report.operator_summary.get("primary_target_exception_status") not in { + None, + "", + "none", + }: _w( f"- Trust Policy Exception: {report.operator_summary.get('primary_target_exception_status')} " f"({report.operator_summary.get('primary_target_exception_reason', 'No trust-policy exception reason is recorded yet.')})" ) - if report.operator_summary.get("primary_target_exception_pattern_status") not in { + if report.operator_summary.get( + "primary_target_exception_pattern_status" + ) not in { None, "", "none", @@ -751,7 +824,9 @@ def write_markdown_report( _w( f"- Recovery Confidence Summary: {report.operator_summary.get('recovery_confidence_summary')}" ) - if report.operator_summary.get("primary_target_exception_retirement_status") not in { + if report.operator_summary.get( + "primary_target_exception_retirement_status" + ) not in { None, "", "none", @@ -769,7 +844,9 @@ def write_markdown_report( f"- Policy Debt Cleanup: {report.operator_summary.get('primary_target_policy_debt_status')} " f"({report.operator_summary.get('primary_target_policy_debt_reason', 'No policy-debt reason is recorded yet.')})" ) - if report.operator_summary.get("primary_target_class_normalization_status") not in { + if report.operator_summary.get( + "primary_target_class_normalization_status" + ) not in { None, "", "none", @@ -797,7 +874,10 @@ def write_markdown_report( _w( "- Why Class Guidance Shifted: " + ", ".join( - report.operator_summary.get("primary_target_class_trust_reweight_reasons") or [] + report.operator_summary.get( + "primary_target_class_trust_reweight_reasons" + ) + or [] ) ) if report.operator_summary.get("primary_target_class_trust_momentum_status"): @@ -805,7 +885,9 @@ def write_markdown_report( f"- Class Trust Momentum: {report.operator_summary.get('primary_target_class_trust_momentum_status')} " f"({report.operator_summary.get('primary_target_class_trust_momentum_score', 0.0):.2f})" ) - if report.operator_summary.get("primary_target_class_reweight_stability_status"): + if report.operator_summary.get( + "primary_target_class_reweight_stability_status" + ): _w( f"- Reweighting Stability: {report.operator_summary.get('primary_target_class_reweight_stability_status')} " f"({report.operator_summary.get('primary_target_class_reweight_transition_status', 'none')}: " @@ -816,12 +898,16 @@ def write_markdown_report( f"- Class Transition Health: {report.operator_summary.get('primary_target_class_transition_health_status')} " f"({report.operator_summary.get('primary_target_class_transition_health_reason', 'No class transition health reason is recorded yet.')})" ) - if report.operator_summary.get("primary_target_class_transition_resolution_status"): + if report.operator_summary.get( + "primary_target_class_transition_resolution_status" + ): _w( f"- Pending Transition Resolution: {report.operator_summary.get('primary_target_class_transition_resolution_status')} " f"({report.operator_summary.get('primary_target_class_transition_resolution_reason', 'No class transition resolution reason is recorded yet.')})" ) - if report.operator_summary.get("primary_target_transition_closure_confidence_label"): + if report.operator_summary.get( + "primary_target_transition_closure_confidence_label" + ): _w( f"- Transition Closure Confidence: {report.operator_summary.get('primary_target_transition_closure_confidence_label')} " f"({report.operator_summary.get('primary_target_transition_closure_confidence_score', 0.0):.2f}; " @@ -837,22 +923,30 @@ def write_markdown_report( f"- Pending Debt Freshness: {report.operator_summary.get('primary_target_pending_debt_freshness_status')} " f"({report.operator_summary.get('primary_target_pending_debt_freshness_reason', 'No pending-debt freshness reason is recorded yet.')})" ) - if report.operator_summary.get("primary_target_closure_forecast_reweight_direction"): + if report.operator_summary.get( + "primary_target_closure_forecast_reweight_direction" + ): _w( f"- Closure Forecast Reweighting: {report.operator_summary.get('primary_target_closure_forecast_reweight_direction')} " f"({report.operator_summary.get('primary_target_closure_forecast_reweight_score', 0.0):.2f})" ) - if report.operator_summary.get("primary_target_closure_forecast_momentum_status"): + if report.operator_summary.get( + "primary_target_closure_forecast_momentum_status" + ): _w( f"- Closure Forecast Momentum: {report.operator_summary.get('primary_target_closure_forecast_momentum_status')} " f"({report.operator_summary.get('primary_target_closure_forecast_momentum_score', 0.0):.2f})" ) - if report.operator_summary.get("primary_target_closure_forecast_freshness_status"): + if report.operator_summary.get( + "primary_target_closure_forecast_freshness_status" + ): _w( f"- Closure Forecast Freshness: {report.operator_summary.get('primary_target_closure_forecast_freshness_status')} " f"({report.operator_summary.get('primary_target_closure_forecast_freshness_reason', 'No closure-forecast freshness reason is recorded yet.')})" ) - if report.operator_summary.get("primary_target_closure_forecast_stability_status"): + if report.operator_summary.get( + "primary_target_closure_forecast_stability_status" + ): _w( f"- Closure Forecast Hysteresis: {report.operator_summary.get('primary_target_closure_forecast_stability_status')} " f"({report.operator_summary.get('primary_target_closure_forecast_hysteresis_status', 'none')}: " @@ -863,12 +957,16 @@ def write_markdown_report( f"- Hysteresis Decay Controls: {report.operator_summary.get('primary_target_closure_forecast_decay_status')} " f"({report.operator_summary.get('primary_target_closure_forecast_decay_reason', 'No closure-forecast decay reason is recorded yet.')})" ) - if report.operator_summary.get("primary_target_closure_forecast_refresh_recovery_status"): + if report.operator_summary.get( + "primary_target_closure_forecast_refresh_recovery_status" + ): _w( f"- Closure Forecast Refresh Recovery: {report.operator_summary.get('primary_target_closure_forecast_refresh_recovery_status')} " f"({report.operator_summary.get('primary_target_closure_forecast_refresh_recovery_score', 0.0):.2f})" ) - if report.operator_summary.get("primary_target_closure_forecast_reacquisition_status"): + if report.operator_summary.get( + "primary_target_closure_forecast_reacquisition_status" + ): _w( f"- Reacquisition Controls: {report.operator_summary.get('primary_target_closure_forecast_reacquisition_status')} " f"({report.operator_summary.get('primary_target_closure_forecast_reacquisition_reason', 'No closure-forecast reacquisition reason is recorded yet.')})" @@ -881,7 +979,9 @@ def write_markdown_report( f"({report.operator_summary.get('primary_target_closure_forecast_reacquisition_persistence_score', 0.0):.2f}; " f"{report.operator_summary.get('primary_target_closure_forecast_reacquisition_age_runs', 0)} run(s))" ) - if report.operator_summary.get("primary_target_closure_forecast_recovery_churn_status"): + if report.operator_summary.get( + "primary_target_closure_forecast_recovery_churn_status" + ): _w( f"- Recovery Churn Controls: {report.operator_summary.get('primary_target_closure_forecast_recovery_churn_status')} " f"({report.operator_summary.get('primary_target_closure_forecast_recovery_churn_reason', 'No recovery-churn reason is recorded yet.')})" @@ -893,7 +993,9 @@ def write_markdown_report( f"- Reacquisition Freshness: {report.operator_summary.get('primary_target_closure_forecast_reacquisition_freshness_status')} " f"({report.operator_summary.get('primary_target_closure_forecast_reacquisition_freshness_reason', 'No reacquisition-freshness reason is recorded yet.')})" ) - if report.operator_summary.get("primary_target_closure_forecast_persistence_reset_status"): + if report.operator_summary.get( + "primary_target_closure_forecast_persistence_reset_status" + ): _w( f"- Persistence Reset Controls: {report.operator_summary.get('primary_target_closure_forecast_persistence_reset_status')} " f"({report.operator_summary.get('primary_target_closure_forecast_persistence_reset_reason', 'No persistence-reset reason is recorded yet.')})" @@ -905,7 +1007,9 @@ def write_markdown_report( f"- Reset Refresh Recovery: {report.operator_summary.get('primary_target_closure_forecast_reset_refresh_recovery_status')} " f"({report.operator_summary.get('primary_target_closure_forecast_reset_refresh_recovery_score', 0.0):.2f})" ) - if report.operator_summary.get("primary_target_closure_forecast_reset_reentry_status"): + if report.operator_summary.get( + "primary_target_closure_forecast_reset_reentry_status" + ): _w( f"- Reset Re-entry Controls: {report.operator_summary.get('primary_target_closure_forecast_reset_reentry_status')} " f"({report.operator_summary.get('primary_target_closure_forecast_reset_reentry_reason', 'No reset re-entry reason is recorded yet.')})" @@ -1182,21 +1286,29 @@ def write_markdown_report( f"- Exception Retirement Summary: {report.operator_summary.get('exception_retirement_summary')}" ) if report.operator_summary.get("policy_debt_summary"): - _w(f"- Policy Debt Summary: {report.operator_summary.get('policy_debt_summary')}") + _w( + f"- Policy Debt Summary: {report.operator_summary.get('policy_debt_summary')}" + ) if report.operator_summary.get("trust_normalization_summary"): _w( f"- Trust Normalization Summary: {report.operator_summary.get('trust_normalization_summary')}" ) if report.operator_summary.get("class_memory_summary"): - _w(f"- Class Memory Summary: {report.operator_summary.get('class_memory_summary')}") + _w( + f"- Class Memory Summary: {report.operator_summary.get('class_memory_summary')}" + ) if report.operator_summary.get("class_decay_summary"): - _w(f"- Class Decay Summary: {report.operator_summary.get('class_decay_summary')}") + _w( + f"- Class Decay Summary: {report.operator_summary.get('class_decay_summary')}" + ) if report.operator_summary.get("class_reweighting_summary"): _w( f"- Class Reweighting Summary: {report.operator_summary.get('class_reweighting_summary')}" ) if report.operator_summary.get("class_momentum_summary"): - _w(f"- Class Momentum Summary: {report.operator_summary.get('class_momentum_summary')}") + _w( + f"- Class Momentum Summary: {report.operator_summary.get('class_momentum_summary')}" + ) if report.operator_summary.get("class_reweight_stability_summary"): _w( f"- Reweighting Stability Summary: {report.operator_summary.get('class_reweight_stability_summary')}" @@ -1261,7 +1373,9 @@ def write_markdown_report( _w( f"- Closure Forecast Reacquisition Summary: {report.operator_summary.get('closure_forecast_reacquisition_summary')}" ) - if report.operator_summary.get("closure_forecast_reacquisition_persistence_summary"): + if report.operator_summary.get( + "closure_forecast_reacquisition_persistence_summary" + ): _w( f"- Reacquisition Persistence Summary: {report.operator_summary.get('closure_forecast_reacquisition_persistence_summary')}" ) @@ -1269,7 +1383,9 @@ def write_markdown_report( _w( f"- Recovery Churn Summary: {report.operator_summary.get('closure_forecast_recovery_churn_summary')}" ) - if report.operator_summary.get("closure_forecast_reacquisition_freshness_summary"): + if report.operator_summary.get( + "closure_forecast_reacquisition_freshness_summary" + ): _w( f"- Reacquisition Freshness Summary: {report.operator_summary.get('closure_forecast_reacquisition_freshness_summary')}" ) @@ -1277,7 +1393,9 @@ def write_markdown_report( _w( f"- Persistence Reset Summary: {report.operator_summary.get('closure_forecast_persistence_reset_summary')}" ) - if report.operator_summary.get("closure_forecast_reset_refresh_recovery_summary"): + if report.operator_summary.get( + "closure_forecast_reset_refresh_recovery_summary" + ): _w( f"- Reset Refresh Recovery Summary: {report.operator_summary.get('closure_forecast_reset_refresh_recovery_summary')}" ) @@ -1285,7 +1403,9 @@ def write_markdown_report( _w( f"- Reset Re-entry Summary: {report.operator_summary.get('closure_forecast_reset_reentry_summary')}" ) - if report.operator_summary.get("closure_forecast_reset_reentry_persistence_summary"): + if report.operator_summary.get( + "closure_forecast_reset_reentry_persistence_summary" + ): _w( f"- Reset Re-entry Persistence Summary: {report.operator_summary.get('closure_forecast_reset_reentry_persistence_summary')}" ) @@ -1293,7 +1413,9 @@ def write_markdown_report( _w( f"- Reset Re-entry Churn Summary: {report.operator_summary.get('closure_forecast_reset_reentry_churn_summary')}" ) - if report.operator_summary.get("closure_forecast_reset_reentry_freshness_summary"): + if report.operator_summary.get( + "closure_forecast_reset_reentry_freshness_summary" + ): _w( f"- Reset Re-entry Freshness Summary: {report.operator_summary.get('closure_forecast_reset_reentry_freshness_summary')}" ) @@ -1301,19 +1423,27 @@ def write_markdown_report( _w( f"- Reset Re-entry Reset Summary: {report.operator_summary.get('closure_forecast_reset_reentry_reset_summary')}" ) - if report.operator_summary.get("closure_forecast_reset_reentry_refresh_recovery_summary"): + if report.operator_summary.get( + "closure_forecast_reset_reentry_refresh_recovery_summary" + ): _w( f"- Reset Re-entry Refresh Recovery Summary: {report.operator_summary.get('closure_forecast_reset_reentry_refresh_recovery_summary')}" ) - if report.operator_summary.get("closure_forecast_reset_reentry_rebuild_summary"): + if report.operator_summary.get( + "closure_forecast_reset_reentry_rebuild_summary" + ): _w( f"- Reset Re-entry Rebuild Summary: {report.operator_summary.get('closure_forecast_reset_reentry_rebuild_summary')}" ) - if report.operator_summary.get("closure_forecast_reset_reentry_rebuild_freshness_summary"): + if report.operator_summary.get( + "closure_forecast_reset_reentry_rebuild_freshness_summary" + ): _w( f"- Reset Re-entry Rebuild Freshness Summary: {report.operator_summary.get('closure_forecast_reset_reentry_rebuild_freshness_summary')}" ) - if report.operator_summary.get("closure_forecast_reset_reentry_rebuild_reset_summary"): + if report.operator_summary.get( + "closure_forecast_reset_reentry_rebuild_reset_summary" + ): _w( f"- Reset Re-entry Rebuild Reset Summary: {report.operator_summary.get('closure_forecast_reset_reentry_rebuild_reset_summary')}" ) @@ -1323,7 +1453,9 @@ def write_markdown_report( _w( f"- Reset Re-entry Rebuild Refresh Recovery Summary: {report.operator_summary.get('closure_forecast_reset_reentry_rebuild_refresh_recovery_summary')}" ) - if report.operator_summary.get("closure_forecast_reset_reentry_rebuild_reentry_summary"): + if report.operator_summary.get( + "closure_forecast_reset_reentry_rebuild_reentry_summary" + ): _w( f"- Reset Re-entry Rebuild Re-entry Summary: {report.operator_summary.get('closure_forecast_reset_reentry_rebuild_reentry_summary')}" ) @@ -1477,7 +1609,9 @@ def write_markdown_report( _w( f"- Reset Re-entry Rebuild Persistence Summary: {report.operator_summary.get('closure_forecast_reset_reentry_rebuild_persistence_summary')}" ) - if report.operator_summary.get("closure_forecast_reset_reentry_rebuild_churn_summary"): + if report.operator_summary.get( + "closure_forecast_reset_reentry_rebuild_churn_summary" + ): _w( f"- Reset Re-entry Rebuild Churn Summary: {report.operator_summary.get('closure_forecast_reset_reentry_rebuild_churn_summary')}" ) @@ -1492,7 +1626,9 @@ def write_markdown_report( ) if report.operator_summary.get("recent_validation_outcomes"): recent_outcomes = [] - for item in (report.operator_summary.get("recent_validation_outcomes") or [])[:3]: + for item in ( + report.operator_summary.get("recent_validation_outcomes") or [] + )[:3]: recent_outcomes.append( f"{item.get('target_label', 'Operator target')} " f"[{item.get('confidence_label', 'low')}] -> {str(item.get('outcome', 'unresolved')).replace('_', ' ')}" @@ -1516,13 +1652,19 @@ def write_markdown_report( ) _w(f" - Why: {item.get('summary', 'No summary available.')}") _w(f" - Lane Reason: {item.get('lane_reason', 'Operator triage')}") - _w(f" - Next: {item.get('recommended_action', 'Review the latest state.')}") - _w(f" - Last Movement: {build_last_movement_label(item, report.review_summary or {})}") + _w( + f" - Next: {item.get('recommended_action', 'Review the latest state.')}" + ) + _w( + f" - Last Movement: {build_last_movement_label(item, report.review_summary or {})}" + ) _w( f" - Follow-Through: {build_follow_through_status_label(item)} — " f"{build_follow_through_summary(item)}" ) - _w(f" - Checkpoint Timing: {build_follow_through_checkpoint_status_label(item)}") + _w( + f" - Checkpoint Timing: {build_follow_through_checkpoint_status_label(item)}" + ) _w( f" - Escalation: {build_follow_through_escalation_status_label(item)} — " f"{build_follow_through_escalation_summary(item)}" @@ -1659,7 +1801,9 @@ def write_markdown_report( _w("") provider_coverage = report.security_posture.get("provider_coverage", {}) open_alerts = report.security_posture.get("open_alerts", {}) - _w(f"- Average posture score: {report.security_posture.get('average_score', 0):.2f}") + _w( + f"- Average posture score: {report.security_posture.get('average_score', 0):.2f}" + ) _w( f"- Critical repos: {', '.join(report.security_posture.get('critical_repos', [])[:5]) or '—'}" ) @@ -1685,7 +1829,9 @@ def write_markdown_report( for entry in risk_lookup.values(): tier_counts[entry["risk_tier"]] = tier_counts.get(entry["risk_tier"], 0) + 1 elevated_repos = sorted( - name for name, entry in risk_lookup.items() if entry["risk_tier"] == "elevated" + name + for name, entry in risk_lookup.items() + if entry["risk_tier"] == "elevated" ) _w("### Risk Posture") _w("") @@ -1742,13 +1888,17 @@ def write_markdown_report( f"- {ACTION_SYNC_CANONICAL_LABELS['historical_portfolio_intelligence']}: {report.intervention_ledger_summary.get('summary')}" ) if report.next_historical_focus.get("summary"): - _w(f"- Next Historical Focus: {report.next_historical_focus.get('summary')}") + _w( + f"- Next Historical Focus: {report.next_historical_focus.get('summary')}" + ) if report.automation_guidance_summary.get("summary"): _w( f"- {ACTION_SYNC_CANONICAL_LABELS['automation_guidance']}: {report.automation_guidance_summary.get('summary')}" ) if report.next_safe_automation_step.get("summary"): - _w(f"- Next Safe Automation Step: {report.next_safe_automation_step.get('summary')}") + _w( + f"- Next Safe Automation Step: {report.next_safe_automation_step.get('summary')}" + ) if report.approval_workflow_summary.get("summary"): _w( f"- {ACTION_SYNC_CANONICAL_LABELS['approval_workflow']}: {report.approval_workflow_summary.get('summary')}" @@ -1807,12 +1957,18 @@ def write_markdown_report( ) _w("") - if report.rollback_preview.get("items") or report.rollback_preview.get("item_count", 0): + if report.rollback_preview.get("items") or report.rollback_preview.get( + "item_count", 0 + ): _w("### Rollback Preview") _w("") - _w(f"- Available: {'yes' if report.rollback_preview.get('available') else 'no'}") + _w( + f"- Available: {'yes' if report.rollback_preview.get('available') else 'no'}" + ) _w(f"- Items: {report.rollback_preview.get('item_count', 0)}") - _w(f"- Fully reversible: {report.rollback_preview.get('fully_reversible_count', 0)}") + _w( + f"- Fully reversible: {report.rollback_preview.get('fully_reversible_count', 0)}" + ) _w("") if report.security_governance_preview: @@ -1829,7 +1985,11 @@ def write_markdown_report( _w("") governance_summary = report.governance_summary or {} - if governance_summary or report.governance_results.get("results") or report.governance_drift: + if ( + governance_summary + or report.governance_results.get("results") + or report.governance_drift + ): _w("### Governance Operator State") _w("") _w( @@ -1837,15 +1997,21 @@ def write_markdown_report( ) _w(f"- Status: {governance_summary.get('status', 'preview')}") _w(f"- Approved: {'yes' if report.governance_approval else 'no'}") - _w(f"- Needs Re-Approval: {'yes' if governance_summary.get('needs_reapproval') else 'no'}") - _w(f"- Drift Count: {governance_summary.get('drift_count', len(report.governance_drift))}") + _w( + f"- Needs Re-Approval: {'yes' if governance_summary.get('needs_reapproval') else 'no'}" + ) + _w( + f"- Drift Count: {governance_summary.get('drift_count', len(report.governance_drift))}" + ) _w( f"- Applyable Count: {governance_summary.get('applyable_count', report.governance_preview.get('applyable_count', 0) if isinstance(report.governance_preview, dict) else 0)}" ) _w( f"- Applied Count: {governance_summary.get('applied_count', len(report.governance_results.get('results', [])))}" ) - _w(f"- Rollback Available: {governance_summary.get('rollback_available_count', 0)}") + _w( + f"- Rollback Available: {governance_summary.get('rollback_available_count', 0)}" + ) if governance_summary.get("approval_age_days") is not None: _w(f"- Approval Age (days): {governance_summary.get('approval_age_days')}") for item in governance_summary.get("top_actions", [])[:4]: @@ -1928,7 +2094,9 @@ def write_markdown_report( for audit in tier_audits: m = audit.metadata name_link = f"[{m.name}]({m.html_url})" - badges_str = " ".join(f"`{b}`" for b in audit.badges[:3]) if audit.badges else "—" + badges_str = ( + " ".join(f"`{b}`" for b in audit.badges[:3]) if audit.badges else "—" + ) desc = _truncate(m.description) lang = m.language or "—" _w( @@ -1971,7 +2139,9 @@ def write_markdown_report( ) _w("") _w("**Current State**") - _w(f"- {briefing.get('current_state_line', 'No current-state summary is recorded yet.')}") + _w( + f"- {briefing.get('current_state_line', 'No current-state summary is recorded yet.')}" + ) _w(f"- URL: {m.html_url}") _w( f"- Description: {briefing.get('current_state', {}).get('description', m.description or 'No description recorded yet.')}" @@ -2024,7 +2194,7 @@ def write_markdown_report( _w("|-----------|-------|-------------|") for r in audit.analyzer_results: findings = ", ".join(r.findings[:2]) if r.findings else "—" - _w(f"| {r.dimension} | {r.score:.2f} | {findings} |") + _w(f"| {display_dimension(r.dimension)} | {r.score:.2f} | {findings} |") _w("") _w( f"**Language:** {m.language or '—'} | " @@ -2075,7 +2245,9 @@ def write_markdown_report( _w( f"- Intent Alignment: {briefing.get('intent_alignment_line', 'missing-contract: Intent alignment cannot be judged until a portfolio catalog contract exists.')}" ) - _w(f"- Checkpoint Timing: {briefing.get('checkpoint_timing_line', 'Unknown')}") + _w( + f"- Checkpoint Timing: {briefing.get('checkpoint_timing_line', 'Unknown')}" + ) _w( f"- Escalation: {briefing.get('escalation_line', 'Unknown: No stronger follow-through escalation is currently surfaced.')}" ) @@ -2141,7 +2313,9 @@ def _write_ranked_list( for i, name in enumerate(names, 1): audit = audit_map.get(name) if audit: - lines.append(f"{i}. {name} — {audit.overall_score:.2f} ({audit.completeness_tier})") + lines.append( + f"{i}. {name} — {audit.overall_score:.2f} ({audit.completeness_tier})" + ) def _group_by_tier(audits: list[RepoAudit]) -> dict[str, list[RepoAudit]]: @@ -2174,7 +2348,9 @@ def _write_reconciliation_section(lines: list[str], report: AuditReport) -> None # On GitHub but not in registry if recon.on_github_not_registry: - _w(f"### On GitHub but NOT in Registry ({len(recon.on_github_not_registry)} repos)") + _w( + f"### On GitHub but NOT in Registry ({len(recon.on_github_not_registry)} repos)" + ) _w("") _w("| Repo | Tier | Score | Language |") _w("|------|------|-------|----------|") @@ -2189,7 +2365,9 @@ def _write_reconciliation_section(lines: list[str], report: AuditReport) -> None # In registry but not on GitHub if recon.in_registry_not_github: - _w(f"### In Registry but NOT on GitHub ({len(recon.in_registry_not_github)} projects)") + _w( + f"### In Registry but NOT on GitHub ({len(recon.in_registry_not_github)} projects)" + ) _w("") _w("| Project | Registry Status |") _w("|---------|----------------|") @@ -2237,7 +2415,9 @@ def _render_weekly_story_sections(_w, weekly_pack: dict) -> None: continue _w(f"### {section.get('label', 'Weekly Story')}") _w("") - _w(f"- Summary: {section.get('headline', 'No section summary is recorded yet.')}") + _w( + f"- Summary: {section.get('headline', 'No section summary is recorded yet.')}" + ) _w( f"- {section.get('next_label', 'Next Step')}: {section.get('next_step', 'No next step is recorded yet.')}" ) @@ -2246,7 +2426,9 @@ def _render_weekly_story_sections(_w, weekly_pack: dict) -> None: if evidence_items: _w("- Evidence:") for item in evidence_items[:5]: - command = f" [{item.get('command_hint')}]" if item.get("command_hint") else "" + command = ( + f" [{item.get('command_hint')}]" if item.get("command_hint") else "" + ) _w( f" - {item.get('label', 'Item')} — {item.get('summary', 'No evidence summary is recorded yet.')}{command}" ) diff --git a/src/scorer.py b/src/scorer.py index c8fbe59..78c5a64 100644 --- a/src/scorer.py +++ b/src/scorer.py @@ -2,10 +2,14 @@ from datetime import datetime, timezone from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from src.badges import compute_badges, suggest_next_badges from src.models import AnalyzerResult, RepoAudit, RepoMetadata +from src.scoring_dimensions import ( + UNSCORED_DIMENSIONS as UNSCORED_DIMENSIONS, + display_dimension as display_dimension, +) if TYPE_CHECKING: from src.github_client import GitHubClient @@ -46,10 +50,11 @@ GRADE_THRESHOLDS = [(0.80, "A"), (0.70, "B"), (0.55, "C"), (0.35, "D"), (0.0, "F")] - -def letter_grade(score: float) -> str: +def letter_grade( + score: float, grade_thresholds: list[tuple[float, str]] | None = None +) -> str: """Map a 0.0-1.0 score to a letter grade A-F.""" - for threshold, grade in GRADE_THRESHOLDS: + for threshold, grade in grade_thresholds or GRADE_THRESHOLDS: if score >= threshold: return grade return "F" @@ -61,13 +66,22 @@ def score_repo( repo_path: Path | None = None, portfolio_lang_freq: dict[str, float] | None = None, custom_weights: dict[str, float] | None = None, + scoring_profile: dict[str, Any] | None = None, github_client: GitHubClient | None = None, *, scorecard_enabled: bool = False, security_offline: bool = False, ) -> RepoAudit: """Compute dual-axis scores: completeness + interest.""" - weights = dict(custom_weights) if custom_weights else dict(WEIGHTS) + profile = scoring_profile or {} + weights = dict(custom_weights or WEIGHTS) + completeness_tiers = [ + tuple(tier) for tier in profile.get("completeness_tiers", COMPLETENESS_TIERS) + ] + grade_thresholds = [ + tuple(threshold) for threshold in profile.get("grade_thresholds", GRADE_THRESHOLDS) + ] + stale_threshold_days = profile.get("stale_threshold_days", STALE_THRESHOLD_DAYS) flags: list[str] = [] # Fork override: reduce activity weight @@ -111,7 +125,7 @@ def score_repo( # Classify completeness tier tier = "abandoned" - for tier_name, threshold in COMPLETENESS_TIERS: + for tier_name, threshold in completeness_tiers: if overall_score >= threshold: tier = tier_name break @@ -141,7 +155,7 @@ def score_repo( # Override: stale >2 years capped at "wip" if metadata.pushed_at: days_since = (datetime.now(timezone.utc) - metadata.pushed_at).days - if days_since > STALE_THRESHOLD_DAYS: + if days_since > stale_threshold_days: flags.append("stale-2yr") if tier in ("shipped", "functional"): tier = "wip" @@ -159,8 +173,8 @@ def score_repo( interest_score=interest_score, completeness_tier=tier, interest_tier=interest_tier, - grade=letter_grade(overall_score), - interest_grade=letter_grade(interest_score), + grade=letter_grade(overall_score, grade_thresholds), + interest_grade=letter_grade(interest_score, grade_thresholds), flags=flags, ) diff --git a/src/scoring_dimensions.py b/src/scoring_dimensions.py new file mode 100644 index 0000000..cda46bc --- /dev/null +++ b/src/scoring_dimensions.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +UNSCORED_DIMENSIONS = frozenset({"description"}) + + +def display_dimension(dimension: str) -> str: + """Return the operator-facing label for a scored or advisory dimension.""" + return f"{dimension} (unscored)" if dimension in UNSCORED_DIMENSIONS else dimension diff --git a/src/serve/routes.py b/src/serve/routes.py index 7298c6d..9cf4fe0 100644 --- a/src/serve/routes.py +++ b/src/serve/routes.py @@ -15,6 +15,7 @@ from fastapi.templating import Jinja2Templates from src.portfolio_truth_types import truth_latest_path +from src.scoring_dimensions import display_dimension from src.serve.runner import SAFE_FLAG_NAMES, get_session, spawn_run, validate_flags router = APIRouter() @@ -194,7 +195,10 @@ async def repo_detail(request: Request, name: str) -> HTMLResponse: """, (name, name, name), ).fetchall() - dimension_scores = [dict(r) for r in rows2] + dimension_scores = [ + {**dict(row), "dimension": display_dimension(str(row["dimension"]))} + for row in rows2 + ] except sqlite3.Error: # Optional dimension breakdown should not block the repo detail page. pass diff --git a/tests/test_portfolio_risk.py b/tests/test_portfolio_risk.py index 6cdd12e..005afec 100644 --- a/tests/test_portfolio_risk.py +++ b/tests/test_portfolio_risk.py @@ -6,7 +6,6 @@ def _baseline_kwargs(**overrides): display_name="SomeRepo", operating_path="maintain", path_override="", - path_confidence="high", context_quality="standard", activity_status="active", registry_status="active", diff --git a/tests/test_scoring_profiles.py b/tests/test_scoring_profiles.py index 1cea698..4d7b4a8 100644 --- a/tests/test_scoring_profiles.py +++ b/tests/test_scoring_profiles.py @@ -1,9 +1,11 @@ from __future__ import annotations import json -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone +from pathlib import Path from src.models import AnalyzerResult, RepoMetadata +from src.app.run_audit import _load_scoring_profile from src.scorer import WEIGHTS, score_repo @@ -77,3 +79,49 @@ def test_profile_json_valid(self, tmp_path): weights = json.loads(profile_path.read_text()) total = sum(weights.values()) assert abs(total - 1.0) < 0.01, f"{profile_path.name} weights sum to {total}" + + def test_flat_profile_loads_weights_without_overrides(self): + profile, profile_name = _load_scoring_profile("default") + + assert profile_name == "default" + assert profile is not None + assert dict(profile) == json.loads( + Path("config/scoring-profiles/default.json").read_text() + ) + assert profile.overrides == {} + + def test_profile_reserved_constants_override_scorer(self, tmp_path, monkeypatch): + profiles_dir = tmp_path / "config/scoring-profiles" + profiles_dir.mkdir(parents=True) + profile_path = profiles_dir / "override.json" + profile_path.write_text( + json.dumps( + { + **WEIGHTS, + "stale_threshold_days": 1, + "grade_thresholds": [[0.9, "A"], [0.0, "F"]], + "completeness_tiers": [["shipped", 0.8], ["functional", 0.7], ["draft", 0.0]], + } + ) + ) + monkeypatch.chdir(tmp_path) + profile, _ = _load_scoring_profile("override") + assert profile is not None + + metadata = _make_metadata( + pushed_at=datetime.now(timezone.utc) - timedelta(days=3) + ) + results = _make_results({dimension: 0.76 for dimension in WEIGHTS}) + default_audit = score_repo(metadata, results) + overridden_audit = score_repo( + metadata, + results, + custom_weights=profile, + scoring_profile=profile.overrides, + ) + + assert dict(profile) == WEIGHTS + assert default_audit.completeness_tier == "shipped" + assert overridden_audit.completeness_tier == "wip" + assert default_audit.grade == "B" + assert overridden_audit.grade == "F" diff --git a/tests/test_scoring_weights.py b/tests/test_scoring_weights.py new file mode 100644 index 0000000..f254eb4 --- /dev/null +++ b/tests/test_scoring_weights.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import importlib +import inspect +import pkgutil + +from src.analyzers import ALL_ANALYZERS +from src.analyzers.base import BaseAnalyzer +from src.scorer import WEIGHTS + + +def test_scoring_weights_sum_to_one() -> None: + assert abs(sum(WEIGHTS.values()) - 1.0) < 1e-9 + + +def test_scoring_weights_cover_exactly_the_composed_dimensions() -> None: + expected_scored_dimensions = { + "readme", + "structure", + "code_quality", + "testing", + "cicd", + "dependencies", + "activity", + "documentation", + "build_readiness", + "community_profile", + } + + assert set(WEIGHTS) == expected_scored_dimensions + assert {analyzer.name for analyzer in ALL_ANALYZERS if analyzer.name in WEIGHTS} == set(WEIGHTS) + + +def test_analyzer_classes_do_not_declare_dead_weight_attributes() -> None: + import src.analyzers as analyzers_package + + analyzer_classes = [BaseAnalyzer] + for module_info in pkgutil.iter_modules(analyzers_package.__path__): + module = importlib.import_module(f"{analyzers_package.__name__}.{module_info.name}") + analyzer_classes.extend( + cls + for _, cls in inspect.getmembers(module, inspect.isclass) + if cls.__module__ == module.__name__ + and issubclass(cls, BaseAnalyzer) + and cls is not BaseAnalyzer + ) + + assert all("weight" not in cls.__dict__ for cls in analyzer_classes)