Phase 2: transitioning reconciler research into architecture documents. New docs: - reconciler.md: fiber tree, reconciliation algorithm (signal-driven props + key-based children), update scheduling, commit order, TypeBox optimization layer, file structure, consumer impact - lifecycle.md: mount/update/dispose phases, fiber tree disposal, partial tree removal, ReactiveRoot.dispose(), finalizeInstance, idempotent disposal, computed vs effect cleanup - ADR-004: key as first-class field on UElement (not a prop) - ADR-005: signal-driven updates for props, reconciliation for structure (hybrid approach, not full tree diffing) Updated docs: - README.md: add reconciler.md, lifecycle.md, ADRs 004/005 to index; update reconciler roadmap with architecture doc links - schema.md: add key?: string to UElement type with TODO comment; update known gaps to reference ADR-004 and reconciler.md; rephrase key constraint as temporary - element-factory.md: update key extraction gap to reference ADR-004 and reconciler.md - host-config.md: reference reconciler.md and lifecycle.md for the reconciler bridge and disposal gaps - reactive-layer.md: reference reconciler.md and lifecycle.md for the signal-host bridge and disposal gaps - events.md: reference lifecycle.md for unmount/dispose gap
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.
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/