--- 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 # component │ │ ├── sequential.ts # component │ │ ├── parallel.ts # component │ │ ├── conditional.ts # component │ │ ├── map.ts # 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, 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` | ``, ``, ``, ``, `` | 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