Implement graph validation functions (validateSchema, validateGraph, validate)
This commit is contained in:
@@ -4,4 +4,9 @@ export {
|
||||
defaultEdgeType,
|
||||
resolveDefaultNodeAttrs,
|
||||
} from "./defaults.js";
|
||||
export { typeCompat, type TypeCompatResult, type TypeMismatch } from "./type-compat.js";
|
||||
export { typeCompat, type TypeCompatResult, type TypeMismatch } from "./type-compat.js";
|
||||
export {
|
||||
validateSchema,
|
||||
validateGraph,
|
||||
validate,
|
||||
} from "../graph/validation.js";
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
NodeNotFoundError,
|
||||
CycleError,
|
||||
} from "../error/index.js";
|
||||
import { validate as _validate } from "./validation.js";
|
||||
import type { AnyValidationError } from "../error/index.js";
|
||||
|
||||
export interface FlowGraphOptions {
|
||||
type?: "directed";
|
||||
@@ -154,6 +156,10 @@ export class FlowGraph<
|
||||
return this._graph.outNeighbors(nodeId) ?? [];
|
||||
}
|
||||
|
||||
validate(schema: TSchema): AnyValidationError[] {
|
||||
return _validate(this, schema as NodeAttrs);
|
||||
}
|
||||
|
||||
static fromSpecs(
|
||||
_specs: unknown[],
|
||||
): FlowGraph<TSchema, TSchema> {
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
export { FlowGraph, type FlowGraphOptions } from "./construction.js";
|
||||
export { FlowGraph, type FlowGraphOptions } from "./construction.js";
|
||||
export {
|
||||
validateSchema,
|
||||
validateGraph,
|
||||
validate,
|
||||
} from "./validation.js";
|
||||
@@ -1 +1,152 @@
|
||||
export {};
|
||||
import { hasCycle } from "graphology-dag";
|
||||
import { Value } from "@alkdev/typebox/value";
|
||||
import type { TSchema } from "@alkdev/typebox";
|
||||
import type { FlowGraph } from "./construction.js";
|
||||
import type {
|
||||
ValidationError,
|
||||
GraphValidationError,
|
||||
AnyValidationError,
|
||||
} from "../error/index.js";
|
||||
|
||||
export function validateSchema<N extends TSchema>(
|
||||
graph: FlowGraph<N, TSchema>,
|
||||
schema: N,
|
||||
): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
graph.forEachNode((nodeKey, attrs) => {
|
||||
const iter = Value.Errors(schema, attrs as Record<string, unknown>);
|
||||
for (const error of iter) {
|
||||
errors.push({
|
||||
type: "schema",
|
||||
nodeKey,
|
||||
field: error.path.replace(/^\//, "") || error.path,
|
||||
message: error.message,
|
||||
value: error.value,
|
||||
});
|
||||
}
|
||||
});
|
||||
return errors;
|
||||
}
|
||||
|
||||
function findCyclesViaDfs(g: FlowGraph<TSchema, TSchema>): string[][] {
|
||||
const dg = g.graph;
|
||||
const visited = new Set<string>();
|
||||
const recStack = new Set<string>();
|
||||
const cycles: string[][] = [];
|
||||
const path: string[] = [];
|
||||
|
||||
function dfs(node: string): void {
|
||||
visited.add(node);
|
||||
recStack.add(node);
|
||||
path.push(node);
|
||||
|
||||
const neighbors = dg.outNeighbors(node) ?? [];
|
||||
for (const neighbor of neighbors) {
|
||||
if (!visited.has(neighbor)) {
|
||||
dfs(neighbor);
|
||||
} else if (recStack.has(neighbor)) {
|
||||
const cycleStart = path.indexOf(neighbor);
|
||||
if (cycleStart !== -1) {
|
||||
cycles.push([...path.slice(cycleStart), neighbor]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path.pop();
|
||||
recStack.delete(node);
|
||||
}
|
||||
|
||||
const nodeKeys = g.nodes();
|
||||
for (const node of nodeKeys) {
|
||||
if (!visited.has(node)) {
|
||||
dfs(node);
|
||||
}
|
||||
}
|
||||
|
||||
return cycles;
|
||||
}
|
||||
|
||||
export function validateGraph(
|
||||
graph: FlowGraph<TSchema, TSchema>,
|
||||
): GraphValidationError[] {
|
||||
const errors: GraphValidationError[] = [];
|
||||
const dg = graph.graph;
|
||||
|
||||
if (hasCycle(dg)) {
|
||||
const cycles = findCyclesViaDfs(graph);
|
||||
errors.push({
|
||||
type: "graph",
|
||||
category: "cycle",
|
||||
details: { cycles },
|
||||
});
|
||||
}
|
||||
|
||||
dg.forEachEdge((_key, _attrs, source, target) => {
|
||||
if (!dg.hasNode(source!)) {
|
||||
errors.push({
|
||||
type: "graph",
|
||||
category: "dangling-reference",
|
||||
details: { source, target: target! },
|
||||
});
|
||||
}
|
||||
if (!dg.hasNode(target!)) {
|
||||
errors.push({
|
||||
type: "graph",
|
||||
category: "dangling-reference",
|
||||
details: { source: source!, target },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const nodeKeys = graph.nodes();
|
||||
for (const nodeKey of nodeKeys) {
|
||||
const inDegree = dg.inDegree(nodeKey);
|
||||
const outDegree = dg.outDegree(nodeKey);
|
||||
if (inDegree === 0 && outDegree === 0) {
|
||||
errors.push({
|
||||
type: "graph",
|
||||
category: "orphan-node",
|
||||
details: { nodeKey },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
graph.forEachNode((nodeKey, attrs) => {
|
||||
const nodeAttrs = attrs as Record<string, unknown>;
|
||||
const nodeStatus = nodeAttrs["status"] as string | undefined;
|
||||
if (nodeStatus === undefined) return;
|
||||
|
||||
const parents = graph.predecessors(nodeKey);
|
||||
for (const parentKey of parents) {
|
||||
const parentAttrs = graph.getNodeAttributes(parentKey) as Record<string, unknown>;
|
||||
const parentStatus = parentAttrs["status"] as string | undefined;
|
||||
if (parentStatus === undefined) continue;
|
||||
|
||||
const terminalParent = parentStatus === "completed" || parentStatus === "failed" || parentStatus === "aborted";
|
||||
const activeChild = nodeStatus === "running" || nodeStatus === "pending" || nodeStatus === "waiting" || nodeStatus === "ready";
|
||||
if (terminalParent && activeChild) {
|
||||
errors.push({
|
||||
type: "graph",
|
||||
category: "status-inconsistency",
|
||||
details: {
|
||||
nodeKey,
|
||||
parentKey,
|
||||
nodeStatus,
|
||||
parentStatus,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function validate<N extends TSchema>(
|
||||
graph: FlowGraph<N, TSchema>,
|
||||
schema: N,
|
||||
): AnyValidationError[] {
|
||||
const schemaErrors = validateSchema(graph, schema);
|
||||
const graphErrors = validateGraph(graph);
|
||||
return [...schemaErrors, ...graphErrors];
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
export * from "./error/index.js";
|
||||
|
||||
export { FlowGraph, type FlowGraphOptions } from "./graph/index.js";
|
||||
export { FlowGraph, type FlowGraphOptions } from "./graph/index.js";
|
||||
export {
|
||||
validateSchema,
|
||||
validateGraph,
|
||||
validate,
|
||||
} from "./graph/validation.js";
|
||||
Reference in New Issue
Block a user