← Files EW AI FlowchartARCHIVED FILE
scripts/validate_efd_json.py
12.9 KB · Oct 2, 2026 · 00:34 UTC
#!/usr/bin/env python3
"""Validate an EFD 2.1 JSON file without executing untrusted content."""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
SCHEMA_PATH = PLUGIN_ROOT / "skills" / "efd-json-forge" / "references" / "efd-2.1.schema.json"
ID_PATTERN = re.compile(r"^[A-Z][A-Z0-9]*-[A-Za-z0-9._-]+$")
def issue(items: list[dict[str, str]], severity: str, path: str, message: str) -> None:
items.append({"severity": severity, "path": path, "message": message})
def resolve_ref(root: dict[str, Any], ref: str) -> dict[str, Any]:
if not ref.startswith("#/"):
raise ValueError(f"unsupported external schema reference: {ref}")
value: Any = root
for token in ref[2:].split("/"):
value = value[token.replace("~1", "/").replace("~0", "~")]
return value
def schema_validate(value: Any, rule: dict[str, Any], root: dict[str, Any], path: str, out: list[dict[str, str]]) -> None:
if "$ref" in rule:
schema_validate(value, resolve_ref(root, rule["$ref"]), root, path, out)
return
expected = rule.get("type")
type_ok = {
"object": isinstance(value, dict),
"array": isinstance(value, list),
"string": isinstance(value, str),
"number": isinstance(value, (int, float)) and not isinstance(value, bool),
"integer": isinstance(value, int) and not isinstance(value, bool),
"boolean": isinstance(value, bool),
"null": value is None,
}
if expected and not type_ok.get(expected, True):
issue(out, "error", path, f"expected {expected}")
return
if "const" in rule and value != rule["const"]:
issue(out, "error", path, f"must equal {rule['const']!r}")
if "enum" in rule and value not in rule["enum"]:
issue(out, "error", path, f"value is not in {rule['enum']!r}")
if isinstance(value, str):
if len(value) < rule.get("minLength", 0):
issue(out, "error", path, "string is too short")
if "maxLength" in rule and len(value) > rule["maxLength"]:
issue(out, "error", path, "string is too long")
if "pattern" in rule and not re.search(rule["pattern"], value):
issue(out, "error", path, f"does not match {rule['pattern']}")
if isinstance(value, (int, float)) and not isinstance(value, bool):
if "minimum" in rule and value < rule["minimum"]:
issue(out, "error", path, f"must be >= {rule['minimum']}")
if "exclusiveMinimum" in rule and value <= rule["exclusiveMinimum"]:
issue(out, "error", path, f"must be > {rule['exclusiveMinimum']}")
if "maximum" in rule and value > rule["maximum"]:
issue(out, "error", path, f"must be <= {rule['maximum']}")
if isinstance(value, list):
if len(value) < rule.get("minItems", 0):
issue(out, "error", path, "array has too few items")
if rule.get("uniqueItems"):
encoded = [json.dumps(item, sort_keys=True, ensure_ascii=False) for item in value]
if len(encoded) != len(set(encoded)):
issue(out, "error", path, "array items must be unique")
item_rule = rule.get("items")
if isinstance(item_rule, dict):
for index, item in enumerate(value):
schema_validate(item, item_rule, root, f"{path}[{index}]", out)
if isinstance(value, dict):
properties = rule.get("properties", {})
for required in rule.get("required", []):
if required not in value:
issue(out, "error", path, f"missing required property {required}")
if rule.get("additionalProperties") is False:
for key in value:
if key not in properties:
issue(out, "error", f"{path}.{key}", "additional property is not allowed")
for key, child_rule in properties.items():
if key in value:
schema_validate(value[key], child_rule, root, f"{path}.{key}", out)
def index_ids(items: Any, path: str, diagnostics: list[dict[str, str]]) -> set[str]:
result: set[str] = set()
if not isinstance(items, list):
return result
for index, item in enumerate(items):
if not isinstance(item, dict) or not isinstance(item.get("id"), str):
continue
item_id = item["id"]
if not ID_PATTERN.fullmatch(item_id):
issue(diagnostics, "error", f"{path}[{index}].id", "invalid stable ID")
if item_id in result:
issue(diagnostics, "error", f"{path}[{index}].id", f"duplicate ID {item_id}")
result.add(item_id)
return result
def semantic_validate(doc: dict[str, Any], diagnostics: list[dict[str, str]]) -> None:
units = index_ids(doc.get("organizationUnits"), "$.organizationUnits", diagnostics)
positions = index_ids(doc.get("positions"), "$.positions", diagnostics)
ai_workers = index_ids(doc.get("aiWorkers", []), "$.aiWorkers", diagnostics)
systems = index_ids(doc.get("systemActors", []), "$.systemActors", diagnostics)
externals = index_ids(doc.get("externalParties", []), "$.externalParties", diagnostics)
process = doc.get("process") if isinstance(doc.get("process"), dict) else {}
workspace = doc.get("workspace") if isinstance(doc.get("workspace"), dict) else {}
project = doc.get("project") if isinstance(doc.get("project"), dict) else {}
if project.get("workspaceId") != workspace.get("id"):
issue(diagnostics, "error", "$.project.workspaceId", "workspace reference does not match workspace.id")
if process.get("projectId") != project.get("id"):
issue(diagnostics, "error", "$.process.projectId", "project reference does not match project.id")
for index, position in enumerate(doc.get("positions", [])):
if not isinstance(position, dict):
continue
for unit_index, unit_id in enumerate(position.get("unitIds", [])):
if unit_id not in units:
issue(diagnostics, "error", f"$.positions[{index}].unitIds[{unit_index}]", "organization unit reference does not exist")
stages = index_ids(process.get("stages"), "$.process.stages", diagnostics)
nodes = process.get("nodes") if isinstance(process.get("nodes"), list) else []
node_ids = index_ids(nodes, "$.process.nodes", diagnostics)
edges = process.get("edges") if isinstance(process.get("edges"), list) else []
edge_ids = index_ids(edges, "$.process.edges", diagnostics)
registries = [units, positions, ai_workers, systems, externals, stages, node_ids, edge_ids]
combined = [item_id for registry in registries for item_id in registry]
if len(combined) != len(set(combined)):
issue(diagnostics, "error", "$", "stable IDs must be unique across entity registries")
participant_sets = {"position": positions, "ai_worker": ai_workers, "system": systems, "external_party": externals}
node_types: dict[str, str] = {}
incoming = {node_id: 0 for node_id in node_ids}
outgoing = {node_id: 0 for node_id in node_ids}
for index, node in enumerate(nodes):
if not isinstance(node, dict):
continue
node_id = node.get("id")
if isinstance(node_id, str):
node_types[node_id] = str(node.get("type", ""))
if node.get("stageId") not in stages:
issue(diagnostics, "error", f"$.process.nodes[{index}].stageId", "stage reference does not exist")
for unit_index, unit_id in enumerate(node.get("unitIds", [])):
if unit_id not in units:
issue(diagnostics, "error", f"$.process.nodes[{index}].unitIds[{unit_index}]", "organization unit reference does not exist")
roles_seen: set[str] = set()
for raci_index, raci in enumerate(node.get("raci", [])):
if not isinstance(raci, dict):
continue
participant_type = raci.get("participantType")
participant_id = raci.get("participantId")
allowed = participant_sets.get(str(participant_type), set())
if participant_id not in allowed:
issue(diagnostics, "error", f"$.process.nodes[{index}].raci[{raci_index}].participantId", "participant reference does not exist")
roles_seen.update(role for role in raci.get("roles", []) if isinstance(role, str))
if node.get("type") in {"task", "system_action", "approval"}:
if "R" not in roles_seen:
issue(diagnostics, "warning", f"$.process.nodes[{index}].raci", "executable node has no Responsible (R)")
if "A" not in roles_seen:
issue(diagnostics, "warning", f"$.process.nodes[{index}].raci", "executable node has no Accountable (A)")
decision_outcomes: dict[str, list[dict[str, Any]]] = {}
for index, edge in enumerate(edges):
if not isinstance(edge, dict):
continue
source = edge.get("sourceId")
target = edge.get("targetId")
if source not in node_ids:
issue(diagnostics, "error", f"$.process.edges[{index}].sourceId", "source node does not exist")
else:
outgoing[source] += 1
if target not in node_ids:
issue(diagnostics, "error", f"$.process.edges[{index}].targetId", "target node does not exist")
else:
incoming[target] += 1
if source in node_ids and node_types.get(source) == "decision":
decision_outcomes.setdefault(source, []).append(edge)
if not any(node_types.get(node_id) == "start" for node_id in node_ids):
issue(diagnostics, "error", "$.process.nodes", "process has no start node")
if not any(node_types.get(node_id) == "end" for node_id in node_ids):
issue(diagnostics, "error", "$.process.nodes", "process has no end node")
for node_id in node_ids:
if incoming[node_id] == 0 and outgoing[node_id] == 0 and len(node_ids) > 1:
issue(diagnostics, "warning", f"$.process.nodes[id={node_id}]", "node is isolated")
if node_types.get(node_id) == "decision":
branches = decision_outcomes.get(node_id, [])
if len(branches) < 2:
issue(diagnostics, "error", f"$.process.nodes[id={node_id}]", "decision requires at least two outgoing branches")
for branch in branches:
if not str(branch.get("label", "")).strip():
issue(diagnostics, "error", f"$.process.edges[id={branch.get('id', '?')}].label", "decision branch requires a label")
layout = doc.get("layout") if isinstance(doc.get("layout"), dict) else {}
if layout.get("processId") != process.get("id"):
issue(diagnostics, "error", "$.layout.processId", "layout process reference does not match process.id")
for index, item in enumerate(layout.get("items", [])):
if isinstance(item, dict) and item.get("efdId") not in node_ids:
issue(diagnostics, "error", f"$.layout.items[{index}].efdId", "layout node reference does not exist")
for index, edge in enumerate(layout.get("edges", [])):
if isinstance(edge, dict) and edge.get("efdId") not in edge_ids:
issue(diagnostics, "error", f"$.layout.edges[{index}].efdId", "layout edge reference does not exist")
mapping = doc.get("drawioMapping") if isinstance(doc.get("drawioMapping"), dict) else {}
if mapping.get("processId") != process.get("id"):
issue(diagnostics, "error", "$.drawioMapping.processId", "draw.io mapping process reference does not match process.id")
valid_efd_ids = node_ids | edge_ids
for index, cell in enumerate(mapping.get("cells", [])):
if isinstance(cell, dict) and cell.get("efdId") not in valid_efd_ids:
issue(diagnostics, "error", f"$.drawioMapping.cells[{index}].efdId", "draw.io cell reference does not exist")
def main() -> int:
parser = argparse.ArgumentParser(description="Validate an EW AI EFD 2.1 JSON file")
parser.add_argument("file", type=Path)
parser.add_argument("--schema", type=Path, default=SCHEMA_PATH)
args = parser.parse_args()
diagnostics: list[dict[str, str]] = []
try:
doc = json.loads(args.file.read_text(encoding="utf-8"))
schema = json.loads(args.schema.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as error:
result = {"valid": False, "errors": 1, "warnings": 0, "diagnostics": [{"severity": "error", "path": "$", "message": str(error)}]}
print(json.dumps(result, ensure_ascii=False, indent=2))
return 1
if not isinstance(doc, dict):
issue(diagnostics, "error", "$", "root must be an object")
else:
schema_validate(doc, schema, schema, "$", diagnostics)
semantic_validate(doc, diagnostics)
errors = sum(item["severity"] == "error" for item in diagnostics)
warnings = sum(item["severity"] == "warning" for item in diagnostics)
print(json.dumps({"valid": errors == 0, "errors": errors, "warnings": warnings, "diagnostics": diagnostics}, ensure_ascii=False, indent=2))
return 0 if errors == 0 else 1
if __name__ == "__main__":
sys.exit(main())
SHA-256: 7177ba2985003a5c49e1ad03f75ea66d112063b5f0c46b96b8ab2ae82ebcd791