open-code-review-toolkit 0.8.7__tar.gz → 0.9.1__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.1}/PKG-INFO +16 -13
  2. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/README.md +15 -12
  3. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/_version.py +2 -2
  4. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/actions.py +37 -13
  5. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/collect.py +12 -8
  6. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/mcp.py +23 -9
  7. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/project.py +16 -0
  8. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/mcp_config.py +17 -2
  9. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/ocr_result.py +1 -1
  10. open_code_review_toolkit-0.9.1/src/ocr_toolkit/posting/approval.py +152 -0
  11. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/posting/formatting.py +30 -15
  12. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/posting/gitlab.py +15 -18
  13. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/posting/workflow.py +41 -44
  14. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/preflight.py +1 -1
  15. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/providers/gitlab.py +27 -4
  16. open_code_review_toolkit-0.9.1/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.1/src/ocr_toolkit/review_receipt.py +197 -135
  18. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/review_runner.py +349 -52
  19. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/.gitignore +0 -0
  20. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/LICENSE +0 -0
  21. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/pyproject.toml +0 -0
  22. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/__init__.py +0 -0
  23. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/cli.py +0 -0
  24. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/common/__init__.py +0 -0
  25. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/common/filesystem.py +0 -0
  26. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/common/git.py +0 -0
  27. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/common/language.py +0 -0
  28. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/common/markdown.py +0 -0
  29. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/common/redaction.py +0 -0
  30. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/config_writer.py +0 -0
  31. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/configure.py +0 -0
  32. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/context/__init__.py +0 -0
  33. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/context/adapters.py +0 -0
  34. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/context/broker.py +0 -0
  35. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/context/ci_outcomes.py +0 -0
  36. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/context/contracts.py +0 -0
  37. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/context/dlp.py +0 -0
  38. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/context/mcp.py +0 -0
  39. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/context/policy.py +0 -0
  40. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/context/recognizers.py +0 -0
  41. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/context/store.py +0 -0
  42. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/__init__.py +0 -0
  43. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/__main__.py +0 -0
  44. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/artifacts.py +0 -0
  45. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/categorize.py +0 -0
  46. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/collectors/__init__.py +0 -0
  47. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/collectors/graphs.py +0 -0
  48. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/collectors/orchestration.py +0 -0
  49. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/collectors/projections.py +0 -0
  50. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/collectors/registry.py +0 -0
  51. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/collectors/sources.py +0 -0
  52. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/coverage.py +0 -0
  53. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/ecosystems/__init__.py +0 -0
  54. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/ecosystems/ansible/__init__.py +0 -0
  55. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/ecosystems/ansible/requirements.py +0 -0
  56. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/ecosystems/ansible/topology.py +0 -0
  57. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/ecosystems/contracts.py +0 -0
  58. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/ecosystems/go.py +0 -0
  59. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/ecosystems/javascript.py +0 -0
  60. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/ecosystems/php.py +0 -0
  61. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/ecosystems/python.py +0 -0
  62. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/frameworks/__init__.py +0 -0
  63. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/frameworks/contracts.py +0 -0
  64. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/frameworks/detection.py +0 -0
  65. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/frameworks/providers/__init__.py +0 -0
  66. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/frameworks/providers/frontend.py +0 -0
  67. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/frameworks/providers/go.py +0 -0
  68. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/frameworks/providers/php.py +0 -0
  69. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/frameworks/providers/python.py +0 -0
  70. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/frameworks/registry.py +0 -0
  71. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/frameworks/schema.py +0 -0
  72. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/frameworks/templates.py +0 -0
  73. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/infrastructure.py +0 -0
  74. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/invocation.py +0 -0
  75. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/model.py +0 -0
  76. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/policy/__init__.py +0 -0
  77. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/policy/contracts.py +0 -0
  78. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/policy/decisions.py +0 -0
  79. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/policy/guidance.py +0 -0
  80. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/policy/registry.py +0 -0
  81. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/policy/schema.py +0 -0
  82. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/policy/scopes.py +0 -0
  83. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/repository.py +0 -0
  84. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/review_context.py +0 -0
  85. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/store/__init__.py +0 -0
  86. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/store/atomic.py +0 -0
  87. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/store/contracts.py +0 -0
  88. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/store/core.py +0 -0
  89. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/store/readback.py +0 -0
  90. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/evidence/store/values.py +0 -0
  91. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/posting/__init__.py +0 -0
  92. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/posting/__main__.py +0 -0
  93. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/posting/comments.py +0 -0
  94. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/posting/gitlab_approval.py +0 -0
  95. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/posting/markers.py +0 -0
  96. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/posting/payloads.py +0 -0
  97. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/posting/reconciliation.py +0 -0
  98. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/posting/result.py +0 -0
  99. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/posting/settings.py +0 -0
  100. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/posting/snapshot.py +0 -0
  101. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/posting/suggestions.py +0 -0
  102. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/posting/transaction.py +0 -0
  103. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/pre_execution.py +0 -0
  104. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/provider_config.py +0 -0
  105. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/provider_failure.py +0 -0
  106. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/providers/__init__.py +0 -0
  107. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/providers/gitlab_ci.py +0 -0
  108. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/providers/gitlab_context.py +0 -0
  109. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/providers/gitlab_discussions.py +0 -0
  110. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/providers/gitlab_identity.py +0 -0
  111. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/providers/gitlab_remediation.py +0 -0
  112. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/py.typed +0 -0
  113. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/src/ocr_toolkit/result_contract.py +0 -0
  114. {open_code_review_toolkit-0.8.7 → open_code_review_toolkit-0.9.1}/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.1
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
@@ -257,13 +257,12 @@ python -m venv .venv
257
257
  python -m pip install open-code-review-toolkit
258
258
  ```
259
259
 
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),
262
- verify it before installation against the [compatibility manifest](compatibility/ocr-support.json),
263
- and place the verified binary on `PATH`. The current Linux amd64 digest is
264
- `1cdc7d1f776f1cdb69850130b930e40f64accc86ecaf09600573b3600456322f`; Darwin arm64 is
265
- `5fdf72e51aae021ac7bf43d7b9dcb160f04880f623c66e8ada5e6ae5a92e172c`. The manifest owns
266
- the other platform hashes.
260
+ The toolkit does not bundle OCR. Use `recommended_version` in the
261
+ [compatibility manifest](compatibility/ocr-support.json) for this toolkit revision
262
+ to select the exact [upstream release](https://github.com/alibaba/open-code-review/releases).
263
+ Download its platform asset, verify it against that release entry's asset SHA-256,
264
+ and place the verified binary on `PATH`. Do not substitute the latest upstream
265
+ release or use another toolkit revision's manifest.
267
266
 
268
267
  Verify the installation without contacting an LLM provider:
269
268
 
@@ -272,14 +271,14 @@ ocr --version
272
271
  ocr-ci --help
273
272
  ```
274
273
 
275
- `ocr --version` must report `open-code-review v1.11.1`; `ocr-ci --help` must exit
274
+ `ocr --version` must report the manifest's exact `recommended_version`; `ocr-ci --help` must exit
276
275
  successfully. `ocr-ci preflight` is an operational configuration check, not the installation
277
276
  smoke test.
278
277
 
279
278
  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
279
  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
280
  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.
281
+ The toolkit 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
282
 
284
283
  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
284
 
@@ -289,7 +288,7 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
289
288
 
290
289
  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
290
 
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.
291
+ 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
292
 
294
293
  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
294
 
@@ -306,8 +305,10 @@ The shipped Repository Evidence Engine reads immutable base/head Git objects, st
306
305
 
307
306
  1. Configure protected/masked `GITLAB_API_TOKEN` and LLM variables in GitLab.
308
307
  2. Pin and checksum the OCR binary.
309
- 3. Install this package.
310
- 4. Run the four public helper stages around `ocr review`:
308
+ 3. Keep `OCR_GITLAB_TARGET_PROTECTION_MODE=required` unless the project deliberately permits an unprotected target in limited, comment-only mode.
309
+ 4. Merge repository review configuration, including the Rules path, into the target branch.
310
+ 5. Enable the CI review job in a later merge request and install this package.
311
+ 6. Run the four public helper stages around `ocr review`:
311
312
 
312
313
  ```console
313
314
  ocr-ci preflight
@@ -318,6 +319,8 @@ ocr-ci post --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log
318
319
 
319
320
  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
321
 
322
+ 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.
323
+
321
324
  ## Configuration and safety
322
325
 
323
326
  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.
@@ -30,13 +30,12 @@ python -m venv .venv
30
30
  python -m pip install open-code-review-toolkit
31
31
  ```
32
32
 
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),
35
- verify it before installation against the [compatibility manifest](compatibility/ocr-support.json),
36
- and place the verified binary on `PATH`. The current Linux amd64 digest is
37
- `1cdc7d1f776f1cdb69850130b930e40f64accc86ecaf09600573b3600456322f`; Darwin arm64 is
38
- `5fdf72e51aae021ac7bf43d7b9dcb160f04880f623c66e8ada5e6ae5a92e172c`. The manifest owns
39
- the other platform hashes.
33
+ The toolkit does not bundle OCR. Use `recommended_version` in the
34
+ [compatibility manifest](compatibility/ocr-support.json) for this toolkit revision
35
+ to select the exact [upstream release](https://github.com/alibaba/open-code-review/releases).
36
+ Download its platform asset, verify it against that release entry's asset SHA-256,
37
+ and place the verified binary on `PATH`. Do not substitute the latest upstream
38
+ release or use another toolkit revision's manifest.
40
39
 
41
40
  Verify the installation without contacting an LLM provider:
42
41
 
@@ -45,14 +44,14 @@ ocr --version
45
44
  ocr-ci --help
46
45
  ```
47
46
 
48
- `ocr --version` must report `open-code-review v1.11.1`; `ocr-ci --help` must exit
47
+ `ocr --version` must report the manifest's exact `recommended_version`; `ocr-ci --help` must exit
49
48
  successfully. `ocr-ci preflight` is an operational configuration check, not the installation
50
49
  smoke test.
51
50
 
52
51
  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
52
  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
53
  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.
54
+ The toolkit 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
55
 
57
56
  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
57
 
@@ -62,7 +61,7 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
62
61
 
63
62
  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
63
 
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.
64
+ 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
65
 
67
66
  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
67
 
@@ -79,8 +78,10 @@ The shipped Repository Evidence Engine reads immutable base/head Git objects, st
79
78
 
80
79
  1. Configure protected/masked `GITLAB_API_TOKEN` and LLM variables in GitLab.
81
80
  2. Pin and checksum the OCR binary.
82
- 3. Install this package.
83
- 4. Run the four public helper stages around `ocr review`:
81
+ 3. Keep `OCR_GITLAB_TARGET_PROTECTION_MODE=required` unless the project deliberately permits an unprotected target in limited, comment-only mode.
82
+ 4. Merge repository review configuration, including the Rules path, into the target branch.
83
+ 5. Enable the CI review job in a later merge request and install this package.
84
+ 6. Run the four public helper stages around `ocr review`:
84
85
 
85
86
  ```console
86
87
  ocr-ci preflight
@@ -91,6 +92,8 @@ ocr-ci post --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log
91
92
 
92
93
  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
94
 
95
+ 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.
96
+
94
97
  ## Configuration and safety
95
98
 
96
99
  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.1'
22
+ __version_tuple__ = version_tuple = (0, 9, 1)
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
+ )