fix build/distribution spec: npm deps not workspace, align configs with sibling projects, resolve review issues
- Replace workspace:* deps with published npm semver ranges (^0.34.49, ^0.1.0) - Expand package.json: add description, publishConfig, scripts, engines, devDependencies, conditional exports with types/default for import+require - Fix tsup entry names (path-prefixed like ujsx), add target: es2022, remove splitting:true (not used by sibling projects) - Align tsconfig with sibling projects: add lib, noUncheckedIndexedAccess, noUnusedLocals, noUnusedParameters, erasableSyntaxOnly, etc. - Expand vitest.config.ts with include, coverage, and path alias - Clarify @preact/signals-core as direct dep (not just transitive via ujsx) - Clarify @alkdev/pubsub is a consumer dependency, not flowgraph's dep - Fix edge key convention: document composite key format for call graph's multi-edge-type scenario (triggered + depends_on between same pair) - Align OperationEdgeAttrs field naming: use detail+mismatches consistently instead of compatibilityDetail - Add InvalidInputError to error hierarchy (referenced in flowgraph-api but was missing) - Fix undefined attrs.category reference in reactive-execution.md - Remove internal drafting note from host-configs.md - Fix ReactiveHostConfig constructor signature inconsistency across docs - Constrain TemplateEdgeAttrs.edgeType to sequential|conditional only
This commit is contained in:
@@ -40,7 +40,7 @@ The operation graph provides static type checking and structural validation. The
|
||||
| `@alkdev/ujsx` | **Direct dependency**. Workflow templates are `UNode` trees rendered through `HostConfig`s. Flowgraph provides the workflow-specific host configurations (graphology DAG, reactive execution). |
|
||||
| `@alkdev/taskgraph` | **Pattern reference**. Flowgraph follows the same graphology-wrapping pattern (`FlowGraph` class like `TaskGraph` class) but enforces DAG invariants instead of allowing cycles. |
|
||||
| `@alkdev/typebox` | **Direct dependency**. All schemas are TypeBox Modules. Runtime validation, JSON Schema export, and `Value.Check`/`Value.Errors`. |
|
||||
| `@alkdev/pubsub` | **Optional peer dependency**. For event-driven call graph population. Flowgraph works in-memory; pubsub connects it to the call protocol. |
|
||||
| `@alkdev/pubsub` | **Consumer dependency, not flowgraph's dep**. For event-driven call graph population. The hub coordinator uses pubsub to subscribe to call events and feed them to flowgraph — flowgraph itself works in-memory with no pubsub import. |
|
||||
| `@alkdev/cograph` | **Future consumer**. The cognitive graph depends on flowgraph for workflow templates and execution tracking. |
|
||||
|
||||
## Current State
|
||||
|
||||
@@ -74,59 +74,159 @@ Package structure, exports map, dependencies, and platform targets.
|
||||
|
||||
## 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": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"./component": {
|
||||
"import": "./dist/component/index.js",
|
||||
"require": "./dist/component/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": "./dist/host/index.js",
|
||||
"require": "./dist/host/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": "./dist/schema/index.js",
|
||||
"require": "./dist/schema/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": "./dist/graph/index.js",
|
||||
"require": "./dist/graph/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": "./dist/reactive/index.js",
|
||||
"require": "./dist/reactive/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": "./dist/analysis/index.js",
|
||||
"require": "./dist/analysis/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": "./dist/error/index.js",
|
||||
"require": "./dist/error/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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"]
|
||||
}
|
||||
"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:
|
||||
@@ -149,14 +249,14 @@ Following the taskgraph pattern, each module has a sub-path export:
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@alkdev/typebox": "workspace:*",
|
||||
"@alkdev/ujsx": "workspace:*",
|
||||
"@preact/signals-core": "^1.x",
|
||||
"graphology": "^0.25",
|
||||
"graphology-dag": "^0.4"
|
||||
"@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": "workspace:*"
|
||||
"@alkdev/operations": "^0.1.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -165,7 +265,7 @@ Following the taskgraph pattern, each module has a sub-path export:
|
||||
|---------|------|-----|
|
||||
| `@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 |
|
||||
| `@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 |
|
||||
@@ -178,42 +278,49 @@ Flowgraph imports `OperationSpec`, `CallEventMap`, and `CallStatus` types from `
|
||||
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`
|
||||
### Why `@preact/signals-core` is a Direct Dependency
|
||||
|
||||
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.
|
||||
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 taskgraph's build pattern:
|
||||
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";
|
||||
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",
|
||||
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"],
|
||||
format: ['esm', 'cjs'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
splitting: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
target: 'es2022',
|
||||
});
|
||||
```
|
||||
|
||||
- **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
|
||||
- **`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
|
||||
|
||||
@@ -221,31 +328,55 @@ export default defineConfig({
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"types": ["vitest/globals"]
|
||||
"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 pattern of all `@alkdev` packages: ES2022 target, Node16 module resolution, strict mode.
|
||||
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 { 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'],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -354,8 +485,8 @@ All error paths should be tested:
|
||||
- **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.
|
||||
- **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
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ Call graph edges carry an `edgeType` attribute:
|
||||
|
||||
`triggered` edges use `${parentRequestId}->${childRequestId}` as the edge key. `depends_on` edges use `${sourceRequestId}->${targetRequestId}:depends_on` to distinguish from `triggered` edges between the same pair.
|
||||
|
||||
Since `multi: false`, there can be at most one `triggered` and one `depends_on` edge between the same pair. The edge key convention ensures deterministic keys.
|
||||
This composite key format is necessary because `multi: false` allows at most one edge per key between a given (source, target) pair. Since a call graph can have both a `triggered` edge (parent→child) and a `depends_on` edge (data dependency) between the same pair of calls, the edge type suffix in the key disambiguates them. See [schema.md#edge-key-convention](schema.md) for the general key convention and the discussion of multi-edge support.
|
||||
|
||||
## Status Lifecycle
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ FlowgraphError # Base class for all flowgraph errors
|
||||
│ ├── DuplicateNodeError # Duplicate node key
|
||||
│ ├── DuplicateEdgeError # Duplicate edge key
|
||||
│ ├── NodeNotFoundError # Referenced node doesn't exist
|
||||
│ └── CycleError # Adding an edge would create a cycle
|
||||
│ ├── CycleError # Adding an edge would create a cycle
|
||||
│ └── InvalidInputError # Invalid input to fromJSON() or constructor
|
||||
├── ValidationError # Schema validation failed (single field)
|
||||
├── GraphValidationError # Graph-level validation issue
|
||||
│ ├── CycleValidationError # Cycle detected in the graph
|
||||
@@ -117,6 +118,19 @@ Thrown when adding an edge would create a cycle. The `cycles` field contains the
|
||||
|
||||
Note: `CycleError` is flowgraph's cycle error, thrown by `addEdge()` during construction. Taskgraph uses a different error name (`CircularDependencyError`, thrown by `topologicalOrder()`). The two are distinct errors for distinct contexts — flowgraph prevents cycles at construction time, taskgraph allows cycles and detects them later.
|
||||
|
||||
### InvalidInputError
|
||||
|
||||
```typescript
|
||||
class InvalidInputError extends ConstructionError {
|
||||
constructor(public readonly errors: ValidationError[]) {
|
||||
super(`Invalid input: ${errors.length} validation error(s)`);
|
||||
this.name = "InvalidInputError";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Thrown by `fromJSON()` when the input data fails schema validation. The `errors` field contains the array of `ValidationError` objects describing which fields failed validation. This is distinct from `ValidationError` (which is a returned result) — `InvalidInputError` is thrown because `fromJSON()` enforces that deserialized data is structurally valid.
|
||||
|
||||
### ValidationError
|
||||
|
||||
```typescript
|
||||
@@ -231,6 +245,7 @@ The distinction between thrown errors and returned errors:
|
||||
| `addNode(key, attrs)` | Throws `DuplicateNodeError` on duplicate key | Adding a duplicate is a programmer error |
|
||||
| `addEdge(source, target)` | Throws `NodeNotFoundError` on missing endpoint | Edge without endpoints is invalid |
|
||||
| `addEdge(source, target)` | Throws `CycleError` if edge creates cycle | DAG invariant must be maintained |
|
||||
| `fromJSON(data)` | Throws `InvalidInputError` on validation failure | Deserialized data must be structurally valid |
|
||||
| `updateNodeStatus(id, status)` | Throws `InvalidTransitionError` on invalid transition | State machine must be enforced |
|
||||
| `validateSchema()` | Returns `ValidationError[]` | Schema issues are validations, not crashes |
|
||||
| `validateGraph()` | Returns `GraphValidationError[]` | Graph issues are validations, not crashes |
|
||||
|
||||
@@ -48,17 +48,13 @@ When ujsx's reconciler calls `HostConfig.createInstance(tag, props, ...)`, the `
|
||||
### Type Parameters
|
||||
|
||||
```typescript
|
||||
const graphologyHost: HostConfig<WorkflowTag, Graph, GraphContext>
|
||||
const graphologyHost: HostConfig<WorkflowTag, GraphNode, GraphContext>
|
||||
```
|
||||
|
||||
- **TTag**: `WorkflowTag`
|
||||
- **Instance**: `Graph` (the graphology `DirectedGraph` instance — every element creates a subgraph reference)
|
||||
- **Instance**: `GraphNode` (a logical representation of what each template node becomes in the graph)
|
||||
- **RootCtx**: `GraphContext` (the root context carrying the graph and metadata)
|
||||
|
||||
Wait — this needs refinement. In graphology, instances aren't subgraphs. Let me reconsider.
|
||||
|
||||
Actually, the GraphologyHostConfig's `Instance` type is a logical representation of what each template node becomes:
|
||||
|
||||
```typescript
|
||||
interface GraphNode {
|
||||
key: string; // The graphology node key
|
||||
|
||||
@@ -54,7 +54,7 @@ Round-trip: `fromSpecs()` → `export()` → `fromJSON()` is lossless.
|
||||
```typescript
|
||||
const graph = new FlowGraph();
|
||||
graph.addOperation(spec);
|
||||
graph.addTypedEdge("task.classify", "task.enrich", { compatible: true, detail: "output → input" });
|
||||
graph.addTypedEdge("task.classify", "task.enrich", { compatible: true, detail: "output → input", mismatches: [] });
|
||||
```
|
||||
|
||||
`addOperation` adds a node. `addTypedEdge` adds a type-compatibility edge. Both throw on duplicates (matching taskgraph's behavior).
|
||||
|
||||
@@ -47,7 +47,9 @@ class WorkflowReactiveRoot {
|
||||
private initializeSignals(): void {
|
||||
for (const node of this.graph.nodes()) {
|
||||
const attrs = this.graph.getNodeAttributes(node);
|
||||
if (attrs.category !== "operation") continue; // Skip structural nodes (already flattened)
|
||||
// In the flattened DAG from GraphologyHostConfig, all nodes represent
|
||||
// operations (structural containers like Sequential/Parallel are transparent
|
||||
// and create no nodes). No filtering needed — every node gets a signal.
|
||||
|
||||
const status = signal<NodeStatus>("idle");
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ Flowgraph's `fromCallEvents()` and `updateFromEvent()` accept this type directly
|
||||
|
||||
### EdgeType
|
||||
|
||||
The type of edge in a flowgraph. Matches the call graph storage schema's `edgeType` column:
|
||||
The type of edge in a flowgraph. Matches the call graph storage schema's `edgeType` column. This is a universal enum that covers all graph modes (operation, call, template), but each graph mode uses only a subset:
|
||||
|
||||
```typescript
|
||||
const EdgeTypeEnum = Type.Union([
|
||||
@@ -161,15 +161,19 @@ const EdgeTypeEnum = Type.Union([
|
||||
type EdgeType = Static<typeof EdgeTypeEnum>;
|
||||
```
|
||||
|
||||
The first three (`triggered`, `depends_on`) match the call graph storage schema. The last two (`sequential`, `conditional`) are template-specific and only exist in workflow template DAGs.
|
||||
|
||||
| Edge Type | Graph Type | Meaning |
|
||||
| Edge Type | Graph Mode | Meaning |
|
||||
|-----------|------------|---------|
|
||||
| `triggered` | Call graph | Parent call triggered child call. Corresponds to `parentRequestId`. |
|
||||
| `depends_on` | Call graph | Data dependency — source needs target's result. |
|
||||
| `typed` | Operation graph | Type compatibility — source's output schema is compatible with target's input schema. |
|
||||
| `sequential` | Template → DAG | Sequential ordering from `<Sequential>` component. |
|
||||
| `conditional` | Template → DAG | Conditional branch from `<Conditional>` component. |
|
||||
| `sequential` | Template DAG | Sequential ordering from `<Sequential>` component. |
|
||||
| `conditional` | Template DAG | Conditional branch from `<Conditional>` component. |
|
||||
|
||||
`EdgeTypeEnum` is the universal enumeration. Each graph mode constrains its edge types through its specific edge attribute schemas:
|
||||
|
||||
- **Operation graphs** only use `typed` edges (`OperationEdgeAttrs`)
|
||||
- **Call graphs** use `triggered` and `depends_on` edges (`CallEdgeAttrs`)
|
||||
- **Template DAGs** use `sequential` and `conditional` edges (`TemplateEdgeAttrs`)
|
||||
|
||||
## Node Attribute Schemas
|
||||
|
||||
@@ -236,21 +240,27 @@ The node key is `requestId`. This matches the call protocol's correlation mechan
|
||||
```typescript
|
||||
const OperationEdgeAttrs = Type.Object({
|
||||
compatible: Type.Boolean({ description: "Whether the source output schema is compatible with the target input schema" }),
|
||||
compatibilityDetail: Type.Optional(Type.String({ description: "Human-readable description of compatibility or mismatch" })),
|
||||
detail: Type.Optional(Type.String({ description: "Human-readable description of compatibility or mismatch" })),
|
||||
mismatches: Type.Optional(Type.Array(Type.Object({ // Structured mismatch details (populated when compatible: false)
|
||||
path: Type.String(),
|
||||
expected: Type.String(),
|
||||
actual: Type.String(),
|
||||
}))),
|
||||
});
|
||||
type OperationEdgeAttrs = Static<typeof OperationEdgeAttrs>;
|
||||
```
|
||||
|
||||
Type-compatibility edges carry a boolean `compatible` flag and optional detail. This allows the operation graph to include both compatible edges (green paths) and incompatible edges (red paths) for diagnostics.
|
||||
Type-compatibility edges carry a boolean `compatible` flag, an optional `detail` string, and optional structured `mismatches`. This allows the operation graph to include both compatible edges (green paths) and incompatible edges (red paths) for diagnostics. The `detail` field provides a human-readable summary, while `mismatches` provides machine-readable field-level diagnostics. The `TypeCompatResult` from `typeCompat()` populates both fields: `detail` for compatible edges and `mismatches` for incompatible ones.
|
||||
|
||||
**Edge type storage**: Operation graph edges always have `edgeType: "typed"` stored on the edge as a separate attribute alongside `OperationEdgeAttrs`. Graphology edges carry both the `OperationEdgeAttrs` (compatible, compatibilityDetail) and the `edgeType` field. The `edgeType` is not inside `OperationEdgeAttrs` because it's a universal edge classification that applies to all edge types across all graph modes (operation, call, template). The `OperationEdgeAttrs` schema only defines the mode-specific attributes.
|
||||
**Edge type storage**: Operation graph edges always have `edgeType: "typed"` stored on the edge as a separate attribute alongside `OperationEdgeAttrs`. Graphology edges carry both the `OperationEdgeAttrs` (compatible, detail, mismatches) and the `edgeType` field. The `edgeType` is not inside `OperationEdgeAttrs` because it's a universal edge classification that applies to all edge types across all graph modes (operation, call, template). The `OperationEdgeAttrs` schema only defines the mode-specific attributes.
|
||||
|
||||
```typescript
|
||||
// How operation graph edges are stored in graphology:
|
||||
{
|
||||
edgeType: "typed", // Universal classification (stored alongside attrs)
|
||||
compatible: true, // OperationEdgeAttrs field
|
||||
compatibilityDetail: "..." // OperationEdgeAttrs field
|
||||
detail: "classify.output → enrich.input", // OperationEdgeAttrs field
|
||||
mismatches: [] // Empty when compatible
|
||||
}
|
||||
```
|
||||
|
||||
@@ -286,7 +296,7 @@ A union type used as the edge attribute type parameter for call graphs (`FlowGra
|
||||
|
||||
```typescript
|
||||
const TemplateEdgeAttrs = Type.Object({
|
||||
edgeType: EdgeTypeEnum, // "sequential" or "conditional"
|
||||
edgeType: Type.Union([Type.Literal("sequential"), Type.Literal("conditional")]),
|
||||
condition: Type.Optional(Type.Unknown()), // For conditional edges: the condition function or expression
|
||||
});
|
||||
type TemplateEdgeAttrs = Static<typeof TemplateEdgeAttrs>;
|
||||
@@ -294,6 +304,8 @@ type TemplateEdgeAttrs = Static<typeof TemplateEdgeAttrs>;
|
||||
|
||||
Template edges carry an `edgeType` to distinguish sequential flow from conditional branching. Conditional edges optionally store a `condition` that determines whether the target node executes.
|
||||
|
||||
**Note**: `TemplateEdgeAttrs.edgeType` uses a constrained union of `"sequential" | "conditional"` rather than the full `EdgeTypeEnum`. Template DAGs never have `triggered`, `depends_on`, or `typed` edges — those belong to call graphs and operation graphs respectively.
|
||||
|
||||
### TemplateNodeAttrs (Workflow Templates)
|
||||
|
||||
Template DAGs use `OperationNodeAttrs` for their operation nodes — the template doesn't need a separate node type because every node in a template DAG corresponds to an operation invocation. The template's structural information (`Sequential`, `Parallel`, `Conditional`, `Map`) is expressed through edges, not through special node types.
|
||||
@@ -372,14 +384,17 @@ ${source}->${target}
|
||||
|
||||
For the operation graph, this means keys like `"task.classify->task.enrich"`. For the call graph, keys like `"req_abc123->req_def456"`.
|
||||
|
||||
Since `multi: false`, there can be at most one edge between any (source, target) pair. When multiple edge types are needed between the same pair (e.g., both `triggered` and `depends_on` between two calls), the graph stores a single edge whose `edgeType` attribute captures the semantic relationship. This is a simplification from the storage schema, which allows multiple edges per (source, target, edgeType) triple — the in-memory graph collapses these into a single edge per (source, target) pair.
|
||||
When multiple edge types exist between the same (source, target) pair (e.g., in the call graph where both `triggered` and `depends_on` edges can connect the same calls), a composite key format is used:
|
||||
|
||||
This is acceptable because:
|
||||
- Operation graphs only have `typed` edges, so no multi-edge concern.
|
||||
- Call graphs rarely have both `triggered` and `depends_on` between the same pair.
|
||||
- Template DAGs only have `sequential` or `conditional` edges.
|
||||
```
|
||||
${source}->${target}:${edgeType}
|
||||
```
|
||||
|
||||
If multi-edge support becomes necessary, the `allowSelfLoops: false` constraint can be relaxed and a composite key format (`${source}->${target}:${edgeType}`) adopted.
|
||||
For example, a `depends_on` edge in the call graph uses `"req_abc123->req_def456:depends_on"` while the `triggered` edge between the same pair uses `"req_abc123->req_def456"`.
|
||||
|
||||
Since `multi: false`, there can be at most one edge per key. The composite key format ensures deterministic keys even when multiple edge types connect the same pair.
|
||||
|
||||
This is an exception to the simple `${source}->${target}` pattern, but it's necessary for the call graph's dual-edge-type scenario. If multi-edge support becomes more broadly needed, the constraint can be relaxed and a uniform composite key format adopted.
|
||||
|
||||
## Constraints
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ The `ReactiveHostConfig` renders a template to a reactive execution engine:
|
||||
import { createRoot } from "@alkdev/ujsx";
|
||||
import { ReactiveHostConfig } from "@alkdev/flowgraph/host/reactive";
|
||||
|
||||
const host = new ReactiveHostConfig(operationRegistry);
|
||||
const host = new ReactiveHostConfig(operationRegistry, workflowRoot);
|
||||
const root = createRoot(host, {});
|
||||
|
||||
const template = h(Sequential, {},
|
||||
|
||||
Reference in New Issue
Block a user