open-code-review-toolkit 0.7.0__tar.gz → 0.8.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 (109) hide show
  1. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/PKG-INFO +6 -5
  2. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/README.md +5 -4
  3. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/pyproject.toml +6 -1
  4. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/_version.py +2 -2
  5. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/cli.py +11 -1
  6. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/configure.py +7 -13
  7. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/context/adapters.py +7 -3
  8. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/context/broker.py +235 -11
  9. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/context/contracts.py +25 -3
  10. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/context/dlp.py +40 -32
  11. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/context/mcp.py +8 -2
  12. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/context/policy.py +80 -8
  13. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/context/store.py +139 -5
  14. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/__main__.py +3 -1
  15. open_code_review_toolkit-0.8.0/src/ocr_toolkit/evidence/actions.py +124 -0
  16. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/artifacts.py +6 -0
  17. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/mcp.py +20 -2
  18. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/project.py +8 -0
  19. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/review_context.py +21 -2
  20. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/mcp_config.py +1 -1
  21. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/ocr_result.py +2 -2
  22. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/posting/approval.py +66 -21
  23. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/posting/formatting.py +136 -144
  24. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/posting/gitlab.py +21 -16
  25. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/posting/markers.py +18 -0
  26. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/posting/snapshot.py +4 -4
  27. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/posting/transaction.py +1 -1
  28. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/posting/workflow.py +32 -8
  29. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/pre_execution.py +40 -9
  30. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/preflight.py +1 -1
  31. open_code_review_toolkit-0.8.0/src/ocr_toolkit/providers/gitlab_context.py +83 -0
  32. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/providers/gitlab_discussions.py +192 -101
  33. open_code_review_toolkit-0.8.0/src/ocr_toolkit/providers/gitlab_identity.py +54 -0
  34. open_code_review_toolkit-0.8.0/src/ocr_toolkit/providers/gitlab_remediation.py +331 -0
  35. open_code_review_toolkit-0.8.0/src/ocr_toolkit/result_usage.py +216 -0
  36. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/review_runner.py +740 -84
  37. open_code_review_toolkit-0.7.0/src/ocr_toolkit/posting/gitlab_identity.py +0 -14
  38. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/.gitignore +0 -0
  39. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/LICENSE +0 -0
  40. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/__init__.py +0 -0
  41. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/common/__init__.py +0 -0
  42. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/common/filesystem.py +0 -0
  43. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/common/git.py +0 -0
  44. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/common/language.py +0 -0
  45. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/common/markdown.py +0 -0
  46. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/common/redaction.py +0 -0
  47. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/config_writer.py +0 -0
  48. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/context/__init__.py +0 -0
  49. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/context/recognizers.py +0 -0
  50. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/__init__.py +0 -0
  51. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/categorize.py +0 -0
  52. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/collect.py +0 -0
  53. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/collectors/__init__.py +0 -0
  54. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/collectors/graphs.py +0 -0
  55. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/collectors/orchestration.py +0 -0
  56. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/collectors/projections.py +0 -0
  57. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/collectors/registry.py +0 -0
  58. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/collectors/sources.py +0 -0
  59. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/coverage.py +0 -0
  60. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/ecosystems/__init__.py +0 -0
  61. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/ecosystems/ansible/__init__.py +0 -0
  62. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/ecosystems/ansible/requirements.py +0 -0
  63. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/ecosystems/ansible/topology.py +0 -0
  64. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/ecosystems/contracts.py +0 -0
  65. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/ecosystems/go.py +0 -0
  66. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/ecosystems/javascript.py +0 -0
  67. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/ecosystems/php.py +0 -0
  68. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/ecosystems/python.py +0 -0
  69. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/frameworks/__init__.py +0 -0
  70. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/frameworks/contracts.py +0 -0
  71. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/frameworks/detection.py +0 -0
  72. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/frameworks/providers/__init__.py +0 -0
  73. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/frameworks/providers/frontend.py +0 -0
  74. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/frameworks/providers/go.py +0 -0
  75. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/frameworks/providers/php.py +0 -0
  76. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/frameworks/providers/python.py +0 -0
  77. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/frameworks/registry.py +0 -0
  78. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/frameworks/schema.py +0 -0
  79. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/frameworks/templates.py +0 -0
  80. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/infrastructure.py +0 -0
  81. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/invocation.py +0 -0
  82. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/model.py +0 -0
  83. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/policy/__init__.py +0 -0
  84. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/policy/contracts.py +0 -0
  85. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/policy/decisions.py +0 -0
  86. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/policy/guidance.py +0 -0
  87. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/policy/registry.py +0 -0
  88. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/policy/schema.py +0 -0
  89. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/policy/scopes.py +0 -0
  90. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/repository.py +0 -0
  91. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/store/__init__.py +0 -0
  92. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/store/atomic.py +0 -0
  93. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/store/contracts.py +0 -0
  94. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/store/core.py +0 -0
  95. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/store/readback.py +0 -0
  96. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/evidence/store/values.py +0 -0
  97. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/posting/__init__.py +0 -0
  98. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/posting/__main__.py +0 -0
  99. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/posting/comments.py +0 -0
  100. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/posting/gitlab_approval.py +0 -0
  101. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/posting/payloads.py +0 -0
  102. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/posting/reconciliation.py +0 -0
  103. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/posting/result.py +0 -0
  104. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/posting/settings.py +0 -0
  105. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/posting/suggestions.py +0 -0
  106. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/providers/__init__.py +0 -0
  107. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/providers/gitlab.py +0 -0
  108. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/src/ocr_toolkit/py.typed +0 -0
  109. {open_code_review_toolkit-0.7.0 → open_code_review_toolkit-0.8.0}/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.8.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
@@ -254,17 +254,17 @@ Stable distributions are published to [PyPI](https://pypi.org/project/open-code-
254
254
 
255
255
  ## How reviews evolve
256
256
 
257
- On a successful rerun, the toolkit replaces untouched OCR-only notes instead of accumulating stale reviews. A human reply transfers that discussion to the team: the conversation is preserved and a matching finding is suppressed. Reply with `/ocr suppress` to keep a discussion open without future repeats, or `/ocr resolve` to suppress it and resolve the discussion after the next successful posting transaction.
257
+ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of accumulating stale reviews. A human reply transfers that discussion to the team: the conversation is preserved and a matching finding is suppressed. Reply with `/ocr suppress` or `@<live-bot-username> suppress` to keep a discussion open without future repeats; use the corresponding `resolve` command to resolve it after the next successful posting transaction. For example, a bot named `mr.bot` accepts the exact reply `@mr.bot resolve`.
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. DLP-clean metadata, generic discussions, and adapter records do not independently block approval, while degraded metadata, DLP rejection, required context degradation, admitted remediation history, legacy receipts, publication filtering, any direct external MCP, author movement, or bot self-authorship prevents an approval write. GitLab approval rules and protected-branch policy remain authoritative. The toolkit only adds an eligible approval; it never removes an existing approval when a later review is ineligible or disabled.
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. Protected-policy enriched reviews can acquire stable GitLab discussions, verified remediation history, and authorized external issue/document records before OCR. Forge-specific acquisition and posting stay at provider edges; the broker, DLP, store, MCP, receipts, and tests use common contracts so a future GitHub adapter can reuse them without inheriting GitLab API semantics. The same built-in MCP exposes only opaque committed `context_list`/`context_get` handles; it has no provider network or arbitrary identifier path. Direct external MCP remains a separate privileged, comment-only operator boundary.
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.
@@ -285,7 +285,7 @@ ocr-ci review --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log -- ...
285
285
  ocr-ci post --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log
286
286
  ```
287
287
 
288
- See the [`examples/gitlab/ocr-review.gitlab-ci.yml`](examples/gitlab/ocr-review.gitlab-ci.yml) example, the [GitLab setup guide](docs/gitlab.md), and [GitLab review operations](docs/operations.md).
288
+ 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).
289
289
 
290
290
  ## Configuration and safety
291
291
 
@@ -295,6 +295,7 @@ Repository content, OCR output, and provider responses are untrusted inputs. The
295
295
 
296
296
  ## Development and release
297
297
 
298
+ - [Documentation index](docs/README.md)
298
299
  - [Contributing](CONTRIBUTING.md)
299
300
  - [Code of Conduct](CODE_OF_CONDUCT.md)
300
301
  - [Development guide](docs/development.md)
@@ -27,17 +27,17 @@ Stable distributions are published to [PyPI](https://pypi.org/project/open-code-
27
27
 
28
28
  ## How reviews evolve
29
29
 
30
- On a successful rerun, the toolkit replaces untouched OCR-only notes instead of accumulating stale reviews. A human reply transfers that discussion to the team: the conversation is preserved and a matching finding is suppressed. Reply with `/ocr suppress` to keep a discussion open without future repeats, or `/ocr resolve` to suppress it and resolve the discussion after the next successful posting transaction.
30
+ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of accumulating stale reviews. A human reply transfers that discussion to the team: the conversation is preserved and a matching finding is suppressed. Reply with `/ocr suppress` or `@<live-bot-username> suppress` to keep a discussion open without future repeats; use the corresponding `resolve` command to resolve it after the next successful posting transaction. For example, a bot named `mr.bot` accepts the exact reply `@mr.bot resolve`.
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. DLP-clean metadata, generic discussions, and adapter records do not independently block approval, while degraded metadata, DLP rejection, required context degradation, admitted remediation history, legacy receipts, publication filtering, any direct external MCP, author movement, or bot self-authorship prevents an approval write. GitLab approval rules and protected-branch policy remain authoritative. The toolkit only adds an eligible approval; it never removes an existing approval when a later review is ineligible or disabled.
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. Protected-policy enriched reviews can acquire stable GitLab discussions, verified remediation history, and authorized external issue/document records before OCR. Forge-specific acquisition and posting stay at provider edges; the broker, DLP, store, MCP, receipts, and tests use common contracts so a future GitHub adapter can reuse them without inheriting GitLab API semantics. The same built-in MCP exposes only opaque committed `context_list`/`context_get` handles; it has no provider network or arbitrary identifier path. Direct external MCP remains a separate privileged, comment-only operator boundary.
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.
@@ -58,7 +58,7 @@ ocr-ci review --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log -- ...
58
58
  ocr-ci post --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log
59
59
  ```
60
60
 
61
- See the [`examples/gitlab/ocr-review.gitlab-ci.yml`](examples/gitlab/ocr-review.gitlab-ci.yml) example, the [GitLab setup guide](docs/gitlab.md), and [GitLab review operations](docs/operations.md).
61
+ 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).
62
62
 
63
63
  ## Configuration and safety
64
64
 
@@ -68,6 +68,7 @@ Repository content, OCR output, and provider responses are untrusted inputs. The
68
68
 
69
69
  ## Development and release
70
70
 
71
+ - [Documentation index](docs/README.md)
71
72
  - [Contributing](CONTRIBUTING.md)
72
73
  - [Code of Conduct](CODE_OF_CONDUCT.md)
73
74
  - [Development guide](docs/development.md)
@@ -106,7 +106,7 @@ source = ["ocr_toolkit"]
106
106
  omit = ["src/ocr_toolkit/_version.py"]
107
107
 
108
108
  [tool.coverage.report]
109
- fail_under = 70
109
+ fail_under = 85
110
110
  show_missing = true
111
111
  skip_covered = true
112
112
  exclude_also = [
@@ -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.8.0'
22
+ __version_tuple__ = version_tuple = (0, 8, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -32,6 +32,11 @@ def build_parser() -> argparse.ArgumentParser:
32
32
  )
33
33
  review_parser.add_argument("--result", required=True, help="OCR JSON output path.")
34
34
  review_parser.add_argument("--stderr", required=True, help="Full OCR stderr artifact path.")
35
+ review_parser.add_argument(
36
+ "--preserve-private-artifacts",
37
+ action="store_true",
38
+ help="Retain sensitive OCR session artifacts after a local diagnostic review.",
39
+ )
35
40
  review_parser.add_argument(
36
41
  "ocr_args", nargs=argparse.REMAINDER, help="OCR review arguments after --."
37
42
  )
@@ -65,7 +70,12 @@ def main(argv: Sequence[str] | None = None) -> int:
65
70
  if args.command == "review":
66
71
  ocr_args = args.ocr_args[1:] if args.ocr_args[:1] == ["--"] else args.ocr_args
67
72
  try:
68
- return review_runner.run_evidence_review(Path(args.result), Path(args.stderr), ocr_args)
73
+ return review_runner.run_evidence_review(
74
+ Path(args.result),
75
+ Path(args.stderr),
76
+ ocr_args,
77
+ preserve_private_artifacts=args.preserve_private_artifacts,
78
+ )
69
79
  except review_runner.ReviewRunnerError as exc:
70
80
  print(f"Cannot run Open Code Review: {exc}", file=sys.stderr)
71
81
  return 2
@@ -74,22 +74,16 @@ def _parse_extra_body(value: str) -> Any:
74
74
 
75
75
 
76
76
  def _llm_protocol() -> str:
77
- """Resolve and validate the OCR 1.7.10 LLM wire protocol."""
78
-
79
- protocol = _env("OCR_LLM_PROTOCOL")
80
- legacy_mode = _env("OCR_USE_ANTHROPIC").lower()
81
- if legacy_mode not in {"", "false", "true"}:
82
- raise OCRRuntimeConfigError("OCR_USE_ANTHROPIC must be true or false when set")
83
- if not protocol:
84
- protocol = "anthropic" if legacy_mode == "true" else "openai"
77
+ """Resolve and validate the explicit OCR LLM wire protocol."""
78
+
79
+ if "OCR_USE_ANTHROPIC" in os.environ:
80
+ raise OCRRuntimeConfigError(
81
+ "OCR_USE_ANTHROPIC was removed; set OCR_LLM_PROTOCOL=anthropic explicitly"
82
+ )
83
+ protocol = _env("OCR_LLM_PROTOCOL", "openai") or "openai"
85
84
  if protocol not in LLM_PROTOCOLS:
86
85
  allowed = ", ".join(sorted(LLM_PROTOCOLS))
87
86
  raise OCRRuntimeConfigError(f"OCR_LLM_PROTOCOL must be one of: {allowed}")
88
-
89
- if legacy_mode == "true" and protocol != "anthropic":
90
- raise OCRRuntimeConfigError("OCR_LLM_PROTOCOL conflicts with OCR_USE_ANTHROPIC")
91
- if legacy_mode == "false" and protocol == "anthropic":
92
- raise OCRRuntimeConfigError("OCR_LLM_PROTOCOL conflicts with OCR_USE_ANTHROPIC")
93
87
  return protocol
94
88
 
95
89
 
@@ -19,7 +19,11 @@ from pathlib import Path
19
19
  from typing import Any
20
20
 
21
21
  from ocr_toolkit.common.redaction import redact_sensitive
22
- from ocr_toolkit.context.contracts import REQUEST_SCHEMA, RESOURCE_CLASSES, RESPONSE_SCHEMA
22
+ from ocr_toolkit.context.contracts import (
23
+ REFERENCE_RESOURCE_CLASSES,
24
+ REQUEST_SCHEMA,
25
+ RESPONSE_SCHEMA,
26
+ )
23
27
 
24
28
  MAX_ADAPTER_CONFIG_BYTES = 64 * 1024
25
29
  MAX_ADAPTERS = 16
@@ -197,7 +201,7 @@ def parse_adapter_config(raw: str | None) -> tuple[AdapterConfig, ...]:
197
201
  resource_classes = _names(
198
202
  item.get("resource_classes"),
199
203
  label="adapter resource classes",
200
- allowed=RESOURCE_CLASSES,
204
+ allowed=REFERENCE_RESOURCE_CLASSES,
201
205
  )
202
206
  if kind == "stdio":
203
207
  if set(item).difference(common | {"command", "args", "env_from"}):
@@ -590,7 +594,7 @@ def authorize_and_resolve(
590
594
  or request.adapter != config.name
591
595
  or request.tenant not in config.tenants
592
596
  or request.resource_class not in config.resource_classes
593
- or request.resource_class not in RESOURCE_CLASSES
597
+ or request.resource_class not in REFERENCE_RESOURCE_CLASSES
594
598
  or request.requested_fields != tuple(sorted(set(request.requested_fields)))
595
599
  or not 1 <= request.max_chars <= 100_000
596
600
  or not 1 <= request.max_bytes <= 400_000
@@ -21,8 +21,10 @@ from ocr_toolkit.context.adapters import (
21
21
  from ocr_toolkit.context.contracts import (
22
22
  ACCOUNT_CLASSES,
23
23
  ContextPolicy,
24
+ ContextProjections,
24
25
  DiscussionPolicy,
25
26
  ReferencePolicy,
27
+ RemediationThreadPolicy,
26
28
  TextBudgets,
27
29
  )
28
30
  from ocr_toolkit.context.dlp import check_text, normalize_text
@@ -50,6 +52,15 @@ class BrokerResult:
50
52
  required_degraded: bool
51
53
 
52
54
 
55
+ @dataclass(frozen=True, slots=True)
56
+ class ContextOrigin:
57
+ """Name one provider composition edge without coupling the broker to its API."""
58
+
59
+ source: str
60
+ adapter: str
61
+ tenant: str
62
+
63
+
53
64
  class DiscussionView(Protocol):
54
65
  """Expose only normalized discussion fields needed for store projection."""
55
66
 
@@ -67,6 +78,31 @@ class DiscussionView(Protocol):
67
78
  digest: str
68
79
 
69
80
 
81
+ class RemediationReplyView(Protocol):
82
+ """Expose one provider-normalized reply in a verified remediation thread."""
83
+
84
+ order: int
85
+ author_class: str
86
+ author_pseudonym: str
87
+ body: str
88
+ created_at: int
89
+ updated_at: int
90
+
91
+
92
+ class RemediationThreadView(Protocol):
93
+ """Expose only the common verified remediation bundle contract."""
94
+
95
+ root_author_pseudonym: str
96
+ root_body: str
97
+ anchor_state: str
98
+ replies: Sequence[RemediationReplyView]
99
+ completeness: str
100
+ resolved_count: int
101
+ outdated_count: int
102
+ version: str
103
+ digest: str
104
+
105
+
70
106
  STATE_RE = re.compile(r"[a-z][a-z0-9_-]{0,63}\Z")
71
107
  PSEUDONYM_RE = re.compile(r"actor-[0-9a-f]{16}\Z")
72
108
  SHA256_RE = re.compile(r"[0-9a-f]{64}\Z")
@@ -79,19 +115,44 @@ def _project(
79
115
  return {field: record[field] for field in fields if field in record}
80
116
 
81
117
 
118
+ def _publish_projection(
119
+ record: Mapping[str, object],
120
+ fields: Sequence[str],
121
+ *,
122
+ budgets: TextBudgets,
123
+ forbidden: tuple[str, ...],
124
+ ) -> dict[str, object] | None:
125
+ """Project publishable fields only after publication-specific text DLP."""
126
+
127
+ projected = _project(record, fields)
128
+ text = projected.get("text")
129
+ if isinstance(text, str):
130
+ checked = check_text(
131
+ text,
132
+ budgets=budgets,
133
+ publication=True,
134
+ forbidden=forbidden,
135
+ )
136
+ if not checked.admitted or checked.text != text:
137
+ return None
138
+ return projected
139
+
140
+
82
141
  def _normalized_record(
83
142
  record: Mapping[str, object],
84
143
  *,
85
- policy: ReferencePolicy,
144
+ budgets: TextBudgets,
145
+ projections: ContextProjections,
146
+ resource_class: str,
86
147
  forbidden: tuple[str, ...],
87
148
  ) -> Mapping[str, object] | None:
88
149
  result: dict[str, object] = {}
89
- for field in policy.projections.retrieve:
150
+ for field in projections.retrieve:
90
151
  if field not in record:
91
152
  continue
92
153
  value = record[field]
93
154
  if field == "text":
94
- checked = check_text(value, budgets=policy.budgets, forbidden=forbidden)
155
+ checked = check_text(value, budgets=budgets, forbidden=forbidden)
95
156
  if not checked.admitted or checked.text is None:
96
157
  return None
97
158
  result[field] = checked.text
@@ -106,7 +167,7 @@ def _normalized_record(
106
167
  )
107
168
  if not checked.admitted:
108
169
  return None
109
- if field == "descriptor" and normalized != policy.resource_class:
170
+ if field == "descriptor" and normalized != resource_class:
110
171
  return None
111
172
  if field == "state" and STATE_RE.fullmatch(normalized) is None:
112
173
  return None
@@ -160,7 +221,9 @@ def prepare_discussion_records(
160
221
  records: Sequence[DiscussionView],
161
222
  *,
162
223
  policy: DiscussionPolicy,
224
+ origin: ContextOrigin,
163
225
  expiry: int,
226
+ forbidden: tuple[str, ...] = (),
164
227
  ) -> tuple[PendingContextRecord, ...]:
165
228
  """Project provider-normalized discussions into the common private store contract."""
166
229
 
@@ -181,18 +244,41 @@ def prepare_discussion_records(
181
244
  "version": record.version,
182
245
  "expiry": expiry,
183
246
  }
184
- retrieved = _project(value, policy.projections.retrieve)
247
+ retrieved = _normalized_record(
248
+ value,
249
+ budgets=policy.budgets,
250
+ projections=policy.projections,
251
+ resource_class="discussion",
252
+ forbidden=forbidden,
253
+ )
254
+ if (
255
+ retrieved is None
256
+ or not isinstance(record.digest, str)
257
+ or SHA256_RE.fullmatch(record.digest) is None
258
+ or normalize_text(record.version) != record.version
259
+ or not record.version
260
+ or len(record.version) > 512
261
+ ):
262
+ continue
263
+ publish = _publish_projection(
264
+ retrieved,
265
+ policy.projections.publish,
266
+ budgets=policy.budgets,
267
+ forbidden=forbidden,
268
+ )
269
+ if publish is None:
270
+ continue
185
271
  pending.append(
186
272
  PendingContextRecord(
187
- source="forge:gitlab_discussions",
188
- adapter="gitlab",
189
- tenant="project",
273
+ source=origin.source,
274
+ adapter=origin.adapter,
275
+ tenant=origin.tenant,
190
276
  canonical_object=hashlib.sha256(f"discussion:{record.digest}".encode()).hexdigest(),
191
277
  resource_class="issue",
192
278
  descriptor="discussion",
193
279
  projections={
194
280
  "model": _project(retrieved, policy.projections.model),
195
- "publish": _project(retrieved, policy.projections.publish),
281
+ "publish": publish,
196
282
  "retain": _project(retrieved, policy.projections.retain),
197
283
  },
198
284
  version=record.version,
@@ -204,6 +290,127 @@ def prepare_discussion_records(
204
290
  return tuple(pending)
205
291
 
206
292
 
293
+ def prepare_remediation_records(
294
+ records: Sequence[RemediationThreadView],
295
+ *,
296
+ policy: RemediationThreadPolicy,
297
+ origin: ContextOrigin,
298
+ expiry: int,
299
+ forbidden: tuple[str, ...] = (),
300
+ ) -> tuple[PendingContextRecord, ...]:
301
+ """DLP-check normalized remediation views and build the fixed private projection."""
302
+
303
+ pending: list[PendingContextRecord] = []
304
+ for record in records:
305
+ root = check_text(record.root_body, budgets=policy.budgets, forbidden=forbidden)
306
+ if (
307
+ not root.admitted
308
+ or root.text != record.root_body
309
+ or not isinstance(record.root_author_pseudonym, str)
310
+ or PSEUDONYM_RE.fullmatch(record.root_author_pseudonym) is None
311
+ or record.anchor_state not in {"current", "outdated", "unpositioned"}
312
+ or record.completeness not in {"complete", "partial"}
313
+ or not isinstance(record.digest, str)
314
+ or SHA256_RE.fullmatch(record.digest) is None
315
+ or normalize_text(record.version) != record.version
316
+ or not record.version
317
+ or len(record.version) > 512
318
+ or not isinstance(record.resolved_count, int)
319
+ or isinstance(record.resolved_count, bool)
320
+ or not isinstance(record.outdated_count, int)
321
+ or isinstance(record.outdated_count, bool)
322
+ or record.resolved_count < 0
323
+ or record.outdated_count < 0
324
+ ):
325
+ continue
326
+ replies: list[dict[str, object]] = []
327
+ valid = True
328
+ for expected_order, reply in enumerate(record.replies):
329
+ checked = check_text(reply.body, budgets=policy.budgets, forbidden=forbidden)
330
+ if (
331
+ reply.order != expected_order
332
+ or reply.author_class not in policy.account_classes
333
+ or reply.author_class == "toolkit_bot"
334
+ or not isinstance(reply.author_pseudonym, str)
335
+ or PSEUDONYM_RE.fullmatch(reply.author_pseudonym) is None
336
+ or not checked.admitted
337
+ or checked.text != reply.body
338
+ or not isinstance(reply.created_at, int)
339
+ or isinstance(reply.created_at, bool)
340
+ or not isinstance(reply.updated_at, int)
341
+ or isinstance(reply.updated_at, bool)
342
+ or reply.created_at < 0
343
+ or reply.updated_at < reply.created_at
344
+ or reply.updated_at > expiry
345
+ ):
346
+ valid = False
347
+ break
348
+ replies.append(
349
+ {
350
+ "order": reply.order,
351
+ "author_class": reply.author_class,
352
+ "author_pseudonym": reply.author_pseudonym,
353
+ "text": checked.text,
354
+ "created_at": reply.created_at,
355
+ "updated_at": reply.updated_at,
356
+ }
357
+ )
358
+ if (
359
+ not valid
360
+ or not replies
361
+ or len(replies) > policy.max_replies_per_thread
362
+ or record.resolved_count > len(replies) + 1
363
+ or record.outdated_count > len(replies) + 1
364
+ or (record.anchor_state == "outdated" and record.outdated_count < 1)
365
+ ):
366
+ continue
367
+ remediation = {
368
+ "root": {
369
+ "text": root.text,
370
+ "author_pseudonym": record.root_author_pseudonym,
371
+ },
372
+ "anchor_state": record.anchor_state,
373
+ "replies": replies,
374
+ "completeness": record.completeness,
375
+ "counts": {
376
+ "replies": len(replies),
377
+ "resolved": record.resolved_count,
378
+ "outdated": record.outdated_count,
379
+ },
380
+ }
381
+ pending.append(
382
+ PendingContextRecord(
383
+ source=origin.source,
384
+ adapter=origin.adapter,
385
+ tenant=origin.tenant,
386
+ canonical_object=hashlib.sha256(
387
+ f"remediation:{record.digest}".encode()
388
+ ).hexdigest(),
389
+ resource_class="remediation_thread",
390
+ descriptor="remediation_thread",
391
+ projections={
392
+ "model": {
393
+ "descriptor": "remediation_thread",
394
+ "remediation_thread": remediation,
395
+ },
396
+ "publish": {"descriptor": "remediation_thread"},
397
+ "retain": {
398
+ "state": record.completeness,
399
+ "count": len(replies),
400
+ "digest": record.digest,
401
+ "version": record.version,
402
+ "expiry": expiry,
403
+ },
404
+ },
405
+ version=record.version,
406
+ digest=record.digest,
407
+ mutable=True,
408
+ expiry=expiry,
409
+ )
410
+ )
411
+ return tuple(pending)
412
+
413
+
207
414
  def acquire_external_records(
208
415
  *,
209
416
  policy: ContextPolicy,
@@ -316,7 +523,13 @@ def acquire_external_records(
316
523
  degradation_counts["invalid"] += 1
317
524
  required_degraded = required_degraded or reference.required
318
525
  continue
319
- normalized = _normalized_record(response.record, policy=reference, forbidden=forbidden)
526
+ normalized = _normalized_record(
527
+ response.record,
528
+ budgets=reference.budgets,
529
+ projections=reference.projections,
530
+ resource_class=reference.resource_class,
531
+ forbidden=forbidden,
532
+ )
320
533
  if normalized is None:
321
534
  completeness[source] = "unavailable"
322
535
  degradation_counts["invalid"] += 1
@@ -377,6 +590,17 @@ def acquire_external_records(
377
590
  total_chars += chars
378
591
  total_bytes += byte_count
379
592
  total_lines += lines
593
+ publish = _publish_projection(
594
+ normalized,
595
+ reference.projections.publish,
596
+ budgets=reference.budgets,
597
+ forbidden=forbidden,
598
+ )
599
+ if publish is None:
600
+ completeness[source] = "unavailable"
601
+ degradation_counts["invalid"] += 1
602
+ required_degraded = required_degraded or reference.required
603
+ continue
380
604
  admitted = PendingContextRecord(
381
605
  source=source,
382
606
  adapter=reference.adapter,
@@ -386,7 +610,7 @@ def acquire_external_records(
386
610
  descriptor=str(normalized.get("descriptor", reference.resource_class)),
387
611
  projections={
388
612
  "model": _project(normalized, reference.projections.model),
389
- "publish": _project(normalized, reference.projections.publish),
613
+ "publish": publish,
390
614
  "retain": _project(normalized, reference.projections.retain),
391
615
  },
392
616
  version=response.version,
@@ -4,12 +4,16 @@ from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass
6
6
 
7
- POLICY_SCHEMA = "ocr.review-context-policy/v1"
8
- STORE_SCHEMA = "ocr.context-store/v1"
7
+ POLICY_SCHEMA_V1 = "ocr.review-context-policy/v1"
8
+ POLICY_SCHEMA_V2 = "ocr.review-context-policy/v2"
9
+ POLICY_SCHEMAS = frozenset({POLICY_SCHEMA_V1, POLICY_SCHEMA_V2})
10
+ POLICY_SCHEMA = POLICY_SCHEMA_V2
11
+ STORE_SCHEMA = "ocr.context-store/v2"
9
12
  REQUEST_SCHEMA = "ocr.context-adapter-request/v1"
10
13
  RESPONSE_SCHEMA = "ocr.context-adapter-response/v1"
11
14
  ACCOUNT_CLASSES = frozenset({"user", "automation", "system", "toolkit_bot"})
12
- RESOURCE_CLASSES = frozenset({"issue", "document"})
15
+ REFERENCE_RESOURCE_CLASSES = frozenset({"issue", "document"})
16
+ STORE_RESOURCE_CLASSES = frozenset({*REFERENCE_RESOURCE_CLASSES, "remediation_thread"})
13
17
  PROJECTION_FIELDS = frozenset(
14
18
  {
15
19
  "descriptor",
@@ -28,6 +32,8 @@ PROJECTION_FIELDS = frozenset(
28
32
  "expiry",
29
33
  }
30
34
  )
35
+ REMEDIATION_MODEL_FIELD = "remediation_thread"
36
+ STORE_PROJECTION_FIELDS = frozenset({*PROJECTION_FIELDS, REMEDIATION_MODEL_FIELD})
31
37
  RETENTION_FIELDS = frozenset({"state", "count", "digest", "version", "expiry"})
32
38
 
33
39
 
@@ -78,6 +84,21 @@ class DiscussionPolicy:
78
84
  projections: ContextProjections
79
85
 
80
86
 
87
+ @dataclass(frozen=True, slots=True)
88
+ class RemediationThreadPolicy:
89
+ """Select verified toolkit-owned GitLab remediation threads."""
90
+
91
+ required: bool
92
+ account_classes: tuple[str, ...]
93
+ include_resolved: bool
94
+ include_outdated: bool
95
+ max_age_seconds: int
96
+ max_threads: int
97
+ max_replies_per_thread: int
98
+ max_items: int
99
+ budgets: TextBudgets
100
+
101
+
81
102
  @dataclass(frozen=True, slots=True)
82
103
  class RecognizerPolicy:
83
104
  """Hold one fixed toolkit-authored candidate grammar."""
@@ -110,5 +131,6 @@ class ContextPolicy:
110
131
  schema_version: str
111
132
  budgets: AggregateBudgets
112
133
  forge_discussions: DiscussionPolicy | None
134
+ remediation_threads: RemediationThreadPolicy | None
113
135
  references: tuple[ReferencePolicy, ...]
114
136
  digest: str