resolve all remaining open questions (OQ-03–OQ-29), add ADR-006

Resolve all 19 remaining open questions across the architecture. Every
question now has a documented resolution with rationale:

- OQ-004/OQ-029: edgeType is a universal required attribute on all edges,
  single graph per FlowGraph instance (ADR-006)
- OQ-011: No OR preconditions for v1; preconditionMode as v2 extension
- OQ-012: maxConcurrency enforced via reactive counting semaphore
- OQ-014: Unknown operationId creates node with pending status
- OQ-017: Expose common graphology traversal methods on FlowGraph (80/20)
- OQ-020: condition as Type.Unknown() with string/function documentation
- OQ-022: Identity imported from @alkdev/operations peer dep
- All other questions resolved with documented rationale

Fix three critical issues found by architecture review:
1. edgeType serialization/validation gap: document two-step validation
2. CallEdgeAttrs runtime discrimination: edgeType as runtime discriminant,
   depends_on edges clarified as observability-only (not execution)
3. ADR-005 signal mutation inconsistency: explicitly distinguish call-level
   statuses (event-log-driven) from workflow-derived statuses (signal-mutation)

Additional clarifications:
- dataFlow inference uses conservative strategy (defaults false)
- Conditional.test string resolution: operationName → status === completed
- Add negated field to TemplateEdgeAttrs for else-branch conditions
- Document edge key priority convention for composite keys
- Add maxConcurrency semaphore design to reactive-execution.md
This commit is contained in:
2026-05-21 09:25:55 +00:00
parent c76be7f689
commit f3e084d02f
9 changed files with 239 additions and 268 deletions

View File

@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-05-21
last_updated: 2026-05-22
---
# Workflow Templates
@@ -130,13 +130,26 @@ When rendered to a graphology DAG, `Conditional` creates an edge with `edgeType:
If the test evaluates to `false` and no `else` branch is provided, the branch nodes transition to `skipped` in `NodeStatus`.
#### String condition resolution
When `Conditional.test` is a string (rather than a function), the HostConfig resolves it at render time using the operation registry. The resolution algorithm is:
- `test: "operationName"` → resolves to `(results) => results["operationName"]?.status === "completed"`, meaning "the then-branch is taken if the referenced operation completed successfully."
- If the referenced operation failed or was aborted, the condition evaluates to `false` and the else-branch is taken (or the then-branch is `skipped` if no else-branch).
- String conditions can only reference predecessor operations by name. For more complex conditions (checking output fields, combining multiple results, etc.), use the function form.
This resolution algorithm is deterministic and produces the same behavior regardless of which HostConfig performs the resolution.
#### 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.
- The `else` branch renders as a separate subgraph with `edgeType: "conditional"`, `condition: <test>`, and `negated: true`. The `negated` flag on `TemplateEdgeAttrs` indicates that the condition is logically negated for the else-branch. At render time, the HostConfig resolves the negation differently depending on the condition form:
- **String condition**: `condition: "fetch-data"` with `negated: true` resolves to `(results) => results["fetch-data"]?.status !== "completed"`.
- **Function condition**: The HostConfig wraps the original function: `condition: (results) => !originalTest(results)`.
- This ensures the else-branch is taken when the original condition evaluates to `false`, regardless of condition form.
- Both branches share the same predecessor — the `Conditional` node's structural position in the template determines the common starting point.
**Reactive rendering (ReactiveHostConfig)**:
@@ -378,9 +391,9 @@ Not all component combinations are valid. The following rules govern which compo
1. ~~**Should `Sequential` and `Parallel` be transparent in the graph?**~~ **Resolved (OQ-05)**: Containers stay transparent. No nodes for `Sequential`, `Parallel`, or `Conditional` in the DAG. Aggregate status for containers is computed as a projection from children's statuses. The `parentMap` and `siblingMap` in `ReactiveContext` provide the structural context for precondition computation.
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.
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 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. With ADR-005's `dataFlow` attribute, data dependencies are now inferable from template expressions — `Conditional.test` and `Map.over` that reference predecessor results set `dataFlow: true` on the corresponding edge. Explicit `depends_on` edges would add manual annotation capability, but the `dataFlow` inference may be sufficient for v1.
3. ~~**Should templates support `depends_on` edges explicitly?**~~ **Resolved (OQ-021)**: No for v1. ADR-005's `dataFlow` inference and the result projection make explicit `depends_on` unnecessary for current use cases. Data dependencies are expressed through the result projection — if B needs A's output, B reads `getResult("A")`. The `dataFlow: true` attribute on edges captures which edges carry data. An explicit `<DependsOn>` component would add template syntax complexity and potentially conflict with structural ordering. If a future use case requires non-adjacent data dependencies that can't be expressed by restructuring the template, `<DependsOn>` can be added as a v2 extension. But v1 intentionally restricts dependencies to follow the structural flow.
4. ~~**How does template instantiation interact with the call protocol?**~~ **Resolved (ADR-005)**: The template bridges to the call protocol through the event log. The hub coordinator appends call protocol events; the reactive layer projects them. Each `<Operation>` node's `requestId` maps to call protocol events via the `nodeKeyToRequestId` map. No callback, no boomerang — the event log is the bridge.