# Working in parallel

The seam is a boundary in the code, but its point is organisational: **a designer and
a developer can work the same surface at the same time, neither blocked on the
other, and still integrate without a reconciliation phase.**

Neither side needs the other's environment. A designer never runs the product. A
developer never waits for a redraw to start wiring. This page is how that actually
works, and where it is thinner than it looks.

## What makes it possible

Three artifacts carry everything the two sides need from each other. All three live
in the drop — none of them is a conversation:

| Artifact | Direction | What it settles |
| --- | --- | --- |
| The `data-bind*` attributes | designer → developer | which slots exist, what kind of write each takes, what they are called |
| The fixture literals | designer → developer | the *shape and formatting* of every value, by example |
| The hash manifest + `verify` | both ways | that neither side moved the boundary while the other wasn't looking |

The second one does the most work and is the easiest to miss. This line:

```html
<span class="kpi" data-bind="obs.health.tracesRetained">1,204</span>
```

tells a developer the slot name, that it is a text write, and that the value arrives
**already formatted** — thousands separator included, because formatting is the
design's call and `project` is where it happens. No spec document states that. The
markup does.

## The designer's loop

```sh
npx node-design-seam preview --drop design/wallet-stage-v1
```

That serves the drop at `http://127.0.0.1:4321` with a toolbar across the top:
**fixture state** × **theme** × **host width**. No config file, no app, no product,
no kernel, no developer. The loop is: edit `stage.css`, refresh, click through the
five states, capture goldens.

Nothing is written into the drop — the preview reads it and serves it from outside,
so the hashes `pin` recorded still hold and `verify` still passes while a preview is
open.

### It drives the real runtime

The served page imports `mountDropFragment` and `createSeam` from this package and
drives them exactly as a stage entry does. It contains **no binder of its own**:
there is no code in it that reads a `data-bind*` attribute and decides what to do.

That matters more than it sounds. Before this command every drop hand-wrote its
harness, and each one re-implemented the write vocabulary from scratch. A harness
that disagrees with `src/apply.js` produces a golden that agrees with *the harness* —
so the pixel comparison, which is the designer's entire acceptance surface, quietly
stops describing the product. There is now no second implementation to drift.

The bindings come from the same scan `bindings` and `verify` run, so a drop can be
previewed **before it has ever been adopted** — which is the state a designer is
actually in.

### Fixture states

View modules need nothing: each is fed its own `fixtures[state]`, which the drop
contract already requires. If a module is missing the selected state the preview
says so in a status line rather than rendering something misleading — a direct check
against the five-state rule.

Scalar slots show the literals authored in the markup, which *is* the `default`
state by definition. To exercise the others, a drop may ship an optional
`tools/preview-states.js`:

```js
// snapshots in the shape seam.update() takes — the same shape a project() returns
export const states = {
  default: { wallet: { summary: { total: "1,204" } } },
  loading: { wallet: { summary: { total: "—" } } },
  empty:   { wallet: { summary: { total: "0" } } },
  error:   { wallet: { summary: { total: "—" } } },
  overflow:{ wallet: { summary: { total: "9,999,999" } } },
};

// optional: merged over the selected state, so themes don't multiply the entries
export const themes = { light: { "wallet.theme": "light" }, dark: { "wallet.theme": "dark" } };
```

Each state re-mounts the fragment from the drop's own bytes before applying, so a
state never inherits writes from the one before it — the same first paint the
product performs.

Other flags: `--port <n>`, and `--html <rel-path>` if the fragment isn't
`stage.html`. Full reference in the [CLI](cli.md#preview).

### Goldens

Capture one per fixture state, in a real browser at a fixed viewport — never
headless, never against a degraded reference. A golden written on a first run
against a missing baseline is a bootstrap, not a comparison; say so in the hand-off.

### Five states, and the one that gets skipped

Every view module ships `default`, `loading`, `empty`, `error` and `overflow`.

`overflow` is the one people skip and the one that breaks layouts. Design it against
the real worst case — longest real strings, fullest real lists, longest real
identifiers, largest realistic counts. If a real value could exceed the `overflow`
fixture, the fixture is what's wrong, and production is where you find out.

`loading` and `error` matter for a blunter reason: **a state with no design gets
invented during integration**, under deadline, by whoever is wiring data. Every
improvised element in a shipped product traces back to a state nobody drew.

## The developer's loop

Three things unblock the developer independently.

**A mounted drop renders itself.** `mountDropFragment` injects `stage.html`
verbatim, so a stage that mounts and never calls `update()` shows the designer's
fixture literals. That is a working screen before a single query exists.
`seam.renderFixtures()` does the same for view modules, feeding each one its own
`fixtures.default`.

```js
const { root } = mountDropFragment({ host, html: DROP_HTML, css: [DROP_CSS] });
const seam = createSeam({ root, bindings: BINDINGS, views: [cards] });
seam.renderFixtures();          // views render from the drop's own fixtures
// no wireStage, no kernel — the scalar slots still show the design's literals
```

**The fixtures are the shape-by-example.** `project` functions can be written
against them before the real endpoint exists — the slot names are known, the
formatting is demonstrated, and the five states tell you which shapes must be
survivable.

**`verify` is entirely static.** Hashes plus a text scan: no browser, no runtime, no
data, no network. It runs in CI on a machine that has never rendered the drop, and
it runs on the developer's laptop in under a second.

So the developer's loop is: adopt the drop, wire against fixtures, `verify`, and
swap in real queries whenever they land — with the fixture render as the fallback
the whole time.

## Where the two loops meet

Only twice, and both are one-directional hand-offs rather than meetings.

**Design → integration: a new drop folder.** The hand-off carries `AGENTS.md`
forward with its `contract:` line intact, every asset inside the folder, and a slot
inventory saying what was declared, what changed, and what was deliberately left a
literal.

**Integration → design: an Ask.** A missing slot, a state that cannot be expressed,
a chart shipped as static markup. Never an edit to the drop, and never an invented
selector.

`node-design-seam adopt` is what turns the first hand-off into work. It prints the
**contract diff** — the slots added and removed since the previous drop:

```
adopt: observability-dashboard-v2 in place.
  contract diff — added: [obs.health.queue.step] removed: [obs.health.legacyTotal]
  next: wire added slots (or record in unwired), delete bindings for removed ones, run verify.
```

That diff is the entire integration to-do list, generated rather than written. It is
also why neither side has to announce a change: a rename shows up as one slot added
and one removed, and `verify` fails until both are dealt with.

## What stops them drifting apart

Nobody is trusted to remember the boundary:

- A designer renames a slot → **`anchors`** fails at adoption, naming the binding
  that no longer resolves. It never becomes a blank value in production.
- A designer adds a slot integration hasn't picked up → **`contract`** fails, and
  coverage prints `12/13` on every run until it is wired or recorded in `unwired`.
- Anyone edits a drop file → **`integrity`** fails on the hash.
- Anyone hand-edits the generated bindings → **`freshness`** fails.

None of those require the other person to be available, or even to know.

## Migrating a hand-written harness

Drops authored before `preview` existed ship their own `tools/preview.html`. Those
still work, but they are the drift risk the command removes, and they should go.

The concrete hazard, from the first drop's harness:

```js
el.hidden = !(fx.when?.[slot] ?? false);   // a missing key renders hidden
```

The real applier skips a source that is **absent** from the snapshot, leaving the
element exactly as the design shipped it — which for an element authored without
`hidden` means *visible*. The harness hides it. Same drop, same fixture, opposite
result, and the golden would side with the harness.

In that drop it is latent: every state defines every `when` key, so its goldens are
correct. It is a trap armed for the next drop whose fixtures are sparser.

To migrate: translate `tools/fixtures.js` into `tools/preview-states.js` (flat
`text`/`attrs`/`when` maps become one snapshot per state), delete `tools/preview.html`,
re-capture goldens through `node-design-seam preview`, and re-run `pin` — the drop's
hashes change because its files did, which is a normal adoption, not a laundered edit.

Note that a hand-written `tools/preview.html` is also a live scan hazard: it sits
under `dropRoot`, so a literal `data-bind="…"` written as an example anywhere in it
mints a phantom slot that `verify` then demands you bind. The served preview cannot
do that, because it never lives in the drop.

## One gap that remains

### The two fixture formats don't meet

The designer writes flat maps keyed by slot:

```js
{ text: { 'obs.health.errorRate': '0.8%' }, attrs: { … }, when: { … } }
```

The developer's snapshot is a nested object walked by dotted path (see
[how a source is resolved](api.md#how-a-source-is-resolved)):

```js
{ obs: { health: { errorRate: '0.8%' } } }
```

So `tools/fixtures.js` — the most precise statement of the contract that exists
anywhere — cannot be loaded as a test snapshot. Every developer retypes it into
`project()` by hand, which is exactly where a formatting mismatch enters and only
the pixel comparison catches it.

**Until an adapter exists:** when a `project` function is finished, diff its output
against the drop's `default` fixture by eye, slot by slot. That is the check the
formats currently prevent from being automated.

## Next

- Doing it for the first time → [Getting started](getting-started.md)
- Deciding where something belongs → [Write vocabulary](write-vocabulary.md)
- A gate is failing → [Troubleshooting](troubleshooting.md)
