release-state-reconcile 0.1.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 LaimaWu
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,177 @@
1
+ Metadata-Version: 2.4
2
+ Name: release-state-reconcile
3
+ Version: 0.1.0
4
+ Summary: Read-only CLI for reconciling release candidates across fixes, backports, checks, release notes, and published releases.
5
+ Author: LaimaWu
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 LaimaWu
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Repository, https://github.com/LaimaWu/release-state-reconcile
29
+ Project-URL: Issues, https://github.com/LaimaWu/release-state-reconcile/issues
30
+ Classifier: Development Status :: 3 - Alpha
31
+ Classifier: Environment :: Console
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Operating System :: OS Independent
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3.10
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Programming Language :: Python :: 3.13
39
+ Classifier: Topic :: Software Development :: Quality Assurance
40
+ Requires-Python: >=3.10
41
+ Description-Content-Type: text/markdown
42
+ License-File: LICENSE
43
+ Dynamic: license-file
44
+
45
+ # Release State Reconcile
46
+
47
+ Read-only CLI for reconciling release candidates across fixes, backports, checks, release notes, and published releases.
48
+
49
+ Given one public GitHub repository, one selected release candidate, and one release line, Release State Reconcile reconstructs the public evidence chain across implementation, backports, checks, release-note evidence, and published releases, surfacing missing links, branch mismatches, contradictions, and `UNKNOWN` states without mutating GitHub.
50
+
51
+ ## Why this exists
52
+
53
+ Release state is often spread across an issue, one or more pull requests, CI checks, changed files, maintenance branches, and release tags. This tool assembles those public facts into one deterministic report and keeps uncertainty explicit. It supports investigation and review; it does not make release decisions.
54
+
55
+ ## Installation
56
+
57
+ Python 3.10 or newer is required.
58
+
59
+ ### Stable GitHub release
60
+
61
+ Install the verified wheel attached to the `v0.1.0` GitHub Release:
62
+
63
+ ```bash
64
+ python -m pip install https://github.com/LaimaWu/release-state-reconcile/releases/download/v0.1.0/release_state_reconcile-0.1.0-py3-none-any.whl
65
+ ```
66
+
67
+ ### Tagged Git source
68
+
69
+ Alternatively, install from the exact `v0.1.0` tag:
70
+
71
+ ```bash
72
+ python -m pip install "git+https://github.com/LaimaWu/release-state-reconcile.git@v0.1.0"
73
+ ```
74
+
75
+ If you use `pipx`, the same tagged source can be installed as an isolated CLI application:
76
+
77
+ ```bash
78
+ pipx install "git+https://github.com/LaimaWu/release-state-reconcile.git@v0.1.0"
79
+ ```
80
+
81
+ ### PyPI status
82
+
83
+ PyPI distribution is planned but not yet available. Do not use `pip install release-state-reconcile` until the package is published on PyPI.
84
+
85
+ ### Contributor and development install
86
+
87
+ From a source checkout, contributors can retain an editable installation:
88
+
89
+ ```bash
90
+ python -m pip install -e .
91
+ ```
92
+
93
+ ## CLI
94
+
95
+ ```bash
96
+ release-state-reconcile \
97
+ --candidate https://github.com/OWNER/REPOSITORY/issues/123 \
98
+ --release-branch release/1.2 \
99
+ --config repository.json \
100
+ --output report.md
101
+ ```
102
+
103
+ Use `--format json` for structured output. If `--output` is omitted, the report is written to standard output.
104
+
105
+ ## Output shape
106
+
107
+ The Markdown report includes an evidence-linked state table, related pull requests, explicit same- and cross-repository relationships, contradictions, and unknown or missing links.
108
+
109
+ ```text
110
+ Fact State
111
+ Original issue/PR CLOSED
112
+ Mainline merge MERGED | OPEN | UNKNOWN
113
+ Required backport PRs MERGED | MISSING | TARGET_BRANCH_MISMATCH | UNKNOWN
114
+ Backport/check state PASS | FAIL | PENDING | UNKNOWN
115
+ Release-note evidence PRESENT | ABSENT | UNKNOWN
116
+ Final tag/release containment CONTAINED | NOT_CONTAINED | UNKNOWN
117
+ ```
118
+
119
+ Each observation carries a public source URL, object type, object ID, observed state, and confidence.
120
+
121
+ ## Configuration
122
+
123
+ Repository conventions live in JSON rather than source code. A minimal configuration looks like this:
124
+
125
+ ```json
126
+ {
127
+ "mainline_branch": "main",
128
+ "backport": {"required": true, "search": true},
129
+ "completion": {"closed_is_complete": true, "labels": []},
130
+ "verification_labels": ["verified"],
131
+ "release_note": {"path_globs": ["changes/**"]},
132
+ "release": {"tag_regex": "^v1\\.2\\.[0-9]+$", "max_releases": 10}
133
+ }
134
+ ```
135
+
136
+ The selected release branch is always supplied explicitly on the command line. Configuration may describe the default branch, whether a backport is expected, repository-specific verification labels, release-note paths, and the release-tag convention.
137
+
138
+ ## Scope and non-goals
139
+
140
+ The tool observes and reconciles public GitHub state. It does not:
141
+
142
+ - decide whether a candidate belongs in a release;
143
+ - assess change risk or release-note quality;
144
+ - approve, merge, label, comment on, or otherwise mutate GitHub objects;
145
+ - orchestrate releases across multiple release lines;
146
+ - provide end-to-end release automation; or
147
+ - use an LLM or make AI-generated release decisions.
148
+
149
+ ## Read-only and security model
150
+
151
+ The GitHub client exposes only HTTP `GET` operations. Authentication is optional: `GITHUB_TOKEN` or `GH_TOKEN` may be supplied to increase public REST API limits, but the tool has no GitHub write methods. Reports may be written to a local path selected by the caller.
152
+
153
+ The runtime uses the Python standard library and the public GitHub REST API. Do not place tokens in configuration files or commit them to source control. See [SECURITY.md](SECURITY.md) for vulnerability reporting guidance.
154
+
155
+ ## Validation
156
+
157
+ The repository contains 21 deterministic unit tests covering closure semantics, relationship discovery, cross-repository preservation, target-branch mismatch handling, external-provider uncertainty, check aggregation, release-note path evidence, and the GET-only client surface. CI runs the full suite on Python 3.10, 3.11, 3.12, and 3.13.
158
+
159
+ Run locally with:
160
+
161
+ ```bash
162
+ python -m unittest discover -s tests -v
163
+ ```
164
+
165
+ ## Known limitations
166
+
167
+ - One repository and one selected release line are evaluated per run.
168
+ - Repository conventions may require configuration.
169
+ - Unsupported external change providers remain `UNKNOWN`.
170
+ - Private CI and off-GitHub approvals are outside scope.
171
+ - Candidate eligibility and release-note quality remain human judgment.
172
+ - Some public issues do not expose enough relationships to reconstruct a useful chain.
173
+ - A complete GitHub search establishes only that no qualifying result was returned for that query; it is not proof that an unreferenced change does not exist.
174
+
175
+ ## License
176
+
177
+ [MIT](LICENSE)
@@ -0,0 +1,133 @@
1
+ # Release State Reconcile
2
+
3
+ Read-only CLI for reconciling release candidates across fixes, backports, checks, release notes, and published releases.
4
+
5
+ Given one public GitHub repository, one selected release candidate, and one release line, Release State Reconcile reconstructs the public evidence chain across implementation, backports, checks, release-note evidence, and published releases, surfacing missing links, branch mismatches, contradictions, and `UNKNOWN` states without mutating GitHub.
6
+
7
+ ## Why this exists
8
+
9
+ Release state is often spread across an issue, one or more pull requests, CI checks, changed files, maintenance branches, and release tags. This tool assembles those public facts into one deterministic report and keeps uncertainty explicit. It supports investigation and review; it does not make release decisions.
10
+
11
+ ## Installation
12
+
13
+ Python 3.10 or newer is required.
14
+
15
+ ### Stable GitHub release
16
+
17
+ Install the verified wheel attached to the `v0.1.0` GitHub Release:
18
+
19
+ ```bash
20
+ python -m pip install https://github.com/LaimaWu/release-state-reconcile/releases/download/v0.1.0/release_state_reconcile-0.1.0-py3-none-any.whl
21
+ ```
22
+
23
+ ### Tagged Git source
24
+
25
+ Alternatively, install from the exact `v0.1.0` tag:
26
+
27
+ ```bash
28
+ python -m pip install "git+https://github.com/LaimaWu/release-state-reconcile.git@v0.1.0"
29
+ ```
30
+
31
+ If you use `pipx`, the same tagged source can be installed as an isolated CLI application:
32
+
33
+ ```bash
34
+ pipx install "git+https://github.com/LaimaWu/release-state-reconcile.git@v0.1.0"
35
+ ```
36
+
37
+ ### PyPI status
38
+
39
+ PyPI distribution is planned but not yet available. Do not use `pip install release-state-reconcile` until the package is published on PyPI.
40
+
41
+ ### Contributor and development install
42
+
43
+ From a source checkout, contributors can retain an editable installation:
44
+
45
+ ```bash
46
+ python -m pip install -e .
47
+ ```
48
+
49
+ ## CLI
50
+
51
+ ```bash
52
+ release-state-reconcile \
53
+ --candidate https://github.com/OWNER/REPOSITORY/issues/123 \
54
+ --release-branch release/1.2 \
55
+ --config repository.json \
56
+ --output report.md
57
+ ```
58
+
59
+ Use `--format json` for structured output. If `--output` is omitted, the report is written to standard output.
60
+
61
+ ## Output shape
62
+
63
+ The Markdown report includes an evidence-linked state table, related pull requests, explicit same- and cross-repository relationships, contradictions, and unknown or missing links.
64
+
65
+ ```text
66
+ Fact State
67
+ Original issue/PR CLOSED
68
+ Mainline merge MERGED | OPEN | UNKNOWN
69
+ Required backport PRs MERGED | MISSING | TARGET_BRANCH_MISMATCH | UNKNOWN
70
+ Backport/check state PASS | FAIL | PENDING | UNKNOWN
71
+ Release-note evidence PRESENT | ABSENT | UNKNOWN
72
+ Final tag/release containment CONTAINED | NOT_CONTAINED | UNKNOWN
73
+ ```
74
+
75
+ Each observation carries a public source URL, object type, object ID, observed state, and confidence.
76
+
77
+ ## Configuration
78
+
79
+ Repository conventions live in JSON rather than source code. A minimal configuration looks like this:
80
+
81
+ ```json
82
+ {
83
+ "mainline_branch": "main",
84
+ "backport": {"required": true, "search": true},
85
+ "completion": {"closed_is_complete": true, "labels": []},
86
+ "verification_labels": ["verified"],
87
+ "release_note": {"path_globs": ["changes/**"]},
88
+ "release": {"tag_regex": "^v1\\.2\\.[0-9]+$", "max_releases": 10}
89
+ }
90
+ ```
91
+
92
+ The selected release branch is always supplied explicitly on the command line. Configuration may describe the default branch, whether a backport is expected, repository-specific verification labels, release-note paths, and the release-tag convention.
93
+
94
+ ## Scope and non-goals
95
+
96
+ The tool observes and reconciles public GitHub state. It does not:
97
+
98
+ - decide whether a candidate belongs in a release;
99
+ - assess change risk or release-note quality;
100
+ - approve, merge, label, comment on, or otherwise mutate GitHub objects;
101
+ - orchestrate releases across multiple release lines;
102
+ - provide end-to-end release automation; or
103
+ - use an LLM or make AI-generated release decisions.
104
+
105
+ ## Read-only and security model
106
+
107
+ The GitHub client exposes only HTTP `GET` operations. Authentication is optional: `GITHUB_TOKEN` or `GH_TOKEN` may be supplied to increase public REST API limits, but the tool has no GitHub write methods. Reports may be written to a local path selected by the caller.
108
+
109
+ The runtime uses the Python standard library and the public GitHub REST API. Do not place tokens in configuration files or commit them to source control. See [SECURITY.md](SECURITY.md) for vulnerability reporting guidance.
110
+
111
+ ## Validation
112
+
113
+ The repository contains 21 deterministic unit tests covering closure semantics, relationship discovery, cross-repository preservation, target-branch mismatch handling, external-provider uncertainty, check aggregation, release-note path evidence, and the GET-only client surface. CI runs the full suite on Python 3.10, 3.11, 3.12, and 3.13.
114
+
115
+ Run locally with:
116
+
117
+ ```bash
118
+ python -m unittest discover -s tests -v
119
+ ```
120
+
121
+ ## Known limitations
122
+
123
+ - One repository and one selected release line are evaluated per run.
124
+ - Repository conventions may require configuration.
125
+ - Unsupported external change providers remain `UNKNOWN`.
126
+ - Private CI and off-GitHub approvals are outside scope.
127
+ - Candidate eligibility and release-note quality remain human judgment.
128
+ - Some public issues do not expose enough relationships to reconstruct a useful chain.
129
+ - A complete GitHub search establishes only that no qualifying result was returned for that query; it is not proof that an unreferenced change does not exist.
130
+
131
+ ## License
132
+
133
+ [MIT](LICENSE)
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "release-state-reconcile"
7
+ version = "0.1.0"
8
+ description = "Read-only CLI for reconciling release candidates across fixes, backports, checks, release notes, and published releases."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {file = "LICENSE"}
12
+ authors = [
13
+ {name = "LaimaWu"}
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Software Development :: Quality Assurance"
26
+ ]
27
+ dependencies = []
28
+
29
+ [project.urls]
30
+ Repository = "https://github.com/LaimaWu/release-state-reconcile"
31
+ Issues = "https://github.com/LaimaWu/release-state-reconcile/issues"
32
+
33
+ [project.scripts]
34
+ release-state-reconcile = "release_state_reconcile.cli:main"
35
+
36
+ [tool.setuptools]
37
+ package-dir = {"" = "src"}
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """Deterministic, read-only GitHub release-state reconciliation."""
2
+
3
+ from .reconcile import reconcile_candidate
4
+
5
+ __all__ = ["reconcile_candidate"]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
4
+
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from .github import GitHubAPIError, GitHubClient
9
+ from .reconcile import reconcile_candidate
10
+ from .report import render_json, render_markdown
11
+
12
+
13
+ def parser() -> argparse.ArgumentParser:
14
+ result = argparse.ArgumentParser(
15
+ description="Reconstruct release-candidate state from public GitHub data (GET-only)."
16
+ )
17
+ result.add_argument("--candidate", required=True, help="Public GitHub issue or PR URL")
18
+ result.add_argument("--release-branch", required=True, help="Target release branch")
19
+ result.add_argument("--config", required=True, type=Path, help="Repository configuration JSON")
20
+ result.add_argument("--format", choices=("markdown", "json"), default="markdown")
21
+ result.add_argument("--output", type=Path, help="Write report to this path instead of stdout")
22
+ return result
23
+
24
+
25
+ def main(argv: list[str] | None = None) -> int:
26
+ args = parser().parse_args(argv)
27
+ try:
28
+ config = json.loads(args.config.read_text(encoding="utf-8"))
29
+ report = reconcile_candidate(
30
+ GitHubClient(), args.candidate, args.release_branch, config
31
+ )
32
+ output = render_json(report) if args.format == "json" else render_markdown(report)
33
+ if args.output:
34
+ args.output.parent.mkdir(parents=True, exist_ok=True)
35
+ args.output.write_text(output + "\n", encoding="utf-8")
36
+ else:
37
+ sys.stdout.write(output + "\n")
38
+ except (ValueError, OSError, json.JSONDecodeError, GitHubAPIError) as exc:
39
+ print(f"error: {exc}", file=sys.stderr)
40
+ return 2
41
+ return 0
42
+
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import urllib.error
6
+ import urllib.parse
7
+ import urllib.request
8
+ from typing import Any
9
+
10
+
11
+ class GitHubAPIError(RuntimeError):
12
+ def __init__(self, status: int, url: str, message: str):
13
+ super().__init__(f"GitHub API {status} for {url}: {message}")
14
+ self.status = status
15
+ self.url = url
16
+
17
+
18
+ class GitHubClient:
19
+ """Small GET-only GitHub REST client.
20
+
21
+ There are deliberately no mutation methods in this class. Authentication is
22
+ optional and only expands the public REST rate limit.
23
+ """
24
+
25
+ api_root = "https://api.github.com"
26
+
27
+ def __init__(self, token: str | None = None, timeout: int = 30):
28
+ self.token = token or os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
29
+ self.timeout = timeout
30
+ self.requests: list[str] = []
31
+ self._cache: dict[str, Any] = {}
32
+
33
+ def _get(self, path: str, params: dict[str, str | int] | None = None) -> Any:
34
+ url = path if path.startswith("https://") else f"{self.api_root}{path}"
35
+ if params:
36
+ url = f"{url}?{urllib.parse.urlencode(params)}"
37
+ if url in self._cache:
38
+ return self._cache[url]
39
+ headers = {
40
+ "Accept": "application/vnd.github+json",
41
+ "User-Agent": "release-state-reconcile/0.1",
42
+ "X-GitHub-Api-Version": "2022-11-28",
43
+ }
44
+ if self.token:
45
+ headers["Authorization"] = f"Bearer {self.token}"
46
+ request = urllib.request.Request(url, method="GET", headers=headers)
47
+ self.requests.append(url)
48
+ try:
49
+ with urllib.request.urlopen(request, timeout=self.timeout) as response:
50
+ payload = json.loads(response.read().decode("utf-8"))
51
+ except urllib.error.HTTPError as exc:
52
+ body = exc.read().decode("utf-8", errors="replace")
53
+ try:
54
+ message = json.loads(body).get("message", body)
55
+ except json.JSONDecodeError:
56
+ message = body
57
+ raise GitHubAPIError(exc.code, url, message) from exc
58
+ self._cache[url] = payload
59
+ return payload
60
+
61
+ def issue(self, owner: str, repo: str, number: int) -> dict[str, Any]:
62
+ return self._get(f"/repos/{owner}/{repo}/issues/{number}")
63
+
64
+ def timeline(self, owner: str, repo: str, number: int) -> list[dict[str, Any]]:
65
+ return self._get(
66
+ f"/repos/{owner}/{repo}/issues/{number}/timeline", {"per_page": 100}
67
+ )
68
+
69
+ def comments(self, owner: str, repo: str, number: int) -> list[dict[str, Any]]:
70
+ return self._get(
71
+ f"/repos/{owner}/{repo}/issues/{number}/comments", {"per_page": 100}
72
+ )
73
+
74
+ def pull(self, owner: str, repo: str, number: int) -> dict[str, Any]:
75
+ return self._get(f"/repos/{owner}/{repo}/pulls/{number}")
76
+
77
+ def check_runs(self, owner: str, repo: str, sha: str) -> dict[str, Any]:
78
+ return self._get(
79
+ f"/repos/{owner}/{repo}/commits/{sha}/check-runs", {"per_page": 100}
80
+ )
81
+
82
+ def pull_files(self, owner: str, repo: str, number: int) -> list[dict[str, Any]]:
83
+ return self._get(
84
+ f"/repos/{owner}/{repo}/pulls/{number}/files", {"per_page": 100}
85
+ )
86
+
87
+ def branch(self, owner: str, repo: str, branch: str) -> dict[str, Any]:
88
+ quoted = urllib.parse.quote(branch, safe="")
89
+ return self._get(f"/repos/{owner}/{repo}/branches/{quoted}")
90
+
91
+ def search_pulls(
92
+ self, owner: str, repo: str, base: str, reference: str
93
+ ) -> dict[str, Any]:
94
+ query = f'repo:{owner}/{repo} is:pr base:"{base}" "{reference}"'
95
+ return self._get("/search/issues", {"q": query, "per_page": 100})
96
+
97
+ def releases(self, owner: str, repo: str) -> list[dict[str, Any]]:
98
+ return self._get(f"/repos/{owner}/{repo}/releases", {"per_page": 100})
99
+
100
+ def release_by_tag(self, owner: str, repo: str, tag: str) -> dict[str, Any]:
101
+ quoted = urllib.parse.quote(tag, safe="")
102
+ return self._get(f"/repos/{owner}/{repo}/releases/tags/{quoted}")
103
+
104
+ def compare(self, owner: str, repo: str, base: str, head: str) -> dict[str, Any]:
105
+ quoted_base = urllib.parse.quote(base, safe="")
106
+ quoted_head = urllib.parse.quote(head, safe="")
107
+ return self._get(f"/repos/{owner}/{repo}/compare/{quoted_base}...{quoted_head}")
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Evidence:
9
+ source_url: str
10
+ object_type: str
11
+ object_id: str
12
+ observed_state: str
13
+ confidence: str = "factual"
14
+
15
+
16
+ @dataclass
17
+ class Finding:
18
+ name: str
19
+ status: str
20
+ summary: str
21
+ evidence: list[Evidence] = field(default_factory=list)
22
+
23
+
24
+ @dataclass
25
+ class PullRecord:
26
+ number: int
27
+ url: str
28
+ title: str
29
+ state: str
30
+ merged: bool
31
+ merged_at: str | None
32
+ merge_sha: str | None
33
+ head_sha: str | None
34
+ base: str
35
+ labels: list[str]
36
+ relation: str
37
+ relation_evidence: list[Evidence] = field(default_factory=list)
38
+ changed_files: int = 0
39
+
40
+
41
+ @dataclass
42
+ class RelationshipRecord:
43
+ url: str
44
+ provider: str
45
+ repository: str | None
46
+ object_type: str
47
+ object_id: str
48
+ scope: str
49
+ relation: str
50
+ evidence: list[Evidence] = field(default_factory=list)
51
+
52
+
53
+ @dataclass
54
+ class CandidateReport:
55
+ candidate_url: str
56
+ repo: str
57
+ number: int
58
+ observed_at: str
59
+ target_release_branch: str
60
+ mainline_branch: str
61
+ findings: list[Finding]
62
+ related_pulls: list[PullRecord]
63
+ relationships: list[RelationshipRecord]
64
+ contradictions: list[Finding]
65
+ unknowns: list[Finding]
66
+ materially_useful: bool
67
+ api_requests: list[str] = field(default_factory=list)
68
+
69
+ def to_dict(self) -> dict[str, Any]:
70
+ return asdict(self)