NodeSpec-Only Specification
1. Scope
This document defines the ViewCompose node-semantics boundary: only NodeSpec is allowed, and the
former parallel Props path no longer exists.
Goals:
- Keep render-pipeline semantics stable, derivable, and testable.
- Prevent dynamic fields from returning and degrading patch/skip semantics.
- Provide one integration template for every new node.
For historical context, see NODE_PROPS_FULL_2026-03-06.md.
2. Current hard boundary
VNodecontains only a non-nullspec: NodeSpec; it has nopropsfield.UiTreeBuilder.emit/emitResolvedrequiresspecand no longer accepts apropsparameter.- The renderer pipeline may read only
NodeSpec + ResolvedModifiers. - Additional metadata such as anchors must travel through modifier elements, for example
Modifier.overlayAnchor(...). - Do not add any
Props/TypedPropKeys/PropKeys/node.propspath.
3. Responsibility split
- Component semantic fields belong in
NodeSpec. - General visual and interaction decoration belongs in
Modifier. - Theme defaults are resolved through
Theme -> Defaultsand injected intoNodeSpec/Modifier.
4. Value admissibility boundary
NodeSpec values participate in VNode equality, patch planning, subtree skipping, diagnostics, and
failed-render rollback. Semantic payloads must therefore be immutable, structurally comparable,
and platform-neutral. Do not retain Android framework objects or mutable interface types merely
because a native View setter accepts them.
Text follows this rule explicitly:
TextNodePropshas one authoritativeTextDocumentfor both plain and rich text.ButtonNodePropsandToggleNodePropsuse nullableStringlabels.- Android
CharSequence,Spanned,Spannable, andEditablevalues exist only in renderer interop code and are converted at the final native binding or input boundary.
This split prevents mutable spans and identity-based platform values from making an unchanged VNode compare differently or a changed value compare equal for the wrong reason.
5. Resolved surface boundary
NodeType.Surface pairs with SurfaceNodeProps, not the general BoxNodeProps. A design-system
component resolves its brush, shape, border, effective dimensions, optional visual height, and
clipping policy before emission. General interaction feedback travels through the ordered
UiInteractionIndication modifier contract rather than Surface, Box, or Row NodeSpec fields. The
Android Renderer executes both snapshots without receiving design-system identity or semantic
token roles.
General caller modifiers remain ordered after the resolved surface. A caller background, border, corner, or shape replaces the component-provided visual surface and uses the complete effective bounds. Exact shadows and elevation may be supplied by the Basic component as ordinary ordered modifier contracts because the renderer already executes them generically.
Component NodeSpecs retain interaction values only when the native backend owns multiple internal
targets that one outer modifier cannot address. Segmented controls and navigation bars therefore
carry complete selected and unselected UiStateLayerColors; a TabRow instead emits eager keyed
child boxes and gives each child its own indication modifier.
6. New-node checklist
Every new first-party node must include:
- a node-specific
NodeSpec; - immutable, structurally comparable, platform-neutral semantic fields;
- DSL parameters mapped to that
NodeSpec, with modifier metadata where necessary; - corresponding renderer binder and patch behavior;
- unit coverage for stable structure, field changes, and interaction changes;
- a Demo verification path and instrumentation where required.
7. Application and third-party extension path
Extensions must also remain spec-only:
- define a custom
NodeSpec; - define custom binder/patch behavior;
- never pass semantics through a dynamic map.
8. Regression prevention
- Unit tests cover strict
requireSpec<T>()reads and failure diagnostics. - Static guard tests scan framework production source and reject a returning
Propssystem. - Architecture and workflow reviews treat NodeSpec-only as a required checkpoint.