From c6db57f21bf08408a9aa7869be795d568422569a Mon Sep 17 00:00:00 2001 From: Saagar Date: Sat, 4 Jul 2026 00:57:25 -0700 Subject: [PATCH] Recognize catalog-backed infra context --- docs/portfolio-context-contract.md | 7 +- src/portfolio_context_contract.py | 13 +++ src/portfolio_truth_reconcile.py | 51 +++++++++-- tests/test_portfolio_context_contract.py | 68 +++++++++++++- tests/test_portfolio_truth.py | 111 +++++++++++++++++++++++ 5 files changed, 241 insertions(+), 9 deletions(-) diff --git a/docs/portfolio-context-contract.md b/docs/portfolio-context-contract.md index 2b19378..117300a 100644 --- a/docs/portfolio-context-contract.md +++ b/docs/portfolio-context-contract.md @@ -100,10 +100,15 @@ Accepted aliases include: - `boilerplate` - primary context exists, but one or more required minimum fields are still missing - `minimum-viable` - - all six required fields are present in the primary context file + - all six required fields are present in the primary context file, or are + present across the primary context file and top-level README fallback - `standard` - minimum-viable plus at least one supporting handoff-style artifact such as `HANDOFF.md`, `IMPLEMENTATION-ROADMAP.md`, `STATUS.md`, `PLAN.md`, or `RESUMPTION-PROMPT.md` + - during portfolio-truth reconciliation only, catalog-backed active infrastructure with + high criticality and `maintain` disposition can also qualify when a separate substantive + top-level `README.md` supports a complete `AGENTS.md` or `CLAUDE.md`. Raw context-file + classification stays conservative so broad README presence does not inflate weak repos. - `full` - standard plus multiple supporting artifacts, including at least one high-signal handoff or discovery doc diff --git a/src/portfolio_context_contract.py b/src/portfolio_context_contract.py index 438318d..76f0981 100644 --- a/src/portfolio_context_contract.py +++ b/src/portfolio_context_contract.py @@ -109,6 +109,7 @@ "HANDOFF.md", } ) +SUBSTANTIVE_README_MIN_CHARS = 2_000 @dataclass(frozen=True) @@ -255,6 +256,18 @@ def friendly_missing_fields(analysis: ContextAnalysis) -> list[str]: return list(analysis.missing_fields) +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} + return ( + primary_context_file in context_file_names + and primary_context_file != "README.md" + and "README.md" in context_file_names + and readme_char_count >= SUBSTANTIVE_README_MIN_CHARS + ) + + 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 9c014e8..2981cfd 100644 --- a/src/portfolio_truth_reconcile.py +++ b/src/portfolio_truth_reconcile.py @@ -13,6 +13,7 @@ group_entry_for_path, load_portfolio_catalog, ) +from src.portfolio_context_contract import has_substantive_readme_support from src.portfolio_pathing import build_operating_path_entry from src.portfolio_risk import build_risk_entry from src.portfolio_truth_sources import ( @@ -50,7 +51,7 @@ "declared.tool_provenance": ["catalog_repo", "catalog_group", "inference", "legacy_registry"], "declared.notes": ["catalog_repo", "catalog_group", "legacy_registry"], "derived.stack": ["workspace", "legacy_registry"], - "derived.context_quality": ["workspace"], + "derived.context_quality": ["workspace", "catalog_repo", "catalog_group"], "derived.context_files": ["workspace"], "derived.primary_context_file": ["workspace"], "derived.project_summary_present": ["workspace"], @@ -168,6 +169,32 @@ def _derive_readme_char_count(project_path: Path | None, has_git: bool) -> int: return 0 +def _catalog_supported_context_quality( + raw_context_quality: str, + *, + raw_project: dict[str, Any], + declared_values: dict[str, Any], + readme_char_count: int, +) -> str: + if raw_context_quality != "minimum-viable": + return raw_context_quality + if declared_values.get("lifecycle_state") != "active": + return raw_context_quality + if declared_values.get("criticality") != "high": + return raw_context_quality + if declared_values.get("intended_disposition") != "maintain": + return raw_context_quality + if declared_values.get("category") != "infrastructure": + 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 []), + readme_char_count, + ): + return raw_context_quality + return "standard" + + @dataclass(frozen=True) class PortfolioTruthBuildResult: snapshot: PortfolioTruthSnapshot @@ -463,10 +490,23 @@ def _build_truth_project( "automation_eligible": bool(repo_entry.get("automation_eligible", False)), } - context_quality = raw_project["context_quality"] + project_path: Path | None = raw_project.get("project_path") + has_git = bool(raw_project["has_git"]) + derived_readme_char_count = _derive_readme_char_count(project_path, has_git) + raw_context_quality = raw_project["context_quality"] + context_quality = _catalog_supported_context_quality( + raw_context_quality, + raw_project=raw_project, + declared_values=declared_values, + readme_char_count=derived_readme_char_count, + ) provenance["derived.context_quality"] = { - "source": "workspace", - "detail": raw_project["context_quality"], + "source": "workspace+catalog" if context_quality != raw_context_quality else "workspace", + "detail": ( + f"{raw_context_quality}->{context_quality}" + if context_quality != raw_context_quality + else raw_context_quality + ), } status_entry = _select_repo_status_entry( @@ -624,12 +664,9 @@ def _build_truth_project( ) # ── Strict local-filesystem signals (Sprint 8.2) ───────────────────────── - project_path: Path | None = raw_project.get("project_path") - has_git = bool(raw_project["has_git"]) derived_has_tests = _derive_has_tests(project_path, has_git) derived_has_ci = _derive_has_ci(project_path, has_git) derived_has_license = _derive_has_license(project_path, has_git) - derived_readme_char_count = _derive_readme_char_count(project_path, has_git) derived_release_count: int | None = None if release_count_by_name is not None: derived_release_count = release_count_by_name.get(raw_project["name"]) diff --git a/tests/test_portfolio_context_contract.py b/tests/test_portfolio_context_contract.py index d017a48..9cf97df 100644 --- a/tests/test_portfolio_context_contract.py +++ b/tests/test_portfolio_context_contract.py @@ -1,6 +1,9 @@ from pathlib import Path -from src.portfolio_context_contract import analyze_project_context +from src.portfolio_context_contract import ( + analyze_project_context, + has_substantive_readme_support, +) # A generic Codex-OS-style AGENTS.md stub: no Portfolio-Context sections, no run guidance. _GENERIC_AGENTS = ( @@ -55,6 +58,69 @@ def test_context_quality_not_none_for_readme_only_repo(tmp_path): assert result.run_instructions_present is True +def test_substantive_readme_support_does_not_promote_raw_context_quality(tmp_path): + agents = ( + "# AGENTS.md\n\n" + "## What This Project Is\n\nA local operating tool for agent coordination.\n\n" + "## Current State\n\nActive infrastructure with verified local checks.\n\n" + "## Stack\n\nPython and shell scripts.\n\n" + "## How To Run\n\nRun `pytest tests` before publishing changes.\n\n" + "## Known Risks\n\nLocal machine paths can drift.\n\n" + "## Next Recommended Move\n\nKeep the verification surface green.\n" + ) + readme = ( + "# Project\n\n" + "This README is intentionally substantive and separate from the primary " + "agent guidance. " + + ("It explains operator workflows, boundaries, commands, and examples. " * 40) + ) + _write(tmp_path, "AGENTS.md", agents) + _write(tmp_path, "README.md", readme) + result = analyze_project_context(tmp_path, ["AGENTS.md", "README.md"]) + assert result.context_quality == "minimum-viable" + assert ( + has_substantive_readme_support( + result.primary_context_file, + ["AGENTS.md", "README.md"], + len(readme), + ) + is True + ) + + +def test_short_readme_support_does_not_promote_complete_primary(tmp_path): + agents = ( + "# AGENTS.md\n\n" + "## What This Project Is\n\nA local operating tool for agent coordination.\n\n" + "## Current State\n\nActive infrastructure with verified local checks.\n\n" + "## Stack\n\nPython and shell scripts.\n\n" + "## How To Run\n\nRun `pytest tests` before publishing changes.\n\n" + "## Known Risks\n\nLocal machine paths can drift.\n\n" + "## Next Recommended Move\n\nKeep the verification surface green.\n" + ) + _write(tmp_path, "AGENTS.md", agents) + _write(tmp_path, "README.md", "# Project\n\nA short companion README.\n") + result = analyze_project_context(tmp_path, ["AGENTS.md", "README.md"]) + assert result.context_quality == "minimum-viable" + + +def test_readme_only_context_stays_minimum_viable_without_separate_primary(tmp_path): + readme = ( + "# Project\n\n" + "This README is intentionally long and complete. " + + ("It documents the project thoroughly. " * 45) + + "\n\n## Overview\n\nA durable local operator tool for coordinating active work.\n\n" + + "\n\n## Current State\n\nActive and verified.\n\n" + "## Stack\n\nPython, pytest, shell scripts, and local JSON state files.\n\n" + "## Usage\n\nRun `pytest tests`.\n\n" + "## Known Risks\n\nLocal paths can drift.\n\n" + "## Next Steps\n\nKeep checks green.\n" + ) + _write(tmp_path, "README.md", readme) + result = analyze_project_context(tmp_path, ["README.md"]) + assert result.context_quality == "minimum-viable" + + 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 c7d9e64..6d657dd 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -762,6 +762,117 @@ def test_primary_context_with_required_sections_becomes_minimum_viable(tmp_path: assert _classify_context_quality(project, ["AGENTS.md"]) == "minimum-viable" +def _complete_agent_context(project_name: str) -> str: + return f"""# {project_name} + +## What This Project Is + +{project_name} is local infrastructure used to coordinate operator workflows. + +## Current State + +Active and maintained, with the core workflow already running locally. + +## Stack + +Python, shell scripts, and local JSON artifacts. + +## How To Run + +Run `pytest tests` before publishing changes. + +## Known Risks + +Local paths and machine-specific dependencies can drift. + +## Next Recommended Move + +Keep the verification surface green and refresh context when workflows change. +""" + + +def _substantive_readme(project_name: str) -> str: + return ( + f"# {project_name}\n\n" + f"{project_name} is a durable operator infrastructure repo with separate " + "agent guidance and README-level workflow documentation.\n\n" + + ("It documents commands, boundaries, recovery paths, examples, and operator usage. " * 35) + ) + + +def test_catalog_backed_high_criticality_infra_readme_support_promotes_to_standard( + tmp_path: Path, +) -> None: + workspace = tmp_path / "workspace" + workspace.mkdir() + project = workspace / "InfraRepo" + project.mkdir() + subprocess.run(["git", "init"], cwd=project, capture_output=True, check=True) + _write(project / "AGENTS.md", _complete_agent_context("InfraRepo")) + _write(project / "README.md", _substantive_readme("InfraRepo")) + _write(project / "pyproject.toml", "[project]\nname = \"infra-repo\"\n") + catalog_path = tmp_path / "portfolio-catalog.yaml" + catalog_path.write_text( + """ +repos: + InfraRepo: + owner: d + lifecycle_state: active + criticality: high + review_cadence: weekly + intended_disposition: maintain + category: infrastructure +""" + ) + + result = build_portfolio_truth_snapshot( + workspace_root=workspace, + catalog_path=catalog_path, + include_notion=False, + ) + + infra = next(project for project in result.snapshot.projects if project.identity.project_key == "InfraRepo") + assert infra.derived.context_quality == "standard" + assert infra.provenance["derived.context_quality"]["source"] == "workspace+catalog" + assert infra.provenance["derived.context_quality"]["detail"] == "minimum-viable->standard" + + +def test_substantive_readme_support_does_not_promote_non_infra_repo(tmp_path: Path) -> None: + workspace = tmp_path / "workspace" + workspace.mkdir() + project = workspace / "ProductRepo" + project.mkdir() + subprocess.run(["git", "init"], cwd=project, capture_output=True, check=True) + _write(project / "AGENTS.md", _complete_agent_context("ProductRepo")) + _write(project / "README.md", _substantive_readme("ProductRepo")) + _write(project / "package.json", '{"dependencies":{"react":"19.0.0"}}') + catalog_path = tmp_path / "portfolio-catalog.yaml" + catalog_path.write_text( + """ +repos: + ProductRepo: + owner: d + lifecycle_state: active + criticality: high + review_cadence: weekly + intended_disposition: maintain + category: commercial +""" + ) + + result = build_portfolio_truth_snapshot( + workspace_root=workspace, + catalog_path=catalog_path, + include_notion=False, + ) + + product = next( + project for project in result.snapshot.projects if project.identity.project_key == "ProductRepo" + ) + assert product.derived.context_quality == "minimum-viable" + assert product.provenance["derived.context_quality"]["source"] == "workspace" + + def test_rendered_registry_round_trips_through_parser( portfolio_workspace: Path, portfolio_catalog: Path,