Record why a document's index attempt failed and say so

A content-index attempt that failed (raster failure, a scan with no
text layer, a provider error) recorded its reason only in the server's
observability stream. In the vault, hasContentIndex false looked
identical for a document nobody ever tried and one that failed three
times, and the detail sheet had nothing to say about either.

Apply the summaryState pattern to the index. Two additive columns on
InboundDocument (migration 0333) record when the last attempt finished
and why it produced no index, as a closed reason set: local-empty,
local-unsupported, decrypt-error, raster-failed, provider-error,
pdf-needs-anthropic, empty-transcription. The writer sits at the choke
point the auto job and the corpus backfill share, and in the manual
index route; a success clears the reason. The provider path's specific
failure now survives the local fallback, since "no text" is less
actionable than "the PDF would not render". rasterizePdf names its
failure class so a build that cannot rasterize at all is distinguished
from a PDF that would not render. New guard in both index paths: an
empty provider transcription never becomes a successful empty index.

The detail sheet renders the reason in plain words (muted, six
locales). The DTO and OpenAPI carry the two fields; the disaster
recovery backup and restore carry them verbatim like their summary
siblings.

Refs #776
This commit is contained in:
Marc-André Bombeck committed 2026-08-13 12:40:50 +02:00
1 parent 45dc93932a
commit 893db887e9
29 files changed
+786 -33

No files matched your search

@@ -10,3 +10,13 @@
UPDATE "inbound_documents"
SET "summary_state" = 'NONE'
WHERE "summary_state" = 'PENDING';
-- Refs #776 — record what became of a document's content-index attempt.
-- Additive, nullable, no backfill: existing rows read NULL ("no attempt
-- recorded"), which is exactly true for them — outcomes only exist from the
-- release that writes them. `last_index_outcome` is a closed string enum
-- enforced in code (see `DocumentIndexOutcomeValue`); NULL after a
-- successful attempt, since the `document_content_index` row itself is the
-- success signal.
ALTER TABLE "inbound_documents" ADD COLUMN "last_index_attempt_at" TIMESTAMP(3);
ALTER TABLE "inbound_documents" ADD COLUMN "last_index_outcome" TEXT;
+16
View File
@@ -7536,6 +7536,22 @@ model InboundDocument {
/// and one mid-flight all leave it null. NONE is the default, which keeps
/// every pre-existing row correct — none of them were ever attempted.
summaryState DocumentSummaryState @default(NONE) @map("summary_state")
/// When the most recent content-index attempt finished (auto job, corpus
/// backfill, or the manual index route — they share one recorder),
/// successful or not. Null until any attempt ran. Diagnostic state the
/// detail view pairs with `lastIndexOutcome`; carried verbatim in the
/// disaster-recovery backup like its summary siblings.
lastIndexAttemptAt DateTime? @map("last_index_attempt_at")
/// Why the most recent index attempt produced NO index — a closed string
/// enum (`DocumentIndexOutcomeValue` in
/// `src/lib/validations/inbound-documents.ts`): local-empty |
/// local-unsupported | decrypt-error | raster-failed | provider-error |
/// pdf-needs-anthropic | empty-transcription. Null after a successful
/// attempt (the `DocumentContentIndex` row itself is the success signal)
/// and before the first attempt. Lets the detail view tell "never tried"
/// from "tried and here is why it failed" instead of showing a bare
/// not-indexed state for both.
lastIndexOutcome String? @map("last_index_outcome")
facts ExtractedFact[]
conditionLinks DocumentConditionLink[]