From 4998c42f9e94fba64d948cba1b2b61f05e938fe2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 12:39:23 +0000 Subject: [PATCH] fix(readiness): exclude the resurrecting PyAutoCTI from the release gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Heart's main went red (27 readiness tests + the polled-count test) after the CTI-resurrection config change (358ef2f) added PyAutoCTI to the config 'libraries' group. That group feeds _GATE_SHA_LIBS, so the release-validation gate began demanding a matching commit_sha for PyAutoCTI on every verdict — but CTI is mid-resurrection and not released, so it has no release evidence to confirm. Result: every verdict picked up a 'release validation partially unconfirmed (unknown HEAD: PyAutoCTI)' stale (-12), and the fixtures (5 classic libs) never matched. Separate 'polled for health' from 'gates the release': - config: PyAutoCTI carries release_gate: false (polled, not gated; flip when it ships). - readiness: new load_release_gate_names() = polled libraries minus release_gate:false; _GATE_SHA_LIBS and the libs default now use it. load_library_names() stays the full polled set. - tests: polled-count sanity 22 -> 25 (CTI added 3 repos); new test asserting CTI is polled but not release-gating. Full suite: 287 passed. The one-word docs change on #90 was unrelated; this failure pre-dated it on main. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01ERDgNVKpQohYXVcJwAHzmw --- config/repos.yaml | 4 ++++ heart/readiness.py | 34 ++++++++++++++++++++++++++++------ tests/test_repo_config.py | 17 ++++++++++++++--- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/config/repos.yaml b/config/repos.yaml index 5c7cf3b..61fec56 100644 --- a/config/repos.yaml +++ b/config/repos.yaml @@ -22,6 +22,10 @@ repos: owner: PyAutoLabs - name: PyAutoCTI owner: PyAutoLabs + # Polled for health/CI, but NOT part of the release-validation gate yet: + # PyAutoCTI is mid-resurrection (not released), so it has no release + # evidence to confirm. Flip to true (or drop this key) when CTI ships. + release_gate: false build_workflow: - name: PyAutoBuild diff --git a/heart/readiness.py b/heart/readiness.py index c47f04c..4033043 100644 --- a/heart/readiness.py +++ b/heart/readiness.py @@ -136,9 +136,12 @@ def load_library_names(config_path: Path | str = CONFIG_PATH) -> list[str]: - """Return repos.libraries[].name from the policy file. Strict: the file - is in-repo and the group is load-bearing (the release gate) — a missing - or empty group is a config bug that must fail loudly.""" + """Return every polled library — repos.libraries[].name from the policy + file. Strict: the file is in-repo and the group is load-bearing — a missing + or empty group is a config bug that must fail loudly. + + This is the full *polled* set (what the tick monitors). The *release gate* + is a subset — see ``load_release_gate_names``.""" cfg = yaml.safe_load(Path(config_path).read_text()) or {} libs = [r["name"] for r in cfg["repos"]["libraries"]] if not libs: @@ -146,6 +149,23 @@ def load_library_names(config_path: Path | str = CONFIG_PATH) -> list[str]: return libs +def load_release_gate_names(config_path: Path | str = CONFIG_PATH) -> list[str]: + """The release-gating libraries: polled libraries minus those a library + marks ``release_gate: false``. A library can be polled for health/CI + (monitoring) without being part of the release-validation gate — e.g. a + library mid-resurrection that is not yet released and so has no release + evidence to confirm. Absent key ⇒ gating (the default); only an explicit + ``false`` opts out. Strict: an empty result is a config bug.""" + cfg = yaml.safe_load(Path(config_path).read_text()) or {} + libs = [ + r["name"] for r in cfg["repos"]["libraries"] + if r.get("release_gate", True) + ] + if not libs: + raise ValueError(f"no release-gating libraries in {config_path}") + return libs + + def _as_int(v: Any, default: int = 0) -> int: try: return int(v) @@ -163,8 +183,10 @@ def _as_int(v: Any, default: int = 0) -> int: VALIDATION_STALE_DAYS = 7 # The library repos whose current `main` HEAD must match a validation report's -# recorded commit_shas for the release-validation gate to go GREEN. -_GATE_SHA_LIBS = tuple(load_library_names()) +# recorded commit_shas for the release-validation gate to go GREEN. This is the +# release-gating subset (a resurrecting, not-yet-released library is polled but +# excluded — see load_release_gate_names). +_GATE_SHA_LIBS = tuple(load_release_gate_names()) def _sha_eq(a: Any, b: Any) -> bool: @@ -206,7 +228,7 @@ def compute( ) -> dict[str, Any]: """Pure verdict function. Never raises on missing/partial data.""" snapshot = snapshot or {} - libs = list(libraries) if libraries is not None else load_library_names() + libs = list(libraries) if libraries is not None else load_release_gate_names() req_wf = required_workflows if required_workflows is not None else load_required_workflows() repos = snapshot.get("repos", {}) or {} ref = _parse_ts(snapshot.get("ts")) or datetime.datetime.now(datetime.timezone.utc) diff --git a/tests/test_repo_config.py b/tests/test_repo_config.py index 5494ef2..20ab649 100644 --- a/tests/test_repo_config.py +++ b/tests/test_repo_config.py @@ -49,10 +49,21 @@ def test_thresholds_have_expected_fields(config): assert thresholds["script_timing"]["baseline_window"] >= 3 -def test_22_repos_polled(config): - """Sanity check the polled count — bumps need a deliberate update.""" +def test_25_repos_polled(config): + """Sanity check the polled count — bumps need a deliberate update. + (25 since the CTI resurrection added PyAutoCTI + autocti_workspace + + autocti_workspace_test to the polled registry.)""" total = sum(len(v) for v in config["repos"].values()) - assert total == 22, f"expected 22 polled repos, got {total}" + assert total == 25, f"expected 25 polled repos, got {total}" + + +def test_cti_polled_but_not_release_gating(config): + """A resurrecting library is polled for health but excluded from the + release-validation gate until it ships (release_gate: false).""" + from heart import readiness + + assert "PyAutoCTI" in readiness.load_library_names() + assert "PyAutoCTI" not in readiness.load_release_gate_names() def _all_names(config):