scripts.code-review
Reference › scripts › code-review
The unified code review tool for orchestrata-ultimate, a single script of about 2,000 lines that runs several checkers over Python files and merges their findings into one report. agent_check calls it as its review step, and comprehensive_review runs it as the first section of its report. The file name has a hyphen, so no module imports it.
Invocation: main takes one or more paths (files or directories) and --output/-o, --format (text, json or markdown), --min-severity (ERROR, WARNING or INFO), --fix to apply fixable ruff issues, --format-code to run the ruff formatter first, --max-files, --diagnostics to also run the test-diagnostic tools (the isolation audit and the graph preflight) and --stdout.
Each checker subclasses BaseChecker, which catches errors per file and adds CodeIssue records to a shared ReviewReport, each tagged with a Severity. RuffChecker and RuffFormatChecker run ruff and map its codes to severities. MyPyChecker runs mypy and parses its output. StyleChecker reads the syntax tree for docstrings, type hints, naming, cyclomatic complexity, test isolation, code patterns and imperative accumulators (a collection initialized empty and filled in a loop). ChecklistChecker checks class responsibility, methods that never use self, and abstract classes.
At a glance
| Source | scripts/code-review.py |
| Lines of code | 2059 |
| Classes | 11 |
| Functions | 10 |
| Imports (internal) | none |
| Imported by | none |
| Tests that import it | none |
Class diagram
Dependency diagram
No internal imports and no importers.
API
Classes
Severity (enum)
Bases: StrEnum · Line 75
Issue severity levels.
| Member | Value |
|---|---|
ERROR |
'ERROR' |
WARNING |
'WARNING' |
INFO |
'INFO' |
CodeIssue (dataclass)
Decorators: dataclass · Line 84
Represents a code review issue.
| Attribute | Type | Default |
|---|---|---|
file |
str |
|
line |
int |
|
column |
int |
0 |
code |
str |
'' |
message |
str |
'' |
severity |
Severity |
Severity.WARNING |
suggestion |
str | None |
None |
category |
str |
'' |
fixable |
bool |
False |
context |
dict[str, Any] |
field(default_factory=dict) |
ReviewReport (dataclass)
Decorators: dataclass · Line 100
Complete code review report.
| Attribute | Type | Default |
|---|---|---|
files_analyzed |
int |
0 |
total_issues |
int |
0 |
issues |
list[CodeIssue] |
field(default_factory=list) |
summary_by_category |
dict[str, int] |
field(default_factory=lambda: defaultdict(int)) |
summary_by_severity |
dict[Severity, int] |
field(default_factory=lambda: defaultdict(int)) |
_seen_issues |
set[tuple[str, int, int, str, str]] |
field(default_factory=set, init=False, repr=False) |
| Method | Signature | Summary |
|---|---|---|
add_issue |
add_issue(issue: CodeIssue) -> None |
Add an issue to the report. |
BaseChecker
Line 129
Base class for all checkers.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(report: ReviewReport) |
|
check_file |
check_file(file_path: Path) -> None |
Check a single file. |
_check_file_impl |
_check_file_impl(file_path: Path) -> None |
Implementation of file checking (to be overridden). |
_report_ruff_not_found |
_report_ruff_not_found(message: str) -> None |
Report RUFF_NOT_FOUND at its pre-Q2b severity (ERROR): a review in which ruff never ran must not be indistinguishable from a clean review — the run's exit code comes from the ERROR count. |
RuffChecker
Bases: BaseChecker · Line 179
Run ruff for formatting and linting.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(report: ReviewReport, fix: bool = False, interpreter: str | None = None) -> None |
|
_check_file_impl |
_check_file_impl(file_path: Path) -> None |
Run ruff on the file. |
_parse_ruff_output |
_parse_ruff_output(output: str) -> None |
Parse ruff output lines. |
_map_ruff_severity |
_map_ruff_severity(code: str) -> Severity |
Map ruff error codes to severity levels. |
_is_ruff_fixable |
_is_ruff_fixable(code: str) -> bool |
Return True if ruff can auto-fix this code. |
RuffFormatChecker
Bases: BaseChecker · Line 316
Run ruff format on files.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(report: ReviewReport, interpreter: str | None = None) -> None |
|
_check_file_impl |
_check_file_impl(file_path: Path) -> None |
Format the file with ruff. |
MyPyChecker
Bases: BaseChecker · Line 381
Run mypy for type checking.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(report: ReviewReport, mypy_cmd: list[str] | None = None) -> None |
|
_check_file_impl |
_check_file_impl(file_path: Path) -> None |
Run mypy on the file. |
_parse_mypy_output |
_parse_mypy_output(output: str) -> None |
Parse mypy output lines. |
StyleChecker
Bases: BaseChecker · Line 470
AST-based style checks (Google Python Style Guide).
| Method | Signature | Summary |
|---|---|---|
_check_file_impl |
_check_file_impl(file_path: Path) -> None |
Run all style checks. |
_check_formatting |
_check_formatting() -> None |
Check tab indentation. |
_check_docstrings |
_check_docstrings(tree: ast.Module) -> None |
Check docstring presence and format. |
_check_type_hints |
_check_type_hints(tree: ast.Module) -> None |
Check type hints on public functions. |
_check_naming_conventions |
_check_naming_conventions(tree: ast.Module) -> None |
Check naming conventions. |
_empty_collection staticmethod |
_empty_collection(node: ast.AST) -> bool |
ATOM empty_collection(Expr) — an empty list/set/dict literal or call. |
_accumulator_inits classmethod |
_accumulator_inits(scope: ast.AST) -> dict[str, int] |
FACT accumulator_init(Var, Line) — name bound to an empty collection. |
_loop_mutations classmethod |
_loop_mutations(loop: ast.AST) -> set[str] |
FACT loop_mutates(Var, Method) — names grown inside the loop body. |
_check_imperative_accumulator |
_check_imperative_accumulator(tree: ast.Module) -> None |
IMPERATIVE_ACCUMULATOR — a loop that only grows a collection. |
_check_complexity |
_check_complexity(tree: ast.Module) -> None |
Check code complexity metrics. |
_check_test_isolation |
_check_test_isolation(file_path: Path) -> None |
Flag global graph/Redis teardowns in tests (standards rule 34). |
_check_code_patterns |
_check_code_patterns(tree: ast.Module) -> None |
Check for discouraged code patterns. |
ChecklistChecker
Bases: BaseChecker · Line 930
Checklist-based code review checks.
| Method | Signature | Summary |
|---|---|---|
_check_file_impl |
_check_file_impl(file_path: Path) -> None |
Run checklist-based checks. |
_check_class_responsibility |
_check_class_responsibility(tree: ast.Module) -> None |
Check class single responsibility (method count). |
_check_method_self_access |
_check_method_self_access(tree: ast.Module) -> None |
Check that methods access self or cls. |
_check_abstract_classes |
_check_abstract_classes(tree: ast.Module) -> None |
Check abstract class usage. |
_check_test_files |
_check_test_files(tree: ast.Module, file_path: Path) -> None |
Special checks for test files. |
TraceabilityChecker
Bases: BaseChecker · Line 1282
Check that all classes and methods have @traces decorators.
| Method | Signature | Summary |
|---|---|---|
_check_file_impl |
_check_file_impl(file_path: Path) -> None |
Check traceability of a single file. |
_check_class |
_check_class(node: ast.ClassDef, file_path: Path) -> None |
Check a single class and its methods. |
_has_traces staticmethod |
_has_traces(node: ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef) -> bool |
|
_is_enum |
_is_enum(node: ast.ClassDef) -> bool |
|
_is_pydantic |
_is_pydantic(node: ast.ClassDef) -> bool |
|
_is_exception staticmethod |
_is_exception(node: ast.ClassDef) -> bool |
|
_is_protocol staticmethod |
_is_protocol(node: ast.ClassDef) -> bool |
|
_is_dataclass staticmethod |
_is_dataclass(node: ast.ClassDef) -> bool |
|
_is_typeddict staticmethod |
_is_typeddict(node: ast.ClassDef) -> bool |
|
_is_abc staticmethod |
_is_abc(node: ast.ClassDef) -> bool |
|
_has_property staticmethod |
_has_property(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool |
|
_has_pydantic_decorator |
_has_pydantic_decorator(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool |
ConnectionStringChecker
Bases: BaseChecker · Line 1532
Check for hardcoded connection strings and env-var defaults.
| Method | Signature | Summary |
|---|---|---|
_check_file_impl |
_check_file_impl(file_path: Path) -> None |
Scan file for hardcoded connection values. |
_is_env_get_call staticmethod |
_is_env_get_call(func: ast.expr) -> bool |
True for the call target of os.environ.get(...), bare environ.get(...), os.getenv(...), or bare getenv(...). |
_collect_env_get_defaults |
_collect_env_get_defaults(tree: ast.AST) -> set[int] |
Return line numbers of string constants used as defaults in os.environ.get(). |
_check_string |
_check_string(value: str, lineno: int) -> None |
Flag strings that look like connection endpoints. |
Functions
| Function | Signature | Summary |
|---|---|---|
find_python_files |
find_python_files(paths: list[str]) -> list[Path] |
Find all Python files in given paths. |
filter_issues_by_severity |
filter_issues_by_severity(issues: list[CodeIssue], min_severity: Severity) -> list[CodeIssue] |
Filter issues by minimum severity. |
output_text |
output_text(report: ReviewReport, filtered_issues: list[CodeIssue], output_file: str | None = None) -> None |
Output report in text format. |
output_json |
output_json(report: ReviewReport, filtered_issues: list[CodeIssue], output_file: str | None = None) -> None |
Output report in JSON format. |
output_markdown |
output_markdown(report: ReviewReport, filtered_issues: list[CodeIssue], output_file: str | None = None) -> None |
Output report in markdown format. |
run_diagnostics |
run_diagnostics(report: ReviewReport) -> None |
Run test diagnostic tools and add findings to the report. |
load_banned_libraries |
load_banned_libraries(repo_root: Path) -> list[str] |
Read .claude/banned-libraries.txt — distribution names that must not be declared as dependencies. |
check_banned_dependencies |
check_banned_dependencies(report: ReviewReport, repo_root: Path) -> None |
SHOWSTOPPER gate: fail if pyproject.toml declares a banned dependency. |
check_prolog_boundary |
check_prolog_boundary(report: ReviewReport, repo_root: Path) -> None |
BOUNDARY gate: fail if any Python Prolog query site targets a predicate/arity that does not exist in the consulted rule base (reports/proposal-wire-prolog-governance.md §7 Phase 1). |
main |
main() -> int |
Main entry point. |
Source
Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/scripts/code-review.py
#!/usr/bin/env python3
"""
Unified Code Review Tool for orchestrata-ultimate.
Combines:
- Ruff formatting and linting (replaces black/flake8)
- MyPy type checking
- AST-based style checks (Google Python Style Guide)
- Checklist-based code review checks
Usage:
python code-review.py src/ # Review directory
python code-review.py --format json --output report.json src/ # JSON output
python code-review.py --min-severity WARNING src/ # Only warnings and errors
"""
import argparse
import ast
import json
import os
import re
import subprocess
import sys
from collections import defaultdict
from dataclasses import asdict, dataclass, field
from datetime import datetime
from enum import StrEnum
from pathlib import Path
from typing import Any
# A tool that DID NOT RUN is not a clean result.
#
# RUFF_NOT_FOUND, RUFF_TIMEOUT, RUFF_FORMAT_ERROR, RUFF_FORMAT_TIMEOUT,
# MYPY_NOT_FOUND and MYPY_TIMEOUT all report that an external checker never
# produced a verdict. They were WARNING severity, and `main()` returns
# `1 if error_count > 0 else 0` — so a run where linting or type checking never
# happened exited 0 and was indistinguishable from a run where it happened and
# found nothing. That is the false-green shape this project has already been
# bitten by twice (a kernel that silently never started; tests passing against
# leaked host services).
#
# Promoted to ERROR on 2026-08-06. Both tools are declared dev dependencies
# (pyproject.toml `ruff>=0.1.0`, `mypy>=1.8.0`) and present in the venv, so
# "not installed" means a broken environment and "timed out" means an
# unfinished check. Neither is a clean review, and neither may report success.
#
# Import conventions are checked from the `ast` tree this file already parses.
# There is no second parser here any more: a tree-sitter copy of the
# relative-import check lived at this call site until 2026-08-06 and was
# deleted, because
# 1. the rule is already owned by the AST toolkit
# (`serialize_codebase_ast.py standards`, rule `no-relative-imports`) —
# this module was the THIRD implementation of one rule;
# 2. it re-parsed a file that line ~412 had just parsed, only to answer the
# same question;
# 3. it was silently dead twice (a NameError on a never-defined `_py_lang()`
# swallowed by a broad except, then a wrong assumption about the
# grammar's node shape) — a check nothing depends on;
# 4. the tree_sitter C extension does not declare free-threaded support, so
# importing it re-enables the GIL for the whole process, poisoning
# janus-transport (SPEC-023) in anything that loads this script.
# A GIL hazard carried for a triplicated check is not a trade worth keeping.
# Children's output is machine-parsed (regex/substring greps): force color
# OFF for every subprocess this tool spawns. An ambient FORCE_COLOR (e.g.
# exported by the launching terminal/agent harness) made mypy/ruff/pytest
# emit ANSI into captured pipes and silently broke the parsers (gate
# 2026-08-05: mypy "0 issues" on a dirty tree, ruff int(line) crash,
# FAILED-line greps missing their lines).
os.environ["NO_COLOR"] = "1" # gate-ok: process-bootstrap env routing
os.environ.pop("FORCE_COLOR", None) # gate-ok: process-bootstrap env routing
class Severity(StrEnum):
"""Issue severity levels."""
ERROR = "ERROR"
WARNING = "WARNING"
INFO = "INFO"
@dataclass
class CodeIssue:
"""Represents a code review issue."""
file: str
line: int
column: int = 0
code: str = ""
message: str = ""
severity: Severity = Severity.WARNING
suggestion: str | None = None
category: str = ""
fixable: bool = False
context: dict[str, Any] = field(default_factory=dict)
@dataclass
class ReviewReport:
"""Complete code review report."""
files_analyzed: int = 0
total_issues: int = 0
issues: list[CodeIssue] = field(default_factory=list)
summary_by_category: dict[str, int] = field(
default_factory=lambda: defaultdict(int)
)
summary_by_severity: dict[Severity, int] = field(
default_factory=lambda: defaultdict(int)
)
_seen_issues: set[tuple[str, int, int, str, str]] = field(
default_factory=set, init=False, repr=False
)
def add_issue(self, issue: CodeIssue) -> None:
"""Add an issue to the report."""
# Create a unique key for deduplication
key = (issue.file, issue.line, issue.column, issue.code, issue.message)
if key in self._seen_issues:
return
self._seen_issues.add(key)
self.issues.append(issue)
self.total_issues += 1
self.summary_by_category[issue.category] += 1
self.summary_by_severity[issue.severity] += 1
class BaseChecker:
"""Base class for all checkers."""
def __init__(self, report: ReviewReport):
self.report = report
self.current_file: str = ""
self.source_lines: list[str] = []
self.source: str = ""
def check_file(self, file_path: Path) -> None:
"""Check a single file."""
self.current_file = str(file_path)
try:
self.source = file_path.read_text(encoding="utf-8")
self.source_lines = self.source.splitlines()
except Exception as e:
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=1,
code="FILE_READ_ERROR",
message=f"Cannot read file: {e}",
severity=Severity.ERROR,
category="system",
)
)
return
self._check_file_impl(file_path)
def _check_file_impl(self, file_path: Path) -> None:
"""Implementation of file checking (to be overridden)."""
pass
def _report_ruff_not_found(self, message: str) -> None:
"""Report RUFF_NOT_FOUND at its pre-Q2b severity (ERROR): a review
in which ruff never ran must not be indistinguishable from a clean
review — the run's exit code comes from the ERROR count."""
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=1,
code="RUFF_NOT_FOUND",
message=message,
severity=Severity.ERROR,
category="ruff",
)
)
class RuffChecker(BaseChecker):
"""Run ruff for formatting and linting."""
def __init__(
self,
report: ReviewReport,
fix: bool = False,
interpreter: str | None = None,
) -> None:
super().__init__(report)
self.fix = fix
# ADR-0089: ruff runs as ``<interpreter> -m ruff``, never as a bare
# name resolved through whatever PATH the session carries. Defaults
# to the running interpreter, matching lint.LintEvaluator.
self.interpreter = interpreter if interpreter is not None else sys.executable
def _check_file_impl(self, file_path: Path) -> None:
"""Run ruff on the file."""
try:
cmd = [
self.interpreter,
"-m",
"ruff",
"check",
"--exit-zero",
"--output-format=concise",
]
if self.fix:
cmd.append("--fix")
cmd.append(str(file_path))
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30,
)
if result.returncode != 0 and "No module named ruff" in result.stderr:
# BEFORE the output-parsing branch: a missing ruff MODULE is
# an infrastructure verdict about the interpreter, not lint
# findings — mirror lint.LintEvaluator (ADR-0089).
self._report_ruff_not_found(
f"ruff is not installed for {self.interpreter} "
"(python -m ruff: No module named ruff)"
)
elif result.stdout:
self._parse_ruff_output(result.stdout)
elif result.returncode != 0:
# Non-zero exit with no parseable output (e.g. the child was
# killed mid-run) is never silence — mirror lint.py, which
# reports the exit code alone.
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=1,
code="RUFF_CHECK_ERROR",
message=f"ruff check failed (exit code {result.returncode})",
severity=Severity.ERROR,
category="ruff",
)
)
except subprocess.TimeoutExpired:
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=1,
code="RUFF_TIMEOUT",
message="Ruff check timed out after 30 seconds",
severity=Severity.ERROR,
category="ruff",
)
)
except FileNotFoundError:
self._report_ruff_not_found(
f"Python interpreter not found: {self.interpreter}"
)
def _parse_ruff_output(self, output: str) -> None:
"""Parse ruff output lines."""
for line in output.strip().split("\n"):
if not line or ":" not in line:
continue
# Format: path:line:col: code message
parts = line.split(":", 3)
if len(parts) < 4:
continue
file_path, line_str, col_str, message = parts
# Map ruff codes to severity
code = message.strip().split()[0] if message.strip() else "RUFF"
severity = self._map_ruff_severity(code)
fixable = self._is_ruff_fixable(code)
self.report.add_issue(
CodeIssue(
file=file_path,
line=int(line_str),
column=int(col_str),
code=code,
message=message.strip(),
severity=severity,
category="ruff",
fixable=fixable,
)
)
def _map_ruff_severity(self, code: str) -> Severity:
"""Map ruff error codes to severity levels."""
# First character indicates category
if code.startswith("F"): # PyFlakes
return Severity.ERROR
elif code.startswith("E"): # Pycodestyle errors
return Severity.ERROR
elif code.startswith("W"): # Pycodestyle warnings
return Severity.WARNING
elif code.startswith("I"): # Isort
return Severity.INFO
elif code.startswith("UP"): # Pyupgrade
return Severity.INFO
else:
return Severity.WARNING
def _is_ruff_fixable(self, code: str) -> bool:
"""Return True if ruff can auto-fix this code."""
# Codes known to be auto-fixable by ruff
fixable_prefixes = ("I", "W", "UP")
fixable_codes = {"F401", "F811", "E501"}
if code.startswith(fixable_prefixes):
return True
if code in fixable_codes:
return True
return False
class RuffFormatChecker(BaseChecker):
"""Run ruff format on files."""
def __init__(self, report: ReviewReport, interpreter: str | None = None) -> None:
super().__init__(report)
# ADR-0089: ruff runs as ``<interpreter> -m ruff`` — see RuffChecker.
self.interpreter = interpreter if interpreter is not None else sys.executable
def _check_file_impl(self, file_path: Path) -> None:
"""Format the file with ruff."""
try:
result = subprocess.run(
[self.interpreter, "-m", "ruff", "format", str(file_path)],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode != 0 and "No module named ruff" in result.stderr:
# Missing ruff MODULE: infrastructure verdict about the
# interpreter, reported BEFORE the generic format-error
# branch — mirror lint.LintEvaluator (ADR-0089).
self._report_ruff_not_found(
f"ruff is not installed for {self.interpreter} "
"(python -m ruff: No module named ruff)"
)
elif result.returncode != 0 and result.stderr:
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=1,
code="RUFF_FORMAT_ERROR",
message=f"Ruff format failed: {result.stderr.strip()}",
severity=Severity.ERROR,
category="ruff",
)
)
elif result.returncode != 0:
# Non-zero exit with no output is never silence.
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=1,
code="RUFF_FORMAT_ERROR",
message=f"Ruff format failed (exit code {result.returncode})",
severity=Severity.ERROR,
category="ruff",
)
)
except subprocess.TimeoutExpired:
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=1,
code="RUFF_FORMAT_TIMEOUT",
message="Ruff format timed out after 30 seconds",
severity=Severity.ERROR,
category="ruff",
)
)
except FileNotFoundError:
self._report_ruff_not_found(
f"Python interpreter not found: {self.interpreter}"
)
class MyPyChecker(BaseChecker):
"""Run mypy for type checking."""
def __init__(self, report: ReviewReport, mypy_cmd: list[str] | None = None) -> None:
super().__init__(report)
# `python -m mypy` (not the bare "mypy" binary) by default: the
# caller's PATH is not guaranteed to include the venv's bin/, but
# sys.executable always resolves to this process's own interpreter,
# which is the venv Python that has mypy installed. ``mypy_cmd`` is
# a real-subprocess injection seam (not mocking) so tests can point
# at a genuinely nonexistent binary to exercise the MYPY_NOT_FOUND
# path without depending on PATH.
self._mypy_cmd = (
mypy_cmd if mypy_cmd is not None else [sys.executable, "-m", "mypy"]
)
def _check_file_impl(self, file_path: Path) -> None:
"""Run mypy on the file."""
try:
result = subprocess.run(
[*self._mypy_cmd, "--no-error-summary", str(file_path)],
capture_output=True,
text=True,
timeout=30,
)
if result.stdout:
self._parse_mypy_output(result.stdout)
except subprocess.TimeoutExpired:
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=1,
code="MYPY_TIMEOUT",
message="MyPy type check timed out after 30 seconds",
severity=Severity.ERROR,
category="mypy",
)
)
except FileNotFoundError:
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=1,
code="MYPY_NOT_FOUND",
message="MyPy not installed. Install with: uv add --dev mypy",
severity=Severity.ERROR,
category="mypy",
)
)
def _parse_mypy_output(self, output: str) -> None:
"""Parse mypy output lines."""
for line in output.strip().split("\n"):
if not line or "error:" not in line:
continue
# Format: path:line: error: message
if ":" not in line:
continue
# Extract file path, line number, and message
parts = line.split(":", 3)
if len(parts) < 4:
continue
file_path, line_str, _, message = parts
# mypy sometimes emits errors without a line number
# (e.g. "path: error: ..." for cross-file diagnostics).
# Skip those — they are not tied to the file under review.
try:
line_num = int(line_str)
except ValueError:
continue
self.report.add_issue(
CodeIssue(
file=file_path,
line=line_num,
code="MYPY_ERROR",
message=message.strip(),
severity=Severity.ERROR,
category="mypy",
)
)
class StyleChecker(BaseChecker):
"""AST-based style checks (Google Python Style Guide)."""
# Google Style constants
MAX_LINE_LENGTH = 88 # Black/ruff default
MAX_FUNCTION_LENGTH = 40
MAX_CLASS_LENGTH = 200
MAX_ARGS = 5
MAX_LOCALS = 15
MAX_RETURNS = 4
MAX_BRANCHES = 12
def _check_file_impl(self, file_path: Path) -> None:
"""Run all style checks."""
# Basic checks that don't require parsing
self._check_formatting()
self._check_test_isolation(file_path)
# AST-based checks
try:
tree = ast.parse(self.source)
self._check_docstrings(tree)
self._check_type_hints(tree)
self._check_naming_conventions(tree)
self._check_complexity(tree)
self._check_imperative_accumulator(tree)
self._check_code_patterns(tree)
except SyntaxError as e:
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=e.lineno or 1,
column=e.offset or 0,
code="SYNTAX_ERROR",
message=f"Syntax error: {e.msg}",
severity=Severity.ERROR,
category="syntax",
)
)
def _check_formatting(self) -> None:
"""Check tab indentation.
TRAILING_WHITESPACE and MISSING_FINAL_NEWLINE were removed on
2026-08-06: ruff already runs W291 and W292 under the enabled "W"
selector, and black REWRITES both (measured — trailing whitespace
removed, final newline added, in one pass). Reporting what an enabled
linter reports and a formatter silently fixes is duplication, not
enforcement.
TAB_INDENTATION stays: it is ERROR severity here, so unlike the other
two it can actually fail the gate.
"""
for i, line in enumerate(self.source_lines, 1):
if "\t" in line:
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=i,
column=line.index("\t"),
code="TAB_INDENTATION",
message="Use spaces for indentation, not tabs",
severity=Severity.ERROR,
category="style",
fixable=True,
)
)
def _check_docstrings(self, tree: ast.Module) -> None:
"""Check docstring presence and format."""
# Check module docstring
module_doc = ast.get_docstring(tree)
if not module_doc:
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=1,
code="MISSING_MODULE_DOCSTRING",
message="Module is missing a docstring",
severity=Severity.INFO,
suggestion='Add a module docstring at the top: """Module description."""',
category="documentation",
)
)
# Check classes and functions
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
class_doc = ast.get_docstring(node)
if not class_doc and not node.name.startswith("_"):
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=node.lineno,
code="MISSING_CLASS_DOCSTRING",
message=f"Class '{node.name}' is missing a docstring",
severity=Severity.INFO,
category="documentation",
)
)
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
func_doc = ast.get_docstring(node)
if (
not func_doc
and not node.name.startswith("_")
and node.name not in ("__init__", "__call__")
):
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=node.lineno,
code="MISSING_FUNCTION_DOCSTRING",
message=f"Function '{node.name}' is missing a docstring",
severity=Severity.INFO,
category="documentation",
)
)
def _check_type_hints(self, tree: ast.Module) -> None:
"""Check type hints on public functions."""
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
# Skip private methods
if node.name.startswith("_") and not node.name.startswith("__"):
continue
# Check return type
if node.returns is None and node.name != "__init__":
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=node.lineno,
code="MISSING_RETURN_TYPE",
message=f"Function '{node.name}' missing return type annotation",
severity=Severity.WARNING,
suggestion="Add '-> ReturnType' annotation",
category="type-safety",
)
)
# Check parameters
for arg in node.args.args:
if arg.annotation is None and arg.arg not in ("self", "cls"):
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=node.lineno,
code="MISSING_PARAMETER_TYPE",
message=f"Parameter '{arg.arg}' in '{node.name}' missing type annotation",
severity=Severity.WARNING,
category="type-safety",
)
)
def _check_naming_conventions(self, tree: ast.Module) -> None:
"""Check naming conventions."""
for node in ast.walk(tree):
# Class names: PascalCase
if isinstance(node, ast.ClassDef):
if node.name.startswith("_"):
continue
if not re.match(r"^[A-Z][a-zA-Z0-9]*$", node.name):
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=node.lineno,
code="CLASS_NAMING",
message=f"Class name '{node.name}' should use PascalCase",
severity=Severity.ERROR,
category="naming",
)
)
# Function names: snake_case (skip special methods and the
# ast.NodeVisitor dispatch protocol, whose visit_<NodeName>
# casing is mandated by the stdlib)
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if not (
node.name.startswith("__") and node.name.endswith("__")
) and not node.name.startswith("visit_"):
if not re.match(r"^[a-z_][a-z0-9_]*$", node.name):
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=node.lineno,
code="FUNCTION_NAMING",
message=f"Function name '{node.name}' should use snake_case",
severity=Severity.ERROR,
category="naming",
)
)
# Methods that GROW a collection in place. Each is the imperative form of
# a declarative construct: append/extend -> comprehension or list(map(..)),
# add -> set comprehension, update -> dict comprehension or a merge.
_ACCUMULATOR_METHODS = {"append", "add", "extend", "update"}
# Empty-collection constructors — the accumulator's initialiser.
_EMPTY_CTORS = {"list", "set", "dict"}
@staticmethod
def _empty_collection(node: ast.AST) -> bool:
"""ATOM `empty_collection(Expr)` — an empty list/set/dict literal or call."""
if isinstance(node, (ast.List, ast.Dict)) and not getattr(
node, "elts", getattr(node, "keys", [])
):
return True
return (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id in StyleChecker._EMPTY_CTORS
and not node.args
)
@classmethod
def _accumulator_inits(cls, scope: ast.AST) -> dict[str, int]:
"""FACT `accumulator_init(Var, Line)` — name bound to an empty collection."""
assigns = [
n for n in ast.walk(scope) if isinstance(n, (ast.Assign, ast.AnnAssign))
]
targets = (
(t, n)
for n in assigns
for t in (n.targets if isinstance(n, ast.Assign) else [n.target])
)
return {
t.id: n.lineno
for t, n in targets
if isinstance(t, ast.Name)
and n.value is not None
and cls._empty_collection(n.value)
}
@classmethod
def _loop_mutations(cls, loop: ast.AST) -> set[str]:
"""FACT `loop_mutates(Var, Method)` — names grown inside the loop body."""
calls = [n for n in ast.walk(loop) if isinstance(n, ast.Call)]
return {
c.func.value.id
for c in calls
if isinstance(c.func, ast.Attribute)
and c.func.attr in cls._ACCUMULATOR_METHODS
and isinstance(c.func.value, ast.Name)
}
def _check_imperative_accumulator(self, tree: ast.Module) -> None:
"""IMPERATIVE_ACCUMULATOR — a loop that only grows a collection.
Atoms/facts this rule is composed from, so it lifts into the Prolog
gate layer unchanged (plans/prolog-gates-implementation-plan.md):
empty_collection(Expr) -- atom
accumulator_init(Var, Line) -- fact
loop_mutates(Var, Method) -- fact
loop_body_size(Loop, N) -- fact
branchless(Loop) -- fact
imperative_accumulator(Var, Line) :-
accumulator_init(Var, _),
loop_mutates(Var, _),
loop_body_size(Loop, N), N =< 2,
branchless(Loop).
`for x in xs: out.append(f(x))` is `[f(x) for x in xs]`; the
imperative form hides the shape of the result behind a mutation and
an initialiser that must be read together. Deliberately narrow: a
loop with branches or a body doing more than accumulate is left
alone, because there the loop is carrying real control flow.
"""
for scope in ast.walk(tree):
if not isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
inits = self._accumulator_inits(scope)
if not inits:
continue
loops = [
n for n in ast.walk(scope) if isinstance(n, (ast.For, ast.AsyncFor))
]
for loop in loops:
# Every statement must BE an accumulator mutation. Bounding
# body SIZE instead let `out.append(x); sink.write(x)` through
# — a loop with a side effect is doing real work and its
# rewrite is not mechanical.
if not loop.body or not all(
isinstance(stmt, ast.Expr)
and isinstance(stmt.value, ast.Call)
and isinstance(stmt.value.func, ast.Attribute)
and stmt.value.func.attr in self._ACCUMULATOR_METHODS
and isinstance(stmt.value.func.value, ast.Name)
and stmt.value.func.value.id in inits
for stmt in loop.body
):
continue
grown = self._loop_mutations(loop) & inits.keys()
for var in sorted(grown):
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=loop.lineno,
code="IMPERATIVE_ACCUMULATOR",
message=(
f"loop only grows '{var}' (initialised empty at "
f"line {inits[var]}) — express the result "
f"directly"
),
severity=Severity.WARNING,
suggestion=(
"use a comprehension, map/filter, or "
"functools.reduce"
),
category="quality",
)
)
def _check_complexity(self, tree: ast.Module) -> None:
"""Check code complexity metrics."""
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
# Function length
if node.end_lineno and node.lineno:
func_lines = node.end_lineno - node.lineno
if func_lines > self.MAX_FUNCTION_LENGTH:
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=node.lineno,
code="FUNCTION_TOO_LONG",
message=f"Function '{node.name}' is too long ({func_lines} lines > {self.MAX_FUNCTION_LENGTH})",
severity=Severity.WARNING,
suggestion="Split into smaller functions",
category="complexity",
)
)
# Argument count
args_count = len(node.args.args) + len(node.args.kwonlyargs)
if node.args.vararg:
args_count += 1
if node.args.kwarg:
args_count += 1
if args_count > self.MAX_ARGS:
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=node.lineno,
code="TOO_MANY_ARGUMENTS",
message=f"Function '{node.name}' has too many arguments ({args_count} > {self.MAX_ARGS})",
severity=Severity.WARNING,
suggestion="Consider using a dataclass or **kwargs",
category="complexity",
)
)
elif isinstance(node, ast.ClassDef):
# Class length
if node.end_lineno and node.lineno:
class_lines = node.end_lineno - node.lineno
if class_lines > self.MAX_CLASS_LENGTH:
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=node.lineno,
code="CLASS_TOO_LONG",
message=f"Class '{node.name}' is too long ({class_lines} lines > {self.MAX_CLASS_LENGTH})",
severity=Severity.WARNING,
suggestion="Consider breaking into smaller classes",
category="complexity",
)
)
def _check_test_isolation(self, file_path: Path) -> None:
"""Flag global graph/Redis teardowns in tests (standards rule 34).
Concurrent tests share ONE Neo4j DB and Redis instance, so a
global ``DETACH DELETE`` / ``flushdb()`` wipes data a peer test
is mid-assertion on. Mirrors check_test_isolation in the review
engine and scripts/gate_parallel_safety.py.
"""
name = file_path.name
is_test = name.startswith("test_") or "tests" in file_path.parts
if not is_test:
return
delete_re = re.compile(r"DETACH\s+DELETE|\)\s*DELETE\s+\w", re.IGNORECASE)
flush_re = re.compile(r"\.flush(?:db|all)\s*\(")
scope_re = re.compile(
r"\$\w+|test_id|_TEST_|__test\b|project_id|session_id|tmp_|uuid",
re.IGNORECASE,
)
lines = self.source_lines
for i, raw in enumerate(lines, 1):
line = raw.split("#", 1)[0]
if not line.strip():
continue
if flush_re.search(line):
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=i,
code="TEST_FLUSH_DB",
message="flushdb/flushall wipes Redis shared by concurrent workers",
severity=Severity.ERROR,
suggestion="Delete only this test's keys (scope by test_id)",
category="test-isolation",
)
)
continue
if delete_re.search(line):
window = "".join(lines[max(0, i - 7) : i + 4])
if not scope_re.search(window):
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=i,
code="TEST_GLOBAL_DELETE",
message="Unscoped Neo4j DELETE can wipe a concurrent test's data",
severity=Severity.ERROR,
suggestion="Add a $param/test_id/project_id filter to the MATCH",
category="test-isolation",
)
)
def _check_code_patterns(self, tree: ast.Module) -> None:
"""Check for discouraged code patterns."""
for node in ast.walk(tree):
# Check for bare except
if isinstance(node, ast.ExceptHandler):
if node.type is None: # Bare except
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=node.lineno,
code="BARE_EXCEPT",
message="Bare 'except:' clause should not be used",
severity=Severity.ERROR,
suggestion="Use 'except Exception:' or specific exception types",
category="error-handling",
)
)
# Check for mutable default arguments
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
for default in node.args.defaults:
if isinstance(default, (ast.List, ast.Dict, ast.Set)):
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=node.lineno,
code="MUTABLE_DEFAULT_ARGUMENT",
message=f"Function '{node.name}' has mutable default argument",
severity=Severity.ERROR,
suggestion="Use None as default and initialize mutable value inside function",
category="best-practices",
)
)
# Check for bare os.environ["X"] reads (standards rule 30c):
class ChecklistChecker(BaseChecker):
"""Checklist-based code review checks."""
def _check_file_impl(self, file_path: Path) -> None:
"""Run checklist-based checks."""
try:
tree = ast.parse(self.source)
self._check_class_responsibility(tree)
self._check_method_self_access(tree)
self._check_abstract_classes(tree)
self._check_test_files(tree, file_path)
except SyntaxError:
pass
def _check_class_responsibility(self, tree: ast.Module) -> None:
"""Check class single responsibility (method count)."""
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
methods = [
n
for n in node.body
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
]
if len(methods) > 10: # Arbitrary threshold
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=node.lineno,
code="CLASS_TOO_MANY_METHODS",
message=f"Class '{node.name}' has many methods ({len(methods)}), consider single responsibility",
severity=Severity.WARNING,
category="design",
)
)
def _check_method_self_access(self, tree: ast.Module) -> None:
"""Check that methods access self or cls."""
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
for method in node.body:
if isinstance(method, (ast.FunctionDef, ast.AsyncFunctionDef)):
# Skip special methods
if method.name.startswith("__") and method.name.endswith("__"):
continue
# Check if method uses self or cls
uses_self = any(
isinstance(n, ast.Name) and n.id == "self"
for n in ast.walk(method)
)
uses_cls = any(
isinstance(n, ast.Name) and n.id == "cls"
for n in ast.walk(method)
)
if not (uses_self or uses_cls):
# Check if it's marked as staticmethod
is_static = any(
isinstance(d, ast.Name) and d.id == "staticmethod"
for d in method.decorator_list
)
if not is_static:
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=method.lineno,
code="METHOD_NO_SELF_ACCESS",
message=f"Method '{method.name}' does not access self or cls; consider making it a static method or module-level function",
severity=Severity.INFO,
category="design",
)
)
def _check_abstract_classes(self, tree: ast.Module) -> None:
"""Check abstract class usage."""
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
# Check for abstract methods
abstract_methods = []
for method in node.body:
if isinstance(method, (ast.FunctionDef, ast.AsyncFunctionDef)):
for decorator in method.decorator_list:
if (
isinstance(decorator, ast.Name)
and decorator.id == "abstractmethod"
):
abstract_methods.append(method.name)
# If has abstract methods, check if inherits from ABC
if abstract_methods:
base_names = []
for base in node.bases:
if isinstance(base, ast.Name):
base_names.append(base.id)
elif isinstance(base, ast.Attribute):
base_names.append(base.attr)
if "ABC" not in base_names and "abc.ABC" not in str(node.bases):
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=node.lineno,
code="ABSTRACT_CLASS_NO_ABC",
message=f"Class '{node.name}' has abstract methods but does not inherit from ABC",
severity=Severity.ERROR,
category="design",
)
)
def _check_test_files(self, tree: ast.Module, file_path: Path) -> None:
"""Special checks for test files."""
filename = file_path.name
if not (filename.startswith("test_") or filename.endswith("_test.py")):
return
# Name alone is not enough: production modules may legitimately be
# called test_*.py because they are ABOUT tests, not written in pytest.
# `src/orchestrata_ultimate/gateway/test_runner.py` is the live
# test-execution service, and its `except ProcessLookupError: pass`
# was reported as "Pytest file contains pass statement" — a finding
# that can never be actioned, because there is no assertion to add.
if "src" in file_path.parts:
return
# Check for pass statements in tests
class PassVisitor(ast.NodeVisitor):
# visit_* casing is mandated by the ast.NodeVisitor dispatch
# protocol; FUNCTION_NAMING findings on these are false positives.
def __init__(self, checker: BaseChecker) -> None:
self.checker = checker
self.current_class: str | None = None
self.current_function: str | None = None
def visit_ClassDef(self, node: ast.ClassDef) -> None: # noqa: N802
old_class = self.current_class
self.current_class = node.name
self.generic_visit(node)
self.current_class = old_class
def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # noqa: N802
old_function = self.current_function
self.current_function = node.name
self.generic_visit(node)
self.current_function = old_function
def visit_Pass(self, node: ast.Pass) -> None: # noqa: N802
self.checker.report.add_issue(
CodeIssue(
file=self.checker.current_file,
line=node.lineno,
code="TEST_PASS_STATEMENT",
message="Pytest file contains pass statement - tests should have assertions",
severity=Severity.ERROR,
category="testing",
)
)
visitor = PassVisitor(self)
visitor.visit(tree)
# Check for mock usage in test files
def find_python_files(paths: list[str]) -> list[Path]:
"""Find all Python files in given paths."""
files = []
for path_str in paths:
path = Path(path_str)
if path.is_file() and path.suffix == ".py":
files.append(path)
elif path.is_dir():
files.extend(path.rglob("*.py"))
return files
def filter_issues_by_severity(
issues: list[CodeIssue], min_severity: Severity
) -> list[CodeIssue]:
"""Filter issues by minimum severity."""
severity_order = {Severity.ERROR: 0, Severity.WARNING: 1, Severity.INFO: 2}
min_level = severity_order[min_severity]
return [issue for issue in issues if severity_order[issue.severity] <= min_level]
def output_text(
report: ReviewReport,
filtered_issues: list[CodeIssue],
output_file: str | None = None,
) -> None:
"""Output report in text format."""
output = sys.stdout if output_file is None else open(output_file, "w")
print(f"\n{'=' * 80}", file=output)
print("UNIFIED CODE REVIEW REPORT", file=output)
print(f"{'=' * 80}", file=output)
print(f"\nFiles Analyzed: {report.files_analyzed}", file=output)
print(f"Total Issues: {len(filtered_issues)}", file=output)
# Quick fixes section
quick_fixes = [i for i in filtered_issues if i.fixable]
if quick_fixes:
print("\n" + "-" * 40, file=output)
print(
f"QUICK FIXES ({len(quick_fixes)} issues — run with --fix and --format-code)",
file=output,
)
print("-" * 40, file=output)
for issue in sorted(quick_fixes, key=lambda x: (x.file, x.line, x.column)):
print(
f" [FIXABLE] Line {issue.line}:{issue.column} [{issue.code}] {issue.message}",
file=output,
)
if issue.suggestion:
print(f" Suggestion: {issue.suggestion}", file=output)
# Summary by category
print("\n" + "-" * 40, file=output)
print("SUMMARY BY CATEGORY", file=output)
print("-" * 40, file=output)
for category, count in sorted(report.summary_by_category.items()):
print(f" {category}: {count}", file=output)
# Summary by severity
print("\n" + "-" * 40, file=output)
print("SUMMARY BY SEVERITY", file=output)
print("-" * 40, file=output)
for severity in Severity:
count = report.summary_by_severity.get(severity, 0)
print(f" {severity.value}: {count}", file=output)
# All issues
print("\n" + "-" * 40, file=output)
print("ALL ISSUES", file=output)
print("-" * 40, file=output)
current_file = None
for issue in sorted(filtered_issues, key=lambda x: (x.file, x.line, x.column)):
if issue.file != current_file:
current_file = issue.file
print(f"\n{current_file}:", file=output)
fix_marker = "[FIXABLE] " if issue.fixable else ""
severity_marker = {
"ERROR": "[ERROR]",
"WARNING": "[WARN]",
"INFO": "[INFO]",
}.get(issue.severity.value, "•")
print(
f" {fix_marker}{severity_marker} Line {issue.line}:{issue.column} [{issue.code}] {issue.message}",
file=output,
)
if issue.suggestion:
print(f" Suggestion: {issue.suggestion}", file=output)
print(f"\n{'=' * 80}", file=output)
if output_file:
output.close()
def output_json(
report: ReviewReport,
filtered_issues: list[CodeIssue],
output_file: str | None = None,
) -> None:
"""Output report in JSON format."""
output = {
"timestamp": datetime.now().isoformat(),
"files_analyzed": report.files_analyzed,
"total_issues": len(filtered_issues),
"summary_by_category": dict(report.summary_by_category),
"summary_by_severity": {
k.value: v for k, v in report.summary_by_severity.items()
},
"quick_fixes": [asdict(issue) for issue in filtered_issues if issue.fixable],
"issues": [asdict(issue) for issue in filtered_issues],
}
json_str = json.dumps(output, indent=2)
if output_file:
Path(output_file).write_text(json_str)
print(f"JSON report written to {output_file}")
else:
print(json_str)
def output_markdown(
report: ReviewReport,
filtered_issues: list[CodeIssue],
output_file: str | None = None,
) -> None:
"""Output report in markdown format."""
lines = [
"# Unified Code Review Report\n",
f"**Generated:** {datetime.now().isoformat()}\n",
f"**Files Analyzed:** {report.files_analyzed}\n",
f"**Total Issues:** {len(filtered_issues)}\n",
"\n## Summary by Category\n",
"| Category | Count |\n|----------|-------|",
]
for category, count in sorted(report.summary_by_category.items()):
lines.append(f"| {category} | {count} |")
lines.extend(
["\n## Summary by Severity\n", "| Severity | Count |\n|----------|-------|"]
)
for severity in Severity:
count = report.summary_by_severity.get(severity, 0)
lines.append(f"| {severity.value} | {count} |")
# Quick fixes section
quick_fixes = [i for i in filtered_issues if i.fixable]
if quick_fixes:
lines.extend(
[
"\n## Quick Fixes\n",
"| File | Line | Code | Message | Suggestion |\n",
"|------|------|------|---------|------------|",
]
)
for issue in sorted(quick_fixes, key=lambda x: (x.file, x.line, x.column)):
suggestion = issue.suggestion or ""
lines.append(
f"| {issue.file} | {issue.line} | {issue.code} | {issue.message} | {suggestion} |"
)
lines.extend(
[
"\n## All Issues\n",
"| File | Line | Code | Severity | Message | Suggestion |\n",
"|------|------|------|----------|---------|------------|",
]
)
for issue in sorted(filtered_issues, key=lambda x: (x.file, x.line, x.column)):
suggestion = issue.suggestion or ""
fix_badge = "[FIXABLE] " if issue.fixable else ""
lines.append(
f"| {issue.file} | {issue.line} | {issue.code} | {issue.severity.value} | {fix_badge}{issue.message} | {suggestion} |"
)
output = "\n".join(lines)
if output_file:
Path(output_file).write_text(output)
print(f"Markdown report written to {output_file}")
else:
print(output)
class TraceabilityChecker(BaseChecker):
"""Check that all classes and methods have @traces decorators."""
ENUM_BASES = {"Enum", "StrEnum", "IntEnum", "Flag", "IntFlag"}
PYDANTIC_BASES = {"BaseModel", "BaseSettings", "CXPMixin", "BaseAgent", "BaseVolk"}
PYDANTIC_DECORATORS = {
"field_validator",
"model_validator",
"validator",
"computed_field",
"field_serializer",
"model_serializer",
}
def _check_file_impl(self, file_path: Path) -> None:
"""Check traceability of a single file."""
rel = str(file_path)
# @traces applies to the traced product package only — the tools
# package (src/orchestrata_tools) and scripts/ are untraced by
# convention (two-lane rule, AGENTS.md "@traces Conventions";
# trace-lint scopes to src/orchestrata_ultimate for the same reason).
parts = Path(rel).parts
if "orchestrata_tools" in parts or "scripts" in parts:
return
# Skip domain/, __init__.py, traceability internals, tests
if "/domain/" in rel or file_path.name == "__init__.py":
return
if "traceability_utils.py" in rel or rel.endswith(
"core/tracing/traceability.py"
):
return
if "tests" in Path(rel).parts:
return
if "proto" in Path(rel).parts:
return
if "in_memory" in rel:
return
try:
tree = ast.parse(self.source, filename=str(file_path))
except SyntaxError:
return
for node in tree.body:
if isinstance(node, ast.ClassDef):
self._check_class(node, file_path)
def _check_class(self, node: ast.ClassDef, file_path: Path) -> None:
"""Check a single class and its methods."""
if self._is_enum(node):
return
if self._is_exception(node):
return
if self._is_protocol(node):
return
if self._is_dataclass(node):
return
if self._is_typeddict(node):
return
if self._is_pydantic(node):
return
if self._is_abc(node):
return
if node.name.startswith("_"):
return
if not self._has_traces(node):
# WARNING, not ERROR: per AGENTS.md "@traces Conventions"
# (sparse-and-accurate beats padded), a class with no
# genuinely-matching requirement STAYS untraced — missing
# @traces is a finding to review, not a failure to block on.
self.report.add_issue(
CodeIssue(
file=str(file_path),
line=node.lineno,
column=node.col_offset,
code="TRACE_MISSING_CLASS",
message=f"Class '{node.name}' has no @traces decorator",
severity=Severity.WARNING,
category="traceability",
)
)
is_pydantic = self._is_pydantic(node)
for item in node.body:
if not isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
# Skip property, pydantic validators, and dunders on pydantic models
if self._has_property(item):
continue
if self._has_pydantic_decorator(item):
continue
if is_pydantic and item.name.startswith("__") and item.name.endswith("__"):
continue
if not self._has_traces(item):
self.report.add_issue(
CodeIssue(
file=str(file_path),
line=item.lineno,
column=item.col_offset,
code="TRACE_MISSING_METHOD",
message=f"Method '{node.name}.{item.name}' has no @traces decorator",
severity=Severity.INFO,
category="traceability",
)
)
@staticmethod
def _has_traces(
node: ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef,
) -> bool:
for dec in node.decorator_list:
if isinstance(dec, ast.Name) and dec.id == "traces":
return True
if (
isinstance(dec, ast.Call)
and isinstance(dec.func, ast.Name)
and dec.func.id == "traces"
):
return True
return False
def _is_enum(self, node: ast.ClassDef) -> bool:
return any(
(
b.id
if isinstance(b, ast.Name)
else (b.attr if isinstance(b, ast.Attribute) else "")
)
in self.ENUM_BASES
for b in node.bases
)
def _is_pydantic(self, node: ast.ClassDef) -> bool:
return any(
(
b.id
if isinstance(b, ast.Name)
else (b.attr if isinstance(b, ast.Attribute) else "")
)
in self.PYDANTIC_BASES
for b in node.bases
)
@staticmethod
def _is_exception(node: ast.ClassDef) -> bool:
base_names = [
(
b.id
if isinstance(b, ast.Name)
else (b.attr if isinstance(b, ast.Attribute) else "")
)
for b in node.bases
]
return any(
name
in ("Exception", "BaseException", "ValueError", "RuntimeError", "TypeError")
or name.endswith("Exception")
or name.endswith("Error")
for name in base_names
)
@staticmethod
def _is_protocol(node: ast.ClassDef) -> bool:
return any(
(
b.id
if isinstance(b, ast.Name)
else (b.attr if isinstance(b, ast.Attribute) else "")
)
== "Protocol"
for b in node.bases
)
@staticmethod
def _is_dataclass(node: ast.ClassDef) -> bool:
for dec in node.decorator_list:
if isinstance(dec, ast.Name) and dec.id == "dataclass":
return True
if (
isinstance(dec, ast.Call)
and isinstance(dec.func, ast.Name)
and dec.func.id == "dataclass"
):
return True
return False
@staticmethod
def _is_typeddict(node: ast.ClassDef) -> bool:
return any(
(
b.id
if isinstance(b, ast.Name)
else (b.attr if isinstance(b, ast.Attribute) else "")
)
== "TypedDict"
for b in node.bases
)
@staticmethod
def _is_abc(node: ast.ClassDef) -> bool:
return any(
(
b.id
if isinstance(b, ast.Name)
else (b.attr if isinstance(b, ast.Attribute) else "")
)
in ("ABC",)
for b in node.bases
)
@staticmethod
def _has_property(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
for dec in node.decorator_list:
if isinstance(dec, ast.Name) and dec.id == "property":
return True
if isinstance(dec, ast.Attribute) and dec.attr in (
"setter",
"deleter",
"getter",
):
return True
return False
def _has_pydantic_decorator(
self, node: ast.FunctionDef | ast.AsyncFunctionDef
) -> bool:
for dec in node.decorator_list:
if isinstance(dec, ast.Name) and dec.id in self.PYDANTIC_DECORATORS:
return True
if (
isinstance(dec, ast.Call)
and isinstance(dec.func, ast.Name)
and dec.func.id in self.PYDANTIC_DECORATORS
):
return True
return False
# Standards-body namespace IRIs: identifiers in RDF/OWL/XSD/XML documents.
_NAMESPACE_IRI_RE = re.compile(
r"https?://(?:www\.)?(?:w3\.org|purl\.org|xmlns\.com|schema\.org"
r"|openrdf\.org|rdfs?\.org)/",
re.I,
)
# %s / %d / {name} / $VAR placeholders mark a template, not a literal address.
_URI_PLACEHOLDER_RE = re.compile(r"%[sd]|\{[^}]*\}|\$\{?\w+")
class ConnectionStringChecker(BaseChecker):
"""Check for hardcoded connection strings and env-var defaults."""
# Regex patterns
IPV4_RE = re.compile(r"(?:\d{1,3}\.){3}\d{1,3}")
URI_RE = re.compile(r"(bolt|redis|mongodb|postgres|mysql|https?)://[^\s\"']+")
LOCALHOST_PORT_RE = re.compile(r"localhost:\d{2,5}")
# TEST-NET and documentation IPs (RFC 5737)
TEST_NET_PREFIXES = ("192.0.2.", "198.51.100.", "203.0.113.")
# Loopback / multicast / reserved
SKIP_IP_PREFIXES = (
"127.",
"0.0.0.0",
"255.",
"240.",
"192.0.2.",
"198.51.100.",
"203.0.113.",
# Link-local (RFC 3927) — includes the 169.254.169.254 cloud-metadata
# endpoint, which appears in SSRF *blocklists* as a security constant,
# never as reachable deployment infrastructure.
"169.254.",
)
def _check_file_impl(self, file_path: Path) -> None:
"""Scan file for hardcoded connection values."""
if "tests" in Path(str(file_path)).parts:
return
try:
tree = ast.parse(self.source)
except SyntaxError:
return
# Collect docstring line numbers to skip
docstring_lines = set()
for node in ast.walk(tree):
if isinstance(
node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)
):
if (
node.body
and isinstance(node.body[0], ast.Expr)
and isinstance(node.body[0].value, ast.Constant)
and isinstance(node.body[0].value.value, str)
):
docstring_lines.add(node.body[0].lineno)
# Collect string constants that are default args to os.environ.get / os.getenv
env_get_default_lines = self._collect_env_get_defaults(tree)
for node in ast.walk(tree):
if isinstance(node, ast.Constant) and isinstance(node.value, str):
if node.lineno in docstring_lines:
continue
if node.lineno in env_get_default_lines:
continue
self._check_string(node.value, node.lineno)
@staticmethod
def _is_env_get_call(func: ast.expr) -> bool:
"""True for the call target of os.environ.get(...), bare
environ.get(...), os.getenv(...), or bare getenv(...).
Matches both the ``os.``-qualified forms (the standard convention
used throughout this codebase — see AGENTS.md "no bare
os.environ['X']") and the bare forms (``from os import environ,
getenv``), so callers don't silently stop recognizing the common
case.
"""
if isinstance(func, ast.Attribute) and func.attr == "get":
if isinstance(func.value, ast.Name) and func.value.id == "environ":
return True
if (
isinstance(func.value, ast.Attribute)
and func.value.attr == "environ"
and isinstance(func.value.value, ast.Name)
and func.value.value.id == "os"
):
return True
return False
if isinstance(func, ast.Attribute) and func.attr == "getenv":
return isinstance(func.value, ast.Name) and func.value.id == "os"
if isinstance(func, ast.Name) and func.id == "getenv":
return True
return False
def _collect_env_get_defaults(self, tree: ast.AST) -> set[int]:
"""Return line numbers of string constants used as defaults in os.environ.get()."""
lines: set[int] = set()
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
func = node.func
if not self._is_env_get_call(func):
continue
# Second positional arg or "default" keyword arg is the fallback
if len(node.args) >= 2:
default_arg = node.args[1]
if isinstance(default_arg, ast.Constant) and isinstance(
default_arg.value, str
):
lines.add(default_arg.lineno)
for kw in node.keywords:
if (
kw.arg == "default"
and isinstance(kw.value, ast.Constant)
and isinstance(kw.value.value, str)
):
lines.add(kw.value.lineno)
return lines
def _check_string(self, value: str, lineno: int) -> None:
"""Flag strings that look like connection endpoints."""
if len(value) < 4:
return
# Skip third-party API URLs (not infrastructure)
if (
value.startswith("https://api.")
or value.startswith("https://dashscope.")
or value.startswith("https://chat.")
or value.startswith("https://kimi.")
):
return
if "openrouter.ai" in value or "orchestrata.dev" in value:
return
# Public package index (pinned-sdist vendor builds), not deployment infra
if "pypi.org" in value or "pythonhosted.org" in value:
return
# Browser User-Agent strings: version tokens like Chrome/122.0.0.0
# false-positive the IPv4 regex; a UA string is not infrastructure.
if value.startswith("Mozilla/"):
return
# Public web endpoints used as env-overridable defaults, not infra
if "duckduckgo.com" in value:
return
# IPv4 address anywhere in the string
ip_match = self.IPV4_RE.search(value)
if ip_match:
ip = ip_match.group(0)
if any(ip.startswith(p) for p in self.SKIP_IP_PREFIXES):
return
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=lineno,
code="HARDCODED_IP",
message=f"Hardcoded IP address found: {value[:60]}",
severity=Severity.ERROR,
category="config",
)
)
return
# Connection URI (bolt://, redis://, etc.)
uri_match = self.URI_RE.search(value)
if uri_match:
uri = uri_match.group(0)
# Skip nonexistent/test hostnames
if "nonexistent" in uri or "custom" in uri:
return
# A namespace IRI is an IDENTIFIER, not an endpoint: nothing
# connects to http://www.w3.org/2002/07/owl#. Firing on the
# @prefix lines of an RDF/TTL template reported a hardcoded
# connection where no connection exists.
if _NAMESPACE_IRI_RE.search(uri):
return
# A format template is not an address: "http://%s:%d" and
# "http://{host}:{port}" are how the endpoint is BUILT from
# resolved values, which is the pattern RUL-003 asks for.
if _URI_PLACEHOLDER_RE.search(uri):
return
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=lineno,
code="HARDCODED_URI",
message=f"Hardcoded connection URI found: {value[:60]}",
severity=Severity.ERROR,
category="config",
)
)
return
# localhost with port
if self.LOCALHOST_PORT_RE.search(value):
self.report.add_issue(
CodeIssue(
file=self.current_file,
line=lineno,
code="HARDCODED_LOCALHOST",
message=f"Hardcoded localhost endpoint found: {value[:60]}",
severity=Severity.ERROR,
category="config",
)
)
def run_diagnostics(report: ReviewReport) -> None:
"""Run test diagnostic tools and add findings to the report."""
import subprocess
# Test isolation audit
try:
result = subprocess.run(
[sys.executable, "scripts/test_isolation_audit.py", "tests/"],
capture_output=True,
text=True,
timeout=60,
)
if result.returncode != 0:
# Parse violations from output
for line in result.stdout.split("\n"):
if "[CREATE_VS_MERGE]" in line or "[HARDCODED_NAME]" in line:
report.add_issue(
CodeIssue(
file="tests/",
line=0,
code="TEST-ISOLATION",
message=line.strip(),
severity=Severity.WARNING,
category="test-isolation",
)
)
elif "[PYTEST_SKIP]" in line:
report.add_issue(
CodeIssue(
file="tests/",
line=0,
code="TEST-SKIP",
message=line.strip(),
severity=Severity.INFO,
category="test-isolation",
)
)
except Exception as e:
print(f"Diagnostic audit failed: {e}", file=sys.stderr)
# graph preflight
try:
result = subprocess.run(
[sys.executable, "scripts/graph_test_preflight.py", "tests/", "--offline"],
capture_output=True,
text=True,
timeout=60,
)
if result.returncode != 0:
for line in result.stdout.split("\n"):
if "[HIGH]" in line:
report.add_issue(
CodeIssue(
file="tests/",
line=0,
code="GRAPH-CONSTRAINT",
message=line.strip(),
severity=Severity.WARNING,
category="neo4j-constraints",
)
)
except Exception as e:
print(f"graph preflight failed: {e}", file=sys.stderr)
def load_banned_libraries(repo_root: Path) -> list[str]:
"""Read .claude/banned-libraries.txt — distribution names that must not be
declared as dependencies. Comments (#) and blanks are ignored. Returns []
if the file is absent."""
banned_file = repo_root / ".claude" / "banned-libraries.txt"
if not banned_file.exists():
return []
names: list[str] = []
for raw in banned_file.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if line and not line.startswith("#"):
names.append(line)
return names
def check_banned_dependencies(report: ReviewReport, repo_root: Path) -> None:
"""SHOWSTOPPER gate: fail if pyproject.toml declares a banned dependency.
Matches a banned name only as a quoted TOML requirement string
(e.g. ``"pytest-xdist>=3.5.0"``) so a name inside a comment (``# pytest-xdist
is BANNED``) is allowed. Single source of truth: .claude/banned-libraries.txt.
"""
pyproject = repo_root / "pyproject.toml"
if not pyproject.exists():
return
banned = load_banned_libraries(repo_root)
if not banned:
return
# Boundary after the name: version op / extras '[' / quote / comma / space /
# end. Includes '[' so "pytest-xdist[psutil]>=3" is still caught.
patterns = {
name: re.compile(
rf"""["']{re.escape(name)}(["'>=~<!;,\s\[]|$)""", re.IGNORECASE
)
for name in banned
}
for lineno, raw in enumerate(
pyproject.read_text(encoding="utf-8").splitlines(), start=1
):
code = raw.split("#", 1)[0] # ignore trailing comments
for name, pat in patterns.items():
if pat.search(code):
report.add_issue(
CodeIssue(
file=str(pyproject),
line=lineno,
code="BANNED-DEP",
message=(
f"Banned dependency '{name}' declared in pyproject.toml "
"— see .claude/banned-libraries.txt. Parallelism comes "
"from scripts/run_tests.py, never pytest-xdist."
),
severity=Severity.ERROR,
category="dependencies",
)
)
def check_prolog_boundary(report: ReviewReport, repo_root: Path) -> None:
"""BOUNDARY gate: fail if any Python Prolog query site targets a
predicate/arity that does not exist in the consulted rule base
(reports/proposal-wire-prolog-governance.md §7 Phase 1). Same
subprocess + repo-root-independent pattern as ``check_banned_dependencies``
— a src-only review must still catch a boundary mismatch anywhere in
``src/orchestrata_ultimate``.
"""
script = repo_root / "scripts" / "check_prolog_boundary.py"
if not script.exists():
return
try:
result = subprocess.run(
[sys.executable, str(script), "--json"],
cwd=str(repo_root),
capture_output=True,
text=True,
timeout=90,
)
except (OSError, subprocess.TimeoutExpired) as exc:
print(f"Prolog boundary gate failed to run: {exc}", file=sys.stderr)
return
if result.returncode == 0:
return
try:
payload = json.loads(result.stdout)
except json.JSONDecodeError:
report.add_issue(
CodeIssue(
file=str(script),
line=0,
code="BOUNDARY",
message=f"check_prolog_boundary.py failed: {result.stderr.strip()}",
severity=Severity.ERROR,
category="prolog-boundary",
)
)
return
for failure in payload.get("failures", []):
report.add_issue(
CodeIssue(
file=failure.get("file", str(script)),
line=failure.get("line", 0),
code="BOUNDARY",
message=(
f"{failure.get('functor')}/{failure.get('arity')} → "
f"{failure.get('candidate')}"
),
severity=Severity.ERROR,
category="prolog-boundary",
)
)
def main() -> int:
"""Main entry point."""
parser = argparse.ArgumentParser(
description="Unified Code Review Tool for orchestrata-ultimate"
)
parser.add_argument(
"paths", nargs="+", help="Python files or directories to analyze"
)
parser.add_argument("--output", "-o", help="Output file for report")
parser.add_argument(
"--format",
choices=["text", "json", "markdown"],
default="text",
help="Output format",
)
parser.add_argument(
"--min-severity",
choices=["ERROR", "WARNING", "INFO"],
default="INFO",
help="Minimum severity to report",
)
parser.add_argument(
"--fix", action="store_true", help="Auto-fix fixable ruff issues"
)
parser.add_argument(
"--format-code",
action="store_true",
help="Format code with ruff before checking",
)
parser.add_argument(
"--max-files",
type=int,
default=None,
help="Maximum number of files to process (for testing)",
)
parser.add_argument(
"--diagnostics",
action="store_true",
help="Run test diagnostic tools (isolation audit, graph preflight)",
)
parser.add_argument(
"--stdout",
action="store_true",
help="Also print the formatted report to stdout",
)
args = parser.parse_args()
# Find files
files = find_python_files(args.paths)
# Filter out virtual environment files
files = [f for f in files if ".venv" not in str(f) and "__pycache__" not in str(f)]
if args.max_files:
files = files[: args.max_files]
print(f"Limiting to {args.max_files} files", file=sys.stderr)
if not files:
print("No Python files found to analyze", file=sys.stderr)
return 0
# Create report and checkers — all run every time for historical record
report = ReviewReport()
checkers: list[BaseChecker] = []
if args.format_code:
checkers.append(RuffFormatChecker(report))
checkers.append(RuffChecker(report, fix=args.fix))
checkers.append(MyPyChecker(report))
checkers.append(StyleChecker(report))
checkers.append(ChecklistChecker(report))
checkers.append(ConnectionStringChecker(report))
checkers.append(TraceabilityChecker(report))
# Run diagnostic tools if requested
if args.diagnostics:
print("Running diagnostic tools...", file=sys.stderr)
run_diagnostics(report)
# Repo root for the gates below — overridable at call time (tests point
# this at an isolated tmp_path so the gates take their no-op early-return
# paths instead of spawning against the real repo). Production callers
# never set the env var, so this resolves identically to before.
gate_root = Path(
os.environ.get(
"CODE_REVIEW_REPO_ROOT", str(Path(__file__).resolve().parent.parent)
)
)
# Banned-dependency gate — one-shot scan of the repo's pyproject.toml,
# independent of which files were passed for review (a src-only review must
# still catch a banned dep). Repo root = parent of this script's dir.
check_banned_dependencies(report, gate_root)
# Prolog boundary gate — one-shot AST walk of the whole src tree,
# independent of which files were passed for review (same rationale as
# the banned-dependency gate above).
check_prolog_boundary(report, gate_root)
# Run checkers on each file
for i, file_path in enumerate(files):
if i % 10 == 0:
print(
f"Processing file {i + 1}/{len(files)}: {file_path.name}",
file=sys.stderr,
)
report.files_analyzed += 1
for checker in checkers:
checker.check_file(file_path)
# Filter issues by severity
min_severity = Severity(args.min_severity)
filtered_issues = filter_issues_by_severity(report.issues, min_severity)
# Always write a dated report to reviews/ for historical record
reviews_dir = Path("reviews")
reviews_dir.mkdir(exist_ok=True)
timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
ext = {"json": "json", "markdown": "md", "text": "txt"}[args.format]
dated_path = reviews_dir / f"code-review-{timestamp}.{ext}"
if args.format == "json":
output_json(report, filtered_issues, str(dated_path))
if args.output:
output_json(report, filtered_issues, args.output)
if args.stdout:
output_json(report, filtered_issues, None)
elif args.format == "markdown":
output_markdown(report, filtered_issues, str(dated_path))
if args.output:
output_markdown(report, filtered_issues, args.output)
if args.stdout:
output_markdown(report, filtered_issues, None)
else: # text format
output_text(report, filtered_issues, str(dated_path))
if args.output:
output_text(report, filtered_issues, args.output)
if args.stdout:
output_text(report, filtered_issues, None)
print(f"Report written to {dated_path}", file=sys.stderr)
# Return non-zero exit code if there are errors
error_count = report.summary_by_severity.get(Severity.ERROR, 0)
return 1 if error_count > 0 else 0
if __name__ == "__main__":
sys.exit(main())