patchahead 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.
Files changed (99) hide show
  1. patchahead-0.3.0/LICENSE +21 -0
  2. patchahead-0.3.0/PKG-INFO +368 -0
  3. patchahead-0.3.0/README.md +317 -0
  4. patchahead-0.3.0/pyproject.toml +112 -0
  5. patchahead-0.3.0/setup.cfg +4 -0
  6. patchahead-0.3.0/src/patchahead/__init__.py +8 -0
  7. patchahead-0.3.0/src/patchahead/analysis/__init__.py +52 -0
  8. patchahead-0.3.0/src/patchahead/analysis/edits.py +143 -0
  9. patchahead-0.3.0/src/patchahead/analysis/index.py +203 -0
  10. patchahead-0.3.0/src/patchahead/analysis/python_ast.py +457 -0
  11. patchahead-0.3.0/src/patchahead/apidiff/__init__.py +23 -0
  12. patchahead-0.3.0/src/patchahead/apidiff/compare.py +366 -0
  13. patchahead-0.3.0/src/patchahead/apidiff/download.py +95 -0
  14. patchahead-0.3.0/src/patchahead/apidiff/surface.py +337 -0
  15. patchahead-0.3.0/src/patchahead/ci.py +301 -0
  16. patchahead-0.3.0/src/patchahead/cli.py +627 -0
  17. patchahead-0.3.0/src/patchahead/config.py +284 -0
  18. patchahead-0.3.0/src/patchahead/demo/__init__.py +256 -0
  19. patchahead-0.3.0/src/patchahead/demo/fixtures/changes/field-rename.md +14 -0
  20. patchahead-0.3.0/src/patchahead/demo/fixtures/changes/invoice-field-rename.md +21 -0
  21. patchahead-0.3.0/src/patchahead/demo/fixtures/changes/kwarg-rename.md +14 -0
  22. patchahead-0.3.0/src/patchahead/demo/fixtures/changes/method-rename.md +12 -0
  23. patchahead-0.3.0/src/patchahead/demo/fixtures/changes/pagination-cursor.json +24 -0
  24. patchahead-0.3.0/src/patchahead/demo/fixtures/changes/pagination-cursor.md +20 -0
  25. patchahead-0.3.0/src/patchahead/demo/fixtures/changes/sdk-v2.md +31 -0
  26. patchahead-0.3.0/src/patchahead/demo/fixtures/orders-service/README.md +51 -0
  27. patchahead-0.3.0/src/patchahead/demo/fixtures/orders-service/app/__init__.py +0 -0
  28. patchahead-0.3.0/src/patchahead/demo/fixtures/orders-service/app/client.py +15 -0
  29. patchahead-0.3.0/src/patchahead/demo/fixtures/orders-service/app/models.py +10 -0
  30. patchahead-0.3.0/src/patchahead/demo/fixtures/orders-service/app/order_report.py +24 -0
  31. patchahead-0.3.0/src/patchahead/demo/fixtures/orders-service/app/order_sync.py +21 -0
  32. patchahead-0.3.0/src/patchahead/demo/fixtures/orders-service/conftest.py +6 -0
  33. patchahead-0.3.0/src/patchahead/demo/fixtures/orders-service/pyproject.toml +16 -0
  34. patchahead-0.3.0/src/patchahead/demo/fixtures/orders-service/tests/test_client.py +14 -0
  35. patchahead-0.3.0/src/patchahead/demo/fixtures/orders-service/tests/test_order_report.py +24 -0
  36. patchahead-0.3.0/src/patchahead/demo/fixtures/orders-service/tests/test_order_sync.py +11 -0
  37. patchahead-0.3.0/src/patchahead/demo/fixtures/orders-service/upstream/__init__.py +0 -0
  38. patchahead-0.3.0/src/patchahead/demo/fixtures/orders-service/upstream/api_v1.py +34 -0
  39. patchahead-0.3.0/src/patchahead/demo/fixtures/orders-service/upstream/api_v2.py +56 -0
  40. patchahead-0.3.0/src/patchahead/demo/serve.py +189 -0
  41. patchahead-0.3.0/src/patchahead/domain/__init__.py +67 -0
  42. patchahead-0.3.0/src/patchahead/domain/change.py +269 -0
  43. patchahead-0.3.0/src/patchahead/domain/completeness.py +91 -0
  44. patchahead-0.3.0/src/patchahead/domain/impact.py +248 -0
  45. patchahead-0.3.0/src/patchahead/domain/patch.py +81 -0
  46. patchahead-0.3.0/src/patchahead/domain/plan.py +170 -0
  47. patchahead-0.3.0/src/patchahead/domain/result.py +210 -0
  48. patchahead-0.3.0/src/patchahead/domain/validation.py +200 -0
  49. patchahead-0.3.0/src/patchahead/engine.py +609 -0
  50. patchahead-0.3.0/src/patchahead/handlers/__init__.py +35 -0
  51. patchahead-0.3.0/src/patchahead/handlers/base.py +211 -0
  52. patchahead-0.3.0/src/patchahead/handlers/field_rename.py +425 -0
  53. patchahead-0.3.0/src/patchahead/handlers/kwarg_rename.py +201 -0
  54. patchahead-0.3.0/src/patchahead/handlers/method_rename.py +608 -0
  55. patchahead-0.3.0/src/patchahead/handlers/pagination.py +582 -0
  56. patchahead-0.3.0/src/patchahead/ingest/__init__.py +32 -0
  57. patchahead-0.3.0/src/patchahead/ingest/base.py +102 -0
  58. patchahead-0.3.0/src/patchahead/ingest/markdown.py +1138 -0
  59. patchahead-0.3.0/src/patchahead/ingest/structured.py +218 -0
  60. patchahead-0.3.0/src/patchahead/llm/__init__.py +28 -0
  61. patchahead-0.3.0/src/patchahead/llm/client.py +152 -0
  62. patchahead-0.3.0/src/patchahead/llm/proposer.py +620 -0
  63. patchahead-0.3.0/src/patchahead/observability.py +223 -0
  64. patchahead-0.3.0/src/patchahead/reporting.py +451 -0
  65. patchahead-0.3.0/src/patchahead/testing/__init__.py +22 -0
  66. patchahead-0.3.0/src/patchahead/testing/discovery.py +113 -0
  67. patchahead-0.3.0/src/patchahead/testing/runner.py +138 -0
  68. patchahead-0.3.0/src/patchahead/validation/__init__.py +5 -0
  69. patchahead-0.3.0/src/patchahead/validation/completeness.py +265 -0
  70. patchahead-0.3.0/src/patchahead/validation/engine.py +531 -0
  71. patchahead-0.3.0/src/patchahead/web/__init__.py +13 -0
  72. patchahead-0.3.0/src/patchahead/web/server.py +279 -0
  73. patchahead-0.3.0/src/patchahead/web/static/index.html +650 -0
  74. patchahead-0.3.0/src/patchahead/workspace.py +382 -0
  75. patchahead-0.3.0/src/patchahead.egg-info/PKG-INFO +368 -0
  76. patchahead-0.3.0/src/patchahead.egg-info/SOURCES.txt +97 -0
  77. patchahead-0.3.0/src/patchahead.egg-info/dependency_links.txt +1 -0
  78. patchahead-0.3.0/src/patchahead.egg-info/entry_points.txt +2 -0
  79. patchahead-0.3.0/src/patchahead.egg-info/requires.txt +34 -0
  80. patchahead-0.3.0/src/patchahead.egg-info/top_level.txt +1 -0
  81. patchahead-0.3.0/tests/test_analysis.py +382 -0
  82. patchahead-0.3.0/tests/test_apidiff.py +303 -0
  83. patchahead-0.3.0/tests/test_ci.py +204 -0
  84. patchahead-0.3.0/tests/test_cli.py +431 -0
  85. patchahead-0.3.0/tests/test_completeness.py +164 -0
  86. patchahead-0.3.0/tests/test_config.py +108 -0
  87. patchahead-0.3.0/tests/test_demo.py +441 -0
  88. patchahead-0.3.0/tests/test_engine_e2e.py +416 -0
  89. patchahead-0.3.0/tests/test_eval_harness.py +611 -0
  90. patchahead-0.3.0/tests/test_evals.py +211 -0
  91. patchahead-0.3.0/tests/test_handlers.py +868 -0
  92. patchahead-0.3.0/tests/test_ingest.py +525 -0
  93. patchahead-0.3.0/tests/test_llm.py +828 -0
  94. patchahead-0.3.0/tests/test_migrate_tests.py +128 -0
  95. patchahead-0.3.0/tests/test_observability.py +62 -0
  96. patchahead-0.3.0/tests/test_packaging.py +257 -0
  97. patchahead-0.3.0/tests/test_validation.py +646 -0
  98. patchahead-0.3.0/tests/test_web.py +286 -0
  99. patchahead-0.3.0/tests/test_workspace.py +265 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PatchAhead contributors
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,368 @@
1
+ Metadata-Version: 2.4
2
+ Name: patchahead
3
+ Version: 0.3.0
4
+ Summary: Find downstream code broken by upstream API changes, propose a minimal migration, and verify it with your tests.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/FrimpsManu/patchahead
7
+ Project-URL: Repository, https://github.com/FrimpsManu/patchahead
8
+ Project-URL: Issues, https://github.com/FrimpsManu/patchahead/issues
9
+ Project-URL: Changelog, https://github.com/FrimpsManu/patchahead/blob/main/CHANGELOG.md
10
+ Keywords: migration,breaking-change,api,codemod,ast,developer-tools
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Code Generators
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
26
+ Provides-Extra: llm
27
+ Requires-Dist: anthropic>=0.40; extra == "llm"
28
+ Provides-Extra: yaml
29
+ Requires-Dist: PyYAML>=6.0; extra == "yaml"
30
+ Provides-Extra: sentry
31
+ Requires-Dist: sentry-sdk>=2.0; extra == "sentry"
32
+ Provides-Extra: web
33
+ Requires-Dist: fastapi>=0.110; extra == "web"
34
+ Requires-Dist: uvicorn>=0.29; extra == "web"
35
+ Provides-Extra: demo
36
+ Requires-Dist: patchahead[web]; extra == "demo"
37
+ Requires-Dist: pytest>=7.4; extra == "demo"
38
+ Provides-Extra: dev
39
+ Requires-Dist: pytest>=7.4; extra == "dev"
40
+ Requires-Dist: ruff>=0.6; extra == "dev"
41
+ Requires-Dist: build>=1.2; extra == "dev"
42
+ Requires-Dist: httpx2>=0.28; extra == "dev"
43
+ Requires-Dist: setuptools>=68; extra == "dev"
44
+ Provides-Extra: all
45
+ Requires-Dist: patchahead[llm]; extra == "all"
46
+ Requires-Dist: patchahead[yaml]; extra == "all"
47
+ Requires-Dist: patchahead[sentry]; extra == "all"
48
+ Requires-Dist: patchahead[web]; extra == "all"
49
+ Requires-Dist: patchahead[demo]; extra == "all"
50
+ Dynamic: license-file
51
+
52
+ # PatchAhead
53
+
54
+ **When an API you depend on changes, PatchAhead updates your Python code to
55
+ match, and proves the fix works with your own tests.**
56
+
57
+ You give it the release note. It finds the code that breaks, writes the
58
+ smallest fix in a temporary copy of your project, and runs your tests. It only
59
+ calls a fix done when a test that failed before the fix passes after it. When it
60
+ is not sure, it says so and leaves your code alone.
61
+
62
+ ```text
63
+ release note -> find affected code -> plan -> patch a copy -> run your tests -> verdict
64
+ ```
65
+
66
+ ## Why this exists
67
+
68
+ Almost every app talks to services it does not control: payments, email,
69
+ storage, AI models. Those services change. A field gets renamed, a method gets
70
+ a new name, pagination switches from page numbers to cursors. Your code did not
71
+ change, but it is now broken.
72
+
73
+ Keeping up is slow and risky by hand. Someone has to read the release note,
74
+ search the codebase, fix every spot without touching unrelated code that
75
+ happens to use the same name, and test it all. Teams put it off, and old
76
+ versions pile up.
77
+
78
+ PatchAhead automates that work, and it is deliberately careful about it. A
79
+ migration tool that makes a wrong edit is worse than no tool, so every step can
80
+ refuse, and nothing is called a success without evidence from your tests.
81
+
82
+ ## See it in 30 seconds
83
+
84
+ ```bash
85
+ pip install 'patchahead[demo]'
86
+ patchahead demo
87
+ ```
88
+
89
+ This opens a local page with six example scenarios against a small, deliberately
90
+ broken service. Three end in a verified fix. The other three show it refusing,
91
+ being rejected by the tests, and reporting a patch it could not prove.
92
+
93
+ ![A verified migration in the PatchAhead demo](https://raw.githubusercontent.com/FrimpsManu/patchahead/main/docs/media/demo-verified.png)
94
+
95
+ Python 3.10 or newer. `pip install patchahead` alone is the core tool, with no
96
+ third-party dependencies on 3.11+; [docs/usage.md](docs/usage.md#install) lists
97
+ the extras.
98
+
99
+ ## What it looks like
100
+
101
+ Given a release note that says the `page` parameter was replaced by `cursor`:
102
+
103
+ ```console
104
+ $ patchahead migrate --repo ./my-service --change ./release-notes.md
105
+
106
+ Pagination is now cursor-based
107
+ 1 finding(s) in 1 file(s), scanned 8 file(s) in 0ms
108
+ + app/order_sync.py:12 high sync_all_orders while True: ... page=page ...
109
+
110
+ proposed diff
111
+ - page = 1
112
+ + cursor = None
113
+ - response = api_client.get_orders(page=page)
114
+ + response = api_client.get_orders(cursor=cursor)
115
+ - if page >= response["total_pages"]:
116
+ + if not response.get("has_more"):
117
+ - page += 1
118
+ + cursor = response.get("next_cursor")
119
+
120
+ validation
121
+ [pass] syntax 1 modified file(s) parse as valid Python
122
+ [pass] scope 1 file(s) changed, all named by the plan; 8 diff line(s)
123
+ [pass] targeted_tests tests/test_order_sync.py: 1 passed
124
+ [pass] regression_tests no new failures
125
+ [pass] migration_assertion the targeted tests failed before the patch and pass after it
126
+
127
+ completeness
128
+ `page` -> `cursor`: no code, dynamic access, or test still uses `page`
129
+
130
+ migrated: migrated 1 file(s); 5/5 gates passed
131
+ ```
132
+
133
+ Your repository was not touched. The diff was made in a temporary copy, and the
134
+ tests ran there. You review it and apply it.
135
+
136
+ The last section matters as much as the tests. Passing tests show that the code
137
+ they run works; they do not show the migration is *finished*. So after
138
+ patching, PatchAhead searches the patched copy for every place the old name
139
+ still appears and sorts them: code it did not rewrite (a bare reference, a
140
+ `getattr(obj, "old_name")`), tests that still use the old name, sites on a
141
+ different object left alone on purpose, and mere mentions in strings, comments,
142
+ config, and docs. `--require-complete` makes CI fail while any code or test
143
+ still uses the old name.
144
+
145
+ ## What it can fix
146
+
147
+ | Change | Example |
148
+ |---|---|
149
+ | A renamed field | `order["total"]` becomes `order["amount"]` |
150
+ | A renamed method | `client.fetch_orders()` becomes `client.list_orders()` |
151
+ | A renamed keyword argument | `fetch(timeout_seconds=5)` becomes `fetch(timeout=5)` |
152
+ | Page numbers to cursors | a `page` / `total_pages` loop becomes `cursor` / `has_more` |
153
+
154
+ It reads release notes the way vendors write them: headings, bullet lists,
155
+ tables, reStructuredText and Sphinx (CPython's own "What's New"), and phrasings like "renamed to", "is now", or
156
+ "deprecated in favor of". Anything outside these four kinds of change is
157
+ reported as unsupported, not forced into one that almost fits.
158
+
159
+ ## No release note? Compare the versions
160
+
161
+ Many libraries describe breaking changes badly, or not at all. PatchAhead can
162
+ read them out of the library itself, by comparing the version you use with the
163
+ one you are upgrading to:
164
+
165
+ ```console
166
+ $ patchahead api-diff pydantic 1.10.13 2.0 --out changes.json
167
+ pydantic 1.10.13 -> 2.0: compared 459 public member(s)
168
+ ...
169
+ method_rename `pydantic.main.BaseModel.dict` renamed to `model_dump`
170
+ method_rename `pydantic.main.BaseModel.parse_obj` renamed to `model_validate`
171
+ reported `pydantic.main.BaseModel.json` is deprecated in favor of `model_dump_json`
172
+ `BaseModel.json` is newly deprecated in favor of `BaseModel.model_dump_json`,
173
+ which does not accept every call it does -- not a rename PatchAhead can apply
174
+ ...
175
+
176
+ $ patchahead migrate --repo ./my-service --change changes.json
177
+ ```
178
+
179
+ It downloads both versions from PyPI as wheels and parses them. Nothing is
180
+ installed or run. A method counts as renamed only when the old one is gone or
181
+ deprecated **and** the new one accepts every call the old one did. A
182
+ replacement that takes different arguments is reported, never applied.
183
+
184
+ ## Use it in CI
185
+
186
+ PatchAhead ships as a GitHub Action. Put it on the pull requests Dependabot or
187
+ Renovate open, and every dependency bump gets checked:
188
+
189
+ ```yaml
190
+ on: pull_request
191
+ permissions:
192
+ contents: read
193
+ pull-requests: write # for the comment; Dependabot's token is read-only otherwise
194
+ jobs:
195
+ patchahead:
196
+ if: github.actor == 'dependabot[bot]'
197
+ runs-on: ubuntu-latest
198
+ steps:
199
+ - uses: actions/checkout@v4
200
+ - uses: FrimpsManu/patchahead@v0.3.0
201
+ with:
202
+ from-pull-request: true
203
+ install-command: pip install -r requirements-dev.txt
204
+ comment: true
205
+ ```
206
+
207
+ It reads the release notes in the pull request **and** compares the two
208
+ versions of each package it bumps. The two check each other: a release note
209
+ that renames something to a name the new version does not have is dropped, with
210
+ a note saying so. Then it migrates a temporary copy, runs your tests, and posts
211
+ the verdict, the diff, and what is left of the old API as one comment, updated
212
+ in place on re-runs. It never commits; `apply: true` writes a verified patch
213
+ into the checkout for a later step to commit. All inputs are in
214
+ [docs/usage.md](docs/usage.md#github-action).
215
+
216
+ ## How it stays safe
217
+
218
+ - **Your code is never written to.** All patching happens in a temporary copy.
219
+ - **It edits as little as possible.** Only the exact tokens that change, so
220
+ comments and formatting stay as they were.
221
+ - **It refuses when unsure.** If `customer["total"]` appears next to the
222
+ `order["total"]` a note is about, it reports that site and leaves it alone.
223
+ - **Tests decide.** A fix counts as done only when a test goes from failing to
224
+ passing. Tests that pass before and after prove nothing, and it says so.
225
+ Tests that use the old API are migrated too, but a test PatchAhead edited
226
+ never counts as proof that its own edit worked.
227
+ - **It shows what is left.** Every place the old name survives is listed, so a
228
+ green test run cannot hide an unfinished migration.
229
+ - **A human approves.** It never applies, commits, or merges anything.
230
+
231
+ It does run your test command, as you, so only point it at code you would run
232
+ tests on anyway. [docs/safety.md](docs/safety.md) has the full threat model.
233
+
234
+ ## System architecture
235
+
236
+ **How one run flows.** A release note goes in, five steps run in order, and a
237
+ verdict comes out. Your repository is only read; the patch is made in a copy.
238
+
239
+ ```mermaid
240
+ flowchart LR
241
+ note["Release note, or two<br/>versions of the library"] --> read
242
+ repo[("Your repository<br/>never written to")] --> find
243
+
244
+ subgraph engine["PatchAhead engine"]
245
+ read["1. Read<br/>what changed"] --> find["2. Find<br/>affected code"]
246
+ find --> plan["3. Plan<br/>the smallest fix"]
247
+ plan --> patch["4. Patch<br/>a temporary copy"]
248
+ patch --> prove["5. Prove<br/>five checks"]
249
+ end
250
+
251
+ repo -. copied .-> patch
252
+ ai["AI fallback<br/>off by default"] -. only if a plan is refused .-> patch
253
+ prove --> result["Diff, verdict,<br/>PR summary"]
254
+ ```
255
+
256
+ **How the code is organized.** The command line, the GitHub Action, the local
257
+ web UI, and the test suite all call the same engine, so there is no separate demo path that behaves
258
+ differently from the real one. The engine runs each step through its own
259
+ package, and the steps pass typed objects to each other through `domain`.
260
+
261
+ ```mermaid
262
+ flowchart TB
263
+ cli["Command line"] --> engine
264
+ web["Local web UI"] --> engine
265
+ action["GitHub Action"] --> engine
266
+ bench["Tests and benchmark"] --> engine
267
+ engine["engine<br/>runs the five steps in order"]
268
+
269
+ engine --> ingest["ingest<br/>reads release notes"]
270
+ engine --> analysis["analysis<br/>parses Python, finds sites"]
271
+ engine --> handlers["handlers<br/>one plugin per kind of change"]
272
+ engine --> workspace["workspace<br/>the temporary copy tests run in"]
273
+ engine --> validation["validation<br/>the five checks"]
274
+ engine -. optional .-> llm["llm<br/>AI fallback"]
275
+
276
+ ingest --> domain
277
+ analysis --> domain
278
+ handlers --> domain
279
+ workspace --> domain
280
+ validation --> domain
281
+ domain["domain<br/>the typed objects passed between steps"]
282
+ ```
283
+
284
+ Each step hands a typed object to the next, and each one can stop the run with
285
+ a reason:
286
+
287
+ | Step | Done by | Produces | Stops the run when |
288
+ |---|---|---|---|
289
+ | 1. Read | `ingest` | one `BreakingChange` per change in the note, with a confidence | the note is unclear, or the change is not one it can migrate |
290
+ | 2. Find | a handler, using `analysis` | an `ImpactReport`: every site using the old name, graded high, medium, or low, with a reason | no code uses the old name |
291
+ | 3. Plan | the same handler | a `MigrationPlan` you can read before anything changes | no site is safe to change, or the code shape is unfamiliar |
292
+ | 4. Patch | the handler, in a `workspace` | a `PatchProposal`: small text edits and a diff | an edit would not apply cleanly |
293
+ | 5. Prove | `validation` | a `ValidationResult` from five checks | a check fails, or the tests prove nothing |
294
+
295
+ The five checks run cheapest first:
296
+
297
+ 1. **Syntax**: every changed file still parses.
298
+ 2. **Scope**: only the files in the plan changed, within size limits.
299
+ 3. **Targeted tests**: the tests for the changed modules pass.
300
+ 4. **Regression**: no test that passed before now fails.
301
+ 5. **Migration assertion**: a test that failed before the patch passes after it.
302
+
303
+ Only the fifth check can make a run `migrated`. Anything less is reported as
304
+ `patched_unverified` or `validation_failed`. After the checks, a completeness
305
+ scan lists every place the old name still appears in the patched copy.
306
+
307
+ Each kind of change is a plugin: a handler class with four methods (`supports`,
308
+ `analyze`, `plan`, `generate`) registered in one place, so the engine has no
309
+ special cases. More detail: [docs/architecture.md](docs/architecture.md).
310
+
311
+ ## How it is measured
312
+
313
+ - **545 automated tests**, covering unit, integration, and full end-to-end runs
314
+ with real test subprocesses.
315
+ - **An evaluation benchmark of 140 cases**, run on every CI build: release notes
316
+ written the way vendors write them, before-and-after library versions
317
+ (including what requests and pydantic actually did), repositories built to
318
+ trick it (unrelated
319
+ objects with the same field name, `os.environ.get` next to a renamed
320
+ `client.get`, Unicode, nested scopes), full migrations, and the checks
321
+ themselves.
322
+ - **Zero wrong edits** across all site cases, and **zero misread changes**
323
+ across all release notes and library comparisons. Both are enforced: a case
324
+ that produces a wrong edit fails the build.
325
+ - **Replayed on real migrations.** 22 public projects that migrated by hand,
326
+ re-migrated by PatchAhead: pydantic 1 -> 2 from nothing but the two library
327
+ versions, and Python 3.12's `unittest` removals from CPython's own release
328
+ note. **509 edits, 337 identical to the maintainers', 0 wrong**; the rest were
329
+ checked against each receiver's class. Method, results, and the wrong edits
330
+ earlier runs made and how they were fixed: [docs/real-world.md](docs/real-world.md).
331
+ - **Known gaps are recorded, not hidden.** Four cases describe things it does
332
+ not do yet, and all of them fail safely by doing nothing. They are listed in
333
+ [docs/evaluation.md](docs/evaluation.md#the-gaps-that-remain).
334
+
335
+ Run it yourself with `python evals/run.py`.
336
+
337
+ ## Limitations
338
+
339
+ - **Python only.**
340
+ - **Four kinds of change.** Other changes are reported as unsupported.
341
+ - **No type inference.** It matches names. In `for o in orders: o["total"]` it
342
+ cannot prove `o` is an order, so it reports the site and does not patch it.
343
+ - **One pagination loop shape.** Other shapes are refused.
344
+ - **Local and single-repository.** No GitHub integration yet.
345
+
346
+ ## Roadmap
347
+
348
+ - Opening the verified fix as a pull request of its own, rather than a comment
349
+ - Reading OpenAPI spec changes directly
350
+ - More kinds of change, such as moved endpoints and changed response shapes
351
+ - Tracking a renamed value through variables (`current = order`)
352
+ - TypeScript
353
+
354
+ ## Documentation
355
+
356
+ | Read | For |
357
+ |---|---|
358
+ | [docs/usage.md](docs/usage.md) | Commands, configuration, exit codes, comparing versions, the GitHub Action, AI mode, web UI |
359
+ | [docs/architecture.md](docs/architecture.md) | How the engine is built, and why |
360
+ | [docs/migrations.md](docs/migrations.md) | Each kind of change in detail, including what it refuses |
361
+ | [docs/safety.md](docs/safety.md) | What it protects you from, and what it does not |
362
+ | [docs/evaluation.md](docs/evaluation.md) | The benchmark, and how to add a case |
363
+ | [docs/real-world.md](docs/real-world.md) | Replaying real migrations: pydantic 1 -> 2, and Python 3.12's unittest removals |
364
+ | [docs/contributing.md](docs/contributing.md) | Setting up, and adding a new kind of change |
365
+
366
+ ## License
367
+
368
+ MIT. See [LICENSE](LICENSE).