#!/usr/bin/env python3 from __future__ import annotations import configparser import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8", errors="replace") if hasattr(sys.stderr, "reconfigure"): sys.stderr.reconfigure(encoding="utf-8", errors="replace") REQUIRED_PATHS = [ "AGENTS.md", ".gitea/workflows/ci.yml", "README.md", "Requirements.md", "docs/TESTS.md", "docs/POWER_OPTIMIZATION.md", "docs/engineering/current-state.md", "docs/engineering/risk-register.md", "docs/engineering/test-matrix.md", "docs/engineering/hil-strategy.md", "hil_tests/README.md", ] TEST_SUITES = [ "test_html_escape", "test_json_codec", "test_lora_transport", "test_meter_fault_count", "test_payload_codec", "test_refactor_smoke", "test_security_fuzz", ] TRACEABILITY_CONTRACTS = [ "LoRa frame format", "CRC validation", "Chunk reassembly", "Payload schema", "Sender-ID validation", "ACK matching", "Time bootstrap", "Retry bounds", "Queue bounds", "Duplicate handling", "MQTT JSON keys", "Home Assistant discovery fields", "CSV compatibility", "HTML escaping", "Web input validation", ] def read_text(path: str) -> str: return (ROOT / path).read_text(encoding="utf-8") def platformio_envs() -> set[str]: parser = configparser.RawConfigParser() parser.optionxform = str with (ROOT / "platformio.ini").open(encoding="utf-8") as handle: parser.read_file(handle) return {section.split(":", 1)[1] for section in parser.sections() if section.startswith("env:")} def main() -> int: errors: list[str] = [] for rel in REQUIRED_PATHS: if not (ROOT / rel).exists(): errors.append(f"Missing required documentation or harness file: {rel}") if errors: for error in errors: print(error) return 1 envs = platformio_envs() tests_doc = read_text("docs/TESTS.md") readme = read_text("README.md") requirements = read_text("Requirements.md") current_state = read_text("docs/engineering/current-state.md") risk_register = read_text("docs/engineering/risk-register.md") test_matrix = read_text("docs/engineering/test-matrix.md") agents = read_text("AGENTS.md") for env in sorted(envs): if f"`{env}`" not in tests_doc: errors.append(f"docs/TESTS.md does not document environment {env}.") for stale in ["lilygo-t3-v1-6-1", "schema v3", "schema `3`"]: if stale in tests_doc: errors.append(f"docs/TESTS.md still contains stale text: {stale}") for suite in TEST_SUITES: if suite not in tests_doc: errors.append(f"docs/TESTS.md does not document suite {suite}.") for contract in TRACEABILITY_CONTRACTS: if contract not in tests_doc or contract not in test_matrix: errors.append(f"Traceability contract missing from docs: {contract}") schema_v4_markers = ["schema v4", "schema `4`", "schema `v4`"] for path, text in [("README.md", readme), ("Requirements.md", requirements), ("current-state.md", current_state)]: if not any(marker in text for marker in schema_v4_markers): errors.append(f"{path} does not document payload schema v4.") if "schema v3" in text or "schema `3`" in text: errors.append(f"{path} still documents schema v3.") for category in [ "Verified Defects", "Probable Risks", "Missing Tests", "Missing Documentation", "Security Decisions", "Hardware-Dependent Questions", ]: if category not in risk_register: errors.append(f"risk-register.md missing category: {category}") if "python tools/verify.py" not in agents: errors.append("AGENTS.md does not name the canonical verification command.") if errors: for error in errors: print(f"ERROR: {error}") return 1 print("Documentation consistency checks passed.") return 0 if __name__ == "__main__": raise SystemExit(main())