ait-mcp
<h1 align="center">๐ก๏ธ AiT-test</h1>
<h3 align="center">Python static analysis that traces every finding back to its source</h3>
<p align="center"><b>Causation Trace ยท Repro Witness ยท Verified Patch ยท Zero Dependencies</b></p>
<img width="100%" alt="AiT-Test: zero runtime dependencies, causation trace, repro witness and a verified patch where a deterministic fix exists, 13 evidence-based rules" src="docs/assets/AiT.gif" />
<p align="center"><i>Overview of the engine's four pillars ยท the numbers in the image come from the author's machine; reproducible figures are under <a href="#metrics">measured results</a></i></p>
---
<p align="center"><a href="#"><img alt="Python 3.10 to 3.15" src="https://img.shields.io/badge/python-3.10%20%E2%86%92%203.15-3776AB?style=flat-square&logo=python&logoColor=white"></a> <a href="#"><img alt="Version v1.0.0" src="https://img.shields.io/badge/version-v1.0.0-6366F1?style=flat-square&logo=semver&logoColor=white"></a> <a href="#tests"><img alt="268 tests passing" src="https://img.shields.io/badge/tests-268%20passing-059669?style=flat-square&logo=checkmarx&logoColor=white"></a> <a href="#ci"><img alt="CI: 15 job runs" src="https://img.shields.io/badge/CI-15%20job%20runs-2088FF?style=flat-square&logo=githubactions&logoColor=white"></a> <a href="https://github.com/xTanTHaix/AiT-Test/actions/workflows/codeql.yml"><img alt="CodeQL status" src="https://img.shields.io/github/actions/workflow/status/xTanTHaix/AiT-Test/codeql.yml?branch=main&label=CodeQL&style=flat-square&logo=githubactions&logoColor=white"></a> <a href="#"><img alt="Zero runtime dependencies" src="https://img.shields.io/badge/runtime%20dependencies-0-22C55E?style=flat-square&logo=python&logoColor=white"></a> <a href="#rules"><img alt="13 rules" src="https://img.shields.io/badge/rules-13-0284C7?style=flat-square&logo=shieldsdotio&logoColor=white"></a> <a href="https://github.com/xTanTHaix/AiT-Test"><img alt="Code size" src="https://img.shields.io/github/languages/code-size/xTanTHaix/AiT-Test?style=flat-square&color=0284C7"></a> <a href="#mcp"><img alt="MCP Server" src="https://img.shields.io/badge/MCP-Server-8B5CF6?style=flat-square&logo=anthropic&logoColor=white"></a> <a href="#"><img alt="Windows and Linux" src="https://img.shields.io/badge/platforms-Windows%20%7C%20Linux-0284C7?style=flat-square&logo=linux&logoColor=white"></a> <a href="#credits"><img alt="Author xTanTHaix" src="https://img.shields.io/badge/author-xTanTHaix-8B5CF6?style=flat-square&logo=github&logoColor=white"></a> <a href="LICENSE"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-F59E0B?style=flat-square&logo=googledocs&logoColor=white"></a></p>
<p align="center"><a href="docs/demo.txt">๐ฌ Real run</a> โข <a href="#rules">๐งญ 13 rules</a> โข <a href="#cli">๐ฅ๏ธ CLI</a> โข <a href="#mcp">๐ค MCP</a> โข <a href="#tests">๐งช Tests</a> โข <a href="#ci">๐ CI</a> โข <a href="#credits">๐
Credits</a></p>
<p align="center"><a href="README.md">English</a> ยท <a href="readme-th.md">เนเธเธข</a></p>
> [!IMPORTANT]
> Runs on the standard library AST alone: **no runtime dependencies and no network access.**
> Witnesses are built as text and syntax-checked only; the code it flags is never executed.
---
## ๐ฏ The problem it solves
A regular linter points at a line but not at the path that made the value `None`.
You open the file, walk backwards to the origin, and guess whether your patch will break something else.
| โ ๏ธ The question left open | โก What AiT-test answers in one result |
| :--- | :--- |
| Where did this `None` come from | **Causation trace**: every step carries `file:line:col` from the producer to the point of failure |
| How do I reproduce it | **Repro witness**: the smallest code that triggers the symptom, plus a `safe_to_execute` flag |
| Will the fix break something else | **Suggested fix**: a unified diff that passes `ast.parse` and is rescanned for new findings |
| Which file should I fix first | **A DEFECT needs evidence**: without concurrency or type evidence the finding drops to WARNING instead of guessing |
---
## โก Getting started
Python 3.10 or newer. Nothing to install:
```bash
python cli.py src
```
Or install the `ait` / `ait-mcp` commands (still no dependencies):
```bash
pip install -e .
ait src
```
The distribution is named `ait-test` (`pip show ait-test`). The engine name reported by `--version` and by MCP `serverInfo` is `ait-engine 1.0.0`. Same project at two layers, not two packages.
<img width="100%" alt="AiT-test catches an unguarded NoneType and confirms the finding is gone after the guard" src="docs/demo.gif" />
<p align="center"><i>A real run on <code>demo.py</code>: N01 detected with a causation trace, witness and a diff that passed re-verification</i></p>
<details>
<summary><b>๐ฌ Click to expand: full output from the GIF above</b></summary>
The full transcript lives in [`docs/demo.txt`](docs/demo.txt), and the GIF above was recorded from the real run.
```text
$ python cli.py demo.py
================== [AIT-N01] Unshielded NoneType Dereference ===================
File: demo.py:10:10
Severity: DEFECT (Confidence: 0.90)
Function: render()
Symbol: value
`value` can be None at this point but is dereferenced without a guard
--- Causation Trace ---
Step 1 | demo.py:9:12
| `lookup()` is declared to return `Optional[str]`, so the result can be None
Step 2 | demo.py:9:4
| `value` receives the return value of `lookup()`
Step 3 | demo.py:10:10
| `value` is dereferenced here without an `is None` guard
--- Suggested Fix ---
--- a/demo.py
+++ b/demo.py
@@ -10,1 +10,3 @@
+ if value is None:
+ raise RuntimeError(f"value is unexpectedly None")
print(value.upper())
========================= 1 failed, 0 passed in 2.59ms =========================
DEFECT 1 ยท WARNING 0 ยท ADVICE 0
exit code: 1
```
The code used for the demo:
```python
from typing import Optional
def lookup(key: str) -> Optional[str]:
return None
def render() -> None:
value = lookup("alpha")
print(value.upper())
```
</details>
---
<a id="rules" name="rules"></a>
## ๐งญ The 13 rules
<details>
<summary><b>๐ Click to expand: all 13 rules and the evidence each one requires</b></summary>
| ID | Level | What it catches | Evidence it requires |
|---|---|---|---|
| `AIT-Z01` | DEFECT | `Popen(..., stdout=PIPE)` followed by `wait()` without draining the pipe | a pipe exists and the child is waited on without a prior `communicate()`/`read()` |
| `AIT-Z02` | DEFECT | a child handle is dropped without `wait`/`poll`/returning ownership | the variable resolves to a process handle |
| `AIT-Z03` | WARNING | a process/thread spawned at module scope without `if __name__ == "__main__":` | the call really sits at module level |
| `AIT-R01` | DEFECT / WARNING | read-modify-write on shared state with no lock around it | concurrency evidence (spawn site, `async def`, or a lock in the class); otherwise not reported |
| `AIT-R02` | WARNING | a condition checked outside the lock and again inside it | a real lock region and the same condition |
| `AIT-A01` | DEFECT / WARNING | a blocking call inside `async def` | `time.sleep`/network/process = DEFECT, file I/O = WARNING; offloading through `to_thread`/executor is not reported |
| `AIT-A02` | WARNING | `create_task()` with nothing holding a reference | not stored in a variable, not awaited, no done callback |
| `AIT-A03` | DEFECT | a coroutine called without `await` | the callee resolves to a coroutine in the same file or to a known API |
| `AIT-M01` | DEFECT | mutating a caller's parameter in place (`append`, `pop`, `update`, ...) | the write targets the parameter name directly |
| `AIT-M02` | DEFECT | a mutable default argument (`[]`, `{}`, `set()`, ...) | a real mutable literal or constructor |
| `AIT-N01` | DEFECT | dereferencing a value that may be `None` without a guard | evidence the value is optional (annotation, `None` literal, a producer returning optional, `dict.get`) |
| `AIT-H01` | WARNING | a bare `except:` that swallows everything | `except: raise` (bare re-raise) is not reported because nothing is swallowed |
| `AIT-H02` | ADVICE | more than four levels of nested control flow | counts real control nodes only and never crosses a scope |
</details>
> [!NOTE]
> **Severity policy:** a DEFECT needs supporting evidence. When concurrency or type evidence is missing,
> the finding drops to WARNING instead of guessing, because one false finding is enough to make people stop reading the report.
---
<a id="cli" name="cli"></a>
## ๐ฅ๏ธ CLI
```text
python cli.py [target] [options]
```
| Flag | Effect |
|---|---|
| `-v, --verbose` | per-file detail |
| `-q, --quiet` | summary only |
| `-x, --exitfirst` | stop at the first file carrying a DEFECT |
| `--lf, --last-failed` | scan only the files that failed last run (needs the cache) |
| `--fix` | show a diff and ask one finding at a time (needs a TTY) |
| `--no-cache` | skip both cache reads and writes |
| `--maxfail N` | show at most N failure blocks |
| `--no-color` | disable ANSI (or set `NO_COLOR=1`) |
| `--version` | print the version |
**Exit code:** `0` clean ยท `1` DEFECT present ยท `2` interrupted ยท `3` parse or internal error ยท `4` usage error
> [!TIP]
> When `--lf` finds a cache it cannot open, no index, or no earlier run, it scans every file and says so on stderr.
> The alternative is analysing zero files and reporting "clean" without having read anything.
<details>
<summary><b>๐ง Click to expand: how <code>--fix</code> works (7 steps)</b></summary>
1. every patch is passed through `ast.parse` again before it is offered
2. findings are asked one at a time (`y` / `N` / `a` for all / `q` to quit)
3. before writing, the original is copied to `.ait_backup/<timestamp>-<pid>/`
4. the file is written to a temporary path and moved with `os.replace` (atomic), as bytes, so line endings are preserved
5. the file is written back with its original codec (`utf-8-sig` keeps the BOM, a BOM-less UTF-8 file stays BOM-less, `cp874` stays `cp874`)
if a patch contains characters the original codec cannot encode, the write is refused rather than silently converting the file to UTF-8
6. that file is rescanned; any new finding triggers an immediate rollback
7. the whole target is rescanned at the end to confirm the final state; a Ctrl-C mid-way prints how many fixes were applied and where the backup lives
</details>
<details>
<summary><b>๐ Click to expand: suppressing intended findings and <code>.aitignore</code></b></summary>
Inline directives work with or without the `AIT-` prefix (people tend to remember the rule number from the output):
```python
value.upper() # ait: disable=AIT-N01
```
```python
except: # ait: disable=H01
pass
```
```python
except: # ait: disable=ALL
pass
```
A suppressed finding is not printed but is still counted in the summary line as `N suppressed`, so nothing disappears quietly.
Or put globs in `.aitignore` at the project root:
```text
legacy_*.py
build/
```
</details>
---
<a id="mcp" name="mcp"></a>
## ๐ค MCP
`mcp.py` speaks JSON-RPC 2.0 over stdio and works with any MCP client. stdout carries protocol frames only; logs go to stderr.
```json
{
"mcpServers": {
"ait": {
"command": "python",
"args": ["L:\\path\\to\\AiT-Test\\mcp.py"]
}
}
}
```
opencode (`opencode.jsonc`) uses `mcp.servers.<name>` with `command` as an array, not the `mcpServers` shape above:
```jsonc
{
"mcp": {
"servers": {
"ait": {
"type": "local",
"command": ["python", "L:\\path\\to\\AiT-Test\\mcp.py"]
}
}
}
}
```
`ait-mcp --help` and `ait-mcp --version` check the install without entering the serve loop.
| Tool | What it does |
|---|---|
| `ait_review` | scans a file or directory and returns findings with causation, witness and diff |
| `ait_slice_context` | returns source, variable table, calls and callers for one symbol (token-efficient) |
| `ait_verify_fix` | compares candidate code against the baseline: what is resolved and what is newly introduced |
<details>
<summary><b>๐ Click to expand: transport contract and the arguments each tool accepts</b></summary>
| Tool | Arguments |
|---|---|
| `ait_review` | `path` (required), `severity_min` = `ADVICE`/`WARNING`/`DEFECT`, `max_findings` = 1โ500 (default 50), `exitfirst`, `use_cache` |
| `ait_slice_context` | `file_path`, `symbol_name` (required), `depth` = 0โ3 (default 1) |
| `ait_verify_fix` | `file_path`, `candidate_code` (required) |
- `isError: false` when the tool ran successfully **even if it found a DEFECT**, because a finding is a usable result, not a tool failure
(`isError: true` is reserved for the tool genuinely failing, for example a candidate that does not parse)
- an argument the schema does not declare, a wrong type, or an out-of-range value gets `-32602` instead of being ignored or silently clamped
- an unexpected exception (say `RecursionError`) is returned as `-32603` and the process keeps serving requests
- `shutdown` and `notifications/exit` stop the serve loop; `initialize` does not advertise a `logging` capability because none is used
- `structuredContent` holds the object once; `content[0].text` is a compact JSON mirror (no indentation) so the payload does not double in size
</details>
<details>
<summary><b>๐งช Click to expand: an <code>ait_verify_fix</code> call</b></summary>
```json
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
"name":"ait_verify_fix",
"arguments":{
"file_path":"demo.py",
"candidate_code":"from typing import Optional\n\ndef lookup(k: str) -> Optional[str]:\n return None\n\ndef render() -> None:\n value = lookup('a')\n if value is None:\n return\n print(value.upper())\n"
}
}}
```
`ait_verify_fix` re-checks AST-level invariants; it is not a mathematical proof.
All it confirms is that the 13 rules find nothing in the new code, which does not mean the program is correct in every case.
</details>
---
## ๐ Capability and evidence
**0 runtime deps ยท offline ยท 13 rules ยท trace + witness on every finding ยท patches only where a deterministic fix exists (A01/A02/H02/M02/R01 have none) ยท digest-aware cache**
<details>
<summary><b>๐ Click to expand: nine dimensions and where the evidence lives</b></summary>
| Dimension | Status | Evidence in the project |
| :--- | :---: | :--- |
| Runtime dependencies | โ
0 | every import comes from the standard library; `python -S` still scans |
| Works offline | โ
yes | no HTTP client, no telemetry |
| Causation trace | โ
every finding | `causation[]` with `file:line:col` from producer to failure point |
| Repro witness | โ
text + syntax check | never executed; deadlock/recursion demos carry `safe_to_execute: false` |
| Suggested patch | โ
re-parsed and rescanned | a patch that introduces a new finding is rejected |
| Cache | โ
sqlite3 WAL | key = path + size + mtime_ns + digest + ruleset bytecode + schema |
| Suppressed finding count | โ
| the summary line reports `N suppressed` |
| Cross-file type knowledge | โ ๏ธ limited | annotations, literals and producers in the same file, plus hard-coded APIs |
| Proves the program correct | โ not the goal | 13 static rules, not formal verification |
</details>
---
<a id="metrics" name="metrics"></a>
## ๐ Measured on this machine
Measured on Windows 11 (AMD64) and WSL2 Ubuntu across three interpreters.
| Scenario | 3.12.10 (Windows) | 3.14.4 (WSL2 Ubuntu) | 3.15.0rc2 (Windows) |
|---|---|---|---|
| Scan `src/` (23 files, cold, `--no-cache`) | **642 ms** | **725 ms** | **510 ms** |
| Rescan (warm cache) | **12.3 ms** | **270 ms**\* | **9.9 ms** |
| Test suite (`unittest`, 268 tests) | **6.9 s OK** | **17.3 s OK** | **6.8 s OK** |
| DEFECT / WARNING / ADVICE on `src/` | 0 / 0 / 0 | 0 / 0 / 0 | 0 / 0 / 0 |
\* WSL reads the repo through `/mnt/l` (drvfs), which slows every sqlite round trip; the engine itself is not the slow part.
Engine size (`src/ait`, 23 files) **7,083 lines** ยท Runtime dependencies **0**
Numbers shift with the machine and file sizes; the cold/warm ratio says more than the absolute values.
The code-size badge at the top counts every line of code in the repository as bytes, tests included, so it measures the whole project rather than `src/ait`.
---
## ๐ง How it works
<details>
<summary><b>๐๏ธ Click to expand: pipeline, cache, regression diff and witness</b></summary>
```text
discover โโโบ parse โโโบ scope tree โโโบ call graph โโโบ data flow
โ โ
โโโโโโโโโโโโโโบ 13 rules โโโโโโโโโโโโโโโโโโ
โ
findings โโโโโโโผโโโโโโโบ causation trace
โโโโโโโโบ repro witness
โโโโโโโโบ suggested patch (re-parsed)
```
- **Cache** is sqlite3 (WAL), keyed on `path + size + mtime_ns + BLAKE2b digest + ruleset fingerprint + schema`.
The digest is compared on lookup, so a rewrite that restores the same size and mtime (`git checkout`, `touch -r`, a restored backup)
does not resurrect results for older content.
The ruleset fingerprint hashes the bytecode of every rule, not just its name, so editing a detector invalidates old results.
A corrupt DB file (bad magic header) is discarded and recreated once; if that fails too, the cache is switched off instead of crashing or looping.
- **Regression diff** compares finding counts per `(rule, symbol)`, not line numbers,
so shifted lines do not produce phantom findings.
- **Witness** is generated as text and parsed for syntax only; nothing is executed
(deadlock/recursion demos carry `safe_to_execute: false`).
</details>
---
## ๐งฑ Project structure
<details>
<summary><b>๐๏ธ Click to expand: the full file map</b></summary>
```text
AiT-Test/
โโโ cli.py # pytest-style entrypoint
โโโ mcp.py # JSON-RPC over stdio entrypoint
โโโ pyproject.toml # packaging (license = MIT)
โโโ LICENSE # MIT license text
โโโ .github/workflows/
โ โโโ ci.yml # 4 jobs: test matrix (12) + package + lint + security
โ โโโ codeql.yml # CodeQL, security-and-quality, weekly
โโโ .github/codeql/
โ โโโ codeql-config.yml # query suite + tests/fixtures excluded
โโโ .github/dependabot.yml # SHA-pinned actions, updated weekly
โโโ src/ait/
โ โโโ engine.py # discover / scan / compare_findings / verify_fix
โ โโโ models.py # Diagnostic, Patch, Witness, ScanResult
โ โโโ context.py # guard detection, suppression, patch builders
โ โโโ cache.py # sqlite3 cache + corruption recovery
โ โโโ reporter.py # terminal renderer
โ โโโ slicer.py # ait_slice_context
โ โโโ flow/ # scope tree, call graph, data flow, witness
โ โโโ rules/ # 13 rules
โโโ tests/ # unittest + fixtures
โโโ docs/ # demo.gif, demo.txt, assets/
```
| Layer | File | Responsibility |
| :--- | :--- | :--- |
| Frontend | `cli.py` | flag parsing, rendering, the `--fix` session, exit codes |
| Frontend | `mcp.py` | JSON-RPC framing, tool schema and validation, stdio loop |
| Core | `engine.py` | discover โ parse โ analyze โ cache โ aggregate |
| Core | `context.py` | guard/dominance lookup, suppression, evidence and patch builders |
| Flow | `flow/scope_tree.py` | symbol table, lexical scopes, binding resolution |
| Flow | `flow/call_graph.py` | call sites, lock regions, spawn sites |
| Flow | `flow/data_flow.py` | backward tracing of optional/`None` sources |
| Flow | `flow/witness.py` | minimal repro text and syntax validation |
| Rules | `rules/*.py` | 13 detectors (Z/R/A/M/N/H) |
| Infra | `cache.py` | sqlite3 WAL, digest and ruleset keyed, prune, `--lf` index |
| Infra | `models.py` | immutable Diagnostic/Patch/Witness, exit codes, SCHEMA_VERSION |
| CI | `.github/workflows/ci.yml` | test matrix, self-scan, stdlib-only run, integration tests |
| CI | `.github/workflows/codeql.yml` | CodeQL security-and-quality, `tests/fixtures` excluded |
| CI | `tests/test_cli_integration.py` | drives the real `cli.py` / `mcp.py` through subprocess and asserts exit codes and JSON |
</details>
---
<a id="tests" name="tests"></a>
## ๐งช Tests
No pytest required:
```bash
python -m unittest discover -s tests -v
```
The fixtures come in pairs: `bad_*.py` (must be caught) and `clean_*.py` (must stay silent).
The suite also asserts that the engine imports nothing outside the standard library (`python -S` still runs it).
```text
Ran 268 tests in ~7s
OK
```
<details>
<summary><b>๐ฅ๏ธ Click to expand: results on 8 interpreters and the environment table</b></summary>
Confirmed on 8 interpreters (7 on Windows 11, 1 on WSL2 Ubuntu): every one returns 268 tests OK, a clean `src/`, and exit 0.
| Interpreter | Source |
|---|---|
| CPython 3.10.21 | uv-managed (Windows 11) |
| CPython 3.11.16 | uv-managed (Windows 11) |
| CPython 3.12.10 | Windows 11 (system) |
| CPython 3.12.14 | `L:\.Python312` |
| CPython 3.13.15 | `L:\.Python313` |
| CPython 3.14.7 | `L:\.Python314` |
| CPython 3.15.0rc2 | `L:\.Python315` |
| CPython 3.14.4 | WSL2 Ubuntu |
```text
3.10.21 โ Ran 268 tests in 7.152s OK
3.11.16 โ Ran 268 tests in 6.990s OK
3.12.10 โ Ran 268 tests in 6.928s OK
3.12.14 โ Ran 268 tests in 6.698s OK
3.13.15 โ Ran 268 tests in 6.724s OK
3.14.4 (WSL2 Ubuntu) โ Ran 268 tests in 17.332s OK
3.14.7 โ Ran 268 tests in 7.319s OK
3.15.0rc2 โ Ran 268 tests in 6.769s OK
```
All 8 scan the 23 files under `src/` without a finding and exit 0.
`tests/fixtures` exits 1 as it should (DEFECT 11 ยท WARNING 4 ยท ADVICE 1).
On 3.15 the suite also passes with `-W error::DeprecationWarning` for all 268 tests,
which means the engine does not touch APIs scheduled for removal, and `python -S` (no site) scans normally.
</details>
---
<a id="ci" name="ci"></a>
## ๐ CI (GitHub Actions)
CI runs on GitHub Actions with every action pinned to a **commit SHA** (not a floating tag), and Dependabot updates them weekly.
CodeQL is a workflow of its own (`codeql.yml`) on the same pinning policy.
<details>
<summary><b>๐งช Click to expand: the <code>test</code> job across 12 environments</b></summary>
| Step / test | What it checks | Pass condition |
|---|---|---|
| `python -m unittest discover -s tests` | 268 tests on that interpreter | must be `OK` |
| `-W error::DeprecationWarning` (3.15 only) | whether it uses APIs scheduled for removal | must be `OK` |
| `python cli.py src --no-cache` | the engine scanning itself | exit 0 ยท DEFECT/WARNING/ADVICE = 0 |
| `python -S cli.py src --no-cache` | no site-packages at all | exit 0, so nothing third-party is required |
| `tests/test_cli_integration.py` (scans `tests/fixtures` through `cli.py`) | a corpus that is meant to fail | exit **1** and a `DEFECT` in the output |
| `tests/test_mcp_protocol.py` (`mcp.py` over stdio) | real JSON-RPC (`initialize` โ `tools/call` โ `shutdown`) | `isError:false` ยท 0 findings ยท `shutdown` answered |
`tests/test_cli_integration.py` and `tests/test_mcp_protocol.py` call `cli.py` / `mcp.py` through `subprocess` the way a user would, instead of importing internal functions.
That catches regressions at the entrypoint level: a wrong exit code, logs leaking into stdout, a broken protocol frame.
</details>
<details>
<summary><b>๐ฆ Click to expand: the <code>package</code> / <code>lint</code> / <code>security</code> jobs</b></summary>
| Job | Tool | What it checks | Pass condition |
|---|---|---|---|
| `package` | `pip wheel` + a clean venv | builds the wheel, installs it with `--no-index --no-deps`, runs `ait --version`, `ait sample.py` and `ait-mcp` outside the source tree | exit 0 for every command |
| `package` | `importlib.metadata` | metadata must have no `Requires-Dist` and all 5 modules must import | `requires == None` |
| `lint` | **Ruff 0.17.2** | `E4/E7/E9/F` (undefined names, unused imports, syntax) | 0 findings |
| `lint` | **Bandit 1.9.4** | medium and above in `src`, `cli.py`, `mcp.py` | 0 findings |
| `lint` | `compileall` | every module compiles | exit 0 |
| `lint` | **actionlint 2.2.0** | workflow syntax and expressions | exit 0 |
| `security` | **OSV-Scanner 2.7.0** | the dependency surface against the OSV database | `No issues found` |
| `security` | shell guard | any `requirements*.txt` / `uv.lock` / `poetry.lock` must stop the run for a real scan | no lockfile found |
Ruff found something the engine itself cannot: `F821 Undefined name 'List'` in `rules/registry.py`,
hidden under `from __future__ import annotations` so it never failed at runtime, but ready to break the moment anyone calls `get_type_hints`. Fixed.
Bandit reports two `B101 assert` sites (`cache.py`, `cli.py`) at **low severity**.
Both asserts are internal invariants after type narrowing, so the threshold is `-ll` (medium and above) and the low findings stay by design.
</details>
<details>
<summary><b>๐ Click to expand: CodeQL, dependency-review and the OSV badge</b></summary>
**CodeQL** runs in its own workflow (`.github/workflows/codeql.yml`) on every push to `main`, on a weekly schedule and on demand.
It uses the `security-and-quality` suite, and `github/codeql-action` is pinned to v4.39.1 by commit SHA like every other action in the repo.
`security-events: write` is granted to the analysis job only.
The first analysis reported 9 results. All nine were fixed rather than dismissed: three empty `except ...: pass` blocks became `contextlib.suppress(...)`,
one duplicate import, one unused import and two unused local bindings were cleaned up,
and the last two pointed at `tests/fixtures`, a corpus that is defective on purpose.
That directory is now excluded through `.github/codeql/codeql-config.yml` instead of being dismissed alert by alert.
**dependency-review** stays off. It needs the dependency graph API, and this package declares no dependencies at all, so it would review an empty set.
**The OSV badge is not on this page.** OSV publishes no official badge, and a static "0 issues" shield would keep saying 0 after the first advisory,
so the status lives in the `security` job of CI where it is actually refreshed. The CodeQL shield at the top of the page is different: it is GitHub's live status for that workflow.
</details>
---
## โ ๏ธ Limits
> [!WARNING]
> Read this before relying on it. The tool answers a narrow set of questions well, and does not replace a type checker, a test suite or code review.
- It is **static analysis**: it never runs the code, so it cannot know real runtime values.
- There is no full type inference, so it knows annotations, literals, producers in the same file, and hard-coded APIs.
- Cross-module: an optional value that comes from a file outside the scan is invisible.
- `AIT-R01` reports only when concurrency evidence exists; a project that spawns threads through an unusual abstraction may slip past.
- A witness is "an example that triggers the symptom", not a test case ready for CI.
- A patch is a syntactic suggestion and may not match your architecture (whether to `raise` or `return` depends on context).
If false positives worry you more than false negatives, start with `--maxfail 5` and go one at a time.
---
<a id="credits" name="credits"></a>
## ๐
Credits and titles
<p align="center"><b>xTanTHaix</b><br><i>Project Architect ยท Maintainer ยท Copyright holder of AiT-test</i></p>
<details>
<summary><b>๐๏ธ Click to expand: titles tied to actual work in this project</b></summary>
| Title | Basis |
| :--- | :--- |
| ๐๏ธ Architect of the 13 Rules | designed the 13-rule set (Z/R/A/M/N/H) and the "a DEFECT needs evidence or it drops to WARNING" policy |
| ๐งช Eight-Interpreter Verifier | confirmed 268 tests on CPython 3.10 โ 3.15 across Windows 11 and WSL2 Ubuntu |
| ๐ก๏ธ Zero-Dependency Keeper | 23 engine files, 7,083 lines, no runtime dependency and fully offline |
| ๐ค MCP Toolsmith | JSON-RPC 2.0 contract for three tools with validation, `structuredContent` and a lifecycle that really closes |
| ๐ CI Gatekeeper | 4 jobs / 15 runs: test matrix on 12 environments + package + lint (ruff, bandit, actionlint) + OSV, plus a CodeQL workflow sitting at 0 open alerts |
</details>
---
<p align="center"><a href="https://ko-fi.com/xtanthaix"><img alt="Ko-fi: buy me a coffee" width="46%" src="https://img.shields.io/badge/Ko--fi-buy%20me%20a%20coffee-FF5E5B?style=flat-square&logo=kofi&logoColor=white"></a></p>
<p align="center"><i>Written and maintained by <a href="https://ko-fi.com/xtanthaix">xTanTHaix</a> ยท if AiT-test saved you time, you can buy the developer a coffee at <a href="https://ko-fi.com/xtanthaix">ko-fi.com/xtanthaix</a> โ</i></p>
---
## ๐ License
MIT. The license expression is declared in `pyproject.toml` (`license = "MIT"`), and the full text is in [`LICENSE`](LICENSE).
---
<p align="center"><i>Static invariants, causation traces and verified patches, with zero runtime dependencies.</i></p>
TDQS
Scored across 3 tools
Each tool has a clearly distinct role: whole-file/directory review, single-symbol extraction, and post-patch AST verification. The explicit USE WHEN / DO NOT USE clauses directly prevent the main overlaps, leaving little room for misselection.
The `ait_` prefix is consistent across all three tools, and the action suffixes are readable. The pattern is slightly uneven (`review` vs. `slice_context` vs. `verify_fix`), but the naming remains predictable enough for an agent.
Three tools are appropriate for a focused static-analysis workflow: audit, inspect one symbol, and verify a candidate fix. Each tool earns its place without redundancy or unnecessary surface area.
The core review-inspect-verify lifecycle is covered. Minor gaps remain, such as a tool to list rules or locate a symbol by name, but the descriptions acknowledge that symbol lookup is out of scope and workable around.