Draft architecture specification for @alkdev/flowgraph — a workflow graph library providing DAG-based orchestration over operations. Covers two graph types (operation graph, call graph), ujsx workflow templates, GraphologyHost and ReactiveHost configs, signal-driven execution, type-compatibility analysis, error hierarchy, and build/distribution. Includes 3 ADRs: ujsx as template IR, DAG-only enforcement, decoupled storage.
11 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-05-19 |
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
│ │ └── index.ts
│ ├── host/
│ │ ├── graphology.ts # GraphologyHostConfig
│ │ ├── reactive.ts # ReactiveHostConfig
│ │ └── index.ts
│ ├── schema/
│ │ ├── enums.ts # CallStatus, EdgeType, NodeCategory, NodeStatus
│ │ ├── node.ts # OperationNodeAttrs, CallNodeAttrs
│ │ ├── edge.ts # TypedEdgeAttrs, 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
│ │ └── 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> |
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.
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