open-code-review-toolkit 0.8.7__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/PKG-INFO +13 -9
  2. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/README.md +12 -8
  3. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/_version.py +2 -2
  4. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/actions.py +37 -13
  5. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/collect.py +12 -8
  6. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/mcp.py +23 -9
  7. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/project.py +16 -0
  8. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/mcp_config.py +17 -2
  9. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/ocr_result.py +1 -1
  10. open_code_review_toolkit-0.9.0/src/ocr_toolkit/posting/approval.py +152 -0
  11. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/formatting.py +30 -15
  12. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/gitlab.py +15 -18
  13. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/workflow.py +41 -44
  14. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/preflight.py +1 -1
  15. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/providers/gitlab.py +27 -4
  16. open_code_review_toolkit-0.9.0/src/ocr_toolkit/review_identity.py +26 -0
  17. open_code_review_toolkit-0.8.7/src/ocr_toolkit/posting/approval.py → open_code_review_toolkit-0.9.0/src/ocr_toolkit/review_receipt.py +197 -135
  18. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/review_runner.py +332 -52
  19. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/.gitignore +0 -0
  20. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/LICENSE +0 -0
  21. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/pyproject.toml +0 -0
  22. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/__init__.py +0 -0
  23. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/cli.py +0 -0
  24. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/common/__init__.py +0 -0
  25. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/common/filesystem.py +0 -0
  26. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/common/git.py +0 -0
  27. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/common/language.py +0 -0
  28. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/common/markdown.py +0 -0
  29. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/common/redaction.py +0 -0
  30. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/config_writer.py +0 -0
  31. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/configure.py +0 -0
  32. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/__init__.py +0 -0
  33. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/adapters.py +0 -0
  34. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/broker.py +0 -0
  35. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/ci_outcomes.py +0 -0
  36. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/contracts.py +0 -0
  37. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/dlp.py +0 -0
  38. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/mcp.py +0 -0
  39. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/policy.py +0 -0
  40. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/recognizers.py +0 -0
  41. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/store.py +0 -0
  42. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/__init__.py +0 -0
  43. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/__main__.py +0 -0
  44. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/artifacts.py +0 -0
  45. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/categorize.py +0 -0
  46. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/collectors/__init__.py +0 -0
  47. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/collectors/graphs.py +0 -0
  48. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/collectors/orchestration.py +0 -0
  49. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/collectors/projections.py +0 -0
  50. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/collectors/registry.py +0 -0
  51. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/collectors/sources.py +0 -0
  52. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/coverage.py +0 -0
  53. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/__init__.py +0 -0
  54. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/ansible/__init__.py +0 -0
  55. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/ansible/requirements.py +0 -0
  56. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/ansible/topology.py +0 -0
  57. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/contracts.py +0 -0
  58. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/go.py +0 -0
  59. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/javascript.py +0 -0
  60. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/php.py +0 -0
  61. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/python.py +0 -0
  62. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/__init__.py +0 -0
  63. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/contracts.py +0 -0
  64. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/detection.py +0 -0
  65. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/providers/__init__.py +0 -0
  66. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/providers/frontend.py +0 -0
  67. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/providers/go.py +0 -0
  68. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/providers/php.py +0 -0
  69. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/providers/python.py +0 -0
  70. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/registry.py +0 -0
  71. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/schema.py +0 -0
  72. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/templates.py +0 -0
  73. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/infrastructure.py +0 -0
  74. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/invocation.py +0 -0
  75. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/model.py +0 -0
  76. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/policy/__init__.py +0 -0
  77. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/policy/contracts.py +0 -0
  78. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/policy/decisions.py +0 -0
  79. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/policy/guidance.py +0 -0
  80. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/policy/registry.py +0 -0
  81. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/policy/schema.py +0 -0
  82. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/policy/scopes.py +0 -0
  83. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/repository.py +0 -0
  84. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/review_context.py +0 -0
  85. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/store/__init__.py +0 -0
  86. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/store/atomic.py +0 -0
  87. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/store/contracts.py +0 -0
  88. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/store/core.py +0 -0
  89. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/store/readback.py +0 -0
  90. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/store/values.py +0 -0
  91. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/__init__.py +0 -0
  92. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/__main__.py +0 -0
  93. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/comments.py +0 -0
  94. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/gitlab_approval.py +0 -0
  95. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/markers.py +0 -0
  96. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/payloads.py +0 -0
  97. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/reconciliation.py +0 -0
  98. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/result.py +0 -0
  99. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/settings.py +0 -0
  100. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/snapshot.py +0 -0
  101. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/suggestions.py +0 -0
  102. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/transaction.py +0 -0
  103. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/pre_execution.py +0 -0
  104. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/provider_config.py +0 -0
  105. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/provider_failure.py +0 -0
  106. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/providers/__init__.py +0 -0
  107. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/providers/gitlab_ci.py +0 -0
  108. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/providers/gitlab_context.py +0 -0
  109. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/providers/gitlab_discussions.py +0 -0
  110. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/providers/gitlab_identity.py +0 -0
  111. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/providers/gitlab_remediation.py +0 -0
  112. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/py.typed +0 -0
  113. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/result_contract.py +0 -0
  114. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.0}/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.7
3
+ Version: 0.9.0
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
@@ -258,11 +258,11 @@ python -m pip install open-code-review-toolkit
258
258
  ```
259
259
 
260
260
  The toolkit does not bundle OCR. Download the platform asset for exact
261
- [Open Code Review 1.11.1](https://github.com/alibaba/open-code-review/releases/tag/v1.11.1),
261
+ [Open Code Review 1.11.3](https://github.com/alibaba/open-code-review/releases/tag/v1.11.3),
262
262
  verify it before installation against the [compatibility manifest](compatibility/ocr-support.json),
263
263
  and place the verified binary on `PATH`. The current Linux amd64 digest is
264
- `1cdc7d1f776f1cdb69850130b930e40f64accc86ecaf09600573b3600456322f`; Darwin arm64 is
265
- `5fdf72e51aae021ac7bf43d7b9dcb160f04880f623c66e8ada5e6ae5a92e172c`. The manifest owns
264
+ `9726204ac81baee153fd65b1ff357c380f73e9d8091c4a73c3c9fb541b5164cb`; Darwin arm64 is
265
+ `515cd92ce300b62dea2bcaf53e910cdf57b681e1d58bbd0a9e8e49617bca52fa`. The manifest owns
266
266
  the other platform hashes.
267
267
 
268
268
  Verify the installation without contacting an LLM provider:
@@ -272,14 +272,14 @@ ocr --version
272
272
  ocr-ci --help
273
273
  ```
274
274
 
275
- `ocr --version` must report `open-code-review v1.11.1`; `ocr-ci --help` must exit
275
+ `ocr --version` must report `open-code-review v1.11.3`; `ocr-ci --help` must exit
276
276
  successfully. `ocr-ci preflight` is an operational configuration check, not the installation
277
277
  smoke test.
278
278
 
279
279
  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.
280
280
  The [versioned compatibility policy](docs/compatibility.md) records tested assets and evidence and describes the conservative Dependabot-like qualification workflow for later upstream releases.
281
281
  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`.
282
- The current OCR 1.11.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.
282
+ The current OCR 1.11.3 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.
283
283
 
284
284
  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.
285
285
 
@@ -289,7 +289,7 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
289
289
 
290
290
  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.
291
291
 
292
- After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v6's exact reviewed source SHA and merge-request author. This write is enabled by default; set `OCR_AUTO_APPROVE=false` when the bot must remain comment-only. DLP-clean metadata, generic discussions, protected same-revision CI outcomes, and adapter records do not independently block approval, while degraded metadata, DLP rejection, required context degradation, admitted remediation history, legacy receipts, publication filtering, any direct external MCP, author movement, or bot self-authorship prevents an approval write. A CI status is review context, never approval authority. GitLab approval rules and protected-branch policy remain authoritative. The toolkit only adds an eligible approval; it never removes an existing approval when a later review is ineligible or disabled.
292
+ After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v8's exact reviewed source SHA, target SHA/protection state, and merge-request author. This write is enabled by default; set `OCR_AUTO_APPROVE=false` when the bot must remain comment-only. DLP-clean metadata, generic discussions, protected same-revision CI outcomes, and adapter records do not independently block approval, while an unprotected target, degraded metadata, DLP rejection, required context degradation, admitted remediation history, legacy receipts, publication filtering, any direct external MCP, author movement, or bot self-authorship prevents an approval write. A CI status is review context, never approval authority. GitLab approval rules, Code Owners, and protected-branch policy remain authoritative merge policy. They are not prerequisites for OCR execution. The toolkit only adds an eligible approval; it never removes an existing approval when a later review is ineligible or disabled.
293
293
 
294
294
  Accepted tradeoffs can be recorded in `.opencodereview/accepted-decisions.md`; the evidence collector supplies only applicable target-ref decisions and never lets a source change self-authorize its review. Root and nested target `AGENTS.md`/`CLAUDE.md` guidance is similarly exposed through the existing evidence MCP with deterministic scope and precedence, while any guidance touched by the merge request is excluded. See [Accepted project decisions](docs/configuration.md#accepted-project-decisions) and [Target project guidance](docs/configuration.md#target-project-guidance) for formats and trust boundaries.
295
295
 
@@ -306,8 +306,10 @@ The shipped Repository Evidence Engine reads immutable base/head Git objects, st
306
306
 
307
307
  1. Configure protected/masked `GITLAB_API_TOKEN` and LLM variables in GitLab.
308
308
  2. Pin and checksum the OCR binary.
309
- 3. Install this package.
310
- 4. Run the four public helper stages around `ocr review`:
309
+ 3. Keep `OCR_GITLAB_TARGET_PROTECTION_MODE=required` unless the project deliberately permits an unprotected target in limited, comment-only mode.
310
+ 4. Merge repository review configuration, including the Rules path, into the target branch.
311
+ 5. Enable the CI review job in a later merge request and install this package.
312
+ 6. Run the four public helper stages around `ocr review`:
311
313
 
312
314
  ```console
313
315
  ocr-ci preflight
@@ -318,6 +320,8 @@ ocr-ci post --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log
318
320
 
319
321
  See the [GitLab mode matrix](examples/gitlab/README.md), the complete [`ocr-review.gitlab-ci.yml`](examples/gitlab/ocr-review.gitlab-ci.yml) pipeline, the [GitLab setup guide](docs/gitlab.md), and [GitLab review operations](docs/operations.md).
320
322
 
323
+ The two-merge-request setup is recommended because repository Rules are loaded only from the captured target commit. On the default protected-target path, a supported one-merge-request alternative may add the Rules and CI job together when that integration merge request does not need review: its first run stops before OCR with a setup-pending note, retrying the same merge request cannot change the captured target Rules, and later merge requests work after it merges. An actually unprotected target with missing target Rules fails closed without promoting the source copy or promising setup-pending status. If the status identity is missing or mismatched, the toolkit likewise uses the generic fail-closed failure note. A green advisory job or pipeline does not prove that OCR or its model executed.
324
+
321
325
  ## Configuration and safety
322
326
 
323
327
  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.
@@ -31,11 +31,11 @@ python -m pip install open-code-review-toolkit
31
31
  ```
32
32
 
33
33
  The toolkit does not bundle OCR. Download the platform asset for exact
34
- [Open Code Review 1.11.1](https://github.com/alibaba/open-code-review/releases/tag/v1.11.1),
34
+ [Open Code Review 1.11.3](https://github.com/alibaba/open-code-review/releases/tag/v1.11.3),
35
35
  verify it before installation against the [compatibility manifest](compatibility/ocr-support.json),
36
36
  and place the verified binary on `PATH`. The current Linux amd64 digest is
37
- `1cdc7d1f776f1cdb69850130b930e40f64accc86ecaf09600573b3600456322f`; Darwin arm64 is
38
- `5fdf72e51aae021ac7bf43d7b9dcb160f04880f623c66e8ada5e6ae5a92e172c`. The manifest owns
37
+ `9726204ac81baee153fd65b1ff357c380f73e9d8091c4a73c3c9fb541b5164cb`; Darwin arm64 is
38
+ `515cd92ce300b62dea2bcaf53e910cdf57b681e1d58bbd0a9e8e49617bca52fa`. The manifest owns
39
39
  the other platform hashes.
40
40
 
41
41
  Verify the installation without contacting an LLM provider:
@@ -45,14 +45,14 @@ ocr --version
45
45
  ocr-ci --help
46
46
  ```
47
47
 
48
- `ocr --version` must report `open-code-review v1.11.1`; `ocr-ci --help` must exit
48
+ `ocr --version` must report `open-code-review v1.11.3`; `ocr-ci --help` must exit
49
49
  successfully. `ocr-ci preflight` is an operational configuration check, not the installation
50
50
  smoke test.
51
51
 
52
52
  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.
53
53
  The [versioned compatibility policy](docs/compatibility.md) records tested assets and evidence and describes the conservative Dependabot-like qualification workflow for later upstream releases.
54
54
  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`.
55
- The current OCR 1.11.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.
55
+ The current OCR 1.11.3 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.
56
56
 
57
57
  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.
58
58
 
@@ -62,7 +62,7 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
62
62
 
63
63
  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.
64
64
 
65
- After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v6's exact reviewed source SHA and merge-request author. This write is enabled by default; set `OCR_AUTO_APPROVE=false` when the bot must remain comment-only. DLP-clean metadata, generic discussions, protected same-revision CI outcomes, and adapter records do not independently block approval, while degraded metadata, DLP rejection, required context degradation, admitted remediation history, legacy receipts, publication filtering, any direct external MCP, author movement, or bot self-authorship prevents an approval write. A CI status is review context, never approval authority. GitLab approval rules and protected-branch policy remain authoritative. The toolkit only adds an eligible approval; it never removes an existing approval when a later review is ineligible or disabled.
65
+ After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v8's exact reviewed source SHA, target SHA/protection state, and merge-request author. This write is enabled by default; set `OCR_AUTO_APPROVE=false` when the bot must remain comment-only. DLP-clean metadata, generic discussions, protected same-revision CI outcomes, and adapter records do not independently block approval, while an unprotected target, degraded metadata, DLP rejection, required context degradation, admitted remediation history, legacy receipts, publication filtering, any direct external MCP, author movement, or bot self-authorship prevents an approval write. A CI status is review context, never approval authority. GitLab approval rules, Code Owners, and protected-branch policy remain authoritative merge policy. They are not prerequisites for OCR execution. The toolkit only adds an eligible approval; it never removes an existing approval when a later review is ineligible or disabled.
66
66
 
67
67
  Accepted tradeoffs can be recorded in `.opencodereview/accepted-decisions.md`; the evidence collector supplies only applicable target-ref decisions and never lets a source change self-authorize its review. Root and nested target `AGENTS.md`/`CLAUDE.md` guidance is similarly exposed through the existing evidence MCP with deterministic scope and precedence, while any guidance touched by the merge request is excluded. See [Accepted project decisions](docs/configuration.md#accepted-project-decisions) and [Target project guidance](docs/configuration.md#target-project-guidance) for formats and trust boundaries.
68
68
 
@@ -79,8 +79,10 @@ The shipped Repository Evidence Engine reads immutable base/head Git objects, st
79
79
 
80
80
  1. Configure protected/masked `GITLAB_API_TOKEN` and LLM variables in GitLab.
81
81
  2. Pin and checksum the OCR binary.
82
- 3. Install this package.
83
- 4. Run the four public helper stages around `ocr review`:
82
+ 3. Keep `OCR_GITLAB_TARGET_PROTECTION_MODE=required` unless the project deliberately permits an unprotected target in limited, comment-only mode.
83
+ 4. Merge repository review configuration, including the Rules path, into the target branch.
84
+ 5. Enable the CI review job in a later merge request and install this package.
85
+ 6. Run the four public helper stages around `ocr review`:
84
86
 
85
87
  ```console
86
88
  ocr-ci preflight
@@ -91,6 +93,8 @@ ocr-ci post --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log
91
93
 
92
94
  See the [GitLab mode matrix](examples/gitlab/README.md), the complete [`ocr-review.gitlab-ci.yml`](examples/gitlab/ocr-review.gitlab-ci.yml) pipeline, the [GitLab setup guide](docs/gitlab.md), and [GitLab review operations](docs/operations.md).
93
95
 
96
+ The two-merge-request setup is recommended because repository Rules are loaded only from the captured target commit. On the default protected-target path, a supported one-merge-request alternative may add the Rules and CI job together when that integration merge request does not need review: its first run stops before OCR with a setup-pending note, retrying the same merge request cannot change the captured target Rules, and later merge requests work after it merges. An actually unprotected target with missing target Rules fails closed without promoting the source copy or promising setup-pending status. If the status identity is missing or mismatched, the toolkit likewise uses the generic fail-closed failure note. A green advisory job or pipeline does not prove that OCR or its model executed.
97
+
94
98
  ## Configuration and safety
95
99
 
96
100
  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.
@@ -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.7'
22
- __version_tuple__ = version_tuple = (0, 8, 7)
21
+ __version__ = version = '0.9.0'
22
+ __version_tuple__ = version_tuple = (0, 9, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -13,8 +13,11 @@ from typing import Any
13
13
 
14
14
  from ocr_toolkit.evidence.store.atomic import atomic_write
15
15
 
16
- ACTION_RECEIPT_SCHEMA = "ocr.evidence-action-receipt/v2"
16
+ ACTION_RECEIPT_SCHEMA = "ocr.evidence-action-receipt/v3"
17
17
  EVIDENCE_ACTIONS = ("summary", "list", "get", "search", "coverage")
18
+ UNATTRIBUTED_ACTION = "unattributed"
19
+ ACTION_RECEIPT_BUCKETS = (*EVIDENCE_ACTIONS, UNATTRIBUTED_ACTION)
20
+ ACTION_RECEIPT_STATES = ("attempted", "completed")
18
21
  MAX_ACTION_CALLS = 1_000_000_000
19
22
  MAX_ACTION_RECEIPT_BYTES = 4_096
20
23
 
@@ -45,23 +48,35 @@ def _serialized_receipt_update(path: Path) -> Iterator[None]:
45
48
  os.close(descriptor)
46
49
 
47
50
 
48
- def _validated_counts(value: Any) -> dict[str, int] | None:
51
+ def _validated_counts(value: Any) -> dict[str, dict[str, int]] | None:
49
52
  if not isinstance(value, dict) or set(value) != {"schema_version", "actions"}:
50
53
  return None
51
54
  actions = value.get("actions")
52
55
  if value.get("schema_version") != ACTION_RECEIPT_SCHEMA or not isinstance(actions, dict):
53
56
  return None
54
- if set(actions) != set(EVIDENCE_ACTIONS):
57
+ if set(actions) != set(ACTION_RECEIPT_STATES):
55
58
  return None
59
+ if any(not isinstance(actions[state], dict) for state in ACTION_RECEIPT_STATES):
60
+ return None
61
+ attempted = actions["attempted"]
62
+ completed = actions["completed"]
63
+ if set(attempted) != set(ACTION_RECEIPT_BUCKETS) or set(completed) != set(EVIDENCE_ACTIONS):
64
+ return None
65
+ counts = (*attempted.values(), *completed.values())
56
66
  if any(
57
67
  not isinstance(count, int) or isinstance(count, bool) or not 0 <= count <= MAX_ACTION_CALLS
58
- for count in actions.values()
68
+ for count in counts
59
69
  ):
60
70
  return None
61
- return {action: actions[action] for action in EVIDENCE_ACTIONS}
71
+ if any(completed[action] > attempted[action] for action in EVIDENCE_ACTIONS):
72
+ return None
73
+ return {
74
+ "attempted": {action: attempted[action] for action in ACTION_RECEIPT_BUCKETS},
75
+ "completed": {action: completed[action] for action in EVIDENCE_ACTIONS},
76
+ }
62
77
 
63
78
 
64
- def read_action_receipt(path: Path) -> dict[str, int] | None:
79
+ def read_action_receipt(path: Path) -> dict[str, dict[str, int]] | None:
65
80
  """Read one bounded receipt or return unavailable for any unsafe shape."""
66
81
 
67
82
  flags = os.O_RDONLY
@@ -94,10 +109,10 @@ def read_action_receipt(path: Path) -> dict[str, int] | None:
94
109
  return _validated_counts(value)
95
110
 
96
111
 
97
- def record_action(path: Path, action: object) -> None:
98
- """Atomically increment one completed action without retaining arguments."""
112
+ def record_action(path: Path, action: object, *, completed: bool = False) -> None:
113
+ """Atomically increment one attempted or completed action without retaining arguments."""
99
114
 
100
- if action not in EVIDENCE_ACTIONS:
115
+ if action not in ACTION_RECEIPT_BUCKETS or (completed and action not in EVIDENCE_ACTIONS):
101
116
  raise ValueError("evidence action is outside the closed receipt enum")
102
117
  with _serialized_receipt_update(path):
103
118
  counts = read_action_receipt(path)
@@ -110,13 +125,22 @@ def record_action(path: Path, action: object) -> None:
110
125
  if exists and counts is None:
111
126
  raise ValueError("evidence action receipt is malformed")
112
127
  if counts is None:
113
- counts = dict.fromkeys(EVIDENCE_ACTIONS, 0)
114
- if counts[action] >= MAX_ACTION_CALLS:
128
+ counts = {
129
+ "attempted": dict.fromkeys(ACTION_RECEIPT_BUCKETS, 0),
130
+ "completed": dict.fromkeys(EVIDENCE_ACTIONS, 0),
131
+ }
132
+ state = "completed" if completed else "attempted"
133
+ if counts[state][action] >= MAX_ACTION_CALLS:
115
134
  raise ValueError("evidence action receipt exceeds the count bound")
116
- counts[action] += 1
135
+ if completed and counts["completed"][action] >= counts["attempted"][action]:
136
+ raise ValueError("completed evidence action has no authenticated attempt")
137
+ counts[state][action] += 1
117
138
  payload = {
118
139
  "schema_version": ACTION_RECEIPT_SCHEMA,
119
- "actions": {name: counts[name] for name in EVIDENCE_ACTIONS},
140
+ "actions": {
141
+ "attempted": {name: counts["attempted"][name] for name in ACTION_RECEIPT_BUCKETS},
142
+ "completed": {name: counts["completed"][name] for name in EVIDENCE_ACTIONS},
143
+ },
120
144
  }
121
145
  atomic_write(
122
146
  path,
@@ -50,8 +50,9 @@ def collect_repository_evidence(
50
50
  base_ref: str | None = None,
51
51
  head_ref: str | None = None,
52
52
  policy_ref: str | None = None,
53
+ include_policy_records: bool = True,
53
54
  ) -> EvidenceStore:
54
- """Build one evidence store from immutable refs and existing bounded collectors."""
55
+ """Build evidence, optionally omitting target-derived policy authority records."""
55
56
 
56
57
  reader = GitRepositoryReader(root or Path.cwd())
57
58
  base_sha, head_sha = _commit_refs(reader, base_ref, head_ref)
@@ -70,13 +71,16 @@ def collect_repository_evidence(
70
71
  changed_paths=changed,
71
72
  coverage_sink=base_coverage,
72
73
  )
73
- policy_facts, policy_diagnostics = collect_ref_facts(
74
- reader,
75
- policy_sha,
76
- RefRole.POLICY,
77
- changed_paths=changed,
78
- )
79
- policy_facts = [record for record in policy_facts if record.kind in POLICY_KINDS]
74
+ if include_policy_records:
75
+ policy_facts, policy_diagnostics = collect_ref_facts(
76
+ reader,
77
+ policy_sha,
78
+ RefRole.POLICY,
79
+ changed_paths=changed,
80
+ )
81
+ policy_facts = [record for record in policy_facts if record.kind in POLICY_KINDS]
82
+ else:
83
+ policy_facts, policy_diagnostics = [], []
80
84
  policy = EvidenceSnapshot(
81
85
  RefRole.POLICY,
82
86
  policy_sha,
@@ -22,7 +22,7 @@ from ocr_toolkit.context.mcp import (
22
22
  tool_definitions,
23
23
  )
24
24
  from ocr_toolkit.context.store import ContextStore
25
- from ocr_toolkit.evidence.actions import record_action
25
+ from ocr_toolkit.evidence.actions import UNATTRIBUTED_ACTION, record_action
26
26
  from ocr_toolkit.evidence.model import CoverageRecord, EvidenceDelta, EvidenceRecord
27
27
  from ocr_toolkit.evidence.policy.schema import is_legacy_policy_value
28
28
  from ocr_toolkit.evidence.store import EvidenceStore, EvidenceStoreError
@@ -785,21 +785,35 @@ def handle_request(
785
785
  return _error(request_id, -32602, "Invalid tool call")
786
786
  name = params.get("name")
787
787
  if isinstance(name, str) and name in {TOOL_NAME, SEARCH_TOOL_NAME, COVERAGE_TOOL_NAME}:
788
+ arguments = params.get("arguments", {})
789
+ if name == TOOL_NAME:
790
+ action = arguments.get("action") if isinstance(arguments, dict) else None
791
+ attempt_action = (
792
+ action
793
+ if isinstance(action, str) and action in {"summary", "list", "get"}
794
+ else UNATTRIBUTED_ACTION
795
+ )
796
+ else:
797
+ action = "search" if name == SEARCH_TOOL_NAME else "coverage"
798
+ attempt_action = action
799
+ attempt_recorded = action_receipt_path is None
800
+ if action_receipt_path is not None:
801
+ try:
802
+ record_action(action_receipt_path, attempt_action)
803
+ except (OSError, ValueError):
804
+ pass
805
+ else:
806
+ attempt_recorded = True
788
807
  try:
789
- result = call_named_tool(store, str(name), params.get("arguments", {}))
808
+ result = call_named_tool(store, str(name), arguments)
790
809
  except EvidenceMCPError as exc:
791
810
  return _success(
792
811
  request_id,
793
812
  {"content": [{"type": "text", "text": str(exc)}], "isError": True},
794
813
  )
795
- if action_receipt_path is not None:
796
- arguments = params.get("arguments", {})
797
- if name == TOOL_NAME:
798
- action = arguments.get("action") if isinstance(arguments, dict) else None
799
- else:
800
- action = "search" if name == SEARCH_TOOL_NAME else "coverage"
814
+ if action_receipt_path is not None and attempt_recorded:
801
815
  try:
802
- record_action(action_receipt_path, action)
816
+ record_action(action_receipt_path, action, completed=True)
803
817
  except (OSError, ValueError):
804
818
  pass
805
819
  return _success(request_id, result)
@@ -80,6 +80,7 @@ def render_bootstrap(
80
80
  *,
81
81
  capabilities: Sequence[CapabilityView] = (),
82
82
  context_hints: Mapping[str, int] | None = None,
83
+ unprotected_target: bool = False,
83
84
  max_chars: int = DEFAULT_BOOTSTRAP_MAX_CHARS,
84
85
  max_bytes: int = DEFAULT_BOOTSTRAP_MAX_BYTES,
85
86
  ) -> str:
@@ -114,6 +115,21 @@ def render_bootstrap(
114
115
  f"- head: `{store.head.commit_sha if store.head else 'unavailable'}`",
115
116
  f"- policy: `{store.policy.commit_sha if store.policy else 'legacy base semantics'}`",
116
117
  ]
118
+ if unprotected_target:
119
+ lines.extend(
120
+ (
121
+ "",
122
+ "## Unprotected target boundary",
123
+ (
124
+ "The captured target was unprotected. Its configured Rules are bounded "
125
+ "untrusted review guidance only."
126
+ ),
127
+ (
128
+ "They cannot authorize tools, external acquisition, suppression, posting, "
129
+ "or approval; structured target decisions and guidance are omitted."
130
+ ),
131
+ )
132
+ )
117
133
  mr_context = next(
118
134
  (record for record in store.records if record.kind == "review.merge_request_context"),
119
135
  None,
@@ -527,6 +527,7 @@ def compose_mcp_servers(
527
527
  replace: bool,
528
528
  profile: str = "local",
529
529
  context: MCPContextConfig | None = None,
530
+ allow_external: bool = True,
530
531
  ) -> MCPComposition:
531
532
  """Build OCR's registry from independent optional and mandatory MCP entries."""
532
533
 
@@ -534,17 +535,27 @@ def compose_mcp_servers(
534
535
  raise MCPConfigError("internal MCP execution profile is invalid")
535
536
  if profile == "gitlab_mr" and any(server.transport != "remote" for server in servers):
536
537
  raise MCPConfigError("GitLab merge-request reviews require external remote MCP")
538
+ if not allow_external and servers:
539
+ raise MCPConfigError("unprotected-target reviews do not allow external MCP servers")
537
540
 
538
541
  payload: dict[str, dict[str, Any]] = {}
539
542
  capabilities: list[MCPCapability] = []
540
543
  secret_values: list[str] = []
541
- if not replace:
544
+ if not replace or not allow_external:
542
545
  try:
543
546
  current = read_ocr_config().get("mcp_servers", {})
544
547
  except OCRConfigError as exc:
545
548
  raise MCPConfigError(str(exc)) from exc
546
549
  if not isinstance(current, dict):
547
550
  raise MCPConfigError("Existing OCR mcp_servers value is not a JSON object")
551
+ if not allow_external and (
552
+ any(not isinstance(name, str) for name in current)
553
+ or any(name != BUILTIN_EVIDENCE_SERVER for name in current)
554
+ ):
555
+ raise MCPConfigError(
556
+ "unprotected-target reviews do not allow inherited external MCP servers"
557
+ )
558
+ if not replace:
548
559
  declared_names = {server.name for server in servers}
549
560
  for name, value in current.items():
550
561
  # Explicit operator input replaces the same named inherited entry;
@@ -632,7 +643,10 @@ def compose_mcp_servers(
632
643
 
633
644
 
634
645
  def build_mcp_composition(
635
- *, profile: str = "local", context: MCPContextConfig | None = None
646
+ *,
647
+ profile: str = "local",
648
+ context: MCPContextConfig | None = None,
649
+ allow_external: bool = True,
636
650
  ) -> MCPComposition:
637
651
  """Parse environment settings into the complete profiled MCP composition."""
638
652
 
@@ -641,6 +655,7 @@ def build_mcp_composition(
641
655
  replace=_replace_configured_servers(),
642
656
  profile=profile,
643
657
  context=context,
658
+ allow_external=allow_external,
644
659
  )
645
660
 
646
661
 
@@ -19,7 +19,7 @@ from ocr_toolkit.common.redaction import sanitize_ocr_value
19
19
  DEFAULT_MAX_RESULT_BYTES = 2_000_000
20
20
  MAX_RESULT_BYTES_HARD_LIMIT = 20_000_000
21
21
  TOOLKIT_RESULT_KEY = "_ocr_toolkit"
22
- TOOLKIT_RESULT_SCHEMA_VERSION = 6
22
+ TOOLKIT_RESULT_SCHEMA_VERSION = 8
23
23
  SUPPORTED_TOOLKIT_RESULT_SCHEMA_VERSIONS = frozenset({TOOLKIT_RESULT_SCHEMA_VERSION})
24
24
  TOOLKIT_ADVISORY_KEY = "_ocr_toolkit_advisory"
25
25
  TOOLKIT_ADVISORY_SCHEMA_VERSION = "ocr.toolkit-advisory/v1"
@@ -0,0 +1,152 @@
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
+ from ocr_toolkit.review_receipt import (
12
+ INVALID_APPROVAL_RECEIPT_REASON,
13
+ automatic_approval_metadata_reason,
14
+ publication_dlp_state,
15
+ publication_outcome_for_summary,
16
+ toolkit_receipt_is_valid,
17
+ )
18
+
19
+ __all__ = [
20
+ "INVALID_APPROVAL_RECEIPT_REASON",
21
+ "ApprovalEligibility",
22
+ "ApprovalResult",
23
+ "ApprovalStatus",
24
+ "approval_summary_line",
25
+ "automatic_approval_metadata_reason",
26
+ "evaluate_approval_policy",
27
+ "provisional_approval_result",
28
+ "publication_dlp_state",
29
+ "publication_outcome_for_summary",
30
+ "toolkit_receipt_is_valid",
31
+ ]
32
+
33
+ ALLOWED_CATEGORIES = frozenset({"style", "documentation", "maintainability"})
34
+ MAX_APPROVABLE_FINDINGS = 3
35
+
36
+
37
+ class ApprovalStatus(str, Enum):
38
+ """Closed public states for one automatic-approval transaction."""
39
+
40
+ APPROVED = "approved"
41
+ NOT_ELIGIBLE = "not eligible"
42
+ DISABLED = "disabled"
43
+ SKIPPED = "skipped"
44
+ FAILED = "failed"
45
+
46
+
47
+ @dataclass(frozen=True, slots=True)
48
+ class ApprovalResult:
49
+ """Bounded status rendered in the review summary and runner log."""
50
+
51
+ status: ApprovalStatus
52
+ reason: str
53
+
54
+
55
+ @dataclass(frozen=True, slots=True)
56
+ class ApprovalEligibility:
57
+ """Policy conclusion before provider state is consulted."""
58
+
59
+ eligible: bool
60
+ result: ApprovalResult
61
+
62
+
63
+ def evaluate_approval_policy(
64
+ setting: BooleanSetting,
65
+ outcome: ReviewOutcome,
66
+ comments: list[dict[str, Any]],
67
+ warnings: list[Any],
68
+ omitted_count: int,
69
+ toolkit_metadata: Any = None,
70
+ ) -> ApprovalEligibility:
71
+ """Evaluate the fixed automatic-approval policy from authoritative OCR data."""
72
+
73
+ if not setting.enabled:
74
+ reason = (
75
+ "configuration was invalid and failed closed"
76
+ if not setting.valid
77
+ else "disabled by OCR_AUTO_APPROVE"
78
+ )
79
+ return ApprovalEligibility(
80
+ False,
81
+ ApprovalResult(ApprovalStatus.DISABLED, reason),
82
+ )
83
+ metadata_reason = automatic_approval_metadata_reason(toolkit_metadata)
84
+ if metadata_reason:
85
+ reason = metadata_reason
86
+ elif toolkit_metadata["evidence"]["mandatory"] is not outcome.requires_evidence_mcp:
87
+ reason = "the review-time approval receipt is missing or invalid"
88
+ elif not outcome.manifest_present:
89
+ reason = "the OCR result has no authoritative coverage manifest"
90
+ elif outcome.kind != "clean" or outcome.budget_exceeded:
91
+ reason = "the OCR review did not complete cleanly"
92
+ elif outcome.failed_count or outcome.waived_count:
93
+ reason = "coverage contained failed or waived items"
94
+ elif warnings:
95
+ reason = "the OCR review reported warnings"
96
+ elif omitted_count:
97
+ reason = "one or more findings were omitted from publication"
98
+ elif len(comments) > MAX_APPROVABLE_FINDINGS:
99
+ reason = f"the review reported more than {MAX_APPROVABLE_FINDINGS} findings"
100
+ else:
101
+ reason = ""
102
+
103
+ if reason:
104
+ return ApprovalEligibility(
105
+ False,
106
+ ApprovalResult(ApprovalStatus.NOT_ELIGIBLE, reason),
107
+ )
108
+
109
+ for comment in comments:
110
+ severity = comment.get("severity")
111
+ category = comment.get("category")
112
+ if severity != "low":
113
+ return ApprovalEligibility(
114
+ False,
115
+ ApprovalResult(
116
+ ApprovalStatus.NOT_ELIGIBLE,
117
+ "a finding had a blocking or malformed severity",
118
+ ),
119
+ )
120
+ if not isinstance(category, str) or category not in ALLOWED_CATEGORIES:
121
+ return ApprovalEligibility(
122
+ False,
123
+ ApprovalResult(
124
+ ApprovalStatus.NOT_ELIGIBLE,
125
+ "a finding had a blocking or malformed category",
126
+ ),
127
+ )
128
+
129
+ return ApprovalEligibility(
130
+ True,
131
+ ApprovalResult(
132
+ ApprovalStatus.SKIPPED,
133
+ "awaiting post-publication SHA verification",
134
+ ),
135
+ )
136
+
137
+
138
+ def approval_summary_line(result: ApprovalResult) -> str:
139
+ """Render exactly one bounded automatic-approval state line."""
140
+
141
+ return f"- Automatic approval: `{result.status.value}` — {result.reason}."
142
+
143
+
144
+ def provisional_approval_result(eligibility: ApprovalEligibility) -> ApprovalResult:
145
+ """Return a fail-closed state safe to publish before provider readback."""
146
+
147
+ if not eligibility.eligible:
148
+ return eligibility.result
149
+ return ApprovalResult(
150
+ ApprovalStatus.FAILED,
151
+ "automatic approval has not yet been confirmed",
152
+ )