diff --git a/src/github_repo_auditor/portfolio_truth_publish.py b/src/github_repo_auditor/portfolio_truth_publish.py index 2f8f65f..45622b5 100644 --- a/src/github_repo_auditor/portfolio_truth_publish.py +++ b/src/github_repo_auditor/portfolio_truth_publish.py @@ -566,18 +566,8 @@ def _publish_portfolio_truth_locked( build_result.snapshot, latest_name ) - with tempfile.NamedTemporaryFile( - "w", delete=False, dir=output_dir, suffix=".registry-check.md" - ) as handle: - temp_registry_path = Path(handle.name) - try: - validate_registry_markdown( - registry_markdown, build_result.snapshot, temp_registry_path - ) - validate_portfolio_report_markdown(report_markdown) - finally: - if temp_registry_path.exists(): - temp_registry_path.unlink() + validate_registry_markdown(registry_markdown, build_result.snapshot) + validate_portfolio_report_markdown(report_markdown) targets = { snapshot_path: snapshot_json, diff --git a/src/github_repo_auditor/portfolio_truth_validate.py b/src/github_repo_auditor/portfolio_truth_validate.py index ab4952e..aa92d9a 100644 --- a/src/github_repo_auditor/portfolio_truth_validate.py +++ b/src/github_repo_auditor/portfolio_truth_validate.py @@ -65,7 +65,7 @@ SecurityFields, ) from github_repo_auditor.producer_preflight import ProducerEvidence -from github_repo_auditor.registry_parser import _normalize, parse_registry +from github_repo_auditor.registry_parser import _normalize, parse_registry_markdown from github_repo_auditor.security_admission import derive_security_admission @@ -2323,14 +2323,9 @@ def validate_publish_targets( def validate_registry_markdown( - markdown: str, snapshot: PortfolioTruthSnapshot, temp_path: Path + markdown: str, snapshot: PortfolioTruthSnapshot, temp_path: Path | None = None ) -> None: - temp_path.write_text(markdown) - try: - parsed = parse_registry(temp_path) - finally: - if temp_path.exists(): - temp_path.unlink() + parsed = parse_registry_markdown(markdown) expected_labels = registry_project_labels(snapshot.projects).values() expected = {_normalize(label.strip()) for label in expected_labels} parsed_names = {_normalize(name) for name in parsed} diff --git a/src/github_repo_auditor/registry_parser.py b/src/github_repo_auditor/registry_parser.py index f7177ba..a0d157b 100644 --- a/src/github_repo_auditor/registry_parser.py +++ b/src/github_repo_auditor/registry_parser.py @@ -28,13 +28,12 @@ def _normalize(name: str) -> str: return n -def parse_registry(path: Path) -> dict[str, str]: - """Parse project-registry.md into {project_name: status} mapping. +def parse_registry_markdown(content: str) -> dict[str, str]: + """Parse registry markdown into {project_name: status} mapping. Handles variable-column markdown tables across multiple sections. Column 1 is always Project, Column 2 is always Status. """ - content = path.read_text(errors="replace") projects: dict[str, str] = {} for line in content.splitlines(): @@ -76,6 +75,11 @@ def parse_registry(path: Path) -> dict[str, str]: return projects +def parse_registry(path: Path) -> dict[str, str]: + """Parse project-registry.md into {project_name: status} mapping.""" + return parse_registry_markdown(path.read_text(errors="replace")) + + @dataclass class RegistryReconciliation: on_github_not_registry: list[str] diff --git a/tests/test_portfolio_truth.py b/tests/test_portfolio_truth.py index da43ce2..5c885e1 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -57,6 +57,7 @@ _same_repository_path, canonicalize_prior_security_truth_payload, validate_portfolio_report_markdown, + validate_registry_markdown, validate_truth_snapshot, ) from github_repo_auditor.project_registry import build_project_registry @@ -4766,6 +4767,26 @@ def test_rendered_registry_round_trips_through_parser( assert "## Cowork Task Notes" in markdown +def test_registry_validation_does_not_write_temp_markdown( + portfolio_workspace: Path, + portfolio_catalog: Path, + legacy_registry: Path, + tmp_path: Path, +) -> None: + result = build_portfolio_truth_snapshot( + workspace_root=portfolio_workspace, + catalog_path=portfolio_catalog, + legacy_registry_path=legacy_registry, + include_notion=False, + ) + markdown = render_registry_markdown(result.snapshot) + temp_path = tmp_path / "registry-check.md" + + validate_registry_markdown(markdown, result.snapshot, temp_path) + + assert not temp_path.exists() + + def test_registry_render_surfaces_security_and_round_trips( portfolio_workspace: Path, portfolio_catalog: Path, diff --git a/tests/test_registry_parser.py b/tests/test_registry_parser.py index a7085fa..0feb5eb 100644 --- a/tests/test_registry_parser.py +++ b/tests/test_registry_parser.py @@ -3,7 +3,12 @@ from datetime import datetime, timezone from github_repo_auditor.models import RepoAudit, RepoMetadata -from github_repo_auditor.registry_parser import _normalize, parse_registry, reconcile +from github_repo_auditor.registry_parser import ( + _normalize, + parse_registry, + parse_registry_markdown, + reconcile, +) def _make_audit(name: str, tier: str = "functional", score: float = 0.6) -> RepoAudit: @@ -47,7 +52,7 @@ def test_strips_readiness_suffix(self): class TestParseRegistry: def test_parses_simple_table(self, tmp_path): registry = tmp_path / "registry.md" - registry.write_text( + markdown = ( "# Projects\n\n" "| Project | Status | Notes |\n" "|---------|--------|-------|\n" @@ -55,8 +60,10 @@ def test_parses_simple_table(self, tmp_path): "| Beta | parked | Stale |\n" "| Gamma | archived | Legacy |\n" ) + registry.write_text(markdown) result = parse_registry(registry) assert result == {"Alpha": "active", "Beta": "parked", "Gamma": "archived"} + assert parse_registry_markdown(markdown) == result def test_skips_invalid_status(self, tmp_path): registry = tmp_path / "registry.md"