resolve architecture review round 2: criticals, warnings, suggestions

- C-05: Add flowgraph-api.md with complete public API surface
- C-06: Document <Map> component in workflow-templates.md
- C-07: Specify Conditional else-branch behavior
- C-08: Add lifecycle/ownership section to reactive-execution.md
- C-09: Add consumer-integration.md end-to-end walkthrough
- W-02: Add reactive error boundary semantics (3 levels)
- W-03: Complete ReactiveContext interface definition
- W-04: Add template composition rules (8 rules)
- W-05: Document removeChild for both HostConfigs
- W-06: Document signal/effect disposal lifecycle
- W-07: Add ADR-004 (no schema version field)
- W-08: Add type compatibility depth/contract to analysis.md
- W-11: Add performance characteristics section
- S-01: Getting Started merged into consumer-integration.md
- S-02: Add flow diagrams for template rendering pipeline
- S-03: Add node status state machine diagram
- S-04: Add testing strategy section
- S-06: Validate source structure cross-references

Review round 2 fixes:
- Define TemplateNodeAttrs as alias for OperationNodeAttrs
- Document CallEventMapValue and CallResult types in schema.md
- Standardize CycleError naming (replace CircularDependencyError)
- Add function form to Map.over type definition
- Define Map aggregate completion/failure semantics
- Fix immutability claim for fromCallEvents
- Clarify edgeType storage alongside OperationEdgeAttrs
- Clarify WorkflowNode.status === statusMap (same Signal)
- Add component-to-tag mapping for WorkflowTag
This commit is contained in:
2026-05-19 13:05:35 +00:00
parent 1dbaccbde3
commit eaeba38e71
13 changed files with 1489 additions and 57 deletions

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-05-19
last_updated: 2026-05-20
---
# Host Configs
@@ -16,7 +16,7 @@ Flowgraph uses ujsx's `HostConfig` pattern to render the same workflow template
| GraphologyHostConfig | `DirectedGraph` | Validate templates, check cycles, compute topological order |
| ReactiveHostConfig | `Map<string, WorkflowNode>` | Runtime execution with signal-driven status propagation |
Both HostConfigs share the same template components (`Operation`, `Sequential`, `Parallel`, `Conditional`) and the same tag type. The difference is what `createInstance` and `appendChild` do:
Both HostConfigs share the same template components (`Operation`, `Sequential`, `Parallel`, `Conditional`, `Map`) and the same tag type. The difference is what `createInstance` and `appendChild` do:
- **GraphologyHostConfig**: Creates graph nodes and edges. `appendChild` adds an edge.
- **ReactiveHostConfig**: Creates a `WorkflowNode` (with a `signal<NodeStatus>`) and registers preconditions. `appendChild` registers the parent-child dependency.
@@ -29,6 +29,20 @@ type WorkflowTag = "operation" | "sequential" | "parallel" | "conditional" | "ma
This constrains `HostConfig<TTag, ...>` to only accept workflow-specific element types. Attempting to render an unsupported tag (e.g., `"div"`) is a type error at compile time.
### Component-to-Tag Mapping
Each `UComponent` function produces a `UElement` with a specific `type` string (the `WorkflowTag`). The mapping is:
| Component function | UElement.type (WorkflowTag) |
|-------------------|---------------------------|
| `Operation` | `"operation"` |
| `Sequential` | `"sequential"` |
| `Parallel` | `"parallel"` |
| `Conditional` | `"conditional"` |
| `Map` | `"map"` |
When ujsx's reconciler calls `HostConfig.createInstance(tag, props, ...)`, the `tag` parameter is the `WorkflowTag` string. For example, `h(Operation, { name: "classify" })` produces `{ type: "operation", props: { name: "classify" }, children: [] }`, and `createInstance("operation", { name: "classify" }, ctx)` is called.
## GraphologyHostConfig
### Type Parameters
@@ -52,6 +66,8 @@ interface GraphNode {
}
```
Where `TemplateNodeAttrs` is a type alias for `OperationNodeAttrs` (see [schema.md](schema.md#TemplateNodeAttrs)) — template nodes carry the same attributes as operation nodes. Structural containers (`Sequential`, `Parallel`, `Conditional`, `Map`) return a `GraphNode` with an empty `attributes` object and a synthetic key.
The `RootCtx` is:
```typescript
@@ -147,9 +163,36 @@ finalizeInstance?(instance: GraphNode, ctx: GraphContext): void {
}
```
### removeChild
```typescript
removeChild(parent: GraphNode, child: GraphNode, ctx: GraphContext): void {
// Remove the edge between parent and child
// Structural containers are transparent, so parent/child are real operation nodes
if (!isStructuralContainer(parent) && !isStructuralContainer(child)) {
ctx.graph.dropEdge(`${parent.key}->${child.key}`);
}
}
```
`removeChild` is called by the ujsx reconciler when a child is removed from a parent. In the `GraphologyHostConfig`, this removes the corresponding DAG edge. The child node itself is NOT removed from the graph — node removal is handled by `removeFromGraph` (see below).
**Note**: The ujsx reconciler is not yet implemented. Currently, `removeChild` is defined but only called in tests. The `GraphologyHostConfig` is mount-only until the reconciler is available.
### removeChildFromHost (node removal)
```typescript
removeChildFromHost?(parent: GraphNode, child: GraphNode, ctx: GraphContext): void {
// Remove the child node from the graph
ctx.graph.dropNode(child.key);
}
```
When the reconciler removes a child entirely (not just moving it to a different parent), it calls `removeChildFromHost`. This removes the node and ALL attached edges (graphology cascading removal). This is important for cleanup when a template is re-rendered and a node no longer exists.
### Cycle Detection
After rendering, the HostConfig checks for cycles using `graphology-dag.hasCycle()`. If a cycle is detected, the rendering throws `CircularDependencyError` with the cycle paths.
After rendering, the HostConfig checks for cycles using `graphology-dag.hasCycle()`. If a cycle is detected, the rendering throws `CycleError` with the cycle paths.
This is the primary validation step: a valid workflow template must produce a valid DAG. Cycles in a template mean infinite loops in execution, which are always design errors.
@@ -190,12 +233,21 @@ Each `WorkflowNode` holds:
```typescript
interface ReactiveContext {
operationRegistry: OperationRegistry;
nodes: Map<string, WorkflowNode>; // All nodes by key
statusSignals: Map<string, Signal<NodeStatus>>; // Status signals by key
operationRegistry: OperationRegistry; // Resolves operation names to specs
nodes: Map<string, WorkflowNode>; // All nodes by key
statusSignals: Map<string, Signal<NodeStatus>>; // Status signals by key (owned by WorkflowReactiveRoot)
preconditions: Map<string, Computed<boolean>>; // Precondition computeds by key (owned by WorkflowReactiveRoot)
blockedByFailure: Map<string, Computed<boolean>>; // blockedByFailure computeds by key (owned by WorkflowReactiveRoot)
parentMap: Map<string, string>; // Child → parent key mapping (for precondition computation)
siblingMap: Map<string, string[]>; // Parent → children keys (for structural context)
results: Map<string, Signal<unknown>>; // Operation output signals by key
}
```
The `ReactiveContext` is constructed during `ReactiveHostConfig` initialization. It receives the `operationRegistry` and empty maps. During `createInstance`, nodes and signals are registered in the context maps. After rendering completes, the context holds a complete index of the reactive workflow tree.
**Important**: `statusSignals`, `preconditions`, and `blockedByFailure` are references to the `WorkflowReactiveRoot`'s maps. The `ReactiveHostConfig` does not own these signals — it looks them up during `createInstance` to wire `WorkflowNode` references. Disposal is the `WorkflowReactiveRoot`'s responsibility.
### createInstance
```typescript
@@ -271,6 +323,38 @@ The reactive engine then starts the call associated with the node (when `ready`)
**Note**: Failure propagation follows dependency edges, not structural scope. A failed node only causes its downstream dependents (via DAG edges) to abort. Sibling branches in a `Parallel` group are independent and continue running. See [reactive-execution.md](reactive-execution.md) for the full failure propagation model.
### removeChild (ReactiveHostConfig)
```typescript
removeChild(parent: WorkflowNode, child: WorkflowNode, ctx: ReactiveContext): void {
// Remove the dependency between parent and child
// The child's preconditions are recomputed automatically (reactive)
parent.children = parent.children.filter(c => c.key !== child.key);
// The child's preconditions and blockedByFailure computeds will re-evaluate
// because the predecessor list changes
}
```
`removeChild` in the reactive host removes the parent-child dependency. Because preconditions and `blockedByFailure` are `computed` values, they automatically re-evaluate when predecessor nodes are removed.
```typescript
removeChildFromHost?(parent: WorkflowNode, child: WorkflowNode, ctx: ReactiveContext): void {
// Dispose the child's reactive state
ctx.nodes.delete(child.key);
ctx.statusSignals.delete(child.key);
ctx.preconditions.delete(child.key);
ctx.blockedByFailure.delete(child.key);
if (child.output) {
// Signal disposal is handled by WorkflowReactiveRoot.dispose()
// Here we just remove the reference from the context maps
}
}
```
For complete reactive teardown (`removeChildFromHost`), the node's signal references are removed from the context maps. The signals themselves (owned by `WorkflowReactiveRoot`) are disposed via `root.dispose()` which is the authoritative cleanup path.
**Important**: Individual node disposal (removing a node mid-execution) is not fully supported until the ujsx reconciler is implemented. Currently, the reactive tree is built once and torn down as a whole via `WorkflowReactiveRoot.dispose()`.
### Abort Cascading
System-level abort (e.g., provider outage) aborts the entire workflow:
@@ -291,6 +375,28 @@ This is reactive — when a parent node's status changes to `aborted`, the `effe
The key insight: the same ujsx template renders to both targets:
```
ujsx Template (UNode tree)
┌─────────┴─────────┐
│ │
GraphologyHostConfig ReactiveHostConfig
│ │
▼ ▼
DirectedGraph (DAG) Reactive Signal Graph
┌──────────────────┐ ┌──────────────────┐
│ Nodes: operations │ │ Nodes: WorkflowNode│
│ Edges: sequential │ │ signal<NodeStatus>│
│ conditional │ │ computed<precond> │
│ typed │ │ computed<blocked> │
└──────────────────┘ └──────────────────┘
│ │
▼ ▼
Structural Runtime Execution
Analysis & Status Tracking &
Validation Abort Propagation
```
```typescript
const template = h(Sequential, {},
h(Operation, { name: "architect" }),