Files
el/ui/vessels/el-graph/src/editor.el

139 lines
6.9 KiB
EmacsLisp

// 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)
}