# @econ-v1/node-design-seam

[![npm](https://img.shields.io/npm/v/@econ-v1/node-design-seam.svg)](https://www.npmjs.com/package/@econ-v1/node-design-seam)

Vendored design drops + data-slot wiring. A designer ships a folder of hash-pinned
files — markup, CSS, view modules — that the product renders byte-for-byte.
Integration binds live kernel data into it through a write vocabulary too narrow to
carry a restyle: set text, set one allowlisted attribute, toggle a state attribute
the design itself declared. Nothing in that vocabulary can write a class, a style,
or an element. The CLI (`pin` / `bindings` / `verify` / `adopt`) is the CI gate that
keeps the tree honest about it, and `preview` lets the designer render the drop
through that same runtime before anyone integrates it.

This package ships three things:

- a small runtime (`createSeam`, `mountDropFragment`, `wireStage`, `applyBindings`)
  that a stage's own entry module calls to mount a drop and feed it data;
- a CLI (`node-design-seam`) that hashes, scans, and verifies a drop against a
  per-app config file;
- a teaching layer — the docs below, a skill for each side of the seam
  (`drop-authoring` for the designing agent, `node-design-seam` for the integrating
  one), and an `AGENTS.md` template that travels inside the drop.

## Documentation

Full docs live in [`docs/`](docs/README.md) and ship inside the published tarball.

| | |
| --- | --- |
| [Getting started](docs/getting-started.md) | install → adopt → wire → verify, end to end |
| [Working in parallel](docs/workflow.md) | designer previews with no stack, developer wires with no data |
| [Concepts](docs/concepts.md) | drops, slots, the seam, and the two hash layers |
| [Write vocabulary](docs/write-vocabulary.md) | the three forms, the seven attributes, and what they can't reach |
| [Configuration](docs/configuration.md) | every config key, its default, and what rejects it |
| [CLI](docs/cli.md) | the four commands: flags, output, exit codes |
| [Runtime API](docs/api.md) | full signatures and types |
| [Troubleshooting](docs/troubleshooting.md) | a decoder for every `verify` failure |
| [CHANGELOG](CHANGELOG.md) | **read before upgrading** — three past releases failed silently |

Two agent skills and one template ship alongside, each written for one side of the seam:

| | |
| --- | --- |
| `skills/drop-authoring/SKILL.md` | **designer** — is this a drop change at all, the authoring sequence, the five fixture states, verification before hand-off |
| `skill/SKILL.md` | **integrator** — the adopt/wire/verify runbook, the hard rules, a decoder for every gate failure |
| `templates/AGENTS.stage-drop.md` | the drop contract itself: copied into a new drop's root as `AGENTS.md`, carrying the `contract:` line the CLI version-gates against |

## Install

```sh
npm i -D @econ-v1/node-design-seam
```

Node 18+. The CLI is invoked via the `node-design-seam` bin it installs; the runtime
is imported at the top of a stage's entry module:

```js
import { createSeam, mountDropFragment, wireStage } from "@econ-v1/node-design-seam";
```

TypeScript declarations ship with the package (generated from the same JSDoc
`checkJs` type-checks — see [Runtime API](docs/api.md#types)), so this resolves
with no `@ts-expect-error`, `Binding` included.

## Quick start

A stage entry mounts the drop's own bytes, creates a seam over the mounted root, and
wires kernel live queries into it — lifecycle-paired to `mount(ctx)` / `unmount()`:

```js
// ui/src/main.js — the stage entry (developer-owned)
import { createSeam, mountDropFragment, wireStage } from "@econ-v1/node-design-seam";
import BINDINGS from "./design/bindings.gen.js";
import cards from "../../design/wallet-stage-v1/views/cards.view.js";
import DROP_HTML from "../../design/wallet-stage-v1/stage.html?raw";
import DROP_CSS from "../../design/wallet-stage-v1/stage.css?raw";

let seam, unwire;

export async function mount(ctx) {
  const { root } = mountDropFragment({ host: ctx.host.root, html: DROP_HTML, css: [DROP_CSS] });
  seam = createSeam({ root, bindings: BINDINGS, views: [cards] });
  unwire = wireStage(seam, ctx, {
    snapshot: { query: "wallet.summary.v1", params: {}, project: toSummarySnapshot },
    views: { "wallet.cards": { query: "wallet.cards.v1", params: {}, project: toCardsModel } },
  });
}

export async function unmount() {
  unwire?.();
  seam?.dispose();
}
```

`mountDropFragment` hosts the drop's `html`/`css` verbatim, by default inside a shadow
root. `createSeam` returns an instance scoped to that root — `update(snapshot)` drives
the scalar bindings, `feedView(id, data)` re-renders a view module into the element
carrying its `data-view-slot="<id>"`. `wireStage` is optional sugar returning a
disposer for `unmount()`.

The `project` functions are the kernel→slot mapping. They conform kernel view-models
to the *design's* slot names, and must return plain data, never markup.

Walked through step by step in [Getting started](docs/getting-started.md); full
signatures in the [Runtime API](docs/api.md).

## The write vocabulary

The design declares its slots in its own markup. The applier can do nothing that
isn't one of these three forms:

| In the drop's markup | What integration may do |
| --- | --- |
| `data-bind="slot.path"` | replace the element's `textContent` — never its markup |
| `data-bind-attr="attrName:slot.path"` | set an allowlisted attribute (several comma-separated pairs allowed) |
| `data-bind-when="slot.path"` | toggle `hidden` when the value is falsy |

The allowlist is exactly seven names — `aria-label`, `aria-valuenow`, `data-count`,
`data-state`, `hidden`, `href`, `title`. `class`, `style` and `id` are excluded and
the applier has no branch that writes them. That absence is the mechanism: "integration
cannot restyle the design" is a property of the code, not a rule to remember.

Continuous geometry (chart paths, bar widths), values carrying structure inside a
string, and theme cannot be expressed this way — and the answer is never a new verb.
See [Write vocabulary](docs/write-vocabulary.md).

## Configuration

One `node-design-seam.config.json` per app repo. Three keys are required:

```json
{
  "dropRoot": "design/wallet-stage-v1",
  "manifestOut": "ui/src/design/drop-manifest.js",
  "bindingsOut": "ui/src/design/bindings.gen.js"
}
```

`notDropContent`, `legacyAnchors` and `unwired` default sensibly. All paths resolve
relative to the config file. Pass a different path with `--config <path>`.
Full reference: [Configuration](docs/configuration.md).

## Commands

| | |
| --- | --- |
| `node-design-seam pin` | hashes every drop file into `manifestOut`. Adoption only — regenerating it to silence a failing `verify` is the one canonical misuse |
| `node-design-seam bindings` | scans the drop's `data-bind*` attributes into `bindingsOut`; refuses to write on any scan error |
| `node-design-seam verify` | the CI gate: integrity, anchors, contract coverage, freshness, plus the contract-version gate |
| `node-design-seam adopt <path>` | the runbook as one command — place, re-point, pin, regenerate, and print the contract diff |
| `node-design-seam preview` | serves the drop through the real runtime for authoring and goldens; `--drop <path>` needs no config |

```json
"scripts": {
  "build": "vite build && node-design-seam verify",
  "verify:design": "node-design-seam verify"
}
```

Full reference: [CLI](docs/cli.md).

## Two hash layers, two lifecycles

The seam manifest (`manifestOut`, from `pin`) records *what design shipped*: it
changes only at adoption and gates the source tree. An app manifest's `ui.integrity`
records *what this build ships*: restamped every build, gating delivery and the
shell's cache verification on load. `verify` runs **before** stamping, so a tampered
drop can never launder itself into a validly-stamped package — the build fails at the
seam gate first. Detail in [Concepts](docs/concepts.md#two-hash-layers-two-lifecycles).

## Contributing

Gates, the rules a contributor must not break, and the release process are in
[CONTRIBUTING.md](CONTRIBUTING.md).

## License

[GNU Affero General Public License v3.0 only](LICENSE) (`AGPL-3.0-only`).

Versions 0.1.1 through 0.1.7 were published under `MIT OR Apache-2.0`; that grant
still stands for those releases.

## Further reading

- Framework design (locked): `docs/superpowers/specs/2026-08-10-design-seam-framework-design.md` in the `econ-v1/node` repo.
- The contract this package implements: `docs/design/DATA_SLOT_CONTRACT.md` in the `econ-v1/node` repo.
