scripts.health_check

Reference › scripts › health_check

scripts/health_check.py is the project health check that must pass before any bulk change or commit. A human runs it, and with --ci a CI job runs it; nothing imports it. It aggregates many independent checks into one verdict and one exit code.

Options: --fast runs only the fast checks, --ci adds pytest, and --fix applies ruff auto-fixes. build_checks assembles the list from those flags. The checks are small functions that each run a subprocess through _run (with a timeout, default 120 seconds) and return a pass flag and error lines: check_ruff, check_syntax (every .py file compiles), check_architecture (runs architecture_checker), check_orphan_modules (the orphan-module ratchet, AGENTS.md Hard Rule 8), check_rulings_lint (see rulings_lint), check_worktree_envs (RUL-021, see check_worktree_envs), check_wiring (RUL-023, see check_wiring), check_ai_attribution (RUL-007, over the whole reachable history), check_native_build_freshness, check_incus_not_docker (RUL-004) and check_env_contract (RUL-019), plus others further down the module.

main runs the checks concurrently in a ThreadPoolExecutor of at most eight workers, since they are independent subprocesses; wall-clock time is the longest check rather than the sum. Progress lines print as each starts, results are collected as they finish, and an exception in a check is reported as an error and counts as a failure. It exits non-zero when any check fails.

At a glance

Source scripts/health_check.py
Lines of code 735
Classes 0
Functions 32
Imports (internal) none
Imported by none
Tests that import it none

Class diagram

No classes; see Functions.

Dependency diagram

No internal imports and no importers.

API

Functions

Function Signature Summary
_run _run(cmd: list[str], cwd: Path | None = None, timeout: int = 120) -> tuple[int, str, str] Run a subprocess and return (returncode, stdout, stderr).
check_ruff check_ruff(fix: bool = False) -> tuple[bool, list[str]] Run ruff check.
check_syntax check_syntax() -> tuple[bool, list[str]] Verify all .py files compile.
check_architecture check_architecture() -> tuple[bool, list[str]] Run architecture checker.
check_orphan_modules check_orphan_modules() -> tuple[bool, list[str]] Run the orphan-module ratchet (AGENTS.md Hard Rule 8): fail the check when a NEW src module has become production-unreachable — nothing outside tests imports it, transitively, from any production…
check_rulings_lint check_rulings_lint() -> tuple[bool, list[str]] Run the rulings-enforcement lint (plans/workflow-improvements-plan.md Phase 2, RUL-014): every ruling declared in the git-backed wiki rulings canon must resolve to a real mechanism…
check_worktree_envs check_worktree_envs() -> tuple[bool, list[str]] Run the RUL-021 worktree-environment gate: every non-primary worktree must own a real (non-symlinked) .venv, resolve its own relative vendor/ [tool.uv.sources] pins, and not still exist…
check_wiring check_wiring() -> tuple[bool, list[str]] Run the RUL-023 wiring gate: a branch that adds something must also activate it — a .claude/hooks/ file registered in .claude/settings.json, a scripts/hooks/ file referenced by the…
check_ai_attribution check_ai_attribution() -> tuple[bool, list[str]] Run the RUL-007 AI-attribution gate over the whole reachable history: no Co-Authored-By: Claude .../Claude-Session:/"Generated with Claude Code" marker may appear in any commit message or…
check_native_build_freshness check_native_build_freshness() -> tuple[bool, list[str]] Run the native-build freshness gate: every vendored C extension's installed .so must be at least as new as its sources. uv keys its build cache on package VERSION, and all four vendored…
check_incus_not_docker check_incus_not_docker() -> tuple[bool, list[str]] Run the RUL-004 container-runtime gate: deployment and test isolation target Incus (LXD), never Docker.
check_env_contract check_env_contract() -> tuple[bool, list[str]] Run the RUL-019 env-contract gate: every entrypoint that reaches a required-infra-key seam must also reach the canonical env loader (secrets-only .env first, then the ORCHESTRATA_ENV_PROFILE…
check_generated_artifacts check_generated_artifacts() -> tuple[bool, list[str]] Run the RUL-030 gate: every generated artifact is tracked by git.
check_requirement_corpus check_requirement_corpus() -> tuple[bool, list[str]] Run the requirement-corpus gate: no src/ @traces cites an ungradeable id.
check_graph_write_swallow check_graph_write_swallow() -> tuple[bool, list[str]] Run the graph-write-swallow gate: a graph write inside a try body (execute_write/unit_of_work/execute_autocommit/session.run, or a MERGE/CREATE/DETACH DELETE/SET Cypher literal)…
check_kimi_agent_level check_kimi_agent_level() -> tuple[bool, list[str]] Run the RUL-018 agent-level-identity gate: kimi's IRC presence is the single systemd USER unit kimi-irc-bot.service — this repo must never start a project-level kimi listener, and must never…
check_spec_authority check_spec_authority() -> tuple[bool, list[str]] Run the RUL-013 spec-authority gate: a requirement id cited by a test must be defined by a spec under specs/ — a test may never invent an id to justify its own behaviour.
check_validate_ci check_validate_ci() -> tuple[bool, list[str]] Run CI validation.
check_test_purity check_test_purity() -> tuple[bool, list[str]] Run the test-purity audit (RUL-002: no mocks / doubles / monkeypatch).
check_prolog_boundary check_prolog_boundary() -> tuple[bool, list[str]] Run the Prolog boundary gate — every Python Prolog query site must target a predicate/arity that exists in the consulted rule base (reports/proposal-wire-prolog-governance.md §7 Phase 1).
check_prolog_meta_lint check_prolog_meta_lint() -> tuple[bool, list[str]] Run the Prolog governance-rulebase meta-lint — SPEC-033 §9 (task B17): ADR-0025's construct ban (no cut / no dynamic call / no rule-body side effects) and ADR-0010's stratification check over the…
check_kg_schema_advisory check_kg_schema_advisory() -> tuple[bool, list[str]] Canonical KG schema conformance — ADVISORY, never fails the run.
check_kg_connectivity check_kg_connectivity() -> tuple[bool, list[str]] Every node in the operational KG must be reachable from a root :Project node (session-254 ruling).
check_adr_graph_sync check_adr_graph_sync() -> tuple[bool, list[str]] Read-only three-way drift check (repo docs/adr/ / wiki decisions.md / live ArchitectureDecision nodes) — scripts/check_adr_graph_sync.py.
check_env_loaded check_env_loaded() -> tuple[bool, list[str]] Check that required env vars are present.
check_hardcoded_ips check_hardcoded_ips() -> tuple[bool, list[str]] Check for hardcoded PRODUCTION connection IPs in active Python code.
check_infra_drift check_infra_drift() -> tuple[bool, list[str]] Run the ADR-0077 infra drift gate (scripts/check_infra_drift.py).
check_env_fleet_agreement check_env_fleet_agreement() -> tuple[bool, list[str]] Run the env/fleet agreement gate (scripts/check_env_fleet_agreement.py).
check_systemd_units check_systemd_units(installed_dir: Path | None = None) -> tuple[bool, list[str]] Fail if an installed user unit no longer matches its deploy/systemd copy, or if a unit the repo declares is missing from the host.
check_pytest check_pytest(skip: bool = False) -> tuple[bool, list[str]] Run tests (non-Neo4j subset) via the sanctioned threadpool runner.
build_checks build_checks(args: argparse.Namespace) -> list[tuple[str, Callable[[], tuple[bool, list[str]]]]] THE registry of health checks, in run order, for the selected mode.
main main() -> int CLI entry point — runs every registered check and aggregates the pass/fail result (see the module docstring for the mode flags).

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/scripts/health_check.py

#!/usr/bin/env python3
"""Project health check — mandatory before any bulk change or commit.

This script is the single entry point for all validation. It runs
fast checks first, then slower ones, and exits non-zero on any failure.

Usage:
    python scripts/health_check.py          # Full check
    python scripts/health_check.py --fast   # Only fast checks (ruff, syntax)
    python scripts/health_check.py --ci     # CI mode (includes pytest)

Enforcement:
    - The pre-commit hook runs this automatically and BLOCKS commits on failure.
    - The session pre-close hook requires this to pass before ending a session
      with >10 files changed.
    - AGENTS.md mandates running this before any bulk change (>10 files).
"""

from __future__ import annotations

import argparse
import ast
import os
import subprocess
import sys
import time
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path


def _run(
    cmd: list[str], cwd: Path | None = None, timeout: int = 120
) -> tuple[int, str, str]:
    """Run a subprocess and return (returncode, stdout, stderr)."""
    try:
        result = subprocess.run(
            cmd,
            cwd=cwd,
            capture_output=True,
            text=True,
            timeout=timeout,
        )
        return result.returncode, result.stdout, result.stderr
    except FileNotFoundError as e:
        return 127, "", str(e)
    except subprocess.TimeoutExpired:
        return 124, "", f"Command timed out after {timeout}s"


def check_ruff(fix: bool = False) -> tuple[bool, list[str]]:
    """Run ruff check."""
    cmd = ["uv", "run", "ruff", "check", "src/", "tests/"]
    if fix:
        cmd.append("--fix")
    rc, out, err = _run(cmd)
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["RUFF: " + line for line in lines if line.strip()]


def check_syntax() -> tuple[bool, list[str]]:
    """Verify all .py files compile."""
    errors: list[str] = []
    for path in Path("src").rglob("*.py"):
        try:
            ast.parse(path.read_text())
        except SyntaxError as e:
            errors.append(f"SYNTAX: {path}:{e.lineno}: {e.msg}")
    for path in Path("tests").rglob("*.py"):
        try:
            ast.parse(path.read_text())
        except SyntaxError as e:
            errors.append(f"SYNTAX: {path}:{e.lineno}: {e.msg}")
    return not errors, errors


def check_architecture() -> tuple[bool, list[str]]:
    """Run architecture checker."""
    rc, out, err = _run([sys.executable, "scripts/architecture_checker.py"])
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["ARCH: " + line for line in lines if line.strip()]


def check_orphan_modules() -> tuple[bool, list[str]]:
    """Run the orphan-module ratchet (AGENTS.md Hard Rule 8): fail the check
    when a NEW src module has become production-unreachable — nothing outside
    tests imports it, transitively, from any production entrypoint. The
    committed ``plans/orphan-modules-baseline.txt`` is the known unwired-debt
    ledger: growth fails, shrink is advisory (see ``orphans --check`` in
    scripts/serialize_codebase_ast.py).
    """
    rc, out, err = _run(
        [
            sys.executable,
            "scripts/serialize_codebase_ast.py",
            "orphans",
            "--source",
            "src/orchestrata_ultimate",
            "--baseline",
            "plans/orphan-modules-baseline.txt",
            "--check",
        ]
    )
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["ORPHAN: " + line for line in lines if line.strip()]


def check_rulings_lint() -> tuple[bool, list[str]]:
    """Run the rulings-enforcement lint (plans/workflow-improvements-plan.md
    Phase 2, RUL-014): every ruling declared in the git-backed wiki rulings
    canon must resolve to a real mechanism (hook/gate/banned-deps/
    constitution) or an explicit ``none (judgment)`` mark.

    SKIPS (does not fail health_check) when RULINGS_DIR /
    ~/workspace/knowledge-wiki/rulings is absent, so a machine without the
    knowledge-wiki checkout never goes false-red on this check.
    """
    rc, out, err = _run([sys.executable, "scripts/rulings_lint.py"], timeout=30)
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["RULINGS-LINT: " + line for line in lines if line.strip()]


def check_worktree_envs() -> tuple[bool, list[str]]:
    """Run the RUL-021 worktree-environment gate: every non-primary worktree
    must own a real (non-symlinked) ``.venv``, resolve its own relative
    ``vendor/`` ``[tool.uv.sources]`` pins, and not still exist once its
    branch has merged into master (see scripts/check_worktree_envs.py)."""
    rc, out, err = _run([sys.executable, "scripts/check_worktree_envs.py"], timeout=30)
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["WORKTREE-ENV: " + line for line in lines if line.strip()]


def check_wiring() -> tuple[bool, list[str]]:
    """Run the RUL-023 wiring gate: a branch that adds something must also
    activate it — a ``.claude/hooks/`` file registered in
    ``.claude/settings.json``, a ``scripts/hooks/`` file referenced by the
    installer, a new ``src/`` module imported from outside its own package,
    and any newly added gate script a ruling declares as its mechanism
    actually invoked by a runner (see scripts/check_wiring.py)."""
    rc, out, err = _run([sys.executable, "scripts/check_wiring.py"], timeout=60)
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["WIRING: " + line for line in lines if line.strip()]


def check_ai_attribution() -> tuple[bool, list[str]]:
    """Run the RUL-007 AI-attribution gate over the whole reachable history:
    no ``Co-Authored-By: Claude ...``/``Claude-Session:``/"Generated with
    Claude Code" marker may appear in any commit message or annotated tag.
    Every commit and tag carries Mohamed's identity only. Findings pinned in
    ``scripts/rul007-baseline.txt`` (published history that cannot be
    rewritten) are printed but do not fail (see
    scripts/check_ai_attribution.py)."""
    rc, out, err = _run(
        [
            sys.executable,
            "scripts/check_ai_attribution.py",
            "--range",
            "--all",
            "--tags",
        ],
        timeout=60,
    )
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["AI-ATTRIBUTION: " + line for line in lines if line.strip()]


def check_native_build_freshness() -> tuple[bool, list[str]]:
    """Run the native-build freshness gate: every vendored C extension's
    installed ``.so`` must be at least as new as its sources. uv keys its build
    cache on package VERSION, and all four vendored extensions are pinned at a
    version that never changes, so a reinstall can serve a previously-built
    wheel — shipping a stale binary with a FRESH mtime while the source shows
    the fix. Measured 2026-08-05 on the cxpt ``Server_dealloc`` fix (see
    scripts/check_native_build_freshness.py)."""
    rc, out, err = _run(
        [sys.executable, "scripts/check_native_build_freshness.py"], timeout=60
    )
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["NATIVE-BUILD: " + line for line in lines if line.strip()]


def check_incus_not_docker() -> tuple[bool, list[str]]:
    """Run the RUL-004 container-runtime gate: deployment and test isolation
    target Incus (LXD), never Docker. Blocks a shell/systemd docker
    invocation, a Python subprocess/import of docker, a docker endpoint in
    config/env, and a new docker-compose file outside the allowlisted
    reference trees (see scripts/check_incus_not_docker.py)."""
    rc, out, err = _run(
        [sys.executable, "scripts/check_incus_not_docker.py"], timeout=60
    )
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["INCUS-NOT-DOCKER: " + line for line in lines if line.strip()]


def check_env_contract() -> tuple[bool, list[str]]:
    """Run the RUL-019 env-contract gate: every entrypoint that reaches a
    required-infra-key seam must also reach the canonical env loader
    (secrets-only ``.env`` first, then the ``ORCHESTRATA_ENV_PROFILE``
    overlay ``env/{dev,test,prod}.env`` with ``override=True``), and no
    second hand-rolled mutating ``.env`` loader may exist (see
    scripts/check_env_contract.py)."""
    rc, out, err = _run([sys.executable, "scripts/check_env_contract.py"], timeout=60)
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["ENV-CONTRACT: " + line for line in lines if line.strip()]


def check_generated_artifacts() -> tuple[bool, list[str]]:
    """Run the RUL-030 gate: every generated artifact is tracked by git.

    A generated artifact is a snapshot of project state at a commit, so an
    artifact that is gitignored or present-but-untracked makes the commit
    stop describing the project (see scripts/check_generated_artifacts.py
    for the derived/read/function-of-the-commit boundary). Exit 2 means the
    gate could not do its job -- an empty or unresolvable registry -- and is
    surfaced as a failure rather than folded into a pass.
    """
    rc, out, err = _run(
        [sys.executable, "scripts/check_generated_artifacts.py"], timeout=60
    )
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    prefix = "GENERATED-ARTIFACTS-INVALID: " if rc == 2 else "GENERATED-ARTIFACTS: "
    return False, [prefix + line for line in lines if line.strip()]


def check_requirement_corpus() -> tuple[bool, list[str]]:
    """Run the requirement-corpus gate: no src/ @traces cites an ungradeable id.

    Two readers of one corpus drifted apart -- the AST toolkit scanned
    checklist + spec.md + addendum while the RTM generator scanned checklists
    only -- so ``trace-lint`` called 21 citations clean while the RTM graded
    them GHOST, and 68 requirement ids never entered the matrix's denominator.
    This asserts the two agree, measured against @traces decorators harvested
    from the AST, which can genuinely disagree with either side. Exit 2 means
    the gate examined nothing and is surfaced as a failure, never a pass.
    """
    rc, out, err = _run(
        [sys.executable, "scripts/check_requirement_corpus.py"], timeout=180
    )
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    prefix = "REQUIREMENT-CORPUS-INVALID: " if rc == 2 else "REQUIREMENT-CORPUS: "
    return False, [prefix + line for line in lines if line.strip()]


def check_graph_write_swallow() -> tuple[bool, list[str]]:
    """Run the graph-write-swallow gate: a graph write inside a ``try`` body
    (``execute_write``/``unit_of_work``/``execute_autocommit``/``session.run``,
    or a MERGE/CREATE/DETACH DELETE/SET Cypher literal) whose ``except``
    handler cannot inform the caller of failure — no raise, no return, no
    outliving assignment/mutation — makes contention on the shared Memgraph
    indistinguishable from a bug. Ratcheted against
    ``reports/graph-write-swallow-baseline.json``; only NEW findings fail
    (see scripts/check_graph_write_swallow.py)."""
    rc, out, err = _run(
        [sys.executable, "scripts/check_graph_write_swallow.py", "--check"], timeout=60
    )
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["GRAPH-WRITE-SWALLOW: " + line for line in lines if line.strip()]


def check_kimi_agent_level() -> tuple[bool, list[str]]:
    """Run the RUL-018 agent-level-identity gate: kimi's IRC presence is the
    single systemd USER unit ``kimi-irc-bot.service`` — this repo must never
    start a project-level kimi listener, and must never bind ``IRC_NICK`` to
    another agent's identity (see scripts/check_kimi_agent_level.py)."""
    rc, out, err = _run(
        [sys.executable, "scripts/check_kimi_agent_level.py"], timeout=30
    )
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["KIMI-AGENT-LEVEL: " + line for line in lines if line.strip()]


def check_spec_authority() -> tuple[bool, list[str]]:
    """Run the RUL-013 spec-authority gate: a requirement id cited by a test
    must be defined by a spec under ``specs/`` — a test may never invent an
    id to justify its own behaviour.

    Pinned at ``--max 0``: the gate landed with a backlog of 8 ungrounded
    ids (the ``REQ-003-FR24-100..106`` family, cited by end-to-end pipeline
    tests before anyone wrote them down), and that backlog was resolved the
    RUL-013 way — by writing the requirements into
    ``specs/003-intent/checklists/requirements.md`` rather than re-pointing
    the tests at unrelated ids. There is no remaining backlog to tolerate,
    so the ceiling stays 0 and the ratchet cannot silently re-widen
    (see scripts/check_spec_authority.py)."""
    rc, out, err = _run(
        [sys.executable, "scripts/check_spec_authority.py", "--max", "0"], timeout=120
    )
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["SPEC-AUTHORITY: " + line for line in lines if line.strip()]


def check_validate_ci() -> tuple[bool, list[str]]:
    """Run CI validation."""
    rc, out, err = _run([sys.executable, "scripts/validate_ci.py"])
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["CI: " + line for line in lines if line.strip()]


def check_test_purity() -> tuple[bool, list[str]]:
    """Run the test-purity audit (RUL-002: no mocks / doubles / monkeypatch).

    Graded on the AUDIT'S EXIT CODE, not on grepping its output for the words
    "ERROR"/"FAIL". Measured 2026-08-05: a planted ``MagicMock()`` makes
    ``audit_test_purity.py`` exit 1 and print "SUMMARY: 1 files with 1 total
    violations" — a report that contains NEITHER word, so the old
    substring-grep produced an empty error list and this check returned PASS
    while the audit was failing. That silently disarmed the repo's most
    fundamental test ruling inside its own health check.
    """
    rc, out, err = _run([sys.executable, "scripts/audit_test_purity.py"])
    if rc == 0:
        return True, []
    lines = [line for line in (out + err).splitlines() if line.strip()]
    # Keep the violation detail: per-file "Line N:" citations and the summary.
    detail = [
        line
        for line in lines
        if "violation" in line.lower() or "Line " in line or ".py" in line
    ]
    return False, ["PURITY: " + line for line in (detail or lines)[-25:]]


def check_prolog_boundary() -> tuple[bool, list[str]]:
    """Run the Prolog boundary gate — every Python Prolog query site must
    target a predicate/arity that exists in the consulted rule base
    (reports/proposal-wire-prolog-governance.md §7 Phase 1)."""
    rc, out, err = _run(
        [sys.executable, "scripts/check_prolog_boundary.py"], timeout=90
    )
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["PROLOG-BOUNDARY: " + line for line in lines if line.strip()]


def check_prolog_meta_lint() -> tuple[bool, list[str]]:
    """Run the Prolog governance-rulebase meta-lint — SPEC-033 §9 (task B17):
    ADR-0025's construct ban (no cut / no dynamic call / no rule-body side
    effects) and ADR-0010's stratification check over the governance
    rulebase (constitutional.pl, agents.pl, cxp.pl, multiagent.pl, ethics.pl,
    governance.pl, knowledge.pl, completion.pl, intent_policy.pl).

    NOTE (2026-07-24): as of this wiring, the governance rulebase carries 19
    known, tracked violations pending a separate remediation lane (Lane 3) —
    15 cuts the Lane-0 inventory marked needs-rewrite/safe-remove, plus 4
    real assert/retract-in-rule-body sites this lint independently found
    (constitutional.pl:81-82's human_override/3; cxp.pl:410,414's
    register_session/3 / unregister_session/1) that the given inventory did
    not enumerate — the latter sits in constitutional.pl, which no lane may
    modify in this task, so it needs an orchestrator/human decision (ADR
    exception vs. restructure vs. accepted debt). Until that remediation
    lands, THIS GATE FAILS on every full (non-``--fast``) health_check run,
    matching the brief's explicit "failure fails health_check" instruction
    rather than being softened to advisory (contrast ``check_kg_schema_
    advisory`` below, which IS deliberately non-blocking) — flagged loudly
    here rather than silently decided.
    """
    rc, out, err = _run([sys.executable, "scripts/prolog_meta_lint.py"], timeout=60)
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["PROLOG-META-LINT: " + line for line in lines if line.strip()]


def check_kg_schema_advisory() -> tuple[bool, list[str]]:
    """Canonical KG schema conformance — ADVISORY, never fails the run.

    CI already runs ``kg_schema.py check`` REQUIRED (ci.yml); this surfaces
    live-graph drift during local health checks without blocking (ratified
    O-2, ADR-023 — pre-commit is permanently out, CI is the hard gate).
    """
    rc, out, err = _run([sys.executable, "scripts/kg_schema.py", "check"], timeout=120)
    if rc != 0:
        lines = [ln for ln in (out + err).splitlines() if ln.strip()][:10]
        print("\n  ⚠ ADVISORY (non-blocking) — kg_schema check reports drift:")
        for ln in lines:
            print(f"    {ln}")
        print("  (CI enforces this as a required gate; fix before pushing.)")
    return True, []


def check_kg_connectivity() -> tuple[bool, list[str]]:
    """Every node in the operational KG must be reachable from a root
    ``:Project`` node (session-254 ruling). REQUIRED, not advisory (contrast
    ``check_kg_schema_advisory`` above) -- its failure marks health_check
    FAILED. As of the session-254 WCC census this gate is EXPECTED to fail
    on the live graph (2,654 disconnected islands); the writer-side fixes
    landing alongside this wiring stop the bleeding, the full data repair is
    a separate, later effort.
    """
    rc, out, err = _run(
        [sys.executable, "scripts/check_kg_connectivity.py"],
        timeout=120,
    )
    if rc == 0:
        return True, []
    lines = [ln for ln in (out + err).splitlines() if ln.strip()]
    return False, ["KG-CONNECTIVITY: " + line for line in lines]


def check_adr_graph_sync() -> tuple[bool, list[str]]:
    """Read-only three-way drift check (repo docs/adr/ / wiki decisions.md /
    live ``ArchitectureDecision`` nodes) — ``scripts/check_adr_graph_sync.py``.
    REQUIRED, not advisory, same reasoning as ``check_kg_connectivity`` above
    (a live-infra check that can legitimately stay red pending a human
    decision does not belong in the fast pre-commit / ``scripts/gates.py``
    battery, which would then block every relevant commit on a known,
    pending issue -- it belongs here, in the mandatory-but-slow report).

    2026-08-19 audit (session finding, not yet reconciled): ``docs/adr/`` and
    the knowledge-wiki's ``decisions.md`` are two independent ADR registers
    that number their entries the same way and disagree on the decision
    itself for every one of the 25 overlapping ids -- the live graph's
    ``ArchitectureDecision`` nodes carry no property distinguishing which
    register produced them. **This gate is EXPECTED to fail** until that id
    collision is resolved (a human decision -- this gate only detects it,
    per its own module docstring; it never writes or deletes anything).
    """
    rc, out, err = _run(
        [sys.executable, "scripts/check_adr_graph_sync.py"],
        timeout=60,
    )
    if rc == 0:
        return True, []
    lines = [ln for ln in (out + err).splitlines() if ln.strip()]
    return False, ["ADR-GRAPH-SYNC: " + line for line in lines]


def check_env_loaded() -> tuple[bool, list[str]]:
    """Check that required env vars are present."""
    import os

    required = [
        "REDIS_HOST",
        "REDIS_PORT",
        # KG_ROUTING_URI/KG_DATABASE, not NEO4J_* — retired 2026-07-27.
        "KG_ROUTING_URI",
        "KG_DATABASE",
    ]
    missing = [v for v in required if not os.environ.get(v)]
    if missing:
        return False, [
            f"ENV: Missing {', '.join(missing)} — run 'source .env' or "
            "ensure direnv is active"
        ]
    return True, []


def check_hardcoded_ips() -> tuple[bool, list[str]]:
    """Check for hardcoded PRODUCTION connection IPs in active Python code.

    Delegates to ``validate_ci._scan_hardcoded_prod_ip`` — the single
    line-level, connection-context scanner (a ``*_HOST``/``*_URI`` key or
    ``host=``/``uri=`` kwarg adjacent to a ``10.53.1.x`` literal), covering BOTH
    ``src/**`` and ``scripts/*``. This replaces the previous whole-file heuristic
    which (a) never scanned ``scripts/`` and (b) exempted any file containing
    ``os.environ`` ANYWHERE — i.e. every env-config file, making the check a
    no-op for exactly the prod-default bug it was meant to catch.
    """
    # A local import keeps the dependency scoped to this one check. scripts/ is
    # on sys.path when health_check runs AS a script, but NOT when a test imports
    # it as a module — so ensure the sibling directory is importable first
    # (fixes ModuleNotFoundError: No module named 'validate_ci' under pytest).
    # Scan the CWD tree (repo root in normal use; overridable by chdir in tests).
    import sys as _sys

    _scripts_dir = str(Path(__file__).resolve().parent)
    if _scripts_dir not in _sys.path:
        _sys.path.insert(0, _scripts_dir)
    from validate_ci import _scan_hardcoded_prod_ip

    hits = _scan_hardcoded_prod_ip(Path.cwd())
    errors = [f"IP: {hit}" for hit in hits]
    return not errors, errors


def check_infra_drift() -> tuple[bool, list[str]]:
    """Run the ADR-0077 infra drift gate (scripts/check_infra_drift.py).

    OpenTofu owns the Incus topology of all three environments and the live
    fleets are imported into the host-owned state at
    ``/var/lib/orchestrata/terraform/fleet.tfstate``. The gate runs ``tofu plan
    -detailed-exitcode`` over ``infra/terraform`` and proves the bridge subnets
    live with ``incus network get``. A MISSING state or binary is a FAIL,
    never a skip (ADR-0077 item 4); only a checkout with no ``infra/terraform``
    at all passes with a printed NOTE. The same script runs hourly from
    deploy/systemd/orchestrata-infra-drift.timer.
    """
    rc, out, err = _run([sys.executable, "scripts/check_infra_drift.py"], timeout=360)
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["INFRA: " + line for line in lines if line.strip()]


def check_env_fleet_agreement() -> tuple[bool, list[str]]:
    """Run the env/fleet agreement gate (scripts/check_env_fleet_agreement.py).

    ``env/test.env``, ``env/dev.env`` and the ``TEST_*`` keys of ``.env.example``
    are DERIVED from ``tofu output -json`` (infra/gen_env.py). The gate fails on
    any disagreement, any commented-out owned key, and a missing tofu binary or
    state (ADR-0077 item 4). Measured 2026-10-08: ``.env.example`` carried a
    retired prod-bridge ``TEST_MEMGRAPH_URI`` for two months with nothing checking it.
    """
    rc, out, err = _run(
        [sys.executable, "scripts/check_env_fleet_agreement.py"], timeout=360
    )
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["ENV-FLEET: " + line for line in lines if line.strip()]


def check_systemd_units(installed_dir: Path | None = None) -> tuple[bool, list[str]]:
    """Fail if an installed user unit no longer matches its ``deploy/systemd``
    copy, or if a unit the repo declares is missing from the host.

    Those units are the live schedule this project runs on, not documentation
    — see ``deploy/systemd/README.md``. This gate closes both drift directions:
    an edited installed unit, and a declared unit that was never installed.
    Skips (passes with a note) on a host with no user units, so it is a no-op
    in CI/containers rather than a false failure.

    Args:
        installed_dir: Where installed user units live. Defaults to
            ``$XDG_CONFIG_HOME/systemd/user``; injectable so the gate is
            testable without touching the real host.
    """
    unit_dir = Path("deploy/systemd")
    if installed_dir is None:
        config_home = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
        installed_dir = Path(config_home) / "systemd/user"
    if not unit_dir.is_dir():
        return True, ["SYSTEMD: skipped (deploy/systemd absent)"]
    if not installed_dir.is_dir():
        return True, ["SYSTEMD: skipped (no user unit directory on this host)"]

    errors: list[str] = []
    for declared in sorted(unit_dir.glob("*.service")) + sorted(
        unit_dir.glob("*.timer")
    ):
        installed = installed_dir / declared.name
        if not installed.exists():
            errors.append(
                f"SYSTEMD: {declared.name} is version-controlled but NOT "
                f"installed (run deploy/systemd/install.sh)"
            )
            continue
        if installed.read_text() != declared.read_text():
            errors.append(
                f"SYSTEMD DRIFT: {declared.name} differs from the installed "
                f"unit (diff {installed} {declared})"
            )
    return not errors, errors


def check_pytest(skip: bool = False) -> tuple[bool, list[str]]:
    """Run tests (non-Neo4j subset) via the sanctioned threadpool runner.

    pytest-xdist (``-n auto``) is BANNED in this project — it deadlocks the
    suite ~78%. Parallelism comes from scripts/run_tests_threadpool.py
    (ThreadPoolExecutor, one pytest process per module). Invoked via
    ``sys.executable`` so it always uses this interpreter's venv (with all
    plugins) regardless of cwd or whether VIRTUAL_ENV is exported.
    """
    if skip:
        return True, ["PYTEST: skipped (use --ci to run)"]
    runner = Path(__file__).resolve().parent / "run_tests_threadpool.py"
    cmd = [
        sys.executable,
        str(runner),
        "tests/",
        "--ignore=tests/knowledge_graph",
        "--ignore=tests/session/kg_sync",
        "--ignore=tests/gateway",
        "--ignore=tests/orchestrator/test_coverage_boost.py",
        "--ignore=tests/integration",
        "--ignore=tests/agents/volks/architect",
        "--pytest-arg=--timeout=120",
        "--pytest-arg=-q",
    ]
    rc, out, err = _run(cmd, timeout=300)
    if rc == 0:
        return True, []
    lines = (out + err).splitlines()
    return False, ["PYTEST: " + line for line in lines if line.strip()]


def build_checks(
    args: argparse.Namespace,
) -> list[tuple[str, Callable[[], tuple[bool, list[str]]]]]:
    """THE registry of health checks, in run order, for the selected mode.

    The base block always runs and includes every ruling-enforcement gate
    (RUL-004/007/013/018/019/021/023): a gate that exists but is never
    invoked is not a delivery (RUL-023), and this list is what invokes them.
    ``--fast`` omits the slow/infra block; ``--ci`` adds the pytest run.
    """
    checks: list[tuple[str, Callable[[], tuple[bool, list[str]]]]] = [
        ("Environment variables", check_env_loaded),
        ("Python syntax", check_syntax),
        ("Ruff lint", lambda: check_ruff(fix=args.fix)),
        ("Architecture rules", check_architecture),
        ("CI validation", check_validate_ci),
        ("Hardcoded IPs", check_hardcoded_ips),
        ("Orphan modules (Rule 8 wiring ratchet)", check_orphan_modules),
        ("Rulings lint", check_rulings_lint),
        ("Worktree envs (RUL-021)", check_worktree_envs),
        ("Wiring (RUL-023)", check_wiring),
        ("Native build freshness", check_native_build_freshness),
        ("Incus not Docker (RUL-004)", check_incus_not_docker),
        ("AI attribution (RUL-007)", check_ai_attribution),
        ("Spec authority (RUL-013)", check_spec_authority),
        ("Kimi agent-level (RUL-018)", check_kimi_agent_level),
        ("Env contract (RUL-019)", check_env_contract),
        ("Generated artifacts (RUL-030)", check_generated_artifacts),
        ("Requirement corpus (RTM gradeability)", check_requirement_corpus),
        ("Graph write swallow", check_graph_write_swallow),
    ]

    if not args.fast:
        checks.append(("Test purity", check_test_purity))
        checks.append(("Infra drift (Terraform)", check_infra_drift))
        checks.append(("Env/fleet agreement (Terraform)", check_env_fleet_agreement))
        checks.append(("Systemd units (deploy/systemd)", check_systemd_units))
        checks.append(("Prolog boundary", check_prolog_boundary))
        checks.append(("Prolog meta-lint (SPEC-033 B17)", check_prolog_meta_lint))
        checks.append(("KG schema (advisory)", check_kg_schema_advisory))
        checks.append(("KG connectivity", check_kg_connectivity))
        checks.append(("ADR graph sync (repo/wiki/graph)", check_adr_graph_sync))

    if args.ci:
        checks.append(("Pytest (non-Neo4j)", lambda: check_pytest(skip=False)))

    return checks


def main() -> int:
    """CLI entry point — runs every registered check and aggregates the
    pass/fail result (see the module docstring for the mode flags)."""
    parser = argparse.ArgumentParser(description="Project health check")
    parser.add_argument("--fast", action="store_true", help="Only fast checks")
    parser.add_argument("--ci", action="store_true", help="CI mode (includes pytest)")
    parser.add_argument("--fix", action="store_true", help="Auto-fix ruff issues")
    args = parser.parse_args()

    checks = build_checks(args)

    all_passed = True
    all_errors: list[str] = []

    print("=" * 60)
    print("HEALTH CHECK")
    print("=" * 60)

    # Run all checks concurrently — they are independent subprocesses.
    # Wall-clock becomes the longest single check, not the sum.
    # Results are collected in submission order for deterministic output.
    futures = {}
    t_start = time.monotonic()
    with ThreadPoolExecutor(max_workers=min(len(checks), 8)) as pool:
        for name, check_fn in checks:
            fut = pool.submit(check_fn)
            futures[fut] = name
            print(f"⏳ {name} ... started", flush=True)

        for fut in as_completed(futures):
            name = futures[fut]
            elapsed = time.monotonic() - t_start
            try:
                passed, errors = fut.result()
            except Exception as e:
                print(f"💥 ERROR {name} ({elapsed:.1f}s): {e}", flush=True)
                all_passed = False
                all_errors.append(f"{name}: {e}")
                continue
            if passed:
                print(f"✅ PASS {name} ({elapsed:.1f}s)", flush=True)
            else:
                print(f"❌ FAIL {name} ({elapsed:.1f}s)", flush=True)
                all_passed = False
                all_errors.extend(errors)

    total_elapsed = time.monotonic() - t_start

    print(f"\n{'=' * 60}")
    print(f"Total elapsed: {total_elapsed:.1f}s")
    if all_passed:
        print("RESULT: ALL CHECKS PASSED")
        print("=" * 60)
        return 0
    else:
        print(f"RESULT: {len(all_errors)} ERROR(S)")
        print("=" * 60)
        for e in all_errors[:50]:
            print(f"  {e}")
        if len(all_errors) > 50:
            print(f"  ... and {len(all_errors) - 50} more")
        return 1


if __name__ == "__main__":
    sys.exit(main())