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)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: stable
|
||||||
last_updated: 2026-05-18
|
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`
|
- **graphology DAG** — structural analysis, cycle detection, topological sort via a `HostConfig`
|
||||||
- **Reactive execution engine** — runtime workflow execution with signal-based status propagation
|
- **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)
|
### 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.
|
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
|
## References
|
||||||
|
|
||||||
- UJSX research index: `docs/research/README.md`
|
- UJSX research index: `../research/README.md`
|
||||||
- Reconciler research: `docs/research/reconciler/`
|
- Reconciler research: `../research/reconciler/`
|
||||||
- SDD process: `docs/sdd_process.md`
|
- SDD process: `../sdd_process.md`
|
||||||
- Preact signals-core: `@preact/signals-core`
|
- Preact signals-core: `@preact/signals-core`
|
||||||
- TypeBox: `@alkdev/typebox`
|
- TypeBox: `@alkdev/typebox`
|
||||||
- Taskgraph_ts architecture pattern: `/workspace/@alkdev/taskgraph_ts/docs/architecture/`
|
- Taskgraph_ts architecture pattern: `/workspace/@alkdev/taskgraph_ts/docs/architecture/`
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: stable
|
||||||
last_updated: 2026-05-18
|
last_updated: 2026-05-18
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: stable
|
||||||
last_updated: 2026-05-18
|
last_updated: 2026-05-18
|
||||||
---
|
---
|
||||||
|
|
||||||
# ADR-001: HTML-agnostic core
|
# ADR-001: HTML-agnostic core
|
||||||
|
|
||||||
**Status**: Proposed
|
**Status**: Accepted
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: stable
|
||||||
last_updated: 2026-05-18
|
last_updated: 2026-05-18
|
||||||
---
|
---
|
||||||
|
|
||||||
# ADR-002: TypeBox Module as type registry
|
# ADR-002: TypeBox Module as type registry
|
||||||
|
|
||||||
**Status**: Proposed
|
**Status**: Accepted
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: stable
|
||||||
last_updated: 2026-05-18
|
last_updated: 2026-05-18
|
||||||
---
|
---
|
||||||
|
|
||||||
# ADR-003: Preact signals-core for reactivity
|
# ADR-003: Preact signals-core for reactivity
|
||||||
|
|
||||||
**Status**: Proposed
|
**Status**: Accepted
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: stable
|
||||||
last_updated: 2026-05-18
|
last_updated: 2026-05-18
|
||||||
---
|
---
|
||||||
|
|
||||||
# ADR-004: `key` as a First-Class Field on `UElement`
|
# ADR-004: `key` as a First-Class Field on `UElement`
|
||||||
|
|
||||||
**Status**: Proposed
|
**Status**: Accepted
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ The TypeBox schema uses `Type.Optional(Type.String())` for `key`, so validation
|
|||||||
|
|
||||||
## References
|
## 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)
|
- Schema architecture: [schema.md](../schema.md)
|
||||||
- Element factory: [element-factory.md](../element-factory.md)
|
- Element factory: [element-factory.md](../element-factory.md)
|
||||||
- Reconciler algorithm: [reconciler.md](../reconciler.md)
|
- Reconciler algorithm: [reconciler.md](../reconciler.md)
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: stable
|
||||||
last_updated: 2026-05-18
|
last_updated: 2026-05-18
|
||||||
---
|
---
|
||||||
|
|
||||||
# ADR-005: Signal-Driven Updates Over Tree Diffing
|
# ADR-005: Signal-Driven Updates Over Tree Diffing
|
||||||
|
|
||||||
**Status**: Proposed
|
**Status**: Accepted
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
@@ -58,8 +58,8 @@ This means 90% of updates (property changes) bypass tree diffing entirely. The r
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- Reactive → Host bridge research: `docs/research/reconciler/01-reactive-host-bridge.md`
|
- Reactive → Host bridge research: `../../research/reconciler/01-reactive-host-bridge.md`
|
||||||
- Key-based children reconciliation: `docs/research/reconciler/02-key-based-children-reconciliation.md`
|
- Key-based children reconciliation: `../../research/reconciler/02-key-based-children-reconciliation.md`
|
||||||
- Reconciler architecture: [reconciler.md](../reconciler.md)
|
- Reconciler architecture: [reconciler.md](../reconciler.md)
|
||||||
- Reactive layer: [reactive-layer.md](../reactive-layer.md)
|
- Reactive layer: [reactive-layer.md](../reactive-layer.md)
|
||||||
- HostConfig interface: [host-config.md](../host-config.md)
|
- HostConfig interface: [host-config.md](../host-config.md)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: stable
|
||||||
last_updated: 2026-05-18
|
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`
|
- 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)
|
- 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)
|
- Reconciler architecture: [reconciler.md](reconciler.md)
|
||||||
- Key-based reconciliation research: `docs/research/reconciler/02-key-based-children-reconciliation.md`
|
- Key-based reconciliation research: `../research/reconciler/02-key-based-children-reconciliation.md`
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: stable
|
||||||
last_updated: 2026-05-18
|
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.
|
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<TType extends keyof UjsxEventMap = keyof UjsxEventMap> = 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 |
|
| Event | When emitted | Payload |
|
||||||
|-------|-------------|---------|
|
|-------|-------------|---------|
|
||||||
| `root.render` | HostConfig calls `render()` on a root | `childCount` of the rendered tree |
|
| `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
|
### 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
|
## Constraints
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: stable
|
||||||
last_updated: 2026-05-18
|
last_updated: 2026-05-18
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -82,6 +82,85 @@ interface Root<TTag extends string, Instance, RootCtx> {
|
|||||||
|
|
||||||
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`.
|
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<ContextValue>)
|
||||||
|
get(): ContextValue
|
||||||
|
get signal(): ReadonlySignal<ContextValue>
|
||||||
|
set(partial: Partial<ContextValue>): void
|
||||||
|
subscribe(fn: (value: ContextValue) => void): () => void
|
||||||
|
fork(overrides: Partial<ContextValue>): Context
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Context` wraps a Preact `signal<ContextValue>` 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<string, unknown>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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()
|
## createRoot()
|
||||||
|
|
||||||
```typescript
|
```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.
|
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()`
|
### 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.
|
- **`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.
|
- **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`.
|
- **`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.
|
- **`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.
|
- **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
|
- Context: `src/core/context.ts` — `Context` class with signal-based values
|
||||||
- Reconciler architecture: [reconciler.md](reconciler.md)
|
- Reconciler architecture: [reconciler.md](reconciler.md)
|
||||||
- Lifecycle management: [lifecycle.md](lifecycle.md)
|
- Lifecycle management: [lifecycle.md](lifecycle.md)
|
||||||
- Reconciler research: `docs/research/reconciler/01-reactive-host-bridge.md` and `03-unmount-dispose-support.md`
|
- Reconciler research: `../research/reconciler/01-reactive-host-bridge.md` and `../research/reconciler/03-unmount-dispose-support.md`
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: stable
|
||||||
last_updated: 2026-05-18
|
last_updated: 2026-05-18
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ All disposal operations must be idempotent. Calling `dispose()` twice must not e
|
|||||||
|
|
||||||
## References
|
## 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)
|
- Reconciler architecture: [reconciler.md](reconciler.md)
|
||||||
- HostConfig interface: [host-config.md](host-config.md)
|
- HostConfig interface: [host-config.md](host-config.md)
|
||||||
- Reactive layer: [reactive-layer.md](reactive-layer.md)
|
- Reactive layer: [reactive-layer.md](reactive-layer.md)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: stable
|
||||||
last_updated: 2026-05-18
|
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.
|
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
|
## setNode
|
||||||
|
|
||||||
```typescript
|
```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.
|
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
|
## 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.
|
`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`
|
- Source: `src/core/pointer.ts`
|
||||||
- UNode schema: `src/core/schema.ts`
|
- UNode schema: `src/core/schema.ts`
|
||||||
- Preact signals: `@preact/signals-core`
|
- 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)
|
- 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)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: stable
|
||||||
last_updated: 2026-05-18
|
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`
|
- ADR-003: `docs/architecture/decisions/003-preact-signals-for-reactivity.md`
|
||||||
- Reconciler architecture: [reconciler.md](reconciler.md)
|
- Reconciler architecture: [reconciler.md](reconciler.md)
|
||||||
- Lifecycle management: [lifecycle.md](lifecycle.md)
|
- Lifecycle management: [lifecycle.md](lifecycle.md)
|
||||||
- Reactive → Host bridge research: `docs/research/reconciler/01-reactive-host-bridge.md`
|
- Reactive → Host bridge research: `../research/reconciler/01-reactive-host-bridge.md`
|
||||||
- Unmount & dispose research: `docs/research/reconciler/03-unmount-dispose-support.md`
|
- Unmount & dispose research: `../research/reconciler/03-unmount-dispose-support.md`
|
||||||
- Preact signals-core: `@preact/signals-core`
|
- Preact signals-core: `@preact/signals-core`
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: stable
|
||||||
last_updated: 2026-05-18
|
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
|
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
|
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
|
## File Structure
|
||||||
|
|
||||||
@@ -256,11 +256,11 @@ Host implementations that currently only support mount-only rendering will start
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- Reconciler research: `docs/research/reconciler/README.md`
|
- Reconciler research: `../research/reconciler/README.md`
|
||||||
- Key field design: `docs/research/reconciler/00-KEY-FIELD-DESIGN.md`
|
- Key field design: `../research/reconciler/00-KEY-FIELD-DESIGN.md`
|
||||||
- Reactive → Host bridge: `docs/research/reconciler/01-reactive-host-bridge.md`
|
- Reactive → Host bridge: `../research/reconciler/01-reactive-host-bridge.md`
|
||||||
- Children reconciliation: `docs/research/reconciler/02-key-based-children-reconciliation.md`
|
- Children reconciliation: `../research/reconciler/02-key-based-children-reconciliation.md`
|
||||||
- TypeBox optimization layer: `docs/research/reconciler/04-typebox-optimization-layer.md`
|
- TypeBox optimization layer: `../research/reconciler/04-typebox-optimization-layer.md`
|
||||||
- Lifecycle management: [lifecycle.md](lifecycle.md)
|
- Lifecycle management: [lifecycle.md](lifecycle.md)
|
||||||
- HostConfig interface: [host-config.md](host-config.md)
|
- HostConfig interface: [host-config.md](host-config.md)
|
||||||
- Reactive layer: [reactive-layer.md](reactive-layer.md)
|
- Reactive layer: [reactive-layer.md](reactive-layer.md)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: stable
|
||||||
last_updated: 2026-05-18
|
last_updated: 2026-05-18
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ function isUPrimitive(node: UNode): node is UPrimitive
|
|||||||
|
|
||||||
| Guard | Logic | Discriminator |
|
| 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"` |
|
| `isURoot` | `typeof === "object" && "type" in node && node.type === "root"` | `type === "root"` |
|
||||||
| `isUPrimitive` | `typeof === "string" \|\| typeof === "number" \|\| typeof === "boolean" \|\| node === null` | Not an object |
|
| `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
|
## 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.
|
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.
|
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`
|
- Source: `src/core/schema.ts`
|
||||||
- Key field ADR: [decisions/004-key-as-first-class-field.md](decisions/004-key-as-first-class-field.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)
|
- Reconciler architecture: [reconciler.md](reconciler.md)
|
||||||
- Key field design research: `docs/research/reconciler/00-KEY-FIELD-DESIGN.md`
|
- Key field design research: `../research/reconciler/00-KEY-FIELD-DESIGN.md`
|
||||||
- TypeBox Module as type registry: `docs/architecture/decisions/002-typebox-module-as-registry.md`
|
- TypeBox Module as type registry: [decisions/002-typebox-module-as-registry.md](decisions/002-typebox-module-as-registry.md)
|
||||||
- HTML-agnostic core: `docs/architecture/decisions/001-html-agnostic-core.md`
|
- HTML-agnostic core: [decisions/001-html-agnostic-core.md](decisions/001-html-agnostic-core.md)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: stable
|
||||||
last_updated: 2026-05-18
|
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.
|
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
|
## Known Gaps
|
||||||
|
|
||||||
### No transform composition beyond next
|
### No transform composition beyond next
|
||||||
|
|||||||
Reference in New Issue
Block a user