# How the Archify Lifecycle Renderer Handles State Transitions and Phase Columns

> Learn how the Archify Lifecycle renderer manages state transitions and phase columns with band-based geometry and configurable path algorithms. Explore automatic port spreading for efficient routing.

- Repository: [tt-a1i/archify](https://github.com/tt-a1i/archify)
- Tags: internals
- Published: 2026-09-01

---

**The Archify Lifecycle renderer maps states to phase columns using band-based geometry calculations and routes transitions through configurable path algorithms with automatic port spreading.**

The `archify/renderers/lifecycle/render-lifecycle.mjs` module transforms JSON lifecycle definitions into SVG diagrams by measuring state positions across phase columns and computing transition paths. This article explains the rendering pipeline, from band mapping and column placement to transition routing and path assembly.

## Mapping States to Bands and Phase Columns

### Band Assignment with `bandFor`

Every state belongs to a **lane**, and each lane maps to a fixed **band** that determines its visual treatment. The `bandFor` function in `archify/renderers/lifecycle/render-lifecycle.mjs` performs this mapping:

```js
// archify/renderers/lifecycle/render-lifecycle.mjs#L98-L105
function bandFor(lane) {
  if (lane === 'main') return 'phase';
  if (lane === 'terminal') return 'outcome';
  return 'event';
}

```

- **`main`** lanes render as **phase** states—the primary workflow steps
- **`terminal`** lanes render as **outcome** states—final success or failure states
- All other lanes render as **event** states—intermediate or auxiliary states

### Geometry Calculation in `measureState`

The `measureState` function computes each state's position using band-specific dimensions and column coordinates. States are centered on X-coordinates from `layout.phaseXs`, `layout.eventXs`, or `layout.outcomeXs` based on their band and `col` index:

```js
// archify/renderers/lifecycle/render-lifecycle.mjs#L107-L128
function measureState(state) {
  const isPhase = bandFor(state.lane) === 'phase';
  const isOutcome = bandFor(state.lane) === 'outcome';
  const width = state.width || (isPhase ? layout.phaseW : isOutcome ? layout.outcomeW : layout.eventW);
  const height = state.height || (isPhase ? layout.phaseH : isOutcome ? layout.outcomeH : layout.eventH);
  const xs = isPhase ? layout.phaseXs : isOutcome ? layout.outcomeXs : layout.eventXs;
  const cx = xs[state.col] ?? xs[xs.length - 1];
  const y = (isPhase ? layout.phaseY : isOutcome ? layout.outcomeY : layout.eventY) + (state.yOffset || 0);
  return {
    ...state,
    width,
    height,
    x: cx - width / 2,
    y,
    cx,
    cy: y + height / 2
  };
}

```

Key behaviors:
- **Column validation**: If `state.col` exceeds available columns, the last column is used (with validation errors raised separately around lines 158–170)
- **Vertical positioning**: Each band has a base Y-coordinate (`phaseY`, `outcomeY`, `eventY`) with optional `yOffset` adjustment
- **Measured state storage**: Results are cached in a `Map` named `states` for O(1) lookup during transition routing

## Routing Transitions Between States

### Side Selection with `transitionSides`

Transitions specify which edges of source and target states to connect. The renderer respects explicit `fromSide`/`toSide` values or computes defaults from relative positions:

```js
// archify/renderers/lifecycle/render-lifecycle.mjs#L74-L80
function transitionSides(transition) {
  const from = states.get(transition.from);
  const to   = states.get(transition.to);
  return {
    fromSide: chosenSide(transition.fromSide, defaultFromSide(from, to)),
    toSide:   chosenSide(transition.toSide,   defaultToSide(from, to)),
  };
}

```

### Automatic Port Spreading

To prevent overlapping arrows when multiple transitions share endpoints, `automaticPortSpread` pre-computes distributed attachment points:

```js
// archify/renderers/lifecycle/render-lifecycle.mjs#L83-L86
const automaticPorts = automaticPortSpread(lifecycle.transitions, states, {
  sideFor: (transition, endpoint) => transitionSides(transition)[endpoint === 'source' ? 'fromSide' : 'toSide'],
});

```

These ports are used unless a transition defines its own `via` points.

### Path Algorithms in `routeVia`

The core routing logic supports multiple strategies via the `route` property:

```js
// archify/renderers/lifecycle/render-lifecycle.mjs#L29-L70
function routeVia(transition, from, to, start, end, fromSide, toSide) {
  if (transition.via) return transition.via;
  switch (transition.route || 'auto') {
    case 'straight':   return [];
    case 'drop':       return [[start[0], (start[1] + end[1]) / 2], [end[0], (start[1] + end[1]) / 2]];
    case 'bottom-channel':
      const yBot = transition.channelY ?? Math.max(from.y + from.height, to.y + to.height) + 34;
      return [[start[0], yBot], [end[0], yBot]];
    case 'top-channel':
      const yTop = transition.channelY ?? Math.min(from.y, to.y) - 28;
      return [[start[0], yTop], [end[0], yTop]];
    case 'right-channel':
      const xR = transition.channelX ?? Math.max(from.x + from.width, to.x + to.width) + 36;
      return [[xR, start[1]], [xR, end[1]]];
    case 'left-channel':
      const xL = transition.channelX ?? Math.min(from.x, to.x) - 36;
      return [[xL, start[1]], [xL, end[1]]];
    case 'auto':
    default:
      if (start[0] === end[0] || start[1] === end[1]) return [];
      const fromVertical = fromSide === 'top' || fromSide === 'bottom';
      const toVertical   = toSide   === 'top' || toSide   === 'bottom';
      if (fromVertical !== toVertical) {
        return [fromVertical ? [start[0], end[1]] : [end[0], start[1]]];
      }
      if (fromVertical) {
        const y = transition.channelY ?? (start[1] + end[1]) / 2;
        return [[start[0], y], [end[0], y]];
      }
      const x = transition.channelX ?? (start[0] + end[0]) / 2;
      return [[x, start[1]], [x, end[1]]];
  }
}

```

**Available route types:**

| Route | Behavior |
|-------|----------|
| `straight` | Direct line, no intermediate points |
| `drop` | Horizontal midpoint with vertical drop |
| `bottom-channel` | Routes below both states at `channelY` or auto-calculated offset |
| `top-channel` | Routes above both states at `channelY` or auto-calculated offset |
| `right-channel` | Routes to the right of both states at `channelX` or auto-calculated offset |
| `left-channel` | Routes to the left of both states at `channelX` or auto-calculated offset |
| `auto` (default) | Single bend for orthogonal sides; horizontal or vertical channel for parallel sides |

### Path Assembly in `pathFor`

The final SVG path combines anchored endpoints, computed via points, and rounded corners:

```js
// archify/renderers/lifecycle/render-lifecycle.mjs#L87-L107
function pathFor(transition) {
  if (pathCache.has(transition)) return pathCache.get(transition);
  const from = states.get(transition.from);
  const to   = states.get(transition.to);
  const ports = automaticPorts.get(transition);
  const { fromSide, toSide } = transitionSides(transition);
  const start = ports?.from || anchor(from, fromSide);
  const end   = ports?.to   || anchor(to,   toSide);
  let via = routeVia(transition, from, to, start, end, fromSide, toSide);
  if (ports && !via.length && Math.abs(start[0] - end[0]) >= 4 && Math.abs(start[1] - end[1]) >= 4) {
    const midX = (start[0] + end[0]) / 2;
    via = [[midX, start[1]], [midX, end[1]]];
  }
  const points = [start, ...via, end];
  const routed = {
    d: roundedPath(points, transition.cornerRadius ?? 10),
    points,
  };
  pathCache.set(transition, routed);
  return routed;
}

```

The `pathCache` Map eliminates redundant computation for repeated transitions.

## Phase Column Layout in Practice

The **phase column system** centers states on pre-computed X-coordinates. In [`archify/examples/agent-run.lifecycle.json`](https://github.com/tt-a1i/archify/blob/main/archify/examples/agent-run.lifecycle.json), the "Executing" state uses `col: 2` to position itself at `layout.phaseXs[2]` (402px in the default template). This columnar approach ensures consistent horizontal alignment across complex lifecycles without manual coordinate specification.

States in the `main` lane automatically participate in phase column layout, while `terminal` and custom lanes use separate coordinate arrays for outcome and event positioning.

## Rendering a Complete Lifecycle Diagram

Consumer code invokes the renderer through a standardized pipeline:

```js
import { loadDiagramWithBrandMarks } from '../shared/cli.mjs';
import { renderSvg } from './render-lifecycle.mjs';

// 1️⃣ Load the JSON definition (here we use the built‑in example)
const { diagram: lifecycle, template, outPath } = await loadDiagramWithBrandMarks({
  rendererDir: new URL(import.meta.url).pathname,
  diagramType: 'lifecycle',
  defaultExample: 'agent-run.lifecycle.json',
});

// 2️⃣ The renderer validates the layout, builds the SVG and writes the file
await writeDiagram({
  outPath,
  template,
  diagramType: 'lifecycle',
  meta: lifecycle.meta,
  svg: renderSvg(),
  cards: lifecycle.cards,
});

```

This mirrors the implementation in `render-lifecycle.mjs` lines 53–61, delegating all measurement, validation, and routing to internal functions.

## Summary

- **Band mapping** assigns lanes to phase/outcome/event categories via `bandFor`, determining which layout coordinates apply
- **Phase column placement** uses `measureState` to center states on `layout.phaseXs` indices, with validation for out-of-range columns
- **Transition routing** combines explicit side selection, automatic port spreading, and seven routing algorithms in `routeVia`
- **Path caching** in `pathFor` improves performance for diagrams with repeated transition patterns
- **Configurable defaults** allow per-transition override of `channelX`, `channelY`, `cornerRadius`, and `via` points

## Frequently Asked Questions

### How do I position a state in a specific phase column?

Set the `col` property in your state definition to the desired column index (0-based). The renderer uses `layout.phaseXs[col]` as the horizontal center. For example, `"col": 2` positions the state at the third phase column coordinate.

### What happens if a state's column index is out of range?

`measureState` falls back to the last available column (`xs[xs.length - 1]`) while a separate validation pass (around lines 158–170) raises an error to alert you to the mismatch between your lifecycle definition and layout template.

### Can I control which side of a state a transition connects to?

Yes. Specify `fromSide` and/or `toSide` on the transition object with values like `"top"`, `"bottom"`, `"left"`, or `"right"`. If omitted, the renderer computes default sides based on the relative positions of the connected states.

### How does the renderer prevent overlapping transition lines?

The `automaticPortSpread` function distributes attachment points along state edges when multiple transitions share the same source or target. Additionally, you can use channel routes (`bottom-channel`, `right-channel`, etc.) or explicit `via` points to separate overlapping paths.