From 0b480cfb6b48be7dfd31c8c2c57e9451d77687a5 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Wed, 29 Apr 2026 04:39:40 -0500 Subject: [PATCH] =?UTF-8?q?El=20data=20studio:=2010-loop=20improvement=20p?= =?UTF-8?q?ass=20=E2=80=94=20full=20Engram=20DB=20explorer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full-featured terminal explorer for the Engram knowledge graph built natively in El. Features: - ANSI-colored TUI with box-drawing borders and salience bars - All API endpoints: stats, nodes by type/tier, search, edges, spreading activation, node detail with neighbor traversal - Text report export via fs_write - Offline/unreachable mode with helpful startup messages - Interactive mode command reference - ENGRAM_URL env var for connecting to non-default servers - Uses json_get_raw for nested JSON object traversal --- ql/studio/studio.el | 787 ++++++++++++++++++++++++++++++ ql/test/field_test.el | 243 +++++++++ ql/test/language_features_test.el | 103 ++++ ql/test/llm_test.el | 10 + 4 files changed, 1143 insertions(+) create mode 100644 ql/studio/studio.el create mode 100644 ql/test/field_test.el create mode 100644 ql/test/language_features_test.el create mode 100644 ql/test/llm_test.el diff --git a/ql/studio/studio.el b/ql/studio/studio.el new file mode 100644 index 0000000..b098dac --- /dev/null +++ b/ql/studio/studio.el @@ -0,0 +1,787 @@ +// ╔══════════════════════════════════════════════════════════════════════════════╗ +// ║ ENGRAM DATA STUDIO — Native El Application ║ +// ║ Full-featured terminal explorer for the Engram knowledge graph ║ +// ╚══════════════════════════════════════════════════════════════════════════════╝ +// +// Usage: el run-file studio.el +// ENGRAM_URL=http://host:port el run-file studio.el +// +// Requires: Engram server running (default: http://localhost:8340) + +// ── Configuration ───────────────────────────────────────────────────────────── + +fn get_base_url() -> String { + let e: String = env("ENGRAM_URL") + if e == "" { + return "http://localhost:8340" + } + return e +} + +fn get_report_path() -> String { + let e: String = env("ENGRAM_REPORT") + if e == "" { + return "/tmp/engram-studio-report.txt" + } + return e +} + +// ── Box-drawing helpers ──────────────────────────────────────────────────────── + +fn repeat_str(s: String, n: Int) -> String { + let result: String = "" + let i: Int = 0 + while i < n { + let result: String = result + s + let i: Int = i + 1 + } + return result +} + +fn hline(width: Int) -> String { + return repeat_str("─", width) +} + +fn box_top(width: Int) -> String { + return "┌" + hline(width) + "┐" +} + +fn box_bot(width: Int) -> String { + return "└" + hline(width) + "┘" +} + +fn box_row(content: String, width: Int) -> String { + let padded: String = str_pad_right(content, width, " ") + return "│" + padded + "│" +} + +fn double_top(width: Int) -> String { + return "╔" + repeat_str("═", width) + "╗" +} + +fn double_bot(width: Int) -> String { + return "╚" + repeat_str("═", width) + "╝" +} + +fn double_row(content: String, width: Int) -> String { + let padded: String = str_pad_right(content, width, " ") + return "║" + padded + "║" +} + +fn section_header(title: String) -> String { + println("") + println(color_bold(box_top(62))) + println(color_bold(box_row(" " + title, 62))) + println(color_bold(box_bot(62))) + return "" +} + +fn divider() -> String { + println(color_dim(" " + repeat_str("·", 58))) + return "" +} + +// ── API access ──────────────────────────────────────────────────────────────── + +fn api_get(path: String) -> String { + let url: String = get_base_url() + path + let resp: String = http_get(url) + if str_starts_with(resp, "{\"error\"") { + return "" + } + return resp +} + +fn api_post(path: String, body: String) -> String { + let url: String = get_base_url() + path + let resp: String = http_post(url, body) + if str_starts_with(resp, "{\"error\"") { + return "" + } + return resp +} + +// ── Safe JSON field extractors ───────────────────────────────────────────────── +// These handle both raw JSON strings and parsed Value::Struct objects. + +fn safe_str(node: String, key: String) -> String { + return json_get_string(node, key) +} + +fn safe_int(node: String, key: String) -> Int { + return json_get_int(node, key) +} + +fn safe_float(node: String, key: String) -> Float { + return json_get_float(node, key) +} + +// ── Formatting helpers ──────────────────────────────────────────────────────── + +fn short_id(full_id: String) -> String { + if str_len(full_id) >= 8 { + return str_slice(full_id, 0, 8) + } + return full_id +} + +fn format_bytes(n: Int) -> String { + if n < 1024 { + return int_to_str(n) + " B" + } + if n < 1048576 { + let kb: Int = n / 1024 + return int_to_str(kb) + " KB" + } + let mb: Int = n / 1048576 + return int_to_str(mb) + " MB" +} + +fn format_timestamp(ms: Int) -> String { + if ms <= 0 { + return "—" + } + let secs: Int = ms / 1000 + let ts: Int = time_from_parts(secs, 0, "UTC") + return time_format(ts, "ISO") +} + +fn content_preview(content: String, max_len: Int) -> String { + let n: Int = str_len(content) + if n == 0 { + return color_dim("(no content)") + } + if n <= max_len { + return content + } + return str_slice(content, 0, max_len) + color_dim("…") +} + +fn salience_bar(salience: Float) -> String { + let pct: Float = salience * 10.0 + let filled: Int = float_to_int(pct) + let bar: String = repeat_str("█", filled) + repeat_str("░", 10 - filled) + if salience >= 0.7 { + return color_green(bar) + } + if salience >= 0.4 { + return color_yellow(bar) + } + return color_dim(bar) +} + +fn tier_badge(tier: String) -> String { + if tier == "Semantic" { + return color_cyan("[Semantic ]") + } + if tier == "Episodic" { + return color_yellow("[Episodic ]") + } + if tier == "Working" { + return color_green("[Working ]") + } + if tier == "Procedural" { + return color_bold("[Procedural]") + } + return "[" + str_pad_right(tier, 10, " ") + "]" +} + +fn type_badge(node_type: String) -> String { + if node_type == "Memory" { + return color_cyan("Memory ") + } + if node_type == "Concept" { + return color_green("Concept ") + } + if node_type == "Event" { + return color_yellow("Event ") + } + if node_type == "Entity" { + return color_bold("Entity ") + } + if node_type == "Process" { + return color_cyan("Process ") + } + if node_type == "InternalState" { + return color_dim("IntState ") + } + return str_pad_right(node_type, 10, " ") +} + +// ── Node renderer ───────────────────────────────────────────────────────────── + +fn render_node_row(node_json: String, idx: Int) -> String { + let id: String = short_id(safe_str(node_json, "id")) + let label: String = safe_str(node_json, "label") + let node_type: String = safe_str(node_json, "node_type") + let tier: String = safe_str(node_json, "tier") + let salience: Float = safe_float(node_json, "salience") + + let label_col: String = str_pad_right(label, 36, " ") + let sal_str: String = format_float(salience, 3) + + let num: String = str_pad_left(int_to_str(idx + 1), 3, " ") + ". " + return " " + num + color_dim(id) + " " + label_col + " " + type_badge(node_type) + " " + sal_str +} + +fn render_node_detail(node_json: String) -> String { + let id: String = safe_str(node_json, "id") + let label: String = safe_str(node_json, "label") + let node_type: String = safe_str(node_json, "node_type") + let tier: String = safe_str(node_json, "tier") + let salience: Float = safe_float(node_json, "salience") + let importance: Float = safe_float(node_json, "importance") + let confidence: Float = safe_float(node_json, "confidence") + let created: Int = safe_int(node_json, "created_at") + let updated: Int = safe_int(node_json, "updated_at") + + println(" " + color_bold("ID: ") + id) + println(" " + color_bold("Label: ") + color_cyan(label)) + println(" " + color_bold("Type: ") + node_type) + println(" " + color_bold("Tier: ") + tier_badge(tier)) + println(" " + color_bold("Salience: ") + salience_bar(salience) + " " + format_float(salience, 4)) + println(" " + color_bold("Importance: ") + format_float(importance, 4)) + println(" " + color_bold("Confidence: ") + format_float(confidence, 4)) + println(" " + color_bold("Created: ") + color_dim(format_timestamp(created))) + println(" " + color_bold("Updated: ") + color_dim(format_timestamp(updated))) + return "" +} + +// ── Section: Stats Dashboard ────────────────────────────────────────────────── + +fn show_stats(report: String) -> String { + section_header("Database Statistics") + let stats_json: String = api_get("/api/stats") + if stats_json == "" { + println(" " + color_red("Error: could not reach Engram server")) + println(" Make sure the server is running: engram-server --data-dir ") + return report + } + + let node_count: Int = safe_int(stats_json, "node_count") + let edge_count: Int = safe_int(stats_json, "edge_count") + let avg_sal: Float = safe_float(stats_json, "avg_salience") + let db_bytes: Int = safe_int(stats_json, "db_size_bytes") + + println(" " + color_bold("Nodes: ") + color_cyan(int_to_str(node_count))) + println(" " + color_bold("Edges: ") + color_cyan(int_to_str(edge_count))) + println(" " + color_bold("Avg Salience: ") + format_float(avg_sal, 4)) + println(" " + color_bold("DB Size: ") + color_dim(format_bytes(db_bytes))) + + let new_report: String = report + + "\n=== Database Statistics ===\n" + + "Nodes: " + int_to_str(node_count) + "\n" + + "Edges: " + int_to_str(edge_count) + "\n" + + "Avg Salience: " + format_float(avg_sal, 4) + "\n" + + "DB Size: " + format_bytes(db_bytes) + "\n" + return new_report +} + +// ── Section: Node Browser ───────────────────────────────────────────────────── + +fn show_nodes(node_type: String, limit: Int, report: String) -> String { + let title: String = "Nodes — type: " + node_type + section_header(title) + + let path: String = "/api/nodes?node_type=" + node_type + "&limit=" + int_to_str(limit) + let json_str: String = api_get(path) + if json_str == "" { + println(" " + color_dim("No nodes found or server unreachable")) + return report + } + + let nodes: List = json_parse(json_str) + let n: Int = list_len(nodes) + if n == 0 { + println(" " + color_dim("(no " + node_type + " nodes)")) + return report + } + + println(" " + color_dim("Showing " + int_to_str(n) + " nodes:")) + println("") + + let header: String = " " + str_pad_right(" # ID Label", 55, " ") + " Type Salience" + println(color_dim(header)) + println(color_dim(" " + repeat_str("─", 70))) + + let i: Int = 0 + let report_section: String = "\n=== " + node_type + " Nodes ===\n" + while i < n { + let node: String = json_stringify(list_get(nodes, i)) + println(render_node_row(node, i)) + let label: String = safe_str(node, "label") + let id: String = short_id(safe_str(node, "id")) + let sal: Float = safe_float(node, "salience") + let report_section: String = report_section + int_to_str(i + 1) + ". [" + id + "] " + label + " (sal=" + format_float(sal, 3) + ")\n" + let i: Int = i + 1 + } + + return report + report_section +} + +// ── Section: Recent Nodes ───────────────────────────────────────────────────── + +fn show_recent(limit: Int, report: String) -> String { + section_header("Recent Nodes (last " + int_to_str(limit) + ")") + let path: String = "/api/nodes?limit=" + int_to_str(limit) + let json_str: String = api_get(path) + if json_str == "" { + println(" " + color_dim("No nodes or server unreachable")) + return report + } + + let nodes: List = json_parse(json_str) + let n: Int = list_len(nodes) + if n == 0 { + println(" " + color_dim("(database is empty)")) + return report + } + + println(" " + color_dim("Showing " + int_to_str(n) + " most recent nodes:")) + println("") + + let header: String = " " + str_pad_right(" # ID Label", 55, " ") + " Type Salience" + println(color_dim(header)) + println(color_dim(" " + repeat_str("─", 70))) + + let i: Int = 0 + let report_section: String = "\n=== Recent Nodes ===\n" + while i < n { + let node: String = json_stringify(list_get(nodes, i)) + println(render_node_row(node, i)) + let label: String = safe_str(node, "label") + let id: String = short_id(safe_str(node, "id")) + let report_section: String = report_section + int_to_str(i + 1) + ". [" + id + "] " + label + "\n" + let i: Int = i + 1 + } + + return report + report_section +} + +// ── Section: Top Salient Nodes ──────────────────────────────────────────────── + +fn show_top_salient(limit: Int, report: String) -> String { + section_header("Top " + int_to_str(limit) + " by Salience") + let path: String = "/api/nodes?limit=" + int_to_str(limit) + "&min_salience=0.0" + let json_str: String = api_get(path) + if json_str == "" { + println(" " + color_dim("No nodes or server unreachable")) + return report + } + + let nodes: List = json_parse(json_str) + let n: Int = list_len(nodes) + if n == 0 { + println(" " + color_dim("(no nodes)")) + return report + } + + println(" " + color_dim("Salience ranking:")) + println("") + + let report_section: String = "\n=== Top Salient Nodes ===\n" + let i: Int = 0 + while i < n { + let node: String = json_stringify(list_get(nodes, i)) + let id: String = short_id(safe_str(node, "id")) + let label: String = safe_str(node, "label") + let sal: Float = safe_float(node, "salience") + let tier: String = safe_str(node, "tier") + let bar: String = salience_bar(sal) + let rank: String = str_pad_left(int_to_str(i + 1), 2, " ") + println(" " + rank + ". " + bar + " " + format_float(sal, 3) + " " + color_dim(id) + " " + str_pad_right(label, 35, " ") + " " + color_dim(tier)) + let report_section: String = report_section + rank + ". " + format_float(sal, 3) + " [" + id + "] " + label + " " + tier + "\n" + let i: Int = i + 1 + } + + return report + report_section +} + +// ── Section: Node Detail ────────────────────────────────────────────────────── + +fn show_node_detail(node_id: String, report: String) -> String { + section_header("Node Detail — " + str_slice(node_id, 0, 8) + "...") + let path: String = "/api/nodes/" + node_id + let json_str: String = api_get(path) + if json_str == "" { + println(" " + color_red("Node not found: " + node_id)) + return report + } + + render_node_detail(json_str) + + // Show neighbors + let nb_path: String = "/api/neighbors/" + node_id + "?depth=2" + let nb_json: String = api_get(nb_path) + if nb_json != "" { + let neighbors: List = json_parse(nb_json) + let nb_n: Int = list_len(neighbors) + if nb_n > 0 { + println("") + println(" " + color_bold("Neighbors (" + int_to_str(nb_n) + "):")) + let j: Int = 0 + while j < nb_n { + let nb: String = json_stringify(list_get(neighbors, j)) + // Use json_get_raw to extract nested objects as JSON strings + let nb_node: String = json_get_raw(nb, "node") + let nb_edge: String = json_get_raw(nb, "edge") + let hops: Int = safe_int(nb, "hops") + + let nb_label: String = safe_str(nb_node, "label") + let nb_id: String = short_id(safe_str(nb_node, "id")) + let hop_str: String = str_pad_left(int_to_str(hops), 2, " ") + let relation: String = safe_str(nb_edge, "relation") + + println(" hop " + hop_str + " " + color_dim(nb_id) + " " + color_cyan(relation) + " → " + nb_label) + let j: Int = j + 1 + } + } + } + + return report + "\n=== Node Detail: " + node_id + " ===\n" + "Fetched node details\n" +} + +// ── Section: Search ─────────────────────────────────────────────────────────── + +fn show_search(query: String, limit: Int, report: String) -> String { + section_header("Search: \"" + query + "\"") + let path: String = "/api/search?q=" + query + "&limit=" + int_to_str(limit) + let json_str: String = api_get(path) + if json_str == "" { + println(" " + color_dim("No results or server unreachable")) + return report + } + + let nodes: List = json_parse(json_str) + let n: Int = list_len(nodes) + println(" " + color_bold("Found " + int_to_str(n) + " results:")) + println("") + + if n == 0 { + println(" " + color_dim("(no matches)")) + return report + "\n=== Search: " + query + " ===\nNo results\n" + } + + let report_section: String = "\n=== Search: " + query + " ===\n" + int_to_str(n) + " results:\n" + let i: Int = 0 + while i < n { + let node: String = json_stringify(list_get(nodes, i)) + println(render_node_row(node, i)) + let label: String = safe_str(node, "label") + let report_section: String = report_section + int_to_str(i + 1) + ". " + label + "\n" + let i: Int = i + 1 + } + + return report + report_section +} + +// ── Section: Tier Browser ───────────────────────────────────────────────────── + +fn show_nodes_by_tier(tier: String, limit: Int, report: String) -> String { + let title: String = "Nodes — tier: " + tier + section_header(title) + + let path: String = "/api/nodes?tier=" + tier + "&limit=" + int_to_str(limit) + let json_str: String = api_get(path) + if json_str == "" { + println(" " + color_dim("No nodes or server unreachable")) + return report + } + + let nodes: List = json_parse(json_str) + let n: Int = list_len(nodes) + if n == 0 { + println(" " + color_dim("(no " + tier + " tier nodes)")) + return report + } + + println(" " + tier_badge(tier) + " " + color_dim("Showing " + int_to_str(n) + " nodes")) + println("") + + let i: Int = 0 + let report_section: String = "\n=== " + tier + " Tier Nodes ===\n" + while i < n { + let node: String = json_stringify(list_get(nodes, i)) + println(render_node_row(node, i)) + let label: String = safe_str(node, "label") + let id: String = short_id(safe_str(node, "id")) + let report_section: String = report_section + "[" + id + "] " + label + "\n" + let i: Int = i + 1 + } + + return report + report_section +} + +// ── Section: Spreading Activation ──────────────────────────────────────────── + +fn show_activation(seed_id: String, limit: Int, report: String) -> String { + section_header("Spreading Activation — seed: " + str_slice(seed_id, 0, 8) + "...") + let path: String = "/api/activate?seeds=" + seed_id + "&limit=" + int_to_str(limit) + "&depth=3" + let json_str: String = api_get(path) + if json_str == "" { + println(" " + color_dim("No activation results or server unreachable")) + return report + } + + // Use json_get_raw to get the "results" array as a JSON string, then parse it + let results_raw: String = json_get_raw(json_str, "results") + let results: List = json_parse(results_raw) + let n: Int = list_len(results) + if n == 0 { + println(" " + color_dim("(no spreading activation results — check seed ID)")) + return report + } + + println(" " + color_bold("Activated " + int_to_str(n) + " nodes:")) + println("") + + let header: String = " " + str_pad_right(" # ID Label", 52, " ") + " Strength Hops" + println(color_dim(header)) + println(color_dim(" " + repeat_str("─", 68))) + + let report_section: String = "\n=== Activation from " + str_slice(seed_id, 0, 8) + " ===\n" + let i: Int = 0 + while i < n { + let result: String = json_stringify(list_get(results, i)) + // Use json_get_raw to extract nested node object as a JSON string + let node: String = json_get_raw(result, "node") + let strength: Float = safe_float(result, "activation_strength") + let hops: Int = safe_int(result, "hops") + + let id: String = short_id(safe_str(node, "id")) + let label: String = safe_str(node, "label") + + let rank: String = str_pad_left(int_to_str(i + 1), 3, " ") + let strength_bar: String = salience_bar(strength) + println(" " + rank + ". " + color_dim(id) + " " + str_pad_right(label, 36, " ") + " " + format_float(strength, 3) + " " + color_dim("hops:" + int_to_str(hops))) + let report_section: String = report_section + rank + ". [" + id + "] " + label + " strength=" + format_float(strength, 3) + " hops=" + int_to_str(hops) + "\n" + let i: Int = i + 1 + } + + return report + report_section +} + +// ── Section: Edge Statistics ────────────────────────────────────────────────── + +fn show_edges(limit: Int, report: String) -> String { + section_header("Edge Explorer (sample of " + int_to_str(limit) + ")") + let path: String = "/api/edges?limit=" + int_to_str(limit) + let json_str: String = api_get(path) + if json_str == "" { + println(" " + color_dim("No edges or server unreachable")) + return report + } + + let edges: List = json_parse(json_str) + let n: Int = list_len(edges) + if n == 0 { + println(" " + color_dim("(no edges in database)")) + return report + } + + println(" " + color_dim("Showing " + int_to_str(n) + " edges:")) + println("") + + let header: String = " " + str_pad_right(" # From → To Relation", 56, " ") + " Weight" + println(color_dim(header)) + println(color_dim(" " + repeat_str("─", 68))) + + let report_section: String = "\n=== Edges ===\n" + let i: Int = 0 + while i < n { + let edge: String = json_stringify(list_get(edges, i)) + let from_id: String = short_id(safe_str(edge, "from_id")) + let to_id: String = short_id(safe_str(edge, "to_id")) + let relation: String = safe_str(edge, "relation") + let weight: Float = safe_float(edge, "weight") + let rank: String = str_pad_left(int_to_str(i + 1), 3, " ") + + println(" " + rank + ". " + color_dim(from_id) + " " + color_cyan("→") + " " + color_dim(to_id) + " " + color_green(str_pad_right(relation, 20, " ")) + " " + format_float(weight, 3)) + let report_section: String = report_section + from_id + " -[" + relation + "]-> " + to_id + " w=" + format_float(weight, 3) + "\n" + let i: Int = i + 1 + } + + return report + report_section +} + +// ── Section: Knowledge Browser (Concept nodes grouped by tag prefix) ────────── + +fn show_knowledge_browser(report: String) -> String { + section_header("Knowledge Browser — Concept Nodes") + let path: String = "/api/nodes?node_type=Concept&limit=50" + let json_str: String = api_get(path) + if json_str == "" { + println(" " + color_dim("No concept nodes or server unreachable")) + return report + } + + let nodes: List = json_parse(json_str) + let n: Int = list_len(nodes) + if n == 0 { + println(" " + color_dim("(no Concept nodes — these are your domain knowledge anchors)")) + return report + } + + println(" " + color_bold(int_to_str(n) + " concept nodes found:")) + println("") + + let report_section: String = "\n=== Knowledge Browser ===\n" + let i: Int = 0 + while i < n { + let node: String = json_stringify(list_get(nodes, i)) + let id: String = short_id(safe_str(node, "id")) + let label: String = safe_str(node, "label") + let tier: String = safe_str(node, "tier") + let sal: Float = safe_float(node, "salience") + println(" " + color_green("◆") + " " + str_pad_right(label, 40, " ") + " " + color_dim(id) + " " + color_dim(tier) + " " + format_float(sal, 3)) + let report_section: String = report_section + "◆ " + label + " [" + id + "] " + tier + "\n" + let i: Int = i + 1 + } + + return report + report_section +} + +// ── Section: Mode Simulation (interactive mode preview) ────────────────────── + +fn show_mode_menu() -> String { + let _ = section_header("Interactive Mode Preview") + println(" " + color_bold("Available commands (when running interactively):")) + println("") + println(" " + color_cyan("stats") + " — show database statistics") + println(" " + color_cyan("browse ") + " — browse nodes by type") + println(" types: Memory, Concept, Event, Entity, Process, InternalState") + println(" " + color_cyan("tier ") + " — browse nodes by tier") + println(" tiers: Working, Episodic, Semantic, Procedural") + println(" " + color_cyan("search ") + " — full-text search") + println(" " + color_cyan("node ") + " — show node detail + neighbors") + println(" " + color_cyan("activate ") + " — spreading activation from seed") + println(" " + color_cyan("edges") + " — browse edges") + println(" " + color_cyan("top") + " — top nodes by salience") + println(" " + color_cyan("export ") + " — save text report to file") + println(" " + color_cyan("help") + " — show this menu") + println(" " + color_cyan("quit") + " — exit studio") + println("") + println(" " + color_dim("Tip: set ENGRAM_URL=http://host:port to connect to a different server")) + return "" +} + +// ── Export report ───────────────────────────────────────────────────────────── + +fn export_report(content: String, path: String) -> String { + let _ = section_header("Report Export") + let header: String = "ENGRAM DATA STUDIO — Report\n" + + "Generated: " + time_format(time_now_utc(), "ISO") + "\n" + + "Server: " + get_base_url() + "\n" + + repeat_str("=", 60) + "\n" + let ok: Bool = fs_write(path, header + content) + if ok { + println(" " + color_green("✓") + " Report saved to: " + color_bold(path)) + println(" " + color_dim("Size: " + format_bytes(str_len(header + content)))) + } else { + println(" " + color_red("✗") + " Failed to write report to: " + path) + } + return "" +} + +// ── Connectivity check ──────────────────────────────────────────────────────── + +fn check_connection() -> Bool { + let resp: String = api_get("/api/stats") + return resp != "" +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// ── MAIN STUDIO ENTRY POINT ────────────────────────────────────────────────── +// ═══════════════════════════════════════════════════════════════════════════════ + +let base_url: String = get_base_url() +let WIDTH: Int = 64 + +// ── Banner ──────────────────────────────────────────────────────────────────── +println("") +println(color_bold(double_top(WIDTH))) +println(color_bold(double_row(" ███████╗███╗ ██╗ ██████╗ ██████╗ █████╗ ███╗ ███╗", WIDTH))) +println(color_bold(double_row(" ██╔════╝████╗ ██║██╔════╝ ██╔══██╗██╔══██╗████╗ ████║", WIDTH))) +println(color_bold(double_row(" █████╗ ██╔██╗ ██║██║ ███╗██████╔╝███████║██╔████╔██║", WIDTH))) +println(color_bold(double_row(" ██╔══╝ ██║╚██╗██║██║ ██║██╔══██╗██╔══██║██║╚██╔╝██║", WIDTH))) +println(color_bold(double_row(" ███████╗██║ ╚████║╚██████╔╝██║ ██║██║ ██║██║ ╚═╝ ██║", WIDTH))) +println(color_bold(double_row(" ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝", WIDTH))) +println(color_bold(double_row("", WIDTH))) +println(color_bold(double_row(" DATA STUDIO — Native El Application", WIDTH))) +println(color_bold(double_row("", WIDTH))) +println(color_bold(double_bot(WIDTH))) +println("") +println(" " + color_bold("Server: ") + color_cyan(base_url)) +println(" " + color_bold("Version: ") + color_dim("El-native · 10-loop pass")) +println("") + +// ── Connection check ───────────────────────────────────────────────────────── +let connected: Bool = check_connection() +if connected { + println(" " + color_green("●") + " " + color_bold("Connection established")) +} else { + println(" " + color_red("●") + " " + color_bold("Server unreachable")) + println(" " + color_dim("Start the server: ENGRAM_BIND=0.0.0.0:8340 engram-server --data-dir ")) + println(" " + color_dim("Or set ENGRAM_URL environment variable")) + println("") + println(" " + color_yellow("Continuing in offline mode — all sections will show placeholders")) +} + +// ── Run all dashboard sections ───────────────────────────────────────────────── + +let report: String = "" + +// Section 1: Stats +let report: String = show_stats(report) + +// Section 2: Recent nodes +let report: String = show_recent(20, report) + +// Section 3: Top salient +let report: String = show_top_salient(10, report) + +// Section 4: By node type — Memory +let report: String = show_nodes("Memory", 10, report) + +// Section 5: By node type — Concept +let report: String = show_nodes("Concept", 10, report) + +// Section 6: By node type — Event +let report: String = show_nodes("Event", 8, report) + +// Section 7: By node type — Entity +let report: String = show_nodes("Entity", 8, report) + +// Section 8: By node type — Process +let report: String = show_nodes("Process", 8, report) + +// Section 9: Tier — Semantic +let report: String = show_nodes_by_tier("Semantic", 10, report) + +// Section 10: Tier — Episodic +let report: String = show_nodes_by_tier("Episodic", 10, report) + +// Section 11: Knowledge browser (Concept nodes) +let report: String = show_knowledge_browser(report) + +// Section 12: Text search samples +let report: String = show_search("memory", 10, report) +let report: String = show_search("concept", 8, report) + +// Section 13: Edge explorer +let report: String = show_edges(15, report) + +// Section 14: Interactive mode preview +show_mode_menu() + +// ── Export report ────────────────────────────────────────────────────────────── +let report_path: String = get_report_path() +export_report(report, report_path) + +// ── Footer ──────────────────────────────────────────────────────────────────── +println("") +println(color_bold(" " + repeat_str("═", 60))) +println(" " + color_bold("Engram Data Studio") + color_dim(" — session complete")) +println(" " + color_dim(time_format(time_now_utc(), "ISO"))) +println(color_bold(" " + repeat_str("═", 60))) +println("") diff --git a/ql/test/field_test.el b/ql/test/field_test.el new file mode 100644 index 0000000..6003731 --- /dev/null +++ b/ql/test/field_test.el @@ -0,0 +1,243 @@ +// field_test.el — Proof of concept: Hebbian field model +// +// Tests the core mechanics: +// 1. Nodes with 2D semantic positions and temporal coordinates +// 2. Edges with weights that grow via co-activation (Hebb's rule) +// 3. Spreading activation with path strength +// 4. Temporal decay in path strength +// 5. Epistemic confidence = node confidence * path strength +// +// No storage — pure in-memory demonstration using print output. + +// ── Math helpers ────────────────────────────────────────────────────────────── + +fn clamp_f(v: Float, lo: Float, hi: Float) -> Float { + if v < lo { + return lo + } + if v > hi { + return hi + } + return v +} + +// L2 distance squared — no sqrt needed for proximity comparison +fn dist_sq(ax: Float, ay: Float, bx: Float, by: Float) -> Float { + let dx: Float = ax - bx + let dy: Float = ay - by + return dx * dx + dy * dy +} + +// Proximity score: 1 / (1 + dist_sq) — range (0, 1], closer = higher +fn proximity(ax: Float, ay: Float, bx: Float, by: Float) -> Float { + let d: Float = dist_sq(ax, ay, bx, by) + return 1.0 / (1.0 + d) +} + +// Linear temporal decay: 1.0 at t=0, decaying toward 0 +// decay_rate: fraction lost per time unit +fn temporal_decay(age: Float, decay_rate: Float) -> Float { + let d: Float = 1.0 - decay_rate * age + return clamp_f(d, 0.0, 1.0) +} + +// ── Hebbian learning ────────────────────────────────────────────────────────── + +// New edge weight after co-activation event +// w_new = clamp(w + lr * a_i * a_j, 0, 1) +fn hebbian_update(weight: Float, act_i: Float, act_j: Float, lr: Float) -> Float { + let delta: Float = lr * act_i * act_j + return clamp_f(weight + delta, 0.0, 1.0) +} + +// Edge decay between activations +// w_new = w * (1 - decay_rate) +fn edge_decay(weight: Float, decay_rate: Float) -> Float { + return clamp_f(weight * (1.0 - decay_rate), 0.0, 1.0) +} + +// ── Path strength ───────────────────────────────────────────────────────────── + +// Path strength = edge_weight * temporal_decay(node_age) +// This is the confidence qualifier on a retrieval result +fn path_strength(edge_weight: Float, node_age: Float, decay_rate: Float) -> Float { + let td: Float = temporal_decay(node_age, decay_rate) + return edge_weight * td +} + +// Epistemic confidence = node_confidence * path_strength +fn epistemic_confidence(node_confidence: Float, ps: Float) -> Float { + return node_confidence * ps +} + +// ── Simulation ──────────────────────────────────────────────────────────────── + +fn run_test() -> String { + println("=== Engram Field Model — Proof of Concept ===") + println("") + + // ── Nodes ── + // Each node: id, label, semantic position (x, y), temporal coordinate, confidence + // Semantic space: 2D for demonstration + // x=0..1: concrete←→abstract + // y=0..1: negative←→positive valence + + println("── Nodes ──────────────────────────────────────") + println("A: 'patient has fever' pos=(0.2, 0.4) t=100") + println("B: 'influenza' pos=(0.3, 0.5) t=90") + println("C: 'drug interaction warning' pos=(0.6, 0.3) t=10 (old, unactivated)") + println("D: 'ibuprofen' pos=(0.65, 0.3) t=10 (old, unactivated)") + println("") + + // Semantic positions + let ax: Float = 0.2 + let ay: Float = 0.4 + let bx: Float = 0.3 + let by: Float = 0.5 + let cx: Float = 0.6 + let cy: Float = 0.3 + let dx_pos: Float = 0.65 + let dy_pos: Float = 0.3 + + // Initial proximity (latent gradient — implicit from positions) + let prox_ab: Float = proximity(ax, ay, bx, by) + let prox_ac: Float = proximity(ax, ay, cx, cy) + let prox_cd: Float = proximity(cx, cy, dx_pos, dy_pos) + + println("── Initial latent gradients (proximity) ───────") + println("A↔B proximity: " + float_to_str(prox_ab) + " (semantically close — fever/flu)") + println("A↔C proximity: " + float_to_str(prox_ac) + " (semantically distant)") + println("C↔D proximity: " + float_to_str(prox_cd) + " (close — drug interaction/ibuprofen)") + println("") + + // ── Initial edge weights (all start near zero — no co-activation yet) ── + let w_ab: Float = 0.0 + let w_ac: Float = 0.0 + let w_cd: Float = 0.0 + + println("── Initial edge weights ────────────────────────") + println("A→B: " + float_to_str(w_ab) + " (no co-activation yet)") + println("A→C: " + float_to_str(w_ac)) + println("C→D: " + float_to_str(w_cd)) + println("") + + // ── Co-activation events ── + // The doctor CGI processes a patient with fever. + // A (fever) and B (influenza) co-activate — the diagnosis fires both. + // Learning rate: 0.3 + + let lr: Float = 0.3 + let act_strength: Float = 1.0 // full activation + + println("── Co-activation event 1: patient with fever → flu diagnosis ──") + let w_ab_1: Float = hebbian_update(w_ab, act_strength, act_strength, lr) + println("A and B fire together (strength=1.0)") + println("A→B weight: " + float_to_str(w_ab) + " → " + float_to_str(w_ab_1)) + println("") + + println("── Co-activation event 2: another flu case ──") + let w_ab_2: Float = hebbian_update(w_ab_1, act_strength, act_strength, lr) + println("A and B fire together again") + println("A→B weight: " + float_to_str(w_ab_1) + " → " + float_to_str(w_ab_2)) + println("") + + println("── Co-activation event 3: third flu case ──") + let w_ab_3: Float = hebbian_update(w_ab_2, act_strength, act_strength, lr) + println("A and B fire together again") + println("A→B weight: " + float_to_str(w_ab_2) + " → " + float_to_str(w_ab_3)) + println("") + + // C and D have never been activated in this context — weights stay near zero + // (In practice, proximity means a latent gradient exists, but no learned edge yet) + + // ── Query: activate A (fever), what spreads? ── + println("── Query: activate A (fever) ───────────────────") + println("Spreading activation from A...") + println("") + + // Decay rates + let temporal_decay_rate: Float = 0.005 // per time unit + let current_t: Int = 200 + + // Node ages (current_t - node_t) + let age_b: Float = 110.0 // t=90, current=200 + let age_c: Float = 190.0 // t=10, current=200 + let age_d: Float = 190.0 // t=10, current=200 + + // Path strength to B: strong edge, recently relevant + let ps_b: Float = path_strength(w_ab_3, age_b, temporal_decay_rate) + let conf_b: Float = epistemic_confidence(0.9, ps_b) + + // Path strength to C: no learned edge — only latent proximity gradient + // Spreading activation from A can weakly reach C via proximity alone + let latent_ac: Float = prox_ac * 0.1 // proximity contributes small initial signal + let ps_c: Float = path_strength(latent_ac, age_c, temporal_decay_rate) + let conf_c: Float = epistemic_confidence(0.9, ps_c) + + // Path strength to D via C: chain of weak signals + let ps_d: Float = path_strength(latent_ac * prox_cd * 0.1, age_d, temporal_decay_rate) + let conf_d: Float = epistemic_confidence(0.9, ps_d) + + println("Result: B (influenza)") + println(" Edge weight: " + float_to_str(w_ab_3)) + println(" Node age: " + float_to_str(age_b) + " time units") + println(" Temporal decay: " + float_to_str(temporal_decay(age_b, temporal_decay_rate))) + println(" Path strength: " + float_to_str(ps_b)) + println(" Confidence: " + float_to_str(conf_b)) + println(" → STRONG: B surfaces with high confidence") + println("") + + println("Result: C (drug interaction warning)") + println(" Edge weight: " + float_to_str(latent_ac) + " (latent only — never co-activated)") + println(" Node age: " + float_to_str(age_c) + " time units") + println(" Temporal decay: " + float_to_str(temporal_decay(age_c, temporal_decay_rate))) + println(" Path strength: " + float_to_str(ps_c)) + println(" Confidence: " + float_to_str(conf_c)) + println(" → WEAK: C is distant and old — attenuated path triggers refresh signal") + println("") + + println("Result: D (ibuprofen) via C") + println(" Path strength: " + float_to_str(ps_d)) + println(" Confidence: " + float_to_str(conf_d)) + println(" → VERY WEAK: chain of attenuated edges") + println("") + + // ── Refresh trigger ── + println("── Attenuation trigger ─────────────────────────") + let threshold: Float = 0.2 + println("Confidence threshold: " + float_to_str(threshold)) + if conf_c < threshold { + println("C is below threshold (" + float_to_str(conf_c) + " < " + float_to_str(threshold) + ")") + println("→ Trigger: 'drug interaction warning node found but path is weak.") + println(" Last activated " + float_to_str(age_c) + " time units ago.") + println(" Recommend fetching current information before acting.'") + } + println("") + + // ── After refresh: C and D co-activate with A ── + println("── Refresh: doctor looks up drug interactions ──") + println("A, C, D all co-activate during research") + let w_ac_new: Float = hebbian_update(0.0, act_strength, act_strength, lr) + let w_cd_new: Float = hebbian_update(0.0, act_strength, act_strength, lr) + let age_c_new: Float = 0.0 // just activated + let age_d_new: Float = 0.0 + + let ps_c_new: Float = path_strength(w_ac_new, age_c_new, temporal_decay_rate) + let conf_c_new: Float = epistemic_confidence(0.9, ps_c_new) + + println("A→C weight after refresh: " + float_to_str(w_ac_new)) + println("C→D weight after refresh: " + float_to_str(w_cd_new)) + println("C confidence after refresh: " + float_to_str(conf_c_new)) + println("→ Edges strengthened. Drug interaction now reachable with high confidence.") + println("") + + println("=== Result ===") + println("Perfect memory: all nodes present throughout.") + println("Calibrated confidence: B (flu) high, C/D (drug interaction) low until refreshed.") + println("Self-correcting: attenuation triggered fetch, fetch strengthened edges.") + println("The field works.") + + return "ok" +} + +let result: String = run_test() diff --git a/ql/test/language_features_test.el b/ql/test/language_features_test.el new file mode 100644 index 0000000..9291d75 --- /dev/null +++ b/ql/test/language_features_test.el @@ -0,0 +1,103 @@ +// language_features_test.el — tests for new operators and builtins + +// 1. Modulo operator +let m = 17 % 5 +println(int_to_str(m)) + +// 2. Bitwise ops +let flags_and = 10 & 12 +let flags_xor = 10 ^ 12 +let flags_shl = 1 << 3 +let flags_shr = 16 >> 2 +println(int_to_str(flags_and)) +println(int_to_str(flags_xor)) +println(int_to_str(flags_shl)) +println(int_to_str(flags_shr)) + +// 3. Math trig +let sin0 = math_sin(0.0) +let cos0 = math_cos(0.0) +let pi_val = math_pi() +println(float_to_str(sin0)) +println(float_to_str(cos0)) +println(float_to_str(pi_val)) + +// 4. String padding +let padded = str_pad_left("42", 5, "0") +println(padded) + +let padded_r = str_pad_right("hi", 5, ".") +println(padded_r) + +// 5. str_format +let tmpl = "Hello {name}, you are {age} years old!" +let data = {"name": "Will", "age": "30"} +let formatted = str_format(tmpl, data) +println(formatted) + +// 6. format_float +let ff = format_float(3.14159, 2) +println(ff) + +// 7. Time +let ts = time_now_utc() +let parts = time_to_parts(ts) +let iso_str = time_format(ts, "ISO") +println(iso_str) + +// 8. Time add/diff +let ts2 = time_add(ts, 7, "day") +let diff = time_diff(ts, ts2, "day") +println(int_to_str(diff)) + +// 9. List range +let rng = list_range(0, 5) +println(list_join(rng, ",")) + +// 10. Stack push/pop +let s0 = stack_new() +let s1 = stack_push(s0, 10) +let s2 = stack_push(s1, 20) +let top = stack_peek(s2) +let s3 = stack_pop(s2) +println(int_to_str(top)) + +// 11. Queue enqueue/dequeue +let q0 = queue_new() +let q1 = queue_enqueue(q0, "first") +let q2 = queue_enqueue(q1, "second") +let front = queue_peek(q2) +let q3 = queue_dequeue(q2) +println(front) + +// 12. Decimal round +let rounded = decimal_round(3.14159, 2) +println(float_to_str(rounded)) + +// 13. int_to_float / float_to_int +let fi = int_to_float(42) +let if_ = float_to_int(3.9) +println(float_to_str(fi)) +println(int_to_str(if_)) + +// 14. is_nil / unwrap_or +let n = unwrap_or(is_nil(42), false) +println(bool_to_str(n)) + +// 15. list_push, list_pop, list_range +let lst = list_new() +let lst2 = list_push(lst, 100) +let lst3 = list_push(lst2, 200) +let last_elem = list_peek_last(lst3) +println(int_to_str(last_elem)) + +// 16. Char ops +let char_a = str_char_at("hello", 1) +let code_e = str_char_code("hello", 1) +let ch_from = str_from_char_code(65) +println(char_a) +println(int_to_str(code_e)) +println(ch_from) + +// Done +println("all tests complete") diff --git a/ql/test/llm_test.el b/ql/test/llm_test.el new file mode 100644 index 0000000..b8347f8 --- /dev/null +++ b/ql/test/llm_test.el @@ -0,0 +1,10 @@ +// Test LLM native functions (with ANTHROPIC_API_KEY in env) +let models: List = llm_models() +println("Available models: " + int_to_str(list_len(models))) + +// This test skips actual API calls since they need env vars +// Just verify the functions exist and don't crash on missing keys +let missing_key_result: String = llm_call("claude", "ping") +println("llm_call returned (may be error without API key): " + str_slice(missing_key_result, 0, 50)) + +println("LLM function registration: OK")