JEPA4Japan · tutorials

Chapter 5: Give the Pixel World a Registry

3,478 words 16 min read #Canvas#Frontend Engineering#Infinite Canvas#ELI5

Define stable IDs, a scene graph, shape/asset/binding records, and a domain-adapter boundary that stays independent of the renderer.

Course progress Course outline 18 of 18 lessons available

Part I: Choose the Surface Before You Draw—Product, Pixels, and Coordinates

  1. 01 Chapter 1: Do Not Draw Yet—Canvas Is Not a Product Architecture available now
  2. 02 Chapter 2: A Sheet of Pixels That Forgets available now
  3. 03 Chapter 3: Turn Drawing into a Replayable Recipe available now
  4. 04 Chapter 4: Four Maps and a Camera available now

Part II: Give the Pixel World a Brain—Model, Scheduling, Input, and Tools

  1. 05 Chapter 5: Give the Pixel World a Registry Current lesson
  2. 06 Chapter 6: Redraw Only When the Light Turns On—Render Scheduling and the React Boundary available now
  3. 07 Chapter 7: Mouse, Touch, and Pen Speak One Language available now
  4. 08 Chapter 8: Find the Big Box Before Inspecting the Edge available now
  5. 09 Chapter 9: Tools Are Traffic Lights, Not a Bag of Booleans available now

Part III: From “It Drags” to “It Is Trustworthy”—Interaction, Text, Assets, and Recovery

  1. 10 Chapter 10: Make the Editor Feel Right available now
  2. 11 Chapter 11: Drawn Text Is Not Editable Text available now
  3. 12 Chapter 12: Borrowed Images Cannot Be Packed Without Rules available now
  4. 13 Chapter 13: Time Machines and Old Boxes available now
  5. 14 Chapter 14: Looking Correct Is Not Being Correct available now

Part IV: Master-Level Decisions—Performance, Workers, GPU, SDKs, Collaboration, and AI

  1. 15 Chapter 15: Do Not Search Ten Thousand Children One by One available now
  2. 16 Chapter 16: Keep the Front Desk Out of the Kitchen—Worker and GPU Upgrades available now
  3. 17 Chapter 17: Build the Car or Buy a Proven Chassis? available now
  4. 18 Chapter 18: People and AI Edit the Same Ledger available now

Start with a game a five-year-old can understand

Take a family photograph, then place the photo and a stack of family-registry cards on a table. The photograph can tell you, “There are three people here.” The cards instead record each person’s name, an unchanging number, who belongs to which family, who stands in front of whom, and who is holding whose hand. Now cut one person out of the photograph and ask: do their name and family relationships automatically disappear from the cards? Replace the photograph with a watercolor painting. Do their numbers need to change?

First predict: if two children are both called “Blue,” can a name alone reliably find the same person? If one card says “My parent is B,” and B’s card says “My parent is A,” what happens when you keep following parents upward?

  1. Issue unique numbersIdentity does not change with appearance
  2. Record relationshipsParent-child, order, connections
  3. Inspect the registryReject orphans and cycles
  4. Choose a projectionCanvas or DOM
A photograph shows only today's appearance; the registry answers “who is this?”

The single truth of this chapter is: the Document Model is the source of truth; the Renderer is only a projection. Pixels contain no Stable ID, parent-child relationship, permission, connection, or business identity.

Translate the toys into Canvas engineering

Family-registry gameCanvas LabResponsibility
Entire registry bookDocumentRecordDocument version and page entry points
Each family pagePageRecordOne scene and its root Shape order
Person cardShapeRecordStable ID, type, parent, local transform, and properties
Family photo/attachmentAssetRecordMetadata for resources such as images
Holding handsBindingRecordSemantic connection between a Connector and target
Parents and childrenParent/Child Scene GraphLocal Transform becomes World along ancestors
Front and back rowsZ-OrderDetermines drawing and hit priority
List of photographersRenderer Registrytype → how to project it
Height-measuring toolGeometry Registrytype → geometry queries
Who is being pointed at todaySession SelectionDoes not belong to the durable Document

The analogy has limits. A Shape may have no real-world “parent,” and a Group is not a person. Several Shapes may refer to the same Asset, while a Binding is not Parent/Child. Real registry cards change infrequently; editor Documents are updated frequently by commands. The metaphor emphasizes identity and relationships, not a requirement to store every subtree as nested JSON. Runtime code uses Normalized Records so it can validate and update by ID.

Kill the misleading intuitions first

  • “If Canvas drew it, it is saved.” A Bitmap stores only colors. It cannot recover objects after reload or Resize, or when exporting semantic data.
  • “Putting CanvasRenderingContext2D in a Shape is convenient.” A Context cannot be serialized and locks the model to the Renderer. A Shape stores domain data only.
  • “Save Selection too so returning is convenient.” Selection, Hover, and camera usually belong to Session; multiplayer Presence is yet another layer. Mixing them into the Document contaminates Undo, collaboration, and persistence.
  • “Use an array index as the object ID.” Reordering, insertion, and collaboration change indexes. Identity must be stable and immutable.
  • “A Group can just store one absolute coordinate.” Children need Local Transforms. A World Transform is derived from the Scene Graph; storing both lets them drift apart.
  • “Drop unknown Shapes immediately.” If an old client opens data from a new client, silent deletion destroys the document. Isolate and preserve the original record while displaying a compatibility placeholder.

Production backpack

Prerequisite contract

The prerequisite is Chapter 4’s Matrix2D and its space naming. Canvas Lab specifies that the Document contains Durable State only. SessionState={selection,camera,activeTool} is separate, and Presence certainly does not enter the Document in this chapter. Every write passes through a Command Boundary. The Renderer receives a read-only snapshot. A Business Domain Object’s ID is retained after adaptation into a Canvas Record.

Formal knowledge

Stable IDs are immutable identities, not display names. A Document aggregates Pages, Shapes, Assets, and Bindings. A Page selects a visible scene. A Shape is a visual record with a type and properties. Groups form a Scene Graph through Parent/Child. Assets manage binary resources independently. Bindings express cross-Shape relationships such as Connectors and ports. Parent IDs and each Page’s root list together form a tree/forest, so cycles and dangling cross-Document references must be forbidden.

Z-Order must not rely on accidental object-iteration order. It can use the childIds order of each parent or an explicit comparable key, but ties require a deterministic tie-break. Local Transform is stored in the record; World Transform is Derived State from the ancestor chain. Selection Bounds, World Bounds, visibility, and cached Paths are also Derived State and are not written back into the source of truth.

A Domain Model describes business concepts such as orders, warehouses, and approvals. A Visual Model describes rectangles, text, and connector ports. An Adapter sits between them: Business Domain Object → Canvas Document Record → Geometry/Renderer → Pixels or DOM. One business object may produce several Shapes, while a Shape may be a note with no business meaning. The two cannot be equated directly.

Normalized Records can be represented on the wire as a record array or ID dictionary; runtime indexes use Map. Deserialization performs Schema Validation before building indexes and checking Duplicate IDs, unknown records, orphans, cycles, and ordering conflicts. Immutable Identity means “change ID” is not an ordinary Update. To copy, create a new ID and repair Bindings explicitly.

The Renderer Registry maps shape.type → ShapeRenderer; the Geometry Registry maps shape.type → ShapeGeometryProvider. When a Renderer is absent, Geometry and Document can still work, and the Renderer can return an Unknown Placeholder. A Command Boundary turns “I want to move this” into a MoveShapeCommand, validates permissions and preconditions, and commits atomically. This prevents UI or Renderer code from mutating a Map arbitrarily.

Evidence and compatibility

The core types here depend only on JavaScript data, not Canvas or DOM, so they run in Node tests. The Wire Format uses only JSON-representable values. Map, Set, DOMMatrix, and functions cannot be stored directly as a JSON Document. Even if you use structuredClone for an in-process snapshot, do not mistake it for a long-term persistence format.

Sources above were checked on 2026-08-29. Long-term compatibility is the responsibility of an explicit schema version and later Migrations. Never depend on a JS engine’s incidental object-property order or prototype.

This chapter’s engineering increment

Starting point: Chapter 3’s temporary Shape[] mixes in selectedIds. Finish line: Durable Document and Session are separate; five Record types, two Registries, validation, and a command interface run in a Node environment without Canvas.

canvas-lab/src/lab/ch05/
  records.ts
  document-store.ts
  registries.ts
  document-store.test.ts

Complete core types and validator:

export type Transform = Readonly<{
  x: number;
  y: number;
  rotation: number;
  scaleX: number;
  scaleY: number;
}>;
export type DocumentRecord = Readonly<{
  id: string;
  type: 'document';
  schemaVersion: 1;
  pageIds: readonly string[];
}>;
export type PageRecord = Readonly<{
  id: string;
  type: 'page';
  name: string;
  rootShapeIds: readonly string[];
}>;
export type ShapeRecord = Readonly<{
  id: string;
  type: 'shape';
  shapeType: string;
  pageId: string;
  parentId: string | null;
  childIds: readonly string[];
  transform: Transform;
  props: Readonly<Record<string, unknown>>;
  businessObjectId: string | null;
}>;
export type AssetRecord = Readonly<{
  id: string;
  type: 'asset';
  kind: 'image';
  source: string;
  width: number;
  height: number;
}>;
export type BindingRecord = Readonly<{
  id: string;
  type: 'binding';
  bindingType: string;
  fromShapeId: string;
  toShapeId: string;
  props: Readonly<Record<string, unknown>>;
}>;
export type AnyRecord = DocumentRecord | PageRecord | ShapeRecord | AssetRecord | BindingRecord;
export type WireDocument = Readonly<{ records: readonly AnyRecord[] }>;
export type RuntimeDocument = Readonly<{
  document: DocumentRecord;
  pages: ReadonlyMap<string, PageRecord>;
  shapes: ReadonlyMap<string, ShapeRecord>;
  assets: ReadonlyMap<string, AssetRecord>;
  bindings: ReadonlyMap<string, BindingRecord>;
  unknown: readonly AnyRecord[];
}>;
export type SessionState = {
  selectedShapeIds: ReadonlySet<string>;
  camera: { x: number; y: number; zoom: number };
  activeTool: string;
};

function isObject(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isStringList(value: unknown): value is readonly string[] {
  return Array.isArray(value) && value.every((item) => typeof item === 'string');
}
function assertUnique(label: string, values: readonly string[]): void {
  if (new Set(values).size !== values.length) throw new Error(`${label} contains duplicate IDs`);
}
function parseRecords(input: unknown): AnyRecord[] {
  if (!isObject(input) || !Array.isArray(input.records))
    throw new Error('Document payload must contain a records array');
  const records: AnyRecord[] = [];
  for (const value of input.records) {
    if (!isObject(value) || typeof value.id !== 'string' || value.id.length === 0)
      throw new Error('Every record needs a non-empty string id');
    if (value.type === 'document') {
      if (value.schemaVersion !== 1 || !isStringList(value.pageIds))
        throw new Error(`Invalid document record: ${value.id}`);
    } else if (value.type === 'page') {
      if (typeof value.name !== 'string' || !isStringList(value.rootShapeIds))
        throw new Error(`Invalid page record: ${value.id}`);
    } else if (value.type === 'shape') {
      const parentIsValid = value.parentId === null || typeof value.parentId === 'string';
      const businessIdIsValid =
        value.businessObjectId === null || typeof value.businessObjectId === 'string';
      const transform = value.transform;
      const transformIsValid =
        isObject(transform) &&
        ['x', 'y', 'rotation', 'scaleX', 'scaleY'].every((key) => Number.isFinite(transform[key]));
      if (
        typeof value.shapeType !== 'string' ||
        typeof value.pageId !== 'string' ||
        !parentIsValid ||
        !isStringList(value.childIds) ||
        !transformIsValid ||
        !isObject(value.props) ||
        !businessIdIsValid
      )
        throw new Error(`Invalid shape record: ${value.id}`);
    } else if (value.type === 'asset') {
      if (
        value.kind !== 'image' ||
        typeof value.source !== 'string' ||
        !Number.isFinite(value.width) ||
        !Number.isFinite(value.height)
      )
        throw new Error(`Invalid asset record: ${value.id}`);
    } else if (value.type === 'binding') {
      if (
        typeof value.bindingType !== 'string' ||
        typeof value.fromShapeId !== 'string' ||
        typeof value.toShapeId !== 'string' ||
        !isObject(value.props)
      )
        throw new Error(`Invalid binding record: ${value.id}`);
    } else {
      throw new Error(`Unknown record type on ${value.id}`);
    }
    records.push(value as AnyRecord);
  }
  return records;
}

export function loadDocument(wire: unknown, knownShapeTypes: ReadonlySet<string>): RuntimeDocument {
  const records = parseRecords(wire);
  const ids = new Set<string>();
  for (const record of records) {
    if (ids.has(record.id)) throw new Error(`Duplicate id: ${record.id}`);
    ids.add(record.id);
  }
  const documents = records.filter((r): r is DocumentRecord => r.type === 'document');
  if (documents.length !== 1) throw new Error('Exactly one document record is required');
  const pages = new Map(
    records.filter((r): r is PageRecord => r.type === 'page').map((r) => [r.id, r]),
  );
  const allShapes = records.filter((r): r is ShapeRecord => r.type === 'shape');
  const shapes = new Map(allShapes.map((r) => [r.id, r]));
  const assets = new Map(
    records.filter((r): r is AssetRecord => r.type === 'asset').map((r) => [r.id, r]),
  );
  const bindings = new Map(
    records.filter((r): r is BindingRecord => r.type === 'binding').map((r) => [r.id, r]),
  );
  assertUnique('document.pageIds', documents[0].pageIds);
  const documentPageIds = new Set(documents[0].pageIds);
  for (const pageId of documentPageIds)
    if (!pages.has(pageId)) throw new Error(`Missing page: ${pageId}`);
  for (const page of pages.values()) {
    if (!documentPageIds.has(page.id)) throw new Error(`Page ${page.id} is outside the document`);
    assertUnique(`Page ${page.id} rootShapeIds`, page.rootShapeIds);
    for (const rootId of page.rootShapeIds) {
      const root = shapes.get(rootId);
      if (!root || root.pageId !== page.id || root.parentId !== null)
        throw new Error(`Broken root order ${page.id} -> ${rootId}`);
    }
  }
  for (const shape of allShapes) {
    const page = pages.get(shape.pageId);
    if (!page || !documentPageIds.has(shape.pageId))
      throw new Error(`Shape ${shape.id} has missing page ${shape.pageId}`);
    assertUnique(`Shape ${shape.id} childIds`, shape.childIds);
    if (shape.parentId) {
      const parent = shapes.get(shape.parentId);
      if (!parent) throw new Error(`Shape ${shape.id} has missing parent ${shape.parentId}`);
      if (parent.pageId !== shape.pageId || !parent.childIds.includes(shape.id))
        throw new Error(`Broken parent link ${shape.parentId} -> ${shape.id}`);
      if (page.rootShapeIds.includes(shape.id))
        throw new Error(`Child shape ${shape.id} also appears in root order`);
    } else if (!page.rootShapeIds.includes(shape.id)) {
      throw new Error(`Root shape ${shape.id} is missing from page order`);
    }
    for (const childId of shape.childIds) {
      const child = shapes.get(childId);
      if (!child || child.parentId !== shape.id || child.pageId !== shape.pageId)
        throw new Error(`Broken child link ${shape.id} -> ${childId}`);
    }
  }
  const visiting = new Set<string>(),
    visited = new Set<string>();
  const visit = (id: string): void => {
    if (visiting.has(id)) throw new Error(`Group cycle at ${id}`);
    if (visited.has(id)) return;
    visiting.add(id);
    for (const child of shapes.get(id)?.childIds ?? []) visit(child);
    visiting.delete(id);
    visited.add(id);
  };
  for (const id of shapes.keys()) visit(id);
  for (const binding of bindings.values()) {
    if (!shapes.has(binding.fromShapeId) || !shapes.has(binding.toShapeId))
      throw new Error(`Binding ${binding.id} has a missing endpoint`);
  }
  const unknown = allShapes.filter((shape) => !knownShapeTypes.has(shape.shapeType));
  return { document: documents[0], pages, shapes, assets, bindings, unknown };
}

export type DocumentCommand =
  | { type: 'move-shape'; shapeId: string; dx: number; dy: number }
  | { type: 'delete-shape'; shapeId: string };

export function executeCommand(runtime: RuntimeDocument, command: DocumentCommand): WireDocument {
  if (command.type !== 'move-shape')
    throw new Error('Delete requires a cascade policy and is not implemented');
  const target = runtime.shapes.get(command.shapeId);
  if (!target) throw new Error(`Unknown shape: ${command.shapeId}`);
  const moved: ShapeRecord = {
    ...target,
    transform: {
      ...target.transform,
      x: target.transform.x + command.dx,
      y: target.transform.y + command.dy,
    },
  };
  const records: AnyRecord[] = [runtime.document, ...runtime.pages.values()];
  for (const shape of runtime.shapes.values()) records.push(shape.id === moved.id ? moved : shape);
  records.push(...runtime.assets.values(), ...runtime.bindings.values());
  return { records };
}

parseRecords is this chapter’s minimal runnable Schema Gate. Unknown record types, missing fields, and non-finite Transforms fail before Map creation; an unknown shapeType, in contrast, is preserved as a compatibility record. A production project can replace this code with Zod, Valibot, or JSON Schema, but it cannot remove the “validate first, normalize second” boundary. rootShapeIds/childIds also carry sibling Z-Order. Both uniqueness and bidirectional parent-child consistency must be checked, or “stable ordering” remains wishful thinking.

The delete command is deliberately rejected for now. Parent, Child, Binding, and Asset references require a cascade, reparent, or reject policy chosen by an ADR; they must not silently leave orphans. This is an “explicit boundary,” not an empty implementation pretending to be complete.

The Registry interface keeps Geometry independent of Canvas:

export type Bounds = { x: number; y: number; width: number; height: number };
export interface ShapeGeometryProvider {
  localBounds(shape: ShapeRecord): Bounds;
  containsLocalPoint(shape: ShapeRecord, point: { x: number; y: number }): boolean;
}
export interface ShapeRenderer<TTarget> {
  render(shape: ShapeRecord, target: TTarget): void;
}
export class Registry<T> {
  private entries = new Map<string, T>();
  constructor(private readonly fallback?: T) {}
  register(type: string, implementation: T): void {
    if (this.entries.has(type)) throw new Error(`Duplicate registry entry: ${type}`);
    this.entries.set(type, implementation);
  }
  resolve(type: string): T {
    const value = this.entries.get(type);
    if (value) return value;
    if (this.fallback) return this.fallback;
    throw new Error(`Missing registry entry: ${type}`);
  }
}

The Renderer Registry uses a compatibility renderer as its fallback. A Geometry Registry generally should not fabricate geometry, so it can omit a fallback and make the missing capability fail explicitly. This prevents “drawing an unknown placeholder” from being misread as “this unknown Shape can already be hit precisely.”

Tests cover duplicate IDs, cycles, identity, and Renderer replacement:

import { describe, expect, it } from 'vitest';
import { loadDocument, type AnyRecord, type SessionState, type WireDocument } from './records';
import { Registry } from './registries';

const base = (): AnyRecord[] => [
  { id: 'doc', type: 'document', schemaVersion: 1, pageIds: ['page'] },
  { id: 'page', type: 'page', name: 'Main', rootShapeIds: ['order'] },
  {
    id: 'order',
    type: 'shape',
    shapeType: 'business-card',
    pageId: 'page',
    parentId: null,
    childIds: [],
    transform: { x: 10, y: 20, rotation: 0, scaleX: 1, scaleY: 1 },
    props: { label: 'Order' },
    businessObjectId: 'order-42',
  },
];
describe('document invariants', () => {
  it('rejects duplicate immutable identities', () => {
    const records = base();
    records.push({ ...records[2] });
    expect(() => loadDocument({ records } as WireDocument, new Set(['business-card']))).toThrow(
      'Duplicate',
    );
  });
  it('rejects a group cycle', () => {
    const records = base();
    records[1] = { ...records[1], rootShapeIds: [] } as AnyRecord;
    records[2] = { ...records[2], parentId: 'group', childIds: ['group'] } as AnyRecord;
    records.push({
      id: 'group',
      type: 'shape',
      shapeType: 'group',
      pageId: 'page',
      parentId: 'order',
      childIds: ['order'],
      transform: { x: 0, y: 0, rotation: 0, scaleX: 1, scaleY: 1 },
      props: {},
      businessObjectId: null,
    });
    expect(() => loadDocument({ records }, new Set(['business-card', 'group']))).toThrow('cycle');
  });
  it('rejects duplicate root z-order entries before rendering', () => {
    const records = base();
    records[1] = { ...records[1], rootShapeIds: ['order', 'order'] } as AnyRecord;
    expect(() => loadDocument({ records }, new Set(['business-card']))).toThrow('duplicate');
  });
  it('preserves identity and Session when a renderer is replaced', () => {
    const runtime = loadDocument({ records: base() }, new Set(['business-card']));
    const session: SessionState = {
      selectedShapeIds: new Set(['order']),
      camera: { x: 0, y: 0, zoom: 1 },
      activeTool: 'select',
    };
    const before = JSON.stringify([...runtime.shapes.values()]);
    const canvas = new Registry<(id: string) => string>(() => 'unknown placeholder');
    const dom = new Registry<(id: string) => string>(() => '<div>unknown</div>');
    canvas.register('business-card', (id) => `canvas:${id}`);
    dom.register('business-card', (id) => `<article data-id="${id}" />`);
    expect(canvas.resolve('business-card')('order')).not.toBe(
      dom.resolve('business-card')('order'),
    );
    expect(JSON.stringify([...runtime.shapes.values()])).toBe(before);
    expect(runtime.shapes.get('order')?.businessObjectId).toBe('order-42');
    expect([...session.selectedShapeIds]).toEqual(['order']);
  });
  it('round-trips an unknown shape and resolves a compatibility renderer', () => {
    const records = base();
    records[2] = { ...records[2], shapeType: 'future-card', props: { future: 7 } } as AnyRecord;
    const runtime = loadDocument({ records }, new Set(['business-card']));
    const renderers = new Registry<(id: string) => string>((id) => `unknown:${id}`);
    expect(runtime.unknown.map((record) => record.id)).toEqual(['order']);
    expect(runtime.shapes.get('order')?.props).toEqual({ future: 7 });
    expect(renderers.resolve('future-card')('order')).toBe('unknown:order');
  });
});

Run npx vitest run src/lab/ch05/document-store.test.ts; expect 5 passed. Then rerun Chapter 3’s visual test: the Renderer now reads records from the new RuntimeDocument, and the pixels should remain unchanged. Return to the registry book: changing the photographer or art style does not change card numbers, and whoever you point at today is not written into a permanent archive.

Break it on purpose

InjectionSymptomEvidenceFixRegression testRecovery
Delete Parent without handling childrenChild Shape has no route to WorldValidator reports missing parentDefine reject/reparent/cascade policyTest every delete policyRestore from transaction snapshot
A→B→A Group cycleRecursion overflows or freezesDFS visiting set detects itReject cycles before commitTwo-node and self-cycle testsReject bad command
Duplicate ID in Wire arrayLater value silently overwrites earlier valueCount differs before normalizationCheck IDs before building Mapduplicate testIsolate bad payload
Duplicate/conflicting order under one ParentDrawing order is unstableTwo render traces differUse one childIds order and validate uniquenesspermutation testRebuild stable order
Unknown Shape typeBlank output or crashunknown list is nonemptyPreserve record, show placeholder, keep read-onlyround-trip unknown propsDo not delete original record
Missing RendererOnly that type cannot be projectedRegistry resolve reaches fallbackcompatibility rendererDocument/Geometry tests still runLoad fallback
Selection mixed into DocumentSave/collaboration produces ghost selectionschema contains selectedMove to SessionStorePersistence snapshot contains no selectionRemove field and migrate

For every case, follow “inject → observe symptom → validate error code/record ID → fix validation or transaction → run Node regression → restore from the last valid Document.” Do not let a Renderer try/catch swallow model corruption; that turns a factual error into apparent visual emptiness.

Pass with evidence

GateAutomated/manualEvidence
Document does not depend on RendererAutomatedRecords module has no DOM/Canvas import and Node tests pass
Replacing Renderer does not change DocumentAutomatedJSON is identical before and after Canvas/DOM implementations
Selection survives without contaminating persistenceAutomatedSession survives separately; Document snapshot has no selection
Persistence Format needs no rewriteAutomatedDifferent Registries read the same WireDocument
Business ID remains stableAutomated/manualStill order-42 after adapter/renderer swap
  • All five Record types, plus Group, Parent/Child, Z-Order, and Local Transform, have explicit contracts.
  • Domain Model, Visual Model, Geometry, and Renderer do not impersonate one another.
  • Normalized Records check duplicate IDs before building indexes.
  • Derived State and Session State are not written into the Durable Document.
  • Renderer/Geometry Registries are independently replaceable, with explicit compatibility behavior for missing implementations.
  • Deleting a Parent, cycles, unknown Shapes, conflicting order, and a missing Renderer have all been injected and recovered.

Explain it to a five-year-old

Without using the words “Document,” “Renderer,” “Scene Graph,” “Registry,” or “Derived State,” answer: Why should replacing a family photo with a cartoon not change a child’s identity-card number? Why should “who I am pointing at today” not be written into the permanent family registry?

Expand a good jargon-free answer

A photograph and a cartoon are two appearances of the same family. The child remains the same child; their name, number, and family relationships do not change with the drawing tool. Pointing at someone describes only what you are doing at this moment, and another person opening the book tomorrow should not inherit your gesture. We write identities and relationships that do not change with the drawing style on permanent cards, and put today's pointing on a separate temporary note. Then changing artists, closing the book, and reopening it never makes us confuse one person with another.