# Flow routing contract

The manifest graph owns routes. Components request an outcome; they do not choose a hard-coded destination.

<!-- funnelsgrove:generated:start contract-v3/contract/flow-routing -->
Step contract version: `3`

Contract hash: `d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf`

| Contract | Match | ID | Name | Type | Kind | Primary | Unique |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `email-capture` | `exact` | `email-capture` | `email-capture` | `form_input` | — | — | `true` |
| `manage-subscription` | `exact` | `manage-subscription` | `manage-subscription` | `subscription_management` | `manage-subscription` | — | `true` |
| `paywall` | `exact` | `paywall` | `paywall` | `paywall_offer` | `paywall` | `true` | `true` |
| `paywall-variant` | `pattern` | `^paywall-[a-z0-9-]+$` | `^paywall-[a-z0-9-]+$` | `paywall_offer` | `paywall` | `false` | `false` |
| `subscription-started` | `exact` | `subscription-started` | `subscription-started` | `purchase_completed` | `subscription-handoff` | — | `true` |

The `hardcoded-navigation` capability is owned by `manifest-flow-controller`.
<!-- funnelsgrove:generated:end contract-v3/contract/flow-routing -->

## Rules

- Give public paths meaningful product slugs.
- Keep `edgesByStepId` sources and targets as explicit string literals.
- Model conditional ownership in `branches`, including readable labels, tags, and owned step IDs.
- Keep every experiment control and variant as a real step with normal outgoing edges.
- Configure the email-to-offer edge in the manifest; the email component never names a paywall route.
- Keep inactive variants unreachable from the default flow rather than deleting their identity.

## Lifecycle-safe navigation

Use the flow controller so answer writes, semantic conversions, completion, and route commits stay ordered. A stale visit must not complete or navigate after a newer visit takes ownership.

For experiments, follow [the add-experiment recipe](../recipes/add-experiment.md). Run `fgrove validate` after any graph change.

## Asset and preload metadata

- Give each funnel-critical image a stable `funnelManifest.assets` entry and attach it through the owning step's `assetIds`; every ID must resolve.
- `assetIds` is routing/preload metadata, not an optimization opt-in. Use framework `priority` only for current first-viewport images.
- `preload: 'idle'` is effective only when a tested shell consumer exists. Warm likely next-step assets only, never the full funnel; do not claim preload behavior from metadata alone.
- Publish optimization is separate: the image-variants stage creates AVIF/WebP sidecars while retaining the original raster fallback.
