feat(graph): implement fromSpecs, addOperation, addTypedEdge, buildTypeEdges
This commit is contained in:
@@ -4,4 +4,5 @@ export {
|
|||||||
defaultEdgeType,
|
defaultEdgeType,
|
||||||
resolveDefaultNodeAttrs,
|
resolveDefaultNodeAttrs,
|
||||||
} from "./defaults.js";
|
} from "./defaults.js";
|
||||||
export { typeCompat, type TypeCompatResult, type TypeMismatch } from "./type-compat.js";
|
export { typeCompat, type TypeCompatResult, type TypeMismatch } from "./type-compat.js";
|
||||||
|
export { buildTypeEdges } from "../graph/construction.js";
|
||||||
@@ -7,6 +7,12 @@ import {
|
|||||||
NodeNotFoundError,
|
NodeNotFoundError,
|
||||||
CycleError,
|
CycleError,
|
||||||
} from "../error/index.js";
|
} from "../error/index.js";
|
||||||
|
import {
|
||||||
|
OperationNodeAttrs as OperationNodeAttrsSchema,
|
||||||
|
OperationEdgeAttrs as OperationEdgeAttrsSchema,
|
||||||
|
} from "../schema/index.js";
|
||||||
|
import type { OperationNodeAttrs } from "../schema/index.js";
|
||||||
|
import { typeCompat, type TypeCompatResult } from "../analysis/type-compat.js";
|
||||||
|
|
||||||
export interface FlowGraphOptions {
|
export interface FlowGraphOptions {
|
||||||
type?: "directed";
|
type?: "directed";
|
||||||
@@ -14,6 +20,26 @@ export interface FlowGraphOptions {
|
|||||||
allowSelfLoops?: false;
|
allowSelfLoops?: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OperationSpec {
|
||||||
|
name: string;
|
||||||
|
namespace: string;
|
||||||
|
version: string;
|
||||||
|
type: "query" | "mutation" | "subscription";
|
||||||
|
inputSchema: TSchema;
|
||||||
|
outputSchema: TSchema;
|
||||||
|
description?: string;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type OperationGraph = FlowGraph<typeof OperationNodeAttrsSchema, typeof OperationEdgeAttrsSchema>;
|
||||||
|
|
||||||
|
type TypedEdgeAttrs = {
|
||||||
|
edgeType: "typed";
|
||||||
|
compatible: boolean;
|
||||||
|
detail?: string;
|
||||||
|
mismatches?: TypeCompatResult["mismatches"];
|
||||||
|
};
|
||||||
|
|
||||||
type Attrs = Record<string, unknown>;
|
type Attrs = Record<string, unknown>;
|
||||||
|
|
||||||
export class FlowGraph<
|
export class FlowGraph<
|
||||||
@@ -154,10 +180,47 @@ export class FlowGraph<
|
|||||||
return this._graph.outNeighbors(nodeId) ?? [];
|
return this._graph.outNeighbors(nodeId) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromSpecs(
|
addOperation(spec: OperationSpec): void {
|
||||||
_specs: unknown[],
|
const key = `${spec.namespace}.${spec.name}`;
|
||||||
): FlowGraph<TSchema, TSchema> {
|
this.addNode(key, {
|
||||||
throw new Error("not implemented");
|
name: spec.name,
|
||||||
|
namespace: spec.namespace,
|
||||||
|
version: spec.version,
|
||||||
|
type: spec.type,
|
||||||
|
inputSchema: spec.inputSchema,
|
||||||
|
outputSchema: spec.outputSchema,
|
||||||
|
...(spec.description !== undefined ? { description: spec.description } : {}),
|
||||||
|
...(spec.tags !== undefined ? { tags: spec.tags } : {}),
|
||||||
|
} as Static<NodeAttrs>);
|
||||||
|
}
|
||||||
|
|
||||||
|
addTypedEdge(
|
||||||
|
source: string,
|
||||||
|
target: string,
|
||||||
|
attrs: { compatible: boolean; detail?: string; mismatches?: TypeCompatResult["mismatches"] },
|
||||||
|
): void {
|
||||||
|
if (!this._graph.hasNode(source)) {
|
||||||
|
throw new NodeNotFoundError(source);
|
||||||
|
}
|
||||||
|
if (!this._graph.hasNode(target)) {
|
||||||
|
throw new NodeNotFoundError(target);
|
||||||
|
}
|
||||||
|
const edgeAttrs: TypedEdgeAttrs = {
|
||||||
|
edgeType: "typed",
|
||||||
|
compatible: attrs.compatible,
|
||||||
|
...(attrs.detail !== undefined ? { detail: attrs.detail } : {}),
|
||||||
|
...(attrs.mismatches !== undefined ? { mismatches: attrs.mismatches } : {}),
|
||||||
|
};
|
||||||
|
this.addEdge(source, target, edgeAttrs as Static<EdgeAttrs>);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromSpecs(specs: OperationSpec[]): OperationGraph {
|
||||||
|
const graph = new FlowGraph<typeof OperationNodeAttrsSchema, typeof OperationEdgeAttrsSchema>();
|
||||||
|
for (const spec of specs) {
|
||||||
|
graph.addOperation(spec);
|
||||||
|
}
|
||||||
|
buildTypeEdges(graph);
|
||||||
|
return graph;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromCallEvents(
|
static fromCallEvents(
|
||||||
@@ -196,4 +259,25 @@ export class FlowGraph<
|
|||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTypeEdges(graph: OperationGraph): void {
|
||||||
|
const nodeKeys = graph.nodes();
|
||||||
|
for (const source of nodeKeys) {
|
||||||
|
for (const target of nodeKeys) {
|
||||||
|
if (source === target) continue;
|
||||||
|
const sourceAttrs = graph.getNodeAttributes(source as never) as unknown as OperationNodeAttrs;
|
||||||
|
const targetAttrs = graph.getNodeAttributes(target as never) as unknown as OperationNodeAttrs;
|
||||||
|
const result = typeCompat(sourceAttrs.outputSchema as TSchema, targetAttrs.inputSchema as TSchema);
|
||||||
|
if (result === undefined) continue;
|
||||||
|
if (graph.hasEdge(source, target)) continue;
|
||||||
|
if (willCreateCycle(graph.graph, source, target)) continue;
|
||||||
|
const detail = result.detail ?? `${sourceAttrs.namespace}.${sourceAttrs.name}.output → ${targetAttrs.namespace}.${targetAttrs.name}.input`;
|
||||||
|
graph.addTypedEdge(source, target, {
|
||||||
|
compatible: result.compatible,
|
||||||
|
detail,
|
||||||
|
...(result.mismatches !== undefined ? { mismatches: result.mismatches } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
export { FlowGraph, type FlowGraphOptions } from "./construction.js";
|
export { FlowGraph, buildTypeEdges, type FlowGraphOptions, type OperationSpec } from "./construction.js";
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from "./error/index.js";
|
export * from "./error/index.js";
|
||||||
|
|
||||||
export { FlowGraph, type FlowGraphOptions } from "./graph/index.js";
|
export { FlowGraph, buildTypeEdges, type FlowGraphOptions, type OperationSpec } from "./graph/index.js";
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { FlowGraph } from "../../src/graph/construction.js";
|
import { Type } from "@alkdev/typebox";
|
||||||
|
import { FlowGraph, buildTypeEdges } from "../../src/graph/construction.js";
|
||||||
|
import type { OperationSpec } from "../../src/graph/construction.js";
|
||||||
import {
|
import {
|
||||||
DuplicateNodeError,
|
DuplicateNodeError,
|
||||||
DuplicateEdgeError,
|
DuplicateEdgeError,
|
||||||
@@ -298,10 +300,6 @@ describe("FlowGraph query methods", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("FlowGraph static stubs", () => {
|
describe("FlowGraph static stubs", () => {
|
||||||
it("fromSpecs throws not implemented", () => {
|
|
||||||
expect(() => FlowGraph.fromSpecs([])).toThrow("not implemented");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fromCallEvents throws not implemented", () => {
|
it("fromCallEvents throws not implemented", () => {
|
||||||
expect(() => FlowGraph.fromCallEvents([])).toThrow("not implemented");
|
expect(() => FlowGraph.fromCallEvents([])).toThrow("not implemented");
|
||||||
});
|
});
|
||||||
@@ -311,11 +309,283 @@ describe("FlowGraph static stubs", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("FlowGraph.addOperation", () => {
|
||||||
|
it("adds an operation node with namespace.name key", () => {
|
||||||
|
const fg = new FlowGraph();
|
||||||
|
const spec: OperationSpec = {
|
||||||
|
name: "classify",
|
||||||
|
namespace: "task",
|
||||||
|
version: "1.0.0",
|
||||||
|
type: "query",
|
||||||
|
inputSchema: Type.Object({ text: Type.String() }),
|
||||||
|
outputSchema: Type.Object({ label: Type.String() }),
|
||||||
|
};
|
||||||
|
fg.addOperation(spec);
|
||||||
|
expect(fg.hasNode("task.classify")).toBe(true);
|
||||||
|
const attrs = fg.getNodeAttributes("task.classify") as Record<string, unknown>;
|
||||||
|
expect(attrs.name).toBe("classify");
|
||||||
|
expect(attrs.namespace).toBe("task");
|
||||||
|
expect(attrs.version).toBe("1.0.0");
|
||||||
|
expect(attrs.type).toBe("query");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws DuplicateNodeError for duplicate operation", () => {
|
||||||
|
const fg = new FlowGraph();
|
||||||
|
const spec: OperationSpec = {
|
||||||
|
name: "classify",
|
||||||
|
namespace: "task",
|
||||||
|
version: "1.0.0",
|
||||||
|
type: "query",
|
||||||
|
inputSchema: Type.Object({ text: Type.String() }),
|
||||||
|
outputSchema: Type.Object({ label: Type.String() }),
|
||||||
|
};
|
||||||
|
fg.addOperation(spec);
|
||||||
|
expect(() => fg.addOperation(spec)).toThrow(DuplicateNodeError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes optional description and tags", () => {
|
||||||
|
const fg = new FlowGraph();
|
||||||
|
const spec: OperationSpec = {
|
||||||
|
name: "classify",
|
||||||
|
namespace: "task",
|
||||||
|
version: "1.0.0",
|
||||||
|
type: "query",
|
||||||
|
inputSchema: Type.Object({ text: Type.String() }),
|
||||||
|
outputSchema: Type.Object({ label: Type.String() }),
|
||||||
|
description: "Classifies text",
|
||||||
|
tags: ["nlp", "ml"],
|
||||||
|
};
|
||||||
|
fg.addOperation(spec);
|
||||||
|
const attrs = fg.getNodeAttributes("task.classify") as Record<string, unknown>;
|
||||||
|
expect(attrs.description).toBe("Classifies text");
|
||||||
|
expect(attrs.tags).toEqual(["nlp", "ml"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("FlowGraph.addTypedEdge", () => {
|
||||||
|
it("adds a typed edge with edgeType: 'typed'", () => {
|
||||||
|
const fg = new FlowGraph();
|
||||||
|
const spec1: OperationSpec = {
|
||||||
|
name: "classify",
|
||||||
|
namespace: "task",
|
||||||
|
version: "1.0.0",
|
||||||
|
type: "query",
|
||||||
|
inputSchema: Type.Object({ text: Type.String() }),
|
||||||
|
outputSchema: Type.Object({ label: Type.String() }),
|
||||||
|
};
|
||||||
|
const spec2: OperationSpec = {
|
||||||
|
name: "enrich",
|
||||||
|
namespace: "task",
|
||||||
|
version: "1.0.0",
|
||||||
|
type: "query",
|
||||||
|
inputSchema: Type.Object({ label: Type.String() }),
|
||||||
|
outputSchema: Type.Object({ enriched: Type.String() }),
|
||||||
|
};
|
||||||
|
fg.addOperation(spec1);
|
||||||
|
fg.addOperation(spec2);
|
||||||
|
fg.addTypedEdge("task.classify", "task.enrich", { compatible: true });
|
||||||
|
expect(fg.hasEdge("task.classify", "task.enrich")).toBe(true);
|
||||||
|
const edgeAttrs = fg.getEdgeAttributes("task.classify", "task.enrich") as Record<string, unknown>;
|
||||||
|
expect(edgeAttrs.edgeType).toBe("typed");
|
||||||
|
expect(edgeAttrs.compatible).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws NodeNotFoundError if source doesn't exist", () => {
|
||||||
|
const fg = new FlowGraph();
|
||||||
|
const spec: OperationSpec = {
|
||||||
|
name: "enrich",
|
||||||
|
namespace: "task",
|
||||||
|
version: "1.0.0",
|
||||||
|
type: "query",
|
||||||
|
inputSchema: Type.Object({ label: Type.String() }),
|
||||||
|
outputSchema: Type.Object({ enriched: Type.String() }),
|
||||||
|
};
|
||||||
|
fg.addOperation(spec);
|
||||||
|
expect(() => fg.addTypedEdge("task.missing", "task.enrich", { compatible: true })).toThrow(NodeNotFoundError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws NodeNotFoundError if target doesn't exist", () => {
|
||||||
|
const fg = new FlowGraph();
|
||||||
|
const spec: OperationSpec = {
|
||||||
|
name: "classify",
|
||||||
|
namespace: "task",
|
||||||
|
version: "1.0.0",
|
||||||
|
type: "query",
|
||||||
|
inputSchema: Type.Object({ text: Type.String() }),
|
||||||
|
outputSchema: Type.Object({ label: Type.String() }),
|
||||||
|
};
|
||||||
|
fg.addOperation(spec);
|
||||||
|
expect(() => fg.addTypedEdge("task.classify", "task.missing", { compatible: true })).toThrow(NodeNotFoundError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes detail and mismatches when provided", () => {
|
||||||
|
const fg = new FlowGraph();
|
||||||
|
const spec1: OperationSpec = {
|
||||||
|
name: "a",
|
||||||
|
namespace: "op",
|
||||||
|
version: "1.0.0",
|
||||||
|
type: "query",
|
||||||
|
inputSchema: Type.Object({ x: Type.String() }),
|
||||||
|
outputSchema: Type.Object({ y: Type.Number() }),
|
||||||
|
};
|
||||||
|
const spec2: OperationSpec = {
|
||||||
|
name: "b",
|
||||||
|
namespace: "op",
|
||||||
|
version: "1.0.0",
|
||||||
|
type: "query",
|
||||||
|
inputSchema: Type.Object({ x: Type.String() }),
|
||||||
|
outputSchema: Type.Object({ z: Type.Boolean() }),
|
||||||
|
};
|
||||||
|
fg.addOperation(spec1);
|
||||||
|
fg.addOperation(spec2);
|
||||||
|
fg.addTypedEdge("op.a", "op.b", {
|
||||||
|
compatible: false,
|
||||||
|
detail: "output mismatch",
|
||||||
|
mismatches: [{ path: "/y", expected: "string", actual: "number" }],
|
||||||
|
});
|
||||||
|
const edgeAttrs = fg.getEdgeAttributes("op.a", "op.b") as Record<string, unknown>;
|
||||||
|
expect(edgeAttrs.compatible).toBe(false);
|
||||||
|
expect(edgeAttrs.detail).toBe("output mismatch");
|
||||||
|
expect(edgeAttrs.mismatches).toEqual([{ path: "/y", expected: "string", actual: "number" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws CycleError if edge would create cycle", () => {
|
||||||
|
const fg = new FlowGraph();
|
||||||
|
fg.addOperation({
|
||||||
|
name: "a", namespace: "op", version: "1.0.0", type: "query",
|
||||||
|
inputSchema: Type.Object({ x: Type.String() }),
|
||||||
|
outputSchema: Type.Object({ y: Type.String() }),
|
||||||
|
});
|
||||||
|
fg.addOperation({
|
||||||
|
name: "b", namespace: "op", version: "1.0.0", type: "query",
|
||||||
|
inputSchema: Type.Object({ y: Type.String() }),
|
||||||
|
outputSchema: Type.Object({ x: Type.String() }),
|
||||||
|
});
|
||||||
|
fg.addTypedEdge("op.a", "op.b", { compatible: true });
|
||||||
|
expect(() => fg.addTypedEdge("op.b", "op.a", { compatible: true })).toThrow(CycleError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("FlowGraph.fromSpecs", () => {
|
||||||
|
it("creates operation nodes for each spec", () => {
|
||||||
|
const specs: OperationSpec[] = [
|
||||||
|
{ name: "extract", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ raw: Type.String() }), outputSchema: Type.Object({ text: Type.String() }) },
|
||||||
|
{ name: "classify", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ text: Type.String() }), outputSchema: Type.Object({ label: Type.String(), score: Type.Number() }) },
|
||||||
|
];
|
||||||
|
const graph = FlowGraph.fromSpecs(specs);
|
||||||
|
expect(graph.order).toBe(2);
|
||||||
|
expect(graph.hasNode("task.extract")).toBe(true);
|
||||||
|
expect(graph.hasNode("task.classify")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds type-compatibility edges via buildTypeEdges", () => {
|
||||||
|
const specs: OperationSpec[] = [
|
||||||
|
{ name: "extract", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ raw: Type.String() }), outputSchema: Type.Object({ text: Type.String() }) },
|
||||||
|
{ name: "classify", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ text: Type.String() }), outputSchema: Type.Object({ label: Type.String(), score: Type.Number() }) },
|
||||||
|
];
|
||||||
|
const graph = FlowGraph.fromSpecs(specs);
|
||||||
|
expect(graph.size).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds compatible edge when output matches input", () => {
|
||||||
|
const specs: OperationSpec[] = [
|
||||||
|
{ name: "extract", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ raw: Type.String() }), outputSchema: Type.Object({ text: Type.String() }) },
|
||||||
|
{ name: "classify", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ text: Type.String() }), outputSchema: Type.Object({ label: Type.String(), score: Type.Number() }) },
|
||||||
|
];
|
||||||
|
const graph = FlowGraph.fromSpecs(specs);
|
||||||
|
expect(graph.hasEdge("task.extract", "task.classify")).toBe(true);
|
||||||
|
const attrs = graph.getEdgeAttributes("task.extract", "task.classify") as Record<string, unknown>;
|
||||||
|
expect(attrs.compatible).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds incompatible edge when output does not match input", () => {
|
||||||
|
const specs: OperationSpec[] = [
|
||||||
|
{ name: "classify", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ text: Type.String() }), outputSchema: Type.Object({ label: Type.String(), score: Type.Number() }) },
|
||||||
|
{ name: "count", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ count: Type.Number() }), outputSchema: Type.Object({ result: Type.Number() }) },
|
||||||
|
];
|
||||||
|
const graph = FlowGraph.fromSpecs(specs);
|
||||||
|
expect(graph.hasEdge("task.classify", "task.count")).toBe(true);
|
||||||
|
const attrs = graph.getEdgeAttributes("task.classify", "task.count") as Record<string, unknown>;
|
||||||
|
expect(attrs.compatible).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips edges when target inputSchema is Unknown", () => {
|
||||||
|
const specs: OperationSpec[] = [
|
||||||
|
{ name: "source", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Object({ x: Type.String() }), outputSchema: Type.Object({ result: Type.String() }) },
|
||||||
|
{ name: "unk_input", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Unknown(), outputSchema: Type.Object({ y: Type.String() }) },
|
||||||
|
];
|
||||||
|
const graph = FlowGraph.fromSpecs(specs);
|
||||||
|
expect(graph.hasEdge("op.source", "op.unk_input")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips edges when source outputSchema is Unknown", () => {
|
||||||
|
const specs: OperationSpec[] = [
|
||||||
|
{ name: "unk_output", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Object({ x: Type.String() }), outputSchema: Type.Unknown() },
|
||||||
|
{ name: "target", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Object({ result: Type.String() }), outputSchema: Type.Object({ z: Type.String() }) },
|
||||||
|
];
|
||||||
|
const graph = FlowGraph.fromSpecs(specs);
|
||||||
|
expect(graph.hasEdge("op.unk_output", "op.target")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws DuplicateNodeError for duplicate specs", () => {
|
||||||
|
const stringOutput = Type.Object({ label: Type.String() });
|
||||||
|
const specs: OperationSpec[] = [
|
||||||
|
{ name: "classify", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ text: Type.String() }), outputSchema: stringOutput },
|
||||||
|
{ name: "classify", namespace: "task", version: "2.0.0", type: "query", inputSchema: Type.Object({ text: Type.String() }), outputSchema: stringOutput },
|
||||||
|
];
|
||||||
|
expect(() => FlowGraph.fromSpecs(specs)).toThrow(DuplicateNodeError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty graph for empty specs", () => {
|
||||||
|
const graph = FlowGraph.fromSpecs([]);
|
||||||
|
expect(graph.order).toBe(0);
|
||||||
|
expect(graph.size).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("buildTypeEdges (standalone)", () => {
|
||||||
|
it("adds edges to an incrementally constructed graph", () => {
|
||||||
|
const fg = new FlowGraph();
|
||||||
|
fg.addOperation({
|
||||||
|
name: "extract", namespace: "task", version: "1.0.0", type: "query",
|
||||||
|
inputSchema: Type.Object({ raw: Type.String() }),
|
||||||
|
outputSchema: Type.Object({ text: Type.String() }),
|
||||||
|
});
|
||||||
|
fg.addOperation({
|
||||||
|
name: "classify", namespace: "task", version: "1.0.0", type: "query",
|
||||||
|
inputSchema: Type.Object({ text: Type.String() }),
|
||||||
|
outputSchema: Type.Object({ label: Type.String(), score: Type.Number() }),
|
||||||
|
});
|
||||||
|
expect(fg.size).toBe(0);
|
||||||
|
buildTypeEdges(fg);
|
||||||
|
expect(fg.size).toBeGreaterThan(0);
|
||||||
|
expect(fg.hasEdge("task.extract", "task.classify")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is callable after adding more operations", () => {
|
||||||
|
const fg = new FlowGraph();
|
||||||
|
fg.addOperation({
|
||||||
|
name: "extract", namespace: "op", version: "1.0.0", type: "query",
|
||||||
|
inputSchema: Type.Object({ raw: Type.String() }),
|
||||||
|
outputSchema: Type.Object({ text: Type.String() }),
|
||||||
|
});
|
||||||
|
buildTypeEdges(fg);
|
||||||
|
expect(fg.size).toBe(0);
|
||||||
|
fg.addOperation({
|
||||||
|
name: "classify", namespace: "op", version: "1.0.0", type: "query",
|
||||||
|
inputSchema: Type.Object({ text: Type.String() }),
|
||||||
|
outputSchema: Type.Object({ label: Type.String(), score: Type.Number() }),
|
||||||
|
});
|
||||||
|
buildTypeEdges(fg);
|
||||||
|
expect(fg.hasEdge("op.extract", "op.classify")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("FlowGraph re-exports", () => {
|
describe("FlowGraph re-exports", () => {
|
||||||
it("is re-exported from src/graph/index.ts", async () => {
|
it("is re-exported from src/graph/index.ts", async () => {
|
||||||
const mod = await import("../../src/graph/index.js");
|
const mod = await import("../../src/graph/index.js");
|
||||||
expect(mod.FlowGraph).toBeDefined();
|
expect(mod.FlowGraph).toBeDefined();
|
||||||
expect(mod.FlowGraphOptions).toBeUndefined();
|
expect(mod.buildTypeEdges).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is re-exported from src/index.ts", async () => {
|
it("is re-exported from src/index.ts", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user