open-code-review-toolkit 0.8.6__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.6 → open_code_review_toolkit-0.9.0}/PKG-INFO +18 -11
  2. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/README.md +17 -10
  3. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/_version.py +2 -2
  4. open_code_review_toolkit-0.9.0/src/ocr_toolkit/context/ci_outcomes.py +141 -0
  5. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/contracts.py +28 -4
  6. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/policy.py +85 -1
  7. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/store.py +92 -1
  8. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/actions.py +38 -14
  9. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/collect.py +21 -10
  10. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/collectors/projections.py +10 -3
  11. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/mcp.py +367 -9
  12. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/project.py +40 -7
  13. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/store/values.py +4 -5
  14. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/mcp_config.py +24 -7
  15. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/ocr_result.py +3 -1
  16. open_code_review_toolkit-0.9.0/src/ocr_toolkit/posting/approval.py +152 -0
  17. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/formatting.py +34 -17
  18. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/gitlab.py +15 -18
  19. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/workflow.py +41 -37
  20. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/preflight.py +1 -1
  21. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/providers/gitlab.py +27 -4
  22. open_code_review_toolkit-0.9.0/src/ocr_toolkit/providers/gitlab_ci.py +292 -0
  23. open_code_review_toolkit-0.9.0/src/ocr_toolkit/review_identity.py +26 -0
  24. open_code_review_toolkit-0.8.6/src/ocr_toolkit/posting/approval.py → open_code_review_toolkit-0.9.0/src/ocr_toolkit/review_receipt.py +204 -137
  25. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/review_runner.py +424 -41
  26. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/.gitignore +0 -0
  27. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/LICENSE +0 -0
  28. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/pyproject.toml +0 -0
  29. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/__init__.py +0 -0
  30. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/cli.py +0 -0
  31. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/common/__init__.py +0 -0
  32. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/common/filesystem.py +0 -0
  33. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/common/git.py +0 -0
  34. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/common/language.py +0 -0
  35. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/common/markdown.py +0 -0
  36. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/common/redaction.py +0 -0
  37. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/config_writer.py +0 -0
  38. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/configure.py +0 -0
  39. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/__init__.py +0 -0
  40. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/adapters.py +0 -0
  41. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/broker.py +0 -0
  42. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/dlp.py +0 -0
  43. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/mcp.py +0 -0
  44. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/context/recognizers.py +0 -0
  45. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/__init__.py +0 -0
  46. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/__main__.py +0 -0
  47. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/artifacts.py +0 -0
  48. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/categorize.py +0 -0
  49. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/collectors/__init__.py +0 -0
  50. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/collectors/graphs.py +0 -0
  51. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/collectors/orchestration.py +0 -0
  52. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/collectors/registry.py +0 -0
  53. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/collectors/sources.py +0 -0
  54. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/coverage.py +0 -0
  55. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/__init__.py +0 -0
  56. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/ansible/__init__.py +0 -0
  57. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/ansible/requirements.py +0 -0
  58. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/ansible/topology.py +0 -0
  59. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/contracts.py +0 -0
  60. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/go.py +0 -0
  61. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/javascript.py +0 -0
  62. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/php.py +0 -0
  63. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/ecosystems/python.py +0 -0
  64. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/__init__.py +0 -0
  65. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/contracts.py +0 -0
  66. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/detection.py +0 -0
  67. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/providers/__init__.py +0 -0
  68. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/providers/frontend.py +0 -0
  69. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/providers/go.py +0 -0
  70. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/providers/php.py +0 -0
  71. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/providers/python.py +0 -0
  72. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/registry.py +0 -0
  73. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/schema.py +0 -0
  74. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/frameworks/templates.py +0 -0
  75. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/infrastructure.py +0 -0
  76. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/invocation.py +0 -0
  77. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/model.py +0 -0
  78. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/policy/__init__.py +0 -0
  79. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/policy/contracts.py +0 -0
  80. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/policy/decisions.py +0 -0
  81. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/policy/guidance.py +0 -0
  82. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/policy/registry.py +0 -0
  83. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/policy/schema.py +0 -0
  84. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/policy/scopes.py +0 -0
  85. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/repository.py +0 -0
  86. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/review_context.py +0 -0
  87. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/store/__init__.py +0 -0
  88. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/store/atomic.py +0 -0
  89. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/store/contracts.py +0 -0
  90. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/store/core.py +0 -0
  91. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/evidence/store/readback.py +0 -0
  92. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/__init__.py +0 -0
  93. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/__main__.py +0 -0
  94. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/comments.py +0 -0
  95. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/gitlab_approval.py +0 -0
  96. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/markers.py +0 -0
  97. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/payloads.py +0 -0
  98. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/reconciliation.py +0 -0
  99. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/result.py +0 -0
  100. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/settings.py +0 -0
  101. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/snapshot.py +0 -0
  102. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/suggestions.py +0 -0
  103. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/posting/transaction.py +0 -0
  104. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/pre_execution.py +0 -0
  105. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/provider_config.py +0 -0
  106. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/provider_failure.py +0 -0
  107. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/providers/__init__.py +0 -0
  108. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/providers/gitlab_context.py +0 -0
  109. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/providers/gitlab_discussions.py +0 -0
  110. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/providers/gitlab_identity.py +0 -0
  111. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/providers/gitlab_remediation.py +0 -0
  112. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/py.typed +0 -0
  113. {open_code_review_toolkit-0.8.6 → open_code_review_toolkit-0.9.0}/src/ocr_toolkit/result_contract.py +0 -0
  114. {open_code_review_toolkit-0.8.6 → 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.6
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
@@ -227,6 +227,9 @@ Description-Content-Type: text/markdown
227
227
 
228
228
  # Open Code Review Toolkit
229
229
 
230
+ [![Version](https://img.shields.io/pypi/v/open-code-review-toolkit?label=version&color=0A66C2)](https://pypi.org/project/open-code-review-toolkit/)
231
+ [![Python](https://img.shields.io/pypi/pyversions/open-code-review-toolkit?logo=python&logoColor=white&label=python)](https://pypi.org/project/open-code-review-toolkit/)
232
+ [![License](https://img.shields.io/pypi/l/open-code-review-toolkit?color=0A66C2)](https://github.com/xeonvs/open-code-review-toolkit/blob/main/LICENSE)
230
233
  [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13906/badge)](https://www.bestpractices.dev/projects/13906)
231
234
  [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/xeonvs/open-code-review-toolkit/badge)](https://securityscorecards.dev/viewer/?uri=github.com/xeonvs/open-code-review-toolkit)
232
235
  [![CodeQL](https://github.com/xeonvs/open-code-review-toolkit/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/xeonvs/open-code-review-toolkit/actions/workflows/codeql.yml)
@@ -255,11 +258,11 @@ python -m pip install open-code-review-toolkit
255
258
  ```
256
259
 
257
260
  The toolkit does not bundle OCR. Download the platform asset for exact
258
- [Open Code Review 1.11.0](https://github.com/alibaba/open-code-review/releases/tag/v1.11.0),
261
+ [Open Code Review 1.11.3](https://github.com/alibaba/open-code-review/releases/tag/v1.11.3),
259
262
  verify it before installation against the [compatibility manifest](compatibility/ocr-support.json),
260
263
  and place the verified binary on `PATH`. The current Linux amd64 digest is
261
- `13f68cc2eca1a36d42140e9d37797b68fea5cbbf4b6345ec01ec1b06910fab60`; Darwin arm64 is
262
- `ac8bf5a0fcd176bb9dcc15b169e90f4b52bf32787adef17a850489dbed97fb78`. The manifest owns
264
+ `9726204ac81baee153fd65b1ff357c380f73e9d8091c4a73c3c9fb541b5164cb`; Darwin arm64 is
265
+ `515cd92ce300b62dea2bcaf53e910cdf57b681e1d58bbd0a9e8e49617bca52fa`. The manifest owns
263
266
  the other platform hashes.
264
267
 
265
268
  Verify the installation without contacting an LLM provider:
@@ -269,14 +272,14 @@ ocr --version
269
272
  ocr-ci --help
270
273
  ```
271
274
 
272
- `ocr --version` must report `open-code-review v1.11.0`; `ocr-ci --help` must exit
275
+ `ocr --version` must report `open-code-review v1.11.3`; `ocr-ci --help` must exit
273
276
  successfully. `ocr-ci preflight` is an operational configuration check, not the installation
274
277
  smoke test.
275
278
 
276
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.
277
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.
278
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`.
279
- The current OCR 1.11.0 integration defaults `OCR_REVIEW_EFFORT` to `medium` for two review rounds. `low` and `high` are explicit one- and three-round alternatives; see the [configuration reference](docs/configuration.md#review-effort) for cost, budget, and precedence boundaries.
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.
280
283
 
281
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.
282
285
 
@@ -286,16 +289,16 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
286
289
 
287
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.
288
291
 
289
- After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v5'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, 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. 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.
290
293
 
291
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.
292
295
 
293
296
  ## Project architecture
294
297
 
295
- The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed facts, scoped completeness, and base/head changes through the mandatory built-in read-only MCP server. Protected-policy enriched reviews can acquire stable GitLab discussions, verified remediation history, and authorized external issue/document records before OCR. Forge-specific acquisition and posting stay at provider edges; the broker, DLP, store, MCP, receipts, and tests use common contracts so a future GitHub adapter can reuse them without inheriting GitLab API semantics. The same built-in MCP exposes only opaque committed `context_list`/`context_get` handles; it has no provider network or arbitrary identifier path. Direct external MCP remains a separate privileged, comment-only operator boundary.
298
+ The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed facts, literal search, scoped absence checks, and base/head changes through three fixed tools in the mandatory built-in read-only MCP server. Protected-policy enriched reviews can acquire stable GitLab discussions, verified remediation history, protected same-revision CI outcomes, and authorized external issue/document records before OCR. Forge-specific acquisition and posting stay at provider edges; the broker, DLP, store, MCP, receipts, and tests use common contracts so a future GitHub adapter can reuse them without inheriting GitLab API semantics. The same built-in MCP exposes only opaque committed `context_list`/`context_get` handles; it has no provider network or arbitrary identifier path. Direct external MCP remains a separate privileged, comment-only operator boundary.
296
299
 
297
300
  - [Toolkit strategy](docs/engineering/toolkit_strategy.md) - durable product boundaries, architecture, invariants, and non-goals.
298
- - [Bounded review context](docs/review-context.md) - protected policy, adapter protocol, GitLab discussions, opaque handles, DLP, receipt, and cleanup contracts.
301
+ - [Bounded review context](docs/review-context.md) - protected policy, GitLab discussions and same-revision CI outcomes, adapter protocol, opaque handles, DLP, receipt, and cleanup contracts.
299
302
  - [Roadmap](ROADMAP.md) - milestone status, dependencies, outcomes, and completion signals.
300
303
  - [Backlog](docs/codex/TASKS_BACKLOG.md) - inactive implementation-ready work; active execution remains in `PLANS.md`.
301
304
 
@@ -303,8 +306,10 @@ The shipped Repository Evidence Engine reads immutable base/head Git objects, st
303
306
 
304
307
  1. Configure protected/masked `GITLAB_API_TOKEN` and LLM variables in GitLab.
305
308
  2. Pin and checksum the OCR binary.
306
- 3. Install this package.
307
- 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`:
308
313
 
309
314
  ```console
310
315
  ocr-ci preflight
@@ -315,6 +320,8 @@ ocr-ci post --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log
315
320
 
316
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).
317
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
+
318
325
  ## Configuration and safety
319
326
 
320
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.
@@ -1,5 +1,8 @@
1
1
  # Open Code Review Toolkit
2
2
 
3
+ [![Version](https://img.shields.io/pypi/v/open-code-review-toolkit?label=version&color=0A66C2)](https://pypi.org/project/open-code-review-toolkit/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/open-code-review-toolkit?logo=python&logoColor=white&label=python)](https://pypi.org/project/open-code-review-toolkit/)
5
+ [![License](https://img.shields.io/pypi/l/open-code-review-toolkit?color=0A66C2)](https://github.com/xeonvs/open-code-review-toolkit/blob/main/LICENSE)
3
6
  [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13906/badge)](https://www.bestpractices.dev/projects/13906)
4
7
  [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/xeonvs/open-code-review-toolkit/badge)](https://securityscorecards.dev/viewer/?uri=github.com/xeonvs/open-code-review-toolkit)
5
8
  [![CodeQL](https://github.com/xeonvs/open-code-review-toolkit/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/xeonvs/open-code-review-toolkit/actions/workflows/codeql.yml)
@@ -28,11 +31,11 @@ python -m pip install open-code-review-toolkit
28
31
  ```
29
32
 
30
33
  The toolkit does not bundle OCR. Download the platform asset for exact
31
- [Open Code Review 1.11.0](https://github.com/alibaba/open-code-review/releases/tag/v1.11.0),
34
+ [Open Code Review 1.11.3](https://github.com/alibaba/open-code-review/releases/tag/v1.11.3),
32
35
  verify it before installation against the [compatibility manifest](compatibility/ocr-support.json),
33
36
  and place the verified binary on `PATH`. The current Linux amd64 digest is
34
- `13f68cc2eca1a36d42140e9d37797b68fea5cbbf4b6345ec01ec1b06910fab60`; Darwin arm64 is
35
- `ac8bf5a0fcd176bb9dcc15b169e90f4b52bf32787adef17a850489dbed97fb78`. The manifest owns
37
+ `9726204ac81baee153fd65b1ff357c380f73e9d8091c4a73c3c9fb541b5164cb`; Darwin arm64 is
38
+ `515cd92ce300b62dea2bcaf53e910cdf57b681e1d58bbd0a9e8e49617bca52fa`. The manifest owns
36
39
  the other platform hashes.
37
40
 
38
41
  Verify the installation without contacting an LLM provider:
@@ -42,14 +45,14 @@ ocr --version
42
45
  ocr-ci --help
43
46
  ```
44
47
 
45
- `ocr --version` must report `open-code-review v1.11.0`; `ocr-ci --help` must exit
48
+ `ocr --version` must report `open-code-review v1.11.3`; `ocr-ci --help` must exit
46
49
  successfully. `ocr-ci preflight` is an operational configuration check, not the installation
47
50
  smoke test.
48
51
 
49
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.
50
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.
51
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`.
52
- The current OCR 1.11.0 integration defaults `OCR_REVIEW_EFFORT` to `medium` for two review rounds. `low` and `high` are explicit one- and three-round alternatives; see the [configuration reference](docs/configuration.md#review-effort) for cost, budget, and precedence boundaries.
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.
53
56
 
54
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.
55
58
 
@@ -59,16 +62,16 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
59
62
 
60
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.
61
64
 
62
- After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v5'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, 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. 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.
63
66
 
64
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.
65
68
 
66
69
  ## Project architecture
67
70
 
68
- The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed facts, scoped completeness, and base/head changes through the mandatory built-in read-only MCP server. Protected-policy enriched reviews can acquire stable GitLab discussions, verified remediation history, and authorized external issue/document records before OCR. Forge-specific acquisition and posting stay at provider edges; the broker, DLP, store, MCP, receipts, and tests use common contracts so a future GitHub adapter can reuse them without inheriting GitLab API semantics. The same built-in MCP exposes only opaque committed `context_list`/`context_get` handles; it has no provider network or arbitrary identifier path. Direct external MCP remains a separate privileged, comment-only operator boundary.
71
+ The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed facts, literal search, scoped absence checks, and base/head changes through three fixed tools in the mandatory built-in read-only MCP server. Protected-policy enriched reviews can acquire stable GitLab discussions, verified remediation history, protected same-revision CI outcomes, and authorized external issue/document records before OCR. Forge-specific acquisition and posting stay at provider edges; the broker, DLP, store, MCP, receipts, and tests use common contracts so a future GitHub adapter can reuse them without inheriting GitLab API semantics. The same built-in MCP exposes only opaque committed `context_list`/`context_get` handles; it has no provider network or arbitrary identifier path. Direct external MCP remains a separate privileged, comment-only operator boundary.
69
72
 
70
73
  - [Toolkit strategy](docs/engineering/toolkit_strategy.md) - durable product boundaries, architecture, invariants, and non-goals.
71
- - [Bounded review context](docs/review-context.md) - protected policy, adapter protocol, GitLab discussions, opaque handles, DLP, receipt, and cleanup contracts.
74
+ - [Bounded review context](docs/review-context.md) - protected policy, GitLab discussions and same-revision CI outcomes, adapter protocol, opaque handles, DLP, receipt, and cleanup contracts.
72
75
  - [Roadmap](ROADMAP.md) - milestone status, dependencies, outcomes, and completion signals.
73
76
  - [Backlog](docs/codex/TASKS_BACKLOG.md) - inactive implementation-ready work; active execution remains in `PLANS.md`.
74
77
 
@@ -76,8 +79,10 @@ The shipped Repository Evidence Engine reads immutable base/head Git objects, st
76
79
 
77
80
  1. Configure protected/masked `GITLAB_API_TOKEN` and LLM variables in GitLab.
78
81
  2. Pin and checksum the OCR binary.
79
- 3. Install this package.
80
- 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`:
81
86
 
82
87
  ```console
83
88
  ocr-ci preflight
@@ -88,6 +93,8 @@ ocr-ci post --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log
88
93
 
89
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).
90
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
+
91
98
  ## Configuration and safety
92
99
 
93
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.6'
22
- __version_tuple__ = version_tuple = (0, 8, 6)
21
+ __version__ = version = '0.9.0'
22
+ __version_tuple__ = version_tuple = (0, 9, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,141 @@
1
+ """Project provider-neutral same-revision CI outcomes into private context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from dataclasses import dataclass
8
+
9
+ from ocr_toolkit.context.contracts import CIOutcomePolicy, TextBudgets
10
+ from ocr_toolkit.context.dlp import check_text
11
+ from ocr_toolkit.context.store import PendingContextRecord
12
+
13
+ CI_OUTCOME_STATUSES = frozenset({"passed", "failed", "skipped", "canceled", "unknown"})
14
+ CI_OUTCOME_REQUIREMENTS = frozenset({"required", "advisory"})
15
+ CI_OUTCOME_ORIGINS = frozenset({"current_pipeline", "same_revision_pipeline"})
16
+ CI_IDENTITY_BUDGETS = TextBudgets(max_chars=256, max_bytes=1_024, max_lines=1)
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class CIOutcome:
21
+ """Carry one normalized provider outcome without raw forge identities."""
22
+
23
+ check: str
24
+ status: str
25
+ requirement: str
26
+ path_prefixes: tuple[str, ...]
27
+ origin: str
28
+ completed_at: int
29
+ version: str
30
+ digest: str
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class CIOutcomeSnapshot:
35
+ """Carry a stable provider snapshot and its closed completeness state."""
36
+
37
+ state: str
38
+ records: tuple[CIOutcome, ...]
39
+ omitted: int
40
+ invalid: int
41
+
42
+
43
+ def prepare_ci_outcome_records(
44
+ snapshot: CIOutcomeSnapshot,
45
+ *,
46
+ policy: CIOutcomePolicy,
47
+ now: int,
48
+ forbidden: tuple[str, ...] = (),
49
+ ) -> tuple[PendingContextRecord, ...]:
50
+ """DLP-check one closed snapshot and build exact immutable store records."""
51
+
52
+ if (
53
+ snapshot.state not in {"complete", "partial", "mutated"}
54
+ or not isinstance(snapshot.omitted, int)
55
+ or isinstance(snapshot.omitted, bool)
56
+ or snapshot.omitted < 0
57
+ or not isinstance(snapshot.invalid, int)
58
+ or isinstance(snapshot.invalid, bool)
59
+ or snapshot.invalid < 0
60
+ or (snapshot.state == "complete" and (snapshot.omitted or snapshot.invalid))
61
+ or (snapshot.state == "mutated" and snapshot.records)
62
+ ):
63
+ return ()
64
+ if snapshot.state == "mutated":
65
+ return ()
66
+ authorized = {check.name: check.path_prefixes for check in policy.checks}
67
+ pending: list[PendingContextRecord] = []
68
+ for record in snapshot.records:
69
+ normalized_identity = {
70
+ "check": record.check,
71
+ "status": record.status,
72
+ "requirement": record.requirement,
73
+ "path_prefixes": list(record.path_prefixes),
74
+ "origin": record.origin,
75
+ "completed_at": record.completed_at,
76
+ }
77
+ expected_digest = hashlib.sha256(
78
+ json.dumps(normalized_identity, sort_keys=True, separators=(",", ":")).encode()
79
+ ).hexdigest()
80
+ if (
81
+ record.check not in authorized
82
+ or record.path_prefixes != authorized[record.check]
83
+ or record.status not in CI_OUTCOME_STATUSES
84
+ or record.requirement not in CI_OUTCOME_REQUIREMENTS
85
+ or record.origin not in CI_OUTCOME_ORIGINS
86
+ or not isinstance(record.completed_at, int)
87
+ or isinstance(record.completed_at, bool)
88
+ or record.completed_at < 0
89
+ or record.completed_at > now + 300
90
+ or now - record.completed_at > policy.max_age_seconds
91
+ or record.version != str(record.completed_at)
92
+ or len(record.digest) != 64
93
+ or any(character not in "0123456789abcdef" for character in record.digest)
94
+ or record.digest != expected_digest
95
+ ):
96
+ continue
97
+ values = (record.check, *record.path_prefixes)
98
+ checked = [
99
+ check_text(value, budgets=CI_IDENTITY_BUDGETS, forbidden=forbidden) for value in values
100
+ ]
101
+ if any(
102
+ not value.admitted or value.text != original for value, original in zip(checked, values)
103
+ ):
104
+ continue
105
+ model = {
106
+ "check": record.check,
107
+ "revision": "reviewed_head",
108
+ "status": record.status,
109
+ "requirement": record.requirement,
110
+ "scope": {"mode": "declared", "path_prefixes": list(record.path_prefixes)},
111
+ "origin": record.origin,
112
+ "completed_at": record.completed_at,
113
+ }
114
+ canonical = json.dumps(model, sort_keys=True, separators=(",", ":"))
115
+ pending.append(
116
+ PendingContextRecord(
117
+ source="forge:ci_outcomes",
118
+ adapter="gitlab",
119
+ tenant="project",
120
+ canonical_object=hashlib.sha256(
121
+ f"ci-outcome:{record.check}:{record.digest}".encode()
122
+ ).hexdigest(),
123
+ resource_class="ci_outcome",
124
+ descriptor="ci_outcome",
125
+ projections={
126
+ "model": {"descriptor": "ci_outcome", "ci_outcome": model},
127
+ "publish": {"descriptor": "ci_outcome"},
128
+ "retain": {
129
+ "state": record.status,
130
+ "digest": hashlib.sha256(canonical.encode()).hexdigest(),
131
+ "version": record.version,
132
+ "expiry": now + 3_600,
133
+ },
134
+ },
135
+ version=record.version,
136
+ digest=hashlib.sha256(canonical.encode()).hexdigest(),
137
+ mutable=False,
138
+ expiry=now + 3_600,
139
+ )
140
+ )
141
+ return tuple(pending)
@@ -6,14 +6,17 @@ from dataclasses import dataclass
6
6
 
7
7
  POLICY_SCHEMA_V1 = "ocr.review-context-policy/v1"
8
8
  POLICY_SCHEMA_V2 = "ocr.review-context-policy/v2"
9
- POLICY_SCHEMAS = frozenset({POLICY_SCHEMA_V1, POLICY_SCHEMA_V2})
10
- POLICY_SCHEMA = POLICY_SCHEMA_V2
9
+ POLICY_SCHEMA_V3 = "ocr.review-context-policy/v3"
10
+ POLICY_SCHEMAS = frozenset({POLICY_SCHEMA_V1, POLICY_SCHEMA_V2, POLICY_SCHEMA_V3})
11
+ POLICY_SCHEMA = POLICY_SCHEMA_V3
11
12
  STORE_SCHEMA = "ocr.context-store/v2"
12
13
  REQUEST_SCHEMA = "ocr.context-adapter-request/v1"
13
14
  RESPONSE_SCHEMA = "ocr.context-adapter-response/v1"
14
15
  ACCOUNT_CLASSES = frozenset({"user", "automation", "system", "toolkit_bot"})
15
16
  REFERENCE_RESOURCE_CLASSES = frozenset({"issue", "document"})
16
- STORE_RESOURCE_CLASSES = frozenset({*REFERENCE_RESOURCE_CLASSES, "remediation_thread"})
17
+ STORE_RESOURCE_CLASSES = frozenset(
18
+ {*REFERENCE_RESOURCE_CLASSES, "remediation_thread", "ci_outcome"}
19
+ )
17
20
  PROJECTION_FIELDS = frozenset(
18
21
  {
19
22
  "descriptor",
@@ -33,7 +36,10 @@ PROJECTION_FIELDS = frozenset(
33
36
  }
34
37
  )
35
38
  REMEDIATION_MODEL_FIELD = "remediation_thread"
36
- STORE_PROJECTION_FIELDS = frozenset({*PROJECTION_FIELDS, REMEDIATION_MODEL_FIELD})
39
+ CI_OUTCOME_MODEL_FIELD = "ci_outcome"
40
+ STORE_PROJECTION_FIELDS = frozenset(
41
+ {*PROJECTION_FIELDS, REMEDIATION_MODEL_FIELD, CI_OUTCOME_MODEL_FIELD}
42
+ )
37
43
  RETENTION_FIELDS = frozenset({"state", "count", "digest", "version", "expiry"})
38
44
 
39
45
 
@@ -99,6 +105,23 @@ class RemediationThreadPolicy:
99
105
  budgets: TextBudgets
100
106
 
101
107
 
108
+ @dataclass(frozen=True, slots=True)
109
+ class CIOutcomeCheckPolicy:
110
+ """Authorize one exact forge check and its protected path scope."""
111
+
112
+ name: str
113
+ path_prefixes: tuple[str, ...]
114
+
115
+
116
+ @dataclass(frozen=True, slots=True)
117
+ class CIOutcomePolicy:
118
+ """Select bounded same-revision forge outcomes under protected policy."""
119
+
120
+ required: bool
121
+ max_age_seconds: int
122
+ checks: tuple[CIOutcomeCheckPolicy, ...]
123
+
124
+
102
125
  @dataclass(frozen=True, slots=True)
103
126
  class RecognizerPolicy:
104
127
  """Hold one fixed toolkit-authored candidate grammar."""
@@ -132,5 +155,6 @@ class ContextPolicy:
132
155
  budgets: AggregateBudgets
133
156
  forge_discussions: DiscussionPolicy | None
134
157
  remediation_threads: RemediationThreadPolicy | None
158
+ ci_outcomes: CIOutcomePolicy | None
135
159
  references: tuple[ReferencePolicy, ...]
136
160
  digest: str
@@ -5,18 +5,23 @@ from __future__ import annotations
5
5
  import hashlib
6
6
  import json
7
7
  import re
8
+ import unicodedata
8
9
  from collections.abc import Callable, Mapping
10
+ from pathlib import PurePosixPath
9
11
  from typing import Any
10
12
  from urllib.parse import urlsplit
11
13
 
12
14
  from ocr_toolkit.context.contracts import (
13
15
  ACCOUNT_CLASSES,
14
16
  POLICY_SCHEMA_V1,
17
+ POLICY_SCHEMA_V2,
15
18
  POLICY_SCHEMAS,
16
19
  PROJECTION_FIELDS,
17
20
  REFERENCE_RESOURCE_CLASSES,
18
21
  RETENTION_FIELDS,
19
22
  AggregateBudgets,
23
+ CIOutcomeCheckPolicy,
24
+ CIOutcomePolicy,
20
25
  ContextContractError,
21
26
  ContextPolicy,
22
27
  ContextProjections,
@@ -32,6 +37,7 @@ MAX_POLICY_BYTES = 64 * 1024
32
37
  MAX_REFERENCES = 16
33
38
  NAME_RE = re.compile(r"[a-z][a-z0-9_-]{0,63}\Z")
34
39
  ISSUE_PREFIX_RE = re.compile(r"[A-Z][A-Z0-9]{0,15}\Z")
40
+ MAX_CI_CHECKS = 32
35
41
 
36
42
 
37
43
  def _pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
@@ -208,6 +214,79 @@ def _remediation_threads(value: object) -> RemediationThreadPolicy:
208
214
  )
209
215
 
210
216
 
217
+ def normalize_ci_path_prefix(value: object) -> str:
218
+ """Validate one protected repository-relative POSIX path prefix."""
219
+
220
+ if (
221
+ not isinstance(value, str)
222
+ or not 1 <= len(value) <= 256
223
+ or "\\" in value
224
+ or "://" in value
225
+ or any(unicodedata.category(character) in {"Cc", "Cf", "Cs"} for character in value)
226
+ or len(value.encode("utf-8")) > 1_024
227
+ ):
228
+ raise ContextContractError("ci_outcomes path prefix is invalid")
229
+ trailing = value.endswith("/")
230
+ path = PurePosixPath(value)
231
+ parts = value.rstrip("/").split("/")
232
+ if path.is_absolute() or any(part in {"", ".", ".."} for part in parts):
233
+ raise ContextContractError("ci_outcomes path prefix is invalid")
234
+ normalized = path.as_posix()
235
+ return f"{normalized}/" if trailing else normalized
236
+
237
+
238
+ def _ci_outcomes(value: object) -> CIOutcomePolicy:
239
+ """Parse exact check names and path scopes from protected policy v3."""
240
+
241
+ item = _object(
242
+ value,
243
+ keys=frozenset({"required", "max_age_seconds", "checks"}),
244
+ label="ci_outcomes",
245
+ )
246
+ checks = item.get("checks")
247
+ if not isinstance(checks, list) or not checks or len(checks) > MAX_CI_CHECKS:
248
+ raise ContextContractError("ci_outcomes.checks is invalid")
249
+ parsed: list[CIOutcomeCheckPolicy] = []
250
+ for raw in checks:
251
+ check = _object(
252
+ raw,
253
+ keys=frozenset({"name", "path_prefixes"}),
254
+ label="ci_outcomes check",
255
+ )
256
+ name = check.get("name")
257
+ prefixes = check.get("path_prefixes")
258
+ if (
259
+ not isinstance(name, str)
260
+ or not 1 <= len(name) <= 128
261
+ or "://" in name
262
+ or any(unicodedata.category(character) in {"Cc", "Cf", "Cs"} for character in name)
263
+ or len(name.encode("utf-8")) > 512
264
+ or not isinstance(prefixes, list)
265
+ or not prefixes
266
+ or len(prefixes) > 32
267
+ ):
268
+ raise ContextContractError("ci_outcomes check is invalid")
269
+ normalized = tuple(normalize_ci_path_prefix(prefix) for prefix in prefixes)
270
+ if list(normalized) != sorted(set(normalized)):
271
+ raise ContextContractError("ci_outcomes path prefixes must be sorted and unique")
272
+ parsed.append(CIOutcomeCheckPolicy(name=name, path_prefixes=normalized))
273
+ names = [check.name for check in parsed]
274
+ if names != sorted(set(names)):
275
+ raise ContextContractError("ci_outcomes check names must be sorted and unique")
276
+ required = item.get("required", False)
277
+ max_age_seconds = item.get("max_age_seconds", 86_400)
278
+ return CIOutcomePolicy(
279
+ required=_boolean(required, label="ci_outcomes.required"),
280
+ max_age_seconds=_integer(
281
+ max_age_seconds,
282
+ minimum=60,
283
+ maximum=604_800,
284
+ label="ci_outcomes.max_age_seconds",
285
+ ),
286
+ checks=tuple(parsed),
287
+ )
288
+
289
+
211
290
  def _recognizer(value: object, *, resource_class: str) -> RecognizerPolicy:
212
291
  item = _object(
213
292
  value,
@@ -328,6 +407,7 @@ def parse_policy(raw: bytes) -> ContextPolicy:
328
407
  "budgets",
329
408
  "forge_discussions",
330
409
  "remediation_threads",
410
+ "ci_outcomes",
331
411
  "references",
332
412
  }
333
413
  ),
@@ -338,6 +418,8 @@ def parse_policy(raw: bytes) -> ContextPolicy:
338
418
  raise ContextContractError("context policy schema version is unsupported")
339
419
  if schema_version == POLICY_SCHEMA_V1 and "remediation_threads" in root:
340
420
  raise ContextContractError("context policy v1 cannot select remediation threads")
421
+ if schema_version in {POLICY_SCHEMA_V1, POLICY_SCHEMA_V2} and "ci_outcomes" in root:
422
+ raise ContextContractError("context policy v1/v2 cannot select CI outcomes")
341
423
  budgets_value = _object(
342
424
  root.get("budgets"),
343
425
  keys=frozenset({"max_records", "max_chars", "max_bytes", "max_lines", "timeout_ms"}),
@@ -364,6 +446,7 @@ def parse_policy(raw: bytes) -> ContextPolicy:
364
446
  remediation = (
365
447
  _remediation_threads(root["remediation_threads"]) if "remediation_threads" in root else None
366
448
  )
449
+ ci_outcomes = _ci_outcomes(root["ci_outcomes"]) if "ci_outcomes" in root else None
367
450
  references_value = root.get("references", [])
368
451
  if (
369
452
  not isinstance(references_value, list)
@@ -375,7 +458,7 @@ def parse_policy(raw: bytes) -> ContextPolicy:
375
458
  identities = [(item.adapter, item.tenant, item.resource_class) for item in references]
376
459
  if len(identities) != len(set(identities)):
377
460
  raise ContextContractError("context policy references collide")
378
- if discussion is None and remediation is None and not references:
461
+ if discussion is None and remediation is None and ci_outcomes is None and not references:
379
462
  raise ContextContractError("context policy must select at least one source")
380
463
  canonical = json.dumps(
381
464
  payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False
@@ -385,6 +468,7 @@ def parse_policy(raw: bytes) -> ContextPolicy:
385
468
  budgets=budgets,
386
469
  forge_discussions=discussion,
387
470
  remediation_threads=remediation,
471
+ ci_outcomes=ci_outcomes,
388
472
  references=references,
389
473
  digest=hashlib.sha256(canonical).hexdigest(),
390
474
  )