Story 2482: one extraction job

πŸ›°οΈ Why this exists

A retry must not pay Textract twice, and a crash after Textract accepts the job must not lose the job identity. The durable baton is the claim ticket: one firm, document, and extraction version can own only one provider job.

✈️ The fixture-only path

direct fixture invoke
  β†’ reserve unique DB baton
  β†’ StartDocumentAnalysis(LAYOUT, TABLES, SIGNATURES)
  β†’ SNS β†’ completion SQS
  β†’ paginated GetDocumentAnalysis
  β†’ one exact JSON object per response page in derived/extraction-record/
  β†’ baton = succeeded

normal upload β†’ existing detect_document_text serving path (unchanged)
The trap avoided. The baton is committed before the provider call. Its stable request token makes retries converge on the same Textract JobId; the database unique key makes concurrent callers converge on the same baton. Completion writes are conditional and deterministic; a conflict is accepted only after the existing page's provider-content digest matches. The baton checkpoints the next token and page number, so a retry resumes instead of replaying an entire large document.

πŸ”¬ What is durable

SurfaceWhy
ops.document_extraction_jobsFirm-scoped RLS baton with a unique document/version start identity, durable pagination cursor, and honest terminal status.
derived/extraction-record/<version>/page-N.jsonEvery paginated provider mapping is stored without flattening, merging, or projecting its blocks.
Existing extraction LambdaInfra IAM restricts Start/Get to this function; a second function would be denied.
No billing eventRFD I-10 bills only full INTAKE-package success, never extraction persistence.

Proof

Known live boundary gap. Infra PR #153 created and applied the topic, queue, role, IAM, and outputs, but not the queue-to-Lambda event-source mapping. The live fleet audit is red on β€œenqueue-to-nowhere.” Functions deliberately does not create that Terraform-owned trigger; the infra twin must land before this rail is live.

🎯 Recall

One durable claim ticket makes every retry and concurrent caller converge on one paid Textract job, while the old serving path stays untouched.