open-code-review-toolkit 0.8.2__tar.gz → 0.8.4__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 (110) hide show
  1. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/PKG-INFO +2 -2
  2. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/README.md +1 -1
  3. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/_version.py +2 -2
  4. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/dlp.py +7 -4
  5. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/ocr_result.py +96 -1
  6. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/approval.py +65 -2
  7. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/formatting.py +72 -59
  8. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/result.py +5 -2
  9. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/settings.py +0 -6
  10. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/workflow.py +48 -9
  11. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/preflight.py +1 -1
  12. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/review_runner.py +134 -105
  13. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/.gitignore +0 -0
  14. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/LICENSE +0 -0
  15. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/pyproject.toml +0 -0
  16. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/__init__.py +0 -0
  17. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/cli.py +0 -0
  18. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/common/__init__.py +0 -0
  19. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/common/filesystem.py +0 -0
  20. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/common/git.py +0 -0
  21. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/common/language.py +0 -0
  22. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/common/markdown.py +0 -0
  23. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/common/redaction.py +0 -0
  24. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/config_writer.py +0 -0
  25. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/configure.py +0 -0
  26. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/__init__.py +0 -0
  27. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/adapters.py +0 -0
  28. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/broker.py +0 -0
  29. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/contracts.py +0 -0
  30. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/mcp.py +0 -0
  31. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/policy.py +0 -0
  32. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/recognizers.py +0 -0
  33. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/store.py +0 -0
  34. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/__init__.py +0 -0
  35. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/__main__.py +0 -0
  36. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/actions.py +0 -0
  37. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/artifacts.py +0 -0
  38. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/categorize.py +0 -0
  39. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/collect.py +0 -0
  40. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/collectors/__init__.py +0 -0
  41. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/collectors/graphs.py +0 -0
  42. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/collectors/orchestration.py +0 -0
  43. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/collectors/projections.py +0 -0
  44. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/collectors/registry.py +0 -0
  45. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/collectors/sources.py +0 -0
  46. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/coverage.py +0 -0
  47. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/__init__.py +0 -0
  48. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/ansible/__init__.py +0 -0
  49. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/ansible/requirements.py +0 -0
  50. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/ansible/topology.py +0 -0
  51. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/contracts.py +0 -0
  52. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/go.py +0 -0
  53. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/javascript.py +0 -0
  54. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/php.py +0 -0
  55. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/python.py +0 -0
  56. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/__init__.py +0 -0
  57. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/contracts.py +0 -0
  58. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/detection.py +0 -0
  59. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/providers/__init__.py +0 -0
  60. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/providers/frontend.py +0 -0
  61. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/providers/go.py +0 -0
  62. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/providers/php.py +0 -0
  63. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/providers/python.py +0 -0
  64. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/registry.py +0 -0
  65. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/schema.py +0 -0
  66. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/templates.py +0 -0
  67. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/infrastructure.py +0 -0
  68. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/invocation.py +0 -0
  69. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/mcp.py +0 -0
  70. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/model.py +0 -0
  71. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/policy/__init__.py +0 -0
  72. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/policy/contracts.py +0 -0
  73. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/policy/decisions.py +0 -0
  74. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/policy/guidance.py +0 -0
  75. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/policy/registry.py +0 -0
  76. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/policy/schema.py +0 -0
  77. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/policy/scopes.py +0 -0
  78. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/project.py +0 -0
  79. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/repository.py +0 -0
  80. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/review_context.py +0 -0
  81. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/store/__init__.py +0 -0
  82. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/store/atomic.py +0 -0
  83. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/store/contracts.py +0 -0
  84. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/store/core.py +0 -0
  85. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/store/readback.py +0 -0
  86. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/store/values.py +0 -0
  87. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/mcp_config.py +0 -0
  88. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/__init__.py +0 -0
  89. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/__main__.py +0 -0
  90. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/comments.py +0 -0
  91. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/gitlab.py +0 -0
  92. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/gitlab_approval.py +0 -0
  93. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/markers.py +0 -0
  94. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/payloads.py +0 -0
  95. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/reconciliation.py +0 -0
  96. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/snapshot.py +0 -0
  97. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/suggestions.py +0 -0
  98. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/transaction.py +0 -0
  99. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/pre_execution.py +0 -0
  100. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/provider_config.py +0 -0
  101. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/provider_failure.py +0 -0
  102. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/providers/__init__.py +0 -0
  103. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/providers/gitlab.py +0 -0
  104. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/providers/gitlab_context.py +0 -0
  105. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/providers/gitlab_discussions.py +0 -0
  106. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/providers/gitlab_identity.py +0 -0
  107. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/providers/gitlab_remediation.py +0 -0
  108. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/py.typed +0 -0
  109. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/result_contract.py +0 -0
  110. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/result_usage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-code-review-toolkit
3
- Version: 0.8.2
3
+ Version: 0.8.4
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
@@ -249,7 +249,7 @@ ocr-ci --help
249
249
  The exact recommended OCR release and its verified asset checksums live in the [versioned compatibility manifest](compatibility/ocr-support.json). CI should pin that release and checksum before execution.
250
250
  The [versioned compatibility policy](docs/compatibility.md) records tested assets and evidence and describes the conservative Dependabot-like qualification workflow for later upstream releases.
251
251
  Review output defaults to English. `OCR_REVIEW_LANGUAGE` accepts another explicit language name when a project needs localized review output; for example, `OCR_REVIEW_LANGUAGE=Russian`.
252
- The current OCR 1.10.0 integration defaults `OCR_REVIEW_EFFORT` to `medium` for two review rounds. `low` and `high` are explicit one- and three-round alternatives; see the [configuration reference](docs/configuration.md#review-effort) for cost, budget, and precedence boundaries.
252
+ The current OCR 1.10.1 integration defaults `OCR_REVIEW_EFFORT` to `medium` for two review rounds. `low` and `high` are explicit one- and three-round alternatives; see the [configuration reference](docs/configuration.md#review-effort) for cost, budget, and precedence boundaries.
253
253
 
254
254
  Stable distributions are published to [PyPI](https://pypi.org/project/open-code-review-toolkit/) and mirrored as checksum-listed, provenance-attested assets in the corresponding [GitHub Release](https://github.com/xeonvs/open-code-review-toolkit/releases). Development snapshots are published only to TestPyPI.
255
255
 
@@ -22,7 +22,7 @@ ocr-ci --help
22
22
  The exact recommended OCR release and its verified asset checksums live in the [versioned compatibility manifest](compatibility/ocr-support.json). CI should pin that release and checksum before execution.
23
23
  The [versioned compatibility policy](docs/compatibility.md) records tested assets and evidence and describes the conservative Dependabot-like qualification workflow for later upstream releases.
24
24
  Review output defaults to English. `OCR_REVIEW_LANGUAGE` accepts another explicit language name when a project needs localized review output; for example, `OCR_REVIEW_LANGUAGE=Russian`.
25
- The current OCR 1.10.0 integration defaults `OCR_REVIEW_EFFORT` to `medium` for two review rounds. `low` and `high` are explicit one- and three-round alternatives; see the [configuration reference](docs/configuration.md#review-effort) for cost, budget, and precedence boundaries.
25
+ The current OCR 1.10.1 integration defaults `OCR_REVIEW_EFFORT` to `medium` for two review rounds. `low` and `high` are explicit one- and three-round alternatives; see the [configuration reference](docs/configuration.md#review-effort) for cost, budget, and precedence boundaries.
26
26
 
27
27
  Stable distributions are published to [PyPI](https://pypi.org/project/open-code-review-toolkit/) and mirrored as checksum-listed, provenance-attested assets in the corresponding [GitHub Release](https://github.com/xeonvs/open-code-review-toolkit/releases). Development snapshots are published only to TestPyPI.
28
28
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.8.2'
22
- __version_tuple__ = version_tuple = (0, 8, 2)
21
+ __version__ = version = '0.8.4'
22
+ __version_tuple__ = version_tuple = (0, 8, 4)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -109,7 +109,7 @@ class ForbiddenMatcher:
109
109
  exact: list[str] = []
110
110
  seen: set[str] = set()
111
111
  for value in values:
112
- candidate = normalize_text(value)
112
+ candidate = normalize_text(value, allow_horizontal_tabs=True)
113
113
  if not candidate:
114
114
  continue
115
115
  for representation in (_display_normalize(candidate), _source_normalize(candidate)):
@@ -157,14 +157,16 @@ class ForbiddenMatcher:
157
157
  return self.match_reason(value) is not None
158
158
 
159
159
 
160
- def normalize_text(value: object) -> str | None:
160
+ def normalize_text(value: object, *, allow_horizontal_tabs: bool = False) -> str | None:
161
161
  """Normalize NFC/newlines and reject unsupported controls."""
162
162
 
163
163
  if not isinstance(value, str):
164
164
  return None
165
165
  normalized = unicodedata.normalize("NFC", value.replace("\r\n", "\n").replace("\r", "\n"))
166
+ allowed_controls = "\n\t" if allow_horizontal_tabs else "\n"
166
167
  if any(
167
- unicodedata.category(character) in {"Cc", "Cf", "Cs", "Zl", "Zp"} and character != "\n"
168
+ unicodedata.category(character) in {"Cc", "Cf", "Cs", "Zl", "Zp"}
169
+ and character not in allowed_controls
168
170
  for character in normalized
169
171
  ):
170
172
  return None
@@ -178,10 +180,11 @@ def check_text(
178
180
  publication: bool = False,
179
181
  forbidden: tuple[str, ...] = (),
180
182
  forbidden_matcher: ForbiddenMatcher | None = None,
183
+ allow_horizontal_tabs: bool = False,
181
184
  ) -> DLPResult:
182
185
  """Apply independent units, redaction, PII, and optional publication checks."""
183
186
 
184
- normalized = normalize_text(value)
187
+ normalized = normalize_text(value, allow_horizontal_tabs=allow_horizontal_tabs)
185
188
  if normalized is None:
186
189
  return DLPResult(False, None, "invalid_text", "type_or_control")
187
190
  normalized_bytes = normalized.encode("utf-8")
@@ -9,6 +9,7 @@ import secrets
9
9
  import stat
10
10
  import sys
11
11
  from collections.abc import Callable, Mapping
12
+ from dataclasses import dataclass
12
13
  from pathlib import Path
13
14
  from typing import Any
14
15
 
@@ -20,6 +21,11 @@ MAX_RESULT_BYTES_HARD_LIMIT = 20_000_000
20
21
  TOOLKIT_RESULT_KEY = "_ocr_toolkit"
21
22
  TOOLKIT_RESULT_SCHEMA_VERSION = 5
22
23
  SUPPORTED_TOOLKIT_RESULT_SCHEMA_VERSIONS = frozenset({TOOLKIT_RESULT_SCHEMA_VERSION})
24
+ TOOLKIT_ADVISORY_KEY = "_ocr_toolkit_advisory"
25
+ TOOLKIT_ADVISORY_SCHEMA_VERSION = "ocr.toolkit-advisory/v1"
26
+ TOOLKIT_ADVISORY_KIND = "background_recommended_limit"
27
+ TOOLKIT_ADVISORY_UNIT = "characters"
28
+ MAX_TOOLKIT_ADVISORY_VALUE = 999_999_999_999
23
29
  # The receipt can name the 16 configured external servers plus the mandatory built-in.
24
30
  MAX_TOOLKIT_MCP_USAGE_SERVERS = 17
25
31
  MAX_TOOLKIT_MCP_TOOLS_PER_SERVER = 128
@@ -27,6 +33,23 @@ MAX_TOOLKIT_MCP_TOOL_NAME_CHARS = 4_096
27
33
  MAX_TOOLKIT_MCP_USAGE_COUNT = 1_000_000_000
28
34
  TOOLKIT_MCP_SERVER_NAME_RE = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
29
35
 
36
+ # Closed names whose numeric call counts may appear in the public review summary.
37
+ # Dynamic external MCP tools remain represented by the receipt's verified
38
+ # per-server aggregate so provider-controlled names do not cross into GitLab.
39
+ PUBLIC_REVIEW_TOOL_CALL_NAMES = frozenset(
40
+ {
41
+ "code_comment",
42
+ "code_search",
43
+ "context_get",
44
+ "context_list",
45
+ "file_find",
46
+ "file_read",
47
+ "file_read_diff",
48
+ "ocr_toolkit_evidence",
49
+ "task_done",
50
+ }
51
+ )
52
+
30
53
 
31
54
  class OcrResultMissing(Exception):
32
55
  """The OCR result artifact is missing or unreadable on disk."""
@@ -40,6 +63,69 @@ class OcrResultTooLarge(Exception):
40
63
  """The OCR result artifact exceeds the configured safety limit."""
41
64
 
42
65
 
66
+ @dataclass(frozen=True, slots=True)
67
+ class OcrToolkitAdvisory:
68
+ """Carry one validated toolkit-authored numeric OCR advisory."""
69
+
70
+ actual: int
71
+ recommended: int
72
+
73
+
74
+ def parse_toolkit_advisory(value: Any) -> OcrToolkitAdvisory:
75
+ """Parse the exact private toolkit advisory without accepting extensions."""
76
+
77
+ if not isinstance(value, dict) or set(value) != {
78
+ "schema_version",
79
+ "kind",
80
+ "actual",
81
+ "recommended",
82
+ "unit",
83
+ }:
84
+ raise OcrResultMalformed("OCR toolkit advisory has an unsupported schema")
85
+ actual = value.get("actual")
86
+ recommended = value.get("recommended")
87
+ if (
88
+ value.get("schema_version") != TOOLKIT_ADVISORY_SCHEMA_VERSION
89
+ or value.get("kind") != TOOLKIT_ADVISORY_KIND
90
+ or value.get("unit") != TOOLKIT_ADVISORY_UNIT
91
+ or not isinstance(actual, int)
92
+ or isinstance(actual, bool)
93
+ or not isinstance(recommended, int)
94
+ or isinstance(recommended, bool)
95
+ or not 0 < recommended < actual <= MAX_TOOLKIT_ADVISORY_VALUE
96
+ ):
97
+ raise OcrResultMalformed("OCR toolkit advisory has invalid closed values")
98
+ return OcrToolkitAdvisory(actual=actual, recommended=recommended)
99
+
100
+
101
+ def background_recommended_advisory(*, actual: int, recommended: int) -> OcrToolkitAdvisory:
102
+ """Construct one validated background recommendation advisory."""
103
+
104
+ return parse_toolkit_advisory(
105
+ {
106
+ "schema_version": TOOLKIT_ADVISORY_SCHEMA_VERSION,
107
+ "kind": TOOLKIT_ADVISORY_KIND,
108
+ "actual": actual,
109
+ "recommended": recommended,
110
+ "unit": TOOLKIT_ADVISORY_UNIT,
111
+ }
112
+ )
113
+
114
+
115
+ def toolkit_advisory_payload(advisory: OcrToolkitAdvisory) -> dict[str, object]:
116
+ """Serialize a validated advisory into its exact private result shape."""
117
+
118
+ payload: dict[str, object] = {
119
+ "schema_version": TOOLKIT_ADVISORY_SCHEMA_VERSION,
120
+ "kind": TOOLKIT_ADVISORY_KIND,
121
+ "actual": advisory.actual,
122
+ "recommended": advisory.recommended,
123
+ "unit": TOOLKIT_ADVISORY_UNIT,
124
+ }
125
+ parse_toolkit_advisory(payload)
126
+ return payload
127
+
128
+
43
129
  def max_result_bytes() -> int:
44
130
  """Return the maximum OCR JSON artifact size to read into memory."""
45
131
 
@@ -217,7 +303,16 @@ def _decode_result(data: bytes) -> Any:
217
303
  except UnicodeDecodeError as exc:
218
304
  raise OcrResultMalformed(str(exc)) from exc
219
305
  try:
220
- return json.loads(text)
306
+
307
+ def reject_duplicate_advisory(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
308
+ result: dict[str, Any] = {}
309
+ for key, value in pairs:
310
+ if key == TOOLKIT_ADVISORY_KEY and key in result:
311
+ raise OcrResultMalformed("OCR result repeats the reserved toolkit advisory")
312
+ result[key] = value
313
+ return result
314
+
315
+ return json.loads(text, object_pairs_hook=reject_duplicate_advisory)
221
316
  except (json.JSONDecodeError, RecursionError) as exc:
222
317
  raise OcrResultMalformed(str(exc)) from exc
223
318
 
@@ -14,10 +14,11 @@ from ocr_toolkit.ocr_result import (
14
14
  TOOLKIT_MCP_SERVER_NAME_RE,
15
15
  )
16
16
  from ocr_toolkit.posting.settings import BooleanSetting
17
- from ocr_toolkit.result_contract import ReviewOutcome
17
+ from ocr_toolkit.result_contract import OcrResultContractError, ReviewOutcome
18
18
 
19
19
  ALLOWED_CATEGORIES = frozenset({"style", "documentation", "maintainability"})
20
20
  MAX_APPROVABLE_FINDINGS = 3
21
+ INVALID_APPROVAL_RECEIPT_REASON = "the review-time approval receipt is missing or invalid"
21
22
 
22
23
 
23
24
  class ApprovalStatus(str, Enum):
@@ -218,6 +219,23 @@ def publication_dlp_state(value: Any) -> str | None:
218
219
  )
219
220
  ):
220
221
  return None
222
+ selected = original["selected"]
223
+ completed = original["completed"]
224
+ reused = original["reused"]
225
+ failed = original["failed"]
226
+ waived = original["waived"]
227
+ outcome = original["outcome"]
228
+ derived_outcomes = {"failed"} | (
229
+ {"skipped"}
230
+ if selected == 0
231
+ else {"clean", "warning"}
232
+ if failed == 0
233
+ else {"failed"}
234
+ if failed == selected
235
+ else {"partial"}
236
+ )
237
+ if selected != completed + reused + failed + waived or outcome not in derived_outcomes:
238
+ return None
221
239
  return "publication-filtered"
222
240
 
223
241
 
@@ -237,7 +255,7 @@ def _valid_dlp_reason_counts(value: Any) -> bool:
237
255
  def automatic_approval_metadata_reason(toolkit_metadata: Any) -> str:
238
256
  """Return the closed review-time receipt blocker for automatic approval."""
239
257
 
240
- invalid = "the review-time approval receipt is missing or invalid"
258
+ invalid = INVALID_APPROVAL_RECEIPT_REASON
241
259
  if not isinstance(toolkit_metadata, dict):
242
260
  return invalid
243
261
  if toolkit_metadata.get("schema_version") != 5 or set(toolkit_metadata) != {
@@ -433,6 +451,51 @@ def automatic_approval_metadata_reason(toolkit_metadata: Any) -> str:
433
451
  return ""
434
452
 
435
453
 
454
+ def toolkit_receipt_is_valid(toolkit_metadata: Any) -> bool:
455
+ """Return whether metadata is an exact receipt v5, including valid blockers."""
456
+
457
+ return automatic_approval_metadata_reason(toolkit_metadata) != INVALID_APPROVAL_RECEIPT_REASON
458
+
459
+
460
+ def publication_outcome_for_summary(outcome: ReviewOutcome, publication: Any) -> ReviewOutcome:
461
+ """Recover only validated original coverage facts from a filtered receipt."""
462
+
463
+ if publication_dlp_state(publication) != "publication-filtered":
464
+ return outcome
465
+ if outcome.kind != "partial" or outcome.manifest_present:
466
+ raise OcrResultContractError(
467
+ "publication-filtered receipt is not bound to a safe result projection"
468
+ )
469
+ original = publication["original"]
470
+ kind = original["outcome"]
471
+ if outcome.budget_exceeded and kind != "partial":
472
+ raise OcrResultContractError(
473
+ "publication-filtered receipt contradicts the result budget state"
474
+ )
475
+ counts = {
476
+ field: original[field] for field in ("selected", "completed", "reused", "failed", "waived")
477
+ }
478
+ manifest_present = any(counts.values())
479
+ status = {
480
+ "clean": "complete" if manifest_present else "success",
481
+ "warning": "completed_with_warnings",
482
+ "partial": "budget_exceeded" if outcome.budget_exceeded else "completed_with_errors",
483
+ "failed": "failed",
484
+ "skipped": "skipped",
485
+ }[kind]
486
+ return ReviewOutcome(
487
+ status=status,
488
+ kind=kind,
489
+ budget_exceeded=outcome.budget_exceeded and kind == "partial",
490
+ manifest_present=manifest_present,
491
+ selected_count=counts["selected"],
492
+ completed_count=counts["completed"],
493
+ reused_count=counts["reused"],
494
+ failed_count=counts["failed"],
495
+ waived_count=counts["waived"],
496
+ )
497
+
498
+
436
499
  def _valid_evidence_actions(value: Any, evidence_calls: Any) -> bool:
437
500
  """Validate verified counts or an explicit unavailable attribution state."""
438
501
 
@@ -20,8 +20,10 @@ from ocr_toolkit.common.redaction import redact_sensitive
20
20
  from ocr_toolkit.ocr_result import (
21
21
  MAX_TOOLKIT_MCP_USAGE_COUNT,
22
22
  MAX_TOOLKIT_MCP_USAGE_SERVERS,
23
+ PUBLIC_REVIEW_TOOL_CALL_NAMES,
23
24
  SUPPORTED_TOOLKIT_RESULT_SCHEMA_VERSIONS,
24
25
  TOOLKIT_MCP_SERVER_NAME_RE,
26
+ OcrToolkitAdvisory,
25
27
  )
26
28
  from ocr_toolkit.posting.approval import (
27
29
  ApprovalResult,
@@ -45,8 +47,6 @@ from ocr_toolkit.posting.settings import (
45
47
  MAX_REVIEWER_GUIDE_LABEL_CHARS,
46
48
  MAX_REVIEWER_GUIDE_LOCATION_CHARS,
47
49
  MAX_REVIEWER_GUIDE_TEXT_CHARS,
48
- MAX_TOOL_CALL_NAME_CHARS,
49
- MAX_TOOL_CALL_SUMMARY_TOOLS,
50
50
  SUGGESTION_HEADER,
51
51
  post_badges,
52
52
  post_emoji,
@@ -374,32 +374,25 @@ def nonnegative_int(value: Any) -> int | None:
374
374
  return None
375
375
 
376
376
 
377
- def truncate_tool_call_name(name: str) -> str:
378
- """Return a compact tool name for one-line MR summaries."""
379
-
380
- if len(name) <= MAX_TOOL_CALL_NAME_CHARS:
381
- return name
382
-
383
- return name[: MAX_TOOL_CALL_NAME_CHARS - 3].rstrip() + "..."
384
-
385
-
386
377
  def tool_call_name(value: Any) -> str:
387
- """Extract a displayable tool name from common OCR tool-call shapes."""
378
+ """Extract one closed public tool name from common OCR call shapes."""
388
379
 
389
380
  if isinstance(value, str):
390
- return clean_text(value)
381
+ name = clean_text(value)
382
+ return name if name in PUBLIC_REVIEW_TOOL_CALL_NAMES else ""
391
383
 
392
384
  if not isinstance(value, dict):
393
385
  return ""
394
386
 
395
387
  for key in ("name", "tool", "tool_name"):
396
388
  name = clean_text(value.get(key))
397
- if name:
389
+ if name in PUBLIC_REVIEW_TOOL_CALL_NAMES:
398
390
  return name
399
391
 
400
392
  function_value = value.get("function")
401
393
  if isinstance(function_value, dict):
402
- return clean_text(function_value.get("name"))
394
+ name = clean_text(function_value.get("name"))
395
+ return name if name in PUBLIC_REVIEW_TOOL_CALL_NAMES else ""
403
396
 
404
397
  return ""
405
398
 
@@ -407,14 +400,17 @@ def tool_call_name(value: Any) -> str:
407
400
  def tool_call_counts_from_items(
408
401
  items: list[Any],
409
402
  ) -> tuple[int | None, list[tuple[str, int]]]:
410
- """Summarize a list-style OCR tool_calls payload."""
403
+ """Summarize admitted calls from a legacy list-style OCR payload."""
411
404
 
412
405
  counts: dict[str, int] = {}
413
406
  for item in items:
414
407
  name = tool_call_name(item)
415
408
  if not name:
416
409
  continue
417
- counts[name] = counts.get(name, 0) + 1
410
+ count = counts.get(name, 0) + 1
411
+ if count > MAX_TOOLKIT_MCP_USAGE_COUNT:
412
+ return None, []
413
+ counts[name] = count
418
414
 
419
415
  total = sum(counts.values())
420
416
  if total == 0 and items:
@@ -424,51 +420,52 @@ def tool_call_counts_from_items(
424
420
 
425
421
 
426
422
  def format_tool_calls_summary(tool_calls: Any) -> str:
427
- """Return one bounded MR summary line for OCR tool-call statistics."""
423
+ """Return one bounded MR line for admitted non-zero OCR tool counts."""
428
424
 
429
425
  entries: list[tuple[str, int]]
430
426
  total: int | None
431
- scalar_total = nonnegative_int(tool_calls)
432
- if scalar_total is not None:
433
- total = scalar_total
434
- entries = []
435
- elif isinstance(tool_calls, list):
427
+ if isinstance(tool_calls, list):
436
428
  total, entries = tool_call_counts_from_items(tool_calls)
437
429
  elif isinstance(tool_calls, dict):
438
430
  by_tool_value = tool_calls.get("by_tool")
439
431
  entries = []
440
- by_tool_total = 0
441
- valid_by_tool_count = False
432
+ admitted_total = 0
442
433
 
443
434
  if isinstance(by_tool_value, dict):
444
435
  for raw_name, raw_count in by_tool_value.items():
445
- count = nonnegative_int(raw_count)
446
- if count is None:
436
+ if not isinstance(raw_name, str) or raw_name not in PUBLIC_REVIEW_TOOL_CALL_NAMES:
447
437
  continue
448
- name = clean_text(raw_name)
449
- if not name:
438
+ if (
439
+ not isinstance(raw_count, int)
440
+ or isinstance(raw_count, bool)
441
+ or not 0 < raw_count <= MAX_TOOLKIT_MCP_USAGE_COUNT
442
+ ):
450
443
  continue
451
- valid_by_tool_count = True
452
- by_tool_total += count
453
- if count > 0:
454
- entries.append((name, count))
444
+ admitted_total += raw_count
445
+ if admitted_total > MAX_TOOLKIT_MCP_USAGE_COUNT:
446
+ return ""
447
+ entries.append((raw_name, raw_count))
455
448
 
456
449
  calls_value = tool_calls.get("calls")
457
- if valid_by_tool_count:
458
- list_total = None
459
- elif isinstance(calls_value, list):
450
+ if not by_tool_value and isinstance(calls_value, list):
460
451
  list_total, entries = tool_call_counts_from_items(calls_value)
461
452
  else:
462
453
  list_total = None
463
454
 
464
- total = nonnegative_int(tool_calls.get("total"))
465
- if total is None:
466
- if valid_by_tool_count:
467
- total = by_tool_total
468
- elif list_total is not None:
455
+ if "total" in tool_calls:
456
+ raw_total = tool_calls["total"]
457
+ if (
458
+ not isinstance(raw_total, int)
459
+ or isinstance(raw_total, bool)
460
+ or not 0 < raw_total <= MAX_TOOLKIT_MCP_USAGE_COUNT
461
+ ):
462
+ return ""
463
+ total = raw_total
464
+ else:
465
+ if list_total is not None:
469
466
  total = list_total
470
- elif by_tool_value == {}:
471
- total = 0
467
+ elif entries:
468
+ total = admitted_total
472
469
  else:
473
470
  return ""
474
471
  else:
@@ -476,21 +473,14 @@ def format_tool_calls_summary(tool_calls: Any) -> str:
476
473
 
477
474
  if total is None:
478
475
  return ""
479
- if total == 0:
476
+ if total == 0 or not entries:
477
+ return ""
478
+ if sum(count for _name, count in entries) > total:
480
479
  return ""
481
480
 
482
481
  line = f"- all OCR tool calls: {total} total"
483
- if not entries:
484
- return line
485
-
486
482
  entries.sort(key=lambda item: (-item[1], item[0]))
487
- shown_entries = entries[:MAX_TOOL_CALL_SUMMARY_TOOLS]
488
- detail_parts = [
489
- f"{inline_code(truncate_tool_call_name(name))}: {count}" for name, count in shown_entries
490
- ]
491
- omitted_entries = len(entries) - len(shown_entries)
492
- if omitted_entries > 0:
493
- detail_parts.append(f"+{omitted_entries} more")
483
+ detail_parts = [f"{inline_code(name)}: {count}" for name, count in entries]
494
484
 
495
485
  return f"{line} ({', '.join(detail_parts)})"
496
486
 
@@ -610,10 +600,10 @@ def format_publication_dlp_details(signal: dict[str, Any] | None) -> str:
610
600
  omitted = signal["omitted"]
611
601
  carried = signal["carried_forward_comments"]
612
602
  completeness = (
613
- "One or more publication units were omitted, so this review is partial and cannot "
614
- "authorize automatic approval."
603
+ "One or more public projection units were omitted. OCR coverage is reported "
604
+ "separately, and automatic approval remains unavailable."
615
605
  if omitted["comments"] or omitted["warnings"]
616
- else "The canonical publication or approval projection changed, so automatic approval remains unavailable."
606
+ else "The public projection changed, so automatic approval remains unavailable."
617
607
  )
618
608
  return "\n".join(
619
609
  [
@@ -841,7 +831,11 @@ def format_reviewer_guide(
841
831
  enumerate(comments),
842
832
  key=lambda item: _guide_comment_rank(item[1], item[0]),
843
833
  )
844
- guide_comments = [comment for _, comment in ranked_comments[:MAX_REVIEWER_GUIDE_COMMENTS]]
834
+ guide_comments = (
835
+ [comment for _, comment in ranked_comments[:MAX_REVIEWER_GUIDE_COMMENTS]]
836
+ if len(comments) >= 2
837
+ else []
838
+ )
845
839
  if guide_comments:
846
840
  lines.append("")
847
841
  lines.append("### Recommended focus areas")
@@ -890,6 +884,8 @@ def _review_outcome_line(
890
884
  marker, status_text = "⚠️", "Review stopped at token budget"
891
885
  elif partial_result:
892
886
  marker, status_text = "⚠️", "Review incomplete"
887
+ elif outcome_status == "publication-filtered":
888
+ marker, status_text = "⚠️", "Review complete with publication filtering"
893
889
  elif outcome_status in {"warning", "completed_with_warnings"} or warning_count:
894
890
  marker, status_text = "⚠️", "Review complete with warnings"
895
891
  elif has_finding_state:
@@ -933,6 +929,17 @@ def _review_outcome_line(
933
929
  return f"{prefix}**{status_text} — {result_text}**"
934
930
 
935
931
 
932
+ def format_ocr_core_advisory(advisory: OcrToolkitAdvisory | None) -> str:
933
+ """Render one validated numeric OCR advisory for Technical details only."""
934
+
935
+ if advisory is None:
936
+ return ""
937
+ return (
938
+ f"- OCR core advisory: background {advisory.actual} characters; recommended "
939
+ f"{advisory.recommended} characters; accepted by OCR core"
940
+ )
941
+
942
+
936
943
  def summarize_result(
937
944
  total: int,
938
945
  inline_count: int,
@@ -944,6 +951,7 @@ def summarize_result(
944
951
  tool_calls_summary: str = "",
945
952
  mcp_usage_summary: str = "",
946
953
  token_usage_summary: str = "",
954
+ ocr_core_advisory_summary: str = "",
947
955
  publication_dlp_details: str = "",
948
956
  reviewer_guide: str = "",
949
957
  fallback_reasons: Mapping[str, int] | None = None,
@@ -1055,7 +1063,12 @@ def summarize_result(
1055
1063
  )
1056
1064
  if reasons:
1057
1065
  technical.append(f"- Fallback reasons: {reasons}")
1058
- for summary in (mcp_usage_summary, tool_calls_summary, token_usage_summary):
1066
+ for summary in (
1067
+ mcp_usage_summary,
1068
+ tool_calls_summary,
1069
+ token_usage_summary,
1070
+ ocr_core_advisory_summary,
1071
+ ):
1059
1072
  if summary:
1060
1073
  technical.append(summary)
1061
1074
  technical.append(f"- Review mode: `{post_mode()}`")
@@ -122,7 +122,10 @@ def _safe_detail(value: object, reason: str) -> str:
122
122
 
123
123
 
124
124
  def normalize_coverage_diagnostics(
125
- outcome: ReviewOutcome, warnings: Sequence[Any]
125
+ outcome: ReviewOutcome,
126
+ warnings: Sequence[Any],
127
+ *,
128
+ legacy_warning_fallback: bool = True,
126
129
  ) -> CoverageDiagnostics:
127
130
  """Normalize manifest failures or legacy warnings once at the posting boundary."""
128
131
 
@@ -136,7 +139,7 @@ def normalize_coverage_diagnostics(
136
139
  )
137
140
  for item in outcome.failed_items
138
141
  )
139
- elif outcome.kind == "partial":
142
+ elif outcome.kind == "partial" and legacy_warning_fallback:
140
143
  for warning in warnings:
141
144
  path = warning.get("file") or warning.get("path") if isinstance(warning, dict) else None
142
145
  candidates.append((path, _legacy_reason(warning), ocr_warning_text(warning)))
@@ -53,12 +53,6 @@ DEFAULT_MAX_POST_COMMENTS = 50
53
53
  MAX_POST_COMMENTS_HARD_LIMIT = 200
54
54
 
55
55
 
56
- MAX_TOOL_CALL_SUMMARY_TOOLS = 6
57
-
58
-
59
- MAX_TOOL_CALL_NAME_CHARS = 48
60
-
61
-
62
56
  MAX_REVIEWER_GUIDE_COMMENTS = 6
63
57
 
64
58