Provisional plan — recorded 2026-08-16 from the review of WebAssembly/component-model#686 (the web-embedding design sketch: components bind directly to WebIDL, imports still wired explicitly by the instantiator). Adopted as the working posture for WIT surface design; converted to a ruling only by the checkpoints at the end. Related: NOTES.md — Why components, App-frame sandboxing; issues #5, #6, #13; upstream tracking in lann/deltic#115.
Scope of relevance
686 (and the Firefox prototype behind it) matters to polymorph only as motivation for future-compatible API shapes. The bet, in one line:
Assume WIT interfaces that mirror WebIDL will eventually get engine-native fast paths; interfaces that don't, won't.
Not a dependency, not a timeline bet: deltic remains the mechanism, the linker remains the permission system, and nothing assumes the embedding ships on the target browser matrix. The sketch's own gaps (no attribute getters/setters, no callbacks, no dynamic union passing) mean event-driven DOM is not natively expressible yet regardless — the RPC/mutation UI architecture stands on its merits.
Two properties of the sketch make the bet safe to take:
- No ambient authority. Every import resolves by lookup on an imports object the instantiator constructs; interposition (JS polyfill in place of a web API) is a stated goal. Permission-as-linker survives the native embedding unchanged: deny-by-unlink still fails at instantiation (unresolvable import →
TypeError), stubs and async prompt-on-first-use imports keep working.
- The import list stays the boundary even under wholesale handover. Passing a real interface object satisfies only the
[method] imports the component declared; the WIT world remains the install-time consent surface. Audit weight shifts to return types and object scope, not method reachability.
Shape rules
For any surface that could ever be satisfied by handing over a platform object ("pass-through-able"):
- Mirror WebIDL names and signatures exactly. The sketch resolves
[method] imports by camelCase(kebab-name) lookup on the interface object's prototype: resource element { query-selector: ... } can someday be satisfied by literally passing Element; a renamed method is JS-glue forever. Deviate from platform naming only where policy demands it, knowing each deviation pins that method to the wrapper path.
- Platform-mirror interfaces are pure subsets. One framework-specific method on a mirrored resource poisons wholesale handover (the extra import resolves to
undefined on the real prototype → instantiation TypeError). Extensions live in sibling interfaces. Subsetting in the other direction is free: the embedding drops extra args and defaults missing ones, so mirrors don't chase optional-parameter growth.
- Stay inside the canonical type table.
string/list/record (camelCase-able field names)/enum (labels map verbatim — no case conversion)/option for nullables/f64 for unrestricted doubles; top-level result<T, E> on throwing functions (maps to exceptions; deltic already aligned via lann/deltic#114). Avoid as API currency: dynamic unions, option<option<T>> (degrades to the generic variant case), error-context (unresolved upstream).
- Handles are
resources — never integer indices or string IDs. Today they're deltic table entries; under native bindings they're host handles. Same unforgeability, zero migration. (Supersedes the earlier u32 asset-index idea: resource asset gives launder-by-construction in future-compatible form.)
- Events as
stream<record>/future<T>, not callback imports. Callbacks are upstream open question 4; streams map to ReadableStream, futures to Promise, and record-shaped events are what the RPC topology wants anyway. Attributes (open question 6) get mechanical get-x/set-x pairs; accept a tooling-level rename if upstream lands a different convention.
- Glue-era optimizations stay out of the contract. Binary mutation encodings, shared-memory rings, batch-shaped APIs: their benefit evaporates under native bindings. Batch host-side (e.g. queue-and-flush per animation frame in the wrapper), never API-side.
The tension, and why it resolves cleanly here
The future-fast shape (DOM-mirror, per-call) is the present-slow shape — the Mozilla Dodrio experiment's 45% was precisely the cost of chatty per-op glue. But in this topology the component boundary is not the bottleneck: every mutation crosses postMessage into the UI frame regardless, and host-side per-frame batching hides per-call overhead at UI rates. A DOM-shaped polymorph:dom world costs little today.
The payoff is placement, not just speed: with native bindings, the same app artifact could instantiate in-frame against real, subtree-scoped elements — collapsing the two-hop RPC path. The existing design already pays for the safety: the sandbox origin's header CSP makes the frame network-dead, so confinement never depended on wrapper-side laundering; under in-frame placement the wrappers' remaining duties degrade to integrity/anti-spoofing, not exfiltration control. Same "one artifact, many placements" argument as headless execution, extended across the trust topology.
Scope split
The bet governs pass-through-able surfaces only: the DOM subset, canvas, streams/codec-shaped utilities. Policy surfaces are exempt and interposed by design, forever: proxied fetch (#7), data services (#8), groups (#9), and polymorph:webcrypto's deliberately reshaped capability API. Shape those for audit, batching, and consent granularity with no WebIDL-fidelity constraint. Keep the two categories in separate WIT packages so the distinction is legible in the surface itself.
Standing rule independent of upstream posture: app components are never loaded declaratively. The sketch defers ESM-integration (where browser-bound ambient authority could appear); polymorph instantiates explicitly regardless of what lands there.
Conversion checkpoints (provisional → ruling)
- The first specified app-facing UI world (
polymorph:dom or successor) is written under these rules, and the walking-skeleton UI spike measures per-call vs batched overhead across the two-hop path under deltic — confirming the "frame hop dominates" premise.
- Upstream resolution of attribute getters/setters and callbacks either matches the conventions above or forces the (tooling-mechanical) rename migration — either outcome converts rule 5 from bet to fact.
- ESM-integration's ambient-binding posture becomes concrete upstream; the declarative-loading rule gets restated against the real design.
Open sub-questions
- Which surfaces beyond DOM/canvas are worth mirroring (TextEncoder/Decoder, CompressionStream, OffscreenCanvas contexts)?
- How subtree scoping is expressed for a future in-frame placement (clamped root resource vs per-node grants).
- Whether the DOM-mirror subset is versioned as its own package with an upstream-alignment changelog, given the no-
@unstable posture.
Provisional plan — recorded 2026-08-16 from the review of WebAssembly/component-model#686 (the web-embedding design sketch: components bind directly to WebIDL, imports still wired explicitly by the instantiator). Adopted as the working posture for WIT surface design; converted to a ruling only by the checkpoints at the end. Related: NOTES.md — Why components, App-frame sandboxing; issues #5, #6, #13; upstream tracking in lann/deltic#115.
Scope of relevance
686 (and the Firefox prototype behind it) matters to polymorph only as motivation for future-compatible API shapes. The bet, in one line:
Not a dependency, not a timeline bet: deltic remains the mechanism, the linker remains the permission system, and nothing assumes the embedding ships on the target browser matrix. The sketch's own gaps (no attribute getters/setters, no callbacks, no dynamic union passing) mean event-driven DOM is not natively expressible yet regardless — the RPC/mutation UI architecture stands on its merits.
Two properties of the sketch make the bet safe to take:
TypeError), stubs and async prompt-on-first-use imports keep working.[method]imports the component declared; the WIT world remains the install-time consent surface. Audit weight shifts to return types and object scope, not method reachability.Shape rules
For any surface that could ever be satisfied by handing over a platform object ("pass-through-able"):
[method]imports bycamelCase(kebab-name)lookup on the interface object's prototype:resource element { query-selector: ... }can someday be satisfied by literally passingElement; a renamed method is JS-glue forever. Deviate from platform naming only where policy demands it, knowing each deviation pins that method to the wrapper path.undefinedon the real prototype → instantiationTypeError). Extensions live in sibling interfaces. Subsetting in the other direction is free: the embedding drops extra args and defaults missing ones, so mirrors don't chase optional-parameter growth.string/list/record(camelCase-able field names)/enum(labels map verbatim — no case conversion)/optionfor nullables/f64for unrestricted doubles; top-levelresult<T, E>on throwing functions (maps to exceptions; deltic already aligned via lann/deltic#114). Avoid as API currency: dynamic unions,option<option<T>>(degrades to the generic variant case),error-context(unresolved upstream).resources — never integer indices or string IDs. Today they're deltic table entries; under native bindings they're host handles. Same unforgeability, zero migration. (Supersedes the earlier u32 asset-index idea:resource assetgives launder-by-construction in future-compatible form.)stream<record>/future<T>, not callback imports. Callbacks are upstream open question 4; streams map toReadableStream, futures toPromise, and record-shaped events are what the RPC topology wants anyway. Attributes (open question 6) get mechanicalget-x/set-xpairs; accept a tooling-level rename if upstream lands a different convention.The tension, and why it resolves cleanly here
The future-fast shape (DOM-mirror, per-call) is the present-slow shape — the Mozilla Dodrio experiment's 45% was precisely the cost of chatty per-op glue. But in this topology the component boundary is not the bottleneck: every mutation crosses postMessage into the UI frame regardless, and host-side per-frame batching hides per-call overhead at UI rates. A DOM-shaped
polymorph:domworld costs little today.The payoff is placement, not just speed: with native bindings, the same app artifact could instantiate in-frame against real, subtree-scoped elements — collapsing the two-hop RPC path. The existing design already pays for the safety: the sandbox origin's header CSP makes the frame network-dead, so confinement never depended on wrapper-side laundering; under in-frame placement the wrappers' remaining duties degrade to integrity/anti-spoofing, not exfiltration control. Same "one artifact, many placements" argument as headless execution, extended across the trust topology.
Scope split
The bet governs pass-through-able surfaces only: the DOM subset, canvas, streams/codec-shaped utilities. Policy surfaces are exempt and interposed by design, forever: proxied fetch (#7), data services (#8), groups (#9), and polymorph:webcrypto's deliberately reshaped capability API. Shape those for audit, batching, and consent granularity with no WebIDL-fidelity constraint. Keep the two categories in separate WIT packages so the distinction is legible in the surface itself.
Standing rule independent of upstream posture: app components are never loaded declaratively. The sketch defers ESM-integration (where browser-bound ambient authority could appear); polymorph instantiates explicitly regardless of what lands there.
Conversion checkpoints (provisional → ruling)
polymorph:domor successor) is written under these rules, and the walking-skeleton UI spike measures per-call vs batched overhead across the two-hop path under deltic — confirming the "frame hop dominates" premise.Open sub-questions
@unstableposture.