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
496 lines
19 KiB
Markdown
496 lines
19 KiB
Markdown
---
|
|
status: reviewed
|
|
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
|
|
|
|
Following the same pattern as `@alkdev/ujsx` and `@alkdev/operations` — conditional exports with explicit `types` and `default` for both import and require:
|
|
|
|
```json
|
|
{
|
|
"name": "@alkdev/flowgraph",
|
|
"version": "0.1.0",
|
|
"description": "Workflow graph library — DAG-based operation orchestration over graphology, with ujsx template composition and reactive execution",
|
|
"type": "module",
|
|
"main": "./dist/index.cjs",
|
|
"module": "./dist/index.js",
|
|
"types": "./dist/index.d.ts",
|
|
"exports": {
|
|
".": {
|
|
"import": {
|
|
"types": "./dist/index.d.ts",
|
|
"default": "./dist/index.js"
|
|
},
|
|
"require": {
|
|
"types": "./dist/index.d.cts",
|
|
"default": "./dist/index.cjs"
|
|
}
|
|
},
|
|
"/component": {
|
|
"import": {
|
|
"types": "./dist/component/index.d.ts",
|
|
"default": "./dist/component/index.js"
|
|
},
|
|
"require": {
|
|
"types": "./dist/component/index.d.cts",
|
|
"default": "./dist/component/index.cjs"
|
|
}
|
|
},
|
|
"/host": {
|
|
"import": {
|
|
"types": "./dist/host/index.d.ts",
|
|
"default": "./dist/host/index.js"
|
|
},
|
|
"require": {
|
|
"types": "./dist/host/index.d.cts",
|
|
"default": "./dist/host/index.cjs"
|
|
}
|
|
},
|
|
"/schema": {
|
|
"import": {
|
|
"types": "./dist/schema/index.d.ts",
|
|
"default": "./dist/schema/index.js"
|
|
},
|
|
"require": {
|
|
"types": "./dist/schema/index.d.cts",
|
|
"default": "./dist/schema/index.cjs"
|
|
}
|
|
},
|
|
"/graph": {
|
|
"import": {
|
|
"types": "./dist/graph/index.d.ts",
|
|
"default": "./dist/graph/index.js"
|
|
},
|
|
"require": {
|
|
"types": "./dist/graph/index.d.cts",
|
|
"default": "./dist/graph/index.cjs"
|
|
}
|
|
},
|
|
"/reactive": {
|
|
"import": {
|
|
"types": "./dist/reactive/index.d.ts",
|
|
"default": "./dist/reactive/index.js"
|
|
},
|
|
"require": {
|
|
"types": "./dist/reactive/index.d.cts",
|
|
"default": "./dist/reactive/index.cjs"
|
|
}
|
|
},
|
|
"/analysis": {
|
|
"import": {
|
|
"types": "./dist/analysis/index.d.ts",
|
|
"default": "./dist/analysis/index.js"
|
|
},
|
|
"require": {
|
|
"types": "./dist/analysis/index.d.cts",
|
|
"default": "./dist/analysis/index.cjs"
|
|
}
|
|
},
|
|
"/error": {
|
|
"import": {
|
|
"types": "./dist/error/index.d.ts",
|
|
"default": "./dist/error/index.js"
|
|
},
|
|
"require": {
|
|
"types": "./dist/error/index.d.cts",
|
|
"default": "./dist/error/index.cjs"
|
|
}
|
|
}
|
|
},
|
|
"publishConfig": {
|
|
"access": "public"
|
|
},
|
|
"files": [
|
|
"dist"
|
|
],
|
|
"scripts": {
|
|
"build": "tsup",
|
|
"build:tsc": "tsc",
|
|
"test": "vitest run",
|
|
"test:watch": "vitest",
|
|
"test:coverage": "vitest run --coverage",
|
|
"lint": "tsc --noEmit",
|
|
"prepublishOnly": "npm run build"
|
|
},
|
|
"keywords": [
|
|
"flowgraph",
|
|
"dag",
|
|
"workflow",
|
|
"graphology",
|
|
"ujsx",
|
|
"operations"
|
|
],
|
|
"license": "MIT OR Apache-2.0",
|
|
"dependencies": {
|
|
"@alkdev/typebox": "^0.34.49",
|
|
"@alkdev/ujsx": "^0.1.0",
|
|
"@preact/signals-core": "^1.14.1",
|
|
"graphology": "^0.26.0",
|
|
"graphology-dag": "^0.4.1"
|
|
},
|
|
"peerDependencies": {
|
|
"@alkdev/operations": "^0.1.0"
|
|
},
|
|
"devDependencies": {
|
|
"@types/node": "^22.0.0",
|
|
"@vitest/coverage-v8": "^3.2.4",
|
|
"tsup": "^8.5.1",
|
|
"typescript": "^5.7.0",
|
|
"vitest": "^3.1.0"
|
|
},
|
|
"engines": {
|
|
"node": ">=18.0.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Exports Map: Sub-path Imports
|
|
|
|
The exports map uses the `"/subpath"` format (with leading slash) to match the pattern used by `@alkdev/ujsx`. This is the Node.js subpath exports convention — consumers import as:
|
|
|
|
```typescript
|
|
import { FlowGraph } from "@alkdev/flowgraph/graph";
|
|
import { typeCompat } from "@alkdev/flowgraph/analysis";
|
|
import { h } from "@alkdev/ujsx";
|
|
import { createRoot } from "@alkdev/ujsx/reactive";
|
|
```
|
|
|
|
Each sub-path has conditional exports for both `import` (ESM) and `require` (CJS), with separate `types` files (`.d.ts` and `.d.cts`). The `typesVersions` field is NOT used — conditional exports with `types` entries replace it for modern Node.js resolution.
|
|
|
|
## 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 (including `CallResultSchema`, `CallResult`) | Schema-only import (no graph dependency) |
|
|
| `@alkdev/flowgraph/graph` | `FlowGraph` class, construction, mutation, queries | Core graph operations |
|
|
| `@alkdev/flowgraph/reactive` | `WorkflowReactiveRoot`, `EventLogProjection`, 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": "^0.34.49",
|
|
"@alkdev/ujsx": "^0.1.0",
|
|
"@preact/signals-core": "^1.14.1",
|
|
"graphology": "^0.26.0",
|
|
"graphology-dag": "^0.4.1"
|
|
},
|
|
"peerDependencies": {
|
|
"@alkdev/operations": "^0.1.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
| 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` | Direct dependency — flowgraph's reactive layer uses signals directly for WorkflowReactiveRoot |
|
|
| `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` is a Direct Dependency
|
|
|
|
While `@preact/signals-core` is also a dependency of `@alkdev/ujsx`, flowgraph lists it as a direct dependency because `WorkflowReactiveRoot` creates `signal()` and `computed()` instances directly. This makes the dependency explicit and ensures version alignment. Consumers import signals from flowgraph's reactive exports, not directly from Preact — the `@alkdev/ujsx/reactive` module re-exports the same signal primitives for consumers who need them, but flowgraph's own reactive module depends on `@preact/signals-core` directly for its internal signal graph construction.
|
|
|
|
### No `workspace:*` — Published npm Packages
|
|
|
|
All `@alkdev` packages are independently published to npm. The dependency versions use semver ranges (`^0.34.49`, `^0.1.0`) matching the pattern used by all sibling packages. During local development, the packages exist as local repos on disk, but they are not linked via a monorepo workspace protocol. Each package is built and published independently.
|
|
|
|
## Build Configuration
|
|
|
|
### tsup.config.ts
|
|
|
|
Following the same pattern as `@alkdev/taskgraph` and `@alkdev/ujsx` — named entry points matching source file paths, no `splitting` flag (ujsx doesn't use it either):
|
|
|
|
```typescript
|
|
import { defineConfig } from 'tsup';
|
|
|
|
export default defineConfig({
|
|
entry: {
|
|
index: 'src/index.ts',
|
|
'component/index': 'src/component/index.ts',
|
|
'host/index': 'src/host/index.ts',
|
|
'schema/index': 'src/schema/index.ts',
|
|
'graph/index': 'src/graph/index.ts',
|
|
'reactive/index': 'src/reactive/index.ts',
|
|
'analysis/index': 'src/analysis/index.ts',
|
|
'error/index': 'src/error/index.ts',
|
|
},
|
|
format: ['esm', 'cjs'],
|
|
dts: true,
|
|
sourcemap: true,
|
|
clean: true,
|
|
target: 'es2022',
|
|
});
|
|
```
|
|
|
|
- **ESM + CJS dual output** — matches all sibling packages
|
|
- **Source maps** — for debugging
|
|
- **Type declarations** — `.d.ts` files for all exports
|
|
- **`target: 'es2022'`** — matches tsconfig target, ensures consistent output
|
|
- **Named entry points with path prefixes** — matches ujsx pattern (e.g., `'core/schema': 'src/core/schema.ts'`), producing output like `dist/component/index.js` consistent with the exports map
|
|
|
|
Note: The `splitting: true` option was in an earlier draft but has been removed. The sibling projects (taskgraph, ujsx, operations) don't use splitting, and the named entry points with sub-paths already provide natural code boundaries for tree-shaking.
|
|
|
|
### tsconfig.json
|
|
|
|
```json
|
|
{
|
|
"compilerOptions": {
|
|
"target": "ES2022",
|
|
"lib": ["ES2022"],
|
|
"module": "Node16",
|
|
"moduleResolution": "Node16",
|
|
"outDir": "./dist",
|
|
"rootDir": "./src",
|
|
"strict": true,
|
|
"declaration": true,
|
|
"declarationMap": true,
|
|
"sourceMap": true,
|
|
"noUncheckedIndexedAccess": true,
|
|
"noUnusedLocals": true,
|
|
"noUnusedParameters": true,
|
|
"noFallthroughCasesInSwitch": true,
|
|
"esModuleInterop": true,
|
|
"skipLibCheck": true,
|
|
"forceConsistentCasingInFileNames": true,
|
|
"resolveJsonModule": true,
|
|
"isolatedModules": true,
|
|
"erasableSyntaxOnly": true
|
|
},
|
|
"include": ["src/**/*.ts"],
|
|
"exclude": ["node_modules", "dist", "test"]
|
|
}
|
|
```
|
|
|
|
Matches the tsconfig patterns of all `@alkdev` packages: ES2022 target, Node16 module resolution, strict mode with additional safety checks (`noUncheckedIndexedAccess`, `noUnusedLocals`, `noUnusedParameters`, `noFallthroughCasesInSwitch`, `erasableSyntaxOnly`).
|
|
|
|
### vitest.config.ts
|
|
|
|
Following the same pattern as `@alkdev/taskgraph` and `@alkdev/ujsx`:
|
|
|
|
```typescript
|
|
import { defineConfig } from 'vitest/config';
|
|
import path from 'node:path';
|
|
|
|
export default defineConfig({
|
|
resolve: {
|
|
alias: {
|
|
'@': path.resolve(__dirname, 'src'),
|
|
},
|
|
},
|
|
test: {
|
|
globals: true,
|
|
include: ['test/**/*.test.ts'],
|
|
coverage: {
|
|
provider: 'v8',
|
|
include: ['src/**/*.ts'],
|
|
exclude: ['src/**/index.ts'],
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
## 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.
|
|
- **Sub-path exports provide natural code boundaries** — named entry points in tsup match source file paths, enabling effective tree-shaking without `splitting: true`.
|
|
- **Vitest for testing** — following the convention of all `@alkdev` packages.
|
|
|
|
## 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 |