#!/usr/bin/env python3 """Fail when the HTTP Client Platform's code and documentation have drifted. The design (ยง33 "Documentation") requires the support matrix, configuration reference, security guide, runbook, and migration guide to match the code. Review cannot hold that line by itself, so this verifier extracts the names that are part of the public contract -- stable exceptions, metric names, configuration properties, startup violation codes, and transports -- and fails when one exists in code but nowhere in the documentation. It deliberately checks one direction only. A name documented but not yet implemented is a plan; a name implemented but undocumented is a surprise for whoever is on call. """ from __future__ import annotations import re import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent PLATFORM = REPO_ROOT / "src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient" BOOTSTRAP = REPO_ROOT / "src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/httpclient" DOCS_DIR = REPO_ROOT / "docs/httpclient" ENV_FIELD_MANIFEST = REPO_ROOT / "docs/httpclient/env-fields.yaml" REQUIRED_DOCS = [ "support-matrix.md", "configuration-reference.md", "retry-and-ambiguity.md", "security.md", "streaming.md", "operations.md", "migration-guide.md", "release-checklist.md", "performance-baseline.md", "repository-adaptation.md", ] def read_docs() -> str: return "\n".join( (DOCS_DIR / name).read_text(encoding="utf-8") for name in REQUIRED_DOCS ) def stable_exceptions() -> list[str]: error_dir = PLATFORM / "api/error" return sorted( path.stem for path in error_dir.glob("Http*Exception.java") if path.stem != "HttpClientException" ) def metric_names() -> list[str]: source = (PLATFORM / "observation/HttpClientObservationNames.java").read_text(encoding="utf-8") return sorted(set(re.findall(r'"(http\.client\.[a-z_.]+)"', source))) def violation_codes() -> list[str]: codes: set[str] = set() for source_file in [ PLATFORM / "profile/ClientProfileValidator.java", PLATFORM / "security/TlsPolicyValidator.java", BOOTSTRAP / "HttpClientStartupValidator.java", ]: source = source_file.read_text(encoding="utf-8") codes.update(re.findall(r'"([A-Z][A-Z0-9_]{4,})"', source)) return sorted(codes) def configuration_properties() -> list[str]: """Every leaf property under `app.httpclient`, nested and dynamic blocks included. Read from the environment-field manifest rather than from the record source. The manifest is derived from `HttpClientPlatformSettings` by `HttpClientEnvironmentKeys` and held to it in both directions by `HttpClientPlatformEnvManifestTest`, so it cannot drift from the code; parsing the record here a second time, with a regex, could only agree with it by luck. The previous version of this function did exactly that and saw eighteen top-level names, which is why a nested pool, timeout or TLS setting could be added and documented nowhere. """ names: set[str] = set() for line in ENV_FIELD_MANIFEST.read_text(encoding="utf-8").splitlines(): stripped = line.strip() if not stripped.startswith("- field:"): continue path = stripped[len("- field:") :].strip() leaf = path.split(".")[-1] # `clients[N]` and `allowed-hosts[M]` are documented by name, not by position. names.add(re.sub(r"\[[NM]\]$", "", leaf)) return sorted(names) def transports() -> list[str]: source = (PLATFORM / "profile/TransportType.java").read_text(encoding="utf-8") body = source[source.index("public enum TransportType") :] return sorted(set(re.findall(r"^\s{2}([A-Z][A-Z_]*),?$", body, flags=re.MULTILINE))) def main() -> int: missing_docs = [name for name in REQUIRED_DOCS if not (DOCS_DIR / name).is_file()] if missing_docs: print("FAIL missing documentation file(s): " + ", ".join(missing_docs)) return 1 documentation = read_docs() failures: list[str] = [] checks = { "stable exception": stable_exceptions(), "metric": metric_names(), "startup violation code": violation_codes(), "configuration property": configuration_properties(), "transport": transports(), } for kind, names in checks.items(): for name in names: if name not in documentation: failures.append(f"{kind} '{name}' exists in code but is not documented") if failures: print(f"FAIL httpclient documentation drift ({len(failures)} finding(s)):") for failure in failures: print(" - " + failure) return 1 total = sum(len(names) for names in checks.values()) print(f"PASS httpclient documentation covers {total} code-derived name(s):") for kind, names in checks.items(): print(f" {kind}: {len(names)}") return 0 if __name__ == "__main__": sys.exit(main())