Goldie Blocks

Status Axes

What status a folio actually carries — one stored axis, one derived, and two that were retired

Status is the thing a schema most often gets wrong by conflating, so each axis below is named separately and says whether it is stored, derived, or retired. The values themselves are declared in code and that declaration governs; this page says what they mean and why the split is drawn where it is.

AxisFieldWhere it lives
Version commit stateversionStatusStored — a column on folioVersions
Collection progressinterviewStatusDerived — computed from the answers, never stored
Reuse posturereuseTypeRetired — library standing derives from libraryPins
Render modegenerationModeRetired with the payment and generation cluster

Stored — versionStatus

Is this row a frozen save point, or the live editing head?

live is the mutable head: every edit, every answer, every computed-field update lands there, and there is exactly one per folio at a time. versioned is a sealed save point — named, immutable, never edited again. convex/schema.ts declares the union; folioVersions carries it.

live → versioned is one-way, and minting is a user act. Sealing the draft creates the next edition and opens a fresh draft for the round of edits after it. Nothing about typing mints a version.

In plain terms: live is the working copy on the desk; versioned is the engrossed copy in the file. A lawyer decides when one becomes the other, the way a developer decides when to commit — not on every keystroke.

Why the axis exists at all. Without it, either every keystroke creates a committed snapshot (storage explodes) or there is no way to name a save point, and the lawyer cannot point at "the version we sent on Tuesday". One axis, two values, one transition. The immutability rule is Sealed Versions Are Immutable; the save lifecycle is in Folios Schema.

Derived — collection progress

How far along is this data collection?

Nothing stores this, and that is the point. The retired three-tier model kept a moduleVersions.interviewStatus column that every write path had to remember to recompute, so it went stale the moment a field was answered by a path that forgot. The rule is now a projection — packages/domain/src/interview/progress.ts, which holds the whole of it at all three heights and governs. A derived status cannot go stale, because there is no second copy.

ValueMeaning
not_startedNothing a person could answer has been answered.
in_progressSome are done, not all.
completeEverything a person could answer is done.
skippedDeliberately passed over. Settable only where the field is not required — the field contract refuses the other case, so a required field is done only by its answer.

A field is done when it holds a value or is skipped, and "complete" means every field a person could answer — not merely every required one. An optional question left open keeps the block in progress. That is the owner's ruling in Interview Completion Is Answered or Skipped, and it is the opposite of the "all required fields" test an earlier revision of this page stated.

Two consequences worth stating, because they are easy to get wrong:

  • Empty is absence, not an answer. An empty string and an empty list leave the collection exactly where an unasked question does. Treating "" as collected would report a document complete because someone tabbed through it.
  • Computed fields count for nothing either way. They are derived, so they can neither be answered nor left unanswered.

A document is never skipped. Skipping is something done to a part, and a document made entirely of skipped parts has nothing left to ask — which is complete. So a document's progress is the narrower three values.

Status reason. A status_reason may explain a non-obvious skip. It is a statement of current state, not a transcript, and must never carry hidden reasoning.

The local skill shares the vocabulary, mirroring per-module status as status in the one-file corpus layout with the same optional status_reason — see Local Skills → File roles.

Retired — reuseType

A folio no longer declares whether it is working or library content. Library standing is a fact about a shelf, not about the card: a libraryPins row names the exact sealed edition that sits on a library, and the folio itself never moves or copies to get there (A Library Is a Shelf, Not a Folio).

The rule the old axis carried survives in a better place: promotion is copy-write. Insertion from a library materialises an independent copy with its own lineage pointer; the source is untouched. What went is the column, not the discipline.

Retired — generationMode

preview / final was a render-time input declared through a document's output.generation keys. That whole payment and generation cluster is retired (see what the Folio schema does not carry), and documentGenerations is dormant with no writer.

The product idea is alive and the mechanism is open: a free Draft render and a paid Final render are recorded and deferred. Whoever builds them decides how a render is marked. It will not be a status column on the folio, because a single folio is validly rendered both ways.

Why not one documentStatus

Because the facts move independently, and an overloaded enum forces every transition to invent a new value. A folio can be on its live head and have every question answered — the lawyer simply has not named a save point yet. It can equally be sealed mid-collection, as a checkpoint. Neither is a contradiction, and no single enum can express both without a combinatorial vocabulary nobody can read.

The same reasoning retired the older workflowStatus and publishStatus: workflowStatus split into the version state and the derived progress above, and publishStatus went because library standing is a pin, not a flag.

On this page