open-code-review-toolkit 0.12.0__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.0 → open_code_review_toolkit-0.13.0}/.gitignore +1 -0
  2. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/PKG-INFO +21 -10
  3. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/README.md +19 -9
  4. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/pyproject.toml +2 -0
  5. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/_version.py +2 -2
  6. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/cli.py +4 -0
  7. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/common/redaction.py +3 -3
  8. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/dlp.py +87 -7
  9. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/artifacts.py +91 -4
  10. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/collect.py +131 -3
  11. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/collectors/orchestration.py +1 -1
  12. open_code_review_toolkit-0.13.0/src/ocr_toolkit/evidence/gitlinks.py +66 -0
  13. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/mcp.py +17 -8
  14. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/model.py +4 -3
  15. open_code_review_toolkit-0.13.0/src/ocr_toolkit/evidence/opaque_code.py +108 -0
  16. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/project.py +24 -1
  17. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/repository.py +65 -9
  18. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/store/contracts.py +3 -2
  19. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/store/core.py +81 -0
  20. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/store/readback.py +11 -1
  21. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/federation/admission.py +15 -5
  22. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/federation/contracts.py +14 -1
  23. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/federation/gateway.py +19 -6
  24. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/federation/runtime.py +8 -8
  25. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/local_chatgpt.py +16 -4
  26. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/ocr_result.py +5 -2
  27. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/formatting.py +34 -7
  28. open_code_review_toolkit-0.13.0/src/ocr_toolkit/posting/freshness.py +166 -0
  29. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/gitlab.py +1 -0
  30. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/gitlab_approval.py +69 -2
  31. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/snapshot.py +3 -6
  32. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/workflow.py +716 -236
  33. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/pre_execution.py +159 -15
  34. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/preflight.py +21 -1
  35. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/gitlab.py +182 -29
  36. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/dlp.py +98 -8
  37. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/federation.py +67 -4
  38. open_code_review_toolkit-0.13.0/src/ocr_toolkit/review_identity.py +58 -0
  39. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/review_receipt.py +234 -52
  40. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/review_runner.py +461 -105
  41. open_code_review_toolkit-0.12.0/src/ocr_toolkit/review_identity.py +0 -26
  42. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/LICENSE +0 -0
  43. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/__init__.py +0 -0
  44. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/common/__init__.py +0 -0
  45. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/common/filesystem.py +0 -0
  46. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/common/git.py +0 -0
  47. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/common/language.py +0 -0
  48. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/common/markdown.py +0 -0
  49. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/config_writer.py +0 -0
  50. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/configure.py +0 -0
  51. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/__init__.py +0 -0
  52. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/broker.py +0 -0
  53. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/ci_outcomes.py +0 -0
  54. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/contracts.py +0 -0
  55. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/mcp.py +0 -0
  56. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/policy.py +0 -0
  57. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/recognizers.py +0 -0
  58. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/context/store.py +0 -0
  59. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/__init__.py +0 -0
  60. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/__main__.py +0 -0
  61. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/actions.py +0 -0
  62. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/categorize.py +0 -0
  63. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/collectors/__init__.py +0 -0
  64. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/collectors/graphs.py +0 -0
  65. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/collectors/projections.py +0 -0
  66. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/collectors/registry.py +0 -0
  67. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/collectors/sources.py +0 -0
  68. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/coverage.py +0 -0
  69. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/__init__.py +0 -0
  70. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/ansible/__init__.py +0 -0
  71. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/ansible/requirements.py +0 -0
  72. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/ansible/topology.py +0 -0
  73. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/contracts.py +0 -0
  74. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/go.py +0 -0
  75. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/javascript.py +0 -0
  76. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/php.py +0 -0
  77. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/ecosystems/python.py +0 -0
  78. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/__init__.py +0 -0
  79. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/contracts.py +0 -0
  80. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/detection.py +0 -0
  81. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/providers/__init__.py +0 -0
  82. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/providers/frontend.py +0 -0
  83. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/providers/go.py +0 -0
  84. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/providers/php.py +0 -0
  85. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/providers/python.py +0 -0
  86. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/registry.py +0 -0
  87. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/schema.py +0 -0
  88. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/frameworks/templates.py +0 -0
  89. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/infrastructure.py +0 -0
  90. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/invocation.py +0 -0
  91. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/policy/__init__.py +0 -0
  92. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/policy/contracts.py +0 -0
  93. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/policy/decisions.py +0 -0
  94. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/policy/guidance.py +0 -0
  95. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/policy/registry.py +0 -0
  96. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/policy/schema.py +0 -0
  97. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/policy/scopes.py +0 -0
  98. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/review_context.py +0 -0
  99. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/store/__init__.py +0 -0
  100. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/store/atomic.py +0 -0
  101. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/evidence/store/values.py +0 -0
  102. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/federation/__init__.py +0 -0
  103. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/federation/registry.py +0 -0
  104. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/federation/relay.py +0 -0
  105. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/federation/transport.py +0 -0
  106. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/mcp_config.py +0 -0
  107. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/__init__.py +0 -0
  108. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/__main__.py +0 -0
  109. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/approval.py +0 -0
  110. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/comments.py +0 -0
  111. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/markers.py +0 -0
  112. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/payloads.py +0 -0
  113. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/reconciliation.py +0 -0
  114. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/result.py +0 -0
  115. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/settings.py +0 -0
  116. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/suggestions.py +0 -0
  117. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/posting/transaction.py +0 -0
  118. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/provider_config.py +0 -0
  119. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/provider_failure.py +0 -0
  120. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/__init__.py +0 -0
  121. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/gitlab_ci.py +0 -0
  122. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/gitlab_context.py +0 -0
  123. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/gitlab_discussions.py +0 -0
  124. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/gitlab_identity.py +0 -0
  125. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/gitlab_remediation.py +0 -0
  126. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/local.py +0 -0
  127. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/providers/local_config.py +0 -0
  128. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/py.typed +0 -0
  129. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/__init__.py +0 -0
  130. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/metadata.py +0 -0
  131. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/model.py +0 -0
  132. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/outcome.py +0 -0
  133. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/result.py +0 -0
  134. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/sections.py +0 -0
  135. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/text.py +0 -0
  136. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/reporting/usage.py +0 -0
  137. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/result_contract.py +0 -0
  138. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/result_usage.py +0 -0
  139. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/review_debug.py +0 -0
  140. {open_code_review_toolkit-0.12.0 → open_code_review_toolkit-0.13.0}/src/ocr_toolkit/review_progress.py +0 -0
@@ -16,3 +16,4 @@ dist/
16
16
  .idea/
17
17
  *.log
18
18
  .codex
19
+ .playwright-cli/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: open-code-review-toolkit
3
- Version: 0.12.0
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
 
@@ -242,6 +243,8 @@ Description-Content-Type: text/markdown
242
243
 
243
244
  Open Code Review Toolkit is an unofficial review control and integration layer for [Alibaba Open Code Review](https://github.com/alibaba/open-code-review), with GitLab CI and standalone local providers. It provides bounded repository evidence, a compact review bootstrap, a built-in read-only MCP server, environment-driven OCR configuration, preflight validation, local Markdown reports, and safe GitLab merge-request posting. It does **not** bundle or download the `ocr` binary.
244
245
 
246
+ [Explore the toolkit site](https://open-code-review-toolkit.pages.dev/) for a quick overview, demo video, and latest release notes.
247
+
245
248
  > [!NOTE]
246
249
  > The project is under active development; the public API, CLI, environment contract, and generated schemas may evolve before 1.0.
247
250
 
@@ -295,7 +298,9 @@ commit without forge credentials:
295
298
 
296
299
  ```console
297
300
  ocr-ci preflight --local
298
- 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
299
304
  ```
300
305
 
301
306
  Local execution uses the same OCR, mandatory evidence MCP, validation and DLP
@@ -319,13 +324,17 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
319
324
 
320
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.
321
326
 
322
- After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v9'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.
328
+
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.
323
332
 
324
- 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.
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.
325
334
 
326
335
  ## Project architecture
327
336
 
328
- 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.
329
338
 
330
339
  - [Toolkit strategy](docs/engineering/toolkit_strategy.md) - durable product boundaries, architecture, invariants, and non-goals.
331
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.
@@ -336,21 +345,23 @@ The shipped Repository Evidence Engine reads immutable base/head Git objects, st
336
345
 
337
346
  1. Configure protected/masked `GITLAB_API_TOKEN` and LLM variables in GitLab.
338
347
  2. Pin and checksum the OCR binary.
339
- 3. Keep `OCR_GITLAB_TARGET_PROTECTION_MODE=required` unless the project deliberately permits an unprotected target in limited, comment-only mode.
340
- 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.
341
350
  5. Enable the CI review job in a later merge request and install this package.
342
351
  6. Run the four public helper stages around `ocr review`:
343
352
 
344
353
  ```console
345
354
  ocr-ci preflight
346
355
  ocr-ci configure
347
- ocr-ci review --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log -- ... --format json
348
- 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"
349
360
  ```
350
361
 
351
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).
352
363
 
353
- 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.
354
365
 
355
366
  ## Configuration and safety
356
367
 
@@ -9,6 +9,8 @@
9
9
 
10
10
  Open Code Review Toolkit is an unofficial review control and integration layer for [Alibaba Open Code Review](https://github.com/alibaba/open-code-review), with GitLab CI and standalone local providers. It provides bounded repository evidence, a compact review bootstrap, a built-in read-only MCP server, environment-driven OCR configuration, preflight validation, local Markdown reports, and safe GitLab merge-request posting. It does **not** bundle or download the `ocr` binary.
11
11
 
12
+ [Explore the toolkit site](https://open-code-review-toolkit.pages.dev/) for a quick overview, demo video, and latest release notes.
13
+
12
14
  > [!NOTE]
13
15
  > The project is under active development; the public API, CLI, environment contract, and generated schemas may evolve before 1.0.
14
16
 
@@ -62,7 +64,9 @@ commit without forge credentials:
62
64
 
63
65
  ```console
64
66
  ocr-ci preflight --local
65
- 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
66
70
  ```
67
71
 
68
72
  Local execution uses the same OCR, mandatory evidence MCP, validation and DLP
@@ -86,13 +90,17 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
86
90
 
87
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.
88
92
 
89
- After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v9'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.
94
+
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.
90
98
 
91
- 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.
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.
92
100
 
93
101
  ## Project architecture
94
102
 
95
- 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.
96
104
 
97
105
  - [Toolkit strategy](docs/engineering/toolkit_strategy.md) - durable product boundaries, architecture, invariants, and non-goals.
98
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.
@@ -103,21 +111,23 @@ The shipped Repository Evidence Engine reads immutable base/head Git objects, st
103
111
 
104
112
  1. Configure protected/masked `GITLAB_API_TOKEN` and LLM variables in GitLab.
105
113
  2. Pin and checksum the OCR binary.
106
- 3. Keep `OCR_GITLAB_TARGET_PROTECTION_MODE=required` unless the project deliberately permits an unprotected target in limited, comment-only mode.
107
- 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.
108
116
  5. Enable the CI review job in a later merge request and install this package.
109
117
  6. Run the four public helper stages around `ocr review`:
110
118
 
111
119
  ```console
112
120
  ocr-ci preflight
113
121
  ocr-ci configure
114
- ocr-ci review --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log -- ... --format json
115
- 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"
116
126
  ```
117
127
 
118
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).
119
129
 
120
- 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.
121
131
 
122
132
  ## Configuration and safety
123
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.0'
22
- __version_tuple__ = version_tuple = (0, 12, 0)
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
@@ -3,14 +3,20 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import html
6
+ import ipaddress
6
7
  import re
7
8
  import unicodedata
8
9
  from collections.abc import Iterator, Mapping
9
10
  from contextlib import contextmanager
10
11
  from contextvars import ContextVar
11
12
  from dataclasses import dataclass
13
+ from datetime import date
12
14
 
13
- from ocr_toolkit.common.redaction import redact_env_secret_values, redact_sensitive
15
+ from ocr_toolkit.common.redaction import (
16
+ SENSITIVE_NAMED_KEY_PATTERN,
17
+ redact_env_secret_values,
18
+ redact_sensitive,
19
+ )
14
20
  from ocr_toolkit.context.contracts import TextBudgets
15
21
 
16
22
  EMAIL_RE = re.compile(
@@ -18,6 +24,20 @@ EMAIL_RE = re.compile(
18
24
  r"@(?:[a-z0-9-]{1,63}\.)+[a-z]{2,63}(?![a-z0-9_-])"
19
25
  )
20
26
  PHONE_RE = re.compile(r"(?<!\w)(?:\+?[1-9][\d .()/-]{7,24}\d)(?!\w)")
27
+ PURE_CREDENTIAL_ASSIGNMENT_RE = re.compile(
28
+ rf"(?im)^[ \t]*{SENSITIVE_NAMED_KEY_PATTERN}[ \t]*:[ \t]*"
29
+ r"(?:(?P<quote>['\"])\{\{[ \t]*[A-Za-z_][A-Za-z0-9_]*[ \t]*\}\}(?P=quote)|"
30
+ r"(?P<empty>\"\"|''))[ \t]*$"
31
+ )
32
+ ISO_DATE_RE = re.compile(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})")
33
+ VERSION_CONTEXT_RE = re.compile(r"(?i)\b(?:version|release|build|pin)[ \t]+(?:v)?$")
34
+ DATE_CONTEXT_RE = re.compile(
35
+ r"(?i)\b(?:date|dated|on|since|until|before|after|expires)(?:[ \t]+on)?[ \t]+$"
36
+ )
37
+ IP_CONTEXT_RE = re.compile(
38
+ r"(?i)(?:\b|_)(?:ipv4|ip|cidr|subnet|address|host|loginhost|listen|allow|network)\b"
39
+ )
40
+ PHONE_CONTEXT_RE = re.compile(r"(?i)\b(?:call|phone|tel|contact)[ \t]*$")
21
41
  MARKDOWN_DEST_RE = re.compile(
22
42
  r"(?:\]\(\s*<?(?:https?://|mailto:)|^\s*\[[^\]\n]+\]:\s*<?(?:https?://|mailto:)|"
23
43
  r"<(?:https?://|mailto:))",
@@ -95,11 +115,59 @@ def _contains_phone(value: str) -> bool:
95
115
  """Recognize formatted phone-like values without classifying bare identifiers."""
96
116
 
97
117
  return any(
98
- match.group(0).startswith("+") or any(separator in match.group(0) for separator in " .()/-")
118
+ (
119
+ match.group(0).startswith("+")
120
+ or any(separator in match.group(0) for separator in " .()/-")
121
+ )
122
+ and not _technical_number(value, match)
99
123
  for match in PHONE_RE.finditer(value)
100
124
  )
101
125
 
102
126
 
127
+ def _technical_number(value: str, match: re.Match[str]) -> bool:
128
+ """Exclude only bounded, parsed technical syntax from phone candidates."""
129
+
130
+ candidate = match.group(0)
131
+ if candidate.startswith("+") or candidate != candidate.strip():
132
+ return False
133
+ before, after = value[: match.start()], value[match.end() :]
134
+ if before.endswith((".", "/", "-")) or re.match(r"(?:[./-]\d)", after):
135
+ return False
136
+ if PHONE_CONTEXT_RE.search(before[-24:]):
137
+ return False
138
+ try:
139
+ if "/" in candidate:
140
+ ipaddress.IPv4Network(candidate, strict=False)
141
+ return True
142
+ elif isinstance(ipaddress.ip_address(candidate), ipaddress.IPv4Address):
143
+ if after.startswith(":"):
144
+ port = re.match(r":(\d{1,5})\b", after)
145
+ return bool(port and 1 <= int(port[1]) <= 65535)
146
+ return bool(IP_CONTEXT_RE.search(before[-96:]))
147
+ except ValueError:
148
+ pass
149
+ if ISO_DATE_RE.fullmatch(candidate) and DATE_CONTEXT_RE.search(before[-32:]):
150
+ try:
151
+ date(int(candidate[:4]), int(candidate[5:7]), int(candidate[8:10]))
152
+ except ValueError:
153
+ return False
154
+ return True
155
+ if re.fullmatch(r"\d{4}\.\d{1,2}\.\d{1,3}", candidate):
156
+ return bool(VERSION_CONTEXT_RE.search(before[-32:]))
157
+ if re.fullmatch(r"\d+\.\d+", candidate):
158
+ return bool(
159
+ re.search(r"(?i)\bdecimal\([ \t]*['\"]$", before[-32:])
160
+ and re.match(r"['\"][ \t]*\)", after)
161
+ )
162
+ return False
163
+
164
+
165
+ def _semantic_secret_text(value: str) -> str:
166
+ """Remove proven no-value assignments only from generic secret classification."""
167
+
168
+ return PURE_CREDENTIAL_ASSIGNMENT_RE.sub("safe_reference_assignment", value)
169
+
170
+
103
171
  def _display_normalize(value: str) -> str:
104
172
  """Approximate closed text rendered by common Markdown/HTML constructs."""
105
173
 
@@ -235,6 +303,7 @@ def check_text(
235
303
  forbidden: tuple[str | ForbiddenValue, ...] = (),
236
304
  forbidden_matcher: ForbiddenMatcher | None = None,
237
305
  allow_horizontal_tabs: bool = False,
306
+ allow_opaque_code: bool = False,
238
307
  enabled: bool | None = None,
239
308
  ) -> DLPResult:
240
309
  """Apply independent units, redaction, PII, and optional publication checks."""
@@ -258,15 +327,26 @@ def check_text(
258
327
  return DLPResult(False, None, "limit", detector)
259
328
  if not (_DLP_ENABLED.get() if enabled is None else enabled):
260
329
  return DLPResult(True, normalized, "disabled")
261
- redacted = redact_env_secret_values(redact_sensitive(normalized))
262
- if redacted != normalized:
330
+ # Exact environment values retain priority even when they resemble a reference.
331
+ if redact_env_secret_values(normalized) != normalized:
332
+ return DLPResult(False, None, "secret", "normalized")
333
+ semantic = _semantic_secret_text(normalized)
334
+ if redact_sensitive(semantic) != semantic:
263
335
  return DLPResult(False, None, "secret", "normalized")
264
336
  decoded = _html_decode(normalized) if publication else normalized
265
337
  displayed = _display_normalize(normalized) if publication else normalized
266
338
  source = _source_normalize(normalized) if publication else normalized
267
- if publication and redact_env_secret_values(redact_sensitive(displayed)) != displayed:
339
+ semantic_displayed = _display_normalize(semantic) if publication else semantic
340
+ semantic_source = _source_normalize(semantic) if publication else semantic
341
+ if publication and (
342
+ redact_env_secret_values(displayed) != displayed
343
+ or redact_sensitive(semantic_displayed) != semantic_displayed
344
+ ):
268
345
  return DLPResult(False, None, "secret", "displayed")
269
- if publication and redact_env_secret_values(redact_sensitive(source)) != source:
346
+ if publication and (
347
+ redact_env_secret_values(source) != source
348
+ or redact_sensitive(semantic_source) != semantic_source
349
+ ):
270
350
  return DLPResult(False, None, "secret", "source")
271
351
  pii_candidates = (("normalized", normalized),)
272
352
  if publication:
@@ -289,7 +369,7 @@ def check_text(
289
369
  else "html_comment"
290
370
  if HTML_COMMENT_RE.search(decoded)
291
371
  else "html_tag"
292
- if HTML_TAG_RE.search(decoded)
372
+ if HTML_TAG_RE.search(decoded) and not (allow_opaque_code and decoded == normalized)
293
373
  else "display_control"
294
374
  if any(_is_display_control(character) for character in decoded)
295
375
  else None
@@ -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: