Files
ujsx/docs/architecture
glm-5.1 0d5b9d5ea8 stabilize architecture docs: address review findings and advance to stable
Critical fixes:
- Restructure pointers.md: move setNode prop-key writes section under
  its own heading (was incorrectly nested under selectNode)
- Add Context/Density/Direction/RenderContext documentation section
  to host-config.md (was only a brief constraint bullet)
- Advance all 5 ADRs from Status: Proposed → Accepted and frontmatter
  from status: draft → status: stable (decisions are driving implementation)
- Add error handling philosophy section to README

Warning/suggestion fixes:
- Add isUElement null check (node !== null) to schema.md discriminator table
- Add UjsxEnvelope convenience type documentation to events.md
- Add Direction Unicode arrow naming note to transforms.md
- Standardize all cross-references from absolute docs/research/ paths
  to relative ../research/ paths across all architecture docs
- Fix schema.md ADR references to use relative paths
- Reduce redundancy between transforms.md and host-config.md Direction notes
- Update all architecture doc frontmatter from draft → stable

Deferred:
- Performance model section (reconciler not yet built)
- Concepts/glossary document (low ROI at current scale)
- Line counts in source references (would date quickly)
2026-05-18 16:10:24 +00:00
..

status, last_updated
status last_updated
stable 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) defining UNode, UElement, URoot, UPrimitive, PropValue, UniversalProps; TypeScript types ComponentFn and UComponent; type guards isUElement, isURoot, isUPrimitive
  • Element factoryh(), createRoot(), createComponent(), Fragment, JSX runtime
  • Reactive layerReactiveRoot, reactiveComponent, reactiveElement, signal/computed/effect/batch
  • HostConfig — generic host interface with createRoot().render() for mount-only rendering
  • TransformsTransformRegistry for bi-directional transforms
  • EventsPubSubLike and EventEnvelope for decoupled event emission
  • PointersValuePointer, selectNode, setNode for 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, no prepareUpdate/commitUpdate calls. See reconciler.md.
  • dispose functions are no-ops — signal subscriptions leak. See lifecycle.md.
  • No key field on UElement — 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 ../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.

Error Handling Philosophy

UJSX follows a tiered approach to errors:

  • Programmer errors throw. Invalid arguments (h() called without a type), missing required parameters, or transform() failing to find a matching rule — these are bugs that should surface immediately with a stack trace. Failing silently makes debugging harder.

  • Operational conditions are no-ops with defined behavior. selectNode returning undefined for invalid paths, setNode returning the node unchanged for unresolvable paths, Fragment with no children producing an empty array — these are not errors, they are well-defined boundary conditions. The caller is responsible for checking the result.

  • Host errors propagate. If HostConfig.createInstance throws, the error bubbles up through mountNode with no host involvement. UJSX does not catch or suppress host errors. The current implementation has no handleError hook on HostConfig — whether to add one is an open question (see host-config.md Open Question 4).

  • Recovery is the caller's responsibility. ReactiveRoot.subscribe() returns a dispose function; if the caller discards it, the effect leaks. unmount() is a stub. There is no automatic retry, circuit breaking, or error event emission for operational failures. These concerns belong to the host or consumer, not to the core library.

References

  • UJSX research index: ../research/README.md
  • Reconciler research: ../research/reconciler/
  • SDD process: ../sdd_process.md
  • Preact signals-core: @preact/signals-core
  • TypeBox: @alkdev/typebox
  • Taskgraph_ts architecture pattern: /workspace/@alkdev/taskgraph_ts/docs/architecture/