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
7 changes: 6 additions & 1 deletion docs/portfolio-context-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions src/portfolio_context_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
"HANDOFF.md",
}
)
SUBSTANTIVE_README_MIN_CHARS = 2_000


@dataclass(frozen=True)
Expand Down Expand Up @@ -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}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require top-level primary context support

When discovery returns nested context files, such as docs/AGENTS.md, this basename-only set treats it the same as a top-level AGENTS.md. A repo with that nested file plus a substantive top-level README but no top-level AGENTS.md/CLAUDE.md will satisfy this helper, so reconciliation can promote an otherwise README-only minimum-viable active infra repo to standard, overstating the primary context contract; compare the relative paths directly before enabling the promotion.

Useful? React with 👍 / 👎.

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 ""
Expand Down
51 changes: 44 additions & 7 deletions src/portfolio_truth_reconcile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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
Comment on lines +187 to +188

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require catalog-backed infrastructure category

For repos whose catalog sets active/high/maintain but omits category, declared_values["category"] can still be infrastructure via the legacy registry fallback. In that scenario this new predicate promotes minimum-viable to standard and records workspace+catalog, even though the infrastructure evidence came from an old derived surface rather than the catalog-backed contract this rule is meant to require; check the category provenance or the repo/group catalog entries instead.

Useful? React with 👍 / 👎.

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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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"])
Expand Down
68 changes: 67 additions & 1 deletion tests/test_portfolio_context_contract.py
Original file line number Diff line number Diff line change
@@ -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 = (
Expand Down Expand Up @@ -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)
Expand Down
111 changes: 111 additions & 0 deletions tests/test_portfolio_truth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down