Application map / Document ingestion

How Document Ingestion Works

The reserve-study pipeline: upload, extraction, ops review, reconciliation, release, and the customer archive decision.

Status: built and merged, not activated in production verified 2026-09-07

The application and engine code are merged and the v2 API surface is deployed, but production is not presenting active customer intake: the emergency pause flag is on, zero documents exist, and no property has ever recorded a v2 cutover. What activation still requires is in the activation runbook. Everything below describes the system as built and specified.

What it does

An authorized user selects a property, uploads its latest reserve-study PDF, and can leave the page. Gallopify durably tracks the document, sends it to the extraction service, gives an internal operator a real-source review and reconciliation workflow, applies the approved study to the property's planner, and asks the customer to decide whether projects absent from the new study should be archived.

V1 extracts projected expenditures only. The extraction engine is a black box whose quality can improve separately; v1 does not import funding schedules, balances, revenues, bank transactions, or compliance fields.

Customer:  Select property + PDF → Uploading → Queued → Processing
           → In review → Projects updated
           → Archive decision required (if projects are absent) → Complete

Ops:       Unclaimed review → claimed by one operator → correct extraction
           → resolve every ambiguous match and validation error
           → approve exact result revision + exact reconciliation proposal
           → one transactional release

Product invariants

  1. A document never disappears. Once its row exists, every transition is durable and every non-terminal document is recoverable after a crash or deploy.
  2. No unreviewed extraction changes portal data. Ops approval is mandatory in v1 — no confidence-based auto-release.
  3. Project identity survives. A matched project keeps its id; linked transactions remain linked.
  4. The approved study is authoritative. Its values replace matched projects' values even when a customer edited the project since the previous study; the conflict is shown to ops and retained in the audit record.
  5. Absence is not deletion. Projects absent from the new study remain active until the customer submits the archive decision; archive is soft and reversible.
  6. Release is atomic and idempotent. A retry cannot duplicate projects or partially apply a study.
  7. Approval cannot go stale silently. It binds to the exact extraction revision, reconciliation proposal, and project versions ops reviewed.
  8. Environment boundaries are physical. Dev and production use different databases, source buckets, extraction deployments, and credentials.
  9. The source shown to ops is real. The actual PDF sits beside the editable result; no fabricated citation highlights (the v1 engine provides no trustworthy ones).
  10. Status is honest. Internal stack traces and engine details never appear in customer copy; authorized intake is available by default, never represented by shipping a disabled uploader.

The state machine

The stored states are deliberately more precise than the customer labels:

Stored statusCustomer labelTerminalMeaning
awaiting_uploadUploadingnoRow exists; no object version registered. Older than 24 h → abandoned.
receivedQueued for processingnoObject verified, pinned, durably queued.
submitting / extractingProcessingnoWorker lease owns engine submission; engine job polled.
in_reviewIn reviewnoImmutable result exists; ops action required.
awaiting_archive_decisionProjects updated — review archivesnoNon-destructive changes live; missing projects still active.
completeCompleteyesRelease applied, no archive action remains.
returnedNeeds a new documentyesOps cannot validate it; customer-facing reason required.
failed_finalProcessing failedyes*Retries exhausted; safe customer-facing reason. Admin retry possible; never blocks a replacement upload.
abandoned / cancelledhidden / Property no longer activeyesNever-completed upload cleaned up; or property archived before release.

The allowed transitions between these states are exhaustive and validated with row locking in one service transaction; returned, complete, abandoned, and cancelled never reopen.

Reconciliation: six exhaustive outcomes

Approved extracted rows are compared first with all active projects for the property (manually created or study-imported alike), then leftovers against archived projects so a reappearing component restores its identity instead of duplicating. Each row and each project participates in at most one match:

OutcomeRelease behavior
matched_unchangedRetain row and id; update source provenance.
matched_changedRetain id; take the study's name, category, dates, amount. Flagged "changed" until acknowledged.
matched_archivedRestore an archived project's identity with incoming values; requires explicit ops confirmation.
newInsert a new project; flagged "new" until acknowledged.
archive_candidateKeep active; create the customer's archive/keep decision item.
ambiguousBlocks release; ops must choose a match or explicitly mark the row new. Splits and merges are always ambiguous — v1 never attaches one study row to multiple projects.

The matching algorithm

Matching is deliberately conservative — it proposes work for ops, it does not create identity from a weak guess. False negatives create ops work; false positives corrupt identity.

  1. Normalize names and categories (Unicode NFKC, lowercase, "&" → "and", punctuation removed, whitespace collapsed; no stemming, numbers kept).
  2. Exact lock: a unique pair matching on normalized name, canonical category, and displayed start fiscal year is locked.
  3. Weighted fuzzy score for remaining active pairs: 0.60 × name token-set similarity (0–100, one pinned implementation — the thresholds depend on it) + category (15 if identical canonical) + year (15 same / 10 within 1 / 5 within 2) + amount (10 within 5% / 7 within 15% / 3 within 30%), capped at 100.
  4. Auto-propose only when the pair is each side's mutual best, scores ≥ 85, and each side's margin over its runner-up is ≥ 10.
  5. Archived candidates are scored the same way afterward but always require explicit operator acceptance.
  6. Any remaining active pair scoring ≥ 60 makes its connected component ambiguous (release-blocking); rows with no such evidence are new, projects not held by an ambiguous component are archive_candidate.

Thresholds are implementation constants gated by a human-labelled matching corpus: 100% precision required for automatic proposals; recall reported but not a ship blocker.

Manual edits: three-way comparison

For a project previously touched by a release, the latest release snapshot provides the base: if the current portal value differs from that base, the field is a manual-edit conflict — ops sees base, current, and incoming, and must acknowledge before approval applies the incoming value. Projects with no prior snapshot get an ordinary two-way diff. The release ledger, not a general versioning subsystem, supplies the history.

Ownership boundary

Gallopify owns the source-object key and document context, the state machine, immutable extraction revisions, ops claims/reviews/approvals, reconciliation proposals, releases, archive decisions, project rows, and notifications. The extraction engine owns only transient compute and its disposable artifacts — it never writes portal data and does not own review or release state. Gallopify integrates solely through an idempotent submit/status facade; the engine's other endpoints are internals, not a contract.

Object custody (S3 invariants)

Availability and the emergency pause

A capabilities endpoint is the sole frontend availability source: every authorized active property reports enabled — there is no allowlist, canary cohort, or rollout mode. The only override is a global emergency pause flag (UPLOAD_PIPELINE_V2_UPLOADS_PAUSED): pause rejects new presigns while history, in-flight processing, review, archive, restore, and cleanup continue. It is an incident brake, not a rollout mechanism — and it is the flag currently holding production intake closed (see the runbook).

Eligible historical uploads from the retired legacy pipeline are imported metadata-only as read-only "Earlier uploads" — property-unassigned, visible only to their original uploader, never able to poll, retry, release, or block a v2 upload. The importer never copies source objects or invents provenance.

Authority and other locked decisions worth knowing