fix: architecture review - address 5 critical issues, 6 warnings, 3 suggestions
Critical fixes: - C1: Create standalone ADR-006 file (edge type consistency), extract from open-questions.md inline content - C2: Convert CallResult from plain interface to TypeBox schema, aligning with 'TypeBox as single source of truth' constraint - C3: Add fromJSON() cycle detection specification - enforce ADR-002 DAG invariant even on deserialized input - C4: Rewrite consumer-integration.md Phase 4 to use ADR-005 event-append pattern instead of direct signal mutation - C5: Fix operator precedence bug in consumer-integration.md (missing parentheses around OR condition) Warnings addressed: - W1: Fix immutability claim - operation graph is 'conventionally immutable', not prevented by API - W2: Add EventLogProjection to reactive exports map - W3: Add CallResult/CallResultSchema to schema exports map - W4: Fix reactive-execution.md Level 1 error handling to use event-append pattern instead of direct signal mutation - W5: Remove duplicate dataFlow inference description in schema.md - W6: Clarify ADR-006 project context (flowgraph vs taskgraph) Suggestions implemented: - S1: Add 'reviewed' document lifecycle status between draft/stable, update all docs to reviewed status - S2: Add carve-out note for analysis result types in schema.md constraints (they are ephemeral, not serialized) - S3: Add isComplete() and getAggregateStatus() convenience methods to WorkflowReactiveRoot specification
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
---
|
||||
status: draft
|
||||
status: reviewed
|
||||
last_updated: 2026-05-22
|
||||
---
|
||||
|
||||
@@ -101,21 +101,22 @@ type NodeStatus = Static<typeof NodeStatusEnum>;
|
||||
|
||||
### CallResult
|
||||
|
||||
The result of a completed call, used by `Conditional.test` and `Map.over` to access predecessor outputs:
|
||||
The result of a completed call, used by `Conditional.test` and `Map.over` to access predecessor outputs. Following the TypeBox-as-single-source-of-truth principle, `CallResult` is defined as a TypeBox schema with the corresponding type derived via `Static`:
|
||||
|
||||
```typescript
|
||||
interface CallResult {
|
||||
status: NodeStatus; // Status of the call (completed, failed, aborted, skipped)
|
||||
output: unknown; // Call output (if completed)
|
||||
error?: { // Call error (if failed)
|
||||
code: string;
|
||||
message: string;
|
||||
details?: unknown;
|
||||
};
|
||||
}
|
||||
const CallResultSchema = Type.Object({
|
||||
status: NodeStatusEnum, // Status of the call (completed, failed, aborted, skipped)
|
||||
output: Type.Unknown(), // Call output (if completed)
|
||||
error: Type.Optional(Type.Object({ // Call error (if failed)
|
||||
code: Type.String(),
|
||||
message: Type.String(),
|
||||
details: Type.Optional(Type.Unknown()),
|
||||
})),
|
||||
});
|
||||
type CallResult = Static<typeof CallResultSchema>;
|
||||
```
|
||||
|
||||
`CallResult` is the value in the `results` map passed to `Conditional.test` and `Map.over` functions. It's derived from `CallNodeAttrs` but simplified for template use — it omits `requestId`, `operationId`, `identity`, and timestamps, preserving only what template logic needs.
|
||||
`CallResult` is the value in the `results` map passed to `Conditional.test` and `Map.over` functions. It's derived from `CallNodeAttrs` but simplified for template use — it omits `requestId`, `operationId`, `identity`, and timestamps, preserving only what template logic needs. The `output` field uses `Type.Unknown()` because call outputs are arbitrary data; the `error` field mirrors the `CallNodeAttrs.error` structure.
|
||||
|
||||
### OperationTypeEnum
|
||||
|
||||
@@ -262,7 +263,7 @@ type OperationEdgeAttrs = Static<typeof OperationEdgeAttrs>;
|
||||
|
||||
Type-compatibility edges carry a boolean `compatible` flag, an optional `detail` string, and optional structured `mismatches`. This allows the operation graph to include both compatible edges (green paths) and incompatible edges (red paths) for diagnostics. The `detail` field provides a human-readable summary, while `mismatches` provides machine-readable field-level diagnostics. The `TypeCompatResult` from `typeCompat()` populates both fields: `detail` for compatible edges and `mismatches` for incompatible ones.
|
||||
|
||||
**Edge type storage (OQ-004)**: `edgeType` is a required universal attribute stored on every edge, regardless of graph mode. This applies uniformly: operation graph edges have `edgeType: "typed"`, call graph edges have `edgeType: "triggered"` or `"depends_on"`, and template edges have `edgeType: "sequential"` or `"conditional"`. The `edgeType` field is stored alongside mode-specific attributes in graphology, not inside the mode-specific attribute schemas (`OperationEdgeAttrs`, `TriggeredEdgeAttrs`, etc.). This ensures consistent serialization/deserialization, uniform graphology queries, and straightforward edge-type filtering. See ADR-006 for the full decision record.
|
||||
**Edge type storage (OQ-004)**: `edgeType` is a required universal attribute stored on every edge, regardless of graph mode. This applies uniformly: operation graph edges have `edgeType: "typed"`, call graph edges have `edgeType: "triggered"` or `"depends_on"`, and template edges have `edgeType: "sequential"` or `"conditional"`. The `edgeType` field is stored alongside mode-specific attributes in graphology, not inside the mode-specific attribute schemas (`OperationEdgeAttrs`, `TriggeredEdgeAttrs`, etc.). This ensures consistent serialization/deserialization, uniform graphology queries, and straightforward edge-type filtering. See [ADR-006](decisions/006-edge-type-consistency.md) (flowgraph) for the full decision record.
|
||||
|
||||
```typescript
|
||||
// How operation graph edges are stored in graphology:
|
||||
@@ -347,12 +348,6 @@ Edges where `dataFlow` cannot be determined (e.g., `Operation.input` is an opaqu
|
||||
|
||||
Over-marking `dataFlow: true` is safe (it just causes an unnecessary type compatibility check), while under-marking is safe (it skips a check that would have passed anyway, but could let a type-incompatible connection through). The conservative strategy errs on the side of under-marking.
|
||||
|
||||
The `dataFlow` attribute is **inferred** by the `GraphologyHostConfig` during template rendering, not manually specified by template authors:
|
||||
|
||||
- A `Sequential` edge where the downstream node references `results["upstreamNode"]` in any expression gets `dataFlow: true`
|
||||
- A `Sequential` edge where no such reference exists gets `dataFlow: false` (the default)
|
||||
- A `Conditional` edge always gets `dataFlow: true` (the condition always reads a predecessor's result)
|
||||
|
||||
This resolves OQ-01 and OQ-02: `typeCompat()` only runs on edges where `dataFlow: true`. Temporal-only edges bypass type checking entirely, since no data flows between the connected nodes.
|
||||
|
||||
**Note**: `TemplateEdgeAttrs.edgeType` uses a constrained union of `"sequential" | "conditional"` rather than the full `EdgeTypeEnum`. Template DAGs never have `triggered`, `depends_on`, or `typed` edges — those belong to call graphs and operation graphs respectively.
|
||||
@@ -427,7 +422,7 @@ For call graphs, edges can be either `triggered` or `depends_on`, distinguished
|
||||
|
||||
## Edge Key Convention
|
||||
|
||||
Following taskgraph's ADR-006, edge keys are deterministic:
|
||||
Following taskgraph's ADR-006 (edge key convention), edge keys are deterministic:
|
||||
|
||||
```
|
||||
${source}->${target}
|
||||
@@ -453,8 +448,8 @@ This is an exception to the simple `${source}->${target}` pattern, but it's nece
|
||||
|
||||
## Constraints
|
||||
|
||||
- **TypeBox schemas are the single source of truth** — no hand-written `interface` or `type` definitions for data shapes. All types are derived via `Static<typeof Schema>`.
|
||||
- **Edge keys are deterministic** — `${source}->${target}` format, following ADR-006 in taskgraph.
|
||||
- **TypeBox schemas are the single source of truth** — no hand-written `interface` or `type` definitions for data shapes that participate in graph attributes, serialization, or runtime validation. All such types are derived via `Static<typeof Schema>`. Exception: analysis result types returned by validation and compatibility functions (e.g., `ValidationError`, `GraphValidationError`, `TypeIncompatError`) are plain interfaces because they are ephemeral result objects, not serialized graph data. They don't need TypeBox schemas because they are never persisted or transmitted — they are consumed locally and discarded.
|
||||
- **Edge keys are deterministic** — `${source}->${target}` format, following taskgraph's ADR-006 (edge key convention).
|
||||
- **No parallel edges** — `multi: false` in graphology. At most one edge per (source, target) pair.
|
||||
- **No self-loops** — `allowSelfLoops: false`. An operation cannot be its own prerequisite.
|
||||
- **ISO timestamp strings** — Call graph timestamps are ISO 8601 strings, matching the storage schema.
|
||||
@@ -464,7 +459,7 @@ This is an exception to the simple `${source}->${target}` pattern, but it's nece
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. ~~**Should `edgeType` be a required field on ALL edges, or only on call graph and template edges?**~~ **Resolved (OQ-004)**: `edgeType` is required on all edges, stored as a universal attribute alongside mode-specific attributes. The mode-specific attribute schemas (`OperationEdgeAttrs`, `TriggeredEdgeAttrs`, `DependencyEdgeAttrs`) do NOT include `edgeType` — it's stored separately in graphology at the same level as the mode-specific attributes. This ensures consistent serialization/deserialization, uniform graphology queries, and straightforward edge-type filtering across all graph modes. See ADR-006.
|
||||
1. ~~**Should `edgeType` be a required field on ALL edges, or only on call graph and template edges?**~~ **Resolved (OQ-004)**: `edgeType` is required on all edges, stored as a universal attribute alongside mode-specific attributes. The mode-specific attribute schemas (`OperationEdgeAttrs`, `TriggeredEdgeAttrs`, `DependencyEdgeAttrs`) do NOT include `edgeType` — it's stored separately in graphology at the same level as the mode-specific attributes. This ensures consistent serialization/deserialization, uniform graphology queries, and straightforward edge-type filtering across all graph modes. See [ADR-006](decisions/006-edge-type-consistency.md) (flowgraph).
|
||||
|
||||
2. ~~**Should `CallNodeAttrs.identity` be a `Type.Record` or the structured `Identity` type from operations?**~~ **Resolved (OQ-022)**: Import the `Identity` type structure from `@alkdev/operations` (peer dependency). Since `@alkdev/operations` is already a peer dependency (for `CallEventMapValue`), adding this type import creates minimal additional coupling. The `CallNodeAttrs.identity` field mirrors the `Identity` interface: `{ id, scopes, resources? }`. Version alignment is handled by semver ranges. The TypeBox schema for `identity` is defined inline in `CallNodeAttrs` to match the shape (not imported as a TypeBox schema, since `@alkdev/operations` defines `Identity` as a TypeScript interface), but the field semantics match exactly.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user