# LifecycleFigure

A state machine: what something can be, and what moves it between states.

- Group: Diagrams
- Import: `import { LifecycleFigure } from '@misoto22/folio/diagrams'`
- Page: https://ui.misoto22.com/components/lifecycle-figure/
- Related: workflow-figure, steps

## When to reach for it

The one figure that spends colour, and it spends exactly the two tokens the system reserves for state. Every other distinction is shape, so a greyscale print keeps six of the eight.

## Anatomy

- **Figure shell** (required) — The frame around the picture. What a machine hands it is a transition list with each note folded in after an em dash, so the condition on an arrow — the timeout, the retry count — is in the text copy and not only on the line.
- **State plate** (required) — One shape per kind: a filled cap for a start, a diamond for a decision, a dashed frame for a wait, a cut corner for an external, and the two washes the system reserves for the terminals. The name is centred, step sits in the leading corner and tag along the bottom edge.
- **Main rail** — The first lane’s states, sorted by col and joined consecutively at the emphasis weight — one line per neighbouring pair the transitions do not already declare, so a spine written out in full is drawn from the transitions and a spine left implied is drawn anyway. Without it the top row reads as five unconnected boxes, because the spine of a machine is the part nobody writes down.
- **Transition** — A routed line with its label on a mask and its note in a quieter line beneath. A declared transition between two rail neighbours replaces the implicit one rather than doubling it.
- **Lane** — A rule with a mono caption above it, drawn only as far as that lane’s own states reach. A lane past the first has its columns shifted along the main rail, which is what keeps a drop out of the spine vertical rather than a dogleg.

## Best practices

### Do

- Keep the first lane for the ordered spine and nothing else. Consecutive states there are joined by the implicit rail even when no transition declares the pair, so a state parked in lane 0 for spacing invents an edge the machine does not have. A state whose lane id nothing declares is kept out of that rail and reported as DIAGRAM_LANE_UNKNOWN instead, because a typo should not be able to add an arrow.
- Count a secondary lane’s columns from the rail. Every col in a lane past the first is shifted two columns along, so col: 0 in the second lane sits under col: 2 in the first — the convention archify’s lifecycle contract already implies, restated here because it is what makes a drop land on the state it came from.
- Say whether a failure is recoverable with an edge, not with the type. type: "failure" only paints the plate; a retryable error and a terminal one are identical until a reader follows the arrows out, and the one with none is where the run ended.

### Don’t

- yOffset moves the plate and moves nothing out of its way. The lane’s rule is drawn from what that lane holds, so a negative nudge drags the rule up with the state and a large positive one drops the plate into the band below — a state needing that much room wants a column of its own rather than a push.
- The figure claims a state can be left only the ways its arrows say. A cancel that can happen from anywhere and is drawn from nowhere reads as impossible, which is a reading the picture makes and the specification never wrote — draw it, or say in the subtitle that it is not drawn.

## Accessibility

- success and failure are the only coloured marks in the package’s diagrams, and each is also a distinct plate shape — colour is never the only carrier.
- The main rail is drawn between consecutive states in the first lane even when the specification does not list those transitions, because they are the diagram’s spine rather than its exceptions.

## LifecycleFigure

A state machine: what something can BE, and what moves it. The one figure here that spends colour, and it spends exactly the two tokens this system reserves for state: `--success` on a terminal success, `--danger` on a terminal failure. Nothing else in the package is allowed those two, and a lifecycle diagram is the case they were reserved for — the whole point of the picture is which end a run came out of. The other six kinds are carried by shape, so a greyscale print loses the two outcomes' hue and keeps every other distinction: a start is a filled cap, a decision is a diamond, a wait is a dashed frame, an external is a plate with a cut corner. A RECOVERABLE FAILURE IS A FAILURE WITH A WAY BACK. `type: "failure"` says how a state is drawn; a transition OUT of it is what says whether the run is over. A retryable error and a terminal one look identical until you follow the arrows, which is correct — the difference between them is a real edge in the machine, not an adjective on a box.

### Props

- `spec` (required) — `LifecycleSpec`.

Also accepts: `FigureChrome`.

## Example — default

```tsx
import { LifecycleFigure } from '@misoto22/folio/diagrams'

<LifecycleFigure
  spec={{
    meta: { title: 'A job run', subtitle: 'The only figure that spends colour, on the two outcomes.' },
    lanes: [
      { id: 'main', label: 'Phases' },
      { id: 'wait', label: 'Interruptions' },
      { id: 'exit', label: 'Terminal' },
    ],
    states: [
      { id: 'queued', type: 'start', label: 'Queued', sublabel: 'accepted', lane: 'main', col: 0, step: '01' },
      { id: 'running', type: 'active', label: 'Running', sublabel: 'tool calls', lane: 'main', col: 1, step: '02' },
      { id: 'review', type: 'decision', label: 'Review', sublabel: 'quality gate', lane: 'main', col: 2, step: '03' },
      { id: 'done', type: 'success', label: 'Completed', sublabel: 'final answer', lane: 'main', col: 3, step: '04' },
      { id: 'blocked', type: 'waiting', label: 'Blocked', sublabel: 'missing input', lane: 'wait', col: 0 },
      { id: 'failed', type: 'failure', label: 'Failed', sublabel: 'timed out', lane: 'exit', col: 0 },
    ],
    transitions: [
      { id: 'a', from: 'review', to: 'blocked', label: 'needs input' },
      { id: 'b', from: 'blocked', to: 'failed', label: 'expired', note: 'after 24h', variant: 'dashed' },
      { id: 'c', from: 'blocked', to: 'running', label: 'answered', variant: 'emphasis' },
    ],
  }}
/>
```

## Example — a decision with both ends

```tsx
import { LifecycleFigure } from '@misoto22/folio/diagrams'

<LifecycleFigure
  spec={{
    meta: { title: 'Card authorisation', subtitle: 'One decision, and the two ends a run can come out of.' },
    lanes: [
      { id: 'flow', label: 'Authorisation' },
      { id: 'refuse', label: 'Refusals' },
    ],
    states: [
      { id: 'submitted', type: 'start', label: 'Submitted', sublabel: 'card token', lane: 'flow', col: 0, step: '01' },
      { id: 'risk', type: 'decision', label: 'Risk score', sublabel: 'model and rules', lane: 'flow', col: 1, step: '02' },
      { id: 'approved', type: 'success', label: 'Approved', sublabel: 'funds held', lane: 'flow', col: 2, step: '03' },
      { id: 'declined', type: 'failure', label: 'Declined', sublabel: 'no retry', lane: 'refuse', col: 0 },
    ],
    transitions: [
      { id: 'no', from: 'risk', to: 'declined', label: 'score above 90', fromSide: 'bottom', toSide: 'top' },
    ],
    cards: [
      { dot: 'emerald', title: 'Two ends', items: ['A run leaves through Approved or through Declined and nowhere else.'] },
      { dot: 'amber', title: 'The rail', items: ['Submitted to Risk score to Approved is drawn without being declared.'] },
    ],
  }}
/>
```

## Example — a failure with a way back

```tsx
import { LifecycleFigure } from '@misoto22/folio/diagrams'

<LifecycleFigure
  spec={{
    meta: { title: 'A delivery attempt', subtitle: 'Two failures on the same plate; one of them is not the end.' },
    lanes: [
      { id: 'run', label: 'Delivery' },
      { id: 'faults', label: 'Faults' },
    ],
    states: [
      { id: 'queued', type: 'start', label: 'Queued', sublabel: 'accepted', lane: 'run', col: 0, step: '01' },
      { id: 'sending', type: 'active', label: 'Sending', sublabel: 'attempt n', lane: 'run', col: 1, step: '02', tag: 'HTTP' },
      { id: 'delivered', type: 'success', label: 'Delivered', sublabel: '2xx', lane: 'run', col: 3, step: '03' },
      { id: 'timeout', type: 'failure', label: 'Timed out', sublabel: 'no response', lane: 'faults', col: 0 },
      { id: 'rejected', type: 'failure', label: 'Rejected', sublabel: 'no retry', lane: 'faults', col: 1 },
    ],
    transitions: [
      { id: 'slow', from: 'sending', to: 'timeout', label: 'no reply in 10s', toSide: 'top' },
      { id: 'retry', from: 'timeout', to: 'sending', label: 'retry', note: 'up to 5 times', variant: 'emphasis', fromSide: 'left', toSide: 'bottom' },
      { id: 'bad', from: 'sending', to: 'rejected', label: '400 invalid body', fromSide: 'bottom', toSide: 'top' },
    ],
    cards: [
      { dot: 'amber', title: 'Same plate, different run', items: ['Timed out and Rejected are drawn identically.', 'Only the arrow leaving Timed out says the run continues.'] },
    ],
  }}
/>
```

## Example — the implicit rail

```tsx
import { LifecycleFigure } from '@misoto22/folio/diagrams'

<LifecycleFigure
  spec={{
    meta: { title: 'An article, from draft to live', subtitle: 'The spine is drawn; only the departures are written.' },
    lanes: [
      { id: 'main', label: 'Review' },
      { id: 'aside', label: 'Off the rail' },
    ],
    states: [
      { id: 'draft', type: 'start', label: 'Draft', sublabel: 'author only', lane: 'main', col: 0, step: '01' },
      { id: 'review', type: 'active', label: 'In review', sublabel: 'two readers', lane: 'main', col: 1, step: '02' },
      { id: 'decide', type: 'decision', label: 'Accept?', sublabel: 'editor', lane: 'main', col: 2, step: '03' },
      { id: 'live', type: 'success', label: 'Published', sublabel: 'on the site', lane: 'main', col: 3, step: '04' },
      { id: 'pulled', type: 'neutral', label: 'Withdrawn', sublabel: 'by the author', lane: 'aside', col: 0 },
      { id: 'held', type: 'waiting', label: 'Held', sublabel: 'awaiting legal', lane: 'aside', col: 1 },
    ],
    transitions: [
      { id: 'a', from: 'review', to: 'pulled', label: 'author pulled it', variant: 'dashed' },
      { id: 'b', from: 'decide', to: 'held', label: 'legal question', note: 'blocks publication' },
      { id: 'c', from: 'held', to: 'decide', label: 'cleared', variant: 'emphasis' },
    ],
  }}
/>
```
