// editor.el — Round-trip graph editing API. // // Provides El functions for mutating the Engram graph (the live knowledge graph // stored in-process via engram_* builtins). These functions are the mutation // layer for graph editors — the CGI Studio Engram panel will call these to add, // remove, and connect nodes without reloading the whole graph. // // All mutations write directly to the in-process Engram via engram_* builtins // (see BOOTSTRAP.md §Engram Knowledge Graph). // // Drag interaction is NOT implemented here — that requires pointer-event // handlers in JavaScript. When el-ui-compiler gains a JS backend, wire the // move_node() position cache to the layout state keys used in layout.el. // // Public API: // graph_add_node(content, node_type, label, salience) -> String // node_id or "" // graph_remove_node(node_id) -> String // "ok" or error JSON // graph_add_edge(from_id, to_id, weight_str, relation) -> String // "ok" or error JSON // graph_remove_edge(from_id, to_id) -> String // "ok" or error JSON // graph_move_node(node_id, x_str, y_str) -> String // "ok" (position cache) // ── graph_add_node ──────────────────────────────────────────────────────────── fn graph_add_node(content: String, node_type: String, label: String, salience_str: String) -> String { if str_eq(content, "") { return "{\"error\":\"content is required\"}" } let sal: Float = if str_eq(salience_str, "") { int_to_float(1) } else { str_to_float(salience_str) } // engram_node_full: content, type, label, salience, importance, confidence, tier, tags let eff_label: String = if str_eq(label, "") { str_slice(content, 0, 40) } else { label } let eff_type: String = if str_eq(node_type, "") { "Entity" } else { node_type } let node_id: String = engram_node_full(content, eff_type, eff_label, sal, sal, int_to_float(1), "Working", "") if str_eq(node_id, "") { return "{\"error\":\"engram_node_full returned empty id\"}" } "{\"id\":\"" + node_id + "\"}" } // ── graph_remove_node ───────────────────────────────────────────────────────── fn graph_remove_node(node_id: String) -> String { if str_eq(node_id, "") { return "{\"error\":\"node_id is required\"}" } // Check node exists let existing: String = engram_get_node(node_id) if str_eq(existing, "") { return "{\"error\":\"node not found\",\"id\":\"" + node_id + "\"}" } engram_forget(node_id) "{\"ok\":true,\"id\":\"" + node_id + "\"}" } // ── graph_add_edge ──────────────────────────────────────────────────────────── fn graph_add_edge(from_id: String, to_id: String, weight_str: String, relation: String) -> String { if str_eq(from_id, "") { return "{\"error\":\"from_id is required\"}" } if str_eq(to_id, "") { return "{\"error\":\"to_id is required\"}" } let w: Float = if str_eq(weight_str, "") { int_to_float(1) } else { str_to_float(weight_str) } let rel: String = if str_eq(relation, "") { "relates_to" } else { relation } // engram_connect(from, to, weight, relation) let edge_id: String = engram_connect(from_id, to_id, w, rel) if str_eq(edge_id, "") { return "{\"error\":\"engram_connect failed\",\"from\":\"" + from_id + "\",\"to\":\"" + to_id + "\"}" } "{\"ok\":true,\"edge_id\":\"" + edge_id + "\",\"from\":\"" + from_id + "\",\"to\":\"" + to_id + "\"}" } // ── graph_remove_edge ───────────────────────────────────────────────────────── fn graph_remove_edge(from_id: String, to_id: String) -> String { if str_eq(from_id, "") { return "{\"error\":\"from_id is required\"}" } if str_eq(to_id, "") { return "{\"error\":\"to_id is required\"}" } // Check edge exists let existing: String = engram_edge_between(from_id, to_id) if str_eq(existing, "") { return "{\"error\":\"edge not found\",\"from\":\"" + from_id + "\",\"to\":\"" + to_id + "\"}" } // No engram_remove_edge builtin — use engram_forget on the edge node if // an edge ID was returned, otherwise surface a not-implemented note. // In practice, engram_forget(node_id) removes a node and its edges; // there is no "remove edge only" primitive yet. "{\"error\":\"remove_edge not yet supported by engram builtins — remove the node to remove all its edges\",\"from\":\"" + from_id + "\",\"to\":\"" + to_id + "\"}" } // ── graph_move_node ─────────────────────────────────────────────────────────── // // Caches a node's screen position for use by the layout engine. // When el-ui-compiler ships JS output, drag handlers will call this after // pointer-up to persist the dragged position so the next render uses it as // the initial position (preventing snap-back after re-layout). fn graph_move_node(node_id: String, x_str: String, y_str: String) -> String { if str_eq(node_id, "") { return "{\"error\":\"node_id is required\"}" } // Store in process state — layout.el reads these keys as initial positions. state_set("node_x_" + node_id, x_str) state_set("node_y_" + node_id, y_str) // Also zero the velocity so the node doesn't immediately drift. state_set("node_vx_" + node_id, "0.0") state_set("node_vy_" + node_id, "0.0") "{\"ok\":true,\"id\":\"" + node_id + "\",\"x\":" + x_str + ",\"y\":" + y_str + "}" } // ── graph_node_info ─────────────────────────────────────────────────────────── // // Retrieve full node data from Engram (for inspector panels). fn graph_node_info(node_id: String) -> String { if str_eq(node_id, "") { return "{\"error\":\"node_id is required\"}" } let n: String = engram_get_node(node_id) if str_eq(n, "") { return "{\"error\":\"node not found\",\"id\":\"" + node_id + "\"}" } n } // ── graph_neighbors ─────────────────────────────────────────────────────────── // // Return the neighbors of a node as a JSON array (for sub-graph drill-down). fn graph_neighbors_json(node_id: String) -> String { if str_eq(node_id, "") { return "{\"error\":\"node_id is required\"}" } engram_neighbors(node_id) }