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:
@@ -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" }),
|
||||
|
||||
Reference in New Issue
Block a user