Orchestrata Ultimate › Gates › review-staged
Gate: review-staged
| Purpose | Run the code review and the test review on exactly the staged Python files and refuse the commit on a blocking finding — or on a review that produced no sound verdict |
| Where it runs | scripts/hooks/pre-commit (after the secrets gate and the ruff auto-fix); ~/bin/precommit-check runs the same command on a temporary index (see commit-precheck) |
| Trigger | git commit with at least one staged .py file (added, copied or modified) |
| Exact command | PRECOMMIT_WALLS_FILE=<tmp> .venv/bin/python scripts/gate.py review-staged [--repo-root R] [--python P] [--timeout S] |
| Blocks | Exit 1 = blocking findings, exit 2 = invalid (a review did not run, did not finish, or left a report that does not add up). The hook blocks on any non-zero exit |
| Ruling / ADR | RUL-008 (review before every commit; its Enforcement names scripts/hooks/pre-commit and code-review.py --min-severity); ADR-0101 (stage walls measured); ADR-0116 (staged source files enter T21); ADR-0025 (no cuts) |
Why it exists
Measured 2026-10-11, the previous shell version of the review "passed every commit", three
ways (commit 8e0df6ff3 and the header of commit_review.pl):
- the hook ran a bare
python, which resolved to another worktree's venv, and the reviewer crashed on import; code-review.pyprinted "JSON report written to ..." before its JSON,json.loadfailed, and|| echo 0read that as "0 issues";test-review.pylost its--formatflag ine1e1a4f77(2026-05-26) while the hook kept passing it; argparse exited 2 under2>/dev/null, so the test review had not run since.
Each was "the reviewer produced nothing" read as "the reviewer found nothing". The fix
moved the verdict out of bash: the driver
(src/orchestrata_ultimate/gates/commit_review.py) writes what each reviewer reported as
Prolog facts, and src/orchestrata_ultimate/prolog/commit_review.pl derives the verdict,
where "every way a report can fail to be sound is a named review_defect/3".
How it works
Scope (review_required/2, reviewer_tree/2, reviewer_covers/2).
| Reviewer | Tree | Files it covers |
|---|---|---|
code_review (scripts/code-review.py) |
src/ |
every staged .py |
test_review (scripts/test-review.py) |
tests/, end2end/, scripts/ |
test-named files (test_*.py, *_test.py), plus every staged scripts/*.py (T21 routing, ADR-0116) |
A conftest.py under tests/ is not test-named, so no test review is required for it:
test-review prints "No test files found" for it, and requiring the review would block
every conftest commit as invalid(no_report) (a false block measured 2026-10-11).
Policy (blocking_severity/2). Code review blocks at error; test review blocks at
error and warning; info never blocks. Both reviewers run at --min-severity INFO,
so the rule file, not a command-line flag, decides what blocks. When
reports/t21-worklist-baseline.json exists, the test review gets --worklist-baseline,
which downgrades the named-debt exact-key T21 WARNINGs to INFO (run_reviewer).
Soundness (review_defect/3). A review is invalid(Why) when it was required and:
| Defect | Meaning |
|---|---|
not_run |
no exit recorded |
exit(N) |
an exit code that is not a verdict (only 0 and 1 are) |
no_report |
a verdict exit but no report that parses |
not_analyzed |
the report says it analyzed 0 files |
count_mismatch(Stated, Parsed) |
the stated issue total differs from the findings parsed |
unknown_severity(S) |
a severity outside error / warning / info |
exit_disagrees(Exit) |
exit 1 with no ERROR, or exit 0 with an ERROR |
commit_defect/1 adds run-level defects: no staged_count, a staged_count that differs
from the listed files (a truncated fact file), and an unrequested_review (driver and
rules disagree on scope). invalid dominates fail.
A reviewer past its timeout (default 600 s, DEFAULT_REVIEW_TIMEOUT_S) records exit 124,
which is not a verdict exit, so the commit is invalid.
What the code review checks
scripts/code-review.py runs these checkers on each file (its main(), read through the
AST toolkit), plus two repo-wide gates:
| Checker | Checks | Severities seen in the source |
|---|---|---|
RuffChecker |
ruff findings | F and E codes ERROR, W WARNING, I and UP INFO, others WARNING; RUFF_NOT_FOUND when ruff is not on PATH |
MyPyChecker |
mypy on the file, 30 s timeout | MYPY_TIMEOUT is an ERROR |
StyleChecker |
formatting, docstrings, type hints, naming, imperative accumulators, complexity, test isolation, code patterns (limits: line 88, function 40 lines, class 200 lines, 5 args, 15 locals, 4 returns, 12 branches) | per rule |
ChecklistChecker |
class responsibility (method count), method self access, abstract classes, test files |
per rule |
ConnectionStringChecker |
hardcoded IPs, URIs and localhost:<port>, env-var defaults |
HARDCODED_IP, HARDCODED_URI, HARDCODED_LOCALHOST are ERROR |
TraceabilityChecker |
@traces on classes and methods (enums, pydantic models, protocols, dataclasses and similar are exempt) |
WARNING, so it does not block a commit |
check_banned_dependencies |
banned packages in pyproject.toml (from .claude/banned-libraries.txt) |
ERROR |
check_prolog_boundary |
every Python Prolog query targets an existing predicate/arity | ERROR |
What the test review checks (T1–T21)
scripts/test-review.py's TestFileChecker.RULES table (read through the AST toolkit,
2026-10-23). The rule ids run T1–T21; T20 is emitted but not in the table, and T11 is in
the table but no check emits it. T1–T20 apply to test-named files; T21 applies to every
.py passed on the command line. Severity is ERROR unless the column says otherwise.
| Rule | Text in RULES |
Severity |
|---|---|---|
| T1 | No mocking — no unittest.mock, MagicMock, AsyncMock, Mock, patch | ERROR |
| T2 | No stubs or fakes — no FakeX, StubX, DummyX classes | ERROR |
| T3 | No try/except/pass blocks | ERROR |
| T4 | Function-scope fixtures only — module scope solely for annotated IMMUTABLE precomputed results (# T4-immutable-module-scope:), class/session never |
ERROR (a session-scoped connection fixture may opt out with a reasoned # gate-ok: <why> above the decorator) |
| T5 | No os.environ mutations | ERROR |
| T6 | No hardcoded ports | ERROR |
| T7 | No pytest.mark.skip or skipif | ERROR |
| T8 | No monkeypatch usage | ERROR |
| T9 | No global mutable state at module level | ERROR |
| T10 | Use test_id fixture for DB/graph (Memgraph) ops | WARNING ("File WRITES to Redis/Memgraph without namespacing") |
| T11 | Use isolation fixture for file/DB resources | listed only; not emitted |
| T12 | No bare os.environ['X'] reads — use os.environ.get() | WARNING |
| T13 | No relative imports | ERROR |
| T14 | Async tests need pytest.mark.asyncio or asyncio_mode | ERROR |
| T15 | No pytest-xdist (-n/--numprocesses/--dist) — it deadlocks this suite; use the threadpool runner | ERROR |
| T16 | No banned-library imports — see .claude/banned-libraries.txt |
ERROR |
| T17 | A uuid-scoped Memgraph chunk_label must drop its schema in teardown | ERROR |
| T18 | Verify through the store/repo's own session() — a bare .driver.session() reads the cluster default database |
ERROR |
| T19 | DDL a test CREATEs, the test must DROP | ERROR |
| T20 | (emitted, not in RULES) @pytest.mark.anyio is forbidden — asyncio_mode is configured, so pytest-asyncio owns async tests |
ERROR |
| T21 | Env key drift/fabrication — a literal similar to but not equal to a declared *_ENV constant |
ERROR for a drifted key, WARNING for an exact literal retyped outside its declaring module |
Because test review blocks at WARNING, T10, T12 and the T21 exact-literal WARNING all block a commit. T21 also flags pre-existing literals in any test file you touch (improvement log D4).
Rulings behind the rules: T1/T8 RUL-002;
T15 RUL-001 and RUL-005;
T21 was minted after the RUL-038 cap was found stamping fabricated env keys (25f06c3f6,
2026-10-20; routing 0d329660d, 0ffb2d1ec).
Typical failures and how to fix them
| What you see | Cause | Fix |
|---|---|---|
Commit blocked — the review found blocking issues |
exit 1; findings are listed above the line | Fix each finding; never weaken a test or add a waiver to pass |
Commit blocked — a review produced no verdict (exit 2) |
a review_defect, for example a reviewer crash, a timeout, or a count mismatch |
Read the reason; run the reviewer by hand on the file (.venv/bin/python scripts/test-review.py --min-severity INFO --stdout <file>) |
MYPY_TIMEOUT ERROR |
mypy took over 30 s on a cold cache (D3) | Warm the cache first: .venv/bin/python -m mypy --no-error-summary <changed files> |
RUFF_NOT_FOUND |
ruff not on PATH | The hook and run_reviewer put the venv's bin first; run from a provisioned checkout |
no project interpreter at .venv/bin/python |
checkout has no venv (RUL-021) | scripts/worktrees.py provision <path> |
| T21 on a line you did not write | T21 covers pre-existing literals in touched files | Import the declared constant instead of retyping the key |
To meet these before git commit, run ~/bin/precommit-check <repo> <files>.
Measured walls
reports/precommit-walls.ndjson (ADR-0101 rule 2) records the stage on each commit. Of
244 attempts between 2026-10-14 and 2026-10-23 that reached the review stage, the median
review_staged_s was 0.18 s and the maximum 123.3 s; 14 attempts were blocked. One test
file took 3.4 s to 18.4 s to review in the three newest rows.
Wiring
- Hook:
scripts/hooks/pre-commitlines 172–198 (read 2026-10-23). - Driver:
src/orchestrata_ultimate/gates/commit_review.py(CommitReview,run_reviewer,render_facts,evaluate); rules:src/orchestrata_ultimate/prolog/commit_review.pl. - CLI:
scripts/gate.py review-staged(cmd_review_staged). - Tests:
tests/scripts/test_hooks_pre_commit.py.
Related
- Gates — master index
- Gate: commit-precheck
- Gate: secrets, Gate: ai-attribution
- Gate: prolog-gate (the same
gate.py,runsubcommand) - Gate: ruff, Gate: mypy, Gate: test-purity
- Workflow — the commit gate
- RUL-008