import argparse import sys from typing import Any import yaml def fail(msg: str) -> None: print(f"ERROR: {msg}", file=sys.stderr) raise SystemExit(1) def expect_type(value: Any, t: type, label: str) -> None: if not isinstance(value, t): fail(f"{label} must be {t.__name__}, got {type(value).__name__}") def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--path", required=True) args = ap.parse_args() with open(args.path, "r", encoding="utf-8") as f: doc = yaml.safe_load(f) expect_type(doc, dict, "root") if doc.get("kind") != "app": fail("root.kind must be 'app'") version = doc.get("version") if not isinstance(version, (str, int, float)): fail("root.version must be a scalar") app = doc.get("app") expect_type(app, dict, "root.app") if app.get("mode") != "workflow": fail("app.mode must be 'workflow'") if not app.get("name"): fail("app.name is required") workflow = doc.get("workflow") expect_type(workflow, dict, "root.workflow") graph = workflow.get("graph") expect_type(graph, dict, "workflow.graph") nodes = graph.get("nodes") edges = graph.get("edges") expect_type(nodes, list, "workflow.graph.nodes") expect_type(edges, list, "workflow.graph.edges") if not nodes: fail("workflow.graph.nodes must be non-empty") node_ids: set[str] = set() node_types: set[str] = set() for i, n in enumerate(nodes): expect_type(n, dict, f"node[{i}]") nid = n.get("id") if not isinstance(nid, str) or not nid: fail(f"node[{i}].id must be non-empty string") if nid in node_ids: fail(f"duplicate node id: {nid}") node_ids.add(nid) data = n.get("data") or {} if isinstance(data, dict): ntype = data.get("type") if isinstance(ntype, str): node_types.add(ntype) # Minimal sanity: ensure a workflow has start/end nodes. if "start" not in node_types: fail("no start node found (node.data.type == 'start')") if "end" not in node_types: fail("no end node found (node.data.type == 'end')") for i, e in enumerate(edges): expect_type(e, dict, f"edge[{i}]") src = e.get("source") tgt = e.get("target") if src not in node_ids: fail(f"edge[{i}].source references unknown node: {src}") if tgt not in node_ids: fail(f"edge[{i}].target references unknown node: {tgt}") print(f"OK: {args.path} looks like a Dify app DSL (kind=app, mode=workflow, nodes={len(nodes)}, edges={len(edges)}).") return 0 if __name__ == "__main__": raise SystemExit(main())