From a22cf140b398e114f61e3baab960de1c275c90fe Mon Sep 17 00:00:00 2001 From: Saagar Date: Sat, 4 Jul 2026 02:28:00 -0700 Subject: [PATCH] fix: require catalog-backed context promotion --- src/portfolio_context_contract.py | 14 +++++-- src/portfolio_truth_reconcile.py | 7 ++++ tests/test_portfolio_context_contract.py | 18 ++++++++ tests/test_portfolio_truth.py | 52 ++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/portfolio_context_contract.py b/src/portfolio_context_contract.py index 76f0981..08fdbcf 100644 --- a/src/portfolio_context_contract.py +++ b/src/portfolio_context_contract.py @@ -129,7 +129,7 @@ class ContextAnalysis: # Utility: prefers CLAUDE.md over AGENTS.md when both present. # Called internally by analyze_project_context() in this module. def choose_primary_context_file(context_files: list[str]) -> str: - normalized = {Path(item).name for item in context_files} + normalized = _top_level_context_file_names(context_files) if "CLAUDE.md" in normalized: return "CLAUDE.md" return "AGENTS.md" @@ -139,7 +139,7 @@ def analyze_project_context( project_path: Path, context_files: list[str], *, readme_text: str = "" ) -> ContextAnalysis: primary_context_file = choose_primary_context_file(context_files) - context_file_names = {Path(item).name for item in context_files} + context_file_names = _top_level_context_file_names(context_files) primary_exists = primary_context_file in context_file_names primary_text = "" if primary_exists: @@ -259,7 +259,7 @@ def friendly_missing_fields(analysis: ContextAnalysis) -> list[str]: def has_substantive_readme_support( primary_context_file: str, context_files: list[str], readme_char_count: int ) -> bool: - context_file_names = {Path(item).name for item in context_files} + context_file_names = _top_level_context_file_names(context_files) return ( primary_context_file in context_file_names and primary_context_file != "README.md" @@ -268,6 +268,14 @@ def has_substantive_readme_support( ) +def _top_level_context_file_names(context_files: list[str]) -> set[str]: + return { + path.name + for item in context_files + if (path := Path(item)).parent in {Path("."), Path("")} + } + + def _read_small_text(path: Path) -> str: if not path.is_file(): return "" diff --git a/src/portfolio_truth_reconcile.py b/src/portfolio_truth_reconcile.py index 2981cfd..e86786a 100644 --- a/src/portfolio_truth_reconcile.py +++ b/src/portfolio_truth_reconcile.py @@ -174,6 +174,7 @@ def _catalog_supported_context_quality( *, raw_project: dict[str, Any], declared_values: dict[str, Any], + provenance: dict[str, dict[str, str]], readme_char_count: int, ) -> str: if raw_context_quality != "minimum-viable": @@ -186,6 +187,11 @@ def _catalog_supported_context_quality( return raw_context_quality if declared_values.get("category") != "infrastructure": return raw_context_quality + if provenance.get("declared.category", {}).get("source") not in { + "catalog_repo", + "catalog_group", + }: + return raw_context_quality if not has_substantive_readme_support( str(raw_project.get("primary_context_file") or ""), list(raw_project.get("context_files") or []), @@ -498,6 +504,7 @@ def _build_truth_project( raw_context_quality, raw_project=raw_project, declared_values=declared_values, + provenance=provenance, readme_char_count=derived_readme_char_count, ) provenance["derived.context_quality"] = { diff --git a/tests/test_portfolio_context_contract.py b/tests/test_portfolio_context_contract.py index 9cf97df..739f513 100644 --- a/tests/test_portfolio_context_contract.py +++ b/tests/test_portfolio_context_contract.py @@ -121,6 +121,24 @@ def test_readme_only_context_stays_minimum_viable_without_separate_primary(tmp_p assert result.context_quality == "minimum-viable" +def test_substantive_readme_support_requires_top_level_primary_context(tmp_path): + readme = "# Project\n\n" + ("README-level operator workflow detail. " * 80) + _write(tmp_path, "README.md", readme) + (tmp_path / "docs").mkdir() + _write(tmp_path / "docs", "AGENTS.md", "# Nested guidance\n") + + result = analyze_project_context(tmp_path, ["docs/AGENTS.md", "README.md"]) + + assert ( + has_substantive_readme_support( + result.primary_context_file, + ["docs/AGENTS.md", "README.md"], + len(readme), + ) + is False + ) + + def test_explicit_readme_text_override_is_honored(tmp_path): # The dormant readme_text param now works as an explicit override (no disk read needed). _write(tmp_path, "AGENTS.md", _GENERIC_AGENTS) diff --git a/tests/test_portfolio_truth.py b/tests/test_portfolio_truth.py index 6d657dd..31a1d7b 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -873,6 +873,58 @@ def test_substantive_readme_support_does_not_promote_non_infra_repo(tmp_path: Pa assert product.provenance["derived.context_quality"]["source"] == "workspace" +def test_legacy_registry_infra_category_does_not_promote_context_quality( + tmp_path: Path, +) -> None: + workspace = tmp_path / "workspace" + workspace.mkdir() + project = workspace / "LegacyInfra" + project.mkdir() + subprocess.run(["git", "init"], cwd=project, capture_output=True, check=True) + _write(project / "AGENTS.md", _complete_agent_context("LegacyInfra")) + _write(project / "README.md", _substantive_readme("LegacyInfra")) + _write(project / "pyproject.toml", "[project]\nname = \"legacy-infra\"\n") + catalog_path = tmp_path / "portfolio-catalog.yaml" + catalog_path.write_text( + """ +repos: + LegacyInfra: + owner: d + lifecycle_state: active + criticality: high + review_cadence: weekly + intended_disposition: maintain +""" + ) + legacy_registry_path = tmp_path / "project-registry.md" + legacy_registry_path.write_text( + """ +# Project Registry + +## Standalone Projects + +| Project | Status | Tool | Context Quality | Stack | Context Files | Category | Notes | +|---------|--------|------|-----------------|-------|---------------|----------|-------| +| LegacyInfra | active | codex | minimum-viable | Python | AGENTS.md | infrastructure | legacy category only | +""" + ) + + result = build_portfolio_truth_snapshot( + workspace_root=workspace, + catalog_path=catalog_path, + legacy_registry_path=legacy_registry_path, + include_notion=False, + ) + + infra = next( + project for project in result.snapshot.projects if project.identity.project_key == "LegacyInfra" + ) + assert infra.declared.category == "infrastructure" + assert infra.provenance["declared.category"]["source"] == "legacy_registry" + assert infra.derived.context_quality == "minimum-viable" + assert infra.provenance["derived.context_quality"]["source"] == "workspace" + + def test_rendered_registry_round_trips_through_parser( portfolio_workspace: Path, portfolio_catalog: Path,