failroute 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 feiiiiii5
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,205 @@
1
+ Metadata-Version: 2.4
2
+ Name: failroute
3
+ Version: 0.3.0
4
+ Summary: Static detection of failure-routing anti-patterns in Python
5
+ Author: fc
6
+ License: MIT
7
+ Keywords: static-analysis,exception-handling,code-quality,llm,reliability
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Quality Assurance
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest>=7; extra == "test"
22
+ Dynamic: license-file
23
+
24
+ # failroute
25
+
26
+ Static detection of **failure-routing** anti-patterns in Python: the practice of
27
+ converting an underlying failure into a success-like outcome at the wrong layer.
28
+
29
+ Failure-routing is the root cause behind some of the most insidious correctness
30
+ bugs in real LLM/eval/agent codebases:
31
+
32
+ ```python
33
+ # Before — a judge API outage becomes a perfect "0.0 score" with no way to tell
34
+ try:
35
+ score = await llm_judge(prompt)
36
+ except Exception:
37
+ return 0.0 # ← silent fallback: failure looks like a legitimate low score
38
+
39
+ # After — the failure propagates; callers can route it to the right outcome
40
+ return await llm_judge(prompt)
41
+ ```
42
+
43
+ ## What it detects
44
+
45
+ | Mode | Pattern |
46
+ | --- | --- |
47
+ | `no-action` | `except ...: pass` — the exception is discarded, callers never learn |
48
+ | `silent-fallback` | handler returns/assigns a constant (`None`, `0`, `0.0`, `False`, `[]`, …) without re-raising |
49
+ | `masked-exception` | **catch-all** handler re-raises conditionally yet also falls through to a success-looking return |
50
+ | `name-shadowing` | `except E as e:` whose body rebinds `e` — Python deletes the binding at handler exit, so later uses raise `NameError` |
51
+
52
+ Findings are emitted as `file:line: mode: message`, or as JSON for CI.
53
+
54
+ ### Logging exemption (two tiers)
55
+
56
+ A handler that *records* the failure is informational, not silent — but what
57
+ counts as a record depends on how wide the handler is:
58
+
59
+ - **Catch-all handlers** (`except:` / `except Exception:`) must log at a
60
+ severity worth reading (`warning`+). A `debug` line or a bare `print(...)`
61
+ does not survive production triage, so it does not exempt.
62
+ - **Typed handlers** name an anticipated failure mode; recording it at *any*
63
+ level (even `logger.info`) is enough.
64
+
65
+ ## Usage
66
+
67
+ ```console
68
+ $ failroute path/to/file.py
69
+ $ failroute path/to/dir # recursive
70
+ $ failroute --repo . # skip .git/.venv/build/...
71
+ $ failroute --repo . --exclude tests/corpus # repeatable path exclusions
72
+ $ failroute --json --repo . | jq 'select(.mode=="silent-fallback")'
73
+ $ failroute --format sarif --output results.sarif --repo . # code scanning
74
+ $ failroute --threshold 5 # exit 1 when more than 5 findings
75
+ $ python -m failroute . # module form (no console script needed)
76
+ ```
77
+
78
+ Exit codes: `0` clean, `1` findings above threshold, `2` usage error.
79
+
80
+ ### Output formats
81
+
82
+ ```console
83
+ $ failroute --format text path/ # default: file:line: mode: message
84
+ $ failroute --format json path/ # one JSON object per finding
85
+ $ failroute --format sarif --output scan.sarif path/ # SARIF 2.1.0
86
+ ```
87
+
88
+ SARIF output plugs straight into [GitHub code scanning](
89
+ https://docs.github.com/en/code-security/code-scanning) via the
90
+ `upload-sarif` action, so findings appear inline on pull requests:
91
+
92
+ ```yaml
93
+ - run: failroute --format sarif --output results.sarif --repo .
94
+ - uses: github/codeql-action/upload-sarif@v3
95
+ with:
96
+ sarif_file: results.sarif
97
+ ```
98
+
99
+ Or use the bundled composite action, which installs failroute, scans, and
100
+ uploads SARIF in one step:
101
+
102
+ ```yaml
103
+ - uses: feiiiiii5/failroute/action@main
104
+ with:
105
+ path: src
106
+ exclude: tests/corpus fixtures
107
+ threshold: "0"
108
+ ```
109
+
110
+ Severity mapping: `silent-fallback` → `error`, `no-action` and
111
+ `masked-exception` → `warning`.
112
+
113
+ ### Suppressing findings
114
+
115
+ Reviewed-and-accepted handlers can be opted out with a line marker (the
116
+ scanner honors both):
117
+
118
+ ```python
119
+ try:
120
+ return best_effort()
121
+ except Exception: # failroute: ignore - documented fallback semantics
122
+ return None
123
+ ```
124
+
125
+ `# pragma: no cover` markers are honored as well (explicitly defensive code).
126
+
127
+ ## Examples that trip it
128
+
129
+ ```python
130
+ def classify(text): # no-action
131
+ try:
132
+ return model.predict(text)
133
+ except Exception:
134
+ pass # 💥 swallowed
135
+
136
+ def score(prompt): # silent-fallback
137
+ try:
138
+ return judge(prompt)
139
+ except Exception:
140
+ return 0.0 # 💥 outage == "0.0 score"
141
+
142
+ def fetch(url): # silent-fallback (assign)
143
+ data = None
144
+ try:
145
+ data = download(url)
146
+ except Exception:
147
+ data = {"items": []} # 💥 error looks like an empty result
148
+ return data
149
+ ```
150
+
151
+ ## What it does *not* flag (by design)
152
+
153
+ * `except KeyboardInterrupt` / `except SystemExit` — normally intentional.
154
+ * Handlers that re-raise unconditionally without a fallback.
155
+ * `except` bodies that log at `warning`/`error` **and** re-raise — the failure
156
+ still propagates; we only flag the success-looking path.
157
+
158
+ Run `failroute` on its own checkout as a smoke test:
159
+
160
+ ```console
161
+ $ pip install -e .
162
+ $ failroute --repo . # expected: zero findings (self-hosting)
163
+ ```
164
+
165
+ ## Benchmarks & validation
166
+
167
+ All numbers below are reproducible from this checkout; nothing here is
168
+ copy-pasted from a run that cannot be re-executed.
169
+
170
+ ### Labelled corpus (precision / recall)
171
+
172
+ `tests/corpus/` holds 19 hand-labelled exception handlers (10 positives across
173
+ all three modes, 9 negatives covering re-raise, log-and-raise, derived values,
174
+ dead code, opt-out markers, and non-fallback constants). Ground truth lives in
175
+ `tests/corpus/manifest.json` and was written from the *semantics* of each
176
+ fixture, independently of tool output.
177
+
178
+ ```
179
+ corpus v1 TP=10 FP=0 FN=0 TN=9
180
+ precision=1.0 recall=1.0
181
+ ```
182
+
183
+ Re-run: `python tools/benchmark.py` (also enforced by `pytest`).
184
+
185
+ ### What syntactic linters miss
186
+
187
+ Against the source packages of 8 real AI/eval repositories (garak,
188
+ inspect_ai, pydantic-ai, uqlm, trl, smolagents, deepteam, fickling),
189
+ failroute reported **647 findings**; ruff's exception-handling rules
190
+ (`S110` try-except-pass, `S112` try-except-continue) reported **80**, of which
191
+ 70 overlap failroute's `no-action` mode. The remaining **390 findings are
192
+ silent-fallback / masked-exception handlers** -- failures converted into
193
+ success-looking values -- a class syntactic rules cannot express by
194
+ construction.
195
+
196
+ Re-run: `python tools/compare_ruff.py <repo> [<repo> ...]`.
197
+ Results are checked into `bench/`.
198
+
199
+ ## Development
200
+
201
+ ```console
202
+ $ pip install -e ".[test]"
203
+ $ pytest
204
+ $ ruff check .
205
+ ```
@@ -0,0 +1,182 @@
1
+ # failroute
2
+
3
+ Static detection of **failure-routing** anti-patterns in Python: the practice of
4
+ converting an underlying failure into a success-like outcome at the wrong layer.
5
+
6
+ Failure-routing is the root cause behind some of the most insidious correctness
7
+ bugs in real LLM/eval/agent codebases:
8
+
9
+ ```python
10
+ # Before — a judge API outage becomes a perfect "0.0 score" with no way to tell
11
+ try:
12
+ score = await llm_judge(prompt)
13
+ except Exception:
14
+ return 0.0 # ← silent fallback: failure looks like a legitimate low score
15
+
16
+ # After — the failure propagates; callers can route it to the right outcome
17
+ return await llm_judge(prompt)
18
+ ```
19
+
20
+ ## What it detects
21
+
22
+ | Mode | Pattern |
23
+ | --- | --- |
24
+ | `no-action` | `except ...: pass` — the exception is discarded, callers never learn |
25
+ | `silent-fallback` | handler returns/assigns a constant (`None`, `0`, `0.0`, `False`, `[]`, …) without re-raising |
26
+ | `masked-exception` | **catch-all** handler re-raises conditionally yet also falls through to a success-looking return |
27
+ | `name-shadowing` | `except E as e:` whose body rebinds `e` — Python deletes the binding at handler exit, so later uses raise `NameError` |
28
+
29
+ Findings are emitted as `file:line: mode: message`, or as JSON for CI.
30
+
31
+ ### Logging exemption (two tiers)
32
+
33
+ A handler that *records* the failure is informational, not silent — but what
34
+ counts as a record depends on how wide the handler is:
35
+
36
+ - **Catch-all handlers** (`except:` / `except Exception:`) must log at a
37
+ severity worth reading (`warning`+). A `debug` line or a bare `print(...)`
38
+ does not survive production triage, so it does not exempt.
39
+ - **Typed handlers** name an anticipated failure mode; recording it at *any*
40
+ level (even `logger.info`) is enough.
41
+
42
+ ## Usage
43
+
44
+ ```console
45
+ $ failroute path/to/file.py
46
+ $ failroute path/to/dir # recursive
47
+ $ failroute --repo . # skip .git/.venv/build/...
48
+ $ failroute --repo . --exclude tests/corpus # repeatable path exclusions
49
+ $ failroute --json --repo . | jq 'select(.mode=="silent-fallback")'
50
+ $ failroute --format sarif --output results.sarif --repo . # code scanning
51
+ $ failroute --threshold 5 # exit 1 when more than 5 findings
52
+ $ python -m failroute . # module form (no console script needed)
53
+ ```
54
+
55
+ Exit codes: `0` clean, `1` findings above threshold, `2` usage error.
56
+
57
+ ### Output formats
58
+
59
+ ```console
60
+ $ failroute --format text path/ # default: file:line: mode: message
61
+ $ failroute --format json path/ # one JSON object per finding
62
+ $ failroute --format sarif --output scan.sarif path/ # SARIF 2.1.0
63
+ ```
64
+
65
+ SARIF output plugs straight into [GitHub code scanning](
66
+ https://docs.github.com/en/code-security/code-scanning) via the
67
+ `upload-sarif` action, so findings appear inline on pull requests:
68
+
69
+ ```yaml
70
+ - run: failroute --format sarif --output results.sarif --repo .
71
+ - uses: github/codeql-action/upload-sarif@v3
72
+ with:
73
+ sarif_file: results.sarif
74
+ ```
75
+
76
+ Or use the bundled composite action, which installs failroute, scans, and
77
+ uploads SARIF in one step:
78
+
79
+ ```yaml
80
+ - uses: feiiiiii5/failroute/action@main
81
+ with:
82
+ path: src
83
+ exclude: tests/corpus fixtures
84
+ threshold: "0"
85
+ ```
86
+
87
+ Severity mapping: `silent-fallback` → `error`, `no-action` and
88
+ `masked-exception` → `warning`.
89
+
90
+ ### Suppressing findings
91
+
92
+ Reviewed-and-accepted handlers can be opted out with a line marker (the
93
+ scanner honors both):
94
+
95
+ ```python
96
+ try:
97
+ return best_effort()
98
+ except Exception: # failroute: ignore - documented fallback semantics
99
+ return None
100
+ ```
101
+
102
+ `# pragma: no cover` markers are honored as well (explicitly defensive code).
103
+
104
+ ## Examples that trip it
105
+
106
+ ```python
107
+ def classify(text): # no-action
108
+ try:
109
+ return model.predict(text)
110
+ except Exception:
111
+ pass # 💥 swallowed
112
+
113
+ def score(prompt): # silent-fallback
114
+ try:
115
+ return judge(prompt)
116
+ except Exception:
117
+ return 0.0 # 💥 outage == "0.0 score"
118
+
119
+ def fetch(url): # silent-fallback (assign)
120
+ data = None
121
+ try:
122
+ data = download(url)
123
+ except Exception:
124
+ data = {"items": []} # 💥 error looks like an empty result
125
+ return data
126
+ ```
127
+
128
+ ## What it does *not* flag (by design)
129
+
130
+ * `except KeyboardInterrupt` / `except SystemExit` — normally intentional.
131
+ * Handlers that re-raise unconditionally without a fallback.
132
+ * `except` bodies that log at `warning`/`error` **and** re-raise — the failure
133
+ still propagates; we only flag the success-looking path.
134
+
135
+ Run `failroute` on its own checkout as a smoke test:
136
+
137
+ ```console
138
+ $ pip install -e .
139
+ $ failroute --repo . # expected: zero findings (self-hosting)
140
+ ```
141
+
142
+ ## Benchmarks & validation
143
+
144
+ All numbers below are reproducible from this checkout; nothing here is
145
+ copy-pasted from a run that cannot be re-executed.
146
+
147
+ ### Labelled corpus (precision / recall)
148
+
149
+ `tests/corpus/` holds 19 hand-labelled exception handlers (10 positives across
150
+ all three modes, 9 negatives covering re-raise, log-and-raise, derived values,
151
+ dead code, opt-out markers, and non-fallback constants). Ground truth lives in
152
+ `tests/corpus/manifest.json` and was written from the *semantics* of each
153
+ fixture, independently of tool output.
154
+
155
+ ```
156
+ corpus v1 TP=10 FP=0 FN=0 TN=9
157
+ precision=1.0 recall=1.0
158
+ ```
159
+
160
+ Re-run: `python tools/benchmark.py` (also enforced by `pytest`).
161
+
162
+ ### What syntactic linters miss
163
+
164
+ Against the source packages of 8 real AI/eval repositories (garak,
165
+ inspect_ai, pydantic-ai, uqlm, trl, smolagents, deepteam, fickling),
166
+ failroute reported **647 findings**; ruff's exception-handling rules
167
+ (`S110` try-except-pass, `S112` try-except-continue) reported **80**, of which
168
+ 70 overlap failroute's `no-action` mode. The remaining **390 findings are
169
+ silent-fallback / masked-exception handlers** -- failures converted into
170
+ success-looking values -- a class syntactic rules cannot express by
171
+ construction.
172
+
173
+ Re-run: `python tools/compare_ruff.py <repo> [<repo> ...]`.
174
+ Results are checked into `bench/`.
175
+
176
+ ## Development
177
+
178
+ ```console
179
+ $ pip install -e ".[test]"
180
+ $ pytest
181
+ $ ruff check .
182
+ ```
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "failroute"
7
+ version = "0.3.0"
8
+ description = "Static detection of failure-routing anti-patterns in Python"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "fc" }]
13
+ keywords = ["static-analysis", "exception-handling", "code-quality", "llm", "reliability"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Software Development :: Quality Assurance",
24
+ ]
25
+ dependencies = []
26
+
27
+ [project.optional-dependencies]
28
+ test = ["pytest>=7"]
29
+
30
+ [project.scripts]
31
+ failroute = "failroute.cli:main"
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
35
+
36
+ [tool.pytest.ini_options]
37
+ testpaths = ["tests"]
38
+ pythonpath = ["src"]
39
+ addopts = "-q"
40
+
41
+ [tool.ruff]
42
+ target-version = "py39"
43
+ line-length = 110
44
+
45
+ [tool.ruff.lint]
46
+ select = ["E", "F", "I", "B", "UP"]
47
+
48
+ [tool.ruff.lint.per-file-ignores]
49
+ # Corpus fixtures are intentionally synthetic snippets with undefined names.
50
+ "tests/corpus/*" = ["F821", "B017"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,27 @@
1
+ """failroute — static detection of failure-routing anti-patterns in Python.
2
+
3
+ Failure-routing is the practice of converting an underlying failure into a
4
+ *success-like* outcome at the wrong layer: swallowing an exception and
5
+ returning a default truthy/"no error" value, transforming a metric failure
6
+ into a 0.0/False score, or logging-and-continuing where the caller is
7
+ contractually entitled to know the operation failed.
8
+
9
+ This module implements an AST-based scanner that flags those patterns, so
10
+ tests / CI can treat them like the correctness bugs they are.
11
+ """
12
+
13
+ from failroute.analyzer import FailureMode, Finding, scan_path, scan_repo, scan_source
14
+ from failroute.cli import main
15
+ from failroute.sarif import to_sarif, to_sarif_json
16
+
17
+ __all__ = [
18
+ "FailureMode",
19
+ "Finding",
20
+ "scan_source",
21
+ "scan_path",
22
+ "scan_repo",
23
+ "main",
24
+ "to_sarif",
25
+ "to_sarif_json",
26
+ ]
27
+ __version__ = "0.3.0"
@@ -0,0 +1,6 @@
1
+ """Allow ``python -m failroute`` invocation."""
2
+
3
+ from failroute.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())