open-code-review-toolkit 0.4.5__tar.gz → 0.4.7__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.
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/PKG-INFO +12 -5
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/README.md +11 -4
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/_version.py +2 -2
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/mcp.py +1 -1
- open_code_review_toolkit-0.4.7/src/ocr_toolkit/posting/approval.py +125 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/posting/formatting.py +46 -43
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/posting/gitlab.py +25 -1
- open_code_review_toolkit-0.4.7/src/ocr_toolkit/posting/gitlab_approval.py +230 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/posting/markers.py +8 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/posting/settings.py +45 -10
- open_code_review_toolkit-0.4.7/src/ocr_toolkit/posting/suggestions.py +212 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/posting/workflow.py +222 -94
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/preflight.py +1 -1
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/.gitignore +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/LICENSE +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/pyproject.toml +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/__init__.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/cli.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/common/__init__.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/common/git.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/common/language.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/common/markdown.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/common/redaction.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/config_writer.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/configure.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/__init__.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/__main__.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/ansible.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/ansible_requirements.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/artifacts.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/categorize.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/collect.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/collectors.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/composer_manifests.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/coverage.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/go_manifests.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/infrastructure.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/invocation.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/javascript_manifests.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/manifest_model.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/model.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/project.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/python_manifests.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/repository.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/store.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/mcp_config.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/ocr_result.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/posting/__init__.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/posting/__main__.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/posting/comments.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/posting/payloads.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/posting/result.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/posting/snapshot.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/providers/__init__.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/providers/gitlab.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/py.typed +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/result_contract.py +0 -0
- {open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/review_runner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: open-code-review-toolkit
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.7
|
|
4
4
|
Summary: Unofficial GitLab CI integration layer for Open Code Review
|
|
5
5
|
Project-URL: Homepage, https://github.com/xeonvs/open-code-review-toolkit
|
|
6
6
|
Project-URL: Repository, https://github.com/xeonvs/open-code-review-toolkit
|
|
@@ -243,7 +243,7 @@ ocr --version
|
|
|
243
243
|
ocr-ci --help
|
|
244
244
|
```
|
|
245
245
|
|
|
246
|
-
The current compatibility target is OCR `1.
|
|
246
|
+
The current compatibility target is OCR `1.9.1`. CI should pin the release and verify its published checksum before execution.
|
|
247
247
|
The [versioned compatibility policy](docs/compatibility.md) records tested assets and evidence and describes the conservative Dependabot-like qualification workflow for later upstream releases.
|
|
248
248
|
Review output defaults to English. `OCR_REVIEW_LANGUAGE` accepts another explicit language name when a project needs localized review output; for example, `OCR_REVIEW_LANGUAGE=Russian`.
|
|
249
249
|
|
|
@@ -255,11 +255,18 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
|
|
|
255
255
|
|
|
256
256
|
Suppression uses both the GitLab diff position and a stable finding fingerprint, so ordinary line shifts do not normally bring the same bug back. A materially changed finding can still receive a new discussion. See [GitLab review operations](docs/operations.md) for the complete lifecycle, posting modes, permissions, failure behavior, and Mermaid state diagram.
|
|
257
257
|
|
|
258
|
+
After every current review note publishes, the GitLab adapter can add a
|
|
259
|
+
conservative approval bound to the exact reviewed source SHA. This write is
|
|
260
|
+
enabled by default; set `OCR_AUTO_APPROVE=false` before upgrading when the bot
|
|
261
|
+
must remain comment-only. GitLab approval rules and protected-branch policy
|
|
262
|
+
remain authoritative. The toolkit only adds an eligible approval; it never
|
|
263
|
+
removes an existing approval when a later review is ineligible or disabled.
|
|
264
|
+
|
|
258
265
|
Project-wide accepted tradeoffs can be recorded separately in `.opencodereview/accepted-decisions.md`; the evidence collector supplies target-ref decisions to OCR and never lets a source change self-authorize its own review. See [Accepted project decisions](docs/configuration.md#accepted-project-decisions) for the entry format, inline marker convention, security boundary, and limitations.
|
|
259
266
|
|
|
260
|
-
## Project
|
|
267
|
+
## Project architecture
|
|
261
268
|
|
|
262
|
-
The
|
|
269
|
+
The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed evidence through the mandatory built-in read-only MCP server. Reviewed external stdio or native HTTPS MCP servers compose alongside it without replacing the built-in evidence boundary.
|
|
263
270
|
|
|
264
271
|
- [Toolkit strategy](docs/engineering/toolkit_strategy.md) - durable product boundaries, architecture, invariants, and non-goals.
|
|
265
272
|
- [Roadmap](ROADMAP.md) - milestone status, dependencies, outcomes, and completion signals.
|
|
@@ -283,7 +290,7 @@ See the fully synthetic [`examples/gitlab/ocr-review.gitlab-ci.yml`](examples/gi
|
|
|
283
290
|
|
|
284
291
|
## Configuration and safety
|
|
285
292
|
|
|
286
|
-
Configuration is environment-
|
|
293
|
+
Configuration is environment-driven. The [configuration reference](docs/configuration.md) documents supported `OCR_*`, `CI_*`, `GITLAB_*`, and MCP inputs. Posting requires `GITLAB_API_TOKEN`; job tokens and legacy aliases are deliberately unsupported.
|
|
287
294
|
|
|
288
295
|
Repository content, OCR output, and provider responses are untrusted inputs. The toolkit applies bounded reads and writes, secret redaction, Unicode normalization, Markdown/quick-action neutralization, fingerprinted comments, ownership boundaries for human replies, and rollback controls. Review the [security and trust model](docs/security.md) before enabling write access.
|
|
289
296
|
|
|
@@ -17,7 +17,7 @@ ocr --version
|
|
|
17
17
|
ocr-ci --help
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
The current compatibility target is OCR `1.
|
|
20
|
+
The current compatibility target is OCR `1.9.1`. CI should pin the release and verify its published checksum before execution.
|
|
21
21
|
The [versioned compatibility policy](docs/compatibility.md) records tested assets and evidence and describes the conservative Dependabot-like qualification workflow for later upstream releases.
|
|
22
22
|
Review output defaults to English. `OCR_REVIEW_LANGUAGE` accepts another explicit language name when a project needs localized review output; for example, `OCR_REVIEW_LANGUAGE=Russian`.
|
|
23
23
|
|
|
@@ -29,11 +29,18 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
|
|
|
29
29
|
|
|
30
30
|
Suppression uses both the GitLab diff position and a stable finding fingerprint, so ordinary line shifts do not normally bring the same bug back. A materially changed finding can still receive a new discussion. See [GitLab review operations](docs/operations.md) for the complete lifecycle, posting modes, permissions, failure behavior, and Mermaid state diagram.
|
|
31
31
|
|
|
32
|
+
After every current review note publishes, the GitLab adapter can add a
|
|
33
|
+
conservative approval bound to the exact reviewed source SHA. This write is
|
|
34
|
+
enabled by default; set `OCR_AUTO_APPROVE=false` before upgrading when the bot
|
|
35
|
+
must remain comment-only. GitLab approval rules and protected-branch policy
|
|
36
|
+
remain authoritative. The toolkit only adds an eligible approval; it never
|
|
37
|
+
removes an existing approval when a later review is ineligible or disabled.
|
|
38
|
+
|
|
32
39
|
Project-wide accepted tradeoffs can be recorded separately in `.opencodereview/accepted-decisions.md`; the evidence collector supplies target-ref decisions to OCR and never lets a source change self-authorize its own review. See [Accepted project decisions](docs/configuration.md#accepted-project-decisions) for the entry format, inline marker convention, security boundary, and limitations.
|
|
33
40
|
|
|
34
|
-
## Project
|
|
41
|
+
## Project architecture
|
|
35
42
|
|
|
36
|
-
The
|
|
43
|
+
The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed evidence through the mandatory built-in read-only MCP server. Reviewed external stdio or native HTTPS MCP servers compose alongside it without replacing the built-in evidence boundary.
|
|
37
44
|
|
|
38
45
|
- [Toolkit strategy](docs/engineering/toolkit_strategy.md) - durable product boundaries, architecture, invariants, and non-goals.
|
|
39
46
|
- [Roadmap](ROADMAP.md) - milestone status, dependencies, outcomes, and completion signals.
|
|
@@ -57,7 +64,7 @@ See the fully synthetic [`examples/gitlab/ocr-review.gitlab-ci.yml`](examples/gi
|
|
|
57
64
|
|
|
58
65
|
## Configuration and safety
|
|
59
66
|
|
|
60
|
-
Configuration is environment-
|
|
67
|
+
Configuration is environment-driven. The [configuration reference](docs/configuration.md) documents supported `OCR_*`, `CI_*`, `GITLAB_*`, and MCP inputs. Posting requires `GITLAB_API_TOKEN`; job tokens and legacy aliases are deliberately unsupported.
|
|
61
68
|
|
|
62
69
|
Repository content, OCR output, and provider responses are untrusted inputs. The toolkit applies bounded reads and writes, secret redaction, Unicode normalization, Markdown/quick-action neutralization, fingerprinted comments, ownership boundaries for human replies, and rollback controls. Review the [security and trust model](docs/security.md) before enabling write access.
|
|
63
70
|
|
{open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/_version.py
RENAMED
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.4.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 4,
|
|
21
|
+
__version__ = version = '0.4.7'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 4, 7)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
{open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/evidence/mcp.py
RENAMED
|
@@ -106,7 +106,7 @@ def _optional_filter(arguments: dict[str, object], name: str) -> str | None:
|
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
def _encode_cursor(offset: int, query: _Query) -> str:
|
|
109
|
-
"""Encode an opaque,
|
|
109
|
+
"""Encode an opaque, query-bound self-validating pagination position."""
|
|
110
110
|
|
|
111
111
|
state = f"{offset}:{query.key()}"
|
|
112
112
|
checksum = hashlib.sha256(f"ocr-evidence-v1:{state}".encode()).hexdigest()[:16]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Conservative policy and typed outcomes for GitLab automatic approval."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ocr_toolkit.posting.settings import BooleanSetting
|
|
10
|
+
from ocr_toolkit.result_contract import ReviewOutcome
|
|
11
|
+
|
|
12
|
+
ALLOWED_CATEGORIES = frozenset({"style", "documentation", "maintainability"})
|
|
13
|
+
MAX_APPROVABLE_FINDINGS = 3
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ApprovalStatus(str, Enum):
|
|
17
|
+
"""Closed public states for one automatic-approval transaction."""
|
|
18
|
+
|
|
19
|
+
APPROVED = "approved"
|
|
20
|
+
NOT_ELIGIBLE = "not eligible"
|
|
21
|
+
DISABLED = "disabled"
|
|
22
|
+
SKIPPED = "skipped"
|
|
23
|
+
FAILED = "failed"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class ApprovalResult:
|
|
28
|
+
"""Bounded status rendered in the review summary and runner log."""
|
|
29
|
+
|
|
30
|
+
status: ApprovalStatus
|
|
31
|
+
reason: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class ApprovalEligibility:
|
|
36
|
+
"""Policy conclusion before provider state is consulted."""
|
|
37
|
+
|
|
38
|
+
eligible: bool
|
|
39
|
+
result: ApprovalResult
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def evaluate_approval_policy(
|
|
43
|
+
setting: BooleanSetting,
|
|
44
|
+
outcome: ReviewOutcome,
|
|
45
|
+
comments: list[dict[str, Any]],
|
|
46
|
+
warnings: list[Any],
|
|
47
|
+
omitted_count: int,
|
|
48
|
+
) -> ApprovalEligibility:
|
|
49
|
+
"""Evaluate the fixed v0.4.7 policy from authoritative OCR data."""
|
|
50
|
+
|
|
51
|
+
if not setting.enabled:
|
|
52
|
+
reason = (
|
|
53
|
+
"configuration was invalid and failed closed"
|
|
54
|
+
if not setting.valid
|
|
55
|
+
else "disabled by OCR_AUTO_APPROVE"
|
|
56
|
+
)
|
|
57
|
+
return ApprovalEligibility(
|
|
58
|
+
False,
|
|
59
|
+
ApprovalResult(ApprovalStatus.DISABLED, reason),
|
|
60
|
+
)
|
|
61
|
+
if not outcome.manifest_present:
|
|
62
|
+
reason = "the OCR result has no authoritative coverage manifest"
|
|
63
|
+
elif outcome.kind != "clean" or outcome.budget_exceeded:
|
|
64
|
+
reason = "the OCR review did not complete cleanly"
|
|
65
|
+
elif outcome.failed_count or outcome.waived_count:
|
|
66
|
+
reason = "coverage contained failed or waived items"
|
|
67
|
+
elif warnings:
|
|
68
|
+
reason = "the OCR review reported warnings"
|
|
69
|
+
elif omitted_count:
|
|
70
|
+
reason = "one or more findings were omitted from publication"
|
|
71
|
+
elif len(comments) > MAX_APPROVABLE_FINDINGS:
|
|
72
|
+
reason = f"the review reported more than {MAX_APPROVABLE_FINDINGS} findings"
|
|
73
|
+
else:
|
|
74
|
+
reason = ""
|
|
75
|
+
|
|
76
|
+
if reason:
|
|
77
|
+
return ApprovalEligibility(
|
|
78
|
+
False,
|
|
79
|
+
ApprovalResult(ApprovalStatus.NOT_ELIGIBLE, reason),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
for comment in comments:
|
|
83
|
+
severity = comment.get("severity")
|
|
84
|
+
category = comment.get("category")
|
|
85
|
+
if severity != "low":
|
|
86
|
+
return ApprovalEligibility(
|
|
87
|
+
False,
|
|
88
|
+
ApprovalResult(
|
|
89
|
+
ApprovalStatus.NOT_ELIGIBLE,
|
|
90
|
+
"a finding had a blocking or malformed severity",
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
if not isinstance(category, str) or category not in ALLOWED_CATEGORIES:
|
|
94
|
+
return ApprovalEligibility(
|
|
95
|
+
False,
|
|
96
|
+
ApprovalResult(
|
|
97
|
+
ApprovalStatus.NOT_ELIGIBLE,
|
|
98
|
+
"a finding had a blocking or malformed category",
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return ApprovalEligibility(
|
|
103
|
+
True,
|
|
104
|
+
ApprovalResult(
|
|
105
|
+
ApprovalStatus.SKIPPED,
|
|
106
|
+
"awaiting post-publication SHA verification",
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def approval_summary_line(result: ApprovalResult) -> str:
|
|
112
|
+
"""Render exactly one bounded automatic-approval state line."""
|
|
113
|
+
|
|
114
|
+
return f"- Automatic approval: `{result.status.value}` — {result.reason}."
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def provisional_approval_result(eligibility: ApprovalEligibility) -> ApprovalResult:
|
|
118
|
+
"""Return a fail-closed state safe to publish before provider readback."""
|
|
119
|
+
|
|
120
|
+
if not eligibility.eligible:
|
|
121
|
+
return eligibility.result
|
|
122
|
+
return ApprovalResult(
|
|
123
|
+
ApprovalStatus.FAILED,
|
|
124
|
+
"automatic approval has not yet been confirmed",
|
|
125
|
+
)
|
|
@@ -17,6 +17,7 @@ from ocr_toolkit.common.markdown import (
|
|
|
17
17
|
)
|
|
18
18
|
from ocr_toolkit.common.redaction import redact_sensitive
|
|
19
19
|
from ocr_toolkit.ocr_result import TOOLKIT_RESULT_SCHEMA_VERSION
|
|
20
|
+
from ocr_toolkit.posting.approval import ApprovalResult, approval_summary_line
|
|
20
21
|
from ocr_toolkit.posting.comments import (
|
|
21
22
|
clean_text,
|
|
22
23
|
code_text,
|
|
@@ -34,14 +35,13 @@ from ocr_toolkit.posting.settings import (
|
|
|
34
35
|
MAX_REVIEWER_GUIDE_LABEL_CHARS,
|
|
35
36
|
MAX_REVIEWER_GUIDE_LOCATION_CHARS,
|
|
36
37
|
MAX_REVIEWER_GUIDE_TEXT_CHARS,
|
|
37
|
-
MAX_SUGGESTION_CODE_CHARS,
|
|
38
|
-
MAX_SUGGESTION_SPAN_LINES,
|
|
39
38
|
MAX_TOOL_CALL_NAME_CHARS,
|
|
40
39
|
MAX_TOOL_CALL_SUMMARY_TOOLS,
|
|
41
40
|
SUGGESTION_HEADER,
|
|
42
41
|
post_emoji,
|
|
43
42
|
post_mode,
|
|
44
43
|
)
|
|
44
|
+
from ocr_toolkit.posting.suggestions import SuggestionDecision, SuggestionState
|
|
45
45
|
|
|
46
46
|
OCR_FINDING_CATEGORIES = {
|
|
47
47
|
"bug",
|
|
@@ -85,22 +85,6 @@ CATEGORY_EMOJI = {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
def suggestion_range_suffix(comment: dict[str, Any]) -> str:
|
|
89
|
-
"""Return a GitLab suggestion range suffix."""
|
|
90
|
-
|
|
91
|
-
end_line = line_number(comment.get("end_line") or comment.get("line"))
|
|
92
|
-
start_line = line_number(comment.get("start_line") or comment.get("line") or end_line)
|
|
93
|
-
|
|
94
|
-
if start_line <= 0 or end_line <= 0 or start_line > end_line:
|
|
95
|
-
return ""
|
|
96
|
-
|
|
97
|
-
span = end_line - start_line
|
|
98
|
-
if span > MAX_SUGGESTION_SPAN_LINES:
|
|
99
|
-
return ""
|
|
100
|
-
|
|
101
|
-
return f"-0+{span}"
|
|
102
|
-
|
|
103
|
-
|
|
104
88
|
def inline_code(value: str) -> str:
|
|
105
89
|
"""Return a Markdown inline-code representation safe for backticks."""
|
|
106
90
|
|
|
@@ -136,34 +120,33 @@ def format_finding_tags(comment: dict[str, Any], *, emoji: bool | None = None) -
|
|
|
136
120
|
return " · ".join(tags)
|
|
137
121
|
|
|
138
122
|
|
|
139
|
-
def format_suggestion_block(
|
|
140
|
-
"""
|
|
123
|
+
def format_suggestion_block(decision: SuggestionDecision) -> str:
|
|
124
|
+
"""Render one previously validated GitLab suggestion decision."""
|
|
141
125
|
|
|
142
|
-
|
|
143
|
-
if not suggestion.strip() or comment.get("_ocr_suggestion_noop") is True:
|
|
144
|
-
return ""
|
|
145
|
-
|
|
146
|
-
if "```" in suggestion:
|
|
126
|
+
if decision.state in {SuggestionState.ABSENT, SuggestionState.NO_OP}:
|
|
147
127
|
return ""
|
|
128
|
+
if decision.state is SuggestionState.OMITTED:
|
|
129
|
+
return f"\n\nSuggestion block was omitted because {decision.omission_message}."
|
|
130
|
+
return (
|
|
131
|
+
f"\n\n{SUGGESTION_HEADER}\n```suggestion:{decision.range_suffix}\n"
|
|
132
|
+
f"{decision.replacement}\n```"
|
|
133
|
+
)
|
|
148
134
|
|
|
149
|
-
if any(line.lstrip().startswith("/") for line in suggestion.splitlines()):
|
|
150
|
-
return ""
|
|
151
135
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
"\n\nSuggestion block was omitted because the generated replacement "
|
|
155
|
-
"was too large to publish safely."
|
|
156
|
-
)
|
|
136
|
+
def format_suggestion_omission(decision: SuggestionDecision) -> str:
|
|
137
|
+
"""Render a bounded explanation for a withheld actionable suggestion."""
|
|
157
138
|
|
|
158
|
-
|
|
159
|
-
if not range_suffix:
|
|
139
|
+
if decision.state is not SuggestionState.OMITTED:
|
|
160
140
|
return ""
|
|
161
|
-
|
|
162
|
-
return f"\n\n{SUGGESTION_HEADER}\n```suggestion:{range_suffix}\n{suggestion}\n```"
|
|
141
|
+
return f"\n\nSuggestion block was omitted because {decision.omission_message}."
|
|
163
142
|
|
|
164
143
|
|
|
165
144
|
def format_inline_comment(
|
|
166
|
-
comment: dict[str, Any],
|
|
145
|
+
comment: dict[str, Any],
|
|
146
|
+
include_suggestion: bool = True,
|
|
147
|
+
*,
|
|
148
|
+
suggestion_decision: SuggestionDecision | None = None,
|
|
149
|
+
emoji: bool | None = None,
|
|
167
150
|
) -> str:
|
|
168
151
|
"""Format one OCR comment as Markdown for an inline GitLab discussion."""
|
|
169
152
|
|
|
@@ -173,12 +156,19 @@ def format_inline_comment(
|
|
|
173
156
|
tags = format_finding_tags(comment, emoji=emoji)
|
|
174
157
|
body = f"{tags}\n\n{content}" if tags else content
|
|
175
158
|
if include_suggestion:
|
|
176
|
-
body += format_suggestion_block(
|
|
159
|
+
body += format_suggestion_block(
|
|
160
|
+
suggestion_decision or SuggestionDecision(SuggestionState.ABSENT)
|
|
161
|
+
)
|
|
177
162
|
|
|
178
163
|
return body
|
|
179
164
|
|
|
180
165
|
|
|
181
|
-
def format_fallback_comment(
|
|
166
|
+
def format_fallback_comment(
|
|
167
|
+
comment: dict[str, Any],
|
|
168
|
+
*,
|
|
169
|
+
suggestion_decision: SuggestionDecision | None = None,
|
|
170
|
+
emoji: bool | None = None,
|
|
171
|
+
) -> str:
|
|
182
172
|
"""Format an OCR comment for a fallback non-inline MR note."""
|
|
183
173
|
|
|
184
174
|
path = clean_text(comment.get("path")) or "unknown"
|
|
@@ -202,10 +192,13 @@ def format_fallback_comment(comment: dict[str, Any], *, emoji: bool | None = Non
|
|
|
202
192
|
f"{format_inline_comment(comment, include_suggestion=False, emoji=emoji)}"
|
|
203
193
|
)
|
|
204
194
|
|
|
195
|
+
decision = suggestion_decision or SuggestionDecision(SuggestionState.ABSENT)
|
|
196
|
+
body += format_suggestion_omission(decision)
|
|
197
|
+
|
|
205
198
|
existing = code_text(comment.get("existing_code"))
|
|
206
199
|
suggestion = code_text(comment.get("suggestion_code"))
|
|
207
200
|
|
|
208
|
-
if existing.strip() and suggestion.strip() and
|
|
201
|
+
if existing.strip() and suggestion.strip() and decision.state is not SuggestionState.NO_OP:
|
|
209
202
|
body += "\n\n<details><summary>Suggested change details</summary>\n\n"
|
|
210
203
|
body += "**Before:**\n"
|
|
211
204
|
body += markdown_code_block(
|
|
@@ -228,16 +221,23 @@ def format_fallback_comment(comment: dict[str, Any], *, emoji: bool | None = Non
|
|
|
228
221
|
|
|
229
222
|
|
|
230
223
|
def format_fallback_comment_chunks(
|
|
231
|
-
comments: Sequence[dict[str, Any]],
|
|
224
|
+
comments: Sequence[tuple[dict[str, Any], SuggestionDecision]],
|
|
225
|
+
*,
|
|
226
|
+
emoji: bool | None = None,
|
|
232
227
|
) -> list[str]:
|
|
233
228
|
"""Split fallback comments into safe chunks before publishing MR notes."""
|
|
234
229
|
|
|
235
230
|
chunks: list[str] = []
|
|
236
231
|
current = ""
|
|
237
232
|
|
|
238
|
-
for comment in comments:
|
|
233
|
+
for comment, suggestion_decision in comments:
|
|
239
234
|
item = truncate_note_body(
|
|
240
|
-
format_fallback_comment(
|
|
235
|
+
format_fallback_comment(
|
|
236
|
+
comment,
|
|
237
|
+
suggestion_decision=suggestion_decision,
|
|
238
|
+
emoji=emoji,
|
|
239
|
+
),
|
|
240
|
+
max_chars=FALLBACK_NOTE_CHUNK_BUDGET,
|
|
241
241
|
)
|
|
242
242
|
separator = "\n\n---\n\n" if current else ""
|
|
243
243
|
|
|
@@ -776,6 +776,7 @@ def summarize_result(
|
|
|
776
776
|
coverage_diagnostics: CoverageDiagnostics | None = None,
|
|
777
777
|
warnings: Sequence[Any] = (),
|
|
778
778
|
suppressed_count: int = 0,
|
|
779
|
+
approval_result: ApprovalResult | None = None,
|
|
779
780
|
emoji: bool | None = None,
|
|
780
781
|
) -> str:
|
|
781
782
|
"""Build one decision-first summary for every validated OCR outcome."""
|
|
@@ -892,6 +893,8 @@ def summarize_result(
|
|
|
892
893
|
technical.append(
|
|
893
894
|
f"- Posting: {inline_count} inline, {fallback_count} fallback, {omitted_count} omitted"
|
|
894
895
|
)
|
|
896
|
+
if approval_result is not None:
|
|
897
|
+
technical.append(approval_summary_line(approval_result))
|
|
895
898
|
if suppressed_count:
|
|
896
899
|
technical.append(f"- Reviewer suppression: {suppressed_count}")
|
|
897
900
|
if coverage_summary:
|
{open_code_review_toolkit-0.4.5 → open_code_review_toolkit-0.4.7}/src/ocr_toolkit/posting/gitlab.py
RENAMED
|
@@ -58,6 +58,7 @@ class GitLabWriteResult:
|
|
|
58
58
|
|
|
59
59
|
status: str
|
|
60
60
|
response: Any | None = None
|
|
61
|
+
http_status: int | None = None
|
|
61
62
|
|
|
62
63
|
@property
|
|
63
64
|
def posted(self) -> bool:
|
|
@@ -282,7 +283,7 @@ def api_write_url_detailed(
|
|
|
282
283
|
print(f"GitLab API error {exc.code} for {method} {url}: {safe_body}", file=sys.stderr)
|
|
283
284
|
if _is_invalid_position_error(exc.code, raw_body):
|
|
284
285
|
return GitLabWriteResult("invalid_position")
|
|
285
|
-
return GitLabWriteResult("write_failed")
|
|
286
|
+
return GitLabWriteResult("write_failed", http_status=exc.code)
|
|
286
287
|
except GitLabResponseTooLarge as exc:
|
|
287
288
|
print(f"GitLab API response too large for {method} {url}: {exc}", file=sys.stderr)
|
|
288
289
|
return GitLabWriteResult("write_failed")
|
|
@@ -515,6 +516,29 @@ def delete_plain_note(config: GitLabConfig, note_id: int) -> bool:
|
|
|
515
516
|
return response is not None
|
|
516
517
|
|
|
517
518
|
|
|
519
|
+
def update_plain_note(config: GitLabConfig, note_id: int, body: str) -> GitLabWriteResult:
|
|
520
|
+
"""Update one known toolkit-owned summary without retrying the write."""
|
|
521
|
+
|
|
522
|
+
return api_write_url_detailed(
|
|
523
|
+
url=f"{config.api_base}/notes/{note_id}",
|
|
524
|
+
api_token=config.api_token,
|
|
525
|
+
auth_header=config.auth_header,
|
|
526
|
+
data={"body": build_marked_note_body(body)},
|
|
527
|
+
method="PUT",
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def approve_merge_request(config: GitLabConfig, sha: str) -> GitLabWriteResult:
|
|
532
|
+
"""Approve exactly one merge-request head without retrying the write."""
|
|
533
|
+
|
|
534
|
+
return api_write_url_detailed(
|
|
535
|
+
url=f"{config.api_base}/approve",
|
|
536
|
+
api_token=config.api_token,
|
|
537
|
+
auth_header=config.auth_header,
|
|
538
|
+
data={"sha": sha},
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
|
|
518
542
|
def delete_discussion_note(config: GitLabConfig, discussion_id: str, note_id: int) -> bool:
|
|
519
543
|
"""Delete a note inside a merge request discussion thread."""
|
|
520
544
|
|