open-code-review-toolkit 0.8.3__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.3 → open_code_review_toolkit-0.8.4}/PKG-INFO +2 -2
  2. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/README.md +1 -1
  3. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/_version.py +2 -2
  4. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/dlp.py +7 -4
  5. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/ocr_result.py +79 -1
  6. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/approval.py +65 -2
  7. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/formatting.py +29 -5
  8. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/result.py +5 -2
  9. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/workflow.py +48 -9
  10. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/preflight.py +1 -1
  11. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/review_runner.py +81 -50
  12. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/.gitignore +0 -0
  13. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/LICENSE +0 -0
  14. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/pyproject.toml +0 -0
  15. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/__init__.py +0 -0
  16. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/cli.py +0 -0
  17. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/common/__init__.py +0 -0
  18. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/common/filesystem.py +0 -0
  19. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/common/git.py +0 -0
  20. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/common/language.py +0 -0
  21. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/common/markdown.py +0 -0
  22. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/common/redaction.py +0 -0
  23. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/config_writer.py +0 -0
  24. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/configure.py +0 -0
  25. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/__init__.py +0 -0
  26. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/adapters.py +0 -0
  27. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/broker.py +0 -0
  28. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/contracts.py +0 -0
  29. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/mcp.py +0 -0
  30. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/policy.py +0 -0
  31. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/recognizers.py +0 -0
  32. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/context/store.py +0 -0
  33. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/__init__.py +0 -0
  34. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/__main__.py +0 -0
  35. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/actions.py +0 -0
  36. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/artifacts.py +0 -0
  37. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/categorize.py +0 -0
  38. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/collect.py +0 -0
  39. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/collectors/__init__.py +0 -0
  40. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/collectors/graphs.py +0 -0
  41. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/collectors/orchestration.py +0 -0
  42. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/collectors/projections.py +0 -0
  43. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/collectors/registry.py +0 -0
  44. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/collectors/sources.py +0 -0
  45. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/coverage.py +0 -0
  46. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/__init__.py +0 -0
  47. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/ansible/__init__.py +0 -0
  48. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/ansible/requirements.py +0 -0
  49. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/ansible/topology.py +0 -0
  50. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/contracts.py +0 -0
  51. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/go.py +0 -0
  52. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/javascript.py +0 -0
  53. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/php.py +0 -0
  54. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/ecosystems/python.py +0 -0
  55. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/__init__.py +0 -0
  56. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/contracts.py +0 -0
  57. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/detection.py +0 -0
  58. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/providers/__init__.py +0 -0
  59. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/providers/frontend.py +0 -0
  60. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/providers/go.py +0 -0
  61. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/providers/php.py +0 -0
  62. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/providers/python.py +0 -0
  63. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/registry.py +0 -0
  64. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/schema.py +0 -0
  65. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/frameworks/templates.py +0 -0
  66. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/infrastructure.py +0 -0
  67. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/invocation.py +0 -0
  68. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/mcp.py +0 -0
  69. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/model.py +0 -0
  70. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/policy/__init__.py +0 -0
  71. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/policy/contracts.py +0 -0
  72. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/policy/decisions.py +0 -0
  73. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/policy/guidance.py +0 -0
  74. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/policy/registry.py +0 -0
  75. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/policy/schema.py +0 -0
  76. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/policy/scopes.py +0 -0
  77. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/project.py +0 -0
  78. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/repository.py +0 -0
  79. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/review_context.py +0 -0
  80. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/store/__init__.py +0 -0
  81. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/store/atomic.py +0 -0
  82. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/store/contracts.py +0 -0
  83. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/store/core.py +0 -0
  84. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/store/readback.py +0 -0
  85. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/evidence/store/values.py +0 -0
  86. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/mcp_config.py +0 -0
  87. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/__init__.py +0 -0
  88. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/__main__.py +0 -0
  89. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/comments.py +0 -0
  90. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/gitlab.py +0 -0
  91. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/gitlab_approval.py +0 -0
  92. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/markers.py +0 -0
  93. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/payloads.py +0 -0
  94. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/reconciliation.py +0 -0
  95. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/settings.py +0 -0
  96. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/snapshot.py +0 -0
  97. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/suggestions.py +0 -0
  98. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/posting/transaction.py +0 -0
  99. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/pre_execution.py +0 -0
  100. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/provider_config.py +0 -0
  101. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/provider_failure.py +0 -0
  102. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/providers/__init__.py +0 -0
  103. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/providers/gitlab.py +0 -0
  104. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/providers/gitlab_context.py +0 -0
  105. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/providers/gitlab_discussions.py +0 -0
  106. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/providers/gitlab_identity.py +0 -0
  107. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/providers/gitlab_remediation.py +0 -0
  108. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/py.typed +0 -0
  109. {open_code_review_toolkit-0.8.3 → open_code_review_toolkit-0.8.4}/src/ocr_toolkit/result_contract.py +0 -0
  110. {open_code_review_toolkit-0.8.3 → 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.3
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.3'
22
- __version_tuple__ = version_tuple = (0, 8, 3)
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
@@ -57,6 +63,69 @@ class OcrResultTooLarge(Exception):
57
63
  """The OCR result artifact exceeds the configured safety limit."""
58
64
 
59
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
+
60
129
  def max_result_bytes() -> int:
61
130
  """Return the maximum OCR JSON artifact size to read into memory."""
62
131
 
@@ -234,7 +303,16 @@ def _decode_result(data: bytes) -> Any:
234
303
  except UnicodeDecodeError as exc:
235
304
  raise OcrResultMalformed(str(exc)) from exc
236
305
  try:
237
- 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)
238
316
  except (json.JSONDecodeError, RecursionError) as exc:
239
317
  raise OcrResultMalformed(str(exc)) from exc
240
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
 
@@ -23,6 +23,7 @@ from ocr_toolkit.ocr_result import (
23
23
  PUBLIC_REVIEW_TOOL_CALL_NAMES,
24
24
  SUPPORTED_TOOLKIT_RESULT_SCHEMA_VERSIONS,
25
25
  TOOLKIT_MCP_SERVER_NAME_RE,
26
+ OcrToolkitAdvisory,
26
27
  )
27
28
  from ocr_toolkit.posting.approval import (
28
29
  ApprovalResult,
@@ -599,10 +600,10 @@ def format_publication_dlp_details(signal: dict[str, Any] | None) -> str:
599
600
  omitted = signal["omitted"]
600
601
  carried = signal["carried_forward_comments"]
601
602
  completeness = (
602
- "One or more publication units were omitted, so this review is partial and cannot "
603
- "authorize automatic approval."
603
+ "One or more public projection units were omitted. OCR coverage is reported "
604
+ "separately, and automatic approval remains unavailable."
604
605
  if omitted["comments"] or omitted["warnings"]
605
- else "The canonical publication or approval projection changed, so automatic approval remains unavailable."
606
+ else "The public projection changed, so automatic approval remains unavailable."
606
607
  )
607
608
  return "\n".join(
608
609
  [
@@ -830,7 +831,11 @@ def format_reviewer_guide(
830
831
  enumerate(comments),
831
832
  key=lambda item: _guide_comment_rank(item[1], item[0]),
832
833
  )
833
- 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
+ )
834
839
  if guide_comments:
835
840
  lines.append("")
836
841
  lines.append("### Recommended focus areas")
@@ -879,6 +884,8 @@ def _review_outcome_line(
879
884
  marker, status_text = "⚠️", "Review stopped at token budget"
880
885
  elif partial_result:
881
886
  marker, status_text = "⚠️", "Review incomplete"
887
+ elif outcome_status == "publication-filtered":
888
+ marker, status_text = "⚠️", "Review complete with publication filtering"
882
889
  elif outcome_status in {"warning", "completed_with_warnings"} or warning_count:
883
890
  marker, status_text = "⚠️", "Review complete with warnings"
884
891
  elif has_finding_state:
@@ -922,6 +929,17 @@ def _review_outcome_line(
922
929
  return f"{prefix}**{status_text} — {result_text}**"
923
930
 
924
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
+
925
943
  def summarize_result(
926
944
  total: int,
927
945
  inline_count: int,
@@ -933,6 +951,7 @@ def summarize_result(
933
951
  tool_calls_summary: str = "",
934
952
  mcp_usage_summary: str = "",
935
953
  token_usage_summary: str = "",
954
+ ocr_core_advisory_summary: str = "",
936
955
  publication_dlp_details: str = "",
937
956
  reviewer_guide: str = "",
938
957
  fallback_reasons: Mapping[str, int] | None = None,
@@ -1044,7 +1063,12 @@ def summarize_result(
1044
1063
  )
1045
1064
  if reasons:
1046
1065
  technical.append(f"- Fallback reasons: {reasons}")
1047
- 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
+ ):
1048
1072
  if summary:
1049
1073
  technical.append(summary)
1050
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)))
@@ -17,11 +17,13 @@ from ocr_toolkit.common.git import isolated_git_environment, read_only_git_prefi
17
17
  from ocr_toolkit.common.markdown import markdown_code_block, neutralize_quick_actions
18
18
  from ocr_toolkit.evidence.artifacts import repository_artifacts
19
19
  from ocr_toolkit.ocr_result import (
20
+ TOOLKIT_ADVISORY_KEY,
20
21
  TOOLKIT_RESULT_KEY,
21
22
  OcrResultMalformed,
22
23
  OcrResultMissing,
23
24
  OcrResultTooLarge,
24
25
  load_ocr_result,
26
+ parse_toolkit_advisory,
25
27
  )
26
28
  from ocr_toolkit.posting import gitlab as gitlab_api
27
29
  from ocr_toolkit.posting.approval import (
@@ -31,6 +33,8 @@ from ocr_toolkit.posting.approval import (
31
33
  evaluate_approval_policy,
32
34
  provisional_approval_result,
33
35
  publication_dlp_state,
36
+ publication_outcome_for_summary,
37
+ toolkit_receipt_is_valid,
34
38
  )
35
39
  from ocr_toolkit.posting.comments import (
36
40
  clean_text,
@@ -41,6 +45,7 @@ from ocr_toolkit.posting.formatting import (
41
45
  format_fallback_comment_chunks,
42
46
  format_inline_comment,
43
47
  format_mcp_usage_summary,
48
+ format_ocr_core_advisory,
44
49
  format_omitted_comments_summary,
45
50
  format_publication_dlp_details,
46
51
  format_reviewer_guide,
@@ -625,6 +630,19 @@ def post_results(config: GitLabConfig, result: dict[str, Any]) -> int:
625
630
  title="**Open Code Review publication policy error**",
626
631
  )
627
632
 
633
+ advisory = None
634
+ if TOOLKIT_ADVISORY_KEY in result:
635
+ try:
636
+ advisory = parse_toolkit_advisory(result[TOOLKIT_ADVISORY_KEY])
637
+ except OcrResultMalformed as exc:
638
+ return invalid_ocr_schema_exit(config, str(exc))
639
+ if not toolkit_receipt_is_valid(toolkit_metadata):
640
+ return invalid_ocr_schema_exit(
641
+ config,
642
+ "OCR toolkit advisory is not bound to a valid receipt v5",
643
+ )
644
+ ocr_core_advisory_summary = format_ocr_core_advisory(advisory)
645
+
628
646
  comments_value = result.get("comments", [])
629
647
  warnings_value = result.get("warnings", [])
630
648
  tool_calls_summary = format_tool_calls_summary(result.get("tool_calls"))
@@ -651,16 +669,25 @@ def post_results(config: GitLabConfig, result: dict[str, Any]) -> int:
651
669
  approval_comments = list(comments)
652
670
 
653
671
  warnings = warnings_value
654
- coverage_diagnostics = normalize_coverage_diagnostics(outcome, warnings)
655
- if outcome.kind == "failed":
672
+ try:
673
+ summary_outcome = publication_outcome_for_summary(outcome, publication)
674
+ except OcrResultContractError as exc:
675
+ return invalid_ocr_schema_exit(config, str(exc))
676
+ coverage_diagnostics = normalize_coverage_diagnostics(
677
+ summary_outcome,
678
+ warnings,
679
+ legacy_warning_fallback=publication_state != "publication-filtered",
680
+ )
681
+ if summary_outcome.kind == "failed":
656
682
  return post_manifest_failure(
657
683
  config,
658
- outcome,
684
+ summary_outcome,
659
685
  outcome_message,
660
686
  warnings,
661
687
  tool_calls_summary=tool_calls_summary,
662
688
  mcp_usage_summary=mcp_usage_summary,
663
689
  token_usage_summary=token_usage_summary,
690
+ ocr_core_advisory_summary=ocr_core_advisory_summary,
664
691
  )
665
692
 
666
693
  billing_reason = llm_billing_failure_reason(warnings)
@@ -733,11 +760,19 @@ def post_results(config: GitLabConfig, result: dict[str, Any]) -> int:
733
760
  summary_run_id = secrets.token_hex(16)
734
761
  receipt_sha, reviewed_author_id = approval_receipt_identity(result.get(TOOLKIT_RESULT_KEY))
735
762
  reviewed_commit = receipt_sha or reviewed_sha()
763
+ summary_status = (
764
+ "publication-filtered"
765
+ if publication_state == "publication-filtered"
766
+ and summary_outcome.kind in {"clean", "warning"}
767
+ else "budget_exceeded"
768
+ if summary_outcome.budget_exceeded
769
+ else summary_outcome.kind
770
+ )
736
771
  reviewer_guide = format_reviewer_guide(
737
772
  comments,
738
773
  omitted_count,
739
- outcome_status="budget_exceeded" if outcome.budget_exceeded else outcome.kind,
740
- coverage_summary=outcome.coverage_summary,
774
+ outcome_status=summary_status,
775
+ coverage_summary=summary_outcome.coverage_summary,
741
776
  )
742
777
 
743
778
  if publishable_comment_count == 0:
@@ -754,13 +789,14 @@ def post_results(config: GitLabConfig, result: dict[str, Any]) -> int:
754
789
  tool_calls_summary=tool_calls_summary,
755
790
  mcp_usage_summary=mcp_usage_summary,
756
791
  token_usage_summary=token_usage_summary,
792
+ ocr_core_advisory_summary=ocr_core_advisory_summary,
757
793
  publication_dlp_details=dlp_details,
758
794
  reviewer_guide=reviewer_guide,
759
795
  reviewed_sha=reviewed_commit,
760
796
  mr_head_sha=mr_head_sha(),
761
- outcome_status=("budget_exceeded" if outcome.budget_exceeded else outcome.kind),
797
+ outcome_status=summary_status,
762
798
  outcome_message=outcome_message,
763
- coverage_summary=outcome.coverage_summary,
799
+ coverage_summary=summary_outcome.coverage_summary,
764
800
  coverage_diagnostics=coverage_diagnostics,
765
801
  warnings=warnings,
766
802
  suppressed_count=suppressed_count,
@@ -940,14 +976,15 @@ def post_results(config: GitLabConfig, result: dict[str, Any]) -> int:
940
976
  tool_calls_summary=tool_calls_summary,
941
977
  mcp_usage_summary=mcp_usage_summary,
942
978
  token_usage_summary=token_usage_summary,
979
+ ocr_core_advisory_summary=ocr_core_advisory_summary,
943
980
  publication_dlp_details=dlp_details,
944
981
  reviewer_guide=reviewer_guide,
945
982
  fallback_reasons=fallback_reasons,
946
983
  reviewed_sha=reviewed_commit,
947
984
  mr_head_sha=mr_head_sha(),
948
- outcome_status="budget_exceeded" if outcome.budget_exceeded else outcome.kind,
985
+ outcome_status=summary_status,
949
986
  outcome_message=outcome_message,
950
- coverage_summary=outcome.coverage_summary,
987
+ coverage_summary=summary_outcome.coverage_summary,
951
988
  coverage_diagnostics=coverage_diagnostics,
952
989
  warnings=warnings,
953
990
  suppressed_count=suppressed_count,
@@ -1060,6 +1097,7 @@ def post_manifest_failure(
1060
1097
  tool_calls_summary: str = "",
1061
1098
  mcp_usage_summary: str = "",
1062
1099
  token_usage_summary: str = "",
1100
+ ocr_core_advisory_summary: str = "",
1063
1101
  ) -> int:
1064
1102
  """Post a manifest-declared run failure while preserving prior review notes."""
1065
1103
 
@@ -1072,6 +1110,7 @@ def post_manifest_failure(
1072
1110
  tool_calls_summary=tool_calls_summary,
1073
1111
  mcp_usage_summary=mcp_usage_summary,
1074
1112
  token_usage_summary=token_usage_summary,
1113
+ ocr_core_advisory_summary=ocr_core_advisory_summary,
1075
1114
  outcome_status="failed",
1076
1115
  outcome_message=message,
1077
1116
  coverage_summary=outcome.coverage_summary,
@@ -28,7 +28,7 @@ DEFAULT_REQUEST_HEADERS = {
28
28
  "Accept": "application/json",
29
29
  "User-Agent": "open-code-review-ci-preflight/1.0",
30
30
  }
31
- EXPECTED_OCR_VERSION = "1.10.0"
31
+ EXPECTED_OCR_VERSION = "1.10.1"
32
32
 
33
33
 
34
34
  class PreflightError(Exception):
@@ -76,13 +76,17 @@ from ocr_toolkit.evidence.store import EvidenceStore, EvidenceStoreError
76
76
  from ocr_toolkit.ocr_result import (
77
77
  MAX_TOOLKIT_MCP_USAGE_COUNT,
78
78
  PUBLIC_REVIEW_TOOL_CALL_NAMES,
79
+ TOOLKIT_ADVISORY_KEY,
79
80
  TOOLKIT_RESULT_KEY,
80
81
  TOOLKIT_RESULT_SCHEMA_VERSION,
81
82
  OcrResultMalformed,
82
83
  OcrResultMissing,
83
84
  OcrResultTooLarge,
85
+ OcrToolkitAdvisory,
86
+ background_recommended_advisory,
84
87
  inspect_ocr_result,
85
88
  load_ocr_result,
89
+ toolkit_advisory_payload,
86
90
  transform_ocr_result,
87
91
  )
88
92
  from ocr_toolkit.posting.result import ocr_warning_text
@@ -156,7 +160,7 @@ class ReviewRunnerError(Exception):
156
160
  class BackgroundQualification:
157
161
  """Carry closed toolkit-authored projections of installed OCR diagnostics."""
158
162
 
159
- warning: str | None = None
163
+ advisory: OcrToolkitAdvisory | None = None
160
164
  operator_notices: tuple[str, ...] = ()
161
165
 
162
166
 
@@ -441,7 +445,13 @@ def _review_receipt(
441
445
  }
442
446
 
443
447
 
444
- def _dlp_reasons(value: object, *, budgets: TextBudgets, matcher: ForbiddenMatcher) -> Counter[str]:
448
+ def _dlp_reasons(
449
+ value: object,
450
+ *,
451
+ budgets: TextBudgets,
452
+ matcher: ForbiddenMatcher,
453
+ allow_horizontal_tabs: bool = False,
454
+ ) -> Counter[str]:
445
455
  """Count closed DLP failures without retaining hostile strings or locations."""
446
456
 
447
457
  reasons: Counter[str] = Counter()
@@ -459,12 +469,24 @@ def _dlp_reasons(value: object, *, budgets: TextBudgets, matcher: ForbiddenMatch
459
469
  budgets=budgets,
460
470
  publication=True,
461
471
  forbidden_matcher=matcher,
472
+ allow_horizontal_tabs=allow_horizontal_tabs,
462
473
  )
463
474
  if not checked.admitted:
464
475
  reasons[checked.reason] += 1
465
476
  return reasons
466
477
 
467
478
 
479
+ def _code_field_allows_horizontal_tabs(path: tuple[object, ...]) -> bool:
480
+ """Allow HTAB only in the two closed code-bearing finding fields."""
481
+
482
+ return bool(
483
+ len(path) == 3
484
+ and path[0] == "comments"
485
+ and isinstance(path[1], int)
486
+ and path[2] in {"existing_code", "suggestion_code"}
487
+ )
488
+
489
+
468
490
  def _private_dlp_decisions(
469
491
  payload: dict[str, object], *, forbidden: tuple[str, ...]
470
492
  ) -> dict[str, object]:
@@ -527,6 +549,7 @@ def _private_dlp_decisions(
527
549
  budgets=budgets,
528
550
  publication=True,
529
551
  forbidden_matcher=matcher,
552
+ allow_horizontal_tabs=_code_field_allows_horizontal_tabs(path),
530
553
  )
531
554
  if checked.admitted:
532
555
  continue
@@ -582,32 +605,36 @@ def _write_private_dlp_decisions(
582
605
  raise ReviewRunnerError("OCR private DLP diagnostics could not be written") from exc
583
606
 
584
607
 
585
- def _publication_sinks(payload: dict[str, object]) -> list[object]:
608
+ def _publication_sinks(payload: dict[str, object]) -> list[tuple[object, bool]]:
586
609
  """Select only OCR-controlled values that the posting owner can render."""
587
610
 
588
- sinks: list[object] = []
611
+ sinks: list[tuple[object, bool]] = []
589
612
  message = payload.get("message")
590
613
  if message is not None:
591
- sinks.append(message)
614
+ sinks.append((message, False))
592
615
  comments = payload.get("comments")
593
616
  if isinstance(comments, list):
594
- sinks.extend(
595
- {key: value for key, value in item.items() if key in QUARANTINE_COMMENT_FIELDS}
596
- for item in comments
597
- if isinstance(item, dict)
598
- )
617
+ for item in comments:
618
+ if not isinstance(item, dict):
619
+ continue
620
+ sinks.extend(
621
+ (
622
+ value,
623
+ isinstance(value, str) and key in {"existing_code", "suggestion_code"},
624
+ )
625
+ for key, value in item.items()
626
+ if key in QUARANTINE_COMMENT_FIELDS
627
+ )
599
628
  warnings = payload.get("warnings")
600
629
  if warnings is not None:
601
- sinks.append(warnings)
630
+ sinks.append((warnings, False))
602
631
  manifest = payload.get("manifest")
603
632
  coverage = manifest.get("coverage") if isinstance(manifest, dict) else None
604
633
  failed = coverage.get("failed") if isinstance(coverage, dict) else None
605
634
  if isinstance(failed, list):
606
- sinks.extend(
607
- {key: item.get(key) for key in ("path", "reason") if key in item}
608
- for item in failed
609
- if isinstance(item, dict)
610
- )
635
+ for item in failed:
636
+ if isinstance(item, dict):
637
+ sinks.extend((item[key], False) for key in ("path", "reason") if key in item)
611
638
  return sinks
612
639
 
613
640
 
@@ -729,7 +756,14 @@ def _safe_publication_comments(
729
756
  for key, field_value in item.items():
730
757
  if not isinstance(key, str) or key not in QUARANTINE_COMMENT_FIELDS:
731
758
  continue
732
- if _dlp_reasons(field_value, budgets=budgets, matcher=matcher):
759
+ if _dlp_reasons(
760
+ field_value,
761
+ budgets=budgets,
762
+ matcher=matcher,
763
+ allow_horizontal_tabs=(
764
+ isinstance(field_value, str) and key in {"existing_code", "suggestion_code"}
765
+ ),
766
+ ):
733
767
  omitted_fields += 1
734
768
  content_unsafe = content_unsafe or key == "content"
735
769
  continue
@@ -844,8 +878,15 @@ def _publication_projection(
844
878
  budgets = TextBudgets(max_chars=2_000_000, max_bytes=8_000_000, max_lines=100_000)
845
879
  matcher = ForbiddenMatcher.compile(forbidden)
846
880
  sink_reasons: Counter[str] = Counter()
847
- for sink in _publication_sinks(payload):
848
- sink_reasons.update(_dlp_reasons(sink, budgets=budgets, matcher=matcher))
881
+ for sink, allow_horizontal_tabs in _publication_sinks(payload):
882
+ sink_reasons.update(
883
+ _dlp_reasons(
884
+ sink,
885
+ budgets=budgets,
886
+ matcher=matcher,
887
+ allow_horizontal_tabs=allow_horizontal_tabs,
888
+ )
889
+ )
849
890
  sanitized, private_reasons, redacted_fields = _sanitize_nonpublication_fields(
850
891
  payload, budgets=budgets, matcher=matcher
851
892
  )
@@ -871,7 +912,7 @@ def _publication_projection(
871
912
  payload.get("warnings"), budgets=budgets, matcher=matcher
872
913
  )
873
914
  projected: dict[str, object] = {
874
- "status": "completed_with_errors",
915
+ "status": "budget_exceeded" if outcome.budget_exceeded else "completed_with_errors",
875
916
  "message": (
876
917
  "Publication policy produced a safe partial OCR result. Independently safe "
877
918
  "findings may be published, but the result must not be treated as a complete "
@@ -883,6 +924,8 @@ def _publication_projection(
883
924
  payload.get("tool_calls"), allowed_tools=allowed_tools
884
925
  ),
885
926
  }
927
+ if outcome.budget_exceeded:
928
+ projected["summary"] = {"budget_exceeded": True}
886
929
  else:
887
930
  reasons = sink_reasons + private_reasons
888
931
  return (
@@ -924,7 +967,7 @@ def _finalize_ocr_result(
924
967
  evidence_action_counts: dict[str, int] | None = None,
925
968
  *,
926
969
  forbidden: tuple[str, ...],
927
- toolkit_warnings: tuple[str, ...] = (),
970
+ toolkit_advisory: OcrToolkitAdvisory | None = None,
928
971
  ) -> tuple[dict[str, int], bool, dict[str, object]]:
929
972
  """Validate, DLP-project, and receipt-bind one result in one atomic read/replace."""
930
973
 
@@ -935,25 +978,12 @@ def _finalize_ocr_result(
935
978
 
936
979
  def finalize(payload: dict[str, object]) -> dict[str, object]:
937
980
  nonlocal filtered, publication, usage
938
- if TOOLKIT_RESULT_KEY in payload:
939
- raise OcrResultMalformed(f"OCR result contains reserved field {TOOLKIT_RESULT_KEY!r}")
981
+ for reserved in (TOOLKIT_RESULT_KEY, TOOLKIT_ADVISORY_KEY):
982
+ if reserved in payload:
983
+ raise OcrResultMalformed(f"OCR result contains reserved field {reserved!r}")
940
984
  warnings = payload.get("warnings", [])
941
985
  if not isinstance(warnings, list):
942
986
  raise OcrResultMalformed("OCR result warnings must be a list")
943
- warning_texts = {ocr_warning_text(warning) for warning in warnings}
944
- appended_warnings: list[str] = []
945
- for warning in toolkit_warnings:
946
- if warning in warning_texts:
947
- continue
948
- warning_texts.add(warning)
949
- appended_warnings.append(warning)
950
- payload = {
951
- **payload,
952
- "warnings": [
953
- *warnings,
954
- *appended_warnings,
955
- ],
956
- }
957
987
  metadata = _review_receipt(
958
988
  payload,
959
989
  composition,
@@ -969,7 +999,10 @@ def _finalize_ocr_result(
969
999
  mcp = metadata.get("mcp")
970
1000
  raw_usage = mcp.get("usage") if isinstance(mcp, dict) else None
971
1001
  usage = dict(raw_usage) if isinstance(raw_usage, dict) else {}
972
- return {**projected, TOOLKIT_RESULT_KEY: metadata}
1002
+ finalized = {**projected, TOOLKIT_RESULT_KEY: metadata}
1003
+ if toolkit_advisory is not None:
1004
+ finalized[TOOLKIT_ADVISORY_KEY] = toolkit_advisory_payload(toolkit_advisory)
1005
+ return finalized
973
1006
 
974
1007
  try:
975
1008
  transform_ocr_result(result_path, finalize)
@@ -1158,14 +1191,14 @@ def _parse_background_preview(*, returncode: int, stderr: bytes) -> BackgroundQu
1158
1191
  or len(max_tools_matches) > 1
1159
1192
  ):
1160
1193
  raise ReviewRunnerError("OCR background preview returned ambiguous diagnostics")
1161
- warning: str | None = None
1194
+ advisory: OcrToolkitAdvisory | None = None
1162
1195
  if soft_matches:
1163
1196
  actual, limit = (int(value) for value in soft_matches[0].groups())
1164
1197
  if not 0 < limit < actual:
1165
1198
  raise ReviewRunnerError("OCR background preview returned invalid thresholds")
1166
- warning = (
1167
- f"Installed OCR reported a {actual}-character review background above its "
1168
- f"recommended {limit}-character threshold; review continued."
1199
+ advisory = background_recommended_advisory(
1200
+ actual=actual,
1201
+ recommended=limit,
1169
1202
  )
1170
1203
  operator_notices: tuple[str, ...] = ()
1171
1204
  if max_tools_matches:
@@ -1179,7 +1212,7 @@ def _parse_background_preview(*, returncode: int, stderr: bytes) -> BackgroundQu
1179
1212
  "effective tool-call limit.",
1180
1213
  )
1181
1214
  return BackgroundQualification(
1182
- warning=warning,
1215
+ advisory=advisory,
1183
1216
  operator_notices=operator_notices,
1184
1217
  )
1185
1218
  if hard_character is not None:
@@ -1295,9 +1328,11 @@ def _run_background_qualified_review(
1295
1328
  file=sys.stderr,
1296
1329
  )
1297
1330
  return 2, BackgroundQualification()
1298
- if qualification.warning is not None:
1331
+ if qualification.advisory is not None:
1299
1332
  print(
1300
- f"OCR background qualification warning: {qualification.warning}",
1333
+ "OCR core advisory: "
1334
+ f"background {qualification.advisory.actual} characters; recommended "
1335
+ f"{qualification.advisory.recommended} characters; accepted by OCR core",
1301
1336
  file=sys.stderr,
1302
1337
  )
1303
1338
  for notice in qualification.operator_notices:
@@ -1933,11 +1968,7 @@ def run_evidence_review(
1933
1968
  enrichment,
1934
1969
  evidence_action_counts,
1935
1970
  forbidden=forbidden,
1936
- toolkit_warnings=(
1937
- (background_qualification.warning,)
1938
- if background_qualification.warning is not None
1939
- else ()
1940
- ),
1971
+ toolkit_advisory=background_qualification.advisory,
1941
1972
  )
1942
1973
  except ReviewRunnerError:
1943
1974
  try: