diff --git a/scripts/consistency_check.py b/scripts/consistency_check.py index ddee209..ed60999 100644 --- a/scripts/consistency_check.py +++ b/scripts/consistency_check.py @@ -38,6 +38,7 @@ Checks: C-32 work-record-not-indexed WARN Yes kind: intake/decision YAML block has no hub id — not indexed in DB (registration, CUST-WP-0061-T02) C-33 work-record-index-stale WARN Yes WORK-RECORDS.md missing or stale — generated per-repo index (CUST-WP-0061-T04) C-34 quality-dor-ready WARN No status=ready without quality_dor DoR-Ok (STATE-WP-0077 soft) + C-35 repo-manager-conformance WARN No Repo Manager flavor/standards contract reports findings (finished¬DoD-Ok is listed by `statehub quality-debt`, not per-file C-warn — avoids historical flood) Usage: @@ -73,6 +74,7 @@ import argparse import os import json import re +import shutil import socket import subprocess import sys @@ -164,6 +166,56 @@ _WORK_REQUEST_RE = re.compile( ) _OPEN_WORKPLAN_STATUSES = {"proposed", "ready", "active", "blocked", "backlog"} + +def _check_repo_manager_conformance(repo_dir: Path, report: "ConsistencyReport") -> None: + """C-35: consume Repo Manager's conformance contract without reimplementing it.""" + configured = os.environ.get("RMGR_BIN", "").strip() + candidates = [configured] if configured else [] + candidates.extend( + [ + str(Path.home() / "repo-manager" / ".venv" / "bin" / "rmgr"), + shutil.which("rmgr") or "", + ] + ) + executable = next((item for item in candidates if item and Path(item).is_file()), None) + if executable is None: + report.add( + severity="INFO", + check_id="C-35", + message="Repo Manager conformance adapter unavailable; set RMGR_BIN or install rmgr", + fixable=False, + ) + return + try: + completed = subprocess.run( + [executable, "conform", "--path", str(repo_dir)], + capture_output=True, + text=True, + check=False, + timeout=30, + ) + payload = json.loads(completed.stdout) + except (OSError, subprocess.TimeoutExpired, json.JSONDecodeError) as exc: + report.add( + severity="INFO", + check_id="C-35", + message=f"Repo Manager conformance adapter could not run: {exc}", + fixable=False, + ) + return + for finding in payload.get("findings") or []: + severity = str(finding.get("severity") or "warning") + report.add( + severity="WARN" if severity in {"missing", "contradictory", "warning"} else "INFO", + check_id="C-35", + message=( + f"Repo Manager {severity}: {finding.get('path') or '(repository)'}: " + f"{finding.get('message') or finding.get('code') or 'conformance finding'}" + ), + file_path=finding.get("path"), + fixable=False, + ) + # Legacy file/API aliases translated before comparison and PATCHing. FILE_TO_DB_WORKSTREAM_STATUS: dict[str, str] = dict(LEGACY_WORKSTREAM_STATUS_ALIASES) @@ -1158,6 +1210,9 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N fixable=False, ) + # C-35: consume the repository-standard answer from Repo Manager. + _check_repo_manager_conformance(repo_dir, report) + # C-31: work-record sidetrack detector (canon work-record-types registry) _check_unregistered_work_records(repo_dir, report) diff --git a/tests/test_consistency_check.py b/tests/test_consistency_check.py index 0a32c45..e32de95 100644 --- a/tests/test_consistency_check.py +++ b/tests/test_consistency_check.py @@ -12,6 +12,7 @@ No network calls, no DB, no live API — these tests run fully offline. """ from __future__ import annotations +import json import os import shutil import subprocess @@ -33,6 +34,7 @@ from consistency_check import ( _BACKGROUND_CHECKS, _api_get, _check_scope_freshness, + _check_repo_manager_conformance, _detect_behind_remote, _git_pull, _patch_frontmatter_field, @@ -69,6 +71,42 @@ class TestResolveTopicDomainSlug: assert resolve_topic_domain_slug("custodian", repo_market_domain="infotech") == "infotech" +class TestRepoManagerConformanceAdapter: + def test_surfaces_repo_manager_findings_without_reimplementing_rules(self, tmp_path, monkeypatch): + rmgr = tmp_path / "rmgr" + rmgr.write_text("stub", encoding="utf-8") + monkeypatch.setenv("RMGR_BIN", str(rmgr)) + payload = { + "ok": False, + "findings": [ + { + "code": "intent-and-goal", + "severity": "contradictory", + "path": "INTENT.md", + "message": "project flavor forbids both purpose files", + } + ], + } + monkeypatch.setattr( + "consistency_check.subprocess.run", + lambda *args, **kwargs: subprocess.CompletedProcess(args[0], 1, json.dumps(payload), ""), + ) + report = ConsistencyReport(repo_slug="demo", repo_path=str(tmp_path)) + _check_repo_manager_conformance(tmp_path, report) + assert len(report.warnings) == 1 + assert report.warnings[0].check_id == "C-35" + assert "project flavor forbids" in report.warnings[0].message + + def test_missing_adapter_is_informational(self, tmp_path, monkeypatch): + monkeypatch.setenv("RMGR_BIN", str(tmp_path / "missing")) + monkeypatch.setattr("consistency_check.shutil.which", lambda _name: None) + monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) + report = ConsistencyReport(repo_slug="demo", repo_path=str(tmp_path)) + _check_repo_manager_conformance(tmp_path, report) + assert len(report.infos) == 1 + assert report.infos[0].check_id == "C-35" + + # --------------------------------------------------------------------------- # parse_frontmatter # ---------------------------------------------------------------------------