computed state
applies the generalized state machine contract to persistence placement. the root standard owns state semantics, fact precedence, terminal classification, guarded commits, replay; this page owns which durable forms may store facts or disposable projections.
why
a stored state that can disagree with its own facts eventually does: the producer that flips a flag can die before flipping it, and nothing reconciles the copy because the fold that would refute it was never declared. deriving state from facts is self-repairing — the fold recomputes from whatever committed, so a killed worker or raced writer cannot leave a permanent lie. one authority: the declared machine, written once, not three producers each re-encoding its edges.
rule
if a state can be computed, and needs to be computed — answers "what state is this entity in right now" — it is not allowed in the database. status columns, boolean progress flags, stored lifecycle fields are caches of a fold over facts; once written they can drift, and nothing reconciles them.
store the fact. derive the state. declare the state machine as the fold.
mandatory: no column that can go stale (operator directive, 2026-09-29)
"the displayStatus is a plague, it can be stale, we need to remove all column in database that can be stale and do computed state instead."
evidence (2026-09-29): youtube.channel.display_status read unknown for a 6.5M-subscriber channel the catalog served, and the website branched on it — rendered a "discover" card on top of the full discovered page. is_resetting / active_reset_id duplicate the active row in reset.session. every such column needs a producer to remember to flip it, and one day one does not.
the law covers every column whose value is a function of other rows we own, not only status enums:
| kind | example | verdict |
|---|---|---|
| lifecycle / display status | display_status, processing_state | forbidden — fold the run/step/item facts. |
| boolean progress flag | is_resetting, is_processed | forbidden — exists(active session) is the answer. |
| pointer to "the current" row | active_reset_id, latest_run_id | forbidden — select the current row by its own facts. |
| count of our own rows | candidate_count, skill_count | forbidden unless it is an immutable receipt (see below). |
| copy of another row's field | a title copied onto a child row | forbidden — join to the owner. |
| external observation | YouTube subscriber_count | allowed — a recorded fact; carry when it was observed. |
| immutable receipt | row_count written once when a snapshot sealed | allowed — a historical fact about that moment, never updated. |
| authored decision | inference_provider, learning_deliverables | allowed — the user or operator set it; nothing computes it. |
| a run's own status | workflow.run.status | allowed only when it is the recorded outcome of that run (a fact), never a fold of its children. |
a concurrency fence (reset_generation, version counters) is allowed only when no stored fact can serve as the fence; prefer a fact the guard already reads (the active session row, a unique constraint).
auditing a schema for stale columns
- list candidates:
information_schema.columnsmatchingstatus,state,is_*,has_*,*_count,active_*,latest_*,current_*, plus every column written by more than one code path. - per candidate: what fact(s) does it summarize? who writes it (
path:line)? who reads it? can it be recomputed from rows we own? - prove staleness live: one sql diff of the column against its fold; every mismatching row is a lie already served.
- classify with the table above; for each forbidden column name the replacement placement (generated column, view, backend fold, recompute cache) and the readers that move to it.
- retire forward-only: readers move to the fold first, writers deleted, then the column dropped in its own migration.
fact versus computed state
| question | answer |
|---|---|
| did something happen or get recorded? | fact. store it: settlement, transcript stored, admission granted, provider instance observed errored. |
| what state is the entity in, given everything? | computed state. a pure function of facts. never a column. |
| is a mirrored external observation a fact? | yes. an observed provider status is a recorded observation; copying it is storing evidence, not deriving. |
| is a parent state (topic over videos) a fact? | no. parent states fold child states: topic = fold(videos), channel = fold(topics, pledge facts). |
the dividing line is recomputation. a fact cannot be recomputed, only recorded. a state can always be recomputed — that is why storing it creates a second, lying copy of the truth.
persistence applies the fold
the state machine declares the closed states and the one exhaustive fold. persistence supplies canonical facts and the atomic fact-commit boundary. transition writers, status repair jobs, producer-owned progress flags remain forbidden — each creates another state authority beside the fold.
where the fold lives
four placements, ordered by increasing distance from the facts:
| placement | what it is | freshness | fits when |
|---|---|---|---|
| generated column (same row) | GENERATED ALWAYS AS over columns of one row | computed at write, always true | the derivation only needs fields of its own row (video processed). |
| sql view (fold in the definition) | CREATE VIEW folding fact tables with joins/aggregates | computed at every read | one sql statement can express the fold; readers want a named table. |
| backend fold (fold in app logic) | typed pure function over fact reads, declared once, shared | computed at every read | the default. typechecked, exhaustive, testable, portable. |
| recompute cache (materialized) | table rebuilt whole by re-running the fold | declared freshness window | fleet/list reads that must select by state (work queues, catalogs). |
decision rules:
- a derived value that only reads its own row is a generated column, not app code — the database maintains it and it cannot drift.
- the backend fold is the default owner of entity state: one authored, typechecked function per entity is the declared state machine. push the fold down into sql (one composed statement or a view) only when reading every fact row into memory is measurably too costly for the read path.
- a view or sql push-down is packaging, not authority: the same fold now lives in sql. keep exactly one expression of it; parse its output through the same typed contract the backend fold uses.
- the recompute cache is the only stored form, and it stores the fold's output, rebuilt whole (next section). an incrementally maintained status column is not any of these placements — it is the forbidden cache.
when a fold is too expensive to run per read
a work queue or ranked list that must select by state without folding every row is the one sanctioned exception — a recompute cache, not a status column:
| property | required answer |
|---|---|
| owner | the fold itself is the owner; the cache stores its output. |
| write | rebuilt by re-running the fold, whole. never incrementally mutated by producers. |
| repair | drop and rebuild. there is no hand-repair, no flag flip. |
| honesty | a typed failure or a declared freshness window, never a silent stand-in for the fold. |
an incrementally-updated status column with a reconciler is not this pattern; it is an undeclared cache pretending to be authority, and it is forbidden.
verification
- during migration, run the fold over live facts and diff against the legacy column; every disagreement is a real lie the column told.
- prove parent folds agree with child folds (
channelversus its topics) on real data. - kill a producer mid-flight: no state goes permanently wrong — the fold still answers from committed facts.
- prove the guard batch: committing a fact under a false precondition fails atomically.
good and bad
bad
// three producers hand-write the same machine:await transitionLifecycle({ from: ["learning"], to: "learned" });await topicsTable.update({ processed: true }); // a workflow was supposed to flip this// a reconciler re-encodes the edges a third time to fix drift.
a stored state that can disagree with its own facts, does. the producer can die before flipping the flag, and the flag then lies forever — no reconciler refutes it because the fold that would was never declared. three producers encode the same machine three ways, so answers diverge under concurrency and retries; every divergence becomes a manual data-repair incident. the interface eventually renders a state the facts cannot support, and nobody can say which copy is authoritative.
good
const state = deriveChannelState(await readChannelFacts(channelId));await commitFact({ kind: "run_completed", channelId, runId }, { guard: state === "releasing" });// every reader derives; every guard folds; nothing to drift.
state is a function, not a row. the machine is declared once as that function, so every reader and guard derives the same answer from whatever facts actually committed; a killed worker or raced writer cannot leave a permanent lie. adding a state means editing one exhaustive, typechecked fold rather than hunting every producer that hand-wrote an edge — drift becomes structurally impossible instead of periodically repaired.