"""Freeze coupling in scripts/jyotish_api_server.py. New endpoints and features must live in new modules and be thinly registered from the main file. Line count is only a coarse guardrail; the live gates are JyotishAPIHandler method count and JyotishAPIHandler.__new__ forgery sites. """ from __future__ import annotations import re from collections import Counter from pathlib import Path ROOT = Path(__file__).resolve().parents[1] API_SERVER = ROOT / "scripts" / "jyotish_api_server.py" AGENTS = ROOT / "AGENTS.md" # Live `wc -l scripts/jyotish_api_server.py` equivalent: # Path.read_bytes().count(b"\n"), measured 2026-09-16 on origin/staging @ 51a65d92. # New features must not consume this budget; open a module instead. JYOTISH_API_SERVER_LINE_COUNT_BASELINE = 11291 JYOTISH_API_SERVER_LINE_COUNT_CAP = JYOTISH_API_SERVER_LINE_COUNT_BASELINE + 300 # Whole-file indent match `^ (?:async )?def \w+`, same count as # TASK-freeze-metric-change-20260915 ยง1. Measured 2026-09-16 @ 51a65d92. JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE = 225 # `JyotishAPIHandler.__new__` in scripts/ and tests/ `*.py`, excluding this file. # Measured 2026-09-16 @ 51a65d92: scripts/ production forgeries = 4, tests/ = 29. JYOTISH_API_HANDLER_NEW_COUNT_BASELINE = 33 HANDLER_METHOD_RE = re.compile(r"^ (?:async )?def \w+", re.MULTILINE) NEW_MARKER = "JyotishAPIHandler.__new__" def _handler_method_count(source: str) -> int: return len(HANDLER_METHOD_RE.findall(source)) def _new_hits() -> Counter[str]: hits: Counter[str] = Counter() skip = Path(__file__).resolve() for folder in (ROOT / "scripts", ROOT / "tests"): for path in sorted(folder.rglob("*.py")): if path.resolve() == skip: continue count = path.read_text(encoding="utf-8").count(NEW_MARKER) if count: hits[path.relative_to(ROOT).as_posix()] = count return hits def test_jyotish_api_handler_method_count_must_not_grow() -> None: source = API_SERVER.read_text(encoding="utf-8") count = _handler_method_count(source) assert count <= JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE, ( f"{API_SERVER.as_posix()} has {count} four-space def methods; " f"cap is {JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE}. Move behaviour " "into a dedicated module; thinly registered from this file." ) def test_jyotish_api_handler_new_count_must_not_grow() -> None: hits = _new_hits() total = sum(hits.values()) listed = ", ".join(f"{path}:{count}" for path, count in sorted(hits.items())) assert total <= JYOTISH_API_HANDLER_NEW_COUNT_BASELINE, ( f"{NEW_MARKER} appears {total} times under scripts/ and tests/; " f"cap is {JYOTISH_API_HANDLER_NEW_COUNT_BASELINE}. Hits: {listed}" ) def test_jyotish_api_server_must_not_grow_beyond_bugfix_slack() -> None: line_count = API_SERVER.read_bytes().count(b"\n") assert line_count <= JYOTISH_API_SERVER_LINE_COUNT_CAP, ( f"{API_SERVER.as_posix()} has {line_count} lines; cap is " f"{JYOTISH_API_SERVER_LINE_COUNT_CAP} ({JYOTISH_API_SERVER_LINE_COUNT_BASELINE} " "baseline + 300 coarse guardrail). New endpoints and features must be new " "modules, thinly registered from this file." ) def test_agents_forbids_growing_jyotish_api_server() -> None: agents = AGENTS.read_text(encoding="utf-8") assert "scripts/jyotish_api_server.py" in agents assert "must not grow" in agents assert "thinly registered" in agents def test_quality_gate_runs_api_server_growth_contract() -> None: from scripts.run_quality_gate import CORE_PYTEST_TARGETS assert "tests/test_api_server_growth_contract.py" in CORE_PYTEST_TARGETS quality_gate = (ROOT / "scripts" / "run_quality_gate.py").read_text(encoding="utf-8") assert '"tests/test_api_server_growth_contract.py"' in quality_gate