Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/portfolio_context_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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:
Expand Down Expand Up @@ -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"
Expand All @@ -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 ""
Expand Down
7 changes: 7 additions & 0 deletions src/portfolio_truth_reconcile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand All @@ -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 []),
Expand Down Expand Up @@ -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"] = {
Expand Down
18 changes: 18 additions & 0 deletions tests/test_portfolio_context_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
52 changes: 52 additions & 0 deletions tests/test_portfolio_truth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down