- 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
365 lines
14 KiB
Markdown
365 lines
14 KiB
Markdown
---
|
|
status: draft
|
|
last_updated: 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
|
|
|
|
```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
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
1. Avoids circular dependency concerns (operations doesn't depend on flowgraph)
|
|
2. Allows flowgraph to work with any version of operations that provides the right types
|
|
3. 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:
|
|
|
|
```typescript
|
|
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.ts` files for all exports
|
|
|
|
### tsconfig.json
|
|
|
|
```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
|
|
|
|
```typescript
|
|
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/schema` don't pull in the graph engine or ujsx
|
|
- Consumers using only `@alkdev/flowgraph/analysis` don't pull in the reactive layer
|
|
- Consumers using `@alkdev/flowgraph/component` get 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:
|
|
|
|
1. **Setup**: Create a `WorkflowReactiveRoot` with a known DAG. Assert initial state (all nodes `idle`).
|
|
|
|
2. **Transition**: Set a node's status signal to a known value. Assert that dependents' `preconditions` and `blockedByFailure` computeds update correctly.
|
|
|
|
3. **Assertion**: Check `node.status.value`, `node.preconditions.value`, `node.blockedByFailure.value` at each step.
|
|
|
|
```typescript
|
|
// 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);
|
|
```
|
|
|
|
4. **Cleanup**: Call `root.dispose()` after each test to prevent signal leaks.
|
|
|
|
### Testing Template Rendering
|
|
|
|
Template rendering tests follow the same pattern for both HostConfigs:
|
|
|
|
1. Define a template
|
|
2. Render to the target (graphology or reactive)
|
|
3. Assert the output (graph structure or signal state)
|
|
|
|
```typescript
|
|
// 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)` throws `InvalidTransitionError`
|
|
- 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/operations` is 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 `.js` extensions for Node16 module resolution.
|
|
- **Code splitting enabled** — tsup's `splitting: true` enables 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 |