# Lifecycle

Edition 2026-08-14. Describes GAVR record schema 1.0.

**One primary state plus orthogonal facets**, never one large enum.

## States

| State       | Meaning                                       |
| ----------- | --------------------------------------------- |
| `RESERVED`  | An identifier exists and there is no content. |
| `DRAFT`     | Content exists and is not public.             |
| `PUBLISHED` | Public.                                       |
| `WITHDRAWN` | Was public, retracted, content retained.      |
| `DISCARDED` | Editorially abandoned, content retained.      |

`DISCARDED` rather than `REJECTED`: `REJECTED` already means three incompatible
things in this domain — an editor rejected a draft, an upstream registry
rejected a record, an operator declined a request — and a reader of a public
vulnerability record will assume the registry sense. `REJECTED` is reserved for
that sense and is a _state of an identifier_, not of a record.

## Transitions

| From        | To                                |
| ----------- | --------------------------------- |
| `RESERVED`  | `DRAFT`, `PUBLISHED`, `DISCARDED` |
| `DRAFT`     | `PUBLISHED`, `DISCARDED`          |
| `PUBLISHED` | `WITHDRAWN`, `DISCARDED`          |
| `WITHDRAWN` | `PUBLISHED`, `DRAFT`, `DISCARDED` |
| `DISCARDED` | `DRAFT`, `PUBLISHED`              |

Everything absent from this table is refused. The complete table is stated
exhaustively in [`../conformance/lifecycle.json`](../conformance/lifecycle.json)
— every ordered pair of distinct states, so an implementation cannot pass by
getting the common edges right.

Five properties, each a decision rather than an omission:

**No state is terminal.** A record withdrawn in error can be restored and an
abandoned one revived. CVE can do neither: a mistaken withdrawal is unfixable
there, so the record stays dead and the correction lives only in whatever prose
someone remembered to add. Since GAVR destroys nothing, every state already
holds its content, and refusing to let it come back would be a policy choice
with no data-integrity argument behind it.

**Nothing returns to `RESERVED`.** It means an identifier exists and there is no
content. Once content exists that statement is false, and a state that lies
about the record is worse than a missing edge.

**`PUBLISHED` does not reach `DRAFT` directly.** Leaving the public surface
should say which of the two things happened. Note the honest consequence:
`PUBLISHED -> DISCARDED -> DRAFT` reaches editing in two hops, so this does not
make un-publishing impossible. It makes it impossible to do _silently_, because
the record passes through a state that records the exit.

**`WITHDRAWN -> PUBLISHED` skips the draft step.** Restoring a record withdrawn
in error is usually a correction of the withdrawal rather than of the record,
and forcing it through `DRAFT` would take it off the public surface a second
time to fix the fact that it should never have left.

**`WITHDRAWN -> DISCARDED` exists** (added 2026-08-25). Every other state could
be abandoned and this one could not, with no argument anywhere for the
exception, while withdrawing a record and later concluding it should never have
been published is the ordinary order in which those two judgements arrive.
Without the edge the only route was `WITHDRAWN -> PUBLISHED -> DISCARDED`, which
puts a record back on the public surface in order to abandon it. Note this is a
_widening_: a record an implementation written before this date would refuse is
one a later one accepts, so a stale validator fails closed.

**The identifier never moves across any of these edges.** Restoration is the
same record returning, not a replacement, which is exactly why it is
expressible: a citation written while the record was withdrawn still resolves to
it afterwards.

## Facets

Facets are orthogonal facts, never encoded into `state`.

| Facet              | Notes                                 |
| ------------------ | ------------------------------------- |
| `disputed`         | With a mandatory reason (GAVR-R-002). |
| `wasWithdrawnOnce` | Latches on. Never cleared.            |
| `embargoUntil`     | Not yet servable, whatever the state. |
| `reviewState`      | Editorial workload.                   |
| assignee           | Who is working on it.                 |

"Assigned but reserved" is `RESERVED` with an assignee. Published + disputed +
embargoed + assigned is four independent facts, not sixteen enum members, and
that is the whole reason for the split.

`disputed` in particular stays off the enum because CVE's DISPUTED-forever
problem comes from treating it as a state. A dispute here is a facet with a
reason, and the substantive disagreement lives in a container of its own.

**`wasWithdrawnOnce` latches server-side.** It is derived from the transition
history, and a submitted document that denies its own withdrawal is refused
(`withdrawal-history-erased`). A reversible lifecycle is only trustworthy if the
reversals stay visible; a document quietly dropping that flag would be the quiet
version of the deletion behaviour this design rejects.

**A state change is a transition; a same-state revision is not.** The table has
no self-edges, correctly, so consulting it for a revision that does not change
state would refuse every routine edit with `PUBLISHED -> PUBLISHED`. Check the
table only when the state actually moves.

**Embargo is evaluated against a clock the caller supplies.** A timestamp
captured once at module load freezes, and then hides every record whose embargo
lifted after the process started — forever, and silently.

## Publication state is separate

A record's own state says what the vulnerability's status is. Whether it is live
at a particular registry is a **per-destination** fact and belongs on a
publication row, not in this enum. A record can be `PUBLISHED` here and pending
at one registry and rejected at another, and collapsing that into one field
loses which.
