Metadata-Version: 2.5
Name: wiringguard
Version: 0.1.0
Summary: Prove the guard you added is still called: AST-checked call-site invariants for Python, because the string stays in the file after the call is gone.
Project-URL: Homepage, https://github.com/luandv92/wiringguard
Project-URL: Issues, https://github.com/luandv92/wiringguard/issues
Project-URL: Changelog, https://github.com/luandv92/wiringguard/blob/main/CHANGELOG.md
Author: luandv92
License: MIT License
        
        Copyright (c) 2026 luandv92
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: architecture,ast,ci,code-quality,fitness-function,guard,linter,regression,static-analysis
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.9
Requires-Dist: tomli>=1.1.0; python_version < '3.11'
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == 'dev'
Description-Content-Type: text/markdown

# wiringguard

[![ci](https://github.com/luandv92/wiringguard/actions/workflows/ci.yml/badge.svg)](https://github.com/luandv92/wiringguard/actions/workflows/ci.yml)
[![pypi](https://img.shields.io/pypi/v/wiringguard)](https://pypi.org/project/wiringguard/)
[![python](https://img.shields.io/pypi/pyversions/wiringguard)](https://pypi.org/project/wiringguard/)

**Prove the guard you added is still called.**

You added a permission check. An audit-log call. A thumbnail step that stops a video going
out with whatever frame the platform picks. Six months later a refactor leaves the import in
place and drops the call. Every test still passes — the tests exercise the happy path, and
the guard was never the thing under test.

```console
pip install wiringguard
```

```toml
# pyproject.toml
[[tool.wiringguard.rules]]
name     = "every upload sets a custom cover"
kind     = "must-call"
where    = "uploaders/*.py"
function = "upload_*"
call     = "ensure_cover"
```

```console
$ wiringguard
uploaders/youtube.py:205: [every upload sets a custom cover]
    upload_video() no longer calls ensure_cover() — the name still appears in the
    file, but only as an import or a mention, never as a call

1 rule(s) | 1 violation(s)
```

## Why not grep

Because grep passes. That output above is from a real file where the string `ensure_cover`
is still present — in a docstring — while the call itself had been replaced. A watchdog
built on `if "ensure_cover" in source` reports green over code that no longer does the thing,
and it reports green forever.

The insidious version is even quieter: the refactor keeps `from covers import ensure_cover`
at the top and removes the one call. Now the symbol is genuinely imported, your editor shows
no warning, the linter shows no unused import if something else touches it, and every text
search finds it.

`wiringguard` parses the file. A call is a call.

## The two kinds of rule

### `must-call` — the guard is still invoked
```toml
[[tool.wiringguard.rules]]
name     = "writes go through the audit log"
kind     = "must-call"
where    = "app/repository/*.py"
function = "save_*"
call     = "audit.record"
```
`function` accepts a glob, or `"*"` for every function in the matched files. If the function
was **renamed or deleted**, that is reported too — a rule quietly guarding nothing is the
failure this tool exists to end, not a pass.

### `only-in` — nobody grew a second path
```toml
[[tool.wiringguard.rules]]
name  = "one place may publish"
kind  = "only-in"
call  = "client.videos.insert"
files = ["uploaders/youtube.py", "uploaders/legacy_*.py"]
```
A second module that quietly grew its own copy of a privileged operation is how an ungated
side door gets born. This finds it.

Rules match on the **suffix** of a dotted call path, with intermediate calls collapsed:
`client.videos().insert(...)` matches `videos.insert` or `client.videos.insert`, and does
**not** match `db.insert`. Name as much of the path as you need to be unmistakable.

## In CI

```yaml
- run: pip install wiringguard
- run: wiringguard
```

| exit | meaning |
|---|---|
| `0` | every rule still holds |
| `1` | something came unwired |
| `2` | could not check — no config, an empty ruleset, a bad path |

`2` is deliberately separate. A tool whose whole subject is *"a check that silently stopped
checking"* must never let *"I could not look"* be read as *"I looked and it was fine"*. For
the same reason, an empty ruleset is refused rather than exiting 0, and files that fail to
parse are reported instead of skipped.

## How it compares

| | scope |
|---|---|
| `import-linter`, `pytest-archon` | which modules may **import** which |
| `semgrep` | general pattern matching, one pattern at a time |
| `wiringguard` | which functions must **call** what, and who may call it at all |

Import contracts do not tell you whether the function is actually invoked. This is the
call-site half of the same idea, in about a hundred lines of AST.

## Configuration

`wiringguard.toml` at the project root, or a `[tool.wiringguard]` section in `pyproject.toml`.
Vendored directories (`.venv`, `site-packages`, `node_modules`, caches, `build`, `dist`) are
skipped. Sources are read as `utf-8-sig`, so a byte-order mark does not turn a check into a
silent skip.

## Requirements

Python 3.9+. No dependencies on 3.11+ (`tomli` below that). Linux, macOS and Windows.

## License

MIT
