flarebisect 0.3.1__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,12 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ venv/
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .env
11
+ .DS_Store
12
+ demo/flaky-counter/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tawin Tangsukson
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,192 @@
1
+ Metadata-Version: 2.4
2
+ Name: flarebisect
3
+ Version: 0.3.1
4
+ Summary: git bisect that treats flakiness as a signal, not noise
5
+ Project-URL: Homepage, https://github.com/kaorii-ako/FlareBisect
6
+ Project-URL: Repository, https://github.com/kaorii-ako/FlareBisect
7
+ Project-URL: Issues, https://github.com/kaorii-ako/FlareBisect/issues
8
+ Author: Tawin Tangsukson
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: bisect,cli,debugging,flaky-tests,git,testing
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Debuggers
23
+ Classifier: Topic :: Software Development :: Testing
24
+ Classifier: Topic :: Software Development :: Version Control :: Git
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: anthropic>=0.40
27
+ Requires-Dist: openai>=1.40
28
+ Requires-Dist: rich>=13.7
29
+ Requires-Dist: typer>=0.12
30
+ Provides-Extra: dev
31
+ Requires-Dist: build>=1.2; extra == 'dev'
32
+ Requires-Dist: pytest>=8.0; extra == 'dev'
33
+ Requires-Dist: twine>=5.0; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # FlareBisect ๐Ÿ”ฅ
37
+
38
+ [![PyPI](https://img.shields.io/pypi/v/flarebisect.svg)](https://pypi.org/project/flarebisect/)
39
+ [![CI](https://github.com/kaorii-ako/FlareBisect/actions/workflows/ci.yml/badge.svg)](https://github.com/kaorii-ako/FlareBisect/actions/workflows/ci.yml)
40
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
41
+
42
+ FlareBisect finds the commit that made your tests flaky, not just the one
43
+ that broke them: it bisects on flake-rate drift instead of pass/fail, then
44
+ flares the culprit with an AI-written root cause.
45
+
46
+ `git bisect` gives you confidently wrong answers when the test you're
47
+ bisecting on is flaky โ€” it treats every run as a clean pass/fail signal, so
48
+ a test that was already unstable *before* you started bisecting can point
49
+ straight at an innocent commit.
50
+
51
+ `flarebisect` bisects on **flake rate**, not pass/fail. It runs each
52
+ candidate commit's test N times in parallel, computes a failure rate, and
53
+ finds the commit where that rate jumps โ€” then asks an LLM to explain, in
54
+ plain English, whether the commit broke the test cleanly or made it flakier.
55
+
56
+ ```
57
+ 12 commits in range ยท 20 runs per commit ยท parallel worktrees
58
+
59
+ commit flake rate result status
60
+ a1b2c3 5/5 good
61
+ 4f9e21 5/5 stable
62
+ 7d3aa0 โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 4/5 wobbling
63
+ 9c1f88 โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 1/5 flare
64
+ HEAD โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 1/5 bad
65
+
66
+ โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
67
+ โ”‚ โœฆ culprit found ยท commit 9c1f88 โ€Šโ€Šโ€Šโ€Šโ€Š โ”‚
68
+ โ”‚ โ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Š โ”‚
69
+ โ”‚ flake rate jumped 20% โ†’ 80% at this commit โ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Š โ”‚
70
+ โ”‚ "add request counter for rate limiting" โ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Š โ”‚
71
+ โ”‚ โ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Š โ”‚
72
+ โ”‚ ๐Ÿ’ก likely cause โ€” shared counter incremented without a lock.โ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ”‚
73
+ โ”‚ concurrent test workers race on the same variable. โ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Š โ”‚
74
+ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
75
+ โฑ 8.4s โšก 5 checked โš™ 4 parallel workers
76
+ ```
77
+
78
+ ## Install
79
+
80
+ ```bash
81
+ pip install flarebisect
82
+ ```
83
+
84
+ For local development:
85
+
86
+ ```bash
87
+ pip install -e ".[dev]"
88
+ pytest -q
89
+ ```
90
+
91
+ Works on Linux, macOS, and Windows โ€” the only external dependency is `git`
92
+ on your `PATH`. Every candidate commit runs in its own `git worktree` under a
93
+ temp directory; your actual working tree and index are never touched.
94
+
95
+ ## AI provider setup
96
+
97
+ The root-cause explanation step works with Claude, OpenAI, Gemini, or a
98
+ local model โ€” anything speaking an OpenAI-compatible chat API (Ollama, LM
99
+ Studio, llama.cpp server, vLLM, and similar all qualify).
100
+
101
+ ```bash
102
+ # cloud
103
+ flarebisect config set-key anthropic sk-ant-...
104
+ flarebisect config set-key openai sk-...
105
+ flarebisect config set-key google AI...
106
+
107
+ # local, no key needed - just have Ollama running
108
+ flarebisect config use ollama
109
+ flarebisect config set-model ollama llama3.1
110
+
111
+ # or let flarebisect pick a model sized to your GPU and pull it for you
112
+ flarebisect models detect # shows detected GPU/VRAM + recommended model
113
+ flarebisect models pull # downloads it and sets it active (run with no args)
114
+
115
+ # any other OpenAI-compatible endpoint
116
+ flarebisect config use custom
117
+ flarebisect config set-base-url custom http://localhost:8080/v1
118
+ flarebisect config set-key custom sk-local
119
+ ```
120
+
121
+ Run `flarebisect config` with no arguments for a guided setup wizard โ€” for
122
+ Ollama it detects your GPU/VRAM, prefills a right-sized model, and offers to
123
+ pull it on the spot.
124
+
125
+ `flarebisect config show` lists the active provider and stored settings
126
+ (keys are masked). Config lives in a JSON file under the OS config dir
127
+ (`~/.config/flarebisect/config.json` on Linux/macOS, `%APPDATA%` on Windows),
128
+ written with owner-only permissions.
129
+
130
+ Per-run overrides skip the config file entirely:
131
+
132
+ ```bash
133
+ flarebisect run ... --provider openai --model gpt-4o-mini --api-key sk-...
134
+ flarebisect run ... --provider ollama --base-url http://localhost:11434/v1
135
+ ```
136
+
137
+ Env vars (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_API_KEY`) work too,
138
+ as a fallback under whatever's in the config file.
139
+
140
+ ## Usage
141
+
142
+ ```bash
143
+ flarebisect run \
144
+ --repo /path/to/repo \
145
+ --good <known-good-sha> \
146
+ --bad <known-bad-sha> \
147
+ --test "pytest -k my_flaky_test" \
148
+ --runs 20 \
149
+ --threshold 0.3
150
+ ```
151
+
152
+ - `--runs` โ€” test executions per commit (default 20), run in parallel
153
+ (capped at your core count so it doesn't oversubscribe). Lower counts are
154
+ faster but noisier โ€” a low sample can misattribute the culprit near the
155
+ threshold boundary.
156
+ - `--threshold` โ€” flake-rate jump (0-1) vs. the `good` baseline required to
157
+ call a commit the culprit (default 0.3).
158
+ - `--no-explain` โ€” skip the LLM root-cause call entirely (fully offline).
159
+
160
+ ## Demo
161
+
162
+ See [`demo/`](demo/) for a self-contained, seeded-flaky-bug repo you can
163
+ bisect against with no network access required for the bisection itself.
164
+
165
+ ## How it works
166
+
167
+ 1. Measure the flake rate at `good` and `bad` as baselines.
168
+ 2. Binary-search the commit range between them.
169
+ 3. At each candidate, run the test `N` times in an isolated worktree and
170
+ compute `failed / N`.
171
+ 4. The culprit is the first commit whose flake rate jumps by `>= threshold`
172
+ over the `good` baseline.
173
+ 5. Verdict: **clean break** (rate goes ~0% โ†’ ~100%) vs. **flakiness
174
+ regression** (rate goes ~0% โ†’ somewhere in between).
175
+ 6. The configured LLM gets the culprit's diff + the flake-rate evidence and
176
+ returns a short root-cause read (race condition, shared mutable state,
177
+ timing assumption, etc.).
178
+
179
+ Binary search assumes the flake rate is roughly monotonic across the range,
180
+ same as ordinary `git bisect` assumes pass/fail is. At low `--runs` counts
181
+ a noisy sample can occasionally violate that near the threshold boundary โ€”
182
+ bump `--runs` if a result looks off.
183
+
184
+ ## Releasing
185
+
186
+ Version is single-sourced from `src/flarebisect/__init__.py`. Bump it, commit,
187
+ tag (`vX.Y.Z`), and cut a GitHub Release โ€” `.github/workflows/publish.yml`
188
+ builds and publishes to PyPI via trusted publishing (OIDC, no stored token).
189
+
190
+ ## License
191
+
192
+ MIT
@@ -0,0 +1,157 @@
1
+ # FlareBisect ๐Ÿ”ฅ
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/flarebisect.svg)](https://pypi.org/project/flarebisect/)
4
+ [![CI](https://github.com/kaorii-ako/FlareBisect/actions/workflows/ci.yml/badge.svg)](https://github.com/kaorii-ako/FlareBisect/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+
7
+ FlareBisect finds the commit that made your tests flaky, not just the one
8
+ that broke them: it bisects on flake-rate drift instead of pass/fail, then
9
+ flares the culprit with an AI-written root cause.
10
+
11
+ `git bisect` gives you confidently wrong answers when the test you're
12
+ bisecting on is flaky โ€” it treats every run as a clean pass/fail signal, so
13
+ a test that was already unstable *before* you started bisecting can point
14
+ straight at an innocent commit.
15
+
16
+ `flarebisect` bisects on **flake rate**, not pass/fail. It runs each
17
+ candidate commit's test N times in parallel, computes a failure rate, and
18
+ finds the commit where that rate jumps โ€” then asks an LLM to explain, in
19
+ plain English, whether the commit broke the test cleanly or made it flakier.
20
+
21
+ ```
22
+ 12 commits in range ยท 20 runs per commit ยท parallel worktrees
23
+
24
+ commit flake rate result status
25
+ a1b2c3 5/5 good
26
+ 4f9e21 5/5 stable
27
+ 7d3aa0 โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 4/5 wobbling
28
+ 9c1f88 โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 1/5 flare
29
+ HEAD โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 1/5 bad
30
+
31
+ โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
32
+ โ”‚ โœฆ culprit found ยท commit 9c1f88 โ€Šโ€Šโ€Šโ€Šโ€Š โ”‚
33
+ โ”‚ โ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Š โ”‚
34
+ โ”‚ flake rate jumped 20% โ†’ 80% at this commit โ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Š โ”‚
35
+ โ”‚ "add request counter for rate limiting" โ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Š โ”‚
36
+ โ”‚ โ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Š โ”‚
37
+ โ”‚ ๐Ÿ’ก likely cause โ€” shared counter incremented without a lock.โ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ”‚
38
+ โ”‚ concurrent test workers race on the same variable. โ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Šโ€Š โ”‚
39
+ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
40
+ โฑ 8.4s โšก 5 checked โš™ 4 parallel workers
41
+ ```
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pip install flarebisect
47
+ ```
48
+
49
+ For local development:
50
+
51
+ ```bash
52
+ pip install -e ".[dev]"
53
+ pytest -q
54
+ ```
55
+
56
+ Works on Linux, macOS, and Windows โ€” the only external dependency is `git`
57
+ on your `PATH`. Every candidate commit runs in its own `git worktree` under a
58
+ temp directory; your actual working tree and index are never touched.
59
+
60
+ ## AI provider setup
61
+
62
+ The root-cause explanation step works with Claude, OpenAI, Gemini, or a
63
+ local model โ€” anything speaking an OpenAI-compatible chat API (Ollama, LM
64
+ Studio, llama.cpp server, vLLM, and similar all qualify).
65
+
66
+ ```bash
67
+ # cloud
68
+ flarebisect config set-key anthropic sk-ant-...
69
+ flarebisect config set-key openai sk-...
70
+ flarebisect config set-key google AI...
71
+
72
+ # local, no key needed - just have Ollama running
73
+ flarebisect config use ollama
74
+ flarebisect config set-model ollama llama3.1
75
+
76
+ # or let flarebisect pick a model sized to your GPU and pull it for you
77
+ flarebisect models detect # shows detected GPU/VRAM + recommended model
78
+ flarebisect models pull # downloads it and sets it active (run with no args)
79
+
80
+ # any other OpenAI-compatible endpoint
81
+ flarebisect config use custom
82
+ flarebisect config set-base-url custom http://localhost:8080/v1
83
+ flarebisect config set-key custom sk-local
84
+ ```
85
+
86
+ Run `flarebisect config` with no arguments for a guided setup wizard โ€” for
87
+ Ollama it detects your GPU/VRAM, prefills a right-sized model, and offers to
88
+ pull it on the spot.
89
+
90
+ `flarebisect config show` lists the active provider and stored settings
91
+ (keys are masked). Config lives in a JSON file under the OS config dir
92
+ (`~/.config/flarebisect/config.json` on Linux/macOS, `%APPDATA%` on Windows),
93
+ written with owner-only permissions.
94
+
95
+ Per-run overrides skip the config file entirely:
96
+
97
+ ```bash
98
+ flarebisect run ... --provider openai --model gpt-4o-mini --api-key sk-...
99
+ flarebisect run ... --provider ollama --base-url http://localhost:11434/v1
100
+ ```
101
+
102
+ Env vars (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_API_KEY`) work too,
103
+ as a fallback under whatever's in the config file.
104
+
105
+ ## Usage
106
+
107
+ ```bash
108
+ flarebisect run \
109
+ --repo /path/to/repo \
110
+ --good <known-good-sha> \
111
+ --bad <known-bad-sha> \
112
+ --test "pytest -k my_flaky_test" \
113
+ --runs 20 \
114
+ --threshold 0.3
115
+ ```
116
+
117
+ - `--runs` โ€” test executions per commit (default 20), run in parallel
118
+ (capped at your core count so it doesn't oversubscribe). Lower counts are
119
+ faster but noisier โ€” a low sample can misattribute the culprit near the
120
+ threshold boundary.
121
+ - `--threshold` โ€” flake-rate jump (0-1) vs. the `good` baseline required to
122
+ call a commit the culprit (default 0.3).
123
+ - `--no-explain` โ€” skip the LLM root-cause call entirely (fully offline).
124
+
125
+ ## Demo
126
+
127
+ See [`demo/`](demo/) for a self-contained, seeded-flaky-bug repo you can
128
+ bisect against with no network access required for the bisection itself.
129
+
130
+ ## How it works
131
+
132
+ 1. Measure the flake rate at `good` and `bad` as baselines.
133
+ 2. Binary-search the commit range between them.
134
+ 3. At each candidate, run the test `N` times in an isolated worktree and
135
+ compute `failed / N`.
136
+ 4. The culprit is the first commit whose flake rate jumps by `>= threshold`
137
+ over the `good` baseline.
138
+ 5. Verdict: **clean break** (rate goes ~0% โ†’ ~100%) vs. **flakiness
139
+ regression** (rate goes ~0% โ†’ somewhere in between).
140
+ 6. The configured LLM gets the culprit's diff + the flake-rate evidence and
141
+ returns a short root-cause read (race condition, shared mutable state,
142
+ timing assumption, etc.).
143
+
144
+ Binary search assumes the flake rate is roughly monotonic across the range,
145
+ same as ordinary `git bisect` assumes pass/fail is. At low `--runs` counts
146
+ a noisy sample can occasionally violate that near the threshold boundary โ€”
147
+ bump `--runs` if a result looks off.
148
+
149
+ ## Releasing
150
+
151
+ Version is single-sourced from `src/flarebisect/__init__.py`. Bump it, commit,
152
+ tag (`vX.Y.Z`), and cut a GitHub Release โ€” `.github/workflows/publish.yml`
153
+ builds and publishes to PyPI via trusted publishing (OIDC, no stored token).
154
+
155
+ ## License
156
+
157
+ MIT
@@ -0,0 +1,68 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "flarebisect"
7
+ dynamic = ["version"]
8
+ description = "git bisect that treats flakiness as a signal, not noise"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Tawin Tangsukson" },
14
+ ]
15
+ keywords = ["git", "bisect", "testing", "flaky-tests", "cli", "debugging"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Software Development :: Debuggers",
28
+ "Topic :: Software Development :: Testing",
29
+ "Topic :: Software Development :: Version Control :: Git",
30
+ ]
31
+ dependencies = [
32
+ "typer>=0.12",
33
+ "rich>=13.7",
34
+ "anthropic>=0.40",
35
+ "openai>=1.40",
36
+ ]
37
+
38
+ [project.optional-dependencies]
39
+ dev = [
40
+ "pytest>=8.0",
41
+ "build>=1.2",
42
+ "twine>=5.0",
43
+ ]
44
+
45
+ [project.urls]
46
+ Homepage = "https://github.com/kaorii-ako/FlareBisect"
47
+ Repository = "https://github.com/kaorii-ako/FlareBisect"
48
+ Issues = "https://github.com/kaorii-ako/FlareBisect/issues"
49
+
50
+ [project.scripts]
51
+ flarebisect = "flarebisect.cli:app"
52
+
53
+ [tool.hatch.version]
54
+ path = "src/flarebisect/__init__.py"
55
+
56
+ [tool.hatch.build.targets.wheel]
57
+ packages = ["src/flarebisect"]
58
+
59
+ [tool.hatch.build.targets.sdist]
60
+ include = [
61
+ "/src",
62
+ "/tests",
63
+ "/README.md",
64
+ "/LICENSE",
65
+ ]
66
+
67
+ [tool.pytest.ini_options]
68
+ testpaths = ["tests"]
@@ -0,0 +1 @@
1
+ __version__ = "0.3.1"
@@ -0,0 +1,37 @@
1
+ """Ask the configured LLM for a short root-cause read on the culprit commit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .bisect import BisectOutcome
6
+ from .providers import ProviderConfig, complete
7
+
8
+ PROMPT_TEMPLATE = """A commit changed a test's failure rate. Diagnose the likely cause.
9
+
10
+ commit {sha}: "{subject}"
11
+
12
+ flake rate before: {before_rate:.0%}
13
+ flake rate after: {culprit_rate:.0%}
14
+ bisection threshold: {threshold:.0%}
15
+ verdict: {verdict}
16
+
17
+ diff:
18
+ ```diff
19
+ {diff}
20
+ ```
21
+
22
+ Answer in exactly 1-2 short sentences, no preamble, no restating the numbers above.
23
+ Point at the specific code change responsible (shared state, missing lock, timing/ordering
24
+ assumption, resource leak, off-by-one, whatever it actually is)."""
25
+
26
+
27
+ def explain(outcome: BisectOutcome, diff: str, threshold: float, provider_cfg: ProviderConfig) -> str:
28
+ prompt = PROMPT_TEMPLATE.format(
29
+ sha=outcome.culprit.sha[:12],
30
+ subject=outcome.culprit.subject,
31
+ before_rate=outcome.before.result.flake_rate,
32
+ culprit_rate=outcome.culprit.result.flake_rate,
33
+ threshold=threshold,
34
+ verdict=outcome.verdict,
35
+ diff=diff[:6000],
36
+ )
37
+ return complete(provider_cfg, prompt)
@@ -0,0 +1,145 @@
1
+ """Flake-rate bisection: binary search over commits for the point where the
2
+ flake rate jumps by >= threshold relative to the known-good baseline."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import time
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Callable
10
+
11
+ from . import git_ops
12
+ from .runner import RunResult, measure_flake_rate, worker_count_for
13
+
14
+
15
+ @dataclass
16
+ class CommitMeasurement:
17
+ sha: str
18
+ subject: str
19
+ result: RunResult
20
+ position: int # -1 = good baseline, 0..n-1 = index within the good..bad range
21
+ status: str = "" # filled in once the culprit is known: good/stable/wobbling/flare/bad
22
+
23
+
24
+ @dataclass
25
+ class BisectOutcome:
26
+ culprit: CommitMeasurement
27
+ before: CommitMeasurement # last known-good-ish measurement (parent side)
28
+ good_baseline: RunResult
29
+ bad_baseline: RunResult
30
+ verdict: str # "clean break" | "flakiness regression"
31
+ measurements: list[CommitMeasurement] # every distinct commit measured, in range order
32
+ total_commits: int
33
+ runs: int
34
+ workers: int
35
+ elapsed_seconds: float
36
+
37
+
38
+ def _measure(repo: Path, sha: str, test_cmd: str, runs: int, position: int) -> CommitMeasurement:
39
+ wt = git_ops.add_worktree(repo, sha)
40
+ try:
41
+ result = measure_flake_rate(test_cmd, wt.path, runs)
42
+ finally:
43
+ git_ops.remove_worktree(repo, wt)
44
+ return CommitMeasurement(
45
+ sha=sha,
46
+ subject=git_ops.commit_subject(repo, sha),
47
+ result=result,
48
+ position=position,
49
+ )
50
+
51
+
52
+ def classify(good_rate: float, culprit_rate: float) -> str:
53
+ if culprit_rate >= 0.9 and good_rate <= 0.1:
54
+ return "clean break"
55
+ return "flakiness regression"
56
+
57
+
58
+ def status_label(rate: float, is_first: bool, is_last: bool, is_culprit: bool) -> str:
59
+ if is_culprit:
60
+ return "flare"
61
+ if is_last:
62
+ return "bad"
63
+ if rate <= 0.0:
64
+ return "good" if is_first else "stable"
65
+ return "wobbling"
66
+
67
+
68
+ def run_bisect(
69
+ repo: Path,
70
+ good: str,
71
+ bad: str,
72
+ test_cmd: str,
73
+ runs: int = 5,
74
+ threshold: float = 0.3,
75
+ on_measure: Callable[[str, CommitMeasurement], None] | None = None,
76
+ ) -> BisectOutcome:
77
+ started = time.perf_counter()
78
+
79
+ good_sha = git_ops.resolve_sha(repo, good)
80
+ bad_sha = git_ops.resolve_sha(repo, bad)
81
+
82
+ good_m = _measure(repo, good_sha, test_cmd, runs, position=-1)
83
+ if on_measure:
84
+ on_measure("good", good_m)
85
+
86
+ commits = git_ops.commit_list(repo, good_sha, bad_sha)
87
+ if not commits:
88
+ raise ValueError("no commits between good and bad")
89
+
90
+ baseline_rate = good_m.result.flake_rate
91
+
92
+ cache: dict[str, CommitMeasurement] = {}
93
+
94
+ def measure_idx(idx: int) -> CommitMeasurement:
95
+ sha = commits[idx]
96
+ if sha not in cache:
97
+ m = _measure(repo, sha, test_cmd, runs, position=idx)
98
+ cache[sha] = m
99
+ if on_measure:
100
+ on_measure(f"candidate[{idx}]", m)
101
+ return cache[sha]
102
+
103
+ bad_m = measure_idx(len(commits) - 1)
104
+
105
+ lo, hi = 0, len(commits) - 1
106
+ culprit_idx = hi # fall back to bad commit if search never narrows
107
+ culprit_measurement = bad_m
108
+
109
+ while lo <= hi:
110
+ mid = (lo + hi) // 2
111
+ m = measure_idx(mid)
112
+ jumped = (m.result.flake_rate - baseline_rate) >= threshold
113
+ if jumped:
114
+ culprit_idx = mid
115
+ culprit_measurement = m
116
+ hi = mid - 1
117
+ else:
118
+ lo = mid + 1
119
+
120
+ before_measurement = measure_idx(culprit_idx - 1) if culprit_idx > 0 else good_m
121
+
122
+ verdict = classify(before_measurement.result.flake_rate, culprit_measurement.result.flake_rate)
123
+
124
+ last_idx = len(commits) - 1
125
+ ordered = [good_m] + [cache[sha] for sha in commits if sha in cache]
126
+ for m in ordered:
127
+ m.status = status_label(
128
+ m.result.flake_rate,
129
+ is_first=(m.position == -1),
130
+ is_last=(m.position == last_idx),
131
+ is_culprit=(m.sha == culprit_measurement.sha),
132
+ )
133
+
134
+ return BisectOutcome(
135
+ culprit=culprit_measurement,
136
+ before=before_measurement,
137
+ good_baseline=good_m.result,
138
+ bad_baseline=bad_m.result,
139
+ verdict=verdict,
140
+ measurements=ordered,
141
+ total_commits=len(commits),
142
+ runs=runs,
143
+ workers=worker_count_for(runs),
144
+ elapsed_seconds=time.perf_counter() - started,
145
+ )