Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| edcec3bdf4 |
@@ -272,6 +272,89 @@ fn route_load_merge(method: String, path: String, body: String) -> String {
|
||||
"{\"ok\":true,\"nodes_added\":" + int_to_str(added_n) + ",\"edges_added\":" + int_to_str(added_e) + ",\"node_count\":" + int_to_str(engram_node_count()) + "}"
|
||||
}
|
||||
|
||||
// route_reseed_nodes — POST /api/nodes/reseed
|
||||
// {"path": "<snapshot-format file>", "replace": ["<id>", ...],
|
||||
// "preserve_edges": true, "_auth": "<key>"}
|
||||
//
|
||||
// ID-PRESERVING install/repair for declarative seed graphs.
|
||||
//
|
||||
// WHY THIS EXISTS (2026-08-10). Two write paths could put a node into the
|
||||
// graph and neither can put a BODY onto an id that already exists:
|
||||
// POST /api/nodes mints a fresh id via engram_node_full, and
|
||||
// POST /api/load-merge honors the declared id but SKIPS anything already
|
||||
// present. That is exactly right for the additive case and leaves one hole:
|
||||
// a node that exists with a truncated body. Forge's genesis seed hit it —
|
||||
// two identity nodes (Voice, Voice Craft) sat in the graph carrying only
|
||||
// their own label as content, 30 and 22 bytes against 4263 and 2590 in the
|
||||
// seed. Their ids are load-bearing (is_protected_node keys on them and 214
|
||||
// declared edges reference them), so "delete and recreate with a new id" is
|
||||
// not a repair, it is a second break.
|
||||
//
|
||||
// Mechanism: engram has no in-place node update, so a replace is
|
||||
// forget-then-merge. engram_forget also drops every INCIDENT EDGE — for
|
||||
// those two nodes that is 85 and 93 edges, almost all of them tag edges and
|
||||
// accumulated hebbian associations that the seed does not declare and could
|
||||
// not restore. preserve_edges (default true) therefore snapshots the graph
|
||||
// before the forget and re-merges that snapshot afterwards: the replaced
|
||||
// node is back by then so it is skipped, and every dropped incident edge
|
||||
// returns through load_merge's (from_id,to_id,relation) dedup. The same
|
||||
// re-merge is the failure path — if the seed merge does not produce the
|
||||
// node, the backup puts the original back. Rollback, not data loss.
|
||||
//
|
||||
// preserve_edges=false skips the two snapshot round-trips (cheap, lossy);
|
||||
// use it only on a graph whose edges are fully declared by the seed.
|
||||
// With no "replace" list this route is exactly /api/load-merge.
|
||||
fn route_reseed_nodes(method: String, path: String, body: String) -> String {
|
||||
let p: String = json_get_string(body, "path")
|
||||
if str_eq(p, "") { return err_json("path is required") }
|
||||
if str_eq(fs_read(p), "") { return err_json("file missing or empty") }
|
||||
|
||||
let dir_raw: String = env("ENGRAM_DATA_DIR")
|
||||
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
|
||||
let backup: String = dir + "/.reseed-backup.json"
|
||||
|
||||
let replace_raw: String = json_get_raw(body, "replace")
|
||||
let n_replace: Int = json_array_len(replace_raw)
|
||||
// Presence-aware: absent key means "preserve", only an explicit false opts out.
|
||||
let pe_raw: String = json_get_raw(body, "preserve_edges")
|
||||
let preserve: Bool = !str_eq(pe_raw, "false")
|
||||
|
||||
let before_n: Int = engram_node_count()
|
||||
let before_e: Int = engram_edge_count()
|
||||
|
||||
let replaced: Int = 0
|
||||
if n_replace > 0 {
|
||||
if preserve { engram_save(backup) }
|
||||
let i: Int = 0
|
||||
while i < n_replace {
|
||||
let rid: String = json_array_get_string(replace_raw, i)
|
||||
if !str_eq(rid, "") {
|
||||
// engram_get_node_json returns "{}" for a miss — only forget
|
||||
// ids that are actually resident, so a typo in the replace
|
||||
// list is a no-op rather than a silent partial run.
|
||||
let existing: String = engram_get_node_json(rid)
|
||||
if !str_eq(existing, "{}") {
|
||||
engram_forget(rid)
|
||||
let replaced = replaced + 1
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
engram_load_merge(p)
|
||||
if replaced > 0 {
|
||||
if preserve { engram_load_merge(backup) }
|
||||
}
|
||||
|
||||
let saved: Int = persist_canonical()
|
||||
"{\"ok\":true,\"replaced\":" + int_to_str(replaced) +
|
||||
",\"nodes_added\":" + int_to_str(engram_node_count() - before_n) +
|
||||
",\"edges_added\":" + int_to_str(engram_edge_count() - before_e) +
|
||||
",\"node_count\":" + int_to_str(engram_node_count()) +
|
||||
",\"edge_count\":" + int_to_str(engram_edge_count()) + "}"
|
||||
}
|
||||
|
||||
// route_emit_ise — write an InternalStateEvent node from the soul daemon.
|
||||
//
|
||||
// Endpoint: POST /api/neuron/state-events
|
||||
@@ -419,6 +502,12 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
}
|
||||
|
||||
// Nodes
|
||||
// Reseed must be tested before the exact "/api/nodes" match below reads
|
||||
// as the general create path — order is not load-bearing (the match is
|
||||
// exact) but keeping them adjacent keeps them from drifting apart.
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/nodes/reseed") || str_eq(clean, "/nodes/reseed")) {
|
||||
return route_reseed_nodes(method, path, body)
|
||||
}
|
||||
if str_eq(method, "POST") && (str_eq(clean, "/api/nodes") || str_eq(clean, "/nodes")) {
|
||||
return route_create_node(method, path, body)
|
||||
}
|
||||
|
||||
Vendored
BIN
Binary file not shown.
@@ -5327,8 +5327,8 @@ el_val_t str_to_float(el_val_t s) {
|
||||
/* ── Math (Float-aware) ──────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t math_sqrt(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); }
|
||||
el_val_t math_log(el_val_t f) { return el_from_float(log10(el_to_float(f))); } /* base-10, per runtime/math.el */
|
||||
el_val_t math_ln(el_val_t f) { return el_from_float(log(el_to_float(f))); } /* natural log */
|
||||
el_val_t math_log(el_val_t f) { return el_from_float(log(el_to_float(f))); }
|
||||
el_val_t math_ln(el_val_t f) { return el_from_float(log(el_to_float(f))); }
|
||||
el_val_t math_sin(el_val_t f) { return el_from_float(sin(el_to_float(f))); }
|
||||
el_val_t math_cos(el_val_t f) { return el_from_float(cos(el_to_float(f))); }
|
||||
el_val_t math_pi(void) { return el_from_float(3.141592653589793238462643383279502884); }
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <math.h> /* fmod, sin, sqrt, ... — used by codegen'd float arithmetic */
|
||||
|
||||
typedef int64_t el_val_t;
|
||||
|
||||
|
||||
@@ -731,41 +731,6 @@ fn cg_expr(expr: Map<String, Any>) -> String {
|
||||
// because both share the int64 slot).
|
||||
}
|
||||
|
||||
// -- Float arithmetic / comparison dispatch -------------------------
|
||||
// When either operand is provably Float, promote BOTH operands to a C
|
||||
// double and emit real double arithmetic. Arithmetic results are
|
||||
// re-wrapped into the el_val_t float slot via el_from_float(); the
|
||||
// integer % becomes fmod(); comparisons yield a bare 0/1. Without this
|
||||
// path, float `+` fell to el_str_concat (segfault) and `- * / %` fell
|
||||
// to integer ops on the raw IEEE-754 bit pattern (garbage). Mixed
|
||||
// Int/Float promotes the Int side with a (double) cast. This runs
|
||||
// AFTER temporal dispatch (a Float is never temporal) and BEFORE the
|
||||
// Int/String paths, and is skipped when either side is a bare Str so
|
||||
// string concat/equality is never disturbed.
|
||||
if !str_eq(left_kind, "Str") {
|
||||
if !str_eq(right_kind, "Str") {
|
||||
let any_float: Bool = false
|
||||
if is_float_expr(left) { let any_float = true }
|
||||
if is_float_expr(right) { let any_float = true }
|
||||
if any_float {
|
||||
let l_d: String = float_operand_c(left, left_c)
|
||||
let r_d: String = float_operand_c(right, right_c)
|
||||
if str_eq(op, "Plus") { return "el_from_float(" + l_d + " + " + r_d + ")" }
|
||||
if str_eq(op, "Minus") { return "el_from_float(" + l_d + " - " + r_d + ")" }
|
||||
if str_eq(op, "Star") { return "el_from_float(" + l_d + " * " + r_d + ")" }
|
||||
if str_eq(op, "Slash") { return "el_from_float(" + l_d + " / " + r_d + ")" }
|
||||
if str_eq(op, "Percent") { return "el_from_float(fmod(" + l_d + ", " + r_d + "))" }
|
||||
if str_eq(op, "Lt") { return "(" + l_d + " < " + r_d + ")" }
|
||||
if str_eq(op, "Gt") { return "(" + l_d + " > " + r_d + ")" }
|
||||
if str_eq(op, "LtEq") { return "(" + l_d + " <= " + r_d + ")" }
|
||||
if str_eq(op, "GtEq") { return "(" + l_d + " >= " + r_d + ")" }
|
||||
if str_eq(op, "EqEq") { return "(" + l_d + " == " + r_d + ")" }
|
||||
if str_eq(op, "NotEq") { return "(" + l_d + " != " + r_d + ")" }
|
||||
// And/Or with a float operand is ill-typed — fall through.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if op == "Plus" {
|
||||
// If either side is a string literal, always concat
|
||||
if left_kind == "Str" {
|
||||
@@ -1441,9 +1406,6 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
||||
if str_eq(ltype, "Int") {
|
||||
add_int_name(name)
|
||||
}
|
||||
if str_eq(ltype, "Float") {
|
||||
add_float_name(name)
|
||||
}
|
||||
// Temporal type annotations register the name with the matching
|
||||
// typed-set so BinOp / comparison codegen routes through the
|
||||
// typed wrappers and forbids cross-type ops.
|
||||
@@ -1503,12 +1465,6 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
|
||||
if is_zone_expr(val) {
|
||||
add_zone_name(name)
|
||||
}
|
||||
// Float inference: an unannotated `let` whose RHS is provably Float
|
||||
// (literal, math builtin, or float arithmetic) registers the name so
|
||||
// later `name <op> ...` routes through the double path.
|
||||
if is_float_expr(val) {
|
||||
add_float_name(name)
|
||||
}
|
||||
let vk: String = val["expr"]
|
||||
if str_eq(vk, "Int") {
|
||||
add_int_name(name)
|
||||
@@ -1828,15 +1784,6 @@ fn is_int_name(name: String) -> Bool {
|
||||
return str_contains(csv, "," + name + ",")
|
||||
}
|
||||
|
||||
// Float-typed binding set — parallel to is_int_name. Populated from `let x:
|
||||
// Float = ...` annotations, `: Float` parameters, and inference from a Float
|
||||
// RHS. Consulted by is_float_expr to route arithmetic through the double path.
|
||||
fn is_float_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__float_names")
|
||||
if str_eq(csv, "") { return false }
|
||||
return str_contains(csv, "," + name + ",")
|
||||
}
|
||||
|
||||
// Same shape as is_int_name, for Instant- and Duration-typed bindings.
|
||||
// Used by the BinOp/comparison codegen to dispatch arithmetic through the
|
||||
// typed runtime wrappers (el_instant_add_dur, el_duration_lt, -) and to
|
||||
@@ -1900,28 +1847,6 @@ fn is_int_call(call_expr: Map<String, Any>) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Known runtime builtins that return Float. Parallel to is_int_call — lets a
|
||||
// Call participate in float arithmetic (and get inferred into __float_names on
|
||||
// an unannotated `let`). New Float-returning builtins must be added here.
|
||||
fn is_float_call(call_expr: Map<String, Any>) -> Bool {
|
||||
let func = call_expr["func"]
|
||||
let fk: String = func["expr"]
|
||||
if !str_eq(fk, "Ident") { return false }
|
||||
let name: String = func["name"]
|
||||
if str_eq(name, "int_to_float") { return true }
|
||||
if str_eq(name, "str_to_float") { return true }
|
||||
if str_eq(name, "json_get_float") { return true }
|
||||
if str_eq(name, "decimal_round") { return true }
|
||||
if str_eq(name, "math_pi") { return true }
|
||||
if str_eq(name, "math_sin") { return true }
|
||||
if str_eq(name, "math_cos") { return true }
|
||||
if str_eq(name, "math_sqrt") { return true }
|
||||
if str_eq(name, "math_log") { return true }
|
||||
if str_eq(name, "math_ln") { return true }
|
||||
if str_eq(name, "el_from_float") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
// Builtins that return an Instant. Used by is_instant_expr and the BinOp
|
||||
// dispatch - `now() + 5.seconds` types as Instant only because we can see
|
||||
// that now() is an Instant-returning Call.
|
||||
@@ -2330,71 +2255,19 @@ fn is_int_expr(expr: Map<String, Any>) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// is_float_expr — true when expr is (or provably evaluates to) a Float value.
|
||||
// Mirrors is_int_expr: Float literal, Neg of a Float, a Float-typed Ident
|
||||
// (registered in __float_names), a Float-returning builtin Call, or a Float
|
||||
// arithmetic BinOp (float propagates: if either operand is Float the result
|
||||
// is Float). Drives the float-arithmetic dispatch in cg_expr and the EqEq/
|
||||
// NotEq float comparison. Kept conservative (only provably-Float) so it never
|
||||
// intercepts a value the Int paths must own — the two predicates are mutually
|
||||
// exclusive on well-typed source.
|
||||
// is_float_expr — true when expr is (or evaluates to) a Float-typed value.
|
||||
// Used in EqEq/NotEq codegen to avoid str_eq on float values.
|
||||
fn is_float_expr(expr: Map<String, Any>) -> Bool {
|
||||
let k: String = expr["expr"]
|
||||
if str_eq(k, "Float") { return true }
|
||||
if str_eq(k, "Neg") {
|
||||
return is_float_expr(expr["inner"])
|
||||
}
|
||||
if str_eq(k, "Ident") {
|
||||
let name: String = expr["name"]
|
||||
return is_float_name(name)
|
||||
}
|
||||
if str_eq(k, "Call") {
|
||||
return is_float_call(expr)
|
||||
}
|
||||
if str_eq(k, "BinOp") {
|
||||
let op: String = expr["op"]
|
||||
if str_eq(op, "Plus") {
|
||||
if is_float_expr(expr["left"]) { return true }
|
||||
if is_float_expr(expr["right"]) { return true }
|
||||
return false
|
||||
}
|
||||
if str_eq(op, "Minus") {
|
||||
if is_float_expr(expr["left"]) { return true }
|
||||
if is_float_expr(expr["right"]) { return true }
|
||||
return false
|
||||
}
|
||||
if str_eq(op, "Star") {
|
||||
if is_float_expr(expr["left"]) { return true }
|
||||
if is_float_expr(expr["right"]) { return true }
|
||||
return false
|
||||
}
|
||||
if str_eq(op, "Slash") {
|
||||
if is_float_expr(expr["left"]) { return true }
|
||||
if is_float_expr(expr["right"]) { return true }
|
||||
return false
|
||||
}
|
||||
if str_eq(op, "Percent") {
|
||||
if is_float_expr(expr["left"]) { return true }
|
||||
if is_float_expr(expr["right"]) { return true }
|
||||
return false
|
||||
}
|
||||
return false
|
||||
let inner = expr["inner"]
|
||||
let ik: String = inner["expr"]
|
||||
if str_eq(ik, "Float") { return true }
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// float_operand_c — render a BinOp operand as a C double for float arithmetic.
|
||||
// A provably-Float operand round-trips through el_to_float() (recovering the
|
||||
// double from the int64 bit-slot); anything else is treated as Int-like and
|
||||
// numerically converted with (double). This is why Float locals/params must
|
||||
// be annotated `: Float` — an unannotated var holding float bits would be
|
||||
// mis-converted by the (double) cast, exactly as the Int system requires
|
||||
// annotations to dispatch arithmetic vs concat.
|
||||
fn float_operand_c(expr: Map<String, Any>, expr_c: String) -> String {
|
||||
if is_float_expr(expr) { return "el_to_float(" + expr_c + ")" }
|
||||
return "(double)(" + expr_c + ")"
|
||||
}
|
||||
|
||||
// -- Capability-kind enforcement ----------------------------------------------
|
||||
//
|
||||
// A program's top-level block (cgi / service / none) determines which
|
||||
@@ -2911,15 +2784,6 @@ fn add_int_name(name: String) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
fn add_float_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__float_names")
|
||||
if str_eq(csv, "") { csv = "," }
|
||||
let key: String = "," + name + ","
|
||||
if str_contains(csv, key) { return true }
|
||||
state_set("__float_names", csv + name + ",")
|
||||
return true
|
||||
}
|
||||
|
||||
fn add_instant_name(name: String) -> Bool {
|
||||
let csv: String = state_get("__instant_names")
|
||||
if str_eq(csv, "") { csv = "," }
|
||||
@@ -3003,7 +2867,6 @@ fn add_zone_name(name: String) -> Bool {
|
||||
|
||||
fn build_int_names_for_params(params: [Map<String, Any>]) -> Bool {
|
||||
state_set("__int_names", ",")
|
||||
state_set("__float_names", ",")
|
||||
state_set("__instant_names", ",")
|
||||
state_set("__duration_names", ",")
|
||||
state_set("__calendar_names", ",")
|
||||
@@ -3022,9 +2885,6 @@ fn build_int_names_for_params(params: [Map<String, Any>]) -> Bool {
|
||||
if str_eq(ptype, "Int") {
|
||||
add_int_name(pname)
|
||||
}
|
||||
if str_eq(ptype, "Float") {
|
||||
add_float_name(pname)
|
||||
}
|
||||
if str_eq(ptype, "Instant") {
|
||||
add_instant_name(pname)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user