open-code-review-toolkit 0.8.2__tar.gz → 0.8.3__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.3}/PKG-INFO +1 -1
  2. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/_version.py +2 -2
  3. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/ocr_result.py +17 -0
  4. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/posting/formatting.py +43 -54
  5. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/posting/settings.py +0 -6
  6. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/review_runner.py +56 -58
  7. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/.gitignore +0 -0
  8. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/LICENSE +0 -0
  9. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/README.md +0 -0
  10. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/pyproject.toml +0 -0
  11. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/__init__.py +0 -0
  12. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/cli.py +0 -0
  13. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/common/__init__.py +0 -0
  14. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/common/filesystem.py +0 -0
  15. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/common/git.py +0 -0
  16. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/common/language.py +0 -0
  17. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/common/markdown.py +0 -0
  18. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/common/redaction.py +0 -0
  19. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/config_writer.py +0 -0
  20. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/configure.py +0 -0
  21. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/context/__init__.py +0 -0
  22. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/context/adapters.py +0 -0
  23. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/context/broker.py +0 -0
  24. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/context/contracts.py +0 -0
  25. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/context/dlp.py +0 -0
  26. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/context/mcp.py +0 -0
  27. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/context/policy.py +0 -0
  28. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/context/recognizers.py +0 -0
  29. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/context/store.py +0 -0
  30. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/__init__.py +0 -0
  31. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/__main__.py +0 -0
  32. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/actions.py +0 -0
  33. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/artifacts.py +0 -0
  34. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/categorize.py +0 -0
  35. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/collect.py +0 -0
  36. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/collectors/__init__.py +0 -0
  37. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/collectors/graphs.py +0 -0
  38. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/collectors/orchestration.py +0 -0
  39. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/collectors/projections.py +0 -0
  40. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/collectors/registry.py +0 -0
  41. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/collectors/sources.py +0 -0
  42. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/coverage.py +0 -0
  43. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/ecosystems/__init__.py +0 -0
  44. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/ecosystems/ansible/__init__.py +0 -0
  45. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/ecosystems/ansible/requirements.py +0 -0
  46. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/ecosystems/ansible/topology.py +0 -0
  47. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/ecosystems/contracts.py +0 -0
  48. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/ecosystems/go.py +0 -0
  49. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/ecosystems/javascript.py +0 -0
  50. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/ecosystems/php.py +0 -0
  51. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/ecosystems/python.py +0 -0
  52. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/frameworks/__init__.py +0 -0
  53. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/frameworks/contracts.py +0 -0
  54. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/frameworks/detection.py +0 -0
  55. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/frameworks/providers/__init__.py +0 -0
  56. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/frameworks/providers/frontend.py +0 -0
  57. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/frameworks/providers/go.py +0 -0
  58. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/frameworks/providers/php.py +0 -0
  59. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/frameworks/providers/python.py +0 -0
  60. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/frameworks/registry.py +0 -0
  61. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/frameworks/schema.py +0 -0
  62. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/frameworks/templates.py +0 -0
  63. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/infrastructure.py +0 -0
  64. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/invocation.py +0 -0
  65. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/mcp.py +0 -0
  66. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/model.py +0 -0
  67. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/policy/__init__.py +0 -0
  68. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/policy/contracts.py +0 -0
  69. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/policy/decisions.py +0 -0
  70. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/policy/guidance.py +0 -0
  71. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/policy/registry.py +0 -0
  72. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/policy/schema.py +0 -0
  73. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/policy/scopes.py +0 -0
  74. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/project.py +0 -0
  75. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/repository.py +0 -0
  76. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/review_context.py +0 -0
  77. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/store/__init__.py +0 -0
  78. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/store/atomic.py +0 -0
  79. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/store/contracts.py +0 -0
  80. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/store/core.py +0 -0
  81. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/store/readback.py +0 -0
  82. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/evidence/store/values.py +0 -0
  83. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/mcp_config.py +0 -0
  84. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/posting/__init__.py +0 -0
  85. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/posting/__main__.py +0 -0
  86. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/posting/approval.py +0 -0
  87. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/posting/comments.py +0 -0
  88. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/posting/gitlab.py +0 -0
  89. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/posting/gitlab_approval.py +0 -0
  90. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/posting/markers.py +0 -0
  91. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/posting/payloads.py +0 -0
  92. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/posting/reconciliation.py +0 -0
  93. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/posting/result.py +0 -0
  94. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/posting/snapshot.py +0 -0
  95. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/posting/suggestions.py +0 -0
  96. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/posting/transaction.py +0 -0
  97. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/posting/workflow.py +0 -0
  98. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/pre_execution.py +0 -0
  99. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/preflight.py +0 -0
  100. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/provider_config.py +0 -0
  101. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/provider_failure.py +0 -0
  102. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/providers/__init__.py +0 -0
  103. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/providers/gitlab.py +0 -0
  104. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/providers/gitlab_context.py +0 -0
  105. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/providers/gitlab_discussions.py +0 -0
  106. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/providers/gitlab_identity.py +0 -0
  107. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/providers/gitlab_remediation.py +0 -0
  108. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/py.typed +0 -0
  109. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/src/ocr_toolkit/result_contract.py +0 -0
  110. {open_code_review_toolkit-0.8.2 → open_code_review_toolkit-0.8.3}/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.3
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
@@ -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.3'
22
+ __version_tuple__ = version_tuple = (0, 8, 3)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -27,6 +27,23 @@ MAX_TOOLKIT_MCP_TOOL_NAME_CHARS = 4_096
27
27
  MAX_TOOLKIT_MCP_USAGE_COUNT = 1_000_000_000
28
28
  TOOLKIT_MCP_SERVER_NAME_RE = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
29
29
 
30
+ # Closed names whose numeric call counts may appear in the public review summary.
31
+ # Dynamic external MCP tools remain represented by the receipt's verified
32
+ # per-server aggregate so provider-controlled names do not cross into GitLab.
33
+ PUBLIC_REVIEW_TOOL_CALL_NAMES = frozenset(
34
+ {
35
+ "code_comment",
36
+ "code_search",
37
+ "context_get",
38
+ "context_list",
39
+ "file_find",
40
+ "file_read",
41
+ "file_read_diff",
42
+ "ocr_toolkit_evidence",
43
+ "task_done",
44
+ }
45
+ )
46
+
30
47
 
31
48
  class OcrResultMissing(Exception):
32
49
  """The OCR result artifact is missing or unreadable on disk."""
@@ -20,6 +20,7 @@ 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,
25
26
  )
@@ -45,8 +46,6 @@ from ocr_toolkit.posting.settings import (
45
46
  MAX_REVIEWER_GUIDE_LABEL_CHARS,
46
47
  MAX_REVIEWER_GUIDE_LOCATION_CHARS,
47
48
  MAX_REVIEWER_GUIDE_TEXT_CHARS,
48
- MAX_TOOL_CALL_NAME_CHARS,
49
- MAX_TOOL_CALL_SUMMARY_TOOLS,
50
49
  SUGGESTION_HEADER,
51
50
  post_badges,
52
51
  post_emoji,
@@ -374,32 +373,25 @@ def nonnegative_int(value: Any) -> int | None:
374
373
  return None
375
374
 
376
375
 
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
376
  def tool_call_name(value: Any) -> str:
387
- """Extract a displayable tool name from common OCR tool-call shapes."""
377
+ """Extract one closed public tool name from common OCR call shapes."""
388
378
 
389
379
  if isinstance(value, str):
390
- return clean_text(value)
380
+ name = clean_text(value)
381
+ return name if name in PUBLIC_REVIEW_TOOL_CALL_NAMES else ""
391
382
 
392
383
  if not isinstance(value, dict):
393
384
  return ""
394
385
 
395
386
  for key in ("name", "tool", "tool_name"):
396
387
  name = clean_text(value.get(key))
397
- if name:
388
+ if name in PUBLIC_REVIEW_TOOL_CALL_NAMES:
398
389
  return name
399
390
 
400
391
  function_value = value.get("function")
401
392
  if isinstance(function_value, dict):
402
- return clean_text(function_value.get("name"))
393
+ name = clean_text(function_value.get("name"))
394
+ return name if name in PUBLIC_REVIEW_TOOL_CALL_NAMES else ""
403
395
 
404
396
  return ""
405
397
 
@@ -407,14 +399,17 @@ def tool_call_name(value: Any) -> str:
407
399
  def tool_call_counts_from_items(
408
400
  items: list[Any],
409
401
  ) -> tuple[int | None, list[tuple[str, int]]]:
410
- """Summarize a list-style OCR tool_calls payload."""
402
+ """Summarize admitted calls from a legacy list-style OCR payload."""
411
403
 
412
404
  counts: dict[str, int] = {}
413
405
  for item in items:
414
406
  name = tool_call_name(item)
415
407
  if not name:
416
408
  continue
417
- counts[name] = counts.get(name, 0) + 1
409
+ count = counts.get(name, 0) + 1
410
+ if count > MAX_TOOLKIT_MCP_USAGE_COUNT:
411
+ return None, []
412
+ counts[name] = count
418
413
 
419
414
  total = sum(counts.values())
420
415
  if total == 0 and items:
@@ -424,51 +419,52 @@ def tool_call_counts_from_items(
424
419
 
425
420
 
426
421
  def format_tool_calls_summary(tool_calls: Any) -> str:
427
- """Return one bounded MR summary line for OCR tool-call statistics."""
422
+ """Return one bounded MR line for admitted non-zero OCR tool counts."""
428
423
 
429
424
  entries: list[tuple[str, int]]
430
425
  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):
426
+ if isinstance(tool_calls, list):
436
427
  total, entries = tool_call_counts_from_items(tool_calls)
437
428
  elif isinstance(tool_calls, dict):
438
429
  by_tool_value = tool_calls.get("by_tool")
439
430
  entries = []
440
- by_tool_total = 0
441
- valid_by_tool_count = False
431
+ admitted_total = 0
442
432
 
443
433
  if isinstance(by_tool_value, dict):
444
434
  for raw_name, raw_count in by_tool_value.items():
445
- count = nonnegative_int(raw_count)
446
- if count is None:
435
+ if not isinstance(raw_name, str) or raw_name not in PUBLIC_REVIEW_TOOL_CALL_NAMES:
447
436
  continue
448
- name = clean_text(raw_name)
449
- if not name:
437
+ if (
438
+ not isinstance(raw_count, int)
439
+ or isinstance(raw_count, bool)
440
+ or not 0 < raw_count <= MAX_TOOLKIT_MCP_USAGE_COUNT
441
+ ):
450
442
  continue
451
- valid_by_tool_count = True
452
- by_tool_total += count
453
- if count > 0:
454
- entries.append((name, count))
443
+ admitted_total += raw_count
444
+ if admitted_total > MAX_TOOLKIT_MCP_USAGE_COUNT:
445
+ return ""
446
+ entries.append((raw_name, raw_count))
455
447
 
456
448
  calls_value = tool_calls.get("calls")
457
- if valid_by_tool_count:
458
- list_total = None
459
- elif isinstance(calls_value, list):
449
+ if not by_tool_value and isinstance(calls_value, list):
460
450
  list_total, entries = tool_call_counts_from_items(calls_value)
461
451
  else:
462
452
  list_total = None
463
453
 
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:
454
+ if "total" in tool_calls:
455
+ raw_total = tool_calls["total"]
456
+ if (
457
+ not isinstance(raw_total, int)
458
+ or isinstance(raw_total, bool)
459
+ or not 0 < raw_total <= MAX_TOOLKIT_MCP_USAGE_COUNT
460
+ ):
461
+ return ""
462
+ total = raw_total
463
+ else:
464
+ if list_total is not None:
469
465
  total = list_total
470
- elif by_tool_value == {}:
471
- total = 0
466
+ elif entries:
467
+ total = admitted_total
472
468
  else:
473
469
  return ""
474
470
  else:
@@ -476,21 +472,14 @@ def format_tool_calls_summary(tool_calls: Any) -> str:
476
472
 
477
473
  if total is None:
478
474
  return ""
479
- if total == 0:
475
+ if total == 0 or not entries:
476
+ return ""
477
+ if sum(count for _name, count in entries) > total:
480
478
  return ""
481
479
 
482
480
  line = f"- all OCR tool calls: {total} total"
483
- if not entries:
484
- return line
485
-
486
481
  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")
482
+ detail_parts = [f"{inline_code(name)}: {count}" for name, count in entries]
494
483
 
495
484
  return f"{line} ({', '.join(detail_parts)})"
496
485
 
@@ -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
 
@@ -75,6 +75,7 @@ from ocr_toolkit.evidence.review_context import (
75
75
  from ocr_toolkit.evidence.store import EvidenceStore, EvidenceStoreError
76
76
  from ocr_toolkit.ocr_result import (
77
77
  MAX_TOOLKIT_MCP_USAGE_COUNT,
78
+ PUBLIC_REVIEW_TOOL_CALL_NAMES,
78
79
  TOOLKIT_RESULT_KEY,
79
80
  TOOLKIT_RESULT_SCHEMA_VERSION,
80
81
  OcrResultMalformed,
@@ -117,6 +118,10 @@ BACKGROUND_SOFT_WARNING_RE = re.compile(
117
118
  r"\[ocr\] --background-file content is ([0-9]{1,12}) characters, exceeding the "
118
119
  r"recommended ([0-9]{1,12}) \(continuing but review quality might be impacted\)\n?\Z"
119
120
  )
121
+ MAX_TOOLS_NORMALIZATION_RE = re.compile(
122
+ r"\[ocr\] --max-tools ([0-9]{1,12}) is below minimum ([0-9]{1,12}), "
123
+ r"using ([0-9]{1,12})\n?\Z"
124
+ )
120
125
  BACKGROUND_HARD_CHARACTER_RE = re.compile(
121
126
  r"Error: background content is ([0-9]{1,12}) characters, exceeding the hard limit of "
122
127
  r"([0-9]{1,12}) \(aborting\)\n?\Z"
@@ -149,9 +154,10 @@ class ReviewRunnerError(Exception):
149
154
 
150
155
  @dataclass(frozen=True, slots=True)
151
156
  class BackgroundQualification:
152
- """Carry one toolkit-authored warning derived from installed OCR output."""
157
+ """Carry closed toolkit-authored projections of installed OCR diagnostics."""
153
158
 
154
159
  warning: str | None = None
160
+ operator_notices: tuple[str, ...] = ()
155
161
 
156
162
 
157
163
  class BackgroundQualificationRejected(ReviewRunnerError):
@@ -593,22 +599,6 @@ def _publication_sinks(payload: dict[str, object]) -> list[object]:
593
599
  warnings = payload.get("warnings")
594
600
  if warnings is not None:
595
601
  sinks.append(warnings)
596
- tool_calls = payload.get("tool_calls")
597
- if isinstance(tool_calls, list):
598
- for item in tool_calls:
599
- if not isinstance(item, dict):
600
- continue
601
- sinks.extend(item.get(key) for key in ("name", "tool", "tool_name") if key in item)
602
- function = item.get("function")
603
- if isinstance(function, dict) and "name" in function:
604
- sinks.append(function.get("name"))
605
- elif isinstance(tool_calls, dict):
606
- by_tool = tool_calls.get("by_tool")
607
- if isinstance(by_tool, dict):
608
- sinks.extend(by_tool)
609
- calls = tool_calls.get("calls")
610
- if isinstance(calls, list):
611
- sinks.extend(_publication_sinks({"tool_calls": calls}))
612
602
  manifest = payload.get("manifest")
613
603
  coverage = manifest.get("coverage") if isinstance(manifest, dict) else None
614
604
  failed = coverage.get("failed") if isinstance(coverage, dict) else None
@@ -633,23 +623,6 @@ def _is_publication_sink_path(path: tuple[object, ...]) -> bool:
633
623
  and path[2] in QUARANTINE_COMMENT_FIELDS
634
624
  ):
635
625
  return True
636
- if len(path) == 3 and path[:2] == ("tool_calls", "by_tool"):
637
- return True
638
- if (
639
- len(path) == 4
640
- and path[0] == "tool_calls"
641
- and isinstance(path[1], int)
642
- and path[2] == "function"
643
- and path[3] == "name"
644
- ):
645
- return True
646
- if (
647
- len(path) == 3
648
- and path[0] == "tool_calls"
649
- and isinstance(path[1], int)
650
- and path[2] in {"name", "tool", "tool_name"}
651
- ):
652
- return True
653
626
  return bool(
654
627
  len(path) >= 5
655
628
  and path[:3] == ("manifest", "coverage", "failed")
@@ -658,6 +631,16 @@ def _is_publication_sink_path(path: tuple[object, ...]) -> bool:
658
631
  )
659
632
 
660
633
 
634
+ def _is_static_public_tool_key_path(path: tuple[object, ...]) -> bool:
635
+ """Return whether a map key is one compile-time public tool label."""
636
+
637
+ return bool(
638
+ len(path) == 3
639
+ and path[:2] == ("tool_calls", "by_tool")
640
+ and path[2] in PUBLIC_REVIEW_TOOL_CALL_NAMES
641
+ )
642
+
643
+
661
644
  def _sanitize_nonpublication_fields(
662
645
  payload: dict[str, object], *, budgets: TextBudgets, matcher: ForbiddenMatcher
663
646
  ) -> tuple[dict[str, object], Counter[str], int]:
@@ -679,7 +662,9 @@ def _sanitize_nonpublication_fields(
679
662
  child_path = (*path, key)
680
663
  if isinstance(source, dict):
681
664
  assert isinstance(key, str)
682
- if not _is_publication_sink_path(child_path):
665
+ if not _is_publication_sink_path(
666
+ child_path
667
+ ) and not _is_static_public_tool_key_path(child_path):
683
668
  checked_key = check_text(
684
669
  key,
685
670
  budgets=budgets,
@@ -783,6 +768,7 @@ def _closed_tool_calls(value: object, *, allowed_tools: frozenset[str]) -> dict[
783
768
  tool: count
784
769
  for tool, count in by_tool.items()
785
770
  if tool in allowed_tools
771
+ and tool in PUBLIC_REVIEW_TOOL_CALL_NAMES
786
772
  and isinstance(count, int)
787
773
  and not isinstance(count, bool)
788
774
  and 0 < count <= MAX_TOOLKIT_MCP_USAGE_COUNT
@@ -809,17 +795,9 @@ def _canonical_result_projection(payload: dict[str, object]) -> bytes:
809
795
  else comment
810
796
  for comment in comments
811
797
  ]
812
- tool_calls = payload.get("tool_calls")
813
- projected_tool_calls: object
814
- if isinstance(tool_calls, dict):
815
- total = tool_calls.get("total")
816
- by_tool = tool_calls.get("by_tool")
817
- projected_tool_calls = {
818
- "total": total,
819
- "by_tool": (dict(sorted(by_tool.items())) if isinstance(by_tool, dict) else by_tool),
820
- }
821
- else:
822
- projected_tool_calls = tool_calls
798
+ projected_tool_calls = _closed_tool_calls(
799
+ payload.get("tool_calls"), allowed_tools=PUBLIC_REVIEW_TOOL_CALL_NAMES
800
+ )
823
801
  projection = {
824
802
  "outcome": {
825
803
  "status": outcome.status,
@@ -953,9 +931,7 @@ def _finalize_ocr_result(
953
931
  filtered = False
954
932
  publication: dict[str, object] = {"state": "passed"}
955
933
  usage: dict[str, int] = {}
956
- allowed_tools = frozenset(
957
- tool for capability in composition.capabilities for tool in capability.tools
958
- )
934
+ allowed_tools = PUBLIC_REVIEW_TOOL_CALL_NAMES
959
935
 
960
936
  def finalize(payload: dict[str, object]) -> dict[str, object]:
961
937
  nonlocal filtered, publication, usage
@@ -1160,7 +1136,7 @@ def _read_bounded_artifact(path: Path, *, limit: int, label: str) -> bytes:
1160
1136
 
1161
1137
 
1162
1138
  def _parse_background_preview(*, returncode: int, stderr: bytes) -> BackgroundQualification:
1163
- """Classify only exact installed-OCR background warning and rejection forms."""
1139
+ """Classify only exact installed-OCR preview diagnostics consumed by the toolkit."""
1164
1140
 
1165
1141
  try:
1166
1142
  text = stderr.decode("utf-8", errors="strict")
@@ -1170,21 +1146,41 @@ def _parse_background_preview(*, returncode: int, stderr: bytes) -> BackgroundQu
1170
1146
  soft_matches = [
1171
1147
  match for line in lines if (match := BACKGROUND_SOFT_WARNING_RE.fullmatch(line)) is not None
1172
1148
  ]
1149
+ max_tools_matches = [
1150
+ match for line in lines if (match := MAX_TOOLS_NORMALIZATION_RE.fullmatch(line)) is not None
1151
+ ]
1173
1152
  hard_character = BACKGROUND_HARD_CHARACTER_RE.fullmatch(text)
1174
1153
  hard_file = BACKGROUND_HARD_FILE_RE.fullmatch(text)
1175
1154
  if returncode == 0:
1176
- if len(soft_matches) != len(lines) or len(soft_matches) > 1:
1155
+ if (
1156
+ len(soft_matches) + len(max_tools_matches) != len(lines)
1157
+ or len(soft_matches) > 1
1158
+ or len(max_tools_matches) > 1
1159
+ ):
1177
1160
  raise ReviewRunnerError("OCR background preview returned ambiguous diagnostics")
1178
- if not soft_matches:
1179
- return BackgroundQualification()
1180
- actual, limit = (int(value) for value in soft_matches[0].groups())
1181
- if not 0 < limit < actual:
1182
- raise ReviewRunnerError("OCR background preview returned invalid thresholds")
1183
- return BackgroundQualification(
1184
- warning=(
1161
+ warning: str | None = None
1162
+ if soft_matches:
1163
+ actual, limit = (int(value) for value in soft_matches[0].groups())
1164
+ if not 0 < limit < actual:
1165
+ raise ReviewRunnerError("OCR background preview returned invalid thresholds")
1166
+ warning = (
1185
1167
  f"Installed OCR reported a {actual}-character review background above its "
1186
1168
  f"recommended {limit}-character threshold; review continued."
1187
1169
  )
1170
+ operator_notices: tuple[str, ...] = ()
1171
+ if max_tools_matches:
1172
+ requested, minimum, normalized = (int(value) for value in max_tools_matches[0].groups())
1173
+ if not 0 < requested < minimum or normalized != minimum:
1174
+ raise ReviewRunnerError("OCR background preview returned invalid tool limits")
1175
+ operator_notices = (
1176
+ "Installed OCR reported "
1177
+ f"--max-tools {requested} below its CLI minimum and a normalization target "
1178
+ f"of {normalized}; the installed OCR template remains authoritative for the "
1179
+ "effective tool-call limit.",
1180
+ )
1181
+ return BackgroundQualification(
1182
+ warning=warning,
1183
+ operator_notices=operator_notices,
1188
1184
  )
1189
1185
  if hard_character is not None:
1190
1186
  actual, limit = (int(value) for value in hard_character.groups())
@@ -1304,6 +1300,8 @@ def _run_background_qualified_review(
1304
1300
  f"OCR background qualification warning: {qualification.warning}",
1305
1301
  file=sys.stderr,
1306
1302
  )
1303
+ for notice in qualification.operator_notices:
1304
+ print(f"OCR argument qualification notice: {notice}", file=sys.stderr)
1307
1305
  return (
1308
1306
  run_review(
1309
1307
  result_path,