paperchain is a small TypeScript protocol library for scenes: flat, typed, geometry-free relations between addresses across paperdoll bodies.
paperdoll has two edge types, both hierarchical and both geometric: connection (ports) and containment (contains / element.body). paperchain adds the third and final edge type — relations. The motivating specimen is two humans holding hands: neither hand contains the other, and a port connection would be wrong, not just awkward, because connections drag geometry with them. Holding hands needs an edge with no geometric consequences.
It has one runtime dependency (paperdoll, the kernel it relates) and does not interpret semantics. holds means nothing to paperchain; kinds are declared in the document with their laws, and the protocol validates declared structure only.
npm install paperchainimport { PAPERCHAIN_PROTOCOL, parseScene, relationsAt } from "paperchain";
const scene = {
protocol: PAPERCHAIN_PROTOCOL,
bodies: {
alice: {
root: "torso",
vessels: {
torso: { ports: { left: { vessel: "left-hand", side: "right" } } },
"left-hand": { ports: { right: { vessel: "torso", side: "left" } } }
}
},
bob: {
root: "torso",
vessels: {
torso: { ports: { right: { vessel: "right-hand", side: "left" } } },
"right-hand": { ports: { left: { vessel: "torso", side: "right" } } }
}
}
},
kinds: {
"holding-hands": { symmetric: true, fromMax: 1 }
},
relations: [
{ kind: "holding-hands", from: "alice/left-hand", to: "bob/right-hand" }
]
};
const parsed = parseScene(scene);
if (!parsed.ok) throw new Error(parsed.errors[0].message);
console.log(relationsAt(parsed.value, "bob/right-hand"));
// [{ kind: "holding-hands", from: "alice/left-hand", to: "bob/right-hand" }]A scene is paperchain's document: named paperdoll bodies embedded by value, a set of declared relation kinds, and a flat table of relations. A scene is self-contained — "every endpoint resolves" is checkable with nothing else in hand, offline, in any language.
A relation endpoint is a scene address: <bodyName>/<paper-doll-v3-address>. The first segment names a body in bodies; the remainder is resolved within that body by paperdoll's address grammar, so endpoints may be vessels or id-bearing elements at any nesting depth (bob/back/field-pack/main-pocket/rope). A bare body name is not an endpoint — an address must reach at least a vessel.
Kinds carry their laws as declarations, exactly as accepts declares compatibility in paperdoll:
symmetric—holding-hands(a, b)≡holding-hands(b, a): one unordered relation.irreflexive—fromandtomust differ. Per-kind, never global: you can hold your own hand.fromMax/toMax— multiplicity budgets: "a hand holds at most one thing." Symmetric kinds usefromMaxas the per-endpoint budget (participation in either position counts); declaringtoMaxon a symmetric kind is itself invalid.
Scene validity is the conjunction of seven laws, with all violations collected and path-annotated ($.relations.3.from):
- Structure — strict shapes, known keys only, valid lowercase ids, exact protocol string.
- Body validity — every body in
bodiesis paper-doll/v3-valid, judged by the kernel itself; error paths are prefixed$.bodies.<name>. - Declared kinds — every
relation.kindexists inkinds. - Existence — every endpoint resolves: the body exists and the remainder resolves via paperdoll's
resolveAddress. - Irreflexivity — when declared on the kind,
from !== to. - No duplicates — relation identity is
(kind, from, to); symmetric kinds canonicalize the endpoint pair, so reversed duplicates are equally invalid. - Multiplicity — when declared, no endpoint exceeds its
fromMax/toMaxbudget for that kind.
Dangling relations are invalid, strictly: deleting a body out from under a relation does not auto-drop the relation, it invalidates the scene. Cleanup travels in the same transaction as the structural change — the rope drops when the arm is severed, as part of the severing.
Containment-shaped relations dissolve into paperdoll recursion. Trading needs a desk — a third body with escrow vessels — and zero relations; likewise chests, mounts-as-saddle-containment, docking-as-hangar. And there are no state-vetoes: no relation law inspects body contents ("may only grapple with a free hand" is consumer judgment, or eventually papermold's). paperchain validates structure: existence, multiplicity, symmetry, irreflexivity. Nothing else.
All operations return new scenes and never mutate their inputs. Destructive operations return what they destroyed, so every edit can be undone without diffing:
import { addRelation, declareKind, deleteBody, insertBody, removeRelation } from "paperchain";
const guarded = declareKind(scene, "guards", { irreflexive: true });
const linked = addRelation(guarded, { kind: "guards", from: "alice/torso", to: "bob/torso" });
const { scene: unlinked, relation } = removeRelation(linked, {
kind: "guards", from: "alice/torso", to: "bob/torso"
});
// relation: the removed relation exactly as stored (symmetric kinds match either order)
const { scene: smaller, body } = deleteBody(unlinked, "bob");
// body: the deleted paperdoll body, ready to re-insert
const { scene: fewer, declaration } = deleteKind(unlinked, "guards");
// declaration: the destroyed kind declaration; throws while relations still use itOperations enforce the local laws (declared kinds, existence, irreflexivity, duplicates, multiplicity, no dangling endpoints) and throw with precise messages. They do not re-validate whole bodies — global validity stays a validateScene concern, mirroring the kernel's local/global law split. deleteBody throws while any relation endpoint enters the body: remove relations first, in the same transaction.
paperdoll— the kernel: bodies, vessels, containment, connection, the address grammar (law 8 is what makes scene addresses possible).paperchain— this library: relations between addresses across bodies.paperfold— dynamics: transactional patches over bodies (scene targeting — where dangling-relation cleanup gives its transactions a real job — is the next phase).papermold— judgment: profiles and structural conformance ("is this really mech-shaped?").
The protocol is not the TypeScript library — it is the document format plus the laws. schema/paperchain-v1.schema.json is a JSON Schema (2020-12) capturing the structural laws; the laws beyond schema expressiveness are specified in docs/spec.md. Any language can validate paperchain scenes.
- constants:
PAPERCHAIN_PROTOCOL - validation:
parseScene,assertScene,validateScene,formatProtocolErrors(re-exported from paperdoll) - addressing:
parseSceneAddress,resolveSceneAddress - kind operations:
declareKind,deleteKind - body operations:
insertBody,deleteBody - relation operations:
addRelation,removeRelation - queries:
relationsAt - types:
Scene,Relation,KindDeclaration,SceneAddress,SplitSceneAddress,BodyName,KindId, plusBody,ProtocolError,ResolvedAddress, andResultre-exported from paperdoll
Validation is strict: unknown keys anywhere in a scene are rejected, all errors are collected with paths, and parseScene returns a deep copy of its input. ContainedElement.data remains opaque — paperchain never reads it.
See docs/rfc-paperchain.md for the pre-RFC that fixed the five hard decisions (recorded 2026-07-10), and docs/spec.md for the hardened v1 specification.