open-code-review-toolkit 0.7.0__tar.gz → 0.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/PKG-INFO +3 -3
  2. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/README.md +2 -2
  3. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/pyproject.toml +5 -0
  4. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/_version.py +2 -2
  5. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/__main__.py +3 -1
  6. open_code_review_toolkit-0.7.1/src/ocr_toolkit/evidence/actions.py +124 -0
  7. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/artifacts.py +4 -0
  8. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/mcp.py +20 -2
  9. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/ocr_result.py +2 -2
  10. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/posting/approval.py +65 -20
  11. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/posting/formatting.py +136 -144
  12. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/posting/workflow.py +9 -8
  13. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/preflight.py +1 -1
  14. open_code_review_toolkit-0.7.1/src/ocr_toolkit/result_usage.py +216 -0
  15. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/review_runner.py +147 -32
  16. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/.gitignore +0 -0
  17. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/LICENSE +0 -0
  18. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/__init__.py +0 -0
  19. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/cli.py +0 -0
  20. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/common/__init__.py +0 -0
  21. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/common/filesystem.py +0 -0
  22. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/common/git.py +0 -0
  23. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/common/language.py +0 -0
  24. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/common/markdown.py +0 -0
  25. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/common/redaction.py +0 -0
  26. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/config_writer.py +0 -0
  27. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/configure.py +0 -0
  28. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/context/__init__.py +0 -0
  29. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/context/adapters.py +0 -0
  30. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/context/broker.py +0 -0
  31. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/context/contracts.py +0 -0
  32. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/context/dlp.py +0 -0
  33. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/context/mcp.py +0 -0
  34. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/context/policy.py +0 -0
  35. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/context/recognizers.py +0 -0
  36. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/context/store.py +0 -0
  37. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/__init__.py +0 -0
  38. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/categorize.py +0 -0
  39. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/collect.py +0 -0
  40. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/collectors/__init__.py +0 -0
  41. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/collectors/graphs.py +0 -0
  42. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/collectors/orchestration.py +0 -0
  43. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/collectors/projections.py +0 -0
  44. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/collectors/registry.py +0 -0
  45. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/collectors/sources.py +0 -0
  46. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/coverage.py +0 -0
  47. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/ecosystems/__init__.py +0 -0
  48. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/ecosystems/ansible/__init__.py +0 -0
  49. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/ecosystems/ansible/requirements.py +0 -0
  50. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/ecosystems/ansible/topology.py +0 -0
  51. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/ecosystems/contracts.py +0 -0
  52. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/ecosystems/go.py +0 -0
  53. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/ecosystems/javascript.py +0 -0
  54. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/ecosystems/php.py +0 -0
  55. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/ecosystems/python.py +0 -0
  56. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/frameworks/__init__.py +0 -0
  57. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/frameworks/contracts.py +0 -0
  58. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/frameworks/detection.py +0 -0
  59. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/frameworks/providers/__init__.py +0 -0
  60. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/frameworks/providers/frontend.py +0 -0
  61. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/frameworks/providers/go.py +0 -0
  62. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/frameworks/providers/php.py +0 -0
  63. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/frameworks/providers/python.py +0 -0
  64. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/frameworks/registry.py +0 -0
  65. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/frameworks/schema.py +0 -0
  66. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/frameworks/templates.py +0 -0
  67. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/infrastructure.py +0 -0
  68. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/invocation.py +0 -0
  69. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/model.py +0 -0
  70. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/policy/__init__.py +0 -0
  71. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/policy/contracts.py +0 -0
  72. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/policy/decisions.py +0 -0
  73. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/policy/guidance.py +0 -0
  74. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/policy/registry.py +0 -0
  75. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/policy/schema.py +0 -0
  76. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/policy/scopes.py +0 -0
  77. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/project.py +0 -0
  78. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/repository.py +0 -0
  79. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/review_context.py +0 -0
  80. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/store/__init__.py +0 -0
  81. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/store/atomic.py +0 -0
  82. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/store/contracts.py +0 -0
  83. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/store/core.py +0 -0
  84. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/store/readback.py +0 -0
  85. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/evidence/store/values.py +0 -0
  86. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/mcp_config.py +0 -0
  87. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/posting/__init__.py +0 -0
  88. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/posting/__main__.py +0 -0
  89. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/posting/comments.py +0 -0
  90. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/posting/gitlab.py +0 -0
  91. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/posting/gitlab_approval.py +0 -0
  92. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/posting/gitlab_identity.py +0 -0
  93. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/posting/markers.py +0 -0
  94. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/posting/payloads.py +0 -0
  95. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/posting/reconciliation.py +0 -0
  96. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/posting/result.py +0 -0
  97. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/posting/settings.py +0 -0
  98. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/posting/snapshot.py +0 -0
  99. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/posting/suggestions.py +0 -0
  100. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/posting/transaction.py +0 -0
  101. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/pre_execution.py +0 -0
  102. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/providers/__init__.py +0 -0
  103. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/providers/gitlab.py +0 -0
  104. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/providers/gitlab_discussions.py +0 -0
  105. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/py.typed +0 -0
  106. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.7.1}/src/ocr_toolkit/result_contract.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-code-review-toolkit
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Unofficial GitLab CI integration layer for Open Code Review
5
5
  Project-URL: Homepage, https://github.com/xeonvs/open-code-review-toolkit
6
6
  Project-URL: Repository, https://github.com/xeonvs/open-code-review-toolkit
@@ -258,13 +258,13 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
258
258
 
259
259
  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.
260
260
 
261
- After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v4's exact reviewed source SHA and merge-request author. This write is enabled by default; set `OCR_AUTO_APPROVE=false` when the bot must remain comment-only. Complete optional MR metadata and complete zero-record enrichment remain eligible, while degraded metadata, required context degradation, admitted mutable context, legacy receipts, any direct external MCP, author movement, or bot self-authorship prevents an approval write. GitLab approval rules and protected-branch policy remain authoritative. The toolkit only adds an eligible approval; it never removes an existing approval when a later review is ineligible or disabled.
261
+ After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v5's exact reviewed source SHA and merge-request author. This write is enabled by default; set `OCR_AUTO_APPROVE=false` when the bot must remain comment-only. Complete optional MR metadata and complete zero-record enrichment remain eligible, while degraded metadata, required context degradation, admitted mutable context, legacy receipts, publication filtering, any direct external MCP, author movement, or bot self-authorship prevents an approval write. GitLab approval rules and protected-branch policy remain authoritative. The toolkit only adds an eligible approval; it never removes an existing approval when a later review is ineligible or disabled.
262
262
 
263
263
  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.
264
264
 
265
265
  ## Project architecture
266
266
 
267
- The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed facts, scoped completeness, and base/head changes through the mandatory built-in read-only MCP server. Version 0.7.0 establishes protected-policy enriched reviews: they can acquire stable GitLab discussions and authorize external issue/document records through a provider-neutral stdio or HTTPS proxy before OCR. The same built-in MCP exposes only opaque committed `context_list`/`context_get` handles; it has no provider network or arbitrary identifier path. Direct external MCP remains a separate privileged, comment-only operator boundary.
267
+ The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed facts, scoped completeness, and base/head changes through the mandatory built-in read-only MCP server. Version 0.7.0 establishes protected-policy enriched reviews: they can acquire stable GitLab discussions and authorize external issue/document records through a provider-neutral stdio or HTTPS proxy before OCR. Version 0.7.1 adds closed receipt-v5 publication semantics, reconciled count-only evidence use, privacy-safe normalized token reporting, and deterministic Recommended focus ranking. The same built-in MCP exposes only opaque committed `context_list`/`context_get` handles; it has no provider network or arbitrary identifier path. Direct external MCP remains a separate privileged, comment-only operator boundary.
268
268
 
269
269
  - [Toolkit strategy](docs/engineering/toolkit_strategy.md) - durable product boundaries, architecture, invariants, and non-goals.
270
270
  - [Bounded review context](docs/review-context.md) - protected policy, adapter protocol, GitLab discussions, opaque handles, DLP, receipt, and cleanup contracts.
@@ -31,13 +31,13 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
31
31
 
32
32
  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.
33
33
 
34
- After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v4's exact reviewed source SHA and merge-request author. This write is enabled by default; set `OCR_AUTO_APPROVE=false` when the bot must remain comment-only. Complete optional MR metadata and complete zero-record enrichment remain eligible, while degraded metadata, required context degradation, admitted mutable context, legacy receipts, any direct external MCP, author movement, or bot self-authorship prevents an approval write. GitLab approval rules and protected-branch policy remain authoritative. The toolkit only adds an eligible approval; it never removes an existing approval when a later review is ineligible or disabled.
34
+ After every current review note publishes, the GitLab adapter can add a conservative approval bound to receipt v5's exact reviewed source SHA and merge-request author. This write is enabled by default; set `OCR_AUTO_APPROVE=false` when the bot must remain comment-only. Complete optional MR metadata and complete zero-record enrichment remain eligible, while degraded metadata, required context degradation, admitted mutable context, legacy receipts, publication filtering, any direct external MCP, author movement, or bot self-authorship prevents an approval write. GitLab approval rules and protected-branch policy remain authoritative. The toolkit only adds an eligible approval; it never removes an existing approval when a later review is ineligible or disabled.
35
35
 
36
36
  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.
37
37
 
38
38
  ## Project architecture
39
39
 
40
- The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed facts, scoped completeness, and base/head changes through the mandatory built-in read-only MCP server. Version 0.7.0 establishes protected-policy enriched reviews: they can acquire stable GitLab discussions and authorize external issue/document records through a provider-neutral stdio or HTTPS proxy before OCR. The same built-in MCP exposes only opaque committed `context_list`/`context_get` handles; it has no provider network or arbitrary identifier path. Direct external MCP remains a separate privileged, comment-only operator boundary.
40
+ The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed facts, scoped completeness, and base/head changes through the mandatory built-in read-only MCP server. Version 0.7.0 establishes protected-policy enriched reviews: they can acquire stable GitLab discussions and authorize external issue/document records through a provider-neutral stdio or HTTPS proxy before OCR. Version 0.7.1 adds closed receipt-v5 publication semantics, reconciled count-only evidence use, privacy-safe normalized token reporting, and deterministic Recommended focus ranking. The same built-in MCP exposes only opaque committed `context_list`/`context_get` handles; it has no provider network or arbitrary identifier path. Direct external MCP remains a separate privileged, comment-only operator boundary.
41
41
 
42
42
  - [Toolkit strategy](docs/engineering/toolkit_strategy.md) - durable product boundaries, architecture, invariants, and non-goals.
43
43
  - [Bounded review context](docs/review-context.md) - protected policy, adapter protocol, GitLab discussions, opaque handles, DLP, receipt, and cleanup contracts.
@@ -133,6 +133,11 @@ directory = "bugfix"
133
133
  name = "🐛 Bug Fixes"
134
134
  showcontent = true
135
135
 
136
+ [[tool.towncrier.type]]
137
+ directory = "maintenance"
138
+ name = "🛠 Maintenance"
139
+ showcontent = true
140
+
136
141
  [[tool.towncrier.type]]
137
142
  directory = "refactor"
138
143
  name = "🔧 Refactoring"
@@ -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.7.0'
22
- __version_tuple__ = version_tuple = (0, 7, 0)
21
+ __version__ = version = '0.7.1'
22
+ __version_tuple__ = version_tuple = (0, 7, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -15,8 +15,10 @@ def main() -> int:
15
15
  parser.add_argument("--context-run-id", default="")
16
16
  parser.add_argument("--context-policy-digest", default="")
17
17
  arguments = parser.parse_args()
18
+ artifacts = repository_artifacts()
18
19
  return serve(
19
- repository_artifacts().store,
20
+ artifacts.store,
21
+ action_receipt_path=artifacts.action_receipt,
20
22
  context_path=arguments.context_store,
21
23
  context_run_id=arguments.context_run_id,
22
24
  context_policy_digest=arguments.context_policy_digest,
@@ -0,0 +1,124 @@
1
+ """Persist and validate count-only built-in evidence action attribution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import stat
8
+ from collections.abc import Iterator
9
+ from contextlib import contextmanager
10
+ from fcntl import LOCK_EX, flock
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from ocr_toolkit.evidence.store.atomic import atomic_write
15
+
16
+ ACTION_RECEIPT_SCHEMA = "ocr.evidence-action-receipt/v1"
17
+ EVIDENCE_ACTIONS = ("summary", "list", "get")
18
+ MAX_ACTION_CALLS = 1_000_000_000
19
+ MAX_ACTION_RECEIPT_BYTES = 4_096
20
+
21
+
22
+ def _locked_receipt_path(path: Path) -> Path:
23
+ """Return one sibling lock path that never enters the public receipt."""
24
+
25
+ return path.with_name(f".{path.name}.lock")
26
+
27
+
28
+ @contextmanager
29
+ def _serialized_receipt_update(path: Path) -> Iterator[None]:
30
+ """Serialize sibling receipt updates through one private regular lock file."""
31
+
32
+ lock_path = _locked_receipt_path(path)
33
+ flags = os.O_CREAT | os.O_RDWR
34
+ if hasattr(os, "O_NOFOLLOW"):
35
+ flags |= os.O_NOFOLLOW
36
+ descriptor = os.open(lock_path, flags, 0o600)
37
+ try:
38
+ metadata = os.fstat(descriptor)
39
+ if not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1:
40
+ raise OSError("evidence action receipt lock is unsafe")
41
+ os.fchmod(descriptor, 0o600)
42
+ flock(descriptor, LOCK_EX)
43
+ yield
44
+ finally:
45
+ os.close(descriptor)
46
+
47
+
48
+ def _validated_counts(value: Any) -> dict[str, int] | None:
49
+ if not isinstance(value, dict) or set(value) != {"schema_version", "actions"}:
50
+ return None
51
+ actions = value.get("actions")
52
+ if value.get("schema_version") != ACTION_RECEIPT_SCHEMA or not isinstance(actions, dict):
53
+ return None
54
+ if set(actions) != set(EVIDENCE_ACTIONS):
55
+ return None
56
+ if any(
57
+ not isinstance(count, int) or isinstance(count, bool) or not 0 <= count <= MAX_ACTION_CALLS
58
+ for count in actions.values()
59
+ ):
60
+ return None
61
+ return {action: actions[action] for action in EVIDENCE_ACTIONS}
62
+
63
+
64
+ def read_action_receipt(path: Path) -> dict[str, int] | None:
65
+ """Read one bounded receipt or return unavailable for any unsafe shape."""
66
+
67
+ flags = os.O_RDONLY
68
+ if hasattr(os, "O_NOFOLLOW"):
69
+ flags |= os.O_NOFOLLOW
70
+ if hasattr(os, "O_NONBLOCK"):
71
+ flags |= os.O_NONBLOCK
72
+ descriptor = -1
73
+ try:
74
+ descriptor = os.open(path, flags)
75
+ metadata = os.fstat(descriptor)
76
+ if (
77
+ not stat.S_ISREG(metadata.st_mode)
78
+ or metadata.st_nlink != 1
79
+ or stat.S_IMODE(metadata.st_mode) & 0o077
80
+ ):
81
+ return None
82
+ raw = os.read(descriptor, MAX_ACTION_RECEIPT_BYTES + 1)
83
+ except OSError:
84
+ return None
85
+ finally:
86
+ if descriptor >= 0:
87
+ os.close(descriptor)
88
+ if len(raw) > MAX_ACTION_RECEIPT_BYTES:
89
+ return None
90
+ try:
91
+ value = json.loads(raw)
92
+ except (UnicodeDecodeError, json.JSONDecodeError, RecursionError):
93
+ return None
94
+ return _validated_counts(value)
95
+
96
+
97
+ def record_action(path: Path, action: object) -> None:
98
+ """Atomically increment one completed action without retaining arguments."""
99
+
100
+ if action not in EVIDENCE_ACTIONS:
101
+ raise ValueError("evidence action is outside the closed receipt enum")
102
+ with _serialized_receipt_update(path):
103
+ counts = read_action_receipt(path)
104
+ try:
105
+ path.lstat()
106
+ except FileNotFoundError:
107
+ exists = False
108
+ else:
109
+ exists = True
110
+ if exists and counts is None:
111
+ raise ValueError("evidence action receipt is malformed")
112
+ if counts is None:
113
+ counts = dict.fromkeys(EVIDENCE_ACTIONS, 0)
114
+ if counts[action] >= MAX_ACTION_CALLS:
115
+ raise ValueError("evidence action receipt exceeds the count bound")
116
+ counts[action] += 1
117
+ payload = {
118
+ "schema_version": ACTION_RECEIPT_SCHEMA,
119
+ "actions": {name: counts[name] for name in EVIDENCE_ACTIONS},
120
+ }
121
+ atomic_write(
122
+ path,
123
+ lambda: json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n",
124
+ )
@@ -18,6 +18,8 @@ class EvidenceArtifacts:
18
18
  policy_rules: Path
19
19
  context_store: Path
20
20
  pre_execution_status: Path
21
+ action_receipt: Path
22
+ action_receipt_lock: Path
21
23
 
22
24
 
23
25
  def repository_artifacts(root: Path | None = None) -> EvidenceArtifacts:
@@ -32,6 +34,8 @@ def repository_artifacts(root: Path | None = None) -> EvidenceArtifacts:
32
34
  policy_rules=directory / "policy-rules.json",
33
35
  context_store=directory / "context.json",
34
36
  pre_execution_status=directory / "pre-execution-status.json",
37
+ action_receipt=directory / "evidence-actions.json",
38
+ action_receipt_lock=directory / ".evidence-actions.json.lock",
35
39
  )
36
40
 
37
41
 
@@ -21,6 +21,7 @@ from ocr_toolkit.context.mcp import (
21
21
  tool_definitions,
22
22
  )
23
23
  from ocr_toolkit.context.store import ContextStore
24
+ from ocr_toolkit.evidence.actions import record_action
24
25
  from ocr_toolkit.evidence.model import CoverageRecord, EvidenceDelta, EvidenceRecord
25
26
  from ocr_toolkit.evidence.policy.schema import is_legacy_policy_value
26
27
  from ocr_toolkit.evidence.store import EvidenceStore, EvidenceStoreError
@@ -399,7 +400,11 @@ def _error(request_id: object, code: int, message: str) -> dict[str, object]:
399
400
 
400
401
 
401
402
  def handle_request(
402
- store: EvidenceStore, raw: object, context_store: ContextStore | None = None
403
+ store: EvidenceStore,
404
+ raw: object,
405
+ context_store: ContextStore | None = None,
406
+ *,
407
+ action_receipt_path: Path | None = None,
403
408
  ) -> dict[str, object] | None:
404
409
  """Handle one validated JSON-RPC request or notification."""
405
410
 
@@ -446,6 +451,13 @@ def handle_request(
446
451
  request_id,
447
452
  {"content": [{"type": "text", "text": str(exc)}], "isError": True},
448
453
  )
454
+ if action_receipt_path is not None:
455
+ arguments = params.get("arguments", {})
456
+ action = arguments.get("action") if isinstance(arguments, dict) else None
457
+ try:
458
+ record_action(action_receipt_path, action)
459
+ except (OSError, ValueError):
460
+ pass
449
461
  return _success(request_id, result)
450
462
  if context_store is not None and isinstance(name, str) and name in {LIST_TOOL, GET_TOOL}:
451
463
  try:
@@ -482,6 +494,7 @@ def serve(
482
494
  stdin: TextIO = sys.stdin,
483
495
  stdout: TextIO = sys.stdout,
484
496
  *,
497
+ action_receipt_path: Path | None = None,
485
498
  context_path: Path | None = None,
486
499
  context_run_id: str = "",
487
500
  context_policy_digest: str = "",
@@ -516,7 +529,12 @@ def serve(
516
529
  response = _error(None, -32700, "Parse error")
517
530
  else:
518
531
  has_request_id = isinstance(request, dict) and "id" in request
519
- response = handle_request(store, request, context_store)
532
+ response = handle_request(
533
+ store,
534
+ request,
535
+ context_store,
536
+ action_receipt_path=action_receipt_path,
537
+ )
520
538
  if response is None or not has_request_id:
521
539
  continue
522
540
  serialized = json.dumps(response, separators=(",", ":"), ensure_ascii=False)
@@ -18,8 +18,8 @@ from ocr_toolkit.common.redaction import sanitize_ocr_value
18
18
  DEFAULT_MAX_RESULT_BYTES = 2_000_000
19
19
  MAX_RESULT_BYTES_HARD_LIMIT = 20_000_000
20
20
  TOOLKIT_RESULT_KEY = "_ocr_toolkit"
21
- TOOLKIT_RESULT_SCHEMA_VERSION = 4
22
- SUPPORTED_TOOLKIT_RESULT_SCHEMA_VERSIONS = frozenset({1, 2, 3, TOOLKIT_RESULT_SCHEMA_VERSION})
21
+ TOOLKIT_RESULT_SCHEMA_VERSION = 5
22
+ SUPPORTED_TOOLKIT_RESULT_SCHEMA_VERSIONS = frozenset({TOOLKIT_RESULT_SCHEMA_VERSION})
23
23
  # The receipt can name the 16 configured external servers plus the mandatory built-in.
24
24
  MAX_TOOLKIT_MCP_USAGE_SERVERS = 17
25
25
  MAX_TOOLKIT_MCP_TOOLS_PER_SERVER = 128
@@ -159,34 +159,42 @@ def _sha256(value: Any) -> bool:
159
159
 
160
160
 
161
161
  def publication_dlp_state(value: Any) -> str | None:
162
- """Validate the exact v4 passed or filtered publication receipt."""
162
+ """Validate the exact v5 publication-policy receipt."""
163
163
 
164
- if value == {"dlp": "passed"}:
164
+ if value == {"state": "passed"}:
165
165
  return "passed"
166
+ if not isinstance(value, dict):
167
+ return None
168
+ if value.get("state") == "private-sanitized":
169
+ if set(value) != {"state", "reason_counts", "sanitized_fields"}:
170
+ return None
171
+ reason_counts = value.get("reason_counts")
172
+ sanitized_fields = value.get("sanitized_fields")
173
+ if (
174
+ not _valid_dlp_reason_counts(reason_counts)
175
+ or not any(reason_counts.values())
176
+ or not isinstance(sanitized_fields, int)
177
+ or isinstance(sanitized_fields, bool)
178
+ or not 0 < sanitized_fields <= MAX_TOOLKIT_MCP_USAGE_COUNT
179
+ ):
180
+ return None
181
+ return "private-sanitized"
166
182
  if not isinstance(value, dict) or set(value) != {
167
- "dlp",
183
+ "state",
168
184
  "reason_counts",
169
185
  "retained",
170
186
  "omitted",
171
187
  "original",
172
188
  }:
173
189
  return None
174
- if value.get("dlp") != "filtered":
190
+ if value.get("state") != "publication-filtered":
175
191
  return None
176
192
  reason_counts = value.get("reason_counts")
177
193
  retained = value.get("retained")
178
194
  omitted = value.get("omitted")
179
195
  original = value.get("original")
180
196
  if (
181
- not isinstance(reason_counts, dict)
182
- or set(reason_counts)
183
- != {"forbidden", "invalid_text", "laundering", "limit", "pii", "secret"}
184
- or any(
185
- not isinstance(count, int)
186
- or isinstance(count, bool)
187
- or not 0 <= count <= MAX_TOOLKIT_MCP_USAGE_COUNT
188
- for count in reason_counts.values()
189
- )
197
+ not _valid_dlp_reason_counts(reason_counts)
190
198
  or not any(reason_counts.values())
191
199
  or not isinstance(retained, dict)
192
200
  or set(retained) != {"comments", "warnings"}
@@ -210,7 +218,20 @@ def publication_dlp_state(value: Any) -> str | None:
210
218
  )
211
219
  ):
212
220
  return None
213
- return "filtered"
221
+ return "publication-filtered"
222
+
223
+
224
+ def _valid_dlp_reason_counts(value: Any) -> bool:
225
+ return bool(
226
+ isinstance(value, dict)
227
+ and set(value) == {"forbidden", "invalid_text", "laundering", "limit", "pii", "secret"}
228
+ and all(
229
+ isinstance(count, int)
230
+ and not isinstance(count, bool)
231
+ and 0 <= count <= MAX_TOOLKIT_MCP_USAGE_COUNT
232
+ for count in value.values()
233
+ )
234
+ )
214
235
 
215
236
 
216
237
  def automatic_approval_metadata_reason(toolkit_metadata: Any) -> str:
@@ -219,10 +240,7 @@ def automatic_approval_metadata_reason(toolkit_metadata: Any) -> str:
219
240
  invalid = "the review-time approval receipt is missing or invalid"
220
241
  if not isinstance(toolkit_metadata, dict):
221
242
  return invalid
222
- schema_version = toolkit_metadata.get("schema_version")
223
- if schema_version in {1, 2, 3}:
224
- return "the review-time approval receipt predates current eligibility controls"
225
- if schema_version != 4 or set(toolkit_metadata) != {
243
+ if toolkit_metadata.get("schema_version") != 5 or set(toolkit_metadata) != {
226
244
  "schema_version",
227
245
  "review",
228
246
  "context",
@@ -375,11 +393,17 @@ def automatic_approval_metadata_reason(toolkit_metadata: Any) -> str:
375
393
  return invalid
376
394
 
377
395
  evidence = toolkit_metadata.get("evidence")
378
- if not isinstance(evidence, dict) or set(evidence) != {"mandatory", "used", "calls"}:
396
+ if not isinstance(evidence, dict) or set(evidence) != {
397
+ "mandatory",
398
+ "used",
399
+ "calls",
400
+ "actions",
401
+ }:
379
402
  return invalid
380
403
  mandatory = evidence.get("mandatory")
381
404
  used = evidence.get("used")
382
405
  evidence_calls = evidence.get("calls")
406
+ evidence_actions = evidence.get("actions")
383
407
  evidence_called = isinstance(evidence_calls, int) and evidence_calls > 0
384
408
  if (
385
409
  not isinstance(mandatory, bool)
@@ -390,6 +414,7 @@ def automatic_approval_metadata_reason(toolkit_metadata: Any) -> str:
390
414
  or used is not evidence_called
391
415
  or (mandatory and not used)
392
416
  or usage.get(builtin_server, 0) != evidence_calls + sum(context_usage.values())
417
+ or not _valid_evidence_actions(evidence_actions, evidence_calls)
393
418
  ):
394
419
  return invalid
395
420
  publication = toolkit_metadata.get("publication")
@@ -397,7 +422,7 @@ def automatic_approval_metadata_reason(toolkit_metadata: Any) -> str:
397
422
  publication_state = publication_dlp_state(publication)
398
423
  if publication_state is None or cleanup != {"result": "passed"}:
399
424
  return invalid
400
- if publication_state == "filtered":
425
+ if publication_state == "publication-filtered":
401
426
  return "publication DLP filtered the complete review result"
402
427
  if context.get("state") == "degraded" or required_degraded:
403
428
  return "the selected review context was degraded"
@@ -406,3 +431,23 @@ def automatic_approval_metadata_reason(toolkit_metadata: Any) -> str:
406
431
  if external:
407
432
  return "external MCP was configured for a comment-only review"
408
433
  return ""
434
+
435
+
436
+ def _valid_evidence_actions(value: Any, evidence_calls: Any) -> bool:
437
+ """Validate verified counts or an explicit unavailable attribution state."""
438
+
439
+ if value == {"state": "unavailable"}:
440
+ return True
441
+ if not isinstance(value, dict) or set(value) != {"state", "summary", "list", "get"}:
442
+ return False
443
+ counts = [value.get(action) for action in ("summary", "list", "get")]
444
+ return bool(
445
+ value.get("state") == "verified"
446
+ and all(
447
+ isinstance(count, int)
448
+ and not isinstance(count, bool)
449
+ and 0 <= count <= MAX_TOOLKIT_MCP_USAGE_COUNT
450
+ for count in counts
451
+ )
452
+ and sum(counts) == evidence_calls
453
+ )