open-code-review-toolkit 0.8.4__tar.gz → 0.8.6__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 (110) hide show
  1. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/PKG-INFO +30 -3
  2. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/README.md +29 -2
  3. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/_version.py +2 -2
  4. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/posting/formatting.py +10 -2
  5. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/posting/workflow.py +1 -1
  6. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/preflight.py +1 -1
  7. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/provider_failure.py +177 -31
  8. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/review_runner.py +35 -14
  9. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/.gitignore +0 -0
  10. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/LICENSE +0 -0
  11. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/pyproject.toml +0 -0
  12. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/__init__.py +0 -0
  13. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/cli.py +0 -0
  14. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/common/__init__.py +0 -0
  15. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/common/filesystem.py +0 -0
  16. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/common/git.py +0 -0
  17. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/common/language.py +0 -0
  18. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/common/markdown.py +0 -0
  19. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/common/redaction.py +0 -0
  20. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/config_writer.py +0 -0
  21. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/configure.py +0 -0
  22. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/context/__init__.py +0 -0
  23. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/context/adapters.py +0 -0
  24. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/context/broker.py +0 -0
  25. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/context/contracts.py +0 -0
  26. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/context/dlp.py +0 -0
  27. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/context/mcp.py +0 -0
  28. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/context/policy.py +0 -0
  29. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/context/recognizers.py +0 -0
  30. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/context/store.py +0 -0
  31. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/__init__.py +0 -0
  32. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/__main__.py +0 -0
  33. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/actions.py +0 -0
  34. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/artifacts.py +0 -0
  35. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/categorize.py +0 -0
  36. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/collect.py +0 -0
  37. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/collectors/__init__.py +0 -0
  38. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/collectors/graphs.py +0 -0
  39. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/collectors/orchestration.py +0 -0
  40. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/collectors/projections.py +0 -0
  41. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/collectors/registry.py +0 -0
  42. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/collectors/sources.py +0 -0
  43. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/coverage.py +0 -0
  44. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/ecosystems/__init__.py +0 -0
  45. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/ecosystems/ansible/__init__.py +0 -0
  46. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/ecosystems/ansible/requirements.py +0 -0
  47. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/ecosystems/ansible/topology.py +0 -0
  48. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/ecosystems/contracts.py +0 -0
  49. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/ecosystems/go.py +0 -0
  50. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/ecosystems/javascript.py +0 -0
  51. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/ecosystems/php.py +0 -0
  52. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/ecosystems/python.py +0 -0
  53. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/frameworks/__init__.py +0 -0
  54. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/frameworks/contracts.py +0 -0
  55. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/frameworks/detection.py +0 -0
  56. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/frameworks/providers/__init__.py +0 -0
  57. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/frameworks/providers/frontend.py +0 -0
  58. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/frameworks/providers/go.py +0 -0
  59. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/frameworks/providers/php.py +0 -0
  60. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/frameworks/providers/python.py +0 -0
  61. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/frameworks/registry.py +0 -0
  62. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/frameworks/schema.py +0 -0
  63. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/frameworks/templates.py +0 -0
  64. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/infrastructure.py +0 -0
  65. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/invocation.py +0 -0
  66. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/mcp.py +0 -0
  67. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/model.py +0 -0
  68. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/policy/__init__.py +0 -0
  69. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/policy/contracts.py +0 -0
  70. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/policy/decisions.py +0 -0
  71. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/policy/guidance.py +0 -0
  72. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/policy/registry.py +0 -0
  73. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/policy/schema.py +0 -0
  74. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/policy/scopes.py +0 -0
  75. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/project.py +0 -0
  76. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/repository.py +0 -0
  77. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/review_context.py +0 -0
  78. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/store/__init__.py +0 -0
  79. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/store/atomic.py +0 -0
  80. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/store/contracts.py +0 -0
  81. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/store/core.py +0 -0
  82. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/store/readback.py +0 -0
  83. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/evidence/store/values.py +0 -0
  84. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/mcp_config.py +0 -0
  85. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/ocr_result.py +0 -0
  86. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/posting/__init__.py +0 -0
  87. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/posting/__main__.py +0 -0
  88. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/posting/approval.py +0 -0
  89. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/posting/comments.py +0 -0
  90. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/posting/gitlab.py +0 -0
  91. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/posting/gitlab_approval.py +0 -0
  92. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/posting/markers.py +0 -0
  93. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/posting/payloads.py +0 -0
  94. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/posting/reconciliation.py +0 -0
  95. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/posting/result.py +0 -0
  96. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/posting/settings.py +0 -0
  97. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/posting/snapshot.py +0 -0
  98. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/posting/suggestions.py +0 -0
  99. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/posting/transaction.py +0 -0
  100. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/pre_execution.py +0 -0
  101. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/provider_config.py +0 -0
  102. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/providers/__init__.py +0 -0
  103. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/providers/gitlab.py +0 -0
  104. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/providers/gitlab_context.py +0 -0
  105. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/providers/gitlab_discussions.py +0 -0
  106. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/providers/gitlab_identity.py +0 -0
  107. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/providers/gitlab_remediation.py +0 -0
  108. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/py.typed +0 -0
  109. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/result_contract.py +0 -0
  110. {open_code_review_toolkit-0.8.4 → open_code_review_toolkit-0.8.6}/src/ocr_toolkit/result_usage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-code-review-toolkit
3
- Version: 0.8.4
3
+ Version: 0.8.6
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
@@ -238,18 +238,45 @@ Open Code Review Toolkit is an unofficial GitLab CI integration layer for [Aliba
238
238
 
239
239
  ## Install
240
240
 
241
- Install the Python package from PyPI and install a supported OCR binary separately:
241
+ Python 3.12 through 3.14 is supported. Install the toolkit CLI in an isolated tool
242
+ environment:
242
243
 
243
244
  ```console
245
+ uv tool install open-code-review-toolkit
246
+ ```
247
+
248
+ If `uv` is unavailable, use `pip` only inside an activated virtual environment; do not
249
+ install the toolkit into a global Python environment:
250
+
251
+ ```console
252
+ python -m venv .venv
253
+ . .venv/bin/activate
244
254
  python -m pip install open-code-review-toolkit
255
+ ```
256
+
257
+ The toolkit does not bundle OCR. Download the platform asset for exact
258
+ [Open Code Review 1.11.0](https://github.com/alibaba/open-code-review/releases/tag/v1.11.0),
259
+ verify it before installation against the [compatibility manifest](compatibility/ocr-support.json),
260
+ and place the verified binary on `PATH`. The current Linux amd64 digest is
261
+ `13f68cc2eca1a36d42140e9d37797b68fea5cbbf4b6345ec01ec1b06910fab60`; Darwin arm64 is
262
+ `ac8bf5a0fcd176bb9dcc15b169e90f4b52bf32787adef17a850489dbed97fb78`. The manifest owns
263
+ the other platform hashes.
264
+
265
+ Verify the installation without contacting an LLM provider:
266
+
267
+ ```console
245
268
  ocr --version
246
269
  ocr-ci --help
247
270
  ```
248
271
 
272
+ `ocr --version` must report `open-code-review v1.11.0`; `ocr-ci --help` must exit
273
+ successfully. `ocr-ci preflight` is an operational configuration check, not the installation
274
+ smoke test.
275
+
249
276
  The exact recommended OCR release and its verified asset checksums live in the [versioned compatibility manifest](compatibility/ocr-support.json). CI should pin that release and checksum before execution.
250
277
  The [versioned compatibility policy](docs/compatibility.md) records tested assets and evidence and describes the conservative Dependabot-like qualification workflow for later upstream releases.
251
278
  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`.
252
- The current OCR 1.10.1 integration defaults `OCR_REVIEW_EFFORT` to `medium` for two review rounds. `low` and `high` are explicit one- and three-round alternatives; see the [configuration reference](docs/configuration.md#review-effort) for cost, budget, and precedence boundaries.
279
+ The current OCR 1.11.0 integration defaults `OCR_REVIEW_EFFORT` to `medium` for two review rounds. `low` and `high` are explicit one- and three-round alternatives; see the [configuration reference](docs/configuration.md#review-effort) for cost, budget, and precedence boundaries.
253
280
 
254
281
  Stable distributions are published to [PyPI](https://pypi.org/project/open-code-review-toolkit/) and mirrored as checksum-listed, provenance-attested assets in the corresponding [GitHub Release](https://github.com/xeonvs/open-code-review-toolkit/releases). Development snapshots are published only to TestPyPI.
255
282
 
@@ -11,18 +11,45 @@ Open Code Review Toolkit is an unofficial GitLab CI integration layer for [Aliba
11
11
 
12
12
  ## Install
13
13
 
14
- Install the Python package from PyPI and install a supported OCR binary separately:
14
+ Python 3.12 through 3.14 is supported. Install the toolkit CLI in an isolated tool
15
+ environment:
15
16
 
16
17
  ```console
18
+ uv tool install open-code-review-toolkit
19
+ ```
20
+
21
+ If `uv` is unavailable, use `pip` only inside an activated virtual environment; do not
22
+ install the toolkit into a global Python environment:
23
+
24
+ ```console
25
+ python -m venv .venv
26
+ . .venv/bin/activate
17
27
  python -m pip install open-code-review-toolkit
28
+ ```
29
+
30
+ The toolkit does not bundle OCR. Download the platform asset for exact
31
+ [Open Code Review 1.11.0](https://github.com/alibaba/open-code-review/releases/tag/v1.11.0),
32
+ verify it before installation against the [compatibility manifest](compatibility/ocr-support.json),
33
+ and place the verified binary on `PATH`. The current Linux amd64 digest is
34
+ `13f68cc2eca1a36d42140e9d37797b68fea5cbbf4b6345ec01ec1b06910fab60`; Darwin arm64 is
35
+ `ac8bf5a0fcd176bb9dcc15b169e90f4b52bf32787adef17a850489dbed97fb78`. The manifest owns
36
+ the other platform hashes.
37
+
38
+ Verify the installation without contacting an LLM provider:
39
+
40
+ ```console
18
41
  ocr --version
19
42
  ocr-ci --help
20
43
  ```
21
44
 
45
+ `ocr --version` must report `open-code-review v1.11.0`; `ocr-ci --help` must exit
46
+ successfully. `ocr-ci preflight` is an operational configuration check, not the installation
47
+ smoke test.
48
+
22
49
  The exact recommended OCR release and its verified asset checksums live in the [versioned compatibility manifest](compatibility/ocr-support.json). CI should pin that release and checksum before execution.
23
50
  The [versioned compatibility policy](docs/compatibility.md) records tested assets and evidence and describes the conservative Dependabot-like qualification workflow for later upstream releases.
24
51
  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`.
25
- The current OCR 1.10.1 integration defaults `OCR_REVIEW_EFFORT` to `medium` for two review rounds. `low` and `high` are explicit one- and three-round alternatives; see the [configuration reference](docs/configuration.md#review-effort) for cost, budget, and precedence boundaries.
52
+ The current OCR 1.11.0 integration defaults `OCR_REVIEW_EFFORT` to `medium` for two review rounds. `low` and `high` are explicit one- and three-round alternatives; see the [configuration reference](docs/configuration.md#review-effort) for cost, budget, and precedence boundaries.
26
53
 
27
54
  Stable distributions are published to [PyPI](https://pypi.org/project/open-code-review-toolkit/) and mirrored as checksum-listed, provenance-attested assets in the corresponding [GitHub Release](https://github.com/xeonvs/open-code-review-toolkit/releases). Development snapshots are published only to TestPyPI.
28
55
 
@@ -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.8.4'
22
- __version_tuple__ = version_tuple = (0, 8, 4)
21
+ __version__ = version = '0.8.6'
22
+ __version_tuple__ = version_tuple = (0, 8, 6)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -650,10 +650,17 @@ SECURITY_SIGNAL_RE = re.compile(
650
650
  r"(?i)\b("
651
651
  r"security|credential|secret|token|password|private[_ -]?token|"
652
652
  r"client[_ -]?secret|api[_ -]?key|authorization|auth|"
653
- r"injection|xss|csrf|ssrf|rce|path traversal|host header|"
653
+ r"xss|csrf|ssrf|rce|path traversal|host header|"
654
654
  r"privilege|permission|access control|vault|leak"
655
655
  r")\b"
656
656
  )
657
+ SECURITY_INJECTION_RE = re.compile(
658
+ r"(?i)(?<!\w)(?:"
659
+ r"(?:os[\s_-]+)?command|shell(?:[\s_-]+command)?|sql|nosql|code|"
660
+ r"(?:server[\s_-]+side[\s_-]+)?template|prompt|ldap|xpath|crlf|"
661
+ r"(?:http[\s_-]+)?header|log|html|script|expression"
662
+ r")[\s_\-\u2010-\u2015]+injection(?!\w)"
663
+ )
657
664
 
658
665
 
659
666
  HIGH_SIGNAL_RE = re.compile(r"(?i)\b(high|critical|blocker|security)\b")
@@ -674,7 +681,8 @@ def comment_signal_text(comment: dict[str, Any]) -> str:
674
681
  def comment_has_security_signal(comment: dict[str, Any]) -> bool:
675
682
  """Return true only when OCR text explicitly carries security wording."""
676
683
 
677
- return bool(SECURITY_SIGNAL_RE.search(comment_signal_text(comment)))
684
+ text = comment_signal_text(comment)
685
+ return bool(SECURITY_SIGNAL_RE.search(text) or SECURITY_INJECTION_RE.search(text))
678
686
 
679
687
 
680
688
  def comment_is_high_signal(comment: dict[str, Any]) -> bool:
@@ -1151,7 +1151,7 @@ def post_llm_provider_failure(
1151
1151
  ),
1152
1152
  ProviderFailureReason.RATE_OR_SPENDING_LIMIT: (
1153
1153
  "The LLM provider rejected the request under a rate or spending limit.",
1154
- "The cause may be ordinary throttling, an account or API-key spending limit, or cost reservation from the requested output cap. Retry later and check account limits. If short probes pass but a full review fails before generation, try an explicit `OCR_LLM_MAX_COMPLETION_TOKENS`, for example `4096`.",
1154
+ "Try lowering `OCR_REVIEW_CONCURRENCY` and/or `OCR_LLM_MAX_COMPLETION_TOKENS`, then start a new merge request pipeline. If it still fails, check the provider's request and account limits.",
1155
1155
  ),
1156
1156
  ProviderFailureReason.OVERLOADED: (
1157
1157
  "The LLM provider reported that it was overloaded.",
@@ -28,7 +28,7 @@ DEFAULT_REQUEST_HEADERS = {
28
28
  "Accept": "application/json",
29
29
  "User-Agent": "open-code-review-ci-preflight/1.0",
30
30
  }
31
- EXPECTED_OCR_VERSION = "1.10.1"
31
+ EXPECTED_OCR_VERSION = "1.11.0"
32
32
 
33
33
 
34
34
  class PreflightError(Exception):
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from collections import Counter
6
6
  from collections.abc import Mapping
7
+ from dataclasses import dataclass
7
8
  from enum import StrEnum
8
9
  from typing import Any
9
10
 
@@ -47,6 +48,55 @@ class ProviderFailureReason(StrEnum):
47
48
  UNKNOWN = "unknown"
48
49
 
49
50
 
51
+ class ProviderFailureDetail(StrEnum):
52
+ """Closed protocol detail safe for one toolkit-authored CI diagnostic."""
53
+
54
+ HTTP_BAD_REQUEST = "http-bad-request"
55
+ HTTP_UNAUTHORIZED = "http-unauthorized"
56
+ HTTP_PAYMENT_REQUIRED = "http-payment-required"
57
+ HTTP_FORBIDDEN = "http-forbidden"
58
+ HTTP_NOT_FOUND = "http-not-found"
59
+ HTTP_REQUEST_TIMEOUT = "http-request-timeout"
60
+ HTTP_CONFLICT = "http-conflict"
61
+ HTTP_CONTENT_TOO_LARGE = "http-content-too-large"
62
+ HTTP_UNPROCESSABLE_CONTENT = "http-unprocessable-content"
63
+ HTTP_RATE_LIMITED = "http-rate-limited"
64
+ HTTP_GATEWAY_TIMEOUT = "http-gateway-timeout"
65
+ HTTP_OVERLOADED = "http-overloaded"
66
+ HTTP_SERVER_ERROR = "http-server-error"
67
+ HTTP_NON_SUCCESS = "http-non-success"
68
+ RESPONSE_DECODE = "response-decode"
69
+ RESPONSE_STATUS = "response-status"
70
+ STREAM = "stream"
71
+ CANCELLED = "cancelled"
72
+ TIMEOUT = "timeout"
73
+ NETWORK = "network"
74
+ UNKNOWN = "unknown"
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class ProviderFailureProjection:
79
+ """Validated provider-neutral failure state retained only for operator output."""
80
+
81
+ reason: ProviderFailureReason
82
+ details: tuple[tuple[ProviderFailureDetail, int], ...]
83
+ status_code: int | None
84
+ failed_requests: int
85
+ cancelled_requests: int
86
+ retried_requests: int
87
+ total_retries: int
88
+ recovered_requests: int
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class _AttemptFailure:
93
+ """Internal terminal-attempt projection after retry-report validation."""
94
+
95
+ reason: ProviderFailureReason
96
+ detail: ProviderFailureDetail
97
+ status_code: int | None
98
+
99
+
50
100
  class RetryReportError(Exception):
51
101
  """The private retry report is malformed or internally inconsistent."""
52
102
 
@@ -79,6 +129,32 @@ def _http_reason(status_code: int) -> ProviderFailureReason:
79
129
  return ProviderFailureReason.REQUEST_REJECTED
80
130
 
81
131
 
132
+ def _http_detail(status_code: int) -> ProviderFailureDetail:
133
+ """Map one bounded HTTP status to provider-neutral protocol detail."""
134
+
135
+ return {
136
+ 400: ProviderFailureDetail.HTTP_BAD_REQUEST,
137
+ 401: ProviderFailureDetail.HTTP_UNAUTHORIZED,
138
+ 402: ProviderFailureDetail.HTTP_PAYMENT_REQUIRED,
139
+ 403: ProviderFailureDetail.HTTP_FORBIDDEN,
140
+ 404: ProviderFailureDetail.HTTP_NOT_FOUND,
141
+ 408: ProviderFailureDetail.HTTP_REQUEST_TIMEOUT,
142
+ 409: ProviderFailureDetail.HTTP_CONFLICT,
143
+ 413: ProviderFailureDetail.HTTP_CONTENT_TOO_LARGE,
144
+ 422: ProviderFailureDetail.HTTP_UNPROCESSABLE_CONTENT,
145
+ 429: ProviderFailureDetail.HTTP_RATE_LIMITED,
146
+ 504: ProviderFailureDetail.HTTP_GATEWAY_TIMEOUT,
147
+ 529: ProviderFailureDetail.HTTP_OVERLOADED,
148
+ }.get(
149
+ status_code,
150
+ (
151
+ ProviderFailureDetail.HTTP_SERVER_ERROR
152
+ if 500 <= status_code <= 599
153
+ else ProviderFailureDetail.HTTP_NON_SUCCESS
154
+ ),
155
+ )
156
+
157
+
82
158
  def _validate_http_class(status_code: int, error_class: str, failure_phase: str) -> None:
83
159
  """Reject attempt fields that contradict OCR's status-derived v1 classifier."""
84
160
 
@@ -97,8 +173,8 @@ def _validate_http_class(status_code: int, error_class: str, failure_phase: str)
97
173
  raise RetryReportError("retry report HTTP status contradicts its error class")
98
174
 
99
175
 
100
- def _attempt_reason(attempt: Mapping[str, Any]) -> ProviderFailureReason | None:
101
- """Validate one attempt and return its closed error reason, if any."""
176
+ def _attempt_failure(attempt: Mapping[str, Any]) -> _AttemptFailure | None:
177
+ """Validate one attempt and return its closed error projection, if any."""
102
178
 
103
179
  outcome = attempt.get("outcome")
104
180
  if outcome not in ATTEMPT_OUTCOMES:
@@ -117,13 +193,18 @@ def _attempt_reason(attempt: Mapping[str, Any]) -> ProviderFailureReason | None:
117
193
 
118
194
  if status_value and not 200 <= status_value <= 299:
119
195
  _validate_http_class(status_value, error_class, failure_phase)
120
- return _http_reason(status_value)
196
+ return _AttemptFailure(_http_reason(status_value), _http_detail(status_value), status_value)
121
197
  if status_value:
122
198
  if failure_phase not in {"response_decode", "response_status", "stream"}:
123
199
  raise RetryReportError("retry report successful HTTP status has invalid failure phase")
124
200
  if error_class not in {"network", "provider", "unknown"}:
125
201
  raise RetryReportError("retry report successful HTTP status has invalid error class")
126
- return ProviderFailureReason.INVALID_RESPONSE
202
+ detail = {
203
+ "response_decode": ProviderFailureDetail.RESPONSE_DECODE,
204
+ "response_status": ProviderFailureDetail.RESPONSE_STATUS,
205
+ "stream": ProviderFailureDetail.STREAM,
206
+ }[failure_phase]
207
+ return _AttemptFailure(ProviderFailureReason.INVALID_RESPONSE, detail, status_value)
127
208
  if failure_phase == "http" or error_class in {
128
209
  "authentication",
129
210
  "overloaded",
@@ -132,18 +213,25 @@ def _attempt_reason(attempt: Mapping[str, Any]) -> ProviderFailureReason | None:
132
213
  }:
133
214
  raise RetryReportError("retry report transport failure has HTTP-only classification")
134
215
  if error_class == "cancelled":
135
- return ProviderFailureReason.CANCELLED
216
+ return _AttemptFailure(
217
+ ProviderFailureReason.CANCELLED, ProviderFailureDetail.CANCELLED, None
218
+ )
136
219
  if error_class == "timeout":
137
- return ProviderFailureReason.TIMEOUT
220
+ return _AttemptFailure(ProviderFailureReason.TIMEOUT, ProviderFailureDetail.TIMEOUT, None)
138
221
  if error_class == "network":
139
222
  if failure_phase in {"response_decode", "response_status", "stream"}:
140
- return ProviderFailureReason.INVALID_RESPONSE
141
- return ProviderFailureReason.NETWORK
142
- return ProviderFailureReason.UNKNOWN
223
+ detail = {
224
+ "response_decode": ProviderFailureDetail.RESPONSE_DECODE,
225
+ "response_status": ProviderFailureDetail.RESPONSE_STATUS,
226
+ "stream": ProviderFailureDetail.STREAM,
227
+ }[failure_phase]
228
+ return _AttemptFailure(ProviderFailureReason.INVALID_RESPONSE, detail, None)
229
+ return _AttemptFailure(ProviderFailureReason.NETWORK, ProviderFailureDetail.NETWORK, None)
230
+ return _AttemptFailure(ProviderFailureReason.UNKNOWN, ProviderFailureDetail.UNKNOWN, None)
143
231
 
144
232
 
145
- def _request_reason(request: Mapping[str, Any]) -> ProviderFailureReason | None:
146
- """Validate one logical request and classify only its terminal failure."""
233
+ def _request_failure(request: Mapping[str, Any]) -> _AttemptFailure | None:
234
+ """Validate one logical request and project only its terminal failure."""
147
235
 
148
236
  outcome = request.get("outcome")
149
237
  if outcome not in REQUEST_OUTCOMES:
@@ -151,7 +239,7 @@ def _request_reason(request: Mapping[str, Any]) -> ProviderFailureReason | None:
151
239
  attempts = request.get("attempts")
152
240
  if not isinstance(attempts, list) or not 0 < len(attempts) <= MAX_ATTEMPTS_PER_REQUEST:
153
241
  raise RetryReportError("retry report request attempts are invalid")
154
- reasons: list[ProviderFailureReason | None] = []
242
+ failures: list[_AttemptFailure | None] = []
155
243
  for index, attempt in enumerate(attempts, start=1):
156
244
  if (
157
245
  not isinstance(attempt, dict)
@@ -159,24 +247,26 @@ def _request_reason(request: Mapping[str, Any]) -> ProviderFailureReason | None:
159
247
  or attempt.get("attempt") != index
160
248
  ):
161
249
  raise RetryReportError("retry report attempt order is invalid")
162
- reasons.append(_attempt_reason(attempt))
250
+ failures.append(_attempt_failure(attempt))
163
251
  if outcome == "succeeded":
164
- if len(attempts) < 2 or any(reason is not None for reason in reasons):
252
+ if len(attempts) < 2 or any(failure is not None for failure in failures):
165
253
  raise RetryReportError("retry report succeeded request is inconsistent")
166
254
  return None
167
255
  if outcome == "recovered":
168
- if reasons[-1] is not None or not any(reason is not None for reason in reasons[:-1]):
256
+ if failures[-1] is not None or not any(failure is not None for failure in failures[:-1]):
169
257
  raise RetryReportError("retry report recovered request is inconsistent")
170
258
  return None
171
259
  if outcome == "cancelled":
172
- return ProviderFailureReason.CANCELLED
173
- if reasons[-1] is None:
260
+ return _AttemptFailure(
261
+ ProviderFailureReason.CANCELLED, ProviderFailureDetail.CANCELLED, None
262
+ )
263
+ if failures[-1] is None:
174
264
  raise RetryReportError("retry report failed request ends in success")
175
- return reasons[-1]
265
+ return failures[-1]
176
266
 
177
267
 
178
- def parse_retry_report_failure(result: object) -> ProviderFailureReason | None:
179
- """Validate retry-report v1 and return one closed terminal failure reason."""
268
+ def parse_retry_report_projection(result: object) -> ProviderFailureProjection | None:
269
+ """Validate retry-report v1 and return one closed numeric failure projection."""
180
270
 
181
271
  if not isinstance(result, dict):
182
272
  raise RetryReportError("OCR result must be an object")
@@ -222,26 +312,82 @@ def parse_retry_report_failure(result: object) -> ProviderFailureReason | None:
222
312
  if total_requests < len(requests):
223
313
  raise RetryReportError("OCR retry report total_requests is inconsistent")
224
314
  for field, expected in expected_counts.items():
225
- if _bounded_count(report.get(field), field, maximum=MAX_RETRY_REQUESTS * 100) != expected:
315
+ maximum = (
316
+ MAX_RETRY_REQUESTS * (MAX_ATTEMPTS_PER_REQUEST - 1)
317
+ if field == "total_retries"
318
+ else MAX_RETRY_REQUESTS
319
+ )
320
+ if _bounded_count(report.get(field), field, maximum=maximum) != expected:
226
321
  raise RetryReportError(f"OCR retry report {field} is inconsistent")
227
322
 
228
- reasons: list[ProviderFailureReason] = []
323
+ failures: list[_AttemptFailure] = []
229
324
  for request in requests:
230
325
  if not isinstance(request, dict):
231
326
  raise RetryReportError("OCR retry report request is invalid")
232
- reason = _request_reason(request)
233
- if reason is not None:
234
- reasons.append(reason)
235
- if not reasons:
327
+ failure = _request_failure(request)
328
+ if failure is not None:
329
+ failures.append(failure)
330
+ if not failures:
236
331
  return None
237
- counts = Counter(reasons)
238
- return next(iter(counts)) if len(counts) == 1 else ProviderFailureReason.MIXED
332
+ reason_counts = Counter(failure.reason for failure in failures)
333
+ reason = next(iter(reason_counts)) if len(reason_counts) == 1 else ProviderFailureReason.MIXED
334
+ detail_counts = Counter(failure.detail for failure in failures)
335
+ statuses = {failure.status_code for failure in failures}
336
+ shared_status = next(iter(statuses)) if len(statuses) == 1 else None
337
+ return ProviderFailureProjection(
338
+ reason=reason,
339
+ details=tuple(sorted(detail_counts.items(), key=lambda item: item[0].value)),
340
+ status_code=shared_status,
341
+ failed_requests=expected_counts["failed_requests"],
342
+ cancelled_requests=expected_counts["cancelled_requests"],
343
+ retried_requests=expected_counts["retried_requests"],
344
+ total_retries=expected_counts["total_retries"],
345
+ recovered_requests=expected_counts["recovered_requests"],
346
+ )
239
347
 
240
348
 
241
- def provider_failure_reason(result: object) -> ProviderFailureReason | None:
242
- """Return a closed reason, degrading malformed private diagnostics to unavailable."""
349
+ def parse_retry_report_failure(result: object) -> ProviderFailureReason | None:
350
+ """Validate retry-report v1 and return one closed terminal failure reason."""
351
+
352
+ projection = parse_retry_report_projection(result)
353
+ return projection.reason if projection is not None else None
354
+
355
+
356
+ def render_provider_diagnostics(projection: ProviderFailureProjection) -> str:
357
+ """Render one bounded deterministic CI line from a validated projection."""
358
+
359
+ fields = [f"summary={projection.reason.value}"]
360
+ if len(projection.details) == 1:
361
+ fields.append(f"detail={projection.details[0][0].value}")
362
+ else:
363
+ details = ",".join(f"{detail.value}:{count}" for detail, count in projection.details)
364
+ fields.append(f"details={details}")
365
+ if projection.status_code is not None:
366
+ fields.append(f"status={projection.status_code}")
367
+ for name in (
368
+ "failed_requests",
369
+ "cancelled_requests",
370
+ "retried_requests",
371
+ "total_retries",
372
+ "recovered_requests",
373
+ ):
374
+ value = getattr(projection, name)
375
+ if value:
376
+ fields.append(f"{name}={value}")
377
+ return "OCR provider diagnostics: " + " ".join(fields)
378
+
379
+
380
+ def provider_failure_projection(result: object) -> ProviderFailureProjection | None:
381
+ """Return closed diagnostics, degrading malformed private data to unavailable."""
243
382
 
244
383
  try:
245
- return parse_retry_report_failure(result)
384
+ return parse_retry_report_projection(result)
246
385
  except RetryReportError:
247
386
  return None
387
+
388
+
389
+ def provider_failure_reason(result: object) -> ProviderFailureReason | None:
390
+ """Return a closed reason, degrading malformed private diagnostics to unavailable."""
391
+
392
+ projection = provider_failure_projection(result)
393
+ return projection.reason if projection is not None else None
@@ -99,7 +99,11 @@ from ocr_toolkit.pre_execution import (
99
99
  PreExecutionStatusError,
100
100
  write_pre_execution_status,
101
101
  )
102
- from ocr_toolkit.provider_failure import ProviderFailureReason, provider_failure_reason
102
+ from ocr_toolkit.provider_failure import (
103
+ ProviderFailureProjection,
104
+ provider_failure_projection,
105
+ render_provider_diagnostics,
106
+ )
103
107
  from ocr_toolkit.providers.gitlab import (
104
108
  GitLabProviderError,
105
109
  acquire_review_snapshot,
@@ -150,6 +154,16 @@ QUARANTINE_COMMENT_FIELDS = frozenset(
150
154
  "suggestion_code",
151
155
  }
152
156
  )
157
+ PROVIDER_PRIVATE_RESULT_KEYS = frozenset(
158
+ {
159
+ "encrypted_content",
160
+ "native_payload",
161
+ "reasoning_content",
162
+ "redacted_thinking",
163
+ "thinking",
164
+ "tool_choice",
165
+ }
166
+ )
153
167
 
154
168
 
155
169
  class ReviewRunnerError(Exception):
@@ -459,8 +473,10 @@ def _dlp_reasons(
459
473
  while stack:
460
474
  nested = stack.pop()
461
475
  if isinstance(nested, dict):
462
- stack.extend(nested.keys())
463
- stack.extend(nested.values())
476
+ for key, value in nested.items():
477
+ if isinstance(key, str) and key in PROVIDER_PRIVATE_RESULT_KEYS:
478
+ reasons["invalid_text"] += 1
479
+ stack.extend((key, value))
464
480
  elif isinstance(nested, list):
465
481
  stack.extend(nested)
466
482
  elif isinstance(nested, str):
@@ -689,6 +705,11 @@ def _sanitize_nonpublication_fields(
689
705
  child_path = (*path, key)
690
706
  if isinstance(source, dict):
691
707
  assert isinstance(key, str)
708
+ if key in PROVIDER_PRIVATE_RESULT_KEYS and not _is_publication_sink_path(
709
+ child_path
710
+ ):
711
+ redacted_fields += 1
712
+ continue
692
713
  if not _is_publication_sink_path(
693
714
  child_path
694
715
  ) and not _is_static_public_tool_key_path(child_path):
@@ -890,7 +911,7 @@ def _publication_projection(
890
911
  sanitized, private_reasons, redacted_fields = _sanitize_nonpublication_fields(
891
912
  payload, budgets=budgets, matcher=matcher
892
913
  )
893
- if not sink_reasons and not private_reasons:
914
+ if not sink_reasons and not private_reasons and redacted_fields == 0:
894
915
  return payload, {"state": "passed"}, False
895
916
 
896
917
  try:
@@ -928,6 +949,8 @@ def _publication_projection(
928
949
  projected["summary"] = {"budget_exceeded": True}
929
950
  else:
930
951
  reasons = sink_reasons + private_reasons
952
+ if not reasons:
953
+ return sanitized, {"state": "passed"}, False
931
954
  return (
932
955
  sanitized,
933
956
  {
@@ -2081,14 +2104,16 @@ def read_stderr_excerpt(stderr_path: Path, max_chars: int = DEFAULT_DIAGNOSTIC_C
2081
2104
  return redact_sensitive(text)[:max_chars]
2082
2105
 
2083
2106
 
2084
- def _closed_provider_failure_reason(result_path: Path) -> ProviderFailureReason | None:
2085
- """Return only a validated retry-report reason from one private result artifact."""
2107
+ def _closed_provider_failure_projection(
2108
+ result_path: Path,
2109
+ ) -> ProviderFailureProjection | None:
2110
+ """Return only validated retry-report diagnostics from one private result."""
2086
2111
 
2087
2112
  try:
2088
2113
  result = load_ocr_result(result_path)
2089
2114
  except (OcrResultMalformed, OcrResultMissing, OcrResultTooLarge):
2090
2115
  return None
2091
- return provider_failure_reason(result)
2116
+ return provider_failure_projection(result)
2092
2117
 
2093
2118
 
2094
2119
  def _resolve_ocr_binary() -> str:
@@ -2155,13 +2180,9 @@ def run_review(
2155
2180
 
2156
2181
  if completed.returncode != 0:
2157
2182
  print(f"Open Code Review exited with code {completed.returncode}.", file=sys.stderr)
2158
- provider_reason = _closed_provider_failure_reason(result_path)
2159
- if provider_reason is not None:
2160
- print(
2161
- f"OCR provider failure classified as {provider_reason.value}; "
2162
- "private diagnostics remain in the owner-only artifacts.",
2163
- file=sys.stderr,
2164
- )
2183
+ provider_projection = _closed_provider_failure_projection(result_path)
2184
+ if provider_projection is not None:
2185
+ print(render_provider_diagnostics(provider_projection), file=sys.stderr)
2165
2186
  else:
2166
2187
  excerpt = read_stderr_excerpt(stderr_path)
2167
2188
  if excerpt: