# Code review: the automated part Four gates existed before this document (the test suites, `apps/studio/tools/ui_check.py`, the real-GL smoke tests, the binding-purity test) and none of them is a linter. This adds the missing *static* layer, deliberately small. ## Why it exists, in one measured fact Until now the three repos had **no Ruff configuration at all**. `ruff check .` therefore ran whatever the installed version enables by default — Ruff 0.16 enables a very broad set (`UP`/`RUF`/`PLR`/`S`/`TRY`/`PERF`/`FURB`/`YTT`/`ASYNC`…) — so the same tree produced **1038 / 438 / 117** findings in `orisys-app` / `orisys-sdk` / `orisys-sensor-design`, and a different Ruff version produced different ones. A gate built on that is not a gate. `ruff.toml` now pins it. ## What is gated, and what is deliberately not | rule | meaning | gated? | why | |---|---|---|---| | `BLE001` | broad `except Exception` | **no** | A broad catch is the *right* choice around device I/O, GL teardown and camera probing. This codebase already records that choice inline as `# noqa: BLE001 - `. Gating it would produce ~100 findings whose only correct fix is "add a reasoned suppression". | | `S110`, `S112` | handler is only `pass` / `continue` | **yes** | The complaint is not the breadth of the catch, it is that the failure becomes **invisible**. Silence is acceptable only with a reasoned suppression or a handler that reports/re-raises. | | `E722` | bare `except:` | **yes** | Strictly worse than `except Exception:` — it also swallows `KeyboardInterrupt`/`SystemExit`. | | `F` | unused import, redefinition, undefined name, unused variable | **yes** | Real defects: `F821`/`F822` are latent `NameError`s, `F811` can be a shadowed definition. | | `B023` | closure over a loop variable | **yes** | Late binding silently produces wrong geometry in the headless Blender scripts. | | `PLW1510` | `subprocess.run` without `check` | **yes** | A swallowed tool failure. | | `RUF100` | unused suppression | reported, **never auto-fixed** | The `- reason` in a `noqa` comment is documentation. A machine must not delete it. Triage by hand. | | `UP`, `I`, `PIE`, `RET501`, `PLR1711`, `SIM114`, `RUF022`, `RUF023` | mechanical modernisation | in the lint config, **not** in the gate | Applied once by `--fix`; gating them would only churn AI-authored diffs. | `E402` (module import not at top of file) is **selected but not gated**. `import` after `os.environ[...]` is deliberate in this codebase — the environment must be set before Qt/cv2/the SDK load — so gating it would be pure noise. But it is the only rule that would catch an *accidental* import moved above that setup, so it stays visible. When you touch one of those files, record the intent as `# noqa: E402 - `; `E701`/`E702` are style, which `ruff format` owns. A gate that cries wolf gets ignored, which is worse than no gate. ## The three commands ```powershell # 1. The agent loop, after every edit — mechanical, no review needed. ruff check --fix # never --unsafe-fixes ruff check --output-format json # what remains, machine-readable # 2. The gate before a change lands. Changed files only, machine-readable, non-zero on findings. python tools/lint_gate.py # vs HEAD python tools/lint_gate.py --base origin/main python tools/lint_gate.py --all # gate rules over the whole repo python tools/lint_gate.py --json # 3. Formatting, changed files only — never a whole-tree rewrite mid-diff. ruff format ``` `$RUFF` may point at a specific ruff binary; otherwise `tools/lint_gate.py` uses `PATH` or the one beside the interpreter. **Pin the version**: the gate prints the version it ran, and findings differ between versions, so a runner must install the same one the developers use (currently 0.16.9). ## What this found when it was introduced Filtering to rules that can indicate a defect produced ~18 candidates rather than style noise — which is the argument for the narrow set: - six latent `NameError`s (`np`, `sys`, `Callable`, `FrameIdentity`, `tessellate_polygon`, and an `__all__` entry naming a function that does not exist) — none caught by any test; - a **duplicated** function definition that silently shadowed the first; - `lower == lower` / `upper == upper` in a range check, and the same pattern inside a **test assertion that could never fail**; - 13 closures over loop variables in the headless Blender/patch scripts; - two `subprocess.run` calls whose failure was silently ignored. ## Follow-up deliberately not done yet `PLR0124` ("name compared with itself") is not in `select`. Eleven of its sites are the deliberate `value != value` NaN idiom and must become `math.isnan(...)` (or carry a reasoned suppression) before the rule can be gated — otherwise it is noise. Once those are converted, add `PLR0124` to `select` and to `GATE_SELECT` in `tools/lint_gate.py`.