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
|
||||
---
|
||||
|
||||
# Workflow Templates
|
||||
@@ -13,7 +13,7 @@ Workflow templates are ujsx trees that define reusable call patterns. Instead of
|
||||
|
||||
```typescript
|
||||
import { h, createRoot } from "@alkdev/ujsx";
|
||||
import { Operation, Sequential, Parallel, Conditional } from "@alkdev/flowgraph/component";
|
||||
import { Operation, Sequential, Parallel, Conditional, Map } from "@alkdev/flowgraph/component";
|
||||
import { GraphologyHostConfig } from "@alkdev/flowgraph/host/graphology";
|
||||
|
||||
const sddPipeline = h(Sequential, {},
|
||||
@@ -38,7 +38,7 @@ The template is a `UNode` tree — a plain data structure that can be:
|
||||
- **Rendered to a graphology DAG** via the `GraphologyHostConfig` for structural analysis
|
||||
- **Rendered to a reactive execution engine** via the `ReactiveHostConfig` for runtime status tracking
|
||||
|
||||
This is the same `UNode` tree that ujsx defines, with flowgraph-specific component functions (`Operation`, `Sequential`, `Parallel`, `Conditional`) that produce `UElement` nodes with workflow-specific props and meaning.
|
||||
This is the same `UNode` tree that ujsx defines, with flowgraph-specific component functions (`Operation`, `Sequential`, `Parallel`, `Conditional`, `Map`) that produce `UElement` nodes with workflow-specific props and meaning.
|
||||
|
||||
## Why ujsx as Template IR
|
||||
|
||||
@@ -128,7 +128,82 @@ const Conditional: UComponent<{
|
||||
|
||||
When rendered to a graphology DAG, `Conditional` creates an edge with `edgeType: "conditional"` and `condition` attribute. When rendered to the reactive engine, the condition is evaluated as a `computed` that depends on the referenced step's status and output.
|
||||
|
||||
If the test evaluates to `false`, the branch is marked `skipped` in `NodeStatus`.
|
||||
If the test evaluates to `false` and no `else` branch is provided, the branch nodes transition to `skipped` in `NodeStatus`.
|
||||
|
||||
#### Else-branch behavior
|
||||
|
||||
When the `else` prop is provided, the `Conditional` renders two subgraphs:
|
||||
|
||||
**DAG rendering (GraphologyHostConfig)**:
|
||||
- The `then` branch (child) renders with an edge from the conditional's predecessor to the first child, with `edgeType: "conditional"` and `condition: <test>`.
|
||||
- The `else` branch renders as a separate subgraph with `edgeType: "conditional"` and `condition: <negated test>`. The negated condition is derived automatically.
|
||||
- Both branches share the same predecessor — the `Conditional` node's structural position in the template determines the common starting point.
|
||||
|
||||
**Reactive rendering (ReactiveHostConfig)**:
|
||||
- When `test` evaluates to `true`: `then`-branch nodes become `ready` (preconditions met). `else`-branch nodes transition to `skipped`. Their `preconditions` are satisfied by the `skipped` state — downstream nodes see the `Conditional` as completed regardless of which branch was taken.
|
||||
- When `test` evaluates to `false`: `else`-branch nodes become `ready`. `then`-branch nodes transition to `skipped`. Downstream nodes after the `Conditional` see all branches as resolved.
|
||||
- When no `else` prop is provided: the `false` branch simply doesn't exist. Nodes after the `Conditional` that depend on it still see it as `completed` because the `Conditional` itself resolves regardless of which path is taken.
|
||||
|
||||
This means a `Conditional` with an `else` branch acts as a **complete error boundary** — downstream nodes are insulated from the branch choice. The `Conditional` is `completed` whether the `then` or `else` branch executed.
|
||||
|
||||
### `<Map>`
|
||||
|
||||
Represents mapping over an array — creates one child instance per array item:
|
||||
|
||||
```typescript
|
||||
const Map: UComponent<{
|
||||
over: Signal<unknown[]> | unknown[] | ((results: Record<string, CallResult>) => unknown[]);
|
||||
// Static array, signal, or function that resolves against predecessor results
|
||||
as: string; // Variable name for each item
|
||||
children: UNode; // Template rendered per item
|
||||
}>;
|
||||
```
|
||||
|
||||
The `<Map>` component dynamically replicates its child template for each element in the `over` array. Each replica gets the current element bound to the variable named by `as`.
|
||||
|
||||
**DAG rendering (GraphologyHostConfig)**:
|
||||
- For each item in `over`, renders a copy of the child template as a node.
|
||||
- Each mapped node has a `sequential` edge from the `Map`'s predecessor (all mapped nodes start at the same point, like `Parallel`).
|
||||
- Mapped nodes are named with a composite key: `${parentKey}.${as}[${index}]`. For example, `<Map over={items} as="item">` with 3 items creates nodes `item[0]`, `item[1]`, `item[2]`.
|
||||
- The `Map` container itself is transparent in the graph (no node for the container).
|
||||
|
||||
**Reactive rendering (ReactiveHostConfig)**:
|
||||
- For each item in `over`, creates a `WorkflowNode` with its own `signal<NodeStatus>` and `computed` preconditions.
|
||||
- All mapped nodes' preconditions are identical: the `Map`'s predecessor must be `completed` (same as `Parallel`).
|
||||
- Each mapped node's `output` signal holds the result of its corresponding call.
|
||||
- The `Map` result is available as an aggregated signal containing all mapped nodes' outputs.
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
h(Sequential, {},
|
||||
h(Operation, { name: "fetch-items" }),
|
||||
h(Map, {
|
||||
over: (results) => results["fetch-items"].output.items,
|
||||
as: "item"
|
||||
},
|
||||
h(Operation, { name: "process-item", input: (results, { item }) => item }),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
This creates a `Sequential` where `process-item` is called once per item returned by `fetch-items`. Each call gets its corresponding item as input.
|
||||
|
||||
**Edge type**: Mapped children use the same `TemplateEdgeAttrs` as `Parallel` children (no `sequential` edges between siblings). The `Map` component is structurally equivalent to a `Parallel` group where the children are dynamically generated from an array.
|
||||
|
||||
**Aggregate completion semantics**: A `Map` node's status follows "worst-case" semantics:
|
||||
|
||||
- If **all** mapped nodes reach a satisfying terminal state (`completed` or `skipped`), the `Map` is considered `completed`.
|
||||
- If **any** mapped node reaches `failed`, the `Map` is considered `failed` (unless caught by a `Conditional`).
|
||||
- If **any** mapped node reaches `aborted`, the `Map` is considered `aborted`.
|
||||
- Downstream nodes whose preconditions include the `Map` will see `blockedByFailure = true` if the `Map` has any `failed` or `aborted` children.
|
||||
|
||||
This means a `Map` with partial failure (some nodes succeeded, one failed) propagates as a failure to downstream dependents. If partial success is needed, the template author should use a `Conditional` to handle the failure case, or process the `Map` results individually rather than treating the `Map` as a single dependency.
|
||||
|
||||
**Reactive behavior for mapped nodes**:
|
||||
- All mapped nodes become `ready` simultaneously when the `Map`'s predecessor completes (parallel start).
|
||||
- If any mapped node fails, only that node transitions to `failed`. Other mapped nodes continue independently (failure follows dependency edges, not structural scope, consistent with `Parallel` behavior).
|
||||
- Mapped nodes participate in failure propagation like any other node: downstream dependents see `blockedByFailure` if a mapped node fails and they depend on it.
|
||||
|
||||
## Template → DAG Conversion
|
||||
|
||||
@@ -259,6 +334,36 @@ Validation checks:
|
||||
|
||||
Validation returns an array of `ValidationError` objects (never throws). See [analysis.md](analysis.md) for the full validation algorithm.
|
||||
|
||||
## Composition Rules
|
||||
|
||||
Not all component combinations are valid. The following rules govern which components can appear as children of which:
|
||||
|
||||
| Parent | Valid children | Notes |
|
||||
|--------|---------------|-------|
|
||||
| `Sequential` | `Operation`, `Sequential`, `Parallel`, `Conditional`, `Map` | Children execute in order |
|
||||
| `Parallel` | `Operation`, `Sequential`, `Parallel`, `Conditional`, `Map` | Children execute concurrently |
|
||||
| `Conditional` (then) | `Operation`, `Sequential`, `Parallel`, `Map` | Single child or wrapped in structural container |
|
||||
| `Conditional` (else) | `Operation`, `Sequential`, `Parallel`, `Map` | Single child or wrapped in structural container |
|
||||
| `Map` | `Operation`, `Sequential`, `Parallel`, `Conditional` | Template rendered per item |
|
||||
|
||||
### Rules
|
||||
|
||||
1. **`Operation` has no children** — an `Operation` is a leaf node. Nesting inside `Operation` is a template validation error.
|
||||
|
||||
2. **`Conditional` takes a single then-child via children, and optional else via `else` prop** — the `children` of `Conditional` are the then-branch. The `else` prop is the alternative branch. Both branches can be single `Operation` nodes or structural containers (`Sequential`, `Parallel`, `Map`).
|
||||
|
||||
3. **`Conditional.test` cannot reference an `Operation` inside the Conditional** — the test evaluates results from predecessor operations, not from the conditional branch itself. This would create a circular dependency.
|
||||
|
||||
4. **`Map.over` must be a serializable expression or signal** — the array can be a static value, a signal, or a function that receives results from predecessor operations. Function-valued `over` props don't survive JSON round-trips (same limitation as `Conditional.test`).
|
||||
|
||||
5. **`Sequential` with one child is valid but degenerate** — it produces no edges (no sequential ordering needed). A single-child `Sequential` is equivalent to the child alone.
|
||||
|
||||
6. **`Parallel` with one child is valid but degenerate** — it produces no edges (no concurrency needed). A single-child `Parallel` is equivalent to the child alone.
|
||||
|
||||
7. **Nesting is allowed to any depth** — `Sequential` inside `Parallel` inside `Sequential` is valid. The DAG flattens nesting into edges between leaf `Operation` nodes.
|
||||
|
||||
8. **Template root must be a structural container** — the root element must be `Sequential`, `Parallel`, or `Map`. A bare `Operation` as root is technically valid but produces a single-node DAG with no edges.
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Templates are ujsx trees** — no custom format, no parser, no compiler. Components are `UComponent` functions that produce `UElement` nodes.
|
||||
@@ -272,7 +377,7 @@ Validation returns an array of `ValidationError` objects (never throws). See [an
|
||||
|
||||
1. **Should `Sequential` and `Parallel` be transparent in the graph?** Currently they produce edges, not nodes. An alternative is to create "virtual" grouping nodes (like a "parallel gateway" in BPMN). This would make the graph structure richer but adds complexity.
|
||||
|
||||
2. **Should templates support loops?** A `<ForEach>` component that iterates over an array and produces a child for each element. This would enable dynamic workflows where the number of parallel calls isn't known at template definition time.
|
||||
2. ~~**Should templates support loops?**~~ **Resolved**: The `<Map>` component provides array iteration — one child per array element. It does NOT support general loops (while, do-while). For repeated execution with conditional exit, use `Conditional` inside a `Sequential` group. General-purpose loops with arbitrary termination conditions are not supported because they would require cycle-supporting templates, which conflicts with the DAG-only invariant.
|
||||
|
||||
3. **Should templates support `depends_on` edges explicitly?** Currently dependencies are inferred from structure (sequential implies dependency). An explicit `<DependsOn target="operation-name" />` component would make data dependencies visible in the template without relying on sequential ordering.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user