diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 614c61c..3c9c760 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,5 +1,5 @@ --- -status: draft +status: stable last_updated: 2026-05-18 --- @@ -76,7 +76,7 @@ Uses UJSX as a direct dependency. Workflow templates are `UNode` trees. Renders - **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. +See `../research/reconciler/05-flowgraph-host-configs.md` for the planned integration. ### Desktop UI (Spoke HUD) @@ -118,11 +118,23 @@ last_updated: YYYY-MM-DD 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](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: `docs/research/README.md` -- Reconciler research: `docs/research/reconciler/` -- SDD process: `docs/sdd_process.md` +- 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/` \ No newline at end of file diff --git a/docs/architecture/build-distribution.md b/docs/architecture/build-distribution.md index 6f35443..b9c0a3f 100644 --- a/docs/architecture/build-distribution.md +++ b/docs/architecture/build-distribution.md @@ -1,5 +1,5 @@ --- -status: draft +status: stable last_updated: 2026-05-18 --- diff --git a/docs/architecture/decisions/001-html-agnostic-core.md b/docs/architecture/decisions/001-html-agnostic-core.md index 2431168..4b021b8 100644 --- a/docs/architecture/decisions/001-html-agnostic-core.md +++ b/docs/architecture/decisions/001-html-agnostic-core.md @@ -1,11 +1,11 @@ --- -status: draft +status: stable last_updated: 2026-05-18 --- # ADR-001: HTML-agnostic core -**Status**: Proposed +**Status**: Accepted ## Context diff --git a/docs/architecture/decisions/002-typebox-module-as-registry.md b/docs/architecture/decisions/002-typebox-module-as-registry.md index bf9feba..df1ab93 100644 --- a/docs/architecture/decisions/002-typebox-module-as-registry.md +++ b/docs/architecture/decisions/002-typebox-module-as-registry.md @@ -1,11 +1,11 @@ --- -status: draft +status: stable last_updated: 2026-05-18 --- # ADR-002: TypeBox Module as type registry -**Status**: Proposed +**Status**: Accepted ## Context diff --git a/docs/architecture/decisions/003-preact-signals-for-reactivity.md b/docs/architecture/decisions/003-preact-signals-for-reactivity.md index 85090bc..abb44be 100644 --- a/docs/architecture/decisions/003-preact-signals-for-reactivity.md +++ b/docs/architecture/decisions/003-preact-signals-for-reactivity.md @@ -1,11 +1,11 @@ --- -status: draft +status: stable last_updated: 2026-05-18 --- # ADR-003: Preact signals-core for reactivity -**Status**: Proposed +**Status**: Accepted ## Context diff --git a/docs/architecture/decisions/004-key-as-first-class-field.md b/docs/architecture/decisions/004-key-as-first-class-field.md index baabcb1..629d07e 100644 --- a/docs/architecture/decisions/004-key-as-first-class-field.md +++ b/docs/architecture/decisions/004-key-as-first-class-field.md @@ -1,11 +1,11 @@ --- -status: draft +status: stable last_updated: 2026-05-18 --- # ADR-004: `key` as a First-Class Field on `UElement` -**Status**: Proposed +**Status**: Accepted ## Context @@ -70,7 +70,7 @@ The TypeBox schema uses `Type.Optional(Type.String())` for `key`, so validation ## References -- Key field design research: `docs/research/reconciler/00-KEY-FIELD-DESIGN.md` +- Key field design research: `../../research/reconciler/00-KEY-FIELD-DESIGN.md` - Schema architecture: [schema.md](../schema.md) - Element factory: [element-factory.md](../element-factory.md) - Reconciler algorithm: [reconciler.md](../reconciler.md) \ No newline at end of file diff --git a/docs/architecture/decisions/005-signal-driven-updates-over-tree-diffing.md b/docs/architecture/decisions/005-signal-driven-updates-over-tree-diffing.md index f31e2b3..07a2857 100644 --- a/docs/architecture/decisions/005-signal-driven-updates-over-tree-diffing.md +++ b/docs/architecture/decisions/005-signal-driven-updates-over-tree-diffing.md @@ -1,11 +1,11 @@ --- -status: draft +status: stable last_updated: 2026-05-18 --- # ADR-005: Signal-Driven Updates Over Tree Diffing -**Status**: Proposed +**Status**: Accepted ## Context @@ -58,8 +58,8 @@ This means 90% of updates (property changes) bypass tree diffing entirely. The r ## References -- Reactive → Host bridge research: `docs/research/reconciler/01-reactive-host-bridge.md` -- Key-based children reconciliation: `docs/research/reconciler/02-key-based-children-reconciliation.md` +- Reactive → Host bridge research: `../../research/reconciler/01-reactive-host-bridge.md` +- Key-based children reconciliation: `../../research/reconciler/02-key-based-children-reconciliation.md` - Reconciler architecture: [reconciler.md](../reconciler.md) - Reactive layer: [reactive-layer.md](../reactive-layer.md) - HostConfig interface: [host-config.md](../host-config.md) \ No newline at end of file diff --git a/docs/architecture/element-factory.md b/docs/architecture/element-factory.md index 5001bca..ef60cbb 100644 --- a/docs/architecture/element-factory.md +++ b/docs/architecture/element-factory.md @@ -1,5 +1,5 @@ --- -status: draft +status: stable last_updated: 2026-05-18 --- @@ -155,4 +155,4 @@ This is documented in the schema architecture ([schema.md](schema.md) — Known - Schema architecture: `docs/architecture/schema.md` - Key field ADR: [decisions/004-key-as-first-class-field.md](decisions/004-key-as-first-class-field.md) - Reconciler architecture: [reconciler.md](reconciler.md) -- Key-based reconciliation research: `docs/research/reconciler/02-key-based-children-reconciliation.md` \ No newline at end of file +- Key-based reconciliation research: `../research/reconciler/02-key-based-children-reconciliation.md` \ No newline at end of file diff --git a/docs/architecture/events.md b/docs/architecture/events.md index f15c41d..d4a006b 100644 --- a/docs/architecture/events.md +++ b/docs/architecture/events.md @@ -1,5 +1,5 @@ --- -status: draft +status: stable last_updated: 2026-05-18 --- @@ -73,6 +73,19 @@ type UjsxEventMap = { The event map defines the known event types and their payload shapes. Each key is an event type string; each value is the expected payload structure. +## UjsxEnvelope + +```typescript +type UjsxEnvelope = EventEnvelope< + TType, + UjsxEventMap[TType] +>; +``` + +A convenience type alias that parameterizes `EventEnvelope` with `UjsxEventMap`. Instead of writing `EventEnvelope<"root.render", { childCount: number }>`, consumers write `UjsxEnvelope<"root.render">`. The type parameter defaults to the full key union, so `UjsxEnvelope` alone means "an envelope for any UJSX event." + +This type is exported from `src/core/events.ts` and re-exported from the barrel. + | Event | When emitted | Payload | |-------|-------------|---------| | `root.render` | HostConfig calls `render()` on a root | `childCount` of the rendered tree | @@ -136,7 +149,7 @@ The event map does not include error events (e.g., `instance.error`, `render.err ### No cleanup on unmount -The current `unmount()` implementation in `HostConfig` does not tear down event subscriptions. If a pubsub emitter was wired to the host's `emit()` during render, unmount does not unsubscribe. This gap is shared with the broader reconciler disposal gap — see [lifecycle.md](lifecycle.md) and the unmount & dispose research (`docs/research/reconciler/03-unmount-dispose-support.md`). +The current `unmount()` implementation in `HostConfig` does not tear down event subscriptions. If a pubsub emitter was wired to the host's `emit()` during render, unmount does not unsubscribe. This gap is shared with the broader reconciler disposal gap — see [lifecycle.md](lifecycle.md) and the unmount & dispose research (`../research/reconciler/03-unmount-dispose-support.md`). ## Constraints diff --git a/docs/architecture/host-config.md b/docs/architecture/host-config.md index c0b3d81..5d51f06 100644 --- a/docs/architecture/host-config.md +++ b/docs/architecture/host-config.md @@ -1,5 +1,5 @@ --- -status: draft +status: stable last_updated: 2026-05-18 --- @@ -82,6 +82,85 @@ interface Root { The `Context` connection is intentional: UJSX wants hosts to make density-aware and target-aware decisions without the host needing to know about signal internals. `Context` provides a reactive `ContextValue` that hosts can read during `createInstance` or `commitUpdate`. +## Context, Density, Direction & RenderContext + +The `context.ts` module exports a set of related types and a class that support adaptive rendering and directional transforms: + +### Context (class) + +```typescript +class Context { + constructor(initial?: Partial) + get(): ContextValue + get signal(): ReadonlySignal + set(partial: Partial): void + subscribe(fn: (value: ContextValue) => void): () => void + fork(overrides: Partial): Context +} +``` + +`Context` wraps a Preact `signal` and provides reactive access to context data. Hosts read context during `createInstance` or `commitUpdate` to adapt rendering (e.g., switching layouts based on `density`). The `signal` getter exposes the underlying `ReadonlySignal` for composition with other reactive primitives. + +- **`get()`** — returns the current `ContextValue` (non-reactive read). +- **`set(partial)`** — shallow-merges `partial` into the current value inside a `batch()`, triggering any subscriptions. +- **`subscribe(fn)`** — calls `fn` on every change via `effect()`. Returns a dispose function. +- **`fork(overrides)`** — creates a new `Context` with the current values shallow-merged with `overrides`. Forked contexts are independent — changes to the fork do not propagate to the parent. + +### ContextValue + +```typescript +interface ContextValue { + density: Density; + target: string; + metadata: Record; +} +``` + +The shape of a context's value. Defaults: + +- `density`: `"full"` +- `target`: `"markdown"` +- `metadata`: `{}` + +### Density + +```typescript +type Density = "full" | "compact" | "minimal" +``` + +Controls rendering granularity for hosts that support adaptive output. `full` means render everything; `compact` and `minimal` are host-defined — UJSX passes the value through, it does not interpret it. A desktop UI host might use `compact` to hide labels and `minimal` to render only essential controls. + +### Direction + +```typescript +type Direction = "ujsx→mdast" | "mdast→ujsx" | "ujsx→jpath" | "jpath→ujsx" | "ujsx→hast" | "hast→ujsx" +``` + +Six directional strings pairing into three bi-directional channels (markdown, JSON path, HTML). `Direction` is defined in `context.ts` because it governs both transform rules and render context — it's not transform-specific. + +The `→` character in direction strings is a Unicode right arrow (U+2192). This was chosen for readability over alternatives like `ujsx-to-mdast` or `ujsx2mdast`. Consumers should be aware of the non-ASCII characters in IDE autocompletion and linting contexts. + +### RenderContext + +```typescript +interface RenderContext extends ContextValue { + direction: Direction; +} +``` + +A convenience type that adds `direction` to `ContextValue`. Used primarily by the transform system to carry conversion direction alongside context data. The transform `ctx` factory function accepts a `Direction` and returns a `TransformContext` (which includes `direction`), not a `RenderContext` — `RenderContext` exists for consumers that want to type-narrow the full context shape. + +### Exports + +All context types are available from the barrel export (`@alkdev/ujsx`) and the `context` sub-path: + +```typescript +import { Context } from "@alkdev/ujsx/context"; // class +import type { Density, Direction, RenderContext } from "@alkdev/ujsx/context"; // types +``` + +`Context` is a runtime export; `Density`, `Direction`, and `RenderContext` are type-only exports. + ## createRoot() ```typescript @@ -170,7 +249,7 @@ unmount() { This means calling `unmount()` followed by creating a new root on the same container will likely result in leaked instances and stale signal effects. -The reconciler architecture ([reconciler.md](reconciler.md)) and lifecycle management ([lifecycle.md](lifecycle.md)) address both gaps. The research documents (`docs/research/reconciler/01-reactive-host-bridge.md` and `03-unmount-dispose-support.md`) provide the detailed implementation plans. +The reconciler architecture ([reconciler.md](reconciler.md)) and lifecycle management ([lifecycle.md](lifecycle.md)) address both gaps. The research documents (`../research/reconciler/01-reactive-host-bridge.md` and `../research/reconciler/03-unmount-dispose-support.md`) provide the detailed implementation plans. ### Event IDs Use `Date.now()` @@ -193,7 +272,7 @@ The reconciler solves this by maintaining a fiber tree alongside the instance tr - **`render()` accepts any `UNode`** — if the node is a `URoot`, its children are mounted directly. If the node is any other `UNode` (element or primitive), it is wrapped in an array and mounted as a single top-level element without a root container. - **Function components are synchronous and transparent** — they receive props and children, return a `UNode`, and produce no host instance. The reconciler research discusses how to handle components that return different tree shapes across renders. - **`Context` is always present** — `createRoot()` guarantees a `Context` exists on the `Root`. If none is provided, a default is created with `density: "full"`, `target: "markdown"`, and empty `metadata`. -- **`RenderContext` extends `ContextValue`** — the `Direction` type (`"ujsx→mdast" | "mdast→ujsx" | "ujsx→jpath" | "jpath→ujsx" | "ujsx→hast" | "hast→ujsx"`) and `RenderContext` interface (which adds `direction` to `ContextValue`) are exported from the `context` sub-path. `RenderContext` is primarily used by the transform system to specify conversion direction. `Density` (`"full" | "compact" | "minimal"`) controls rendering granularity for hosts that support adaptive output. +- **`RenderContext` extends `ContextValue`** — adds `direction` to the context value. See [Context, Density, Direction & RenderContext](#context-density-direction--rendercontext) for full documentation of these types. - **`container` is opaque** — UJSX passes it to `createRootContext` and stores it on `Root`, but never inspects it. The host defines what it means. - **Mount is depth-first, post-order** — children are fully constructed before being appended to their parent. Hosts can rely on this ordering invariant. @@ -216,4 +295,4 @@ The reconciler solves this by maintaining a fiber tree alongside the instance tr - Context: `src/core/context.ts` — `Context` class with signal-based values - Reconciler architecture: [reconciler.md](reconciler.md) - Lifecycle management: [lifecycle.md](lifecycle.md) -- Reconciler research: `docs/research/reconciler/01-reactive-host-bridge.md` and `03-unmount-dispose-support.md` \ No newline at end of file +- Reconciler research: `../research/reconciler/01-reactive-host-bridge.md` and `../research/reconciler/03-unmount-dispose-support.md` \ No newline at end of file diff --git a/docs/architecture/lifecycle.md b/docs/architecture/lifecycle.md index 7020d27..cf69808 100644 --- a/docs/architecture/lifecycle.md +++ b/docs/architecture/lifecycle.md @@ -1,5 +1,5 @@ --- -status: draft +status: stable last_updated: 2026-05-18 --- @@ -176,7 +176,7 @@ All disposal operations must be idempotent. Calling `dispose()` twice must not e ## References -- Unmount & dispose research: `docs/research/reconciler/03-unmount-dispose-support.md` +- Unmount & dispose research: `../research/reconciler/03-unmount-dispose-support.md` - Reconciler architecture: [reconciler.md](reconciler.md) - HostConfig interface: [host-config.md](host-config.md) - Reactive layer: [reactive-layer.md](reactive-layer.md) diff --git a/docs/architecture/pointers.md b/docs/architecture/pointers.md index 2a5f565..850bb73 100644 --- a/docs/architecture/pointers.md +++ b/docs/architecture/pointers.md @@ -1,5 +1,5 @@ --- -status: draft +status: stable last_updated: 2026-05-18 --- @@ -78,19 +78,6 @@ If the current node is not a `UElement` (i.e., it's a `UPrimitive` — a string, When a string segment resolves to a prop value that is a primitive (string, number, boolean, null), `selectNode` returns `undefined`. Only non-null object prop values — including arrays, since `typeof [] === "object"` — can be navigation targets. This means `props.items` where `items` is an array can be navigated into, but `props.title` where `title` is a string cannot. -`setNode` mirrors this behavior for writes: a non-numeric string segment sets `props[segment] = value`, performing a shallow merge of that key into the element's props. This allows targeted prop updates via path-based navigation. - -### setNode prop-key writes - -Non-numeric path segments in `setNode` set values into the `props` object: - -```typescript -setNode(root, ["title"], someNode) -// Produces: { ...rootEl, props: { ...rootEl.props, title: someNode } } -``` - -This shallow-merges a key into `props`. The `value` must be a valid `PropValue` (or `UNode`) for the result to remain type-safe. - ## setNode ```typescript @@ -123,6 +110,19 @@ This structural sharing means `setNode` is O(depth) in allocations, not O(size o When the head segment is a valid non-negative integer, `setNode` shallow-copies the `children` array and replaces the element at that index. Out-of-range indices are ignored — the copy is made but no element is replaced. This is a defensive choice: silent no-op is preferable to throwing or growing the array. +### Prop-key path writes + +Non-numeric path segments set values into the `props` object: + +```typescript +setNode(root, ["title"], someValue) +// Produces: { ...rootEl, props: { ...rootEl.props, title: someValue } } +``` + +This shallow-merges a key into `props`. The `value` argument must be a valid `PropValue` (or `UNode`) for the result to remain type-safe. Unlike numeric segments (which navigate into `children`), non-numeric segments write to `props` at the current level without deeper navigation — the remaining path tail determines what happens next. + +This mirrors `selectNode`'s resolution: where `selectNode` reads `props[segment]` and navigates into non-null object values, `setNode` writes `props[segment] = value` as a shallow merge. + ## Relationship to the Reconciler `ValuePointer`, `selectNode`, and `setNode` are **not** the reconciler's update mechanism. They are lower-level utilities that the reconciler can use internally. The reconciler's fiber tree manages structural changes (diffing, adding, removing children). Pointers handle targeted reads and writes at known positions — a finer granularity than the reconciler's subtree updates. @@ -159,4 +159,4 @@ Paths are exact sequences of segments. There is no support for `*` (any child), - Source: `src/core/pointer.ts` - UNode schema: `src/core/schema.ts` - Preact signals: `@preact/signals-core` -- Reconciler research: `docs/research/reconciler/` (see 01-reactive-host-bridge.md and 02-key-based-children-reconciliation.md for how selectors and path-based targeting integrate with the fiber tree) \ No newline at end of file +- Reconciler research: `../research/reconciler/` (see 01-reactive-host-bridge.md and 02-key-based-children-reconciliation.md for how selectors and path-based targeting integrate with the fiber tree) \ No newline at end of file diff --git a/docs/architecture/reactive-layer.md b/docs/architecture/reactive-layer.md index 3a31ead..5353624 100644 --- a/docs/architecture/reactive-layer.md +++ b/docs/architecture/reactive-layer.md @@ -1,5 +1,5 @@ --- -status: draft +status: stable last_updated: 2026-05-18 --- @@ -152,6 +152,6 @@ Calling `render()` a second time replaces `this.renderDisposer` without disposin - ADR-003: `docs/architecture/decisions/003-preact-signals-for-reactivity.md` - Reconciler architecture: [reconciler.md](reconciler.md) - Lifecycle management: [lifecycle.md](lifecycle.md) -- Reactive → Host bridge research: `docs/research/reconciler/01-reactive-host-bridge.md` -- Unmount & dispose research: `docs/research/reconciler/03-unmount-dispose-support.md` +- Reactive → Host bridge research: `../research/reconciler/01-reactive-host-bridge.md` +- Unmount & dispose research: `../research/reconciler/03-unmount-dispose-support.md` - Preact signals-core: `@preact/signals-core` \ No newline at end of file diff --git a/docs/architecture/reconciler.md b/docs/architecture/reconciler.md index dfa0af2..a57afd4 100644 --- a/docs/architecture/reconciler.md +++ b/docs/architecture/reconciler.md @@ -1,5 +1,5 @@ --- -status: draft +status: stable last_updated: 2026-05-18 --- @@ -209,7 +209,7 @@ Optimizations are applied in order: 4. **Add `Value.Mutate`** (if function values are handled correctly) — preserve reference identity 5. **Add `Value.Diff` for prop payloads** (optional, catch errors) — granular diff payloads for hosts that want them -> See the research in `docs/research/reconciler/04-typebox-optimization-layer.md` for detailed analysis. +> See the research in `../research/reconciler/04-typebox-optimization-layer.md` for detailed analysis. ## File Structure @@ -256,11 +256,11 @@ Host implementations that currently only support mount-only rendering will start ## References -- Reconciler research: `docs/research/reconciler/README.md` -- Key field design: `docs/research/reconciler/00-KEY-FIELD-DESIGN.md` -- Reactive → Host bridge: `docs/research/reconciler/01-reactive-host-bridge.md` -- Children reconciliation: `docs/research/reconciler/02-key-based-children-reconciliation.md` -- TypeBox optimization layer: `docs/research/reconciler/04-typebox-optimization-layer.md` +- Reconciler research: `../research/reconciler/README.md` +- Key field design: `../research/reconciler/00-KEY-FIELD-DESIGN.md` +- Reactive → Host bridge: `../research/reconciler/01-reactive-host-bridge.md` +- Children reconciliation: `../research/reconciler/02-key-based-children-reconciliation.md` +- TypeBox optimization layer: `../research/reconciler/04-typebox-optimization-layer.md` - Lifecycle management: [lifecycle.md](lifecycle.md) - HostConfig interface: [host-config.md](host-config.md) - Reactive layer: [reactive-layer.md](reactive-layer.md) \ No newline at end of file diff --git a/docs/architecture/schema.md b/docs/architecture/schema.md index 10d7275..c22c5e8 100644 --- a/docs/architecture/schema.md +++ b/docs/architecture/schema.md @@ -1,5 +1,5 @@ --- -status: draft +status: stable last_updated: 2026-05-18 --- @@ -124,7 +124,7 @@ function isUPrimitive(node: UNode): node is UPrimitive | Guard | Logic | Discriminator | |-------|-------|---------------| -| `isUElement` | `typeof === "object" && "type" in node && "props" in node && "children" in node && node.type !== "root"` | Has `type`/`props`/`children` keys AND `type` is not `"root"` | +| `isUElement` | `typeof node === "object" && node !== null && "type" in node && "props" in node && "children" in node && node.type !== "root"` | Has `type`/`props`/`children` keys, is not null, and `type` is not `"root"` | | `isURoot` | `typeof === "object" && "type" in node && node.type === "root"` | `type === "root"` | | `isUPrimitive` | `typeof === "string" \|\| typeof === "number" \|\| typeof === "boolean" \|\| node === null` | Not an object | @@ -151,7 +151,7 @@ The reconciler architecture (see [reconciler.md](reconciler.md) and [ADR-004](de ## Open Questions -1. **Should `key` accept numbers?** React coerces number keys to strings. The current proposal enforces `string` only — simpler, no implicit coercion. Users can wrap in `String()`. See [ADR-004](decisions/004-key-as-first-class-field.md) and research: [00-KEY-FIELD-DESIGN.md](../../research/reconciler/00-KEY-FIELD-DESIGN.md). +1. **Should `key` accept numbers?** React coerces number keys to strings. The current proposal enforces `string` only — simpler, no implicit coercion. Users can wrap in `String()`. See [ADR-004](decisions/004-key-as-first-class-field.md) and research: [00-KEY-FIELD-DESIGN.md](../research/reconciler/00-KEY-FIELD-DESIGN.md). 2. **Should `UPrimitive` include `undefined`?** Currently `null` represents an explicitly empty value. `undefined` means "absent" and should not appear as a tree node. This is consistent with how JSX treats `undefined` children (rendered to nothing), but some hosts might benefit from an explicit "missing" sentinel. No current use case justifies this. 3. **Should `UniversalProps` constrain value types per-host?** The open schema allows any `PropValue` for any key. A host that wants stricter prop contracts (e.g., `onClick` must be a function, `className` must be a string) must validate at its own boundary. A future host-typed props system could be layered on top without changing the base schema. @@ -160,6 +160,6 @@ The reconciler architecture (see [reconciler.md](reconciler.md) and [ADR-004](de - Source: `src/core/schema.ts` - Key field ADR: [decisions/004-key-as-first-class-field.md](decisions/004-key-as-first-class-field.md) - Reconciler architecture: [reconciler.md](reconciler.md) -- Key field design research: `docs/research/reconciler/00-KEY-FIELD-DESIGN.md` -- TypeBox Module as type registry: `docs/architecture/decisions/002-typebox-module-as-registry.md` -- HTML-agnostic core: `docs/architecture/decisions/001-html-agnostic-core.md` \ No newline at end of file +- Key field design research: `../research/reconciler/00-KEY-FIELD-DESIGN.md` +- TypeBox Module as type registry: [decisions/002-typebox-module-as-registry.md](decisions/002-typebox-module-as-registry.md) +- HTML-agnostic core: [decisions/001-html-agnostic-core.md](decisions/001-html-agnostic-core.md) \ No newline at end of file diff --git a/docs/architecture/transforms.md b/docs/architecture/transforms.md index 608f34d..7c909c4 100644 --- a/docs/architecture/transforms.md +++ b/docs/architecture/transforms.md @@ -1,5 +1,5 @@ --- -status: draft +status: stable last_updated: 2026-05-18 --- @@ -117,6 +117,8 @@ The six directions pair into three bi-directional channels: These are the directions currently defined. Additional directions (e.g., `ujsx→dom`, `dom→ujsx`) can be added by extending the `Direction` union in `context.ts`. The registry itself is generic and does not enumerate directions. +The `→` character in direction strings is Unicode U+2192 (RIGHTWARDS ARROW), chosen for readability over alternatives like `ujsx-to-mdast`. See [host-config.md](host-config.md) for full documentation of `Direction`, `Density`, and `RenderContext`. + ## Known Gaps ### No transform composition beyond next