ADR-0013 — Federation: many models, explicit per-model state

Context

Today this viewer holds one model. viewport.showModel(meshes, resolveGuid, modelId) replaces whatever was there, disposeScenes it, and clears the selection. createMassingViewer takes a single modelId. There is no way to hold an architectural model and a structural model at once, which is the ordinary condition of every real coordination session.

This is the one genuine architectural gap the 2026-08-09 platform review found. Everything else the review proposed either exists in a sibling repository or was rejected for cause; federation is absent and needed.

Decision

A federation manager with explicit per-model state — not scene groups.

Models are registered, not swapped. Each carries its own state:

Identity stays GlobalId-keyed across models. This is not a preference: massing's planPaneSelection and specPane already cross modes by GlobalId, and @massing/markup anchors topics to GlobalIds. A federation that introduced a per-model transient id as the cross-boundary identity would break both. Where a GlobalId collides across models — and it does, because two consultants can export the same GUID — the pair (modelId, guid) is the key, with modelId first so a single-model host reads unchanged.

Consequences

A breaking change to the viewport API, batched with ADR-0012. showModel becomes add/remove/update against a registry. ADR-0012 makes createViewport async for WebGPURenderer.init(). Both break the same surface, massing is mid-adoption, so they ship together and the consumer absorbs one break rather than two. This is the whole reason they are sequenced adjacently in docs/roadmap.md rather than by appeal.

showMeshes generalises rather than changes meaning. Added 2026-08-09 for a host that never hands over IFC text, it already takes an explicit statement of how the kernel learns about the model. Federation adds which model, which is the same shape of explicitness — and the reason that field was made required rather than defaulted applies again: with several models loaded, "the kernel's current model" stops being a well-defined idea at all.

The memory gate becomes more load-bearing, not less. It asserts that twenty model replacements leave exactly the same GPU buffer count. Under federation the equivalent invariant is that adding and removing a model returns to baseline, per model — and with several models resident, a leak per add is far easier to introduce and far harder to notice. The gate extends to add/remove cycles.

Per-model state is a migration surface. Saved views, markup and drawings all become model-scoped. Every persisted artefact already carries {schemaVersion, data} with one pure vN→vN+1 step per version, and a golden fixture of each old format committed while it was current. That machinery exists precisely for this.

Alternatives rejected