Fixes from architecture review (4 critical, 10 warnings):
Critical:
- Fix selectNode/setNode docs to accurately describe prop-key
navigation behavior including array support and prop-key writes
- Document RenderContext/Density exported types in host-config
- Resolve ADR dual status ambiguity with clarifying note in README
(frontmatter status = editorial, body Status = decision)
- Effect types already addressed in prior commit
Warnings addressed:
- Add Fragment re-export note to jsx-runtime section in
build-distribution
- Document childCtx/transformCtx helper functions in transforms.md
- Document render() accepting non-root UNode in host-config
- Add Value.Hash re-entrancy constraint to reconciler.md
- Add true-passthrough constraint and h('root') special case
to element-factory constraints
- Add _idCounter bundling caveat note
Review document added at docs/reviews/architecture-review-2026-05-18.md
with full findings, source verification table, and recommendations.
8.7 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-05-18 |
@alkdev/ujsx Architecture
Universal JSX — runtime-agnostic reactive tree primitives with TypeBox schemas. UJSX treats JSX as an intermediate representation for multi-target rendering.
Why This Exists
UJSX fills a specific niche: generic tree construction and rendering where the same declarative template can target different hosts (markdown, graph structures, UI frameworks, workflow engines). It borrows the JSX mental model — nested elements with props and children — but strips away all platform-specific assumptions.
No onClick, no className, no style. The tree is a pure data structure (UNode) validated by TypeBox schemas, and the rendering contract is a HostConfig that decides what "create", "update", and "remove" mean for its target.
This makes UJSX useful for:
- Workflow definitions — operations as elements, dependencies as parent-child structure, rendered to graphology DAGs or reactive execution engines
- Structured document generation — markdown, HTML, or any text format via transform rules
- Desktop UI — instanced glyphs, panels, layouts rendered to Three.js or similar (the spoke UI use case)
Core Principle
The tree is the truth. Hosts are interpreters. UJSX defines what a tree looks like (UNode, UElement, URoot), how it's constructed (h(), createComponent()), and how it can react to changes (signals). It does not dictate what the tree means — that's the host's job.
Current State
UJSX is functional but incomplete. The core primitives exist and are tested:
- Schema — TypeBox Module (
UJSX) definingUNode,UElement,URoot,UPrimitive,PropValue,UniversalProps; TypeScript typesComponentFnandUComponent; type guardsisUElement,isURoot,isUPrimitive - Element factory —
h(),createRoot(),createComponent(),Fragment, JSX runtime - Reactive layer —
ReactiveRoot,reactiveComponent,reactiveElement, signal/computed/effect/batch - HostConfig — generic host interface with
createRoot().render()for mount-only rendering - Transforms —
TransformRegistryfor bi-directional transforms - Events —
PubSubLikeandEventEnvelopefor decoupled event emission - Pointers —
ValuePointer,selectNode,setNodefor tree navigation and targeted mutation
Known gaps (documented in architecture docs, planned for reconciler implementation):
unmount()is a stub — no fiber tree teardown, no instance removal, no signal disposal. See host-config.md and lifecycle.md.render()is mount-only — no re-render, no diffing, noprepareUpdate/commitUpdatecalls. See reconciler.md.disposefunctions are no-ops — signal subscriptions leak. See lifecycle.md.- No
keyfield onUElement— positional matching only. See ADR-004.
Architecture Documents
| Document | Content |
|---|---|
| schema.md | TypeBox Module, UNode/UElement/URoot/UPrimitive types, type guards |
| element-factory.md | h(), createRoot(), createComponent(), Fragment, JSX runtime |
| reactive-layer.md | ReactiveRoot, reactiveComponent, reactiveElement, signals, disposal gaps |
| host-config.md | HostConfig interface, createRoot(), mount-only rendering, reconciler gap |
| reconciler.md | Fiber tree, reconciliation algorithm, update scheduling, TypeBox optimizations |
| lifecycle.md | Mount, update, unmount/dispose lifecycle, signal cleanup, partial tree teardown |
| transforms.md | TransformRegistry, TransformRule, TransformContext, bi-directional transforms |
| events.md | EventEnvelope, PubSubLike, UjsxEventMap |
| pointers.md | ValuePointer, selectNode, setNode, tree navigation |
| build-distribution.md | Package structure, exports map, dependencies, runtime targets |
Design Decisions
| ADR | Decision |
|---|---|
| 001 | HTML-agnostic core — no DOM-specific props |
| 002 | TypeBox Module IS the type registry |
| 003 | Preact signals-core for reactivity |
| 004 | key as a first-class field on UElement — not a prop |
| 005 | Signal-driven updates for props, reconciliation for structure |
Consumer Context
UJSX is designed as a library consumed by other projects, not an end-user application. Understanding these consumers shapes the API design:
Flowgraph (@alkdev/flowgraph)
Uses UJSX as a direct dependency. Workflow templates are UNode trees. Renders them to:
- graphology DAG — structural analysis, cycle detection, topological sort via a
HostConfig - Reactive execution engine — runtime workflow execution with signal-based status propagation
See docs/research/reconciler/05-flowgraph-host-configs.md for the planned integration.
Desktop UI (Spoke HUD)
Uses UJSX to define instanced glyph layouts, panels, and adaptive-density content. Renders via a Three.js HostConfig. Signal-driven property updates flow through prepareUpdate/commitUpdate to GPU buffers.
OpenCode Plugin (future)
An OpenCode plugin that provides UJSX-based template operations. Would use TransformRegistry for bi-directional markdown↔UJSX conversion.
Reconciler Roadmap
The reconciler bridges the reactive layer to the host layer, enabling signal-driven updates and key-based children reconciliation. Architecture docs define the WHAT and WHY; research docs contain the detailed implementation plans.
| Phase | Description | Architecture | Research |
|---|---|---|---|
| 0 | key field on UElement |
ADR-004, schema.md | 00-KEY-FIELD-DESIGN.md |
| 1 | Reactive → Host bridge (fiber tree, signal-driven updates) | reconciler.md, ADR-005 | 01-reactive-host-bridge.md |
| 2 | Key-based children reconciliation (LIS algorithm) | reconciler.md | 02-key-based-children-reconciliation.md |
| 3 | Unmount & dispose support | lifecycle.md | 03-unmount-dispose-support.md |
| 4 | TypeBox value optimization layer | reconciler.md (TypeBox Optimization Layer section) | 04-typebox-optimization-layer.md |
| 5 | Flowgraph HostConfig implementations | Downstream consumer (@alkdev/flowgraph), not ujsx |
05-flowgraph-host-configs.md |
Document Lifecycle
Architecture documents use YAML frontmatter with status and last_updated fields:
---
status: draft | stable | deprecated
last_updated: YYYY-MM-DD
---
| Status | Meaning | Transitions |
|---|---|---|
draft |
Under active development. Content may change significantly. Implementation should not start until the document reaches stable. |
→ stable when implementation is complete and API contract is verified by tests. |
stable |
API contracts are locked. Changes require a review cycle and may warrant an ADR if they affect documented decisions. | → deprecated when superseded. → draft if a fundamental redesign is needed (rare). |
deprecated |
Superseded by another document. Kept for reference. Links should point to the replacement. | Removed when no longer referenced. |
ADR documents use a separate Status field in their body: Proposed, Accepted, Deprecated, or Superseded. ADRs never revert from Accepted. Note that ADR frontmatter (status: draft) refers to the document's editorial status (is the writing complete?), while the body Status refers to the decision's status (is this decision finalized?). A Proposed decision can have a draft document; an Accepted decision has a stable document.
References
- UJSX research index:
docs/research/README.md - Reconciler research:
docs/research/reconciler/ - SDD process:
docs/sdd_process.md - Preact signals-core:
@preact/signals-core - TypeBox:
@alkdev/typebox - Taskgraph_ts architecture pattern:
/workspace/@alkdev/taskgraph_ts/docs/architecture/