diff --git a/src/portfolio_context_triage.py b/src/portfolio_context_triage.py index d9ac750..af0fec8 100644 --- a/src/portfolio_context_triage.py +++ b/src/portfolio_context_triage.py @@ -1,5 +1,6 @@ # src/portfolio_context_triage.py """Context triage runner — B1 of Arc H operational stream.""" + from __future__ import annotations from dataclasses import dataclass @@ -17,6 +18,9 @@ class FailureMode(str, Enum): _DESCRIPTION_CONFIDENCE_WARN_BELOW = 0.5 _CATALOG_COMPLETENESS_WARN_BELOW = 0.6 _WEAK_CONTEXT_QUALITIES = {"none", "boilerplate"} +# Context quality measures "can someone resume this?" — irrelevant for repos +# that are intentionally not under active development, so don't flag them. +_CONTEXT_EXEMPT_LIFECYCLES = {"archived", "dormant"} def assess_repo_failure_modes(repo: dict[str, Any]) -> list[FailureMode]: @@ -45,12 +49,20 @@ def assess_repo_failure_modes(repo: dict[str, Any]) -> list[FailureMode]: modes.append(FailureMode.CATALOG) context_quality = _repo_context_quality(repo) - if context_quality in _WEAK_CONTEXT_QUALITIES: + if ( + context_quality in _WEAK_CONTEXT_QUALITIES + and _repo_lifecycle_state(repo) not in _CONTEXT_EXEMPT_LIFECYCLES + ): modes.append(FailureMode.CONTEXT) return modes +def _repo_lifecycle_state(repo: dict[str, Any]) -> str: + declared = repo.get("declared") if isinstance(repo.get("declared"), dict) else {} + return str(declared.get("lifecycle_state") or "").strip().lower() + + @dataclass class TriageEntry: repo_name: str @@ -95,8 +107,4 @@ def _repo_context_quality(repo: dict[str, Any]) -> str: def run_triage(repos: list[dict[str, Any]]) -> list[TriageEntry]: """Return TriageEntry for every repo that has at least one failure mode.""" - return [ - TriageEntry.from_repo(repo) - for repo in repos - if assess_repo_failure_modes(repo) - ] + return [TriageEntry.from_repo(repo) for repo in repos if assess_repo_failure_modes(repo)] diff --git a/tests/test_portfolio_context_triage.py b/tests/test_portfolio_context_triage.py index c9732d5..b1959fa 100644 --- a/tests/test_portfolio_context_triage.py +++ b/tests/test_portfolio_context_triage.py @@ -1,5 +1,6 @@ # tests/test_portfolio_context_triage.py """Tests for the context triage runner (Arc H B1).""" + import json from src.portfolio_context_triage import ( @@ -15,6 +16,7 @@ def _entry( readme_stale_by_age=False, catalog_completeness=1.0, context_quality="full", + lifecycle_state="active", ) -> dict: return { "name": "test-repo", @@ -24,6 +26,7 @@ def _entry( }, "catalog_completeness": catalog_completeness, "context_quality": context_quality, + "declared": {"lifecycle_state": lifecycle_state}, } @@ -62,6 +65,45 @@ def test_nested_portfolio_truth_context_quality_flagged(): assert FailureMode.CONTEXT in modes +# Context quality is a "can someone resume this?" signal — irrelevant for repos +# that are intentionally not under active development. Archived and dormant +# repos must not be context-flagged even when their docs are boilerplate. + + +def test_archived_repo_not_context_flagged(): + modes = assess_repo_failure_modes( + _entry(context_quality="boilerplate", lifecycle_state="archived") + ) + assert FailureMode.CONTEXT not in modes + + +def test_dormant_repo_not_context_flagged(): + modes = assess_repo_failure_modes(_entry(context_quality="none", lifecycle_state="dormant")) + assert FailureMode.CONTEXT not in modes + + +def test_active_repo_with_weak_context_still_flagged(): + modes = assess_repo_failure_modes( + _entry(context_quality="boilerplate", lifecycle_state="active") + ) + assert FailureMode.CONTEXT in modes + + +def test_maintenance_repo_with_weak_context_still_flagged(): + modes = assess_repo_failure_modes( + _entry(context_quality="boilerplate", lifecycle_state="maintenance") + ) + assert FailureMode.CONTEXT in modes + + +def test_missing_lifecycle_state_defaults_to_flagged(): + # no declared.lifecycle_state -> enforce context quality (do not suppress) + modes = assess_repo_failure_modes( + {"identity": {"display_name": "RepoB"}, "context_quality": "none"} + ) + assert FailureMode.CONTEXT in modes + + def test_severity_critical_when_multiple_failure_modes(): entry = _entry( description_confidence=0.2,