// arbor-diagram — diagram intermediate representation (AST + IR). // // Where arbor-core supplies the *.arbor source-language model — Mermaid-safe // IDs, ArborShape strings, ArborEdgeKind strings, and the lowered "diagram- // form" map — arbor-diagram exposes the same lowered model as the canonical // IR for downstream serializers (arbor-render and any future Mermaid-style // emitter). The two vessels overlap by design: arbor-core is responsible for // *naming* the schema; arbor-diagram is responsible for *building* values // against it. // // The Rust crate ships small AST builder structs (`DiagramNode::new`, // `DiagramEdge::with_label`, `DiagramGraph::add_node`). El has no method // chaining, no Default::default(), no enum types. The El idiom is a stack // of immutable maps with explicit constructor + with_* helpers that take // the value and return a freshly-allocated map. // // Public surface: // make_node(id, label) → DiagramNode // with_shape(node, shape) → DiagramNode // with_sublabel(node, sublabel) → DiagramNode // with_style(node, fill, stroke, color) → DiagramNode // // make_edge(from, to) → DiagramEdge // with_label(edge, label) // with_line(edge, line) // "solid"/"dashed"/"dotted"/"thick" // with_arrow(edge, arrow) // "forward"/"backward"/"both"/"none" // // make_group(id, label) → DiagramGroup // with_node(group, node_id) // with_nodes(group, [node_id]) // with_direction(group, dir) // // make_graph(title) → DiagramGraph // with_direction(graph, dir) // graph_add_node(graph, node) → DiagramGraph // graph_add_edge(graph, edge) → DiagramGraph // graph_add_group(graph, group) → DiagramGraph // graph_node(graph, id) → DiagramNode | empty map // // Shape vocabulary (lowered): see arbor-core. The local copy here mirrors // the table in arbor-core/src/main.el so this vessel is hermetic. // ── NodeShape vocabulary ──────────────────────────────────────────────────── fn node_shape_rectangle() -> String { "rectangle" } fn node_shape_rounded_rect() -> String { "rounded_rect" } fn node_shape_stadium() -> String { "stadium" } fn node_shape_cylinder() -> String { "cylinder" } fn node_shape_diamond() -> String { "diamond" } fn node_shape_parallelogram() -> String { "parallelogram" } fn node_shape_database() -> String { "database" } fn node_shape_subroutine() -> String { "subroutine" } fn node_shape_valid(s: String) -> Bool { if str_eq(s, "rectangle") { return true } if str_eq(s, "rounded_rect") { return true } if str_eq(s, "stadium") { return true } if str_eq(s, "cylinder") { return true } if str_eq(s, "diamond") { return true } if str_eq(s, "parallelogram") { return true } if str_eq(s, "database") { return true } if str_eq(s, "subroutine") { return true } false } // ── EdgeLine vocabulary ───────────────────────────────────────────────────── fn edge_line_solid() -> String { "solid" } fn edge_line_dashed() -> String { "dashed" } fn edge_line_dotted() -> String { "dotted" } fn edge_line_thick() -> String { "thick" } fn edge_line_valid(s: String) -> Bool { if str_eq(s, "solid") { return true } if str_eq(s, "dashed") { return true } if str_eq(s, "dotted") { return true } if str_eq(s, "thick") { return true } false } // ── EdgeArrow vocabulary ──────────────────────────────────────────────────── fn edge_arrow_forward() -> String { "forward" } fn edge_arrow_backward() -> String { "backward" } fn edge_arrow_both() -> String { "both" } fn edge_arrow_none() -> String { "none" } fn edge_arrow_valid(s: String) -> Bool { if str_eq(s, "forward") { return true } if str_eq(s, "backward") { return true } if str_eq(s, "both") { return true } if str_eq(s, "none") { return true } false } // ── Direction vocabulary ──────────────────────────────────────────────────── fn direction_top_down() -> String { "top-down" } fn direction_left_right() -> String { "left-right" } fn direction_right_left() -> String { "right-left" } fn direction_bottom_up() -> String { "bottom-up" } fn direction_valid(s: String) -> Bool { if str_eq(s, "top-down") { return true } if str_eq(s, "left-right") { return true } if str_eq(s, "right-left") { return true } if str_eq(s, "bottom-up") { return true } false } // ── DiagramNode ───────────────────────────────────────────────────────────── fn make_node(id: String, label: String) -> Map { { "id": id, "label": label, "sublabel": "", "shape": "rectangle", "style_fill": "", "style_stroke": "", "style_color": "" } } fn with_shape(node: Map, shape: String) -> Map { { "id": node["id"], "label": node["label"], "sublabel": node["sublabel"], "shape": shape, "style_fill": node["style_fill"], "style_stroke": node["style_stroke"], "style_color": node["style_color"] } } fn with_sublabel(node: Map, sublabel: String) -> Map { { "id": node["id"], "label": node["label"], "sublabel": sublabel, "shape": node["shape"], "style_fill": node["style_fill"], "style_stroke": node["style_stroke"], "style_color": node["style_color"] } } fn with_style(node: Map, fill: String, stroke: String, color: String) -> Map { { "id": node["id"], "label": node["label"], "sublabel": node["sublabel"], "shape": node["shape"], "style_fill": fill, "style_stroke": stroke, "style_color": color } } // ── DiagramEdge ───────────────────────────────────────────────────────────── fn make_edge(from: String, to: String) -> Map { { "from": from, "to": to, "label": "", "line": "solid", "arrow": "forward" } } fn with_label(edge: Map, label: String) -> Map { { "from": edge["from"], "to": edge["to"], "label": label, "line": edge["line"], "arrow": edge["arrow"] } } fn with_line(edge: Map, line: String) -> Map { { "from": edge["from"], "to": edge["to"], "label": edge["label"], "line": line, "arrow": edge["arrow"] } } fn with_arrow(edge: Map, arrow: String) -> Map { { "from": edge["from"], "to": edge["to"], "label": edge["label"], "line": edge["line"], "arrow": arrow } } // ── DiagramGroup ──────────────────────────────────────────────────────────── fn make_group(id: String, label: String) -> Map { let empty: [String] = native_list_empty() { "id": id, "label": label, "node_ids": empty, "direction": "" } } fn with_node(group: Map, node_id: String) -> Map { let cur: [String] = group["node_ids"] let next: [String] = native_list_append(cur, node_id) { "id": group["id"], "label": group["label"], "node_ids": next, "direction": group["direction"] } } fn with_nodes(group: Map, ids: [String]) -> Map { let cur: [String] = group["node_ids"] let n: Int = el_list_len(ids) let i = 0 while i < n { let cur = native_list_append(cur, get(ids, i)) let i = i + 1 } { "id": group["id"], "label": group["label"], "node_ids": cur, "direction": group["direction"] } } fn with_group_direction(group: Map, dir: String) -> Map { { "id": group["id"], "label": group["label"], "node_ids": group["node_ids"], "direction": dir } } // ── DiagramGraph ──────────────────────────────────────────────────────────── fn make_graph(title: String) -> Map { let empty_n: [Map] = native_list_empty() let empty_e: [Map] = native_list_empty() let empty_g: [Map] = native_list_empty() { "title": title, "direction": "top-down", "nodes": empty_n, "edges": empty_e, "groups": empty_g } } fn with_direction(graph: Map, dir: String) -> Map { { "title": graph["title"], "direction": dir, "nodes": graph["nodes"], "edges": graph["edges"], "groups": graph["groups"] } } fn graph_add_node(graph: Map, node: Map) -> Map { let cur: [Map] = graph["nodes"] let next: [Map] = native_list_append(cur, node) { "title": graph["title"], "direction": graph["direction"], "nodes": next, "edges": graph["edges"], "groups": graph["groups"] } } fn graph_add_edge(graph: Map, edge: Map) -> Map { let cur: [Map] = graph["edges"] let next: [Map] = native_list_append(cur, edge) { "title": graph["title"], "direction": graph["direction"], "nodes": graph["nodes"], "edges": next, "groups": graph["groups"] } } fn graph_add_group(graph: Map, group: Map) -> Map { let cur: [Map] = graph["groups"] let next: [Map] = native_list_append(cur, group) { "title": graph["title"], "direction": graph["direction"], "nodes": graph["nodes"], "edges": graph["edges"], "groups": next } } // Find a node by id. Returns an empty map (no "id" field) when not present. fn graph_node(graph: Map, id: String) -> Map { let nodes: [Map] = graph["nodes"] let n: Int = el_list_len(nodes) let i = 0 while i < n { let nd: Map = get(nodes, i) let nid: String = nd["id"] if str_eq(nid, id) { return nd } let i = i + 1 } let empty: Map = el_map_new(0) empty } // ── Smoke test ────────────────────────────────────────────────────────────── fn fail(label: String, got: String, want: String) -> Int { println("FAIL " + label + " got=[" + got + "] want=[" + want + "]") state_set("smoke_failures", "1") 0 } fn check_eq(label: String, got: String, want: String) -> Int { if got == want { println("ok " + label + " = " + got) return 1 } fail(label, got, want) } // Vocabulary self-checks check_eq("shape rectangle valid", bool_to_str(node_shape_valid("rectangle")), "true") check_eq("shape hexagon invalid", bool_to_str(node_shape_valid("hexagon")), "false") check_eq("line dashed valid", bool_to_str(edge_line_valid("dashed")), "true") check_eq("arrow both valid", bool_to_str(edge_arrow_valid("both")), "true") check_eq("dir top-down valid", bool_to_str(direction_valid("top-down")), "true") // Node builder let n0: Map = make_node("svc", "Service") check_eq("node default shape", n0["shape"], "rectangle") check_eq("node default sublabel empty", n0["sublabel"], "") let n1: Map = with_shape(n0, "cylinder") check_eq("node with_shape", n1["shape"], "cylinder") check_eq("node id preserved", n1["id"], "svc") let n2: Map = with_sublabel(n1, "v0.1.0") check_eq("node with_sublabel", n2["sublabel"], "v0.1.0") let n3: Map = with_style(n2, "#0052A0", "#0052A0", "#ffffff") check_eq("node style fill", n3["style_fill"], "#0052A0") check_eq("node style color", n3["style_color"], "#ffffff") // Edge builder let e0: Map = make_edge("a", "b") check_eq("edge default line", e0["line"], "solid") check_eq("edge default arrow", e0["arrow"], "forward") let e1: Map = with_line(e0, "dashed") let e2: Map = with_arrow(e1, "both") let e3: Map = with_label(e2, "calls") check_eq("edge line", e3["line"], "dashed") check_eq("edge arrow", e3["arrow"], "both") check_eq("edge label", e3["label"], "calls") // Group builder let g0: Map = make_group("core", "Application Core") let g1: Map = with_node(g0, "api") let g2: Map = with_node(g1, "svc") let ids2: [String] = g2["node_ids"] check_eq("group with two nodes", int_to_str(el_list_len(ids2)), "2") let g3: Map = make_group("infra", "Infrastructure") let extras: [String] = native_list_empty() let extras = native_list_append(extras, "db") let extras = native_list_append(extras, "cache") let g4: Map = with_nodes(g3, extras) let ids4: [String] = g4["node_ids"] check_eq("group with_nodes appends", int_to_str(el_list_len(ids4)), "2") // Graph builder + lookup let G0: Map = make_graph("System") let G1: Map = with_direction(G0, "left-right") let G2: Map = graph_add_node(G1, n3) let nb: Map = make_node("b", "Backend") let G3: Map = graph_add_node(G2, nb) let G4: Map = graph_add_edge(G3, e3) let G5: Map = graph_add_group(G4, g4) check_eq("graph title", G5["title"], "System") check_eq("graph direction", G5["direction"], "left-right") let gn: [Map] = G5["nodes"] let ge: [Map] = G5["edges"] let gg: [Map] = G5["groups"] check_eq("graph nodes count", int_to_str(el_list_len(gn)), "2") check_eq("graph edges count", int_to_str(el_list_len(ge)), "1") check_eq("graph groups count", int_to_str(el_list_len(gg)), "1") let found: Map = graph_node(G5, "svc") check_eq("graph_node found", found["id"], "svc") let missing: Map = graph_node(G5, "nonexistent") let missing_id: String = missing["id"] if str_len(missing_id) == 0 { println("ok graph_node missing returns empty") } else { println("FAIL graph_node missing returned: " + missing_id) state_set("smoke_failures", "1") } println("") let failures: String = state_get("smoke_failures") if str_eq(failures, "1") { println("arbor-diagram: FAILED") exit_program(1) } else { println("arbor-diagram: ok") }