Commit Graph
2798 Commits
Author SHA1 Message Date
Chris CoutinhoandClaude Opus 4.8 1c6b1a84ea feat(usage): record per-tenant usage events into the app DB
Deck #67 data-plane slice: tenant Pods record billable operations
(embedding queries, pages/chunks embedded) into an app-DB usage_events
table that the control plane later pulls read-only into the billing
ledger and syncs to Stripe Meter Events.

- migration 007: usage_events table (Postgres TIMESTAMPTZ/JSONB/UUID
  with portable SQLite fallbacks), indexed (occurred_at, metric) for the
  CP rollup's per-day range scan + GROUP BY metric.
- UsageEventStore: best-effort, flag-gated writer reusing the shared
  RefreshTokenStorage engine; ON CONFLICT (event_id) DO NOTHING for
  idempotent retries; dialect-branched occurred_at bind. All work
  (incl. metadata JSON encode) is swallowed so a metering failure never
  surfaces to the user op.
- USAGE_METERING_ENABLED flag (default off) wired through Settings +
  env map; off-path touches no storage, so OSS self-hosters get an empty
  table and zero write overhead.
- two recording hooks: embeddings_queries (per nc_semantic_search, which
  nc_semantic_search_answer reuses) and pages_chunks (after dense
  embedding succeeds, covering both in-process and procrastinate paths).
- storage.acquire()/.dialect public seams so the sibling store doesn't
  reach into the underscored internal.
- tests parametrized over SQLite + Postgres: flag-off no-op, roundtrip,
  ON CONFLICT dedup, JSON/NULL metadata, and the best-effort swallow of
  both DB errors and unserializable metadata.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:13:14 +02:00
github-actions[bot] 42505f8f87 bump: version 0.106.0 → 0.107.0 2026-06-06 12:25:28 +00:00
Chris CoutinhoandGitHub da839d592f Merge pull request #868 from cbcoutinho/feat/page-aware-chunking
feat(vector): page-aware PDF chunking for predictable per-page retrieval
2026-06-06 14:25:02 +02:00
Chris CoutinhoandClaude Opus 4.8 0fada20d35 test(vector): pin empty-chunk-list parity for all-blank pages
Address claude-review round 5 on PR #868: add
test_all_blank_pages_returns_empty_list documenting that PageAwareChunker
returns [] when every page is blank — and asserting parity with
DocumentChunker, which already returns [] for whitespace-only non-empty
content. The empty-chunk-list case is therefore pre-existing pipeline
behavior, not new to this PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:20:03 +02:00
Chris CoutinhoandClaude Opus 4.8 446320983a fix(vector): skip page-assignment span/warning on empty boundaries
Address claude-review round 4 on PR #868: tighten the assign_page_numbers
guard from `page_boundaries is not None` to a truthy check, so a PDF with an
empty boundary list no longer enters the trace span and fires the alarming
"NO page numbers assigned" warning for a harmless no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:13:07 +02:00
Chris CoutinhoandClaude Opus 4.8 20b4bc6ab9 test(vector): mark test_document_chunker as unit
Address claude-review round 3 on PR #868: add module-level
`pytestmark = pytest.mark.unit` so TestPageAwareChunker and
TestDocumentChunkerPositions are collected under `-m unit`, matching
test_processor_routing.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:07:01 +02:00
Chris CoutinhoandClaude Opus 4.8 bb4ef809c2 test(vector): unit-test page-aware routing; clarify fallback comment
Address claude-review round 2 on PR #868:
- Extract the use_page_aware branching into a pure `should_use_page_aware`
  helper and cover the (doc_type, page_boundaries, page_aware_setting) matrix
  in tests/unit/test_processor_routing.py (file+boundaries+enabled, empty
  list, None, non-file doc types, disabled setting).
- Clarify the PageAwareChunker.chunk_text no-boundaries comment: the processor
  pre-filters via should_use_page_aware, so that branch is a direct-call safety
  net, not a production indexing path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:02:05 +02:00
Chris CoutinhoandClaude Opus 4.8 8d20339b3a fix(vector): route empty page_boundaries to char-based path; test ws offsets
Address claude-review round 1 on PR #868:
- use_page_aware now gates on `bool(page_boundaries)` instead of
  `is not None`, so a PDF that yields an empty boundary list takes the
  char-based path explicitly (assign_page_numbers no-ops on []) rather than
  the page-aware chunker's no-boundaries fallback. Same result, clearer intent.
- add test_oversized_page_with_leading_whitespace_offsets, exercising the
  start+start_index offset path for an oversized page whose sub-chunks have
  leading whitespace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:56:30 +02:00
Chris CoutinhoandClaude Opus 4.8 4977216b62 docs: correct chunk-size units (characters, default 2048) in configuration
DOCUMENT_CHUNK_SIZE/OVERLAP were documented as "words" with a 512/50
default; the implementation measures characters and defaults to 2048/200
(config.py, DocumentChunker). Update docs/configuration.md (config block,
tuning guidance, examples, env-var table) and env.sample accordingly, and
cross-reference DOCUMENT_CHUNK_PAGE_AWARE for the PDF path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:46:32 +02:00
Chris CoutinhoandClaude Opus 4.8 2f2a7f9659 feat(vector): page-aware PDF chunking for predictable per-page retrieval
Add PageAwareChunker, which splits paginated documents (PDFs) on page
boundaries first and only character-splits pages larger than chunk_size.
No chunk spans a page boundary, so page_number is always exact and stored
excerpts never lead with a neighbouring page's text. When chunk_size is at
least the largest page, this yields exactly one chunk per page: a
predictable vector count (== page count), a flat per-page embedding cost,
and zero cross-page overlap duplication.

Gated by DOCUMENT_CHUNK_PAGE_AWARE (default true). When false, the legacy
char-based DocumentChunker + post-hoc assign_page_numbers path runs
unchanged. Only doc_type="file" with page_boundaries (PDFs) takes the
page-aware path; notes/deck/news are unaffected.

Measured on a 15-page record (query "leadership award louis", target =
top-half of page 15): char-based degraded the target to dense-rank 10 at
cs=2048 (OCR) and mislabeled its page; page-aware restored rank 1 across
every fusion/modality and chunk size, with correct page labels and clean
snippets.

BREAKING CHANGE: PDFs are re-chunked page-aware by default. Existing
deployments will re-index PDF content on the next vector sync (different
chunk counts and page_number labels). Set DOCUMENT_CHUNK_PAGE_AWARE=false
to retain the previous char-based behaviour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:42:25 +02:00
Chris Coutinho 40e56aeaea build: Bump astrolabe submodule 2026-06-06 09:34:30 +02:00
github-actions[bot] 74b864354f bump: version 0.105.0 → 0.106.0 2026-06-06 07:33:10 +00:00
Chris CoutinhoandGitHub 5863498734 Merge pull request #865 from cbcoutinho/feat/webhook-tag-indexing
feat(vector): index files in real time on vector-index tag changes
2026-06-06 09:32:48 +02:00
Chris CoutinhoandGitHub ad5d54ea99 Merge pull request #866 from cbcoutinho/renovate/nextcloud-33-33.0.5
chore(deps): update nextcloud-33:33.0.5 docker digest to 96f8b6a
2026-06-06 09:13:20 +02:00
Chris CoutinhoandGitHub 2401cd01a9 Merge pull request #867 from cbcoutinho/renovate/anthropics-claude-code-action-1.x
chore(deps): update anthropics/claude-code-action action to v1.0.139
2026-06-06 09:13:10 +02:00
renovate-bot-cbcoutinho[bot]andGitHub fe0ac9d2a9 chore(deps): update anthropics/claude-code-action action to v1.0.139 2026-06-06 04:23:28 +00:00
renovate-bot-cbcoutinho[bot]andGitHub faefd46b49 chore(deps): update nextcloud-33:33.0.5 docker digest to 96f8b6a 2026-06-06 04:23:20 +00:00
Chris CoutinhoandClaude Opus 4.8 1322e5aba0 feat(vector): index files in real time on vector-index tag changes
Tagging an existing file/folder emits only OCP\SystemTag\MapperEvent — never a
Node*Event — so tagged PDFs were previously only picked up by the hourly
scanner. Subscribe to the tag event and reconcile membership so adding/removing
the `vector-index` tag (re)indexes in near-real time.

- webhook_presets: add OCP\SystemTag\MapperEvent to the files_sync preset
  (NC 32+, where MapperEvent gained getWebhookSerializable(); harmless on older
  servers — it just never fires).
- webhook_parser: parse MapperEvent (objectType=files) into a path-less file
  "reconcile" task. The payload carries only a fileid + tagIds (no name/path),
  so assign and unassign both collapse to a reconcile.
- processor._reconcile_tag_event: resolve the fileid against the user's current
  vector-index PDFs (find_files_by_tag). Present -> index with the resolved
  path/etag; absent -> flip to delete. Naturally handles "an unrelated tag
  changed" and a tagged folder's own fileid (no-op; the scanner still expands
  folders to descendants).
- Unit tests for the parser branch and the reconcile.

The matching admin-UI preset change ships separately in the astrolabe app repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 16:20:05 +02:00
github-actions[bot] 78f3f284a6 bump: version 0.104.1 → 0.105.0 2026-06-05 03:22:24 +00:00
Chris CoutinhoandGitHub 55f5473752 Merge pull request #863 from cbcoutinho/feat/ocr-quality-trigger
feat: quality + scan OCR escalation trigger (junk-text-layer scans)
2026-06-05 05:22:03 +02:00
Chris CoutinhoandClaude Opus 4.8 36209e3160 docs(review): note image_heavy only fires when scan detection is on
Address PR #863 round 4: classify_from_text's docstring now states that the
image_heavy flag (and the image-coverage trigger) are only set when
image_coverage is supplied, so the flag reads zero for tenants with
DOCUMENT_OCR_DETECT_SCANNED=false -- self-documenting the metric semantics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 05:16:25 +02:00
Chris CoutinhoandClaude Opus 4.8 820be135fb fix(review): unify "scanned" flag name + log image_coverage length drift
Address PR #863 round 3:

- classify_from_text emits the "scanned" flag (was "no_text_layer") for the
  empty-text-layer case -- same name + meaning as classify_pdf, so
  astrolabe_document_classifier_flag_total isn't split across two labels for the
  same concept (and matches the metric's documented vocab).
- classify_from_text logs at DEBUG when image_coverage length != the expected
  min(pages, MAX_SAMPLED_PAGES), so a 1:1-alignment contract break (extractor
  reorders/skips pages) surfaces instead of silently misattributing coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 05:10:09 +02:00
Chris CoutinhoandClaude Opus 4.8 0287bd9175 fix(review): align classify_pdf routing with the hot path + scan-tail test
Address PR #863 round 2:

- classify_pdf now flags a page needs_ocr on the SAME three signals as
  classify_from_text (image scan OR low text-quality OR near-empty), not image
  coverage alone. Previously a word-merged digital doc with no images routed
  "fast" via classify_pdf but "ocr" via the pipeline -- so an operator
  reproducing routing offline got a different answer. They now match.
- Add a test that when image_coverage is shorter than the page boundaries (the
  MAX_SAMPLED_PAGES cap on large scans), the leading page uses the scan signal
  and later pages fall back to text-quality.

Left as-is: overlong_score (>20) partially overlaps merge_score (>12) -- the
double-penalty on very-long tokens is intentional, not a bug (per review).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 05:01:09 +02:00
Chris CoutinhoandClaude Opus 4.8 fbc9a3a675 fix(review): align quality threshold, cap + DRY scan coverage, warn on failure
Address PR #863 review:

- MIN_TEXT_QUALITY 0.45 -> 0.5 so the module/diagnostic default matches the
  DOCUMENT_OCR_MIN_TEXT_QUALITY setting (registry always passes the setting; this
  keeps classify_pdf and the test/default path on the production threshold).
- image_coverage_per_page is bounded to MAX_SAMPLED_PAGES (the image pass is the
  costly part, so a 200-page scan isn't fully rasterised on the hot path); pages
  beyond the cap fall back to the text-quality signal, and page_fraction still
  gates over every page.
- Extracted _page_image_coverage(page) helper, shared by classify_pdf and
  image_coverage_per_page (DRY + keeps the tiling-double-count note in one place).
- Scan-detection failure logs at WARNING (not DEBUG) so a systematic failure on
  an OCR-enabled tenant is visible at LOG_LEVEL=INFO.
- Add the missing DOCUMENT_OCR_MIN_PAGE_CHARS range-validator test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 04:53:19 +02:00
Chris CoutinhoandClaude Opus 4.8 b1f347b8fc feat: quality + scan OCR escalation trigger (junk-text-layer scans)
The hot-path classifier escalated to OCR purely on character count, so a
scanned/handwritten PDF with a low-quality embedded text layer (>16 chars/page
but garbled) routed `fast` and indexed the junk -- e.g. Student 147.pdf's
"Little Acoms Primary"/"0110912020", which pollutes the vector and demotes the
doc in search (Deck #207).

- classifier: recalibrate `_text_quality` with a long-token-fraction term that
  detects word-merging (dropped inter-word spaces) -- the dominant junk-layer
  failure the old whitespace/overlong(>20) terms missed. Measured: the Student
  147 scan ~0.42 (60% pages junk) vs >=0.94 for clean digital docs.
- classify_from_text now routes on quality + scan: a page is OCR-worthy if
  near-empty OR low text-quality OR (when OCR + scan detection are enabled) it's
  mostly a raster image. New `image_coverage_per_page` re-opens the PDF for the
  scan signal, so that cost is paid only by OCR-opted-in tenants. Thresholds are
  passed in from per-tenant settings (keyword-only).
- config: 4 per-tenant settings -- DOCUMENT_OCR_MIN_TEXT_QUALITY (0.5),
  DOCUMENT_OCR_PAGE_FRACTION (0.5), DOCUMENT_OCR_MIN_PAGE_CHARS (16),
  DOCUMENT_OCR_DETECT_SCANNED (true) -- with range validators.
- metrics: new astrolabe_document_ocr_page_fraction histogram (the value the
  page-fraction threshold acts on) alongside document_text_quality, so operators
  can tune the OCR escalation per tenant (quality vs cost).

Escalation gate, OCR backends, and off-by-default behavior unchanged (#858).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 04:44:15 +02:00
github-actions[bot] d421bf6953 bump: version 0.104.0 → 0.104.1 2026-06-05 01:28:03 +00:00
Chris CoutinhoandGitHub 5277081e22 Merge pull request #862 from cbcoutinho/fix/pypdfium2-page-close-and-coverage
fix: close pypdfium2 page handle on error + cover classifier/OCR edge cases
2026-06-05 03:27:40 +02:00
Chris CoutinhoandClaude Opus 4.8 e68096a780 test(review): correct the junk-layer test comment
The high ocr_frac is driven by each segment being shorter than MIN_PAGE_CHARS
(needs_ocr), not by text quality; quality drives bad_text_layer separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 03:24:13 +02:00
Chris CoutinhoandClaude Opus 4.8 9ffe0645b8 fix: close pypdfium2 page handle on error + cover classifier/OCR edge cases
Follow-up to the tiered document processor (#858), landing the round-4 review
nits the reviewer approved without:

- pypdfium2_fast: free the page handle in an outer finally so a corrupt page
  that makes get_textpage() raise can't orphan it.
- test: classify_from_text junk-text-layer path (non-zero chars, low quality,
  high ocr_frac) flags bad_text_layer -- the hot-path coverage gap.
- test: build_ocr_backend raises ValueError when the gateway M2M client_id is
  set without its token_url/secret.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 03:21:10 +02:00
Chris CoutinhoandGitHub ba3cb575fe Merge pull request #859 from cbcoutinho/renovate/anthropics-claude-code-action-1.x
chore(deps): update anthropics/claude-code-action action to v1.0.136
2026-06-05 02:50:41 +02:00
Chris CoutinhoandGitHub 5fa10558b7 Merge pull request #860 from cbcoutinho/renovate/quay.io-keycloak-keycloak-26.x
chore(deps): update quay.io/keycloak/keycloak docker tag to v26.6.3
2026-06-05 02:50:08 +02:00
Chris CoutinhoandGitHub f859d6b374 Merge pull request #861 from cbcoutinho/renovate/hoverkraft-tech-compose-action-3.x
chore(deps): update hoverkraft-tech/compose-action action to v3
2026-06-05 02:49:34 +02:00
github-actions[bot] 3e681ef2ee bump: version 0.103.0 → 0.104.0 2026-06-05 00:47:32 +00:00
Chris CoutinhoandGitHub c67b998784 Merge pull request #858 from cbcoutinho/feat/tiered-doc-processor-b2-tier1
feat: tiered PDF processor — pypdfium2 fast path (deprecate pymupdf4llm)
2026-06-05 02:43:11 +02:00
renovate-bot-cbcoutinho[bot]andGitHub 157b1bb9d3 chore(deps): update hoverkraft-tech/compose-action action to v3 2026-06-05 00:36:32 +00:00
renovate-bot-cbcoutinho[bot]andGitHub afe6ed6af7 chore(deps): update quay.io/keycloak/keycloak docker tag to v26.6.3 2026-06-05 00:35:55 +00:00
renovate-bot-cbcoutinho[bot]andGitHub c6d36a8e64 chore(deps): update anthropics/claude-code-action action to v1.0.136 2026-06-05 00:35:47 +00:00
Chris CoutinhoandClaude Opus 4.8 bc957b5bc7 fix(review): _enum_fields validation, gate classify_from_text flags, OCR warnings
Address PR #858 review round 3:

- document_tier1_engine / document_ocr_provider now validate + normalize via
  Settings.__post_init__ _enum_fields (the repo's canonical opt-in-enum pattern;
  case-insensitive) instead of dynaconf Validators. A typo now raises ValueError
  at load and "Gateway" normalizes to "gateway".
- classify_from_text gates no_text_layer/bad_text_layer on ocr_frac >=
  OCR_PAGE_FRACTION, matching classify_pdf -- a "fast"-routed doc with a few junk
  pages no longer emits a misleading flag (keeps the shadow vs hot-path
  classification metrics consistent).
- build_ocr_backend warns when an EXPLICIT provider is misconfigured
  (gateway without EMBEDDING_GATEWAY_URL, mistral without MISTRAL_API_KEY)
  instead of silently returning None.
- Pypdfium2FastProcessor.health_check probes the import; documented why
  OcrProcessor.health_check is unconditionally True (lazy per-tenant backends).
- Removed the leftover per-boundary / per-chunk debug logging loops.

Tests: enum normalization + rejection for the two new settings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 02:35:26 +02:00
Chris CoutinhoandClaude Opus 4.8 f1272dfe84 fix(review): lock OCR backend init, warn on rollback fallthrough, zero-page metric
Address PR #858 review round 2:

- OcrProcessor backend resolution is now guarded by an anyio.Lock (lazy-init,
  double-checked) so a burst of concurrent first-OCR calls resolves the backend
  once instead of each fetching its own gateway M2M token.
- The document_tier1_engine=pymupdf rollback now logs a warning when it falls
  back to the fast processor (no 'structured' registered) instead of silently
  using the very engine the operator opted out of.
- classify_from_text defaults ocr_frac to 0.0 (not 1.0) for a zero-page PDF, so
  the recorded classification metric is "fast" (no OCR evidence) rather than a
  misleading "ocr"; the no_text_layer/bad_text_layer flags are gated on having
  sampled at least one page.

New tests: zero-page classify routes fast, rollback-fallback warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 02:24:08 +02:00
Chris CoutinhoandClaude Opus 4.8 1634e8adc2 fix(review): cache OCR backend, drop asserts, real pipeline_tier, guard zero-page
Address PR #858 review:

- 🔴 OcrProcessor now resolves its backend once and reuses it. Rebuilding per
  call created a fresh GatewayTokenProvider each time -- discarding its M2M-token
  cache, so every OCR'd document fetched a new token -- and a new Mistral client.
- 🔴 build_ocr_backend uses explicit ValueError (not assert, which is stripped
  under `python -O`) for the gateway M2M triple.
- PIPELINE_TIER in the Qdrant payload now reflects the tier that actually
  produced the doc: the registry stamps result.metadata["pipeline_tier"] and the
  processor reads it (was hardcoded "fast", wrong for OCR/structured).
- Escalation now requires classification.page_count > 0, so a zero-page
  (empty/corrupt) PDF isn't pointlessly sent to OCR; documented that a fast
  FAILURE (encrypted/unopenable) is a hard failure and is not OCR-escalated.
- Documented the OCR page_boundaries separator-attribution choice.
- Downgraded the per-document page-boundary / page-assignment INFO logs to debug.

New tests: zero-page no-escalation, pipeline_tier stamping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 02:13:09 +02:00
Chris CoutinhoandGitHub cc441ba0db Merge pull request #837 from cbcoutinho/renovate/docker.io-library-mariadb-11.8.8-noble
chore(deps): update docker.io/library/mariadb:11.8.8-noble docker digest to be1ef4f
2026-06-05 01:55:13 +02:00
Chris CoutinhoandGitHub c3684f8558 Merge pull request #838 from cbcoutinho/renovate/actions-checkout-6.x
chore(deps): update actions/checkout action to v6.0.3
2026-06-05 01:55:07 +02:00
Chris CoutinhoandGitHub 1937de7e85 Merge pull request #839 from cbcoutinho/renovate/anthropics-claude-code-action-1.x
chore(deps): update anthropics/claude-code-action action to v1.0.135
2026-06-05 01:54:42 +02:00
Chris CoutinhoandGitHub 574438d3be Merge pull request #843 from cbcoutinho/renovate/docker.io-library-nextcloud-32.x
chore(deps): update docker.io/library/nextcloud docker tag to v32.0.11
2026-06-05 01:54:36 +02:00
Chris CoutinhoandGitHub 08d7a3f2c8 Merge pull request #844 from cbcoutinho/renovate/docker.io-qdrant-qdrant-1.x
chore(deps): update docker.io/qdrant/qdrant docker tag to v1.18.2
2026-06-05 01:54:24 +02:00
Chris CoutinhoandGitHub 153af5ebe1 Merge pull request #845 from cbcoutinho/renovate/ghcr.io-astral-sh-uv-0.x
chore(deps): update ghcr.io/astral-sh/uv docker tag to v0.11.19
2026-06-05 01:54:18 +02:00
Chris CoutinhoandGitHub ec9c6b01f8 Merge pull request #846 from cbcoutinho/renovate/nextcloud-32-32.x
chore(deps): update nextcloud-32 docker tag to v32.0.11
2026-06-05 01:54:11 +02:00
Chris CoutinhoandGitHub 9b4f8eeb14 Merge pull request #847 from cbcoutinho/renovate/nextcloud-33-33.x
chore(deps): update nextcloud-33 docker tag to v33.0.5
2026-06-05 01:54:05 +02:00
Chris CoutinhoandClaude Opus 4.8 4dbf362261 fix: OCR escalation falls back to tier-1 result when OCR can't run
OCR is an enhancement, not a gate. Previously, escalating a scanned doc to the
OCR tier returned the OCR result unconditionally -- so with DOCUMENT_OCR_ENABLED
=true but no backend configured (no gateway URL / no MISTRAL_API_KEY) the OCR
processor returned success=False and the whole document was marked failed and
skipped: strictly worse than leaving OCR off (where it would at least index the
tier-1 text).

Now the registry keeps the tier-1 fast result when the OCR escalation doesn't
succeed (no backend, API down, empty output), logging a warning. A
misconfiguration degrades gracefully instead of dropping scanned docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 01:49:52 +02:00
github-actions[bot] ccff5389b0 bump: version 0.102.0 → 0.103.0 2026-06-04 23:43:48 +00:00