Coverage for little_loops / learning_tests / release_gate.py: 0%
53 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-29 00:54 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-29 00:54 -0500
1"""Pre-release audit gate for learning test records (ENH-2214).
3Checks whether any actively-imported packages have stale or refuted learning
4test records. Returns 0 (pass) or 1 (block) based on the ``release_gate``
5config value.
7Called from ``/ll:manage-release`` before creating a git tag.
8"""
10from __future__ import annotations
12import datetime
13import json
14from pathlib import Path
16from little_loops.config.core import resolve_config_path
17from little_loops.config.features import LearningTestsConfig
18from little_loops.learning_tests import list_records
19from little_loops.learning_tests.gate import is_record_stale
20from little_loops.learning_tests.import_scan import get_imported_packages
23def _load_lt_config(cwd: Path) -> LearningTestsConfig:
24 config_path = resolve_config_path(cwd)
25 if config_path is None:
26 return LearningTestsConfig()
27 try:
28 data = json.loads(config_path.read_text(encoding="utf-8"))
29 except (OSError, json.JSONDecodeError):
30 return LearningTestsConfig()
31 if not isinstance(data, dict):
32 return LearningTestsConfig()
33 return LearningTestsConfig.from_dict(data.get("learning_tests", {}))
36def run_release_gate(cwd: Path, *, base_dir: Path | None = None) -> int:
37 """Run the learning-test pre-release audit gate.
39 Args:
40 cwd: Project root used to load config and resolve ``scan_dirs``.
41 base_dir: Override for the ``.ll/learning-tests`` directory (useful in tests).
43 Returns:
44 0 if the gate passes (no issues, ``release_gate="warn"``, or ``enabled=False``).
45 1 if ``release_gate="block"`` and stale/refuted active packages are found.
46 """
47 lt_config = _load_lt_config(cwd)
49 if not lt_config.enabled:
50 return 0
52 resolved_base = base_dir if base_dir is not None else cwd / ".ll" / "learning-tests"
53 records = list_records(base_dir=resolved_base)
55 problem_records = [
56 r
57 for r in records
58 if r.status == "refuted" or is_record_stale(r, lt_config.stale_after_days)
59 ]
61 if not problem_records:
62 return 0
64 source_dirs = [cwd / d for d in lt_config.scan_dirs]
65 imported_packages = get_imported_packages(source_dirs)
67 hits = [r for r in problem_records if r.target in imported_packages]
69 if not hits:
70 return 0
72 today = datetime.date.today()
73 print("")
74 print("⚠ Learning Test Pre-Release Audit")
75 print(f"{'Package':<30} {'Status':<10} {'Record Date':<14} Days Since Proven")
76 print("-" * 70)
77 for record in hits:
78 status = "refuted" if record.status == "refuted" else "stale"
79 try:
80 record_date = datetime.date.fromisoformat(record.date)
81 age = str((today - record_date).days)
82 except (ValueError, TypeError):
83 age = "?"
84 print(f"{record.target:<30} {status:<10} {record.date:<14} {age}")
85 print("")
87 if lt_config.release_gate == "block":
88 print(
89 "✗ Release blocked: fix or re-prove the above records, "
90 "or set release_gate: warn to proceed."
91 )
92 return 1
94 print("⚠ Continuing with warning (set release_gate: block to abort on stale/refuted records).")
95 return 0