---
name: node-design-seam
description: Use when adopting or updating a design drop, wiring live application data into a designed stage/view, or reacting to any `node-design-seam verify` failure. Covers the adopt-wire-verify runbook, the hard rules that keep integration from restyling a drop, and what each of the four gate checks actually means. For designing or changing a surface rather than wiring one — a new card, panel, state or restyle — use the `drop-authoring` skill instead.
---

# node-design-seam

This is the integrator-side teaching layer for the design-seam framework: a vendored
design drop (a designer's hash-pinned `stage.html`/`stage.css`/view modules) plus a
closed write-vocabulary that lets a developer's agent push live data into it without
ever touching a design-owned file. The gates (`node-design-seam verify` in CI) enforce
that boundary as code; this skill exists so the first attempt is right instead of CI
teaching by rejection.

Run the CLI yourself at every step below — this is a loop, not a one-shot lookup.

## Runbook checklist

- [ ] **Adopt** a new/updated drop: `node-design-seam adopt <path-to-new-drop>` (or,
      if placing the folder by hand: copy it into place, then run `node-design-seam pin`
      followed by `node-design-seam bindings`). `adopt` prints the contract diff —
      slots added/removed since the previous drop. Read it.
- [ ] **Wire** the stage entry, in this order, lifecycle-paired to the stage's
      `mount(ctx)` / `unmount()` (never fire-and-forget):
      1. `mountDropFragment({ host, html, css })` — hosts the drop's own bytes verbatim.
      2. `createSeam({ root, bindings, views })` — one seam instance per mounted stage.
      3. Write `project` functions — plain data transforms from kernel view-models to
         the *design's* slot names. Never markup, never DOM.
      4. `wireStage(seam, ctx, { snapshot, views })` — subscribes `ctx.liveQuery` per
         entry and returns a disposer; call that disposer in `unmount()`.
- [ ] **Verify**: `node-design-seam verify`. Fix every failure before moving on —
      see the failure table below for what each one actually means.
- [ ] **Pixel side-by-side**: compare the mounted stage against design's own golden
      fixtures (shipped under the drop's `tools/`). A green `verify` proves the wiring
      is legal; it does not prove it looks right — only the visual comparison does.

## Hard rules

These are the ways integration could quietly turn into restyling. All of them are
either unenforceable-by-code or actively defeat the enforcement if broken — treat
them as absolute:

- **Never edit a file under the drop root.** Not to fix a typo, not "just this once."
  If something needs to change in the drop, that's a design change — escalate it.
- **Never regenerate the manifest (`node-design-seam pin`) to silence a failure.**
  A manifest change must always accompany an actual new drop arriving through
  `adopt`. Running `pin` against a drop you just hand-edited launders the edit.
- **Never hand-edit `bindings.gen.js`.** It is generated output; hand-edits are
  invisible to `bindings` and will be silently clobbered or caught by the
  freshness check — either way you've wasted the edit and hidden the intent.
- **Never add an attribute to the allowlist or a verb to the applier.** The
  three-form vocabulary (`text` / `attr` / `presence`) and the seven-attribute
  allowlist are the entire mechanism that makes "integration cannot restyle the
  design" a property of the code. Widening either one defeats the package.
- **Never prune bindings that share an anchor.** A drop may put several
  comma-separated `attr:slot` pairs on one `data-bind-attr`, and each pair is
  emitted as its own binding with the same anchor — the full literal attribute
  value. Three rows with an identical `anchor` and different `source`s is that
  form, working correctly, not a generator bug or a duplicate. The applier
  re-resolves the selector per binding and writes one attribute each. Deleting the
  "extras" deletes real writes, and the freshness check will fail on the edit.
- **Never invent a selector.** If the markup you need doesn't expose a
  `data-bind`/`data-bind-attr`/`data-bind-when`/`data-view-slot` anchor, that slot
  doesn't exist yet — ask design for it. Do not target a class name, an id, or
  structural position instead.
- **A legacy anchor must be attribute-selector-shaped.** Every entry in the config's
  `legacyAnchors` is validated at load: `anchor` must match `[name="value"]` — the
  same literal-token shape the generator itself always produces. `loadConfig` throws,
  naming the offending index, on anything else (a class selector, a descendant
  combinator, `:hover`, …). This isn't stylistic — `verify`'s anchor check is a plain
  substring test against the drop's text, so only that form can ever be found there.

## When the drop seems to need a verb you don't have

The "never widen the vocabulary" rule is only followable if you know the right
alternative. Three requests come up repeatedly and all three look, at first, like the
applier is missing something. None of them are:

| The request | Why it isn't a new verb | What to do instead |
| --- | --- | --- |
| "This bar's width / this chart's path has to come from the data." | Continuous geometry can't be a text or attribute write, and adding a `style` verb would hand integration full restyling power — the exact access the package removes. | It belongs in a view module, where design's own code builds the markup from data you feed it. If the drop shipped the chart as static markup, that's a drop bug: file an Ask. |
| "The value has a redaction marker / highlight / unit inside the string, and a text bind prints it raw." | `data-bind` sets `textContent` — by design it cannot emit markup, which is what stops integration from injecting elements into a drop. | Same answer: a view module parses the structure and renders it. Feed the raw string; let design's code segment it. |
| "The drop needs to follow the host's light/dark theme." | The drop is inside a shadow root and inherits nothing, so no CSS-only mechanism reaches it; `prefers-color-scheme` tracks the OS, not the host's theme control, and desyncs on manual override. | Feed the theme as data into a state slot the drop declares (`data-bind-attr="data-state:…"`), exactly like any other state. If the drop declares no theme slot, it supports one theme — ask design, don't force it. |

The shared shape: when something can't be expressed, the answer is either a view module
(design's code, design's markup) or a slot the drop declares. It is never a new verb and
never a selector you invent.

## Failure table

`node-design-seam verify` runs four checks plus a contract-version gate. Each
failure line is prefixed with the check name — use that prefix to find the right
row below. None of these are fixed by editing the drop.

| Check | What it actually means | Correct response |
| --- | --- | --- |
| `integrity` | A drop file's hash no longer matches the pinned manifest — either someone edited a file under the drop root, or a new drop arrived without going through `adopt`. | Revert the edit if the drop wasn't meant to change; if a new drop really did arrive, run `node-design-seam adopt <path>` (or `pin` + `bindings`) properly instead of hand-patching. |
| `anchors` | A binding's anchor selector no longer resolves against the drop's markup — the drop's own contract (its `data-bind*` attributes) changed. | Re-run `node-design-seam bindings` and reconcile the resulting diff with design; do not "fix" the selector by hand or repoint it at a different element. |
| `contract` (scan error) | The drop's markup can't be read — an attribute outside the allowlist, a malformed `attr:slot` pair, a quote in a slot name, a view module with no `id`. The failure line names the file. | A drop bug: you cannot fix it on your side, and `bindings` refuses to write the table until it is. File an Ask with design. |
| `contract` (coverage) | Coverage mismatch — either a declared slot in the drop has no binding and isn't in `unwired`, or a binding targets a slot the drop no longer declares. | Wire the new slot (finish the runbook's wiring step), or explicitly record it in the config's `unwired` list as tracked debt. Never delete the binding to make the check pass unless the slot is genuinely gone. If it appeared right after upgrading the package, read "After a package upgrade" below first. |
| `freshness` | The committed `bindings.gen.js` doesn't match a fresh regeneration — almost always a hand-edit of generated output. | Run `node-design-seam bindings` and commit the result. Never hand-edit `bindings.gen.js`. If it appeared right after upgrading the package with no drop change and no edit of yours, that is expected — see below. |
| `contract-version` | The drop's `AGENTS.md` declares a `contract:` version newer than this CLI supports — the drop was authored against a newer write-vocabulary than you have installed. | Upgrade `@econ-v1/node-design-seam` to a version that supports the declared contract; do not attempt to wire the drop against an older vocabulary. |

## After a package upgrade

A scanner fix changes what the drop is understood to declare, without a byte of the
drop changing. `integrity` still passes — the hashes are untouched — while
`freshness` and `contract` both fail, and regenerating with
`node-design-seam bindings` leaves a diff showing slots you have never seen. That
combination is the fix landing, not a regression.

Upgrading across **0.1.5** does exactly this to any drop that puts several
comma-separated pairs on one `data-bind-attr`. Before 0.1.5 the scanner read that
whole attribute value as a single binding whose `source` was the rest of the string
verbatim — `obs.a, data-count:obs.b, aria-valuenow:obs.c`. No snapshot supplies a
slot by that name, so every attribute on that element was dead and kept the
designer's fixture literal in production. `verify` reported full coverage throughout,
because the declared-slot list and the bindings list were both derived from that same
misreading and agreed with each other.

So when new slots appear on upgrade:

1. Regenerate (`node-design-seam bindings`) and read the diff as a contract diff.
2. Treat every newly visible slot as **unwired, and previously broken** — not as new
   design work. Look at the element in the drop: whatever it renders today is the
   fixture value, frozen.
3. Wire them. Reaching for `unwired` here records as "tracked debt" something that is
   actually a live defect on the surface — only use it if you would have deferred
   that slot anyway.
4. A `source` containing a comma anywhere in a committed `bindings.gen.js` is the
   pre-0.1.5 signature. `grep 'source: "[^"]*,' <bindingsOut>` finds it.

## Escalation

- **You need a design change** (a missing slot, a layout that can't express a real
  state, a selector that doesn't exist): file an Ask with the design side (in the
  node monorepo: `docs/design/asks/`; out-of-repo apps: the design engagement's
  agreed channel) describing what's needed. Never edit the drop to work around it,
  and never invent a selector to route around the gap.
- **Design shipped a slot you're not using yet**: either wire it now, or record it
  visibly in the config's `unwired` list so `verify`'s coverage report stays honest
  about the debt. Silence (an unbound, unlisted slot) is what the `contract` check
  exists to catch.
