Merge branch 'feat/analysis-build-type-edges'
# Conflicts: # src/analysis/index.ts # src/graph/construction.ts # src/index.ts
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { Type, type TSchema } from "@alkdev/typebox";
|
||||
import { typeCompat, type TypeCompatResult, type TypeMismatch } from "../../src/analysis/type-compat.js";
|
||||
import { typeCompat, buildTypeEdges, type TypeCompatResult, type TypeMismatch } from "../../src/analysis/type-compat.js";
|
||||
import { FlowGraph } from "../../src/graph/construction.js";
|
||||
import type { OperationSpec } from "../../src/graph/construction.js";
|
||||
|
||||
describe("typeCompat", () => {
|
||||
describe("exact match", () => {
|
||||
@@ -411,4 +413,111 @@ describe("typeCompat", () => {
|
||||
expect(typeof mismatch.actual).toBe("string");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildTypeEdges", () => {
|
||||
it("adds compatible edges for matching output→input schemas", () => {
|
||||
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() }) });
|
||||
buildTypeEdges(fg);
|
||||
expect(fg.hasEdge("task.extract", "task.classify")).toBe(true);
|
||||
const attrs = fg.getEdgeAttributes("task.extract", "task.classify") as Record<string, unknown>;
|
||||
expect(attrs.edgeType).toBe("typed");
|
||||
expect(attrs.compatible).toBe(true);
|
||||
});
|
||||
|
||||
it("adds incompatible edges when schemas mismatch", () => {
|
||||
const fg = new FlowGraph();
|
||||
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() }) });
|
||||
fg.addOperation({ name: "count", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ count: Type.Number() }), outputSchema: Type.Object({ result: Type.Number() }) });
|
||||
buildTypeEdges(fg);
|
||||
expect(fg.hasEdge("task.classify", "task.count")).toBe(true);
|
||||
const attrs = fg.getEdgeAttributes("task.classify", "task.count") as Record<string, unknown>;
|
||||
expect(attrs.edgeType).toBe("typed");
|
||||
expect(attrs.compatible).toBe(false);
|
||||
expect(attrs.mismatches).toBeDefined();
|
||||
});
|
||||
|
||||
it("incompatible edges include mismatches array", () => {
|
||||
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({ value: Type.String() }) });
|
||||
fg.addOperation({ name: "b", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Object({ value: Type.Number() }), outputSchema: Type.Object({ z: Type.Boolean() }) });
|
||||
buildTypeEdges(fg);
|
||||
const attrs = fg.getEdgeAttributes("op.a", "op.b") as Record<string, unknown>;
|
||||
expect(attrs.compatible).toBe(false);
|
||||
expect(Array.isArray(attrs.mismatches)).toBe(true);
|
||||
expect((attrs.mismatches as Array<unknown>).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("does not add edges when either schema is Unknown", () => {
|
||||
const fg = new FlowGraph();
|
||||
fg.addOperation({ name: "unk_out", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Object({ x: Type.String() }), outputSchema: Type.Unknown() });
|
||||
fg.addOperation({ name: "unk_in", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Unknown(), outputSchema: Type.Object({ y: Type.String() }) });
|
||||
fg.addOperation({ name: "normal", namespace: "op", version: "1.0.0", type: "query", inputSchema: Type.Object({ y: Type.String() }), outputSchema: Type.Object({ x: Type.String() }) });
|
||||
buildTypeEdges(fg);
|
||||
expect(fg.hasEdge("op.unk_out", "op.unk_in")).toBe(false);
|
||||
expect(fg.hasEdge("op.unk_out", "op.normal")).toBe(false);
|
||||
expect(fg.hasEdge("op.normal", "op.unk_in")).toBe(false);
|
||||
});
|
||||
|
||||
it("sets detail to namespace.name.output → namespace.name.input for compatible edges", () => {
|
||||
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() }) });
|
||||
buildTypeEdges(fg);
|
||||
const attrs = fg.getEdgeAttributes("task.extract", "task.classify") as Record<string, unknown>;
|
||||
expect(attrs.detail).toContain("task.extract.output → task.classify.input");
|
||||
});
|
||||
|
||||
it("is callable after incremental addOperation calls", () => {
|
||||
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() }) });
|
||||
buildTypeEdges(fg);
|
||||
expect(fg.hasEdge("op.extract", "op.classify")).toBe(true);
|
||||
});
|
||||
|
||||
it("produces edges for three operations in a pipeline", () => {
|
||||
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() }) });
|
||||
fg.addOperation({ name: "enrich", namespace: "task", version: "1.0.0", type: "query", inputSchema: Type.Object({ label: Type.String() }), outputSchema: Type.Object({ enriched: Type.String() }) });
|
||||
buildTypeEdges(fg);
|
||||
expect(fg.hasEdge("task.extract", "task.classify")).toBe(true);
|
||||
expect(fg.hasEdge("task.classify", "task.enrich")).toBe(true);
|
||||
expect(fg.hasEdge("task.extract", "task.enrich")).toBe(true);
|
||||
const e2c = fg.getEdgeAttributes("task.extract", "task.classify") as Record<string, unknown>;
|
||||
const c2e = fg.getEdgeAttributes("task.classify", "task.enrich") as Record<string, unknown>;
|
||||
const e2e = fg.getEdgeAttributes("task.extract", "task.enrich") as Record<string, unknown>;
|
||||
expect(e2c.compatible).toBe(true);
|
||||
expect(c2e.compatible).toBe(true);
|
||||
expect(e2e.compatible).toBe(false);
|
||||
});
|
||||
|
||||
it("does not add self-loops", () => {
|
||||
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({ x: Type.String() }) });
|
||||
buildTypeEdges(fg);
|
||||
expect(fg.size).toBe(0);
|
||||
});
|
||||
|
||||
it("returns empty graph with no edges for empty graph", () => {
|
||||
const fg = new FlowGraph();
|
||||
buildTypeEdges(fg);
|
||||
expect(fg.order).toBe(0);
|
||||
expect(fg.size).toBe(0);
|
||||
});
|
||||
|
||||
it("skips edges that would already exist", () => {
|
||||
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({ z: Type.String() }) });
|
||||
buildTypeEdges(fg);
|
||||
const sizeAfterFirst = fg.size;
|
||||
buildTypeEdges(fg);
|
||||
expect(fg.size).toBe(sizeAfterFirst);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user