open-code-review-toolkit 0.12.1__tar.gz → 0.13.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 (140) hide show
  1. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/PKG-INFO +19 -10
  2. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/README.md +17 -9
  3. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/pyproject.toml +2 -0
  4. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/_version.py +2 -2
  5. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/cli.py +4 -0
  6. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/common/redaction.py +3 -3
  7. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/artifacts.py +91 -4
  8. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/collect.py +131 -3
  9. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/collectors/orchestration.py +1 -1
  10. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/mcp.py +17 -8
  11. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/model.py +4 -3
  12. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/project.py +24 -1
  13. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/repository.py +65 -9
  14. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/store/contracts.py +3 -2
  15. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/store/core.py +81 -0
  16. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/store/readback.py +11 -1
  17. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/ocr_result.py +5 -2
  18. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/formatting.py +12 -1
  19. open_code_review_toolkit-0.13.0/src/ocr_toolkit/posting/freshness.py +166 -0
  20. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/gitlab.py +1 -0
  21. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/gitlab_approval.py +69 -2
  22. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/snapshot.py +3 -6
  23. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/workflow.py +673 -238
  24. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/pre_execution.py +60 -3
  25. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/preflight.py +19 -0
  26. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/gitlab.py +172 -28
  27. open_code_review_toolkit-0.13.0/src/ocr_toolkit/review_identity.py +58 -0
  28. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/review_receipt.py +215 -57
  29. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/review_runner.py +312 -110
  30. open_code_review_toolkit-0.12.1/src/ocr_toolkit/review_identity.py +0 -26
  31. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/.gitignore +0 -0
  32. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/LICENSE +0 -0
  33. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/__init__.py +0 -0
  34. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/common/__init__.py +0 -0
  35. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/common/filesystem.py +0 -0
  36. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/common/git.py +0 -0
  37. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/common/language.py +0 -0
  38. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/common/markdown.py +0 -0
  39. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/config_writer.py +0 -0
  40. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/configure.py +0 -0
  41. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/__init__.py +0 -0
  42. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/broker.py +0 -0
  43. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/ci_outcomes.py +0 -0
  44. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/contracts.py +0 -0
  45. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/dlp.py +0 -0
  46. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/mcp.py +0 -0
  47. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/policy.py +0 -0
  48. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/recognizers.py +0 -0
  49. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/store.py +0 -0
  50. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/__init__.py +0 -0
  51. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/__main__.py +0 -0
  52. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/actions.py +0 -0
  53. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/categorize.py +0 -0
  54. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/collectors/__init__.py +0 -0
  55. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/collectors/graphs.py +0 -0
  56. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/collectors/projections.py +0 -0
  57. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/collectors/registry.py +0 -0
  58. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/collectors/sources.py +0 -0
  59. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/coverage.py +0 -0
  60. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/__init__.py +0 -0
  61. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/ansible/__init__.py +0 -0
  62. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/ansible/requirements.py +0 -0
  63. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/ansible/topology.py +0 -0
  64. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/contracts.py +0 -0
  65. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/go.py +0 -0
  66. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/javascript.py +0 -0
  67. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/php.py +0 -0
  68. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/python.py +0 -0
  69. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/__init__.py +0 -0
  70. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/contracts.py +0 -0
  71. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/detection.py +0 -0
  72. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/providers/__init__.py +0 -0
  73. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/providers/frontend.py +0 -0
  74. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/providers/go.py +0 -0
  75. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/providers/php.py +0 -0
  76. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/providers/python.py +0 -0
  77. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/registry.py +0 -0
  78. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/schema.py +0 -0
  79. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/templates.py +0 -0
  80. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/gitlinks.py +0 -0
  81. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/infrastructure.py +0 -0
  82. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/invocation.py +0 -0
  83. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/opaque_code.py +0 -0
  84. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/policy/__init__.py +0 -0
  85. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/policy/contracts.py +0 -0
  86. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/policy/decisions.py +0 -0
  87. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/policy/guidance.py +0 -0
  88. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/policy/registry.py +0 -0
  89. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/policy/schema.py +0 -0
  90. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/policy/scopes.py +0 -0
  91. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/review_context.py +0 -0
  92. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/store/__init__.py +0 -0
  93. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/store/atomic.py +0 -0
  94. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/store/values.py +0 -0
  95. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/federation/__init__.py +0 -0
  96. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/federation/admission.py +0 -0
  97. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/federation/contracts.py +0 -0
  98. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/federation/gateway.py +0 -0
  99. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/federation/registry.py +0 -0
  100. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/federation/relay.py +0 -0
  101. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/federation/runtime.py +0 -0
  102. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/federation/transport.py +0 -0
  103. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/local_chatgpt.py +0 -0
  104. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/mcp_config.py +0 -0
  105. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/__init__.py +0 -0
  106. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/__main__.py +0 -0
  107. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/approval.py +0 -0
  108. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/comments.py +0 -0
  109. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/markers.py +0 -0
  110. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/payloads.py +0 -0
  111. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/reconciliation.py +0 -0
  112. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/result.py +0 -0
  113. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/settings.py +0 -0
  114. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/suggestions.py +0 -0
  115. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/transaction.py +0 -0
  116. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/provider_config.py +0 -0
  117. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/provider_failure.py +0 -0
  118. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/__init__.py +0 -0
  119. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/gitlab_ci.py +0 -0
  120. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/gitlab_context.py +0 -0
  121. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/gitlab_discussions.py +0 -0
  122. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/gitlab_identity.py +0 -0
  123. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/gitlab_remediation.py +0 -0
  124. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/local.py +0 -0
  125. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/local_config.py +0 -0
  126. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/py.typed +0 -0
  127. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/__init__.py +0 -0
  128. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/dlp.py +0 -0
  129. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/federation.py +0 -0
  130. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/metadata.py +0 -0
  131. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/model.py +0 -0
  132. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/outcome.py +0 -0
  133. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/result.py +0 -0
  134. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/sections.py +0 -0
  135. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/text.py +0 -0
  136. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/usage.py +0 -0
  137. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/result_contract.py +0 -0
  138. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/result_usage.py +0 -0
  139. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/review_debug.py +0 -0
  140. {open_code_review_toolkit-0.12.1 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/review_progress.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: open-code-review-toolkit
3
- Version: 0.12.1
3
+ Version: 0.13.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
@@ -228,6 +228,7 @@ Requires-Dist: certifi<2027,>=2026.10.19
228
228
  Requires-Dist: httpx2<3,>=2.13.0
229
229
  Requires-Dist: jsonschema<5,>=4.26.0
230
230
  Requires-Dist: mcp<3,>=2.2.0
231
+ Requires-Dist: opentelemetry-api<2,>=1.45.1
231
232
  Requires-Dist: pyjwt<3,>=2.15.0
232
233
  Description-Content-Type: text/markdown
233
234
 
@@ -297,7 +298,9 @@ commit without forge credentials:
297
298
 
298
299
  ```console
299
300
  ocr-ci preflight --local
300
- ocr-ci review --local --result review.json --stderr review.stderr --report review.md -- --commit HEAD
301
+ review_dir=$(mktemp -d)
302
+ review_dir=$(cd "$review_dir" && pwd -P)
303
+ ocr-ci review --local --result "$review_dir/review.json" --stderr "$review_dir/review.stderr" --report "$review_dir/review.md" -- --commit HEAD
301
304
  ```
302
305
 
303
306
  Local execution uses the same OCR, mandatory evidence MCP, validation and DLP
@@ -321,13 +324,17 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
321
324
 
322
325
  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.
323
326
 
324
- After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v10's exact reviewed source SHA, target SHA/protection state, merge-request author, DLP mode, and reconciled federation use. 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, unused governed services, and successful `review_read` tool use do not independently block approval. An unprotected target, disabled DLP, degraded metadata, a DLP rejection, required context degradation, admitted remediation history, legacy receipts, publication filtering, used advisory tools, uncertain federation accounting, 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.
327
+ After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v11's exact reviewed diff, actual target, policy authority, merge-request author, DLP mode, and reconciled federation use. 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, unused governed services, and successful `review_read` tool use do not independently block approval. An unprotected target, disabled DLP, degraded metadata, a DLP rejection, required context degradation, admitted remediation history, older receipts, publication filtering, used advisory tools, uncertain federation accounting, 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.
325
328
 
326
- 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.
329
+ An intentionally absent toolkit receipt retains compatible direct posting outside
330
+ GitLab CI without automatic approval. A present v9/v10 receipt can be read for historical
331
+ diagnostics, but cannot publish a new current MR review.
332
+
333
+ Accepted tradeoffs can be recorded in `.opencodereview/accepted-decisions.md`; the evidence collector supplies only applicable decisions from the selected policy commit and never lets a source change self-authorize its review. Root and nested policy `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.
327
334
 
328
335
  ## Project architecture
329
336
 
330
- 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 the mandatory built-in read-only MCP server. That internal server is always present and cannot be removed or replaced by operator configuration. Protected-policy enriched reviews can also acquire stable GitLab discussions, verified remediation history, and protected same-revision CI outcomes before OCR. Optional external tools use the governed registry-v2 federation gateway: the toolkit freezes explicit allowlists, validates schemas, origins, DLP and budgets, and reconciles content-free usage receipts while OCR remains the only model loop. Local review permits explicit HTTPS or stdio upstreams; GitLab permits HTTPS only.
337
+ 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 the mandatory built-in read-only MCP server. GitLab reviews can also expose a bounded, separately labelled actual-target snapshot for relevant code context; it does not become policy or another reviewed delta. The internal server is always present and cannot be removed or replaced by operator configuration. Protected-policy enriched reviews can also acquire stable GitLab discussions, verified remediation history, and protected same-revision CI outcomes before OCR. Optional external tools use the governed registry-v2 federation gateway: the toolkit freezes explicit allowlists, validates schemas, origins, DLP and budgets, and reconciles content-free usage receipts while OCR remains the only model loop. Local review permits explicit HTTPS or stdio upstreams; GitLab permits HTTPS only.
331
338
 
332
339
  - [Toolkit strategy](docs/engineering/toolkit_strategy.md) - durable product boundaries, architecture, invariants, and non-goals.
333
340
  - [Bounded review context](docs/review-context.md) - protected policy, GitLab discussions and same-revision CI outcomes, legacy reference migration, opaque handles, DLP, receipt, and cleanup contracts.
@@ -338,21 +345,23 @@ The shipped Repository Evidence Engine reads immutable base/head Git objects, st
338
345
 
339
346
  1. Configure protected/masked `GITLAB_API_TOKEN` and LLM variables in GitLab.
340
347
  2. Pin and checksum the OCR binary.
341
- 3. Keep `OCR_GITLAB_TARGET_PROTECTION_MODE=required` unless the project deliberately permits an unprotected target in limited, comment-only mode.
342
- 4. Merge repository review configuration, including the Rules path, into the target branch.
348
+ 3. With toolkit 0.13.0, leave `OCR_GITLAB_TARGET_PROTECTION_MODE` unset to use its `auto` default for a single MR targeting an unprotected branch under the same project's protected default-branch policy. Select explicit `required` to reject unprotected targets, or `unprotected` for the earlier limited profile.
349
+ 4. Merge repository review configuration, including the Rules path, into the protected policy branch before relying on it.
343
350
  5. Enable the CI review job in a later merge request and install this package.
344
351
  6. Run the four public helper stages around `ocr review`:
345
352
 
346
353
  ```console
347
354
  ocr-ci preflight
348
355
  ocr-ci configure
349
- ocr-ci review --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log -- ... --format json
350
- ocr-ci post --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log
356
+ review_result="/tmp/ocr-result-${CI_JOB_ID}.json"
357
+ review_stderr="/tmp/ocr-stderr-${CI_JOB_ID}.log"
358
+ ocr-ci review --result "$review_result" --stderr "$review_stderr" -- ... --format json || export OCR_EXIT_CODE=$?
359
+ ocr-ci post --result "$review_result" --stderr "$review_stderr"
351
360
  ```
352
361
 
353
362
  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).
354
363
 
355
- 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.
364
+ The two-merge-request setup is recommended because repository Rules must already exist on the captured policy commit. For a protected target that is its own authority; for an unprotected target in `auto`, it is the protected default branch. The integration MR can add Rules and CI together when it need not be reviewed: its first run may stop with setup-pending, and later MRs work after the policy change merges. Retrying the same MR cannot promote its source Rules. An unprotected target in explicit `unprotected` mode with missing target Rules fails closed. A green advisory job or pipeline does not prove that OCR or its model executed. See the [GitLab setup guide](docs/gitlab.md#first-integration-sequence) for the exact boundary.
356
365
 
357
366
  ## Configuration and safety
358
367
 
@@ -64,7 +64,9 @@ commit without forge credentials:
64
64
 
65
65
  ```console
66
66
  ocr-ci preflight --local
67
- ocr-ci review --local --result review.json --stderr review.stderr --report review.md -- --commit HEAD
67
+ review_dir=$(mktemp -d)
68
+ review_dir=$(cd "$review_dir" && pwd -P)
69
+ ocr-ci review --local --result "$review_dir/review.json" --stderr "$review_dir/review.stderr" --report "$review_dir/review.md" -- --commit HEAD
68
70
  ```
69
71
 
70
72
  Local execution uses the same OCR, mandatory evidence MCP, validation and DLP
@@ -88,13 +90,17 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
88
90
 
89
91
  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.
90
92
 
91
- After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v10's exact reviewed source SHA, target SHA/protection state, merge-request author, DLP mode, and reconciled federation use. 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, unused governed services, and successful `review_read` tool use do not independently block approval. An unprotected target, disabled DLP, degraded metadata, a DLP rejection, required context degradation, admitted remediation history, legacy receipts, publication filtering, used advisory tools, uncertain federation accounting, 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.
93
+ After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v11's exact reviewed diff, actual target, policy authority, merge-request author, DLP mode, and reconciled federation use. 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, unused governed services, and successful `review_read` tool use do not independently block approval. An unprotected target, disabled DLP, degraded metadata, a DLP rejection, required context degradation, admitted remediation history, older receipts, publication filtering, used advisory tools, uncertain federation accounting, 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.
92
94
 
93
- 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.
95
+ An intentionally absent toolkit receipt retains compatible direct posting outside
96
+ GitLab CI without automatic approval. A present v9/v10 receipt can be read for historical
97
+ diagnostics, but cannot publish a new current MR review.
98
+
99
+ Accepted tradeoffs can be recorded in `.opencodereview/accepted-decisions.md`; the evidence collector supplies only applicable decisions from the selected policy commit and never lets a source change self-authorize its review. Root and nested policy `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.
94
100
 
95
101
  ## Project architecture
96
102
 
97
- 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 the mandatory built-in read-only MCP server. That internal server is always present and cannot be removed or replaced by operator configuration. Protected-policy enriched reviews can also acquire stable GitLab discussions, verified remediation history, and protected same-revision CI outcomes before OCR. Optional external tools use the governed registry-v2 federation gateway: the toolkit freezes explicit allowlists, validates schemas, origins, DLP and budgets, and reconciles content-free usage receipts while OCR remains the only model loop. Local review permits explicit HTTPS or stdio upstreams; GitLab permits HTTPS only.
103
+ 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 the mandatory built-in read-only MCP server. GitLab reviews can also expose a bounded, separately labelled actual-target snapshot for relevant code context; it does not become policy or another reviewed delta. The internal server is always present and cannot be removed or replaced by operator configuration. Protected-policy enriched reviews can also acquire stable GitLab discussions, verified remediation history, and protected same-revision CI outcomes before OCR. Optional external tools use the governed registry-v2 federation gateway: the toolkit freezes explicit allowlists, validates schemas, origins, DLP and budgets, and reconciles content-free usage receipts while OCR remains the only model loop. Local review permits explicit HTTPS or stdio upstreams; GitLab permits HTTPS only.
98
104
 
99
105
  - [Toolkit strategy](docs/engineering/toolkit_strategy.md) - durable product boundaries, architecture, invariants, and non-goals.
100
106
  - [Bounded review context](docs/review-context.md) - protected policy, GitLab discussions and same-revision CI outcomes, legacy reference migration, opaque handles, DLP, receipt, and cleanup contracts.
@@ -105,21 +111,23 @@ The shipped Repository Evidence Engine reads immutable base/head Git objects, st
105
111
 
106
112
  1. Configure protected/masked `GITLAB_API_TOKEN` and LLM variables in GitLab.
107
113
  2. Pin and checksum the OCR binary.
108
- 3. Keep `OCR_GITLAB_TARGET_PROTECTION_MODE=required` unless the project deliberately permits an unprotected target in limited, comment-only mode.
109
- 4. Merge repository review configuration, including the Rules path, into the target branch.
114
+ 3. With toolkit 0.13.0, leave `OCR_GITLAB_TARGET_PROTECTION_MODE` unset to use its `auto` default for a single MR targeting an unprotected branch under the same project's protected default-branch policy. Select explicit `required` to reject unprotected targets, or `unprotected` for the earlier limited profile.
115
+ 4. Merge repository review configuration, including the Rules path, into the protected policy branch before relying on it.
110
116
  5. Enable the CI review job in a later merge request and install this package.
111
117
  6. Run the four public helper stages around `ocr review`:
112
118
 
113
119
  ```console
114
120
  ocr-ci preflight
115
121
  ocr-ci configure
116
- ocr-ci review --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log -- ... --format json
117
- ocr-ci post --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log
122
+ review_result="/tmp/ocr-result-${CI_JOB_ID}.json"
123
+ review_stderr="/tmp/ocr-stderr-${CI_JOB_ID}.log"
124
+ ocr-ci review --result "$review_result" --stderr "$review_stderr" -- ... --format json || export OCR_EXIT_CODE=$?
125
+ ocr-ci post --result "$review_result" --stderr "$review_stderr"
118
126
  ```
119
127
 
120
128
  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).
121
129
 
122
- 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.
130
+ The two-merge-request setup is recommended because repository Rules must already exist on the captured policy commit. For a protected target that is its own authority; for an unprotected target in `auto`, it is the protected default branch. The integration MR can add Rules and CI together when it need not be reviewed: its first run may stop with setup-pending, and later MRs work after the policy change merges. Retrying the same MR cannot promote its source Rules. An unprotected target in explicit `unprotected` mode with missing target Rules fails closed. A green advisory job or pipeline does not prove that OCR or its model executed. See the [GitLab setup guide](docs/gitlab.md#first-integration-sequence) for the exact boundary.
123
131
 
124
132
  ## Configuration and safety
125
133
 
@@ -27,6 +27,8 @@ dependencies = [
27
27
  "mcp>=2.2.0,<3",
28
28
  # MCP crypto accepts older PyJWT; enforce its security floor for resolver installs.
29
29
  "PyJWT>=2.15.0,<3",
30
+ # MCP accepts an OpenTelemetry API version with vulnerable tracestate parsing.
31
+ "opentelemetry-api>=1.45.1,<2",
30
32
  "jsonschema>=4.26.0,<5",
31
33
  "httpx2>=2.13.0,<3",
32
34
  "anyio>=4.10.0,<5",
@@ -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.12.1'
22
- __version_tuple__ = version_tuple = (0, 12, 1)
21
+ __version__ = version = '0.13.0'
22
+ __version_tuple__ = version_tuple = (0, 13, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
+ import os
6
7
  import sys
7
8
  from collections.abc import Sequence
8
9
  from pathlib import Path
@@ -93,6 +94,9 @@ def main(argv: Sequence[str] | None = None) -> int:
93
94
  **({"report_path": Path(args.report)} if args.report is not None else {}),
94
95
  **({"debug_dir": Path(args.debug_dir)} if args.debug_dir is not None else {}),
95
96
  )
97
+ except review_runner.ReviewRunnerNoHandoff as exc:
98
+ print(f"Cannot run Open Code Review: {exc}", file=sys.stderr)
99
+ return os.EX_TEMPFAIL
96
100
  except review_runner.ReviewRunnerError as exc:
97
101
  if not isinstance(exc, review_runner.ReviewRejected):
98
102
  print(f"Cannot run Open Code Review: {exc}", file=sys.stderr)
@@ -147,9 +147,9 @@ def redact_env_secret_values(text: str) -> str:
147
147
  def redact_sensitive(text: str) -> str:
148
148
  """Replace known secret values from the process environment with `***`.
149
149
 
150
- Used before publishing OCR-controlled text to the merge request, including
151
- stderr excerpts from `OCR_POST_ERROR_DETAILS=1` and structured OCR JSON
152
- fields. Performs two passes:
150
+ Used before publishing OCR-controlled text to the merge request and when
151
+ rendering bounded local diagnostics from structured OCR JSON fields.
152
+ Performs two passes:
153
153
 
154
154
  1. Replace each known env value (length >= 4). This is exact-match
155
155
  replacement over a small allowlist of secret-bearing environment
@@ -4,7 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  import stat
7
+ from collections.abc import Iterator
8
+ from contextlib import contextmanager
7
9
  from dataclasses import dataclass
10
+ from fcntl import LOCK_EX, LOCK_NB, flock
8
11
  from pathlib import Path
9
12
 
10
13
 
@@ -18,6 +21,7 @@ class EvidenceArtifacts:
18
21
  policy_rules: Path
19
22
  context_store: Path
20
23
  pre_execution_status: Path
24
+ review_run_lock: Path
21
25
  action_receipt: Path
22
26
  action_receipt_lock: Path
23
27
  dlp_decisions: Path
@@ -35,6 +39,7 @@ def repository_artifacts(root: Path | None = None) -> EvidenceArtifacts:
35
39
  policy_rules=directory / "policy-rules.json",
36
40
  context_store=directory / "context.json",
37
41
  pre_execution_status=directory / "pre-execution-status.json",
42
+ review_run_lock=directory / ".review-run.lock",
38
43
  action_receipt=directory / "evidence-actions.json",
39
44
  action_receipt_lock=directory / ".evidence-actions.json.lock",
40
45
  dlp_decisions=directory / "private-dlp-decisions.json",
@@ -48,10 +53,92 @@ def prepare_artifact_directory(artifacts: EvidenceArtifacts) -> None:
48
53
  if directory.is_symlink():
49
54
  raise OSError(f"refusing private artifact directory symlink: {directory}")
50
55
  directory.mkdir(mode=0o700, parents=False, exist_ok=True)
51
- metadata = directory.stat(follow_symlinks=False)
52
- if not stat.S_ISDIR(metadata.st_mode):
53
- raise OSError(f"private artifact path is not a directory: {directory}")
54
- directory.chmod(0o700)
56
+ descriptor = os.open(
57
+ directory,
58
+ os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0),
59
+ )
60
+ try:
61
+ metadata = os.fstat(descriptor)
62
+ named = directory.stat(follow_symlinks=False)
63
+ if (
64
+ not stat.S_ISDIR(metadata.st_mode)
65
+ or metadata.st_uid != os.getuid()
66
+ or (metadata.st_dev, metadata.st_ino) != (named.st_dev, named.st_ino)
67
+ ):
68
+ raise OSError(f"private artifact path is not a safe directory: {directory}")
69
+ os.fchmod(descriptor, 0o700)
70
+ named = directory.stat(follow_symlinks=False)
71
+ if (metadata.st_dev, metadata.st_ino) != (named.st_dev, named.st_ino):
72
+ raise OSError(f"private artifact directory changed during setup: {directory}")
73
+ finally:
74
+ os.close(descriptor)
75
+
76
+
77
+ class ReviewRunBusy(OSError):
78
+ """Another review owns the fixed private artifacts for this repository."""
79
+
80
+
81
+ @contextmanager
82
+ def locked_review_run(artifacts: EvidenceArtifacts) -> Iterator[None]:
83
+ """Hold one stable private lock across a review's entire artifact lifetime."""
84
+
85
+ prepare_artifact_directory(artifacts)
86
+ directory = -1
87
+ descriptor = -1
88
+ try:
89
+ directory = os.open(
90
+ artifacts.directory,
91
+ os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0),
92
+ )
93
+ directory_stat = os.fstat(directory)
94
+ named_directory = artifacts.directory.stat(follow_symlinks=False)
95
+ if (
96
+ not stat.S_ISDIR(directory_stat.st_mode)
97
+ or directory_stat.st_uid != os.getuid()
98
+ or (directory_stat.st_dev, directory_stat.st_ino)
99
+ != (named_directory.st_dev, named_directory.st_ino)
100
+ or stat.S_IMODE(directory_stat.st_mode) & 0o077
101
+ ):
102
+ raise OSError("private review lock directory is unsafe")
103
+ descriptor = os.open(
104
+ artifacts.review_run_lock.name,
105
+ os.O_CREAT | os.O_RDWR | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0),
106
+ 0o600,
107
+ dir_fd=directory,
108
+ )
109
+ opened = os.fstat(descriptor)
110
+ named = os.stat(
111
+ artifacts.review_run_lock.name,
112
+ dir_fd=directory,
113
+ follow_symlinks=False,
114
+ )
115
+ if (
116
+ not stat.S_ISREG(opened.st_mode)
117
+ or opened.st_uid != os.getuid()
118
+ or opened.st_nlink != 1
119
+ or stat.S_IMODE(opened.st_mode) & 0o077
120
+ or (opened.st_dev, opened.st_ino) != (named.st_dev, named.st_ino)
121
+ ):
122
+ raise OSError("private review lock file is unsafe")
123
+ try:
124
+ flock(descriptor, LOCK_EX | LOCK_NB)
125
+ except BlockingIOError as exc:
126
+ raise ReviewRunBusy("another review is already running in this repository") from exc
127
+ named = os.stat(
128
+ artifacts.review_run_lock.name,
129
+ dir_fd=directory,
130
+ follow_symlinks=False,
131
+ )
132
+ if (opened.st_dev, opened.st_ino) != (named.st_dev, named.st_ino):
133
+ raise OSError("private review lock file changed during acquisition")
134
+ # Keep the named inode across runs; unlinking it would let a second
135
+ # review lock a replacement while this review still holds the old one.
136
+ yield
137
+ finally:
138
+ if descriptor >= 0:
139
+ os.close(descriptor)
140
+ if directory >= 0:
141
+ os.close(directory)
55
142
 
56
143
 
57
144
  def write_private_bytes(path: Path, content: bytes) -> None:
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
- from pathlib import Path
6
+ from pathlib import Path, PurePosixPath
7
7
 
8
8
  from ocr_toolkit.evidence.categorize import categorize_paths
9
9
  from ocr_toolkit.evidence.collectors import collect_ref_facts, fact_deltas
@@ -16,7 +16,11 @@ from ocr_toolkit.evidence.repository import (
16
16
  file_deltas,
17
17
  )
18
18
  from ocr_toolkit.evidence.store import EvidenceStore, EvidenceStoreError
19
- from ocr_toolkit.evidence.store.contracts import POLICY_KINDS
19
+ from ocr_toolkit.evidence.store.contracts import POLICY_KINDS, TARGET_CONTEXT_KINDS
20
+
21
+ MAX_TARGET_FILE_RECORDS = 64
22
+ MAX_TARGET_DEPENDENCY_RECORDS = 64
23
+ MAX_TARGET_DIAGNOSTICS = 32
20
24
 
21
25
 
22
26
  def _commit_refs(
@@ -44,15 +48,62 @@ def _component_for_path(path: str) -> str:
44
48
  return path.split("/", 1)[0] if "/" in path else "repository"
45
49
 
46
50
 
51
+ def _relevant_target_dependency(path: str, changed_paths: tuple[str, ...]) -> bool:
52
+ """Select manifests in a changed path's own directory or an ancestor."""
53
+
54
+ parent = PurePosixPath(path).parent.as_posix()
55
+ return any(
56
+ changed == path or parent == "." or changed.startswith(parent + "/")
57
+ for changed in changed_paths
58
+ )
59
+
60
+
61
+ def _add_target_diagnostic(store: EvidenceStore, message: str) -> bool:
62
+ """Keep an optional target notice only when the base evidence still serializes."""
63
+
64
+ prior_diagnostics = list(store.diagnostics)
65
+ try:
66
+ store.add_diagnostic(message)
67
+ store.to_json()
68
+ except EvidenceStoreError as exc:
69
+ store.diagnostics = prior_diagnostics
70
+ if str(exc) not in {
71
+ "serialized evidence store exceeds its byte budget",
72
+ "evidence diagnostic must contain between 1 and 1024 characters",
73
+ }:
74
+ raise
75
+ return False
76
+ return True
77
+
78
+
79
+ def _as_target_record(record: EvidenceRecord, target_sha: str) -> EvidenceRecord:
80
+ """Reuse a collected code fact without inheriting its old ref or trust label."""
81
+
82
+ return EvidenceRecord(
83
+ kind=record.kind,
84
+ value=record.value,
85
+ source_path=record.source_path,
86
+ ref=RefRole.TARGET,
87
+ commit_sha=target_sha,
88
+ component=record.component,
89
+ provenance=record.provenance,
90
+ confidence=record.confidence,
91
+ trust=TrustClass.TARGET_REPOSITORY,
92
+ sensitivity=record.sensitivity,
93
+ staleness=record.staleness,
94
+ )
95
+
96
+
47
97
  def collect_repository_evidence(
48
98
  root: Path | None = None,
49
99
  *,
50
100
  base_ref: str | None = None,
51
101
  head_ref: str | None = None,
52
102
  policy_ref: str | None = None,
103
+ target_ref: str | None = None,
53
104
  include_policy_records: bool = True,
54
105
  ) -> EvidenceStore:
55
- """Build evidence, optionally omitting target-derived policy authority records."""
106
+ """Build B/H diff evidence, separate P policy, and optional untrusted T context."""
56
107
 
57
108
  reader = GitRepositoryReader(root or Path.cwd())
58
109
  base_sha, head_sha = _commit_refs(reader, base_ref, head_ref)
@@ -219,4 +270,81 @@ def collect_repository_evidence(
219
270
  *policy_diagnostics,
220
271
  ):
221
272
  store.add_diagnostic(diagnostic)
273
+ if target_ref is not None:
274
+ # The actual target may differ from both the historical diff base and
275
+ # the protected policy branch. Its facts are optional code context and
276
+ # must never consume the budget reserved for B/H/P or alter their deltas.
277
+ store.to_json() # A pre-existing B/H/P budget failure remains a normal error.
278
+ target_sha = reader.resolve_commit(target_ref)
279
+ store.target = EvidenceSnapshot(RefRole.TARGET, target_sha, ())
280
+ try:
281
+ header_size = len(store.to_json().encode("utf-8"))
282
+ except EvidenceStoreError as exc:
283
+ if str(exc) != "serialized evidence store exceeds its byte budget":
284
+ raise
285
+ header_size = store.limits.max_bytes
286
+ if header_size > store.limits.max_bytes - 256:
287
+ store.target = None
288
+ _add_target_diagnostic(store, "actual target context omitted by evidence byte limit")
289
+ return store
290
+ if target_sha in {base_sha, head_sha}:
291
+ # The common unchanged-target case needs no third repository scan.
292
+ prior = base if target_sha == base_sha else head
293
+ prior_facts = base_facts if target_sha == base_sha else head_facts
294
+ target_files = EvidenceSnapshot(
295
+ RefRole.TARGET,
296
+ target_sha,
297
+ tuple(_as_target_record(record, target_sha) for record in prior.records),
298
+ diagnostics=prior.diagnostics,
299
+ )
300
+ target_facts = [_as_target_record(record, target_sha) for record in prior_facts]
301
+ target_fact_diagnostics: list[str] = []
302
+ else:
303
+ try:
304
+ target_files = build_file_snapshot(
305
+ reader, target_sha, RefRole.TARGET, paths=changed
306
+ )
307
+ target_facts, target_fact_diagnostics = collect_ref_facts(
308
+ reader, target_sha, RefRole.TARGET, changed_paths=changed
309
+ )
310
+ except RepositoryEvidenceError as exc:
311
+ if str(exc) != "repository tree exceeds the bounded entry limit":
312
+ raise
313
+ # T's commit identity was resolved before this optional scan.
314
+ # A target-only tree limit cannot change B/H/P or their deltas.
315
+ _add_target_diagnostic(
316
+ store, "actual target code context omitted by repository tree entry limit"
317
+ )
318
+ return store
319
+ diagnostics = (*target_files.diagnostics, *target_fact_diagnostics)
320
+ target_diagnostics = diagnostics[:MAX_TARGET_DIAGNOSTICS]
321
+ files = target_files.records[:MAX_TARGET_FILE_RECORDS]
322
+ dependencies = tuple(
323
+ record
324
+ for record in target_facts
325
+ if record.kind in TARGET_CONTEXT_KINDS - {"repository.file"}
326
+ and _relevant_target_dependency(record.source_path, changed)
327
+ )
328
+ dependencies = tuple(
329
+ sorted(dependencies, key=lambda item: (item.kind, item.source_path, item.id))
330
+ )
331
+ selected_dependencies = dependencies[:MAX_TARGET_DEPENDENCY_RECORDS]
332
+ omitted = (
333
+ len(target_files.records) > MAX_TARGET_FILE_RECORDS
334
+ or len(dependencies) > MAX_TARGET_DEPENDENCY_RECORDS
335
+ )
336
+ for record in (*files, *selected_dependencies):
337
+ if not store.add_target_context(record):
338
+ omitted = True
339
+ if omitted:
340
+ _add_target_diagnostic(
341
+ store, "target code context is incomplete due to bounded evidence limits"
342
+ )
343
+ if diagnostics:
344
+ _add_target_diagnostic(store, "target code context has unavailable source details")
345
+ for diagnostic in target_diagnostics:
346
+ if not _add_target_diagnostic(store, f"target:{diagnostic}"):
347
+ break
348
+ if len(diagnostics) > MAX_TARGET_DIAGNOSTICS:
349
+ _add_target_diagnostic(store, "target collection diagnostics were truncated")
222
350
  return store
@@ -78,7 +78,7 @@ def collect_ref_facts(
78
78
  diagnostics = []
79
79
  trust = (
80
80
  TrustClass.TARGET_REPOSITORY
81
- if ref in {RefRole.BASE, RefRole.POLICY}
81
+ if ref in {RefRole.BASE, RefRole.POLICY, RefRole.TARGET}
82
82
  else TrustClass.SOURCE_REPOSITORY
83
83
  )
84
84
  changed_exact = tuple(sorted(set(changed_paths)))
@@ -23,7 +23,7 @@ from ocr_toolkit.context.mcp import (
23
23
  )
24
24
  from ocr_toolkit.context.store import ContextStore
25
25
  from ocr_toolkit.evidence.actions import UNATTRIBUTED_ACTION, record_action
26
- from ocr_toolkit.evidence.model import CoverageRecord, EvidenceDelta, EvidenceRecord
26
+ from ocr_toolkit.evidence.model import CoverageRecord, EvidenceDelta, EvidenceRecord, RefRole
27
27
  from ocr_toolkit.evidence.policy.schema import is_legacy_policy_value
28
28
  from ocr_toolkit.evidence.store import EvidenceStore, EvidenceStoreError
29
29
 
@@ -161,6 +161,14 @@ def evidence_summary(store: EvidenceStore) -> dict[str, object]:
161
161
  "policy": policy,
162
162
  "merge_request_context": merge_request_context,
163
163
  "policy_ref": store.policy.commit_sha if store.policy else None,
164
+ "target": store.target.commit_sha if store.target else None,
165
+ "target_context": {
166
+ "records": sum(record.ref is RefRole.TARGET for record in store.records),
167
+ "trust": "target_repository" if store.target else None,
168
+ "content_role": "untrusted_code_evidence" if store.target else None,
169
+ "authoritative_for_policy": False,
170
+ "absence_authoritative": False,
171
+ },
164
172
  "coverage_contract": ("repository.evidence-coverage/v1" if store.coverage else "absent"),
165
173
  "base": store.base.commit_sha if store.base else None,
166
174
  "head": store.head.commit_sha if store.head else None,
@@ -233,8 +241,8 @@ def _list_records(store: EvidenceStore, arguments: dict[str, object]) -> dict[st
233
241
  raise EvidenceMCPError("delta_kind requires kind=repository.evidence_delta")
234
242
  if query.kind == "repository.evidence_delta" and query.ref is not None:
235
243
  raise EvidenceMCPError("evidence deltas span base and head and do not accept ref")
236
- if query.ref not in {None, "base", "head", "policy", "shared"}:
237
- raise EvidenceMCPError("ref must be base, head, policy, or shared")
244
+ if query.ref not in {None, "base", "head", "policy", "target", "shared"}:
245
+ raise EvidenceMCPError("ref must be base, head, policy, target, or shared")
238
246
  page_size = arguments.get("page_size", DEFAULT_PAGE_SIZE)
239
247
  if isinstance(page_size, bool) or not isinstance(page_size, int):
240
248
  raise EvidenceMCPError("page_size must be an integer")
@@ -375,8 +383,8 @@ def _search_records(store: EvidenceStore, arguments: dict[str, object]) -> dict[
375
383
  raise EvidenceMCPError("delta_kind requires kind=repository.evidence_delta")
376
384
  if kind == "repository.evidence_delta" and ref is not None:
377
385
  raise EvidenceMCPError("evidence deltas span base and head and do not accept ref")
378
- if ref not in {None, "base", "head", "policy", "shared"}:
379
- raise EvidenceMCPError("ref must be base, head, policy, or shared")
386
+ if ref not in {None, "base", "head", "policy", "target", "shared"}:
387
+ raise EvidenceMCPError("ref must be base, head, policy, target, or shared")
380
388
  max_results = arguments.get("max_results", DEFAULT_SEARCH_RESULTS)
381
389
  if isinstance(max_results, bool) or not isinstance(max_results, int):
382
390
  raise EvidenceMCPError("max_results must be an integer")
@@ -560,7 +568,8 @@ def _tool_definition() -> dict[str, object]:
560
568
  return {
561
569
  "name": TOOL_NAME,
562
570
  "description": (
563
- "Read bounded, redacted repository evidence for immutable base/head refs. "
571
+ "Read bounded, redacted repository evidence for immutable base/head refs and "
572
+ "optional actual-target code context (ref=target). "
564
573
  "Use summary first, list to narrow, and get for one stable record. Query "
565
574
  "kind=repository.evidence_delta with optional delta_kind for base/head changes. "
566
575
  "Current structured decisions and guidance are target-derived non-authoritative "
@@ -601,7 +610,7 @@ def _tool_definition() -> dict[str, object]:
601
610
  },
602
611
  "ref": {
603
612
  "type": "string",
604
- "enum": ["base", "head", "policy", "shared"],
613
+ "enum": ["base", "head", "policy", "target", "shared"],
605
614
  "description": "Optional immutable-ref filter for action=list only.",
606
615
  },
607
616
  "page_size": {
@@ -659,7 +668,7 @@ def _search_tool_definition() -> dict[str, object]:
659
668
  ),
660
669
  },
661
670
  "component": {"type": "string", "maxLength": 256},
662
- "ref": {"type": "string", "enum": ["base", "head", "policy", "shared"]},
671
+ "ref": {"type": "string", "enum": ["base", "head", "policy", "target", "shared"]},
663
672
  "max_results": {
664
673
  "type": "integer",
665
674
  "minimum": 1,
@@ -43,6 +43,7 @@ class RefRole(str, Enum):
43
43
  BASE = "base"
44
44
  HEAD = "head"
45
45
  POLICY = "policy"
46
+ TARGET = "target"
46
47
  SHARED = "shared"
47
48
 
48
49
 
@@ -408,9 +409,9 @@ class EvidenceSnapshot:
408
409
  """Ensure snapshot records match the declared ref and are ordered."""
409
410
 
410
411
  if self.ref is RefRole.SHARED:
411
- raise ValueError("snapshot ref must be base, head, or policy")
412
- if self.ref is RefRole.POLICY and self.coverage:
413
- raise ValueError("policy snapshot cannot contain coverage")
412
+ raise ValueError("snapshot ref must be base, head, policy, or target")
413
+ if self.ref in {RefRole.POLICY, RefRole.TARGET} and self.coverage:
414
+ raise ValueError("policy and target snapshots cannot contain coverage")
414
415
  if len(self.commit_sha) != 40 or not all(c in "0123456789abcdef" for c in self.commit_sha):
415
416
  raise ValueError("snapshot commit_sha must be a lowercase 40-character SHA-1")
416
417
  for record in self.records: