diff --git a/config/portfolio-catalog.yaml b/config/portfolio-catalog.yaml index 8526241..659dfc5 100644 --- a/config/portfolio-catalog.yaml +++ b/config/portfolio-catalog.yaml @@ -225,6 +225,19 @@ repos: review_cadence: weekly intended_disposition: maintain tool_provenance: claude-code + notes: Local checkout for the canonical saagpatel/OrbitMechanics browser-game repo. + saagpatel/OrbitMechanics: + owner: d + purpose: browser orbital-mechanics puzzle game with real Newtonian physics and handcrafted levels + lifecycle_state: active + criticality: medium + review_cadence: weekly + intended_disposition: maintain + category: fun + tool_provenance: claude-code + maturity_program: maintain + target_maturity: operating + notes: Canonical GitHub repo identity; local checkout remains OrbitMechanic. saagpatel/operant: owner: d purpose: operating-agent calibration benchmark for evaluating agent decision quality and orchestration judgment diff --git a/src/cli.py b/src/cli.py index e5e45a3..fb61cc2 100644 --- a/src/cli.py +++ b/src/cli.py @@ -1916,6 +1916,7 @@ def _apply_portfolio_catalog(report: AuditReport, args) -> AuditReport: def _apply_scorecards(report: AuditReport, args) -> AuditReport: from src.report_enrichment import build_maturity_gap_summary, build_scorecard_line + from src.portfolio_catalog import build_intent_alignment_summary, evaluate_intent_alignment from src.scorecards import ( DEFAULT_SCORECARDS_PATH, evaluate_scorecards_for_report, @@ -1926,20 +1927,51 @@ def _apply_scorecards(report: AuditReport, args) -> AuditReport: scorecards_data = load_scorecards(Path(scorecards_path)) repo_results, summary, programs = evaluate_scorecards_for_report(report, scorecards_data) by_repo = {result.get("repo", ""): result for result in repo_results} + queue_by_repo = { + str(item.get("repo") or item.get("repo_name") or "").strip(): item + for item in (report.operator_queue or []) + if str(item.get("repo") or item.get("repo_name") or "").strip() + } for audit in report.audits: result = by_repo.get(audit.metadata.name, {}) audit.scorecard = dict(result) if audit.portfolio_catalog: audit.portfolio_catalog["scorecard"] = dict(result) + operator_focus = audit.portfolio_catalog.get("operator_focus", "") + if not operator_focus: + from src.report_enrichment import build_operator_focus + + operator_focus = build_operator_focus(queue_by_repo.get(audit.metadata.name, {})) + intent_alignment, intent_alignment_reason = evaluate_intent_alignment( + audit.portfolio_catalog, + completeness_tier=audit.completeness_tier, + archived=audit.metadata.archived, + operator_focus=operator_focus, + ) + audit.portfolio_catalog.update( + { + "intent_alignment": intent_alignment, + "intent_alignment_reason": intent_alignment_reason, + "intent_alignment_line": f"{intent_alignment}: {intent_alignment_reason}", + "operator_focus": operator_focus, + } + ) + catalog_by_repo = {audit.metadata.name: audit.portfolio_catalog for audit in report.audits} for item in report.operator_queue: repo_name = str(item.get("repo") or item.get("repo_name") or "").strip() result = by_repo.get(repo_name, {}) + catalog_entry = catalog_by_repo.get(repo_name, {}) + if catalog_entry: + item["portfolio_catalog"] = dict(catalog_entry) + item["intent_alignment"] = catalog_entry.get("intent_alignment", "") + item["intent_alignment_reason"] = catalog_entry.get("intent_alignment_reason", "") if result: item["scorecard"] = dict(result) item["scorecard_line"] = build_scorecard_line(item) item["maturity_gap_summary"] = build_maturity_gap_summary(item) report.scorecards_summary = summary report.scorecard_programs = programs + report.intent_alignment_summary = build_intent_alignment_summary(report.audits) return report diff --git a/src/portfolio_catalog.py b/src/portfolio_catalog.py index d183a92..53cfdfc 100644 --- a/src/portfolio_catalog.py +++ b/src/portfolio_catalog.py @@ -489,6 +489,9 @@ def evaluate_intent_alignment( disposition = _safe_text(entry.get("intended_disposition")).lower() tier = _safe_text(completeness_tier).lower() focus = _safe_text(operator_focus) + raw_scorecard = entry.get("scorecard") + scorecard = raw_scorecard if isinstance(raw_scorecard, dict) else {} + scorecard_status = _safe_text(scorecard.get("status")).lower() if disposition == "archive" and (archived or tier in {"abandoned", "skeleton"}): return ( @@ -511,6 +514,11 @@ def evaluate_intent_alignment( "aligned", "The repo is holding a maintain posture without urgent or revalidation pressure.", ) + if disposition == "maintain" and scorecard_status == "on-track": + return ( + "aligned", + "The repo is meeting its maintain scorecard target.", + ) if disposition == "finish" and tier in {"wip", "functional"} and focus != "Revalidate": return ("aligned", "The repo still looks finishable rather than fully off-track.") diff --git a/tests/test_cli_hardening.py b/tests/test_cli_hardening.py index a4c0257..ef7f004 100644 --- a/tests/test_cli_hardening.py +++ b/tests/test_cli_hardening.py @@ -459,6 +459,60 @@ def test_apply_scorecards_populates_report(monkeypatch, sample_metadata, tmp_pat assert updated.operator_queue[0]["scorecard_line"].startswith("Scorecard: Maintain") +def test_apply_scorecards_refreshes_maintain_intent_alignment(sample_metadata, tmp_path): + scorecards_path = tmp_path / "scorecards.yaml" + scorecards_path.write_text( + """ +programs: + maintain: + label: Maintain + target_maturity: operating + rules: + - key: testing + label: Testing + check: dimension_at_least + dimension: testing + threshold: 0.80 + partial_threshold: 0.60 + weight: 1.0 +""" + ) + + audit = RepoAudit( + metadata=sample_metadata, + analyzer_results=[AnalyzerResult("testing", 1.0, 1.0, [], {})], + overall_score=1.0, + completeness_tier="", + flags=[], + lenses={"ship_readiness": {"score": 1.0}}, + security_posture={"score": 1.0}, + portfolio_catalog={ + "has_explicit_entry": True, + "intended_disposition": "maintain", + "maturity_program": "maintain", + "target_maturity": "operating", + "intent_alignment": "needs-review", + "intent_alignment_reason": "Stale pre-scorecard alignment.", + "operator_focus": "Act Now", + }, + ) + report = AuditReport.from_audits("testuser", [audit], [], 1) + report.operator_queue = [ + { + "repo": "test-repo", + "title": "Review test-repo", + "intent_alignment": "needs-review", + } + ] + + updated = cli._apply_scorecards(report, _make_args(scorecards=scorecards_path)) + + assert updated.audits[0].scorecard["status"] == "on-track" + assert updated.audits[0].portfolio_catalog["intent_alignment"] == "aligned" + assert "scorecard" in updated.audits[0].portfolio_catalog["intent_alignment_reason"] + assert updated.operator_queue[0]["intent_alignment"] == "aligned" + + def test_control_center_snapshot_rehydrates_portfolio_context( tmp_path, sample_metadata, diff --git a/tests/test_operator_os_seam_linter.py b/tests/test_operator_os_seam_linter.py index ddc6ae0..d8c4ab2 100644 --- a/tests/test_operator_os_seam_linter.py +++ b/tests/test_operator_os_seam_linter.py @@ -48,6 +48,12 @@ def _passing_paths(tmp_path: Path) -> tuple[Path, list[Path]]: return truth, [registry, report] +def _refresh_truth_for_cli(path: Path) -> None: + payload = json.loads(path.read_text()) + payload["generated_at"] = datetime.now(UTC).isoformat() + path.write_text(json.dumps(payload)) + + def _write_identity_truth(path: Path) -> None: _write_truth( path, @@ -405,6 +411,7 @@ def test_identity_resolution_since_includes_new_timestamped_rows(tmp_path: Path) def test_cli_identity_resolution_is_opt_in(tmp_path: Path) -> None: truth, markdown = _passing_paths(tmp_path) + _refresh_truth_for_cli(truth) bridge_db = tmp_path / "bridge.db" _write_bridge_db(bridge_db, session_cost_names=["085"]) @@ -445,6 +452,7 @@ def test_cli_identity_resolution_is_opt_in(tmp_path: Path) -> None: def test_cli_identity_since_filters_timestamped_identity_rows(tmp_path: Path) -> None: truth, markdown = _passing_paths(tmp_path) _write_identity_truth(truth) + _refresh_truth_for_cli(truth) bridge_db = tmp_path / "bridge.db" notification_db = tmp_path / "notification.sqlite3" notion_snapshot = tmp_path / "notion.json" diff --git a/tests/test_portfolio_catalog.py b/tests/test_portfolio_catalog.py index eea34ec..f6b3554 100644 --- a/tests/test_portfolio_catalog.py +++ b/tests/test_portfolio_catalog.py @@ -304,6 +304,28 @@ def test_evaluate_intent_alignment_uses_disposition_and_focus(): ) +def test_evaluate_intent_alignment_trusts_on_track_maintain_scorecard(): + maintain_entry = { + "has_explicit_entry": True, + "intended_disposition": "maintain", + "scorecard": { + "status": "on-track", + "maturity_level": "leading", + "target_maturity": "operating", + }, + } + + alignment, reason = evaluate_intent_alignment( + maintain_entry, + completeness_tier="", + archived=False, + operator_focus="Act Now", + ) + + assert alignment == "aligned" + assert "scorecard" in reason + + def test_catalog_line_and_summaries_cover_missing_contracts(): audits = [ {