Surface / Surface and Bundle
How they compose
They are designed to overlap without merging:
- A Surface may read across placements. A screen showing two placed children declares one Surface over both slices; the placements still own their transitions. Observation is not ownership.
- An agent's context may be a Surface over bundle state. The agent sees the projection and sends the root Messages that route to the children. No second transition path appears.
- Sync replicates a writable projection, not a placement. The projection
may select a placed slice's fields; replay still runs through the parent's
update, which routes to the child. - The Module collects both. A placement contributes a contract owning its
Model path; a Surface (and each integration's wiring contract) sits beside
it so
validatesees observers and owners in one list. - An assembly holds both.
Page.assembletakes placements and integration wirings in one list with shared key/tag/resource checks; see Wiring. Placements route by wrapper, wirings route by tag.
Taking the MediaQuery bundle from foldkit-bundle's README and reading its
field through a Surface looks like this — the first half places the owner,
the second half declares an observer over the same Model and Message:
const Dark = Bundle.declare(MediaQuery, 'dark')
const Model = Schema.Struct({ ...Dark.fields, helpOpen: Schema.Boolean })
const Message = defineMessageUnion({ ...Dark.cases, ClickedHelp: {} })
const App = Surface.application({ Model, Message })
const Page = Bundle.parent({ Model, Message })
const placements = Page.assemble(Page.at(Dark, { args: { query: '(prefers-color-scheme: dark)' } }))
// Observer, not a second owner: renders and inspects the placed slice.
const DarkScreen = App.surface('DarkScreen', {
model: ({ model }) => ({ matches: model.dark.matches }),
messages: [Message.ClickedHelp],
})DarkScreen may read model.dark and may cause ClickedHelp. It may not
cause GotDarkMessage, and it owns no transitions. The placement owns the
dark slice's transitions and nothing else.
Wrapped versus flat Messages
The rule both packages share, stated once: a child machine's Messages are wrapped because the parent owns routing to a slice; an integration's Messages are flat cases of the application union because they are facts about the whole application that agents and journals name directly.
- Wrapping an integration's Messages adds nothing: the router would unwrap them before folding.
- Flattening a child's Messages breaks routing: two placements would claim
the same tags, and
assemblerefuses that. - A Surface's
messageslist is neither: it is an allowlist of what a feature may construct, enforced by the builder's type, not a routing table.