Work / Chitragupt / Wiki / Synthesis
document-status-graph
Synthesiscanonicalverified 2026-07-19
SYNTHESIS.DOCUMENT-STATUS-GRAPHDocument status — full state graph
Summary
The persisted DocumentStatus enum has 15 values (packages/shared/src/schemas/document.ts), guarded by a legal-transitions table in apps/functions/src/_lib/document-status.ts. Every docRef.update({ status: X }) site in the functions codebase routes through assertTransition(from, to) before writing. This page is the human-readable version of the same graph — refer to the code file as the authoritative source when they diverge.
Built from
- inbox-document-status — the 15-value enum
- upload-only — the concept that makes the graph load-bearing
apps/functions/src/_lib/document-status.ts— the enforced tableapps/functions/src/__tests__/document-status.test.ts— 72 tests
Status classes
| Class | Members | Notes |
|---|---|---|
| Seed | _new (sentinel, not persisted) |
Only ingest handlers legally write from _new |
| Midstream | parse_pending, view_only, unreviewed, needs_classification, needs_ocr, pending_password, pending_identity_confirmation |
The doc is being processed or awaiting user input |
| Terminal — happy | confirmed |
Ledger fan-out ran; the row is live in the pillars |
| Terminal — rejection | rejected_format, rejected_quota, rejected_duplicate, rejected_oversized, rejected_owner_denied, rejected_transfer_denied, rejected_parser_error |
No outbound edges except the two documented undos |
| Terminal — supersede | superseded |
The row was replaced by a newer version; ledger entries reconciled |
Legal transitions (target ← sources)
| Target | Legal sources |
|---|---|
parse_pending |
_new, all midstream, confirmed (rescan resets to parse) |
view_only |
_new |
unreviewed |
parse_pending, view_only, pending_password, pending_identity_confirmation, needs_classification, needs_ocr, unreviewed, _new (family-transfer accept only) |
needs_classification |
parse_pending, view_only, needs_classification |
needs_ocr |
parse_pending, view_only, needs_ocr |
pending_password |
parse_pending, view_only, pending_password |
pending_identity_confirmation |
parse_pending, view_only, unreviewed, rejected_owner_denied (restore), pending_identity_confirmation |
confirmed |
unreviewed, pending_identity_confirmation |
rejected_quota |
_new |
rejected_oversized |
_new, view_only (backfill oversize) |
rejected_format |
parse_pending, view_only, pending_password, needs_classification, needs_ocr |
rejected_duplicate |
all midstream |
rejected_owner_denied |
pending_identity_confirmation, rejected_transfer_denied |
rejected_transfer_denied |
pending_identity_confirmation |
rejected_parser_error |
parse_pending |
superseded |
all midstream, confirmed |
Undo paths
rejected_owner_denied → pending_identity_confirmation—restoreOwnershipcallable. Undoes an accidental deny before the 7-day sweep deletes the row.rejected_transfer_denied → rejected_owner_denied—denyOwnershipcallable. Uploader re-denies a bounced family-transfer.
These are the ONLY documented outbound edges from terminal-rejection states. Everything else is a bug — the test suite Graph invariants — no orphans, no dead ends asserts this at CI.
Canonical documented chains
- Happy path:
_new → parse_pending → unreviewed → confirmed - Free tier:
_new → view_only → (backfill) → unreviewed → confirmed - Identity mismatch → restore:
_new → parse_pending → pending_identity_confirmation → rejected_owner_denied → pending_identity_confirmation → unreviewed → confirmed - Bounced transfer → owner-denied:
_new → parse_pending → pending_identity_confirmation → rejected_transfer_denied → rejected_owner_denied - Encrypted PDF:
_new → parse_pending → pending_password → unreviewed → confirmed - Parse rot:
_new → parse_pending × 5 → rejected_parser_error - Rescan on confirmed:
confirmed → parse_pending → unreviewed → confirmed - Dedup exact_duplicate:
_new → parse_pending → unreviewed → rejected_duplicate - Supersede on confirmed:
confirmed → superseded
Enforcement layers
- Zod:
DocumentStatusSchema.parse(status)at every schema boundary rejects unknown values. - assertTransition: called before every persisted status write; throws
HttpsError("failed-precondition", "Illegal document_status transition: {from} → {to}")on mismatch. - Firestore rules: every user-visible collection is
allow write: if false— the client cannot forge a status. - Tests: 72 unit + integration tests including graph invariants, documented chains, terminal invariants.
Related
- inbox-document-status — the enum entity page
- document-pipeline-boundaries — where each transition happens in the pipeline
- upload-only — why the pipeline is server-only-write
- read-only-review — the invariant that makes user status writes illegal
Last refresh
2026-07-19 — created after the state-machine work in commits dcb2b893, 4a933f28, 4e25a2d3.
Every project of mine is written down like this.
Read the résumé