From d05cbc0dc15fdaf69611e4a3bff7f636adfd76a0 Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Thu, 18 Jun 2026 01:02:32 +0200 Subject: [PATCH] docs(ingest): refresh BatchPending + classifier-vocab docs; add 4-rung suppressed test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address claude-review round 5 on #922 (verdict: good to merge after the docstring): - BatchPending docstring: the deferred job stays on its own `(ocr-upstream)` tier queue, not the retired `(ocr)` — batch mode is the upstream Mistral path only. - classifier.py: clarify that `recommended_tier == "ocr"` is the classifier's COARSE vocabulary ("needs OCR"), resolved to a concrete rung (ocr-incluster -> ocr-upstream) by the registry — NOT a TIER_LADDER tier name. - Added test_evaluate_escalation_suppressed_targets_incluster_four_rung: with both OCR rungs registered but both flags off, the suppressed what-if-OCR signal names the cheapest ideal rung (ocr-incluster), not ocr-upstream. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../document_processors/classifier.py | 5 +++- .../document_processors/escalation.py | 3 ++- tests/unit/test_registry_tiering.py | 25 +++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/nextcloud_mcp_server/document_processors/classifier.py b/nextcloud_mcp_server/document_processors/classifier.py index ecf76877..878c7d22 100644 --- a/nextcloud_mcp_server/document_processors/classifier.py +++ b/nextcloud_mcp_server/document_processors/classifier.py @@ -89,7 +89,10 @@ class DocClassification: total_chars: int mean_text_quality: float ocr_page_fraction: float # fraction of sampled pages flagged needs_ocr - recommended_tier: str # "fast" | "structured" | "ocr" + # Classifier vocabulary (coarse): "fast" | "structured" | "ocr". "ocr" means + # "needs OCR" — the registry resolves it to a concrete rung (ocr-incluster -> + # ocr-upstream) via next_available_tier; it is NOT the TIER_LADDER tier name. + recommended_tier: str mean_control_ratio: float = 0.0 # doc-level C0-control-char ratio (glyph-leak) flags: set[str] = field( default_factory=set diff --git a/nextcloud_mcp_server/document_processors/escalation.py b/nextcloud_mcp_server/document_processors/escalation.py index e6c771b4..ed6de863 100644 --- a/nextcloud_mcp_server/document_processors/escalation.py +++ b/nextcloud_mcp_server/document_processors/escalation.py @@ -144,7 +144,8 @@ class BatchPending(Exception): ``except Exception`` on the indexing path, never counted as a drop/parse error, and never marks the placeholder failed (the doc isn't done yet). Unlike ``EscalateError`` it does NOT change queue — the job stays on its own - (``ocr``) tier queue and is simply deferred. + (``ocr-upstream``) tier queue and is simply deferred (batch mode is the + upstream Mistral path only; the in-cluster rung is synchronous). """ def __init__(self, *, retry_in: int) -> None: diff --git a/tests/unit/test_registry_tiering.py b/tests/unit/test_registry_tiering.py index af7e28fb..c9610619 100644 --- a/tests/unit/test_registry_tiering.py +++ b/tests/unit/test_registry_tiering.py @@ -659,6 +659,31 @@ def test_evaluate_escalation_suppressed_when_ocr_disabled(monkeypatch): assert decision == EscalationDecision("suppressed", "ocr-upstream", "empty_text") +def test_evaluate_escalation_suppressed_targets_incluster_four_rung(monkeypatch): + """Four-rung registry, BOTH OCR flags off: the suppressed what-if-OCR signal + names the *cheapest* ideal rung (ocr-incluster), not ocr-upstream, since the + ideal-target walk (ignore_ocr_enabled) picks the cheapest registered OCR rung.""" + monkeypatch.setattr(reg_mod, "record_document_classification", MagicMock()) + r = _registry( + (_Fake("fast", "fast"), 20), + (_Fake("structured", "structured"), 10), + (_Fake("ocr-incluster", "ocr-incluster"), 6), + (_Fake("ocr-upstream", "ocr-upstream"), 5), + ) + res = ProcessingResult( + text="", + metadata={ + "page_count": 1, + "page_boundaries": [{"page": 1, "start_offset": 0, "end_offset": 0}], + }, + processor="fast", + ) + decision = r.evaluate_escalation( + res, b"%PDF", "fast", _Settings(ocr=False, ocr_incluster=False) + ) + assert decision == EscalationDecision("suppressed", "ocr-incluster", "empty_text") + + def test_evaluate_escalation_lowconf_suppressed_when_only_ocr_disabled(monkeypatch): """fast+ocr only, OCR off, junk text: the next rung is the disabled ocr, so the would-be hop is suppressed (not a structured hop, which isn't registered)."""