patchahead 0.3.0__py3-none-any.whl

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 (75) hide show
  1. patchahead/__init__.py +8 -0
  2. patchahead/analysis/__init__.py +52 -0
  3. patchahead/analysis/edits.py +143 -0
  4. patchahead/analysis/index.py +203 -0
  5. patchahead/analysis/python_ast.py +457 -0
  6. patchahead/apidiff/__init__.py +23 -0
  7. patchahead/apidiff/compare.py +366 -0
  8. patchahead/apidiff/download.py +95 -0
  9. patchahead/apidiff/surface.py +337 -0
  10. patchahead/ci.py +301 -0
  11. patchahead/cli.py +627 -0
  12. patchahead/config.py +284 -0
  13. patchahead/demo/__init__.py +256 -0
  14. patchahead/demo/fixtures/changes/field-rename.md +14 -0
  15. patchahead/demo/fixtures/changes/invoice-field-rename.md +21 -0
  16. patchahead/demo/fixtures/changes/kwarg-rename.md +14 -0
  17. patchahead/demo/fixtures/changes/method-rename.md +12 -0
  18. patchahead/demo/fixtures/changes/pagination-cursor.json +24 -0
  19. patchahead/demo/fixtures/changes/pagination-cursor.md +20 -0
  20. patchahead/demo/fixtures/changes/sdk-v2.md +31 -0
  21. patchahead/demo/fixtures/orders-service/README.md +51 -0
  22. patchahead/demo/fixtures/orders-service/app/__init__.py +0 -0
  23. patchahead/demo/fixtures/orders-service/app/client.py +15 -0
  24. patchahead/demo/fixtures/orders-service/app/models.py +10 -0
  25. patchahead/demo/fixtures/orders-service/app/order_report.py +24 -0
  26. patchahead/demo/fixtures/orders-service/app/order_sync.py +21 -0
  27. patchahead/demo/fixtures/orders-service/conftest.py +6 -0
  28. patchahead/demo/fixtures/orders-service/pyproject.toml +16 -0
  29. patchahead/demo/fixtures/orders-service/tests/test_client.py +14 -0
  30. patchahead/demo/fixtures/orders-service/tests/test_order_report.py +24 -0
  31. patchahead/demo/fixtures/orders-service/tests/test_order_sync.py +11 -0
  32. patchahead/demo/fixtures/orders-service/upstream/__init__.py +0 -0
  33. patchahead/demo/fixtures/orders-service/upstream/api_v1.py +34 -0
  34. patchahead/demo/fixtures/orders-service/upstream/api_v2.py +56 -0
  35. patchahead/demo/serve.py +189 -0
  36. patchahead/domain/__init__.py +67 -0
  37. patchahead/domain/change.py +269 -0
  38. patchahead/domain/completeness.py +91 -0
  39. patchahead/domain/impact.py +248 -0
  40. patchahead/domain/patch.py +81 -0
  41. patchahead/domain/plan.py +170 -0
  42. patchahead/domain/result.py +210 -0
  43. patchahead/domain/validation.py +200 -0
  44. patchahead/engine.py +609 -0
  45. patchahead/handlers/__init__.py +35 -0
  46. patchahead/handlers/base.py +211 -0
  47. patchahead/handlers/field_rename.py +425 -0
  48. patchahead/handlers/kwarg_rename.py +201 -0
  49. patchahead/handlers/method_rename.py +608 -0
  50. patchahead/handlers/pagination.py +582 -0
  51. patchahead/ingest/__init__.py +32 -0
  52. patchahead/ingest/base.py +102 -0
  53. patchahead/ingest/markdown.py +1138 -0
  54. patchahead/ingest/structured.py +218 -0
  55. patchahead/llm/__init__.py +28 -0
  56. patchahead/llm/client.py +152 -0
  57. patchahead/llm/proposer.py +620 -0
  58. patchahead/observability.py +223 -0
  59. patchahead/reporting.py +451 -0
  60. patchahead/testing/__init__.py +22 -0
  61. patchahead/testing/discovery.py +113 -0
  62. patchahead/testing/runner.py +138 -0
  63. patchahead/validation/__init__.py +5 -0
  64. patchahead/validation/completeness.py +265 -0
  65. patchahead/validation/engine.py +531 -0
  66. patchahead/web/__init__.py +13 -0
  67. patchahead/web/server.py +279 -0
  68. patchahead/web/static/index.html +650 -0
  69. patchahead/workspace.py +382 -0
  70. patchahead-0.3.0.dist-info/METADATA +368 -0
  71. patchahead-0.3.0.dist-info/RECORD +75 -0
  72. patchahead-0.3.0.dist-info/WHEEL +5 -0
  73. patchahead-0.3.0.dist-info/entry_points.txt +2 -0
  74. patchahead-0.3.0.dist-info/licenses/LICENSE +21 -0
  75. patchahead-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,51 @@
1
+ # orders-service (PatchAhead example repository)
2
+
3
+ A small, self-contained Python service that integrates with a third-party
4
+ **Orders API**. It exists so you can run PatchAhead against a *separate*
5
+ repository without writing one first.
6
+
7
+ This repo is **intentionally broken**. The vendored upstream client in
8
+ `upstream/` is the new (v2) version of the Orders API; the integration code in
9
+ `app/` is still written against the old (v1) contract. So:
10
+
11
+ ```console
12
+ $ cd "$(patchahead demo --print-paths | awk '/^repository/ {print $2}')"
13
+ $ python -m pytest
14
+ ...
15
+ FAILED tests/test_order_sync.py::test_sync_all_orders - KeyError: 'total_pages'
16
+ FAILED tests/test_order_report.py::test_total_revenue - KeyError: 'total'
17
+ FAILED tests/test_client.py::test_fetch_recent - TypeError: ... 'timeout_seconds'
18
+ 3 failed
19
+ ```
20
+
21
+ That is the situation PatchAhead is for. Each failure corresponds to one
22
+ breaking change described in `../changes/`:
23
+
24
+ | Change document | Breaking change | Breaks |
25
+ |---|---|---|
26
+ | `../changes/pagination-cursor.md` | page-based -> cursor-based pagination | `app/order_sync.py` |
27
+ | `../changes/field-rename.md` | order field `total` -> `amount` | `app/order_report.py` |
28
+ | `../changes/kwarg-rename.md` | `timeout_seconds=` -> `timeout=` | `app/client.py` |
29
+ | `../changes/method-rename.md` | `fetch_orders()` -> `list_orders()` | `app/client.py` |
30
+ | `../changes/pagination-cursor.json` | the same pagination change, as structured JSON | `app/order_sync.py` |
31
+ | `../changes/sdk-v2.md` | both SDK changes in one release note | `app/client.py` |
32
+ | `../changes/invoice-field-rename.md` | a rename on `invoice` objects — **nothing here** | nothing: it is refused |
33
+
34
+ The easiest way to see all of this is `patchahead demo`, which serves these
35
+ files through the UI with a short explanation of each scenario. To drive them
36
+ from the command line instead, `patchahead demo --print-paths` prints where this
37
+ directory and `../changes` landed in your installation:
38
+
39
+ ```bash
40
+ repo=$(patchahead demo --print-paths | awk '/^repository/ {print $2}')
41
+ changes=$(patchahead demo --print-paths | awk '/^changes/ {print $2}')
42
+
43
+ patchahead analyze --repo "$repo" --change "$changes/pagination-cursor.md"
44
+ patchahead migrate --repo "$repo" --change "$changes/pagination-cursor.md"
45
+ ```
46
+
47
+ `migrate` copies this repo into a temporary workspace, patches the copy, runs the
48
+ tests there, and prints a diff. This directory is left untouched.
49
+
50
+ `upstream/api_v1.py` is kept only as documentation of the contract the `app/`
51
+ code was originally written against. Nothing imports it.
@@ -0,0 +1,15 @@
1
+ """Thin wrapper over the upstream Orders API client.
2
+
3
+ Written against the v1 SDK surface: ``fetch_orders(..., timeout_seconds=...)``.
4
+ """
5
+
6
+ DEFAULT_TIMEOUT_SECONDS = 15.0
7
+
8
+
9
+ def fetch_recent(api_client, limit=3):
10
+ return api_client.fetch_orders(limit=limit, timeout_seconds=DEFAULT_TIMEOUT_SECONDS)
11
+
12
+
13
+ def fetch_one(api_client):
14
+ orders = api_client.fetch_orders(limit=1, timeout_seconds=5.0)
15
+ return orders[0] if orders else None
@@ -0,0 +1,10 @@
1
+ """Lightweight domain models for the downstream app."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class Order:
8
+ id: int
9
+ customer: str
10
+ total: float
@@ -0,0 +1,24 @@
1
+ """Revenue reporting over synced orders.
2
+
3
+ Reads each order's `total` field. Written against the v1 Orders schema.
4
+ """
5
+
6
+ # Unrelated identifiers that merely contain the word "total". A correct
7
+ # migration must not touch any of these.
8
+ TOTAL_LABEL = "total"
9
+
10
+
11
+ def total_revenue(orders):
12
+ return round(sum(order["total"] for order in orders), 2)
13
+
14
+
15
+ def revenue_by_customer(orders):
16
+ totals = {}
17
+ for order in orders:
18
+ name = order["customer"]
19
+ totals[name] = round(totals.get(name, 0) + order["total"], 2)
20
+ return totals
21
+
22
+
23
+ def format_summary(orders):
24
+ return f"{TOTAL_LABEL}: {total_revenue(orders)}"
@@ -0,0 +1,21 @@
1
+ """Downstream order-sync integration.
2
+
3
+ Syncs ALL orders from the upstream Orders API. Written against the v1
4
+ (page-based) pagination contract.
5
+ """
6
+
7
+
8
+ def sync_all_orders(api_client):
9
+ page = 1
10
+ all_orders = []
11
+
12
+ while True:
13
+ response = api_client.get_orders(page=page)
14
+ all_orders.extend(response["orders"])
15
+
16
+ if page >= response["total_pages"]:
17
+ break
18
+
19
+ page += 1
20
+
21
+ return all_orders
@@ -0,0 +1,6 @@
1
+ """Put this repository's own packages on sys.path for the tests."""
2
+
3
+ import os
4
+ import sys
5
+
6
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@@ -0,0 +1,16 @@
1
+ [project]
2
+ name = "orders-service"
3
+ version = "0.1.0"
4
+ description = "Example downstream service for PatchAhead."
5
+ requires-python = ">=3.10"
6
+
7
+ [tool.patchahead]
8
+ # PatchAhead reads this section. Everything here is optional; these values
9
+ # happen to match what PatchAhead would infer anyway, and are written out so
10
+ # the example doubles as documentation of the config format.
11
+ source_dirs = ["app"]
12
+ test_command = "python -m pytest"
13
+ max_changed_files = 3
14
+
15
+ [tool.pytest.ini_options]
16
+ testpaths = ["tests"]
@@ -0,0 +1,14 @@
1
+ """Integration test for the thin upstream client wrapper."""
2
+
3
+ from app import client
4
+ from upstream.api_v2 import OrdersAPIClient
5
+
6
+
7
+ def test_fetch_recent():
8
+ orders = client.fetch_recent(OrdersAPIClient(), limit=2)
9
+
10
+ assert [o["id"] for o in orders] == [1, 2]
11
+
12
+
13
+ def test_fetch_one():
14
+ assert client.fetch_one(OrdersAPIClient())["id"] == 1
@@ -0,0 +1,24 @@
1
+ """Integration test for revenue reporting against the live upstream."""
2
+
3
+ from app import order_report
4
+ from upstream.api_v2 import OrdersAPIClient
5
+
6
+
7
+ def _orders():
8
+ return OrdersAPIClient().get_orders(cursor=None)["orders"]
9
+
10
+
11
+ def test_total_revenue():
12
+ assert order_report.total_revenue(_orders()) == 55.5
13
+
14
+
15
+ def test_revenue_by_customer():
16
+ by_customer = order_report.revenue_by_customer(_orders())
17
+
18
+ assert by_customer == {"Ada": 42.0, "Linus": 13.5}
19
+
20
+
21
+ def test_summary_label_is_not_a_data_field():
22
+ # `TOTAL_LABEL` is display text, not an API field. A migration that renames
23
+ # the `total` field must leave it alone.
24
+ assert order_report.TOTAL_LABEL == "total"
@@ -0,0 +1,11 @@
1
+ """Integration test for the downstream order-sync against the live upstream."""
2
+
3
+ from app import order_sync
4
+ from upstream.api_v2 import OrdersAPIClient
5
+
6
+
7
+ def test_sync_all_orders():
8
+ orders = order_sync.sync_all_orders(OrdersAPIClient())
9
+
10
+ assert len(orders) == 6, f"expected 6 orders, got {len(orders)}"
11
+ assert sorted(o["id"] for o in orders) == [1, 2, 3, 4, 5, 6]
@@ -0,0 +1,34 @@
1
+ """Vendored Orders API client -- v1.
2
+
3
+ Kept purely as documentation of the contract `app/` was originally written
4
+ against. Nothing imports this module; the service depends on ``api_v2``.
5
+ """
6
+
7
+ _ORDERS = [
8
+ {"id": 1, "customer": "Ada", "total": 42.00},
9
+ {"id": 2, "customer": "Linus", "total": 13.50},
10
+ {"id": 3, "customer": "Grace", "total": 88.10},
11
+ {"id": 4, "customer": "Alan", "total": 27.75},
12
+ {"id": 5, "customer": "Edsger", "total": 64.20},
13
+ {"id": 6, "customer": "Margaret", "total": 9.99},
14
+ ]
15
+
16
+ _PAGE_SIZE = 2
17
+
18
+
19
+ class OrdersAPIClient:
20
+ """Page-based Orders API client (v1)."""
21
+
22
+ version = "v1"
23
+
24
+ def get_orders(self, page: int = 1, **_ignored):
25
+ total_pages = (len(_ORDERS) + _PAGE_SIZE - 1) // _PAGE_SIZE
26
+ start = (page - 1) * _PAGE_SIZE
27
+ return {
28
+ "orders": _ORDERS[start : start + _PAGE_SIZE],
29
+ "page": page,
30
+ "total_pages": total_pages,
31
+ }
32
+
33
+ def fetch_orders(self, limit=3, timeout_seconds=30.0):
34
+ return _ORDERS[:limit]
@@ -0,0 +1,56 @@
1
+ """Vendored Orders API client -- v2 (the version this repo now depends on).
2
+
3
+ BREAKING CHANGES vs v1, all of which the `app/` code has not been migrated to:
4
+
5
+ 1. Pagination moved from integer ``page`` + ``total_pages`` to opaque
6
+ ``cursor`` + ``next_cursor`` + ``has_more``. The ``page`` and
7
+ ``total_pages`` response fields are GONE.
8
+
9
+ ``get_orders`` still ACCEPTS a ``page`` kwarg, so old callers do not get a
10
+ loud ``TypeError`` -- they get something quieter and worse: a response with
11
+ no ``total_pages``, which raises ``KeyError`` and (in sloppier code) can
12
+ silently sync incomplete data.
13
+
14
+ 2. The order money field ``total`` was renamed to ``amount``.
15
+
16
+ 3. ``fetch_orders()`` was renamed to ``list_orders()``.
17
+
18
+ 4. The ``timeout_seconds=`` keyword argument was renamed to ``timeout=``.
19
+
20
+ Fully in-memory and deterministic -- no network.
21
+ """
22
+
23
+ _ORDERS = [
24
+ {"id": 1, "customer": "Ada", "amount": 42.00},
25
+ {"id": 2, "customer": "Linus", "amount": 13.50},
26
+ {"id": 3, "customer": "Grace", "amount": 88.10},
27
+ {"id": 4, "customer": "Alan", "amount": 27.75},
28
+ {"id": 5, "customer": "Edsger", "amount": 64.20},
29
+ {"id": 6, "customer": "Margaret", "amount": 9.99},
30
+ ]
31
+
32
+ _PAGE_SIZE = 2
33
+ # Opaque cursor -> start index into the dataset.
34
+ _CURSOR_START = {None: 0, "c1": 2, "c2": 4}
35
+
36
+
37
+ class OrdersAPIClient:
38
+ """Cursor-based Orders API client (v2)."""
39
+
40
+ version = "v2"
41
+
42
+ def get_orders(self, cursor=None, page=None, **_ignored):
43
+ start = _CURSOR_START.get(cursor, 0)
44
+ end = start + _PAGE_SIZE
45
+ chunk = _ORDERS[start:end]
46
+ has_more = end < len(_ORDERS)
47
+ next_cursor = None
48
+ if has_more:
49
+ next_cursor = "c1" if end == 2 else "c2"
50
+ return {"orders": chunk, "next_cursor": next_cursor, "has_more": has_more}
51
+
52
+ def list_orders(self, limit=3, timeout=30.0):
53
+ """v2 name for what v1 called ``fetch_orders(..., timeout_seconds=...)``."""
54
+ if not isinstance(timeout, (int, float)):
55
+ raise TypeError(f"timeout must be a number, got {timeout!r}")
56
+ return _ORDERS[:limit]
@@ -0,0 +1,189 @@
1
+ """Starting the bundled demo: pick a port, print the URL, serve the UI.
2
+
3
+ Everything here is about removing steps between ``pip install`` and seeing the
4
+ product work. It contains no migration logic and no second execution path -- it
5
+ builds the same FastAPI app :mod:`patchahead.web.server` builds for any
6
+ repository, hands it the bundled fixtures, and runs it on the loopback
7
+ interface.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import importlib.util
13
+ import logging
14
+ import os
15
+ import socket
16
+ import sys
17
+ import threading
18
+ import webbrowser
19
+ from pathlib import Path
20
+
21
+ from patchahead import __version__
22
+ from patchahead.demo import DemoError, changes_root, repo_root, scenarios
23
+
24
+ log = logging.getLogger("patchahead.demo")
25
+
26
+ DEFAULT_PORT = 8000
27
+ #: How far to look for a free port before giving up. Twenty is enough to get
28
+ #: past a handful of other dev servers and small enough to fail quickly.
29
+ PORT_SEARCH_RANGE = 20
30
+ HOST = "127.0.0.1"
31
+
32
+
33
+ def port_is_free(port: int, host: str = HOST) -> bool:
34
+ """Whether a server could bind this port right now.
35
+
36
+ Asks the only question that matters -- "can I bind?" -- rather than probing
37
+ for a listener, because a port can be unbindable for reasons other than
38
+ something serving on it.
39
+ """
40
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
41
+ try:
42
+ probe.bind((host, port))
43
+ except OSError:
44
+ return False
45
+ return True
46
+
47
+
48
+ def choose_port(preferred: int = DEFAULT_PORT, *, explicit: bool = False) -> int:
49
+ """Return a bindable port, or explain why there is not one.
50
+
51
+ When the user named a port, an occupied one is an error: silently serving
52
+ somewhere else would send them to a URL they did not ask for, and in the
53
+ worst case to a *different* server that is already there. When the port is
54
+ just our default, moving on is the friendlier behaviour and is announced.
55
+ """
56
+ if port_is_free(preferred):
57
+ return preferred
58
+ if explicit:
59
+ raise DemoError(
60
+ f"port {preferred} is already in use. Pass a different --port, or stop "
61
+ f"whatever is listening on {HOST}:{preferred}."
62
+ )
63
+ for candidate in range(preferred + 1, preferred + 1 + PORT_SEARCH_RANGE):
64
+ if port_is_free(candidate):
65
+ log.info("port %d was busy; using %d instead", preferred, candidate)
66
+ return candidate
67
+ raise DemoError(
68
+ f"ports {preferred}-{preferred + PORT_SEARCH_RANGE} are all in use. "
69
+ f"Free one, or pass --port."
70
+ )
71
+
72
+
73
+ def browser_is_practical() -> bool:
74
+ """Whether opening a browser would do something useful.
75
+
76
+ On a headless machine -- a container, a CI runner, an SSH session --
77
+ ``webbrowser.open`` can launch a terminal browser over the top of the
78
+ output, which is worse than doing nothing. The URL is always printed, so
79
+ declining here costs the user one click.
80
+ """
81
+ if os.environ.get("PATCHAHEAD_NO_BROWSER"):
82
+ return False
83
+ if sys.platform in ("darwin", "win32"):
84
+ return True
85
+ if not (os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")):
86
+ return False
87
+ try:
88
+ webbrowser.get()
89
+ except webbrowser.Error:
90
+ return False
91
+ return True
92
+
93
+
94
+ def _open_later(url: str, delay: float = 1.0) -> None:
95
+ """Open the browser once the server has had a moment to start listening."""
96
+
97
+ def _open() -> None:
98
+ try:
99
+ webbrowser.open(url)
100
+ except Exception as exc: # pragma: no cover - platform dependent
101
+ log.debug("could not open a browser: %s", exc)
102
+
103
+ timer = threading.Timer(delay, _open)
104
+ timer.daemon = True
105
+ timer.start()
106
+
107
+
108
+ def test_runner_available() -> bool:
109
+ """Whether the bundled repository's test command can actually run.
110
+
111
+ The demo's whole point is the difference between "patched" and "verified",
112
+ and verification needs a test runner. Without pytest every scenario reports,
113
+ honestly and uselessly, that the test command could not start -- so this is
114
+ checked up front and the fix is printed, rather than leaving the viewer to
115
+ conclude the tool is broken.
116
+ """
117
+ return importlib.util.find_spec("pytest") is not None
118
+
119
+
120
+ def banner(url: str, repo: Path, test_command: str, *, can_run_tests: bool = True) -> str:
121
+ """The text printed before the server takes over the terminal."""
122
+ lines = [
123
+ "",
124
+ f" PatchAhead {__version__} — demo",
125
+ "",
126
+ f" Open {url}",
127
+ "",
128
+ f" Repository {repo}",
129
+ f" Scenarios {len(scenarios())} bundled, including one PatchAhead refuses",
130
+ "",
131
+ " This is the real engine. Migrating copies the bundled repository to a",
132
+ f" temporary directory, patches the copy, and runs `{test_command}` there.",
133
+ " The bundled repository is never modified. Serving on localhost only.",
134
+ "",
135
+ " Ctrl-C to stop.",
136
+ "",
137
+ ]
138
+ if not can_run_tests:
139
+ lines[-1:] = [
140
+ " WARNING: pytest is not installed, so no scenario can be verified.",
141
+ " Every run will report that the test command could not start.",
142
+ " Fix: pip install 'patchahead[demo]'",
143
+ "",
144
+ ]
145
+ return "\n".join(lines)
146
+
147
+
148
+ def serve(
149
+ port: int = DEFAULT_PORT,
150
+ *,
151
+ port_was_explicit: bool = False,
152
+ open_browser: bool = True,
153
+ scenario: str = "",
154
+ ) -> int:
155
+ """Run the demo server until interrupted. Returns a process exit code."""
156
+ from patchahead.config import load as load_config
157
+ from patchahead.web.server import create_app
158
+
159
+ repo = repo_root()
160
+ changes = changes_root()
161
+
162
+ try:
163
+ import uvicorn
164
+ except ImportError:
165
+ log.error(
166
+ "the demo needs its extra dependencies: pip install 'patchahead[demo]' "
167
+ "(FastAPI, uvicorn, and the pytest that verifies the migrations)"
168
+ )
169
+ return 2
170
+
171
+ chosen = choose_port(port, explicit=port_was_explicit)
172
+ app = create_app(repo, changes, scenarios())
173
+ url = f"http://{HOST}:{chosen}"
174
+ if scenario:
175
+ url = f"{url}/?scenario={scenario}"
176
+
177
+ print(
178
+ banner(
179
+ url,
180
+ repo,
181
+ load_config(repo).test_command,
182
+ can_run_tests=test_runner_available(),
183
+ )
184
+ )
185
+ if open_browser and browser_is_practical():
186
+ _open_later(url)
187
+
188
+ uvicorn.run(app, host=HOST, port=chosen, log_level="warning")
189
+ return 0
@@ -0,0 +1,67 @@
1
+ """The PatchAhead domain model.
2
+
3
+ Everything that crosses a stage boundary is one of these objects. No stage
4
+ passes a loosely-structured dict to another stage.
5
+ """
6
+
7
+ from patchahead.domain.change import (
8
+ BreakingChange,
9
+ ChangeKind,
10
+ Confidence,
11
+ Evidence,
12
+ PaginationContract,
13
+ Severity,
14
+ SymbolTarget,
15
+ )
16
+ from patchahead.domain.impact import (
17
+ AccessKind,
18
+ CodeReference,
19
+ ImpactFinding,
20
+ ImpactGraph,
21
+ ImpactReport,
22
+ )
23
+ from patchahead.domain.patch import FileEdit, PatchProposal
24
+ from patchahead.domain.plan import MigrationPlan, Risk, TextEdit, Transformation
25
+ from patchahead.domain.result import (
26
+ AnalysisResult,
27
+ MigrationResult,
28
+ MigrationRun,
29
+ Outcome,
30
+ )
31
+ from patchahead.domain.validation import (
32
+ GateName,
33
+ GateResult,
34
+ GateStatus,
35
+ TestRun,
36
+ ValidationResult,
37
+ )
38
+
39
+ __all__ = [
40
+ "AccessKind",
41
+ "AnalysisResult",
42
+ "BreakingChange",
43
+ "ChangeKind",
44
+ "CodeReference",
45
+ "Confidence",
46
+ "Evidence",
47
+ "FileEdit",
48
+ "GateName",
49
+ "GateResult",
50
+ "GateStatus",
51
+ "ImpactFinding",
52
+ "ImpactGraph",
53
+ "ImpactReport",
54
+ "MigrationPlan",
55
+ "MigrationResult",
56
+ "MigrationRun",
57
+ "Outcome",
58
+ "PaginationContract",
59
+ "PatchProposal",
60
+ "Risk",
61
+ "Severity",
62
+ "SymbolTarget",
63
+ "TestRun",
64
+ "TextEdit",
65
+ "Transformation",
66
+ "ValidationResult",
67
+ ]