scripts.test-review

Reference › scripts › test-review

scripts/test-review.py is the test file reviewer: it checks test files against the orchestrata-ultimate test standards and reports findings with severity ERROR, WARNING or INFO. A human or a review gate runs it; it has no internal imports. See audit_test_purity, test_isolation_audit and gate_parallel_safety for related checks.

find_test_files expands the paths given, and TestFileChecker.check_file parses each file with ast and runs a set of rule methods, each recording issues in a TestReport. The rules cover imports (including banned libraries and relative imports), fixtures, skips, mocking, monkeypatch, os.environ use, try-except-pass, module-level mutable state, hard-coded ports, xdist use, test-id usage, anyio marks and async tests, graph-write and Redis-write isolation, teardown of per-test DDL and chunk-label schema, bare driver sessions, and environment-key drift against registries built from the repository. A # gate-ok: comment exempts a line and the next. String literals and comments are blanked first (_strip_strings_and_comments), and code samples that carry Cypher as an inner literal are recognised so they are not flagged.

Options: positional paths, --output or -o (report file), --stdout, --min-severity (default WARNING) and --worklist-baseline (a JSON file whose rows are downgraded to INFO as named, non-blocking debt, while new findings keep their severity). With no test files it exits 0. Otherwise it exits 1 when any ERROR was found.

At a glance

Source scripts/test-review.py
Lines of code 1769
Classes 4
Functions 10
Imports (internal) none
Imported by none
Tests that import it none

Class diagram

Class diagram scripts.test-review

Dependency diagram

No internal imports and no importers.

API

Constants

Name Type Value
_SEVERITY_RANK dict[Severity, int] {Severity.ERROR: 3, Severity.WARNING: 2, Severity.INFO: 1}

Classes

Severity (enum)

Bases: StrEnum · Line 178

Member Value
ERROR 'ERROR'
WARNING 'WARNING'
INFO 'INFO'

TestIssue (dataclass)

Decorators: dataclass · Line 185

Attribute Type Default
file str
line int
rule str
message str
severity Severity

TestReport (dataclass)

Decorators: dataclass · Line 194

Attribute Type Default
files_analyzed int 0
issues list[TestIssue] field(default_factory=list)
Method Signature Summary
add add(file: str, line: int, rule: str, message: str, severity: Severity) -> None

TestFileChecker

Line 221

AST-based checker for test file compliance.

Method Signature Summary
_is_test_named staticmethod _is_test_named(path: Path) -> bool True when path is test-named (test_*.py or *_test.py).
__init__ __init__(report: TestReport, registry_roots: list[Path] | None = None) -> None
check_file check_file(path: Path) -> None
_string_stmt_lines _string_stmt_lines(tree: ast.AST) -> set[int] Line numbers spanned by bare string-literal statements — i.e. module/ class/function docstrings and standalone string blocks.
_check_imports _check_imports(path: Path, tree: ast.AST, lines: list[str]) -> None
_check_fixtures _check_fixtures(path: Path, tree: ast.AST, lines: list[str]) -> None
_gate_ok staticmethod _gate_ok(node: ast.FunctionDef | ast.AsyncFunctionDef, lines: list[str]) -> bool True when a reasoned # gate-ok: <why> sits above the decorator.
_line_gate_ok staticmethod _line_gate_ok(lineno: int, lines: list[str]) -> bool Reasoned # gate-ok: <why> on the 1-based lineno or the line above — the adjacency rule test_isolation_audit.py::_is_waived uses, so per-line waivers are portable between the two gates.
_t4_immutable_ok staticmethod _t4_immutable_ok(node: ast.FunctionDef | ast.AsyncFunctionDef, lines: list[str]) -> bool The module-scope opt-in: annotation comment + return-only body.
_check_skips _check_skips(path: Path, tree: ast.AST, lines: list[str]) -> None
_check_mocking _check_mocking(path: Path, tree: ast.AST, lines: list[str]) -> None
_check_try_except_pass _check_try_except_pass(path: Path, tree: ast.AST, lines: list[str]) -> None
_mutable_literal staticmethod _mutable_literal(node: ast.AST) -> bool ATOM — the expression constructs a mutable container.
_is_module_state classmethod _is_module_state(name: str, value: ast.AST) -> bool FACT — a module-level binding that is STATE rather than a constant.
_check_global_state _check_global_state(path: Path, tree: ast.Module, lines: list[str]) -> None T9 — no module-level mutable state.
_check_monkeypatch _check_monkeypatch(path: Path, tree: ast.AST, lines: list[str]) -> None AST-based: only a real monkeypatch.<attr> access on a Name literally bound to monkeypatch counts.
_check_os_environ _check_os_environ(path: Path, tree: ast.AST, lines: list[str]) -> None AST-based: only a real os.environ[...] subscript — an ast.Subscript node whose value is the attribute access os.environ — counts.
_is_os_environ staticmethod _is_os_environ(node: ast.AST) -> bool
_check_hardcoded_ports _check_hardcoded_ports(path: Path, lines: list[str], doc_lines: set[int]) -> None
_check_no_xdist _check_no_xdist(path: Path, tree: ast.AST) -> None AST-based: only string arguments to an actual process invocation (subprocess.run/Popen/call/..., os.system, or pytest.main) count.
_is_invocation_call classmethod _is_invocation_call(func: ast.expr) -> bool
_iter_call_string_args staticmethod _iter_call_string_args(node: ast.Call) -> list[str]
_load_banned_libs _load_banned_libs(anchor: Path) -> list[str] Find and parse .claude/banned-libraries.txt by walking up from a test file (works whether test-review runs in-repo or globally-installed).
_check_banned_libraries _check_banned_libraries(path: Path, lines: list[str], doc_lines: set[int]) -> None
_check_test_id_usage _check_test_id_usage(path: Path, tree: ast.AST, lines: list[str]) -> None T10 — a module that WRITES to Redis/Memgraph must namespace by test_id.
_check_relative_imports _check_relative_imports(path: Path, tree: ast.AST, lines: list[str]) -> None
_anyio_mark staticmethod _anyio_mark(node: ast.AST) -> ast.AST | None Return the pytest.mark.anyio decorator on node, or None.
_check_anyio_marks _check_anyio_marks(path: Path) -> None T20 — no @pytest.mark.anyio while asyncio_mode is configured.
_check_async_tests _check_async_tests(path: Path, tree: ast.AST, lines: list[str]) -> None
_check_chunk_label_schema_teardown _check_chunk_label_schema_teardown(path: Path, tree: ast.AST) -> None T17 — a per-test Memgraph chunk_label must drop its schema.
_norm_label staticmethod _norm_label(token: str) -> str Label token, comparable between a CREATE and its DROP.
_token classmethod _token(match: re.Match[str]) -> str Normalised label from a DDL match, whichever alternative fired.
_fstring_piece staticmethod _fstring_piece(value: ast.AST) -> str One f-string segment as text, an interpolation as {name}.
_query_text classmethod _query_text(node: ast.AST) -> str | None Literal text of a string node, f-string placeholders preserved.
_ids_under staticmethod _ids_under(roots: Iterable[ast.AST]) -> frozenset[int] Identity of every node in the subtrees rooted at roots.
_docstring staticmethod _docstring(node: ast.AST) -> ast.Constant | None The node's docstring constant, when it has one.
_check_ddl_teardown _check_ddl_teardown(path: Path, tree: ast.AST) -> None T19 — DDL a test CREATEs, the test must DROP.
_check_bare_driver_session _check_bare_driver_session(path: Path, tree: ast.AST) -> None T18 — a raw <x>.driver.session() reads the WRONG database.
_only_cluster_level _only_cluster_level(tree: ast.AST, call: ast.Call) -> bool True when the with-block around call runs cluster-level DDL only.
_has_asyncio_mode _has_asyncio_mode() -> bool Check if project has asyncio_mode = 'auto' in pytest config.
_build_env_registry _build_env_registry(exclude_file: Path | None = None) -> dict[str, tuple[str, str, int]] Build the registry of declared env key constants.
_build_general_registry _build_general_registry(exclude_file: Path | None = None) -> dict[str, tuple[str, str, int]] Build the GENERAL constants registry (T17-GEN): every module-level UPPER_CASE string-constant assignment EXCEPT the *_ENV names (which the landed T21 tier owns).
_token_similarity _token_similarity(literal: str, registry_value: str) -> tuple[bool, int] Check if literal shares >=2 consecutive tokens with registry_value.
_is_env_key_shaped staticmethod _is_env_key_shaped(literal: str) -> bool True when literal is env-key-shaped: contains '_' and all cased chars are uppercase.
_check_env_key_drift _check_env_key_drift(path: Path, tree: ast.AST, lines: list[str]) -> None T21 — detect env key drift/fabrication by comparing literals to declared constants.
_add _add(path: Path, node: ast.AST, rule: str, message: str, severity: Severity = Severity.ERROR) -> None

Functions

Function Signature Summary
_strip_strings_and_comments _strip_strings_and_comments(lines: list[str]) -> list[str] Return lines with every string literal and comment blanked out.
_is_code_sample _is_code_sample(text: str) -> bool True when text is Python source carrying Cypher as an inner literal.
_gate_ok_lines _gate_ok_lines(lines: list[str]) -> set[int] Lines covered by a # gate-ok: declaration (the marker and the next).
_has_graph_writes _has_graph_writes(code: list[str], tree: ast.AST, raw: list[str]) -> bool True when the module both calls the driver and carries write Cypher.
_executes_cypher_write _executes_cypher_write(call: ast.Call, exempt: set[int]) -> bool True when write-Cypher is passed INTO call and call runs it.
_has_redis_writes _has_redis_writes(code: list[str]) -> bool
find_test_files find_test_files(paths: list[str]) -> list[Path]
output_text output_text(report: TestReport, min_severity: Severity, output_file: str | None = None) -> None
_apply_worklist_baseline _apply_worklist_baseline(report: TestReport, baseline_path: str) -> None Downgrade baseline rows (T21 exact-key WARNINGs named at dispatch time) to INFO — named debt, non-blocking.
main main() -> int

Source

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

#!/usr/bin/env python3
"""Test file reviewer — checks orchestrata-ultimate test standards.

Usage:
    python test-review.py tests/test_foo.py
    python test-review.py --format text --stdout tests/test_foo.py
    python test-review.py end2end/harness/
"""

from __future__ import annotations

import argparse
import ast
import re
import sys
from collections.abc import Iterable
from dataclasses import dataclass, field
from enum import StrEnum
from pathlib import Path


def _strip_strings_and_comments(lines: list[str]) -> list[str]:
    """Return *lines* with every string literal and comment blanked out.

    Line-regex rules that scan raw source cannot tell a real call from the
    same text quoted inside a docstring or inside test DATA — modules that
    assert on code samples carry things like ``"return redis.Redis(host='x')"``
    as a plain string. Blanking those regions first keeps the rules matching
    code only. Tokenizing preserves line numbering: each token's span is
    replaced in place with spaces.

    Falls back to the original lines when the source does not tokenize (a
    syntax error is reported by other rules, not swallowed here).
    """
    import io
    import tokenize

    blanked = list(lines)
    try:
        tokens = list(tokenize.generate_tokens(io.StringIO("\n".join(lines)).readline))
    except tokenize.TokenError, IndentationError, SyntaxError:
        return blanked

    for tok in tokens:
        if tok.type not in (tokenize.STRING, tokenize.COMMENT):
            continue
        (srow, scol), (erow, ecol) = tok.start, tok.end
        for row in range(srow, erow + 1):
            idx = row - 1
            if idx >= len(blanked):
                break
            line = blanked[idx]
            start = scol if row == srow else 0
            end = ecol if row == erow else len(line)
            blanked[idx] = line[:start] + " " * (end - start) + line[end:]
    return blanked


# A Cypher WRITE is a clause applied to something, not the bare keyword.
# Matching the word alone (the form until 2026-08-06) fired on `'CREATE'` used
# as a plain operation-name value in test expectations, and on docstring prose
# such as "Fix: Use MERGE or DELETE existing node first" — the artifact-vs-
# description confusion again, in a rule that then reported the module as
# writing to the graph. CREATE/MERGE must be followed by a node/edge pattern;
# SET/DELETE/REMOVE by an identifier.
# Case-SENSITIVE on purpose: Cypher clauses are written uppercase in this
# codebase, and matching case-insensitively turns every ordinary Python
# `create(...)` / `.set(...)` call into a graph write (measured: T10 went
# 12 -> 31 findings the moment IGNORECASE was added).
_CYPHER_WRITE = re.compile(
    r"\b(CREATE|MERGE)\s*\("
    r"|\b(SET|DELETE|REMOVE)\s+[\w`]"
    r"|\bDROP\s+(INDEX|CONSTRAINT)\b"
)
# Calls that RECORD Cypher rather than execute it. A write-Cypher string handed
# to one of these is fixture DATA (written to a temp file for the tool under
# test to scan, or shelled out), never a graph write.
_CYPHER_DATA_SINKS = frozenset(
    {"write_text", "write_bytes", "write", "writelines", "dump", "dumps"}
)

# A string that is itself PYTHON SOURCE is a code sample handed to a checker,
# not a query. tests/scripts/test_code_review_tool.py feeds the reviewer whole
# function bodies like ``def test_wipe():\n    session.run("MATCH (n) DETACH
# DELETE n")`` -- the Cypher there is quoted INSIDE the sample and never runs.
_PY_SOURCE_MARKERS = re.compile(r"^\s*(?:def |class |import |from \w+ import )", re.M)


def _is_code_sample(text: str) -> bool:
    """True when ``text`` is Python source carrying Cypher as an inner literal."""
    return "\n" in text and bool(_PY_SOURCE_MARKERS.search(text))


# Generic verbs (set/delete/expire) need a redis-ish receiver — a bare
# `.set(...)` matches far too much. The rest are distinctive enough to match any
# receiver, and every one of them mutates.
_REDIS_WRITE = re.compile(
    r"\bredis\w*\.(set|delete|expire|rename|incr|decr)\("
    r"|\.(setex|psetex|getset|setnx|msetnx|mset|hset|hsetnx|hdel"
    r"|hincrby|lpush|rpush|lpop|rpop|linsert|lset|ltrim|sadd|srem"
    r"|spop|zadd|zrem|zincrby|pexpire|persist|flushdb|flushall"
    r"|xadd|xtrim|xdel)\(",
    re.I,
)


def _gate_ok_lines(lines: list[str]) -> set[int]:
    """Lines covered by a ``# gate-ok:`` declaration (the marker and the next).

    The project's established way of saying "this Cypher is DATA, not a query
    anyone runs". Six gates honour it; this one did not, so a module whose
    fixtures are Cypher STRINGS the tooling scans was reported as writing to
    the graph.
    """
    marked = {n for n, line in enumerate(lines, start=1) if "gate-ok:" in line}
    return marked | {n + 1 for n in marked}


def _has_graph_writes(code: list[str], tree: ast.AST, raw: list[str]) -> bool:
    """True when the module both calls the driver and carries write Cypher.

    Requiring both keeps read-only query modules out: the call alone says
    nothing about whether anything is mutated.

    ``code`` is the comment/string-blanked source used for the call scan;
    ``raw`` is the original, because the ``# gate-ok:`` markers live in
    comments and are blanked out of ``code`` by construction.
    """
    exempt = _gate_ok_lines(raw)
    return any(
        _executes_cypher_write(call, exempt)
        for call in ast.walk(tree)
        if isinstance(call, ast.Call)
    )


def _executes_cypher_write(call: ast.Call, exempt: set[int]) -> bool:
    """True when write-Cypher is passed INTO ``call`` and ``call`` runs it.

    Keyed on where the Cypher GOES, not on what the receiver is named. Two
    earlier forms both failed on this codebase:

    * matching a bare ``.run(`` classed ``subprocess.run(...)`` as a driver call,
      so a module whose Cypher is fixture text (test_graph_test_preflight.py)
      read as writing to the graph;
    * allow-listing driver method names missed project helpers -- ``session.run``
      reached through ``def w(cypher)`` (test_requirement_resolution.py) and
      ``syncer._run("MERGE ...")`` (kg_sync/test_stage_session.py) are real
      writes under names no allow-list predicts.

    Passing Cypher to a call means something executes it, unless the callee is a
    known data sink or a subprocess spawn.
    """
    func = call.func
    name = func.attr if isinstance(func, ast.Attribute) else getattr(func, "id", "")
    if name in _CYPHER_DATA_SINKS:
        return False
    if (
        isinstance(func, ast.Attribute)
        and getattr(func.value, "id", "") == "subprocess"
    ):
        return False
    return any(
        isinstance(node, ast.Constant)
        and isinstance(node.value, str)
        and _CYPHER_WRITE.search(node.value)
        and not _is_code_sample(node.value)
        and getattr(node, "lineno", 0) not in exempt
        for arg in [*call.args, *(kw.value for kw in call.keywords)]
        for node in ast.walk(arg)
    )


def _has_redis_writes(code: list[str]) -> bool:
    return any(_REDIS_WRITE.search(line) for line in code)


class Severity(StrEnum):
    ERROR = "ERROR"
    WARNING = "WARNING"
    INFO = "INFO"


@dataclass
class TestIssue:
    file: str
    line: int
    rule: str
    message: str
    severity: Severity


@dataclass
class TestReport:
    files_analyzed: int = 0
    issues: list[TestIssue] = field(default_factory=list)

    def add(
        self, file: str, line: int, rule: str, message: str, severity: Severity
    ) -> None:
        self.issues.append(TestIssue(file, line, rule, message, severity))


def find_test_files(paths: list[str]) -> list[Path]:
    files: list[Path] = []
    for p in paths:
        path = Path(p)
        if path.is_file() and path.suffix == ".py":
            # Always include explicit .py files (for T21 env-key rule)
            files.append(path)
        elif path.is_dir():
            # Directories only include test-named files (T1-T20 scoping)
            files.extend(
                f
                for f in path.rglob("*.py")
                if f.name.startswith("test_") or f.name.endswith("_test.py")
            )
    return sorted(set(files))


class TestFileChecker:
    """AST-based checker for test file compliance."""

    RULES = {
        "T1": "No mocking — no unittest.mock, MagicMock, AsyncMock, Mock, patch",
        "T2": "No stubs or fakes — no FakeX, StubX, DummyX classes",
        "T3": "No try/except/pass blocks",
        "T4": "Function-scope fixtures only — module scope solely for annotated IMMUTABLE precomputed results ('# T4-immutable-module-scope:'), class/session never",
        "T5": "No os.environ mutations",
        "T6": "No hardcoded ports",
        "T7": "No pytest.mark.skip or skipif",
        "T8": "No monkeypatch usage",
        "T9": "No global mutable state at module level",
        "T10": "Use test_id fixture for DB/graph (Memgraph) ops",
        "T11": "Use isolation fixture for file/DB resources",
        "T12": "No bare os.environ['X'] reads — use os.environ.get()",
        "T13": "No relative imports",
        "T14": "Async tests need pytest.mark.asyncio or asyncio_mode",
        "T15": "No pytest-xdist (-n/--numprocesses/--dist) — it deadlocks this suite; use the threadpool runner",
        "T16": "No banned-library imports — see .claude/banned-libraries.txt",
        "T17": "A uuid-scoped Memgraph chunk_label must drop its schema in teardown — clear() leaves the constraint + indexes behind forever",
        "T18": "Verify through the store/repo's own session() — a bare `.driver.session()` reads the cluster default database, not the one under test",
        "T19": "DDL a test CREATEs, the test must DROP — node sweeps delete nodes only, so a constraint/index outlives the run forever and is replayed at boot recovery",
        "T21": "Env key drift/fabrication — compare string literals against declared constants (names ending in _ENV); flag similar-but-not-equal keys (ERROR) and exact literals outside declaring module (WARNING)",
    }

    @staticmethod
    def _is_test_named(path: Path) -> bool:
        """True when path is test-named (test_*.py or *_test.py).

        This predicate matches find_test_files' directory-scanning logic and
        determines which files are subject to T1-T20 rules. The T21 env-key
        rule runs on ALL explicit .py files regardless of naming.
        """
        return path.name.startswith("test_") or path.name.endswith("_test.py")

    def __init__(
        self, report: TestReport, registry_roots: list[Path] | None = None
    ) -> None:
        self.report = report
        # Nodes of the file under check — populated once per file by
        # check_file() and read by every rule (see the note there).
        self._nodes: list[ast.AST] = []
        # Banned distribution names (.claude/banned-libraries.txt), discovered
        # lazily from the first test file's repo. None = not yet loaded.
        self._banned_libs: list[str] | None = None
        # Registry of declared env key constants, built once per review run.
        # If registry_roots is None, use the default src/orchestrata_ultimate + scripts.
        self._registry_roots = registry_roots
        self._env_registry: dict[str, tuple[str, str, int]] | None = None
        self._general_registry: dict[str, tuple[str, str, int]] | None = None

    def check_file(self, path: Path) -> None:
        self.report.files_analyzed += 1
        source = path.read_text(encoding="utf-8")
        lines = source.splitlines()

        try:
            tree = ast.parse(source)
        except SyntaxError as exc:
            self.report.add(
                str(path),
                exc.lineno or 1,
                "SYNTAX",
                f"SyntaxError: {exc.msg}",
                Severity.ERROR,
            )
            return

        # ONE walk per file, shared by every check. Each check used to call
        # ast.walk(tree) itself — 17 full traversals of the same tree, on
        # every one of ~1550 test modules. Measured 2026-08-06: 36.07s wall /
        # 35.69s CPU over tests/ before this cache.
        self._nodes = list(ast.walk(tree))

        # T1-T20 rules are gated behind test-name scoping (same predicate as
        # find_test_files for directory scanning). T21 (env-key rule) runs on
        # ALL explicit .py files regardless of naming.
        if self._is_test_named(path):
            self._check_imports(path, tree, lines)
            self._check_fixtures(path, tree, lines)
            self._check_skips(path, tree, lines)
            self._check_mocking(path, tree, lines)
            self._check_try_except_pass(path, tree, lines)
            doc_lines = self._string_stmt_lines(tree)
            self._check_global_state(path, tree, lines)
            self._check_monkeypatch(path, tree, lines)
            self._check_os_environ(path, tree, lines)
            self._check_hardcoded_ports(path, lines, doc_lines)
            self._check_no_xdist(path, tree)
            self._check_banned_libraries(path, lines, doc_lines)
            self._check_test_id_usage(path, tree, lines)
            self._check_relative_imports(path, tree, lines)
            self._check_async_tests(path, tree, lines)
            self._check_anyio_marks(path)
            self._check_chunk_label_schema_teardown(path, tree)
            self._check_ddl_teardown(path, tree)
            self._check_bare_driver_session(path, tree)
        self._check_env_key_drift(path, tree, lines)

    # ------------------------------------------------------------------
    # Individual checks
    # ------------------------------------------------------------------

    def _string_stmt_lines(self, tree: ast.AST) -> set[int]:
        """Line numbers spanned by bare string-literal statements — i.e. module/
        class/function docstrings and standalone string blocks. Trigger words
        (monkeypatch, ``-n auto``) inside these are documentation, not code, so
        the text-based checks skip them. Assignments/calls that embed a string
        (e.g. ``cmd = "pytest -n auto"``) are NOT bare Expr statements and remain
        checked, so real usage is still caught.
        """
        out: set[int] = set()
        for node in self._nodes:
            if (
                isinstance(node, ast.Expr)
                and isinstance(node.value, ast.Constant)
                and isinstance(node.value.value, str)
            ):
                end = getattr(node, "end_lineno", node.lineno) or node.lineno
                out.update(range(node.lineno, end + 1))
        return out

    def _check_imports(self, path: Path, tree: ast.AST, lines: list[str]) -> None:
        for node in self._nodes:
            if isinstance(node, ast.Import):
                for alias in node.names:
                    name = alias.name
                    if name in ("unittest.mock", "mock"):
                        self._add(
                            path,
                            node,
                            "T1",
                            f"Import of '{name}' is forbidden (no mocking)",
                        )
            elif isinstance(node, ast.ImportFrom):
                module = node.module or ""
                if "unittest.mock" in module or "mock" in module.split("."):
                    self._add(
                        path,
                        node,
                        "T1",
                        f"Import from '{module}' is forbidden (no mocking)",
                    )
                names = [a.name for a in node.names]
                for n in names:
                    if n in ("MagicMock", "AsyncMock", "Mock", "patch", "mocker"):
                        self._add(
                            path,
                            node,
                            "T1",
                            f"Import of '{n}' is forbidden (no mocking)",
                        )

    def _check_fixtures(self, path: Path, tree: ast.AST, lines: list[str]) -> None:
        # T4 harmonized with the CLAUDE.md Key Rule wording ("no module/class
        # scope for MUTABLE state"): scope='module' is allowed ONLY for an
        # immutable precomputed result, opted in explicitly with a
        # '# T4-immutable-module-scope: <why>' comment directly above the
        # decorator AND a return-only body (no yield — a teardown implies a
        # lifecycle, which implies state). class/session scope stay forbidden
        # unconditionally.
        for node in self._nodes:
            if not isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
                continue
            for dec in node.decorator_list:
                if not isinstance(dec, ast.Call):
                    continue
                func = dec.func
                name = (
                    func.attr
                    if isinstance(func, ast.Attribute)
                    else getattr(func, "id", "")
                )
                if name != "fixture":
                    continue
                for kw in dec.keywords:
                    if kw.arg == "scope" and isinstance(kw.value, ast.Constant):
                        # ast.Constant.value is Any, so a bytes literal
                        # (scope=b"session") would render as "b'session'" in
                        # the message below. Only a str scope is meaningful to
                        # pytest; anything else cannot name a forbidden scope.
                        if not isinstance(kw.value.value, str):
                            continue
                        scope: str = kw.value.value
                        if scope == "session" and self._gate_ok(node, lines):
                            # Session-scoped CONNECTION infra (the conftest
                            # neo4j_driver precedent) carries no mutable test
                            # state and is what makes the shared-index test
                            # files affordable. gate_parallel_safety.py already
                            # accepts a reasoned '# gate-ok:' waiver on exactly
                            # these lines; honouring the SAME marker here keeps
                            # one waiver vocabulary across both reviewers
                            # instead of one tool contradicting the other.
                            continue
                        if scope in ("class", "session"):
                            self._add(
                                path,
                                node,
                                "T4",
                                f"Fixture with scope='{scope}' is forbidden — use function scope only"
                                + (
                                    " (session-scoped CONNECTION infra may opt out with a reasoned '# gate-ok: <why>' comment directly above the decorator)"
                                    if scope == "session"
                                    else ""
                                ),
                            )
                        elif scope == "module" and not self._t4_immutable_ok(
                            node, lines
                        ):
                            self._add(
                                path,
                                node,
                                "T4",
                                "Fixture with scope='module' is forbidden — use function scope, or opt in an IMMUTABLE precomputed result with '# T4-immutable-module-scope: <why>' directly above the decorator (return-only, no yield)",
                            )

    @staticmethod
    def _gate_ok(
        node: ast.FunctionDef | ast.AsyncFunctionDef, lines: list[str]
    ) -> bool:
        """True when a reasoned ``# gate-ok: <why>`` sits above the decorator.

        Same marker and same "a reason is mandatory, a bare marker does not
        waive" rule as ``scripts/gate_parallel_safety.py`` (``_WAIVER_RE``), so
        one annotation satisfies both reviewers rather than each tool inventing
        its own vocabulary.
        """
        first_line = min([d.lineno for d in node.decorator_list] + [node.lineno])
        for offset in (2, 3):  # the line above the decorator, and the one above that
            if first_line >= offset:
                if re.search(r"#\s*gate-ok:\s*\S", lines[first_line - offset]):
                    return True
        return False

    @staticmethod
    def _line_gate_ok(lineno: int, lines: list[str]) -> bool:
        """Reasoned ``# gate-ok: <why>`` on the 1-based *lineno* or the line
        above — the adjacency rule ``test_isolation_audit.py::_is_waived``
        uses, so per-line waivers are portable between the two gates.
        """
        for idx in (lineno - 1, lineno - 2):
            if 0 <= idx < len(lines) and re.search(r"#\s*gate-ok:\s*\S", lines[idx]):
                return True
        return False

    @staticmethod
    def _t4_immutable_ok(
        node: ast.FunctionDef | ast.AsyncFunctionDef, lines: list[str]
    ) -> bool:
        """The module-scope opt-in: annotation comment + return-only body."""
        first_line = min([d.lineno for d in node.decorator_list] + [node.lineno])
        above = lines[first_line - 2] if first_line >= 2 else ""
        if "T4-immutable-module-scope:" not in above:
            return False
        return not any(isinstance(n, ast.Yield | ast.YieldFrom) for n in ast.walk(node))

    def _check_skips(self, path: Path, tree: ast.AST, lines: list[str]) -> None:
        for node in self._nodes:
            if isinstance(node, ast.Attribute):
                if node.attr in ("skip", "skipif"):
                    self._add(
                        path, node, "T7", f"Use of pytest.mark.{node.attr} is forbidden"
                    )

    def _check_mocking(self, path: Path, tree: ast.AST, lines: list[str]) -> None:
        for node in self._nodes:
            if isinstance(node, ast.Name):
                if node.id in ("MagicMock", "AsyncMock", "Mock"):
                    self._add(
                        path,
                        node,
                        "T1",
                        f"Use of '{node.id}' is forbidden (no mocking)",
                    )
            elif isinstance(node, ast.ClassDef):
                if any(node.name.startswith(p) for p in ("Fake", "Stub", "Dummy")):
                    self._add(
                        path,
                        node,
                        "T2",
                        f"Class '{node.name}' appears to be a stub/fake (no stubs/fakes)",
                    )

    def _check_try_except_pass(
        self, path: Path, tree: ast.AST, lines: list[str]
    ) -> None:
        for node in self._nodes:
            if isinstance(node, ast.Try):
                for handler in node.handlers:
                    if len(handler.body) == 1 and isinstance(handler.body[0], ast.Pass):
                        self._add(
                            path, handler, "T3", "try/except/pass block is forbidden"
                        )

    # Names that are shared mutable state whatever they are bound to.
    _STATE_NAMES = frozenset({"_cache", "_state", "_registry", "_instances"})

    # Names RESERVED BY PYTEST at module level — pytest's own documented API,
    # not state this suite owns. ``pytestmark`` is read by the collector at
    # import time to apply markers to every test in the module; nothing
    # mutates it, and nothing carries data between tests through it. The
    # mutable-container heuristic below could not tell pytest's protocol from
    # a suite-authored list and flagged all 62 modules that use the idiom —
    # a rule with no model of the framework it reviews.
    _PYTEST_RESERVED = frozenset(
        {"pytestmark", "collect_ignore", "collect_ignore_glob"}
    )

    @staticmethod
    def _mutable_literal(node: ast.AST) -> bool:
        """ATOM — the expression constructs a mutable container."""
        if isinstance(node, (ast.List, ast.Dict, ast.Set, ast.ListComp, ast.DictComp)):
            return True
        return (
            isinstance(node, ast.Call)
            and isinstance(node.func, ast.Name)
            and node.func.id in ("list", "dict", "set", "defaultdict", "Counter")
        )

    @classmethod
    def _is_module_state(cls, name: str, value: ast.AST) -> bool:
        """FACT — a module-level binding that is STATE rather than a constant.

        Two ways to qualify: a known shared-state name, or a mutable container
        bound to a non-UPPER_CASE name. The name list alone caught exactly four
        spellings and missed every other module-level mutable — UPPER_CASE is
        the project's constant convention, so anything else holding a mutable
        container is state.
        """
        if name in cls._PYTEST_RESERVED:
            return False
        return name in cls._STATE_NAMES or (
            not name.isupper() and cls._mutable_literal(value)
        )

    def _check_global_state(
        self, path: Path, tree: ast.Module, lines: list[str]
    ) -> None:
        """T9 — no module-level mutable state."""
        bindings = (
            (node, target.id)
            for node in tree.body
            if isinstance(node, ast.Assign)
            for target in node.targets
            if isinstance(target, ast.Name)
        )
        for node, name in bindings:
            if self._is_module_state(name, node.value):
                self._add(
                    path,
                    node,
                    "T9",
                    f"Module-level mutable state '{name}' is forbidden",
                )

    def _check_monkeypatch(self, path: Path, tree: ast.AST, lines: list[str]) -> None:
        """AST-based: only a real ``monkeypatch.<attr>`` access on a Name
        literally bound to ``monkeypatch`` counts. A line-based/regex scan
        (the prior implementation) false-positived on any line merely
        *containing* the substring "monkeypatch" — e.g. an identifier like
        a fixture named ``monkeypatch_free_chdir`` (declaring it does NOT
        use monkeypatch), or a string literal used as test data for a
        meta-test of a pattern-detector (e.g. ``lines = ["monkeypatch.setenv(...)\\n"]``
        fed into ``verification.review.checks_safety`` or this very tool's
        own test suite). Walking ``ast.Attribute`` nodes only matches actual
        attribute access in real code, never substrings or string contents.
        """
        for node in self._nodes:
            if not isinstance(node, ast.Attribute):
                continue
            if not (
                isinstance(node.value, ast.Name) and node.value.id == "monkeypatch"
            ):
                continue
            # monkeypatch.setenv/delenv is allowed in tests per AGENTS.md.
            if node.attr in ("setenv", "delenv"):
                continue
            self._add(
                path,
                node,
                "T8",
                "Use of monkeypatch is forbidden (only monkeypatch.setenv/delenv allowed)",
            )

    def _check_os_environ(self, path: Path, tree: ast.AST, lines: list[str]) -> None:
        """AST-based: only a real ``os.environ[...]`` subscript — an
        ``ast.Subscript`` node whose value is the attribute access
        ``os.environ`` — counts. A text/regex scan over raw lines (the prior
        implementation) false-positived on any line merely *containing* the
        substring, including inert string-literal fixture data belonging to
        OTHER tools' tests — e.g. Python source text fed to
        ``verification.review.checks_safety.check_env_access`` via
        ``ast.parse(source)``, which is not real code in THIS file, just a
        string constant. Walking ``ast.Subscript`` nodes only matches genuine
        subscript syntax; a string can never masquerade as one.

        A T5 mutation accepts a reasoned ``# gate-ok: <why>`` on the flagged
        line or the line above — the SAME waiver channel (marker, mandatory
        reason, adjacency rule) as ``test_isolation_audit.py``'s
        OS_ENVIRON_MUTATION check, so one annotation satisfies both gates.
        Two gates disagreeing about the same save/restore seam is worse than
        either being absent (see the T4 cross-reference note above).
        """
        for node in self._nodes:
            if not isinstance(node, ast.Subscript):
                continue
            if not self._is_os_environ(node.value):
                continue
            if isinstance(node.ctx, ast.Store | ast.Del):
                if self._line_gate_ok(node.lineno, lines):
                    continue
                self._add(path, node, "T5", "Mutation of os.environ[...] is forbidden")
            else:
                self._add(
                    path,
                    node,
                    "T12",
                    "Use os.environ.get(...) instead of os.environ[...]",
                    severity=Severity.WARNING,
                )

    @staticmethod
    def _is_os_environ(node: ast.AST) -> bool:
        return (
            isinstance(node, ast.Attribute)
            and node.attr == "environ"
            and isinstance(node.value, ast.Name)
            and node.value.id == "os"
        )

    def _check_hardcoded_ports(
        self, path: Path, lines: list[str], doc_lines: set[int]
    ) -> None:
        for i, line in enumerate(lines, 1):
            if i in doc_lines or line.lstrip().startswith("#"):
                continue
            if re.search(r"port\s*=\s*5\d{4}", line):
                self.report.add(
                    str(path),
                    i,
                    "T6",
                    "Hardcoded port detected — use dynamic allocation",
                    Severity.ERROR,
                )

    # SHOWSTOPPER: pytest-xdist (-n / --numprocesses / --dist) deadlocks this
    # suite (~78% stall). Use the threadpool runner instead
    # (scripts/run_tests_threadpool.py / core.threading.test_executor).
    _XDIST_TEXT_RE = re.compile(
        r"--dist\b|--numprocesses\b|pytest-xdist|(?<![\w-])-n[ =]*(auto|\d)"
    )
    # Callables that actually spawn/execute a command — the only place an
    # xdist CLI flag could ever really run pytest in xdist mode.
    _INVOCATION_ATTRS = {
        "run",
        "call",
        "check_call",
        "check_output",
        "Popen",
        "system",
        "popen",
    }

    def _check_no_xdist(self, path: Path, tree: ast.AST) -> None:
        """AST-based: only string arguments to an actual process invocation
        (``subprocess.run``/``Popen``/``call``/..., ``os.system``, or
        ``pytest.main``) count. A text/regex scan over raw lines (the prior
        implementation) false-positived on inert string-literal fixture data
        belonging to OTHER tools' tests — e.g. a fake ``.gitlab-ci.yml`` body
        ("head -n 100", "--dist-dir=dist") written via ``write_text`` for
        scripts/validate_ci.py's own gate tests, which is YAML text, never
        actually invoked as a command here. Restricting to real invocation
        call sites means a string can never masquerade as a genuine one.
        """
        for node in self._nodes:
            if not isinstance(node, ast.Call) or not self._is_invocation_call(
                node.func
            ):
                continue
            command = " ".join(self._iter_call_string_args(node))
            if not command or "ORCHESTRATA_WORKER_SLOT" in command:
                continue
            if self._XDIST_TEXT_RE.search(command):
                self._add(
                    path,
                    node,
                    "T15",
                    "pytest-xdist (-n/--numprocesses/--dist) is forbidden — it deadlocks this "
                    "suite (~78%); use scripts/run_tests_threadpool.py (core.threading.test_executor)",
                )

    @classmethod
    def _is_invocation_call(cls, func: ast.expr) -> bool:
        if isinstance(func, ast.Attribute):
            if func.attr in cls._INVOCATION_ATTRS:
                return True
            return (
                func.attr == "main"
                and isinstance(func.value, ast.Name)
                and func.value.id == "pytest"
            )
        if isinstance(func, ast.Name):
            return func.id in cls._INVOCATION_ATTRS
        return False

    @staticmethod
    def _iter_call_string_args(node: ast.Call) -> list[str]:
        out: list[str] = []

        def collect(n: ast.AST) -> None:
            if isinstance(n, ast.Constant) and isinstance(n.value, str):
                out.append(n.value)
            elif isinstance(n, ast.List | ast.Tuple):
                for elt in n.elts:
                    collect(elt)

        for arg in node.args:
            collect(arg)
        for kw in node.keywords:
            if kw.value is not None:
                collect(kw.value)
        return out

    def _load_banned_libs(self, anchor: Path) -> list[str]:
        """Find and parse .claude/banned-libraries.txt by walking up from a test
        file (works whether test-review runs in-repo or globally-installed).
        Cached after the first lookup."""
        if self._banned_libs is not None:
            return self._banned_libs
        self._banned_libs = []
        for parent in [anchor.resolve(), *anchor.resolve().parents]:
            candidate = parent / ".claude" / "banned-libraries.txt"
            if candidate.exists():
                self._banned_libs = [
                    ln.strip()
                    for ln in candidate.read_text(encoding="utf-8").splitlines()
                    if ln.strip() and not ln.strip().startswith("#")
                ]
                break
        return self._banned_libs

    def _check_banned_libraries(
        self, path: Path, lines: list[str], doc_lines: set[int]
    ) -> None:
        # A test file must not import a banned library. Derives the import module
        # from each banned distribution name (pytest-xdist -> xdist). xdist's
        # command flags are already covered by T15; this catches the import.
        banned = self._load_banned_libs(path)
        if not banned:
            return
        import_names = {b.removeprefix("pytest-").replace("-", "_"): b for b in banned}
        pattern = re.compile(
            r"^\s*(?:import|from)\s+("
            + "|".join(re.escape(n) for n in import_names)
            + r")\b"
        )
        # Scan with string literals and comments blanked (same treatment as
        # T10 below): an ``import xdist`` inside a payload string is test DATA
        # that never runs — gate-test fixtures carry banned source as strings.
        code = _strip_strings_and_comments(lines)
        for i, line in enumerate(code, 1):
            if i in doc_lines or line.lstrip().startswith("#"):
                continue
            m = pattern.match(line)
            if m:
                dist = import_names[m.group(1)]
                self.report.add(
                    str(path),
                    i,
                    "T16",
                    f"banned library '{dist}' imported — see "
                    ".claude/banned-libraries.txt",
                    Severity.ERROR,
                )

    def _check_test_id_usage(self, path: Path, tree: ast.AST, lines: list[str]) -> None:
        """T10 — a module that WRITES to Redis/Memgraph must namespace by test_id.

        The point of ``test_id`` is collision-freedom between parallel workers
        sharing one real database, so only WRITES can violate it. Reads have
        nothing to namespace, and three things used to be flagged that cannot
        collide with anything:

        - read-only Cypher (``session.run("RETURN 1 AS ok")``);
        - ``.pipeline(`` on any receiver — this project's own ``PipelineRunner``
          and volk pipelines match it far more often than redis-py does;
        - ``redis.Redis(`` appearing inside a docstring or a string literal
          that IS the test data (modules that assert on code samples).

        The scan therefore runs over source with strings and comments blanked
        out, and requires an actual write: a redis write command, or a Cypher
        write clause in one of the module's string constants.
        """
        code = _strip_strings_and_comments(lines)
        writes = _has_graph_writes(code, tree, lines) or _has_redis_writes(code)

        # `test_id` is one way to namespace; a per-test `uuid4()` prefix is the
        # other, and it buys the same collision-freedom. Checking only for the
        # fixture NAME flagged modules that already isolate correctly.
        has_namespacing = any(
            "test_id" in line or re.search(r"\buuid\d?\(|\buuid\.uuid\d\(", line)
            for line in code
        )
        has_conftest = path.name == "conftest.py"
        if writes and not has_namespacing and not has_conftest:
            self.report.add(
                str(path),
                1,
                "T10",
                "File WRITES to Redis/Memgraph without namespacing — use the "
                "test_id fixture or a per-test uuid4() prefix",
                Severity.WARNING,
            )

    def _check_relative_imports(
        self, path: Path, tree: ast.AST, lines: list[str]
    ) -> None:
        for node in self._nodes:
            if isinstance(node, ast.ImportFrom):
                if node.level > 0:
                    self._add(
                        path,
                        node,
                        "T13",
                        "Relative import is forbidden — use absolute imports",
                    )

    @staticmethod
    def _anyio_mark(node: ast.AST) -> ast.AST | None:
        """Return the `pytest.mark.anyio` decorator on *node*, or None.

        Matches both the bare form (`@pytest.mark.anyio`) and the called form
        (`@pytest.mark.anyio(...)`), on functions and on classes.
        """
        decorators = getattr(node, "decorator_list", [])
        for dec in decorators:
            target = dec.func if isinstance(dec, ast.Call) else dec
            if isinstance(target, ast.Attribute) and target.attr == "anyio":
                inner = target.value
                if isinstance(inner, ast.Attribute) and inner.attr == "mark":
                    return dec
        return None

    def _check_anyio_marks(self, path: Path) -> None:
        """T20 — no `@pytest.mark.anyio` while asyncio_mode is configured.

        Under `asyncio_mode = auto`, pytest-asyncio already owns every async
        test and async fixture. Adding the anyio mark puts two plugins in the
        same regime, and they do not agree about which event loop a FIXTURE
        runs on versus the TEST that consumes it. That split is what broke
        tests/gateway/handlers/test_projects.py on Python 3.14.6t (backlog
        P2.14); 55 further files carried the same latent shape and were
        de-marked. This rule is why they stay de-marked.
        """
        if not self._has_asyncio_mode():
            return
        for node in self._nodes:
            if not isinstance(
                node, ast.AsyncFunctionDef | ast.FunctionDef | ast.ClassDef
            ):
                continue
            dec = self._anyio_mark(node)
            if dec is not None:
                self._add(
                    path,
                    dec,
                    "T20",
                    "@pytest.mark.anyio is forbidden — asyncio_mode is configured, so "
                    "pytest-asyncio owns async tests and fixtures; running both plugins "
                    "splits the fixture loop from the test loop (P2.14). Delete the mark.",
                )

    def _check_async_tests(self, path: Path, tree: ast.AST, lines: list[str]) -> None:
        # Check project-wide asyncio_mode config
        has_asyncio_mode = self._has_asyncio_mode()
        for node in self._nodes:
            if isinstance(node, ast.AsyncFunctionDef):
                if node.name.startswith("test_"):
                    # Check if decorated with pytest.mark.asyncio
                    has_marker = any(
                        isinstance(d, ast.Call)
                        and isinstance(d.func, ast.Attribute)
                        and d.func.attr == "asyncio"
                        for d in node.decorator_list
                    )
                    if not has_marker and not has_asyncio_mode:
                        self._add(
                            path,
                            node,
                            "T14",
                            f"Async test '{node.name}' needs @pytest.mark.asyncio or asyncio_mode in config",
                        )

    def _check_chunk_label_schema_teardown(self, path: Path, tree: ast.AST) -> None:
        """T17 — a per-test Memgraph ``chunk_label`` must drop its schema.

        ``connect()`` creates a uniqueness constraint and two lookup indexes
        for whatever ``chunk_label`` names. ``clear()``/``clear_prefixed()``
        delete NODES only; the schema survives, and Memgraph replays every
        entry at boot recovery. When the label carries the ``test_id`` uuid
        that is unbounded growth: 3,379 leaked ``*__TEST_*`` labels (6,713
        indexes + 3,323 constraints guarding 9 nodes) cost the shared cluster
        ~17 minutes of pegged CPU per data-node restart in the 2026-07-15
        incident, and two fixtures had regrown the same leak by 2026-07-26.

        Fixed per-WORKER labels (``f"TestChunk{worker_slot}"``) are bounded by
        the worker count and deliberately keep their schema so the warm-run
        ``connect()`` short-circuit stays effective — this rule only fires on
        labels interpolating ``test_id``.
        """
        for node in self._nodes:
            if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue

            uuid_scoped_label: ast.AST | None = None
            for sub in ast.walk(node):
                if not isinstance(sub, ast.keyword) or sub.arg != "chunk_label":
                    continue
                if not isinstance(sub.value, ast.JoinedStr):
                    continue
                names = {
                    v.value.id
                    for v in sub.value.values
                    if isinstance(v, ast.FormattedValue)
                    and isinstance(v.value, ast.Name)
                }
                if "test_id" in names:
                    uuid_scoped_label = sub.value
                    break

            if uuid_scoped_label is None:
                continue

            # Only functions that OWN the graph's lifecycle can drop its
            # schema. A pure config factory (builds a SemanticGraphConfig and
            # returns it) has no teardown to put the drop in -- its consumers
            # do, and they are checked on their own. Ownership evidence: the
            # function yields the resource, or calls a lifecycle method on it.
            attrs = {
                sub.attr for sub in ast.walk(node) if isinstance(sub, ast.Attribute)
            }
            owns_lifecycle = bool(
                attrs & {"clear", "clear_prefixed", "close", "connect"}
            ) or any(
                isinstance(sub, (ast.Yield, ast.YieldFrom)) for sub in ast.walk(node)
            )
            if not owns_lifecycle:
                continue

            drops = "drop_chunk_label_schema" in attrs
            if not drops:
                self._add(
                    path,
                    node,
                    "T17",
                    f"'{node.name}' builds a uuid-scoped chunk_label but never calls "
                    "drop_chunk_label_schema() — its constraint + 2 indexes leak on "
                    "every run and Memgraph replays them at boot recovery",
                )

    # Cypher DDL as it appears inside a test's query strings. The label token
    # is captured VERBATIM: for an interpolated label the capture is the
    # placeholder itself (``{label}``), which is precisely what lets a CREATE
    # pair with its DROP without resolving the expression.
    _DDL_CREATE = re.compile(
        r"CREATE\s+CONSTRAINT\s+ON\s*\(\s*\w+\s*:\s*(?P<label>[^)\s]+)"
        r"|CREATE\s+(?:\w+\s+)?INDEX\s+ON\s*:\s*(?P<ilabel>[^(\s]+)",
        re.IGNORECASE,
    )
    _DDL_DROP = re.compile(
        r"DROP\s+CONSTRAINT\s+ON\s*\(\s*\w+\s*:\s*(?P<label>[^)\s]+)"
        r"|DROP\s+(?:\w+\s+)?INDEX\s+ON\s*:\s*(?P<ilabel>[^(\s]+)",
        re.IGNORECASE,
    )

    # Cheap prefilter: only strings mentioning CREATE or DROP can carry DDL.
    _DDL_WORD = re.compile(r"CREATE|DROP", re.IGNORECASE)

    @staticmethod
    def _norm_label(token: str) -> str:
        """Label token, comparable between a CREATE and its DROP."""
        return token.strip().strip("`").strip()

    @classmethod
    def _token(cls, match: re.Match[str]) -> str:
        """Normalised label from a DDL match, whichever alternative fired."""
        return cls._norm_label(match.group("label") or match.group("ilabel") or "")

    @staticmethod
    def _fstring_piece(value: ast.AST) -> str:
        """One f-string segment as text, an interpolation as ``{name}``."""
        if isinstance(value, ast.Constant) and isinstance(value.value, str):
            return value.value
        if not isinstance(value, ast.FormattedValue):
            return ""
        inner = value.value
        if isinstance(inner, ast.Name):
            return "{" + inner.id + "}"
        if isinstance(inner, ast.Attribute):
            return "{" + inner.attr + "}"
        return "{?}"

    @classmethod
    def _query_text(cls, node: ast.AST) -> str | None:
        """Literal text of a string node, f-string placeholders preserved.

        An interpolated label survives as ``{name}`` rather than being lost,
        so ``f"...ON (n:{label})..."`` and its DROP produce the same token.
        """
        if isinstance(node, ast.Constant) and isinstance(node.value, str):
            return node.value
        if isinstance(node, ast.JoinedStr):
            return "".join(map(cls._fstring_piece, node.values))
        return None

    @staticmethod
    def _ids_under(roots: Iterable[ast.AST]) -> frozenset[int]:
        """Identity of every node in the subtrees rooted at ``roots``."""
        return frozenset(id(sub) for root in roots for sub in ast.walk(root))

    @staticmethod
    def _docstring(node: ast.AST) -> ast.Constant | None:
        """The node's docstring constant, when it has one."""
        body = getattr(node, "body", None)
        if not isinstance(body, list) or not body:
            return None
        first = body[0]
        if (
            isinstance(first, ast.Expr)
            and isinstance(first.value, ast.Constant)
            and isinstance(first.value.value, str)
        ):
            return first.value
        return None

    def _check_ddl_teardown(self, path: Path, tree: ast.AST) -> None:
        """T19 — DDL a test CREATEs, the test must DROP.

        ``CREATE CONSTRAINT`` / ``CREATE INDEX`` on a per-test label is
        permanent: node sweeps delete NODES only, the schema survives, and
        Memgraph replays every entry at boot recovery. T17 covers exactly one
        shape of this — the ``chunk_label=`` keyword — and structurally cannot
        see raw Cypher, which is how every other site writes it.

        Measured 2026-08-06: 57 leaked constraints across 7 databases on the
        test cluster, from five fixtures issuing raw ``CREATE CONSTRAINT`` with
        no drop. ``sweep_test_pollution.py`` contains no ``DROP CONSTRAINT`` at
        all, so nothing in the project could ever have cleaned them.

        Pairing is by label TOKEN, so an interpolated label pairs through its
        placeholder. DDL that a test merely ASSERTS ABOUT — a generator's
        expected output compared against a literal — is not executed DDL and
        is excluded; counting it would repeat the artifact-vs-description
        confusion this project has now measured eight times.
        """
        # Strings that DESCRIBE DDL rather than run it. Two shapes:
        # operands of an assert/comparison (a generator's expected output),
        # and docstrings — a fixture that documents the DDL it measured
        # ("CREATE INDEX ON :<label>(id) (2,514ms) ...") is prose, not a
        # statement. Missing the docstring case produced a false positive on
        # tests/semantic_graph/test_memgraph_adapter.py the first time this
        # rule ran: the ninth instance in one day of a scanner with no model
        # of which input is the artifact and which is prose about it
        # (concepts/gates-must-separate-artifact-from-description).
        nodes = list(self._nodes)

        described = self._ids_under(
            n for n in nodes if isinstance(n, (ast.Assert, ast.Compare))
        ) | frozenset(map(id, filter(None, (self._docstring(n) for n in nodes))))

        # An f-string's own literal pieces are Constants too; scoring them
        # separately would split "CREATE CONSTRAINT ON (n:" from its label.
        joined = [n for n in nodes if isinstance(n, ast.JoinedStr)]
        inner = self._ids_under(joined) - frozenset(map(id, joined))

        # DDL only counts when it reaches an execution seam. A test-review
        # corpus is Python source stored as DATA — this rule's own test file
        # embeds "CREATE CONSTRAINT ..." in string constants it writes to a
        # temp file — and a string that is never handed to a call executes
        # nothing.
        executed = self._ids_under(n for n in nodes if isinstance(n, ast.Call))

        texts = [
            (n, text)
            for n, text in ((n, self._query_text(n)) for n in nodes)
            if text and id(n) not in inner and self._DDL_WORD.search(text)
        ]

        dropped = frozenset(
            self._token(m) for _, text in texts for m in self._DDL_DROP.finditer(text)
        )
        created: dict[str, ast.AST] = {}
        for node, text in texts:
            if id(node) in described or id(node) not in executed:
                continue
            created.update(
                {
                    self._token(m): node
                    for m in self._DDL_CREATE.finditer(text)
                    if self._token(m) not in created
                }
            )

        # A DROP whose label is COMPUTED can drop anything, so it satisfies
        # every create in the file. The idiom is a teardown loop —
        # ``for label in (A, B): run(f"DROP CONSTRAINT ON (n:{label}) ...")``
        # — which creates by literal and drops by loop variable, so literal
        # pairing would flag correct teardown as a leak.
        wildcard_drop = any("{" in token for token in dropped)
        leaked = (
            {}
            if wildcard_drop
            else {
                label: node for label, node in created.items() if label not in dropped
            }
        )

        for label, node in sorted(leaked.items()):
            self._add(
                path,
                node,
                "T19",
                f"creates DDL on ':{label}' and never drops it — the schema "
                "outlives the test forever (node sweeps delete nodes only) and "
                "Memgraph replays it at every boot recovery; drop it in teardown",
            )

    def _check_bare_driver_session(self, path: Path, tree: ast.AST) -> None:
        """T18 — a raw ``<x>.driver.session()`` reads the WRONG database.

        Since the 2026-07-26 orchgraph cutover the operational KG and the
        shared semantic/RAG graph are two DATABASES on one cluster, so the
        URI no longer picks between them — the ``database=`` argument does.
        Stores and repositories thread it on every call; reaching past them
        to ``.driver.session()`` does not, so the session lands on the
        cluster default.

        This is a FALSE-GREEN shape, not a style nit: five sync/promotion
        test modules were verifying an empty database and passing anyway
        (measured 2026-07-26). Use the object's own ``session()`` —
        ``store.session()``, ``repo.session()`` — which binds the database
        the code under test actually writes to.
        """
        kg_operational = any(
            isinstance(n, ast.Name) and n.id in ("NEO4J_URI", "resolve_kg_database")
            for n in self._nodes
        )
        for node in self._nodes:
            if not isinstance(node, ast.Call):
                continue
            func = node.func
            # Shape: <anything>.driver.session(...)
            if not isinstance(func, ast.Attribute) or func.attr != "session":
                continue
            # Two shapes reach a raw driver: `<obj>.driver.session()` and a
            # bare driver held directly (`neo4j_cleanup_driver.session()`) —
            # the second is how a fixture verifying the wrong database
            # slipped past the first sweep.
            #
            # The bare-name shape is only flagged in modules that target the
            # OPERATIONAL KG (`kg_operational` below). Semantic-graph tests
            # use the same driver class against the cluster DEFAULT database,
            # which is correct for them — flagging those turned 0 findings
            # into 352.
            receiver = func.value
            if isinstance(receiver, ast.Attribute):
                if receiver.attr != "driver":
                    continue
            elif isinstance(receiver, ast.Name):
                if not receiver.id.endswith("driver") or not kg_operational:
                    continue
            else:
                continue
            # `database=` supplied explicitly is the caller doing it right.
            if any(kw.arg == "database" for kw in node.keywords):
                continue
            # A session running only CLUSTER-level statements has no database
            # to bind: `SHOW DATABASES` enumerates the cluster, and there is
            # no store or repository whose session() could scope it. Measured
            # 2026-08-06 on tests/core/test_worker_database_mapping.py, where
            # the raw driver is deliberate and correct.
            if self._only_cluster_level(tree, node):
                continue
            self._add(
                path,
                node,
                "T18",
                "bare `.driver.session()` reads the cluster default database, "
                "not the one under test — use the store/repo's own session()",
            )

    # Statements that address the CLUSTER, not a database within it. A
    # session running only these has nothing to scope: `database=` on a
    # `SHOW DATABASES` call would name one of the very things being listed.
    _CLUSTER_LEVEL = re.compile(
        r"^\s*(SHOW\s+(DATABASES|VERSION|REPLICATION\s+ROLE|STORAGE\s+INFO)"
        r"|CREATE\s+DATABASE|DROP\s+DATABASE|USE\s+DATABASE)\b",
        re.IGNORECASE,
    )

    def _only_cluster_level(self, tree: ast.AST, call: ast.Call) -> bool:
        """True when the ``with``-block around ``call`` runs cluster-level DDL only.

        Conservative by construction: a block that runs even one ordinary
        query, or whose statements cannot be read statically, is NOT exempt —
        so the false-green shape T18 exists to catch stays caught.
        """
        blocks = [
            node
            for node in self._nodes
            if isinstance(node, (ast.With, ast.AsyncWith))
            and any(item.context_expr is call for item in node.items)
        ]
        if not blocks:
            return False

        runs = [
            sub
            for stmt in blocks[0].body
            for sub in ast.walk(stmt)
            if isinstance(sub, ast.Call)
            and isinstance(sub.func, ast.Attribute)
            and sub.func.attr == "run"
        ]
        # A query with no readable literal cannot be proven cluster-level.
        texts = [self._query_text(r.args[0]) if r.args else None for r in runs]
        return (
            bool(texts)
            and all(t is not None for t in texts)
            and all(self._CLUSTER_LEVEL.match(t) for t in texts if t)
        )

    def _has_asyncio_mode(self) -> bool:
        """Check if project has asyncio_mode = 'auto' in pytest config."""
        project_root = Path("/home/mohamed/workspace/orchestrata-ultimate-architecture")
        for config_file in ("pyproject.toml", "pytest.ini", "setup.cfg"):
            p = project_root / config_file
            if p.exists():
                text = p.read_text()
                if "asyncio_mode" in text:
                    return True
        return False

    # ------------------------------------------------------------------
    # T21 — env key drift/fabrication detection
    # ------------------------------------------------------------------

    def _build_env_registry(
        self, exclude_file: Path | None = None
    ) -> dict[str, tuple[str, str, int]]:
        """Build the registry of declared env key constants.

        Walks all .py files under registry_roots (default: src/orchestrata_ultimate + scripts),
        collecting module-level assignments where target ends with '_ENV' and value is a string Constant.

        Excludes the file being reviewed (if provided) to avoid self-tripping when the file
        being checked also declares constants.

        Returns: dict mapping constant_value -> (constant_name, declaring_module, lineno)
        Cache built once per checker instance per review run.
        """
        if self._env_registry is not None:
            return self._env_registry

        roots = (
            self._registry_roots
            if self._registry_roots
            else [
                Path(__file__).resolve().parents[1] / "src" / "orchestrata_ultimate",
                Path(__file__).resolve().parents[1] / "scripts",
            ]
        )

        registry: dict[str, tuple[str, str, int]] = {}
        module_paths: list[Path] = []

        for root in roots:
            if not root.exists():
                continue
            module_paths.extend(root.rglob("*.py"))

        for path in module_paths:
            # Skip the file being reviewed
            if exclude_file and path == exclude_file:
                continue

            try:
                source = path.read_text(encoding="utf-8")
                tree = ast.parse(source)
            except SyntaxError:
                continue

            # Only check module-level assignments (in the body of the Module node)
            for node in tree.body:
                if not isinstance(node, ast.Assign):
                    continue
                for target in node.targets:
                    if not isinstance(target, ast.Name):
                        continue
                    name = target.id
                    if not name.endswith("_ENV"):
                        continue
                    if not isinstance(node.value, ast.Constant) or not isinstance(
                        node.value.value, str
                    ):
                        continue
                    value = node.value.value
                    registry[value] = (name, str(path), node.lineno)

        self._env_registry = registry
        return registry

    def _build_general_registry(
        self, exclude_file: Path | None = None
    ) -> dict[str, tuple[str, str, int]]:
        """Build the GENERAL constants registry (T17-GEN): every module-level
        UPPER_CASE string-constant assignment EXCEPT the ``*_ENV`` names (which
        the landed T21 tier owns). The general tier is exact-match WARNING only —
        the measured >=2-token drift over all UPPER_CASE values is ~100% FP, so
        no drift ERROR is emitted here (T17 Stage A ruling)."""
        if self._general_registry is not None:
            return self._general_registry

        roots = (
            self._registry_roots
            if self._registry_roots
            else [
                Path(__file__).resolve().parents[1] / "src" / "orchestrata_ultimate",
                Path(__file__).resolve().parents[1] / "scripts",
            ]
        )

        registry: dict[str, tuple[str, str, int]] = {}
        for root in roots:
            if not root.exists():
                continue
            for path in root.rglob("*.py"):
                if exclude_file and path == exclude_file:
                    continue
                try:
                    tree = ast.parse(path.read_text(encoding="utf-8"))
                except SyntaxError:
                    continue
                for node in tree.body:
                    if not isinstance(node, ast.Assign):
                        continue
                    for target in node.targets:
                        if not isinstance(target, ast.Name):
                            continue
                        name = target.id
                        if name.endswith("_ENV") or not name.isupper():
                            continue
                        if not isinstance(node.value, ast.Constant) or not isinstance(
                            node.value.value, str
                        ):
                            continue
                        registry[node.value.value] = (name, str(path), node.lineno)

        self._general_registry = registry
        return registry

    def _token_similarity(self, literal: str, registry_value: str) -> tuple[bool, int]:
        """Check if literal shares >=2 consecutive tokens with registry_value.

        Tokenize by splitting on '_', drop empties, compare token runs.
        Returns (similar, max_consecutive_tokens) where similar means >=2 consecutive shared tokens.

        Case-insensitive comparison.
        """
        if literal == registry_value:
            return (False, 0)  # Exact match is not drift

        def tokenize(s: str) -> list[str]:
            return [t for t in s.lower().split("_") if t]

        literal_tokens = tokenize(literal)
        registry_tokens = tokenize(registry_value)

        if not literal_tokens or not registry_tokens:
            return (False, 0)

        # Find max consecutive matching run
        max_consecutive = 0
        for i, lt in enumerate(literal_tokens):
            for j, rt in enumerate(registry_tokens):
                consecutive = 0
                li, ri = i, j
                while (
                    li < len(literal_tokens)
                    and ri < len(registry_tokens)
                    and literal_tokens[li] == registry_tokens[ri]
                ):
                    consecutive += 1
                    li += 1
                    ri += 1
                max_consecutive = max(max_consecutive, consecutive)

        return (max_consecutive >= 2, max_consecutive)

    @staticmethod
    def _is_env_key_shaped(literal: str) -> bool:
        """True when literal is env-key-shaped: contains '_' and all cased chars are uppercase.

        Env keys follow UPPER_SNAKE_CASE by universal convention. This shape gate filters
        out lowercase/mixed-case literals (JSON field names, dict payload keys) that share
        tokens with env constants but are NOT env keys.

        Example: "head_commit" → False (lowercase), "ORCH_HEAD_COMMIT" → True (uppercase)
        """
        if "_" not in literal:
            return False
        # Every cased character must be uppercase
        for c in literal:
            if c.isalpha() and not c.isupper():
                return False
        return True

    def _check_env_key_drift(self, path: Path, tree: ast.AST, lines: list[str]) -> None:
        """T21 — detect env key drift/fabrication by comparing literals to declared constants.

        Two levels:
        - T21 ERROR: literal shares >=2 consecutive tokens with a registry value
          but is != it (e.g. "ORCH_TEST_DEADLINE" vs "ORCHESTRATA_TEST_DEADLINE")
        - T21 WARNING: literal exactly matches a registry value used outside its
          declaring module → should import the constant instead

        SHAPE GATE: a literal enters T21 comparison ONLY if it is env-key-shaped
        (contains '_' AND every cased character is uppercase). This filters out
        lowercase/mixed-case literals like JSON field names ("head_commit") that
        share tokens with env constants but are not env key literals.

        Non-goals (do NOT flag): infra keys with NO declared constant; comments/
        docstrings; f-strings' dynamic parts. Self-exemption via path exemption.

        The rule applies to ANY .py file passed on the CLI (T1-T20 keep their
        current test-named scoping unchanged).
        """
        # Build registry excluding the file being reviewed
        registry = self._build_env_registry(exclude_file=path)

        # Skip the rule's own test file to avoid false positives from fixture code
        # (concatenated fixture literals and the self-exemption pattern).
        # D6: self-exemption via documented path exemption - the test file
        # contains fixture string literals that would otherwise trigger T21.
        if "tests/scripts/test_test_review_tool.py" in str(path):
            return

        # D8: self-exemption for the review tool itself — it uses env key literals
        # in string checks (not os.environ accesses) and importing the constants
        # would be circular (the tool checks the constants it uses).
        if "scripts/test-review.py" in str(path):
            return

        # Track which module declared each constant for exact-match warnings
        # Maps literal value -> (constant_name, declaring_module)
        constant_to_module: dict[str, tuple[str, str]] = {}
        for value, (name, module, _) in registry.items():
            constant_to_module[value] = (name, module)

        # T17-GEN general tier: exact-match WARNING only (no drift — the >=2-token
        # drift over all UPPER_CASE values is ~100% FP). Separate map so the
        # landed *_ENV tier semantics stay byte-stable.
        general_registry = self._build_general_registry(exclude_file=path)
        general_constant_to_module: dict[str, tuple[str, str]] = {}
        for value, (name, module, _) in general_registry.items():
            general_constant_to_module[value] = (name, module)

        # Collect docstring node IDs to exclude
        docstring_ids = set()
        for node in ast.walk(tree):
            if isinstance(
                node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Module)
            ):
                docstring = self._docstring(node)
                if docstring is not None:
                    docstring_ids.add(id(docstring))

        # Collect ALL string literals in code position (excluding docstrings)
        # This matches the brief's "ast.walk over ast.Constant where value is str"
        string_literals: list[tuple[str, int]] = []
        for node in ast.walk(tree):
            if isinstance(node, ast.Constant) and isinstance(node.value, str):
                # Skip empty strings and docstrings
                if node.value and id(node) not in docstring_ids:
                    string_literals.append((node.value, node.lineno))

        # Remove duplicates while preserving line order
        seen: set[str] = set()
        unique_literals: list[tuple[str, int]] = []
        for lit, lineno in string_literals:
            if lit not in seen:
                seen.add(lit)
                unique_literals.append((lit, lineno))

        # Check each literal
        for literal, lineno in unique_literals:
            # SHAPE GATE: skip literals that are not env-key-shaped
            if not self._is_env_key_shaped(literal):
                continue

            # Skip literals ending in '_ENV' (constant NAME-shaped strings, not key values)
            if literal.endswith("_ENV"):
                continue

            # Skip infra keys that have no declared constant (RUL-003 style inline literals)
            # A literal is "infra" if it doesn't match any registry value and doesn't
            # share >=2 tokens with any registry value
            # D11: exact match PRECEDES drift. If literal exactly equals ANY registry value,
            # it's an exact-match issue (WARNING), NOT a drift issue (ERROR).
            has_match = False
            exact_match_value = None  # The registry value that matches exactly, if any
            best_match = (
                None  # (reg_value, max_consecutive_tokens) for similar-but-not-equal
            )
            for reg_value in registry:
                if literal == reg_value:
                    has_match = True
                    exact_match_value = reg_value  # Record exact match
                    # Keep checking for drift against other entries? No - exact match wins.
                    # Per D11: exact match PRECEDES drift. Stop checking similarity.
                    break
                similar, max_consecutive = self._token_similarity(literal, reg_value)
                if similar:
                    has_match = True
                    if best_match is None or max_consecutive > best_match[1]:
                        best_match = (reg_value, max_consecutive)

            # Create a simple node-like object for reporting line numbers
            class LineNode:
                def __init__(self, lineno):
                    self.lineno = lineno

            line_node = LineNode(lineno)

            # T17-GEN general tier: exact-match WARNING (retyped constant value),
            # no drift — the >=2-token drift over all UPPER_CASE values is ~100% FP.
            # Runs for every shape-gated literal, independent of the *_ENV match
            # (a general constant's value is not an env key, so it has no *_ENV
            # entry to match against).
            if literal in general_constant_to_module:
                gen_name, gen_module = general_constant_to_module[literal]
                if gen_module != str(path):
                    self._add(
                        path,
                        line_node,
                        "T21",
                        f"T21 retyped constant '{literal}' has a declared "
                        f"constant NAME='{gen_name}' at {gen_module} — import it "
                        f"instead of retyping",
                        Severity.WARNING,
                    )

            if not has_match:
                # No registry match at all - this is an infra key, skip it
                continue

            # D11: exact match PRECEDES drift. If literal exactly equals ANY registry value,
            # it's an exact-match issue (WARNING), NOT a drift issue (ERROR).
            # Check exact match first.
            has_drift_issue = False
            if exact_match_value is not None:
                # Exact match found - issue WARNING (unless self-declaration)
                # (drift check skipped - exact match wins)
                pass
            elif best_match is not None:
                # Drift issue (similar but not equal)
                reg_value, _ = best_match
                const_name, const_module, _ = registry[reg_value]
                has_drift_issue = True
                self._add(
                    path,
                    line_node,
                    "T21",
                    f"T21 possible fabricated/drifted env key '{literal}' — "
                    f"declared constant NAME='{const_name}' at {const_module}:{_}",
                    Severity.ERROR,
                )
            else:
                # Fallback: iterate through registry (shouldn't happen given has_match=True)
                for reg_value, (const_name, const_module, _) in registry.items():
                    similar, _ = self._token_similarity(literal, reg_value)
                    if similar:
                        self._add(
                            path,
                            line_node,
                            "T21",
                            f"T21 possible fabricated/drifted env key '{literal}' — "
                            f"declared constant NAME='{const_name}' at {const_module}:{_}",
                            Severity.ERROR,
                        )
                        break  # One error per literal is enough

            # Check for exact match outside declaring module
            # Skip if the literal was already flagged as drift (same literal can't be both)
            # OR if the literal is declared in this file (self-declaration)
            if has_drift_issue:
                # Already flagged as drift, skip exact match warning
                pass
            else:
                # Check for self-declaration
                is_self_declaration = False
                for node in tree.body:
                    if isinstance(node, ast.Assign):
                        for target in node.targets:
                            if isinstance(target, ast.Name):
                                if target.id.endswith("_ENV"):
                                    if isinstance(
                                        node.value, ast.Constant
                                    ) and isinstance(node.value.value, str):
                                        if node.value.value == literal:
                                            is_self_declaration = True
                                            break
                        if is_self_declaration:
                            break

                if not is_self_declaration and literal in constant_to_module:
                    const_name, declaring_module = constant_to_module[literal]
                    if declaring_module != str(path):
                        self._add(
                            path,
                            line_node,
                            "T21",
                            f"T21 env key '{literal}' has a declared constant NAME='{const_name}' at {declaring_module} — "
                            f"import it instead of retyping",
                            Severity.WARNING,
                        )

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    def _add(
        self,
        path: Path,
        node: ast.AST,
        rule: str,
        message: str,
        severity: Severity = Severity.ERROR,
    ) -> None:
        self.report.add(str(path), getattr(node, "lineno", 1), rule, message, severity)


# Severity rank, most severe first — used to filter by --min-severity. Severity
# names do NOT sort correctly alphabetically (e.g. "INFO" < "WARNING" would
# wrongly include INFO issues when min-severity=WARNING), so filtering must use
# this explicit rank rather than comparing Severity.value strings.
_SEVERITY_RANK: dict[Severity, int] = {
    Severity.ERROR: 3,
    Severity.WARNING: 2,
    Severity.INFO: 1,
}


def output_text(
    report: TestReport, min_severity: Severity, output_file: str | None = None
) -> None:
    out = sys.stdout if output_file is None else open(output_file, "w")
    min_rank = _SEVERITY_RANK[min_severity]
    filtered = [i for i in report.issues if _SEVERITY_RANK[i.severity] >= min_rank]

    print("=" * 80, file=out)
    print("TEST REVIEW REPORT", file=out)
    print("=" * 80, file=out)
    print(f"\nFiles Analyzed: {report.files_analyzed}", file=out)
    print(f"Total Issues: {len(filtered)}\n", file=out)

    if not filtered:
        print("✅ All test files comply with standards.", file=out)
    else:
        by_severity: dict[Severity, list[TestIssue]] = {s: [] for s in Severity}
        for i in filtered:
            by_severity[i.severity].append(i)

        for sev in (Severity.ERROR, Severity.WARNING, Severity.INFO):
            items = by_severity[sev]
            if items:
                print(f"\n{sev.value} ({len(items)}):", file=out)
                for item in items:
                    print(
                        f"  {item.file}:{item.line} [{item.rule}] {item.message}",
                        file=out,
                    )

    if output_file:
        out.close()


def _apply_worklist_baseline(report: TestReport, baseline_path: str) -> None:
    """Downgrade baseline rows (T21 exact-key WARNINGs named at dispatch time) to
    INFO — named debt, non-blocking. New findings keep their severity. A
    missing/unparsable baseline is a no-op (the baseline is advisory, not a
    correctness gate)."""
    import json

    try:
        data = json.loads(Path(baseline_path).read_text(encoding="utf-8"))
    except OSError, json.JSONDecodeError:
        return
    baseline = {(Path(r["file"]).name, r["line"]) for r in data.get("rows", [])}
    for issue in report.issues:
        if issue.rule != "T21" or issue.severity != Severity.WARNING:
            continue
        if (Path(issue.file).name, issue.line) in baseline:
            issue.severity = Severity.INFO


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Test file reviewer for orchestrata-ultimate"
    )
    parser.add_argument("paths", nargs="+", help="Test files or directories to review")
    parser.add_argument("--output", "-o", help="Output file for report")
    parser.add_argument("--stdout", action="store_true", help="Print report to stdout")
    parser.add_argument(
        "--min-severity", choices=["ERROR", "WARNING", "INFO"], default="WARNING"
    )
    parser.add_argument(
        "--worklist-baseline",
        help="Worklist-baseline JSON (T21-ROUTING): baseline rows downgrade to "
        "INFO (named debt, non-blocking); new findings keep their severity.",
    )
    args = parser.parse_args()

    files = find_test_files(args.paths)
    if not files:
        print("No test files found.", file=sys.stderr)
        return 0

    report = TestReport()
    checker = TestFileChecker(report)
    for f in files:
        checker.check_file(f)

    if args.worklist_baseline:
        _apply_worklist_baseline(report, args.worklist_baseline)

    min_sev = Severity(args.min_severity)
    output_text(report, min_sev, None if args.stdout else args.output)

    if args.stdout and args.output:
        output_text(report, min_sev, args.output)

    error_count = sum(1 for i in report.issues if i.severity == Severity.ERROR)
    return 1 if error_count > 0 else 0


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