Files
flowgraph/docs/architecture/build-distribution.md
glm-5.1 907c33650f fix: architecture review - address 5 critical issues, 6 warnings, 3 suggestions
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
2026-05-21 19:40:45 +00:00

19 KiB

status, last_updated
status last_updated
reviewed 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:

{
  "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:

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

{
  "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):

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

{
  "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:

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.

// 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);
  1. 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)
// 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