@econ-v1/node-design-seam
v0.1.8
Published
Vendored design drops + data-slot wiring: the closed-vocabulary seam runtime and the pin/bindings/verify CLI
Readme
@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-authoringfor the designing agent,node-design-seamfor the integrating one), and anAGENTS.mdtemplate that travels inside the drop.
Documentation
Full docs live in docs/ and ship inside the published tarball.
| | |
| --- | --- |
| Getting started | install → adopt → wire → verify, end to end |
| Working in parallel | designer previews with no stack, developer wires with no data |
| Concepts | drops, slots, the seam, and the two hash layers |
| Write vocabulary | the three forms, the seven attributes, and what they can't reach |
| Configuration | every config key, its default, and what rejects it |
| CLI | the four commands: flags, output, exit codes |
| Runtime API | full signatures and types |
| Troubleshooting | a decoder for every verify failure |
| CHANGELOG | 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
npm i -D @econ-v1/node-design-seamNode 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:
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), 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():
// 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; full signatures in the Runtime API.
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.
Configuration
One node-design-seam.config.json per app repo. Three keys are required:
{
"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.
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 |
"scripts": {
"build": "vite build && node-design-seam verify",
"verify:design": "node-design-seam verify"
}Full reference: CLI.
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.
Contributing
Gates, the rules a contributor must not break, and the release process are in CONTRIBUTING.md.
License
GNU Affero General Public License v3.0 only (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.mdin theecon-v1/noderepo. - The contract this package implements:
docs/design/DATA_SLOT_CONTRACT.mdin theecon-v1/noderepo.
