Skip to main content
Glama
README.md
<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

A4.6/5.0

Scored across 3 tools

Disambiguation5/5

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.

Naming Consistency4/5

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.

Tool Count5/5

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.

Completeness4/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues