- 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
14 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-05-20 |
Build & Distribution
Package structure, exports map, dependencies, and platform targets.
Package Structure
@alkdev/flowgraph/
├── src/
│ ├── component/ # ujsx workflow components
│ │ ├── operation.ts # <Operation> component
│ │ ├── sequential.ts # <Sequential> component
│ │ ├── parallel.ts # <Parallel> component
│ │ ├── conditional.ts # <Conditional> component
│ │ ├── map.ts # <Map> component
│ │ └── index.ts
│ ├── host/
│ │ ├── graphology.ts # GraphologyHostConfig
│ │ ├── reactive.ts # ReactiveHostConfig
│ │ └── index.ts
│ ├── schema/
│ │ ├── enums.ts # CallStatus, EdgeType, NodeCategory, NodeStatus
│ │ ├── node.ts # OperationNodeAttrs, CallNodeAttrs
│ │ ├── edge.ts # OperationEdgeAttrs, CallEdgeAttrs, TemplateEdgeAttrs
│ │ ├── graph.ts # SerializedGraph, FlowGraphSerialized
│ │ └── index.ts
│ ├── graph/
│ │ ├── construction.ts # FlowGraph class (fromSpecs, fromCallEvents, fromJSON, etc.)
│ │ ├── validation.ts # validateSchema, validateGraph, validate
│ │ ├── queries.ts # topologicalOrder, hasCycles, ancestors, descendants, etc.
│ │ ├── mutation.ts # addNode, addEdge, updateNodeStatus, removeNode, etc.
│ │ └── index.ts
│ ├── reactive/
│ │ ├── workflow.ts # WorkflowReactiveRoot (signal-backed execution)
│ │ ├── node-status.ts # Signal<NodeStatus>, computed preconditions, computed blockedByFailure
│ │ └── index.ts
│ ├── analysis/
│ │ ├── type-compat.ts # typeCompat, buildTypeEdges, analyzeTypeCompat
│ │ ├── workflow.ts # validateTemplate, validatePreconditions
│ │ ├── defaults.ts # resolveDefaults for CallStatus, EdgeType, etc.
│ │ └── index.ts
│ ├── error/
│ │ └── index.ts # FlowgraphError hierarchy
│ └── index.ts # Barrel export
├── test/
│ ├── graph/
│ │ ├── construction.test.ts
│ │ ├── validation.test.ts
│ │ ├── queries.test.ts
│ │ └── mutation.test.ts
│ ├── schema/
│ │ └── enums.test.ts
│ ├── analysis/
│ │ ├── type-compat.test.ts
│ │ └── workflow.test.ts
│ ├── component/
│ │ └── components.test.ts
│ ├── host/
│ │ ├── graphology.test.ts
│ │ └── reactive.test.ts
│ └── error/
│ └── errors.test.ts
├── package.json
├── tsconfig.json
├── tsup.config.ts
├── vitest.config.ts
└── AGENTS.md
Package JSON
{
"name": "@alkdev/flowgraph",
"version": "0.1.0",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./component": {
"import": "./dist/component/index.js",
"require": "./dist/component/index.cjs"
},
"./host": {
"import": "./dist/host/index.js",
"require": "./dist/host/index.cjs"
},
"./schema": {
"import": "./dist/schema/index.js",
"require": "./dist/schema/index.cjs"
},
"./graph": {
"import": "./dist/graph/index.js",
"require": "./dist/graph/index.cjs"
},
"./reactive": {
"import": "./dist/reactive/index.js",
"require": "./dist/reactive/index.cjs"
},
"./analysis": {
"import": "./dist/analysis/index.js",
"require": "./dist/analysis/index.cjs"
},
"./error": {
"import": "./dist/error/index.js",
"require": "./dist/error/index.cjs"
}
},
"typesVersions": {
"*": {
"component": ["./dist/component/index.d.ts"],
"host": ["./dist/host/index.d.ts"],
"schema": ["./dist/schema/index.d.ts"],
"graph": ["./dist/graph/index.d.ts"],
"reactive": ["./dist/reactive/index.d.ts"],
"analysis": ["./dist/analysis/index.d.ts"],
"error": ["./dist/error/index.d.ts"]
}
}
}
Exports Map
Following the taskgraph pattern, each module has a sub-path export:
| Sub-path | Content | Use case |
|---|---|---|
@alkdev/flowgraph |
Barrel export (everything) | Full import |
@alkdev/flowgraph/component |
<Operation>, <Sequential>, <Parallel>, <Conditional>, <Map> |
Template authoring |
@alkdev/flowgraph/host |
GraphologyHostConfig, ReactiveHostConfig |
ujsx HostConfig implementations |
@alkdev/flowgraph/schema |
TypeBox schemas, enums, types | Schema-only import (no graph dependency) |
@alkdev/flowgraph/graph |
FlowGraph class, construction, mutation, queries |
Core graph operations |
@alkdev/flowgraph/reactive |
WorkflowReactiveRoot, signal-based execution |
Runtime execution |
@alkdev/flowgraph/analysis |
typeCompat, validateTemplate, ordering functions |
Analysis and validation |
@alkdev/flowgraph/error |
Error classes | Error handling |
Dependencies
Production Dependencies
{
"dependencies": {
"@alkdev/typebox": "workspace:*",
"@alkdev/ujsx": "workspace:*",
"@preact/signals-core": "^1.x",
"graphology": "^0.25",
"graphology-dag": "^0.4"
},
"peerDependencies": {
"@alkdev/operations": "workspace:*"
}
}
| Package | Role | Why |
|---|---|---|
@alkdev/typebox |
Schema definitions, validation, Value.Check, Value.Errors |
Direct dependency — all schemas are TypeBox |
@alkdev/ujsx |
UNode, HostConfig, createRoot, h(), ReactiveRoot | Direct dependency — workflow templates are ujsx trees |
@preact/signals-core |
signal, computed, effect, batch |
Transitive via ujsx, re-exported for flowgraph's reactive layer |
graphology |
DirectedGraph data structure |
Core graph engine — same as taskgraph |
graphology-dag |
topologicalSort, hasCycle, parallelGroups |
DAG-specific algorithms |
@alkdev/operations |
OperationSpec, CallEventMap, CallStatus |
Peer dependency — type imports only, no runtime dependency |
Why @alkdev/operations is a Peer Dependency
Flowgraph imports OperationSpec, CallEventMap, and CallStatus types from @alkdev/operations, but does not depend on the runtime (registry, call handler, pending request map). Making it a peer dependency:
- Avoids circular dependency concerns (operations doesn't depend on flowgraph)
- Allows flowgraph to work with any version of operations that provides the right types
- Reduces bundle size for consumers that don't use operations
Why @preact/signals-core via @alkdev/ujsx
Flowgraph's reactive layer uses signal(), computed(), and effect() from @preact/signals-core. These are re-exported from @alkdev/ujsx/reactive so consumers don't need to import directly from Preact. If ujsx ever changes its reactive primitive library, only ujsx's re-export needs updating.
Build Configuration
tsup.config.ts
Following taskgraph's build pattern:
import { defineConfig } from "tsup";
export default defineConfig({
entry: {
index: "src/index.ts",
component: "src/component/index.ts",
host: "src/host/index.ts",
schema: "src/schema/index.ts",
graph: "src/graph/index.ts",
reactive: "src/reactive/index.ts",
analysis: "src/analysis/index.ts",
error: "src/error/index.ts",
},
format: ["esm", "cjs"],
dts: true,
clean: true,
splitting: true,
sourcemap: true,
});
- ESM + CJS dual output — matches all sibling packages
- Code splitting — enables tree-shaking for sub-path imports
- Source maps — for debugging
- Type declarations —
.d.tsfiles for all exports
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"types": ["vitest/globals"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "test"]
}
Matches the tsconfig pattern of all @alkdev packages: ES2022 target, Node16 module resolution, strict mode.
vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
},
});
Platform Targets
Following taskgraph's philosophy: pure JavaScript, no native addons.
| Platform | Support Level | Notes |
|---|---|---|
| Node.js | Primary | All dependencies are pure JS |
| Deno | Compatible | ESM-first, no Node-specific APIs used |
| Bun | Compatible | All dependencies are Bun-compatible |
| Browser | Compatible | graphology and signals-core work in browsers |
The library has no native dependencies, no filesystem access, and no Node-specific APIs (no fs, path, child_process, etc.). This makes it platform-agnostic.
Tree-Shaking
The sub-path export structure enables effective tree-shaking:
- Consumers using only
@alkdev/flowgraph/schemadon't pull in the graph engine or ujsx - Consumers using only
@alkdev/flowgraph/analysisdon't pull in the reactive layer - Consumers using
@alkdev/flowgraph/componentget ujsx but not graphology (templates can be defined without importing the graph engine)
The barrel export (@alkdev/flowgraph) re-exports everything for convenience, but consumers concerned about bundle size should use sub-path imports.
Testing Strategy
Unit Test Categories
| Category | What to test | How |
|---|---|---|
| Schema validation | TypeBox schemas validate/correct shapes | Value.Check() / Value.Errors() |
| Graph construction | fromSpecs, fromCallEvents, fromJSON |
Build graphs, assert node/edge counts |
| Graph mutations | addNode, addEdge, updateStatus |
Assert success, assert throws on violations |
| Graph queries | topologicalOrder, ancestors, descendants |
Known graphs, expected results |
| Type compatibility | typeCompat for known schema pairs |
Compatible/incompatible/unknown |
| Template validation | validateTemplate against known graphs |
Known valid/invalid templates |
| Error hierarchy | CycleError, InvalidTransitionError, etc. |
Assert throw types, assert message format |
| Reactive execution | Signal propagation, preconditions, abort cascade | Set up mini reactive graph, assert state transitions |
Testing Reactive Graphs
Testing signal-based state propagation requires specific patterns:
-
Setup: Create a
WorkflowReactiveRootwith a known DAG. Assert initial state (all nodesidle). -
Transition: Set a node's status signal to a known value. Assert that dependents'
preconditionsandblockedByFailurecomputeds update correctly. -
Assertion: Check
node.status.value,node.preconditions.value,node.blockedByFailure.valueat each step.
// Example test pattern
const root = new WorkflowReactiveRoot(dag);
const nodeA = root.statusMap.get("A")!;
const nodeB = root.statusMap.get("B")!;
// Initially: both idle
expect(nodeA.value).toBe("idle");
expect(nodeB.preconditions.value).toBe(false); // A not completed yet
// Complete A → B's preconditions met
nodeA.value = "completed";
expect(nodeB.preconditions.value).toBe(true);
- Cleanup: Call
root.dispose()after each test to prevent signal leaks.
Testing Template Rendering
Template rendering tests follow the same pattern for both HostConfigs:
- Define a template
- Render to the target (graphology or reactive)
- Assert the output (graph structure or signal state)
// GraphologyHostConfig test
const host = new GraphologyHostConfig();
const root = createRoot(host, new DirectedGraph());
root.render(template);
const graph = root.ctx.graph;
expect(graph.nodes()).toEqual(["A", "B", "C"]);
expect(graph.edges()).toEqual(["A->B", "B->C"]);
// ReactiveHostConfig test
const reactiveHost = new ReactiveHostConfig(registry, workflowRoot);
const reactiveRoot = createRoot(reactiveHost, {});
reactiveRoot.render(template);
expect(workflowRoot.statusMap.size).toBe(3);
Testing Error Paths
All error paths should be tested:
- Cycle detection: adding a cycle-creating edge throws
CycleError - Duplicate node/edge: adding duplicates throws
ConstructionError - Invalid status transition:
updateStatus(completed → running)throwsInvalidTransitionError - Validation errors:
validateGraph()returns arrays, never throws
Constraints
- No filesystem access — flowgraph is a pure computation library. Persistence is the hub's concern.
- No network access — no HTTP clients, WebSocket connections, or Redis clients. All data comes in through constructor arguments.
@alkdev/operationsis a peer dependency — type imports only, no runtime dependency on the operations registry or call protocol.- ESM-first — the package is authored in ESM with CJS output generated by tsup. All internal imports use
.jsextensions for Node16 module resolution. - Code splitting enabled — tsup's
splitting: trueenables optimal code splitting for sub-path imports. - Vitest for testing — following the monorepo convention.
References
- Taskgraph build configuration:
@alkdev/taskgraph_ts/tsup.config.ts,@alkdev/taskgraph_ts/tsconfig.json - ujsx build configuration:
@alkdev/ujsx/tsup.config.ts - graphology: https://github.com/graphology/graphology
- graphology-dag: https://github.com/graphology/graphology-dag