1"""Pre-release audit gate for learning test records (ENH-2214).

2

3 Checks whether any actively-imported packages have stale or refuted learning

4 test records. Returns 0 (pass) or 1 (block) based on the ``release_gate``

5 config value.

6

7 Called from ``/ll:manage-release`` before creating a git tag.

8"""

9

10 from __future__ import annotations

11

12 import datetime

13 import json

14 from pathlib import Path

15

16 from little_loops.config.core import resolve_config_path

17 from little_loops.config.features import LearningTestsConfig

18 from little_loops.learning_tests import list_records

19 from little_loops.learning_tests.gate import is_record_stale

20 from little_loops.learning_tests.import_scan import get_imported_packages

21

22

23 def _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", {}))

34

35

36 def run_release_gate(cwd: Path, *, base_dir: Path | None = None) -> int:

37 """Run the learning-test pre-release audit gate.

38

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).

42

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)

48

49 if not lt_config.enabled:

50 return 0

51

52 resolved_base = base_dir if base_dir is not None else cwd / ".ll" / "learning-tests"

53 records = list_records(base_dir=resolved_base)

54

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 ]

60

61 if not problem_records:

62 return 0

63

64 source_dirs = [cwd / d for d in lt_config.scan_dirs]

65 imported_packages = get_imported_packages(source_dirs)

66

67 hits = [r for r in problem_records if r.target in imported_packages]

68

69 if not hits:

70 return 0

71

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("")

86

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

93

94 print("⚠ Continuing with warning (set release_gate: block to abort on stale/refuted records).")

95 return 0