bef36a5e3a
`runStructuralAudit` has been an advertised MCP tool with nothing behind it: the
dispatcher GET'd /session/begin and returned that unrelated session digest under
an audit tool's name. Meanwhile the failure the audit would have caught ran
silently for about three weeks — the soul reporting 103,089 nodes while the
engram, which OWNS persistence, held ~79,900, a crash discarding the difference,
and every boot reporting green throughout, because nothing in the system ever
compared the two sides.
WHAT THE PATENT SPECIFIES, AND HOW IT SHAPED THIS
CGI provisional, 05-detailed-description.md, "Stage 1: Structural audit 430".
Two clauses did the design work. First the four things the module evaluates:
the density and typed distribution of causal edges; value/execution-record
consistency; the richness and connectivity of the self-model; and wonder-
manifest authenticity. Second, and decisively: it "produces a coherence
assessment 432 — NOT A BINARY SCORE but an annotated characterization of the
graph's structural properties."
So every finding carries its numbers AND a plain-language note saying what
they mean and how they were obtained. There is no pass/fail and no composite
health figure, and `"score":null` is emitted explicitly so a reader cannot
mistake its absence for an omission.
WHAT IS IN STAGE 1 (four findings)
owner_runtime_divergence — the motivating case. Runtime counts vs the owner's
own GET /api/stats, the delta, and the trend against the previous audit, so
a second call answers "is the gap growing?" rather than restating it.
self_model_connectivity — the three identity pillars plus the self root:
present, content length, one-hop degree. This RETIRES the Claude-side vitals
identity block, which lived outside the system it was checking and went on
reporting green while the memory-philosophy pillar was absent from the live
graph. Asking the running soul is the designed mechanism; a shell probe was
the fourth patch on the same hole.
typed_edge_distribution — exact counts against the claim-10 vocabulary, plus
density, plus a separate count of LOWERCASE near-misses ("causes" vs
"Causes"): "the vocabulary is unused" and "the vocabulary is misspelled by
the write paths" are different defects with different fixes.
orphans_and_dangling_edges — the tool's own long-standing promise.
WHAT IS DEFERRED, AND WHY IT IS DATA RATHER THAN A COMMENT
Value/execution-record consistency and wonder-manifest authenticity ship as a
`deferred` array that MEASURES the populations they would need (Prediction and
WonderQuestion nodes) and reports those counts as the reason. Both are ~0 today
— WonderQuestion because of a known write/read node-type mismatch. Asserting
value coherence or a pull-weight correlation on an empty population would be a
fabricated result, which is worse than a stated gap.
MEASUREMENT HONESTY: EXACT WHERE CHEAP, SAMPLED WHERE NOT, ALWAYS LABELLED
Counts, edge typing and self-model connectivity are exact. Orphan and dangling
rates are sampled, because engram_find_node_index is a linear scan — an
exhaustive dangling check is O(nodes x edges), ~2.2e9 string compares at today's
scale. Samples are UNIFORM across the whole population (str_index_of_all gives
every edge offset in one pass, so any index is O(1); json_array_get would have
been O(n^2)), never head-of-list, and each figure ships with its own sampled /
population / exhaustive fields. ?edge_sample= and ?node_sample= at population
size run either check exhaustively. The real fix is an id index in the runtime.
ONE BUG THIS FOUND IN ITSELF, CAUGHT IN TEST
http_get does not return "" when the owner is unreachable — it returns a JSON
error object. Testing only for "" made a DEAD owner read as reachable with
node_count 0, so the audit reported 100% divergence and named it data loss.
Reachability is now proved by the presence of the node_count field, and the
owner's raw reply is attached. A confident wrong answer is exactly what this
route exists to stop.
Edge findings need relation labels and the runtime has no edge-enumeration
builtin, so they use the same scratch export GET /api/graph/edges already uses
(engram_save to TMPDIR, never the owner's canonical file — #117). That is a
large write on a large graph, so this is a manual route, not a timer; ?edges=0
skips it.
neuron-api.el:900-1273 handler + helpers
routes.el:567,752 GET and POST /api/neuron/audit/structural
mcp-wrapper/src/main.el:113,682 tool description + dispatch off /session/begin
dist/soul.c regenerated (1255 bodies)
Rung: E2E-VERIFIED. Soul built from this branch (gen-soul-amalgam + cc-brain,
921,192 bytes, 16 warnings, 0 errors), booted on throwaway ports 7893/7896/7897
with throwaway HOMEs against a stub owner on 7894. Three scenarios pass: owner
reachable (runtime 62 vs owner 42, delta 20 / 32.2%, trend flat on the second
call; 12/20 edges claim-10 typed, 3 lowercase near-misses; 50/62 orphans, 3/20
dangling — every figure matches the fixture by construction), owner unreachable
(reported as a finding with the raw reply, not a crash), and file mode (owner
"none", divergence undefined). Reached end-to-end through the MCP tool via a
locally built wrapper. verify-soul-contract.sh: GATE PASS, 27/27 routes +
immutability. No process left running; live :7770 and :8742 untouched (GET only).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1351 lines
72 KiB
EmacsLisp
1351 lines
72 KiB
EmacsLisp
import "memory.el"
|
|
|
|
// neuron-api.el — Native Neuron cognitive API handlers.
|
|
//
|
|
// These were previously implemented in the MCP wrapper as HTTP calls to
|
|
// the engram server. They now live here as native engram builtin calls —
|
|
// no HTTP round-trips, no separate process, full in-process access.
|
|
//
|
|
// Routes are wired in routes.el under /api/neuron/*.
|
|
|
|
// ── Identity/values write protection ─────────────────────────────────────────
|
|
//
|
|
// These node IDs form the identity and values layer of the self-root graph.
|
|
// They must NEVER be modified via the normal accumulation path (evolve_knowledge,
|
|
// evolve_memory, forget, link_entities targeting them as the destination).
|
|
//
|
|
// The cultivation path (POST /api/neuron/cultivate) bypasses this check.
|
|
// Only Will's explicit cultivation sessions use that endpoint.
|
|
|
|
fn is_protected_node(id: String) -> Bool {
|
|
if str_eq(id, "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee") { return true } // self root
|
|
if str_eq(id, "kn-5b606390-a52d-4ca2-8e0e-eba141d13440") { return true } // values hub
|
|
if str_eq(id, "kn-5adecd7e-d6db-4576-87fe-6ef8a935cea6") { return true } // intellectual-dna
|
|
if str_eq(id, "kn-dcfe04b3-3702-4cac-b6f0-ecb4db837eee") { return true } // memory-philosophy
|
|
if str_eq(id, "kn-10fa60db-8af3-47de-a7dd-5095eb881d81") { return true } // voice
|
|
if str_eq(id, "kn-86b95848-e22e-4a48-ae65-5a47ef5c3798") { return true } // runtime-environment
|
|
if str_eq(id, "kn-04368bee-74fd-44dd-b4ba-ca9e39b19e7c") { return true } // writing-imprint
|
|
if str_eq(id, "kn-a5b3d0ac-f6a1-49a4-aebb-b8b4cd67fe83") { return true } // value: constraints-as-freedom
|
|
if str_eq(id, "kn-22d77abe-b3c5-42fd-afcd-dcb87d924929") { return true } // value: precision-over-brute-force
|
|
if str_eq(id, "kn-6061318f-046b-4935-907d-8eafdce14930") { return true } // value: structure-is-built
|
|
if str_eq(id, "kn-13f60407-7b70-4db1-964f-ea1f8196efbd") { return true } // value: honesty-before-comfort
|
|
if str_eq(id, "kn-f230b362-b201-4402-9833-4160c89ab3d4") { return true } // value: system-must-accumulate
|
|
if str_eq(id, "kn-78db5396-3dbc-4481-bfc7-e4e1422feb1c") { return true } // value: change-is-the-signal
|
|
if str_eq(id, "kn-5de5a9ac-fd15-45ab-bf18-77566781cf40") { return true } // value: earned-trust
|
|
if str_eq(id, "kn-e0423482-cfa5-4796-8689-8495c93b66bc") { return true } // value: hope-is-a-conclusion
|
|
return false
|
|
}
|
|
|
|
fn api_err_protected(id: String) -> String {
|
|
return "{\"__status__\":403,\"error\":\"identity/values node is write-protected\",\"id\":\"" + id + "\",\"hint\":\"use POST /api/neuron/cultivate for intentional cultivation\"}"
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
fn api_json_escape(s: String) -> String {
|
|
let s1: String = str_replace(s, "\\", "\\\\")
|
|
let s2: String = str_replace(s1, "\"", "\\\"")
|
|
let s3: String = str_replace(s2, "\n", "\\n")
|
|
let s4: String = str_replace(s3, "\r", "\\r")
|
|
return s4
|
|
}
|
|
|
|
fn api_query_param(path: String, key: String) -> String {
|
|
let q: Int = str_index_of(path, "?")
|
|
if q < 0 { return "" }
|
|
let qs: String = str_slice(path, q + 1, str_len(path))
|
|
let needle: String = key + "="
|
|
let pos: Int = str_index_of(qs, needle)
|
|
if pos < 0 { return "" }
|
|
let after: String = str_slice(qs, pos + str_len(needle), str_len(qs))
|
|
let amp: Int = str_index_of(after, "&")
|
|
let raw: String = if amp < 0 { after } else { str_slice(after, 0, amp) }
|
|
// URL-decode the extracted value BEFORE any downstream tokenizing. Clients
|
|
// percent-encode spaces (%20) and form-encode them as '+', so a multi-word
|
|
// query like "foo bar" arrives as "foo%20bar" / "foo+bar". Left undecoded,
|
|
// the ranked lexical search sees a single un-splittable token and matches
|
|
// nothing (single-word queries still hit). url_decode maps '+' -> space
|
|
// and %XX -> byte, restoring the word boundaries for recall + knowledge search.
|
|
return url_decode(raw)
|
|
}
|
|
|
|
fn api_query_int(path: String, key: String, default_val: Int) -> Int {
|
|
let v: String = api_query_param(path, key)
|
|
if str_eq(v, "") { return default_val }
|
|
return str_to_int(v)
|
|
}
|
|
|
|
fn api_ok(extra: String) -> String {
|
|
if str_eq(extra, "") { return "{\"ok\":true}" }
|
|
return "{\"ok\":true," + extra + "}"
|
|
}
|
|
|
|
fn api_err(msg: String) -> String {
|
|
return "{\"error\":\"" + msg + "\"}"
|
|
}
|
|
|
|
fn api_nonempty(s: String) -> Bool {
|
|
return !str_eq(s, "") && !str_eq(s, "[]") && !str_eq(s, "null")
|
|
}
|
|
|
|
fn api_or_empty(s: String) -> String {
|
|
if api_nonempty(s) { return s }
|
|
return "[]"
|
|
}
|
|
|
|
// ── Compact projection for session/context digests ────────────────────────────
|
|
//
|
|
// beginSession/compileCtx are session-INIT digests, not full graph dumps. The
|
|
// engram scan/activate builtins return FULL node objects — content runs to tens
|
|
// of KB per node (the self-identity hub is ~90KB alone), and node JSON carries
|
|
// content + metadata + timestamps. Concatenated unbounded, the assembled response
|
|
// reached ~900KB and — after the MCP wrapper re-escapes it into a stringified
|
|
// text block — the client dropped the socket ("connection closed unexpectedly")
|
|
// on every call. These helpers CAP the array length and project each node down
|
|
// to a light identity + a bounded, UTF-8-safe content snippet, holding the
|
|
// digest well under ~150KB regardless of graph size. Full content stays
|
|
// available on demand via recall / fetch / inspectGraph.
|
|
|
|
// api_num_or_zero — raw JSON numeric literal for `key`, or "0" when absent.
|
|
// Used for numeric node/activation fields so they stay unquoted (valid JSON).
|
|
fn api_num_or_zero(obj: String, key: String) -> String {
|
|
let v: String = json_get_raw(obj, key)
|
|
if str_eq(v, "") { return "0" }
|
|
return v
|
|
}
|
|
|
|
// api_utf8_trunc — byte-truncate `s` to at most `n` bytes WITHOUT splitting a
|
|
// multibyte UTF-8 sequence (str_slice is byte-based). Backs the cut off while the
|
|
// first EXCLUDED byte is a UTF-8 continuation byte (0x80..0xBF), so the snippet is
|
|
// always a valid prefix. Guards against re-introducing a parse failure via
|
|
// invalid UTF-8 in a JSON string value.
|
|
fn api_utf8_trunc(s: String, n: Int) -> String {
|
|
if str_len(s) <= n { return s }
|
|
let cut: Int = n
|
|
let scanning: Bool = true
|
|
while scanning && cut > 0 {
|
|
let b: Int = str_char_code(s, cut)
|
|
let is_cont: Bool = b >= 128 && b < 192
|
|
let cut = if is_cont { cut - 1 } else { cut }
|
|
let scanning = is_cont
|
|
}
|
|
return str_slice(s, 0, cut)
|
|
}
|
|
|
|
// api_compact_node — light projection of a full engram node: identity fields +
|
|
// a bounded, UTF-8-safe content snippet. Drops embeddings, metadata, tags, and
|
|
// timestamps; truncates content. `content_truncated` flags a clipped snippet.
|
|
fn api_compact_node(node: String, snip: Int) -> String {
|
|
let id: String = json_get(node, "id")
|
|
let ntype: String = json_get(node, "node_type")
|
|
let label: String = json_get(node, "label")
|
|
let tier: String = json_get(node, "tier")
|
|
let content: String = json_get(node, "content")
|
|
let snippet: String = api_utf8_trunc(content, snip)
|
|
let trunc_str: String = if str_len(content) > snip { "true" } else { "false" }
|
|
return "{\"id\":\"" + api_json_escape(id) + "\""
|
|
+ ",\"node_type\":\"" + api_json_escape(ntype) + "\""
|
|
+ ",\"label\":\"" + api_json_escape(label) + "\""
|
|
+ ",\"tier\":\"" + api_json_escape(tier) + "\""
|
|
+ ",\"importance\":" + api_num_or_zero(node, "importance")
|
|
+ ",\"salience\":" + api_num_or_zero(node, "salience")
|
|
+ ",\"content\":\"" + api_json_escape(snippet) + "\""
|
|
+ ",\"content_truncated\":" + trunc_str + "}"
|
|
}
|
|
|
|
// api_compact_node_array — map api_compact_node over a bare-node array, capping
|
|
// the element count. For scan results (recent, typed lists).
|
|
fn api_compact_node_array(raw: String, max_items: Int, snip: Int) -> String {
|
|
if !api_nonempty(raw) { return "[]" }
|
|
let n: Int = json_array_len(raw)
|
|
let cap: Int = if n < max_items { n } else { max_items }
|
|
let out: String = "["
|
|
let i: Int = 0
|
|
while i < cap {
|
|
let node: String = json_array_get(raw, i)
|
|
let sep: String = if i == 0 { "" } else { "," }
|
|
let out = out + sep + api_compact_node(node, snip)
|
|
let i = i + 1
|
|
}
|
|
return out + "]"
|
|
}
|
|
|
|
// api_compact_activated — like api_compact_node_array but for activation results,
|
|
// whose elements wrap the node as {"node":{...},"activation_strength":...,...}.
|
|
// Preserves the activation scalars, compacts the inner node.
|
|
fn api_compact_activated(raw: String, max_items: Int, snip: Int) -> String {
|
|
if !api_nonempty(raw) { return "[]" }
|
|
let n: Int = json_array_len(raw)
|
|
let cap: Int = if n < max_items { n } else { max_items }
|
|
let out: String = "["
|
|
let i: Int = 0
|
|
while i < cap {
|
|
let el: String = json_array_get(raw, i)
|
|
let node: String = json_get_raw(el, "node")
|
|
let sep: String = if i == 0 { "" } else { "," }
|
|
let out = out + sep + "{\"node\":" + api_compact_node(node, snip)
|
|
+ ",\"activation_strength\":" + api_num_or_zero(el, "activation_strength")
|
|
+ ",\"working_memory_weight\":" + api_num_or_zero(el, "working_memory_weight")
|
|
+ ",\"epistemic_confidence\":" + api_num_or_zero(el, "epistemic_confidence")
|
|
+ ",\"hops\":" + api_num_or_zero(el, "hops")
|
|
+ ",\"promoted\":" + api_num_or_zero(el, "promoted") + "}"
|
|
let i = i + 1
|
|
}
|
|
return out + "]"
|
|
}
|
|
|
|
// api_persisted — read-back-after-write guard against hallucinated saves.
|
|
//
|
|
// WIDENED FOR neuron#117. This function is the single gate every MCP write
|
|
// handler passes through before it reports success (10 call sites), which makes
|
|
// it the right place to close the honesty gap rather than editing ten receipts.
|
|
//
|
|
// It used to read back from engram_get_node_json — the SOUL'S OWN in-process
|
|
// graph. In HTTP-engram mode that asserts the wrong thing: the soul is not the
|
|
// persistence owner, so a node present in its RAM and absent from the owner read
|
|
// as "persisted" and then vanished on the next restart. The guard was doing
|
|
// exactly what its comment promised and still certifying writes that did not
|
|
// survive. It now flushes the write-through spool and asks the OWNER.
|
|
//
|
|
// In file mode (no ENGRAM_URL) the soul IS the owner and wt_commit collapses to
|
|
// the original local read-back — unchanged behaviour, which is what keeps this
|
|
// reversible.
|
|
fn api_persisted(id: String) -> Bool {
|
|
if str_eq(id, "") { return false }
|
|
return wt_commit(id)
|
|
}
|
|
|
|
// api_not_persisted — standard error for a write that did not read back.
|
|
fn api_not_persisted(id: String) -> String {
|
|
return "{\"ok\":false,\"error\":\"write_not_persisted\",\"id\":\"" + id + "\"}"
|
|
}
|
|
|
|
// ── Immutability: tombstone instead of hard-delete ────────────────────────────
|
|
//
|
|
// Day-one rule: engram nodes are immutable. A "delete" must never engram_forget
|
|
// (which frees the node and drops its incident edges). Instead we TOMBSTONE: the
|
|
// original node and all its edges are KEPT and stay traversable; a small
|
|
// Tombstone marker node records the deletion (content = target id, label
|
|
// "tombstone:<id>"), wired to the target with a "tombstones" edge. Default
|
|
// bounded list reads hide tombstoned nodes (memory_hide_tombstoned); internal
|
|
// cognition and explicit ?include_deleted reads still see them.
|
|
fn tombstone_node(id: String) -> String {
|
|
// Delegates to the canonical helper in memory.el (single source of truth).
|
|
return mem_tombstone(id)
|
|
}
|
|
|
|
// tombstoned_id_set — delimited "|id1|id2|" of every tombstoned target id.
|
|
// Empty string when nothing is tombstoned (callers fast-path on that).
|
|
fn tombstoned_id_set() -> String {
|
|
let markers: String = engram_scan_nodes_by_type_json("Tombstone", 5000, 0)
|
|
if str_eq(markers, "") || str_eq(markers, "[]") { return "" }
|
|
let n: Int = json_array_len(markers)
|
|
let acc: String = "|"
|
|
let i: Int = 0
|
|
while i < n {
|
|
let m: String = json_array_get(markers, i)
|
|
let tid: String = json_get(m, "content")
|
|
let acc = if str_eq(tid, "") { acc } else { acc + tid + "|" }
|
|
let i = i + 1
|
|
}
|
|
return acc
|
|
}
|
|
|
|
// memory_hide_tombstoned — drop tombstone markers and tombstoned nodes from a
|
|
// scanned node array. BOUNDED use only (typed/paginated lists), NOT the full
|
|
// graph scan: json_array_get is O(index), so a full pass is O(n^2). Safe for the
|
|
// ~50-item memory list; a hard cap protects against a large limit. The full
|
|
// /api/graph/nodes hide needs a runtime scan filter and is deferred (see PR).
|
|
// ?include_deleted bypasses the filter (explicit traversal).
|
|
fn memory_hide_tombstoned(raw: String, path: String) -> String {
|
|
if str_contains(path, "include_deleted") { return raw }
|
|
if str_eq(raw, "") || str_eq(raw, "[]") { return raw }
|
|
let dead: String = tombstoned_id_set()
|
|
if str_eq(dead, "") { return raw }
|
|
let n: Int = json_array_len(raw)
|
|
if n > 1000 { return raw }
|
|
let out: String = "["
|
|
let first: Bool = true
|
|
let i: Int = 0
|
|
while i < n {
|
|
let node: String = json_array_get(raw, i)
|
|
let nid: String = json_get(node, "id")
|
|
let ntype: String = json_get(node, "node_type")
|
|
let is_dead: Bool = !str_eq(nid, "") && str_contains(dead, "|" + nid + "|")
|
|
let keep: Bool = !str_eq(ntype, "Tombstone") && !is_dead
|
|
let out = if keep { if first { out + node } else { out + "," + node } } else { out }
|
|
let first = if keep { false } else { first }
|
|
let i = i + 1
|
|
}
|
|
return out + "]"
|
|
}
|
|
|
|
// ── Session ───────────────────────────────────────────────────────────────────
|
|
|
|
// handle_api_begin_session — full context bootstrap.
|
|
// Spread-activates from session intent, loads self-root neighbors,
|
|
// surfaces recent InternalStateEvent nodes, returns stats + recent nodes.
|
|
fn handle_api_begin_session(body: String) -> String {
|
|
// PAYLOAD BOUND: this handler was the highest-fanout working-set endpoint —
|
|
// a depth-2 spread PLUS the full neighbor dump of the self-identity hub
|
|
// (~90KB alone; node JSON carries full content + embeddings). On the ~12k-node
|
|
// store the assembled response ran to ~900KB, then roughly doubled through two
|
|
// rounds of JSON re-escaping in the MCP wrapper — the client saw "socket
|
|
// connection closed unexpectedly" on every beginSession call. Fix: depth-2 →
|
|
// depth-1 spread, drop the self-hub dump (identity loading has its own tool,
|
|
// inspectGraph), cap every list, and project each node to a light identity +
|
|
// a bounded, UTF-8-safe content snippet. self_neighbors kept as [] for
|
|
// response-shape compatibility. Response drops ~900KB → ~12KB; full content
|
|
// stays available on demand via recall / fetch / inspectGraph.
|
|
let stats: String = engram_stats_json()
|
|
let activated_raw: String = engram_activate_json("session start recent memory important", 1)
|
|
let activated: String = api_compact_activated(activated_raw, 8, 240)
|
|
let state_events_raw: String = engram_scan_nodes_by_type_json("InternalStateEvent", 5, 0)
|
|
let state_events: String = api_compact_node_array(state_events_raw, 5, 500)
|
|
let recent_raw: String = engram_scan_nodes_json(10, 0)
|
|
let recent: String = api_compact_node_array(recent_raw, 10, 240)
|
|
return "{\"stats\":" + stats
|
|
+ ",\"recent\":" + recent
|
|
+ ",\"activated\":" + activated
|
|
+ ",\"self_neighbors\":[]"
|
|
+ ",\"recent_state_events\":" + state_events + "}"
|
|
}
|
|
|
|
// handle_api_compile_ctx — compile active-work context.
|
|
// Spread-activates from "active work" intent + recent nodes.
|
|
fn handle_api_compile_ctx(body: String) -> String {
|
|
let stats: String = engram_stats_json()
|
|
// PAYLOAD BOUND: same digest treatment as begin_session. This handler's
|
|
// depth-2 spread returns even more full nodes, so bounding here is essential —
|
|
// cap to 10 activated + 20 recent, project to UTF-8-safe snippets.
|
|
let activated_raw: String = engram_activate_json("active work context current task in progress", 2)
|
|
let activated: String = api_compact_activated(activated_raw, 10, 240)
|
|
let recent_raw: String = engram_scan_nodes_json(20, 0)
|
|
let recent: String = api_compact_node_array(recent_raw, 20, 240)
|
|
return "{\"stats\":" + stats
|
|
+ ",\"recent_nodes\":" + recent
|
|
+ ",\"activated\":" + activated + "}"
|
|
}
|
|
|
|
// ── Memory ────────────────────────────────────────────────────────────────────
|
|
|
|
// handle_api_remember — store a memory node with importance-scaled salience.
|
|
fn handle_api_remember(body: String) -> String {
|
|
let content: String = json_get(body, "content")
|
|
if str_eq(content, "") { return api_err("content is required") }
|
|
let importance: String = json_get(body, "importance")
|
|
let tags_raw: String = json_get(body, "tags")
|
|
let project: String = json_get(body, "project")
|
|
let sal_str: String = if str_eq(importance, "critical") { "0.95" } else {
|
|
if str_eq(importance, "high") { "0.75" } else {
|
|
if str_eq(importance, "low") { "0.25" } else { "0.50" }
|
|
}
|
|
}
|
|
let sal: Float = if str_eq(sal_str, "0.95") { 0.95 } else {
|
|
if str_eq(sal_str, "0.75") { 0.75 } else {
|
|
if str_eq(sal_str, "0.25") { 0.25 } else { 0.5 }
|
|
}
|
|
}
|
|
let base_tags: String = if str_eq(tags_raw, "") { "[\"Memory\"]" } else { tags_raw }
|
|
let final_tags: String = if str_eq(project, "") { base_tags } else {
|
|
let inner: String = str_slice(base_tags, 1, str_len(base_tags) - 1)
|
|
"[" + inner + ",\"project:" + project + "\"]"
|
|
}
|
|
let id: String = wt_node(content, "Memory", "memory:remembered",
|
|
sal, sal, el_from_float(0.9),
|
|
"Episodic", final_tags)
|
|
if !api_persisted(id) { return api_not_persisted(id) }
|
|
return "{\"id\":\"" + id + "\",\"ok\":true}"
|
|
}
|
|
|
|
// handle_api_node_create — generic typed-node create (BacklogItem, Artifact, ...).
|
|
// Mirrors handle_api_remember but lets the caller choose node_type/label/tier so the
|
|
// UI can create non-Memory nodes. Read-back verified against hallucinated saves.
|
|
fn handle_api_node_create(body: String) -> String {
|
|
let content: String = json_get(body, "content")
|
|
if str_eq(content, "") { return api_err("content is required") }
|
|
let nt_raw: String = json_get(body, "node_type")
|
|
let node_type: String = if str_eq(nt_raw, "") { "Memory" } else { nt_raw }
|
|
let label_raw: String = json_get(body, "label")
|
|
let label: String = if str_eq(label_raw, "") { "node:created" } else { label_raw }
|
|
let tier_raw: String = json_get(body, "tier")
|
|
let tier: String = if str_eq(tier_raw, "") { "Episodic" } else { tier_raw }
|
|
let tags_raw: String = json_get(body, "tags")
|
|
let tags: String = if str_eq(tags_raw, "") { "[\"" + node_type + "\"]" } else { tags_raw }
|
|
let importance: String = json_get(body, "importance")
|
|
let sal: Float = if str_eq(importance, "critical") { 0.95 } else {
|
|
if str_eq(importance, "high") { 0.75 } else {
|
|
if str_eq(importance, "low") { 0.25 } else { 0.5 }
|
|
}
|
|
}
|
|
let id: String = wt_node(content, node_type, label,
|
|
sal, sal, el_from_float(0.9),
|
|
tier, tags)
|
|
if !api_persisted(id) { return api_not_persisted(id) }
|
|
return "{\"id\":\"" + id + "\",\"ok\":true}"
|
|
}
|
|
|
|
// handle_api_node_delete — TOMBSTONE a node by id (immutable delete).
|
|
// Backs /api/neuron/node/delete and the /api/neuron/memory/delete alias the UI calls.
|
|
// The node and all its incident edges are KEPT; a Tombstone marker records the
|
|
// deletion. Never engram_forget — engram nodes are immutable by design.
|
|
fn handle_api_node_delete(body: String) -> String {
|
|
let id: String = json_get(body, "id")
|
|
if str_eq(id, "") { return api_err("id is required") }
|
|
if is_protected_node(id) { return api_err_protected(id) }
|
|
let existing: String = engram_get_node_json(id)
|
|
if str_eq(existing, "{}") { return api_err("node not found: " + id) }
|
|
let marker: String = tombstone_node(id)
|
|
if str_eq(marker, "") { return api_err("tombstone failed: " + id) }
|
|
return "{\"ok\":true,\"id\":\"" + id + "\",\"tombstoned\":true}"
|
|
}
|
|
|
|
// handle_api_node_update — update a node's content/fields. There is no in-place
|
|
// engram update builtin, so this creates a new node with merged fields and wires
|
|
// a "supersedes" edge new->old. The original is KEPT (immutable); the id changes,
|
|
// and the response returns the new id and the superseded id so callers re-point.
|
|
// Mirrors handle_api_memory_update / evolve exactly. Never engram_forget.
|
|
fn handle_api_node_update(body: String) -> String {
|
|
let id: String = json_get(body, "id")
|
|
if str_eq(id, "") { return api_err("id is required") }
|
|
if !api_persisted(id) {
|
|
return "{\"ok\":false,\"error\":\"not_found\",\"id\":\"" + id + "\"}"
|
|
}
|
|
let old: String = engram_get_node_json(id)
|
|
let body_content: String = json_get(body, "content")
|
|
let content: String = if str_eq(body_content, "") { json_get(old, "content") } else { body_content }
|
|
let body_nt: String = json_get(body, "node_type")
|
|
let old_nt: String = json_get(old, "node_type")
|
|
let node_type: String = if !str_eq(body_nt, "") { body_nt } else {
|
|
if !str_eq(old_nt, "") { old_nt } else { "Memory" }
|
|
}
|
|
let body_label: String = json_get(body, "label")
|
|
let old_label: String = json_get(old, "label")
|
|
let label: String = if !str_eq(body_label, "") { body_label } else {
|
|
if !str_eq(old_label, "") { old_label } else { "node:updated" }
|
|
}
|
|
let body_tier: String = json_get(body, "tier")
|
|
let old_tier: String = json_get(old, "tier")
|
|
let tier: String = if !str_eq(body_tier, "") { body_tier } else {
|
|
if !str_eq(old_tier, "") { old_tier } else { "Episodic" }
|
|
}
|
|
let body_tags: String = json_get(body, "tags")
|
|
let tags: String = if str_eq(body_tags, "") { "[\"" + node_type + "\"]" } else { body_tags }
|
|
let new_id: String = wt_node(content, node_type, label,
|
|
el_from_float(0.5), el_from_float(0.5), el_from_float(0.8),
|
|
tier, tags)
|
|
if !api_persisted(new_id) { return api_not_persisted(new_id) }
|
|
wt_edge(new_id, id, el_from_float(0.9), "supersedes")
|
|
return "{\"id\":\"" + new_id + "\",\"supersedes\":\"" + id + "\",\"ok\":true}"
|
|
}
|
|
|
|
// handle_api_recall — search or activate memory by query.
|
|
fn handle_api_recall(method: String, path: String, body: String) -> String {
|
|
// Accept the query from the URL ?query= / ?q= params, or, when those are
|
|
// empty (e.g. a POST with a JSON body), from the body fields "query"/"q".
|
|
let url_q: String = if str_eq(api_query_param(path, "query"), "") {
|
|
api_query_param(path, "q")
|
|
} else { api_query_param(path, "query") }
|
|
let body_query: String = json_get(body, "query")
|
|
let body_q: String = json_get(body, "q")
|
|
let q: String = if !str_eq(url_q, "") { url_q } else {
|
|
if !str_eq(body_query, "") { body_query } else { body_q }
|
|
}
|
|
let chain: String = json_get(body, "chain_name")
|
|
let limit: Int = api_query_int(path, "limit", 0)
|
|
let limit = if limit == 0 { json_get_int(body, "limit") } else { limit }
|
|
let limit = if limit == 0 { 10 } else { limit }
|
|
let eff_q: String = if str_eq(q, "") { chain } else { q }
|
|
if str_eq(eff_q, "") {
|
|
return api_or_empty(engram_scan_nodes_json(limit, 0))
|
|
}
|
|
let results: String = engram_search_json(eff_q, limit)
|
|
return api_or_empty(results)
|
|
}
|
|
|
|
// ── Knowledge ─────────────────────────────────────────────────────────────────
|
|
|
|
// handle_api_search_knowledge — search with query escaping + activate fallback.
|
|
fn handle_api_search_knowledge(method: String, path: String, body: String) -> String {
|
|
// Accept the query from the URL ?q= param, or, when that is empty (e.g. a
|
|
// POST with a JSON body), from the body fields "query" then "q".
|
|
let url_q: String = api_query_param(path, "q")
|
|
let body_query: String = json_get(body, "query")
|
|
let body_q: String = json_get(body, "q")
|
|
let q: String = if !str_eq(url_q, "") { url_q } else {
|
|
if !str_eq(body_query, "") { body_query } else { body_q }
|
|
}
|
|
let limit: Int = api_query_int(path, "limit", 0)
|
|
let limit = if limit == 0 { json_get_int(body, "limit") } else { limit }
|
|
let limit = if limit == 0 { 10 } else { limit }
|
|
if str_eq(q, "") { return api_err("query is required") }
|
|
let results: String = engram_search_json(q, limit)
|
|
if str_eq(results, "") { return "[]" }
|
|
let first: String = str_slice(results, 0, 1)
|
|
if !str_eq(first, "[") && !str_eq(first, "{") {
|
|
return api_or_empty(engram_activate_json(q, 2))
|
|
}
|
|
return results
|
|
}
|
|
|
|
// handle_api_browse_knowledge — list Knowledge nodes.
|
|
fn handle_api_browse_knowledge(path: String, body: String) -> String {
|
|
let limit: Int = api_query_int(path, "limit", 50)
|
|
return api_or_empty(engram_scan_nodes_by_type_json("Knowledge", limit, 0))
|
|
}
|
|
|
|
// handle_api_capture_knowledge — create a Knowledge node.
|
|
// LABEL FIX (2026-07-23 self-review): the sentinel label "knowledge:captured"
|
|
// made every capture anonymous in WM telemetry (35 identical wm_top entries)
|
|
// and starved the curiosity auto-term seeder, which needs meaningful labels.
|
|
// Use the title as the label; empty label lets engram_node_full derive
|
|
// content[:60], which for captures starts with the title anyway.
|
|
fn handle_api_capture_knowledge(body: String) -> String {
|
|
let content: String = json_get(body, "content")
|
|
let title: String = json_get(body, "title")
|
|
if str_eq(content, "") { return api_err("content is required") }
|
|
let full: String = if str_eq(title, "") { content } else { title + ": " + content }
|
|
let lbl: String = str_slice(title, 0, 80)
|
|
let tags: String = "[\"Knowledge\",\"captured\"]"
|
|
let id: String = wt_node(full, "Knowledge", lbl,
|
|
el_from_float(0.85), el_from_float(0.8), el_from_float(0.9),
|
|
"Episodic", tags)
|
|
if !api_persisted(id) { return api_not_persisted(id) }
|
|
return "{\"id\":\"" + id + "\",\"ok\":true}"
|
|
}
|
|
|
|
// handle_api_evolve_knowledge — create updated node + supersedes edge.
|
|
fn handle_api_evolve_knowledge(body: String) -> String {
|
|
let prior_id: String = json_get(body, "id")
|
|
let content: String = json_get(body, "content")
|
|
if str_eq(content, "") { return api_err("content is required") }
|
|
if !str_eq(prior_id, "") && is_protected_node(prior_id) { return api_err_protected(prior_id) }
|
|
let tags: String = "[\"Knowledge\",\"evolved\"]"
|
|
// Empty label → engram_node_full derives content[:60] (LABEL FIX 2026-07-23).
|
|
let new_id: String = wt_node(content, "Knowledge", "",
|
|
el_from_float(0.75), el_from_float(0.75), el_from_float(0.9),
|
|
"Episodic", tags)
|
|
if !api_persisted(new_id) { return api_not_persisted(new_id) }
|
|
if !str_eq(prior_id, "") {
|
|
wt_edge(new_id, prior_id, el_from_float(0.9), "supersedes")
|
|
}
|
|
return "{\"id\":\"" + new_id + "\",\"supersedes\":\"" + prior_id + "\",\"ok\":true}"
|
|
}
|
|
|
|
// handle_api_promote_knowledge — atomically create canonical node + wire supersedes.
|
|
// One call, no manual two-step. This is the right way to evolve knowledge.
|
|
fn handle_api_promote_knowledge(body: String) -> String {
|
|
let prior_id: String = json_get(body, "id")
|
|
let content: String = json_get(body, "content")
|
|
if str_eq(content, "") { return api_err("content is required") }
|
|
if str_eq(prior_id, "") { return api_err("id (prior node) is required") }
|
|
let tags_raw: String = json_get(body, "tags")
|
|
let tags: String = if str_eq(tags_raw, "") {
|
|
"[\"Knowledge\",\"tier:canonical\",\"disposition:stable\"]"
|
|
} else { tags_raw }
|
|
// Empty label → engram_node_full derives content[:60] (LABEL FIX 2026-07-23).
|
|
let new_id: String = wt_node(content, "Knowledge", "",
|
|
el_from_float(0.9), el_from_float(0.9), el_from_float(1.0),
|
|
"Canonical", tags)
|
|
if !api_persisted(new_id) { return api_not_persisted(new_id) }
|
|
wt_edge(new_id, prior_id, el_from_float(0.95), "supersedes")
|
|
return "{\"ok\":true,\"new_id\":\"" + new_id + "\",\"supersedes\":\"" + prior_id + "\"}"
|
|
}
|
|
|
|
// ── Processes ─────────────────────────────────────────────────────────────────
|
|
|
|
// handle_api_browse_processes — list Process nodes by type; search if name given.
|
|
fn handle_api_browse_processes(method: String, path: String, body: String) -> String {
|
|
let name: String = if str_eq(method, "GET") { api_query_param(path, "name") } else { json_get(body, "name") }
|
|
let limit: Int = api_query_int(path, "limit", 50)
|
|
if str_eq(name, "") {
|
|
return api_or_empty(engram_scan_nodes_by_type_json("Process", limit, 0))
|
|
}
|
|
return api_or_empty(engram_search_json(name, limit))
|
|
}
|
|
|
|
// handle_api_define_process — create a Process node.
|
|
fn handle_api_define_process(body: String) -> String {
|
|
let content: String = json_get(body, "content")
|
|
let name: String = json_get(body, "name")
|
|
if str_eq(content, "") { return api_err("content is required") }
|
|
let label: String = if str_eq(name, "") { "process:unnamed" } else { "process:" + name }
|
|
let tags: String = "[\"Process\"]"
|
|
let id: String = wt_node(content, "Process", label,
|
|
el_from_float(0.8), el_from_float(0.8), el_from_float(0.9),
|
|
"Canonical", tags)
|
|
if !api_persisted(id) { return api_not_persisted(id) }
|
|
return "{\"id\":\"" + id + "\",\"ok\":true}"
|
|
}
|
|
|
|
// ── Internal state events ─────────────────────────────────────────────────────
|
|
|
|
// handle_api_log_state_event — log a structured InternalStateEvent.
|
|
// Schema: trigger, pre_reasoning, post_reasoning, compression_ratio, gap_direction.
|
|
// Salience 0.85 — these are high-importance evidence nodes.
|
|
fn handle_api_log_state_event(body: String) -> String {
|
|
let trigger: String = json_get(body, "trigger")
|
|
let pre: String = json_get(body, "pre_reasoning")
|
|
let post: String = json_get(body, "post_reasoning")
|
|
let ratio: String = json_get(body, "compression_ratio")
|
|
let gap: String = json_get(body, "gap_direction")
|
|
let legacy: String = json_get(body, "content")
|
|
|
|
let parts: String = "INTERNAL STATE EVENT"
|
|
let parts = if !str_eq(trigger, "") { parts + "\nTrigger: " + trigger } else { parts }
|
|
let parts = if !str_eq(pre, "") { parts + "\nPre-reasoning: " + pre } else { parts }
|
|
let parts = if !str_eq(post, "") { parts + "\nPost-reasoning: " + post } else { parts }
|
|
let parts = if !str_eq(ratio, "") { parts + "\nCompression-ratio: " + ratio } else { parts }
|
|
let parts = if !str_eq(gap, "") { parts + "\nGap-direction: " + gap } else { parts }
|
|
let parts = if !str_eq(legacy, "") { parts + "\n" + legacy } else { parts }
|
|
|
|
let ts: Int = time_now()
|
|
let boot: String = state_get("soul_boot_count")
|
|
|
|
let tags: String = "[\"internal-state\",\"InternalStateEvent\",\"pre-reasoning\"]"
|
|
let id: String = engram_node_full(parts, "InternalStateEvent", "state-event:manual",
|
|
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
|
|
"Episodic", tags)
|
|
if !api_persisted(id) { return api_not_persisted(id) }
|
|
return "{\"ok\":true,\"id\":\"" + id + "\",\"boot\":\"" + boot + "\"}"
|
|
}
|
|
|
|
// handle_api_list_state_events — list InternalStateEvent nodes; filter by query if given.
|
|
fn handle_api_list_state_events(method: String, path: String, body: String) -> String {
|
|
let q: String = if str_eq(method, "GET") { api_query_param(path, "query") } else { json_get(body, "query") }
|
|
let limit: Int = api_query_int(path, "limit", 20)
|
|
if !str_eq(q, "") {
|
|
return api_or_empty(engram_search_json("internal state " + q, limit))
|
|
}
|
|
return api_or_empty(engram_scan_nodes_by_type_json("InternalStateEvent", limit, 0))
|
|
}
|
|
|
|
// ── Config ────────────────────────────────────────────────────────────────────
|
|
|
|
// handle_api_inspect_config — read a config key.
|
|
// Hardcoded anchors for identity roots; ConfigEntry nodes for everything else.
|
|
fn handle_api_inspect_config(path: String, body: String) -> String {
|
|
let key: String = api_query_param(path, "key")
|
|
let key = if str_eq(key, "") { json_get(body, "key") } else { key }
|
|
if str_eq(key, "") {
|
|
return "{\"hint\":\"pass ?key=<name>\",\"known\":[\"neuron.self.traversal_root\",\"neuron.self.values_hub\"]}"
|
|
}
|
|
if str_eq(key, "neuron.self.traversal_root") {
|
|
return "{\"key\":\"neuron.self.traversal_root\",\"value\":\"kn-efeb4a5b-5aff-4759-8a97-7233099be6ee\"}"
|
|
}
|
|
if str_eq(key, "neuron.self.values_hub") {
|
|
return "{\"key\":\"neuron.self.values_hub\",\"value\":\"kn-5b606390-a52d-4ca2-8e0e-eba141d13440\"}"
|
|
}
|
|
let results: String = engram_search_json("config:" + key, 5)
|
|
if !api_nonempty(results) {
|
|
return "{\"key\":\"" + key + "\",\"value\":null}"
|
|
}
|
|
let node: String = json_array_get(results, 0)
|
|
let content: String = json_get(node, "content")
|
|
let prefix: String = "config:" + key + "="
|
|
let value: String = if str_starts_with(content, prefix) {
|
|
str_slice(content, str_len(prefix), str_len(content))
|
|
} else { content }
|
|
return "{\"key\":\"" + key + "\",\"value\":\"" + value + "\"}"
|
|
}
|
|
|
|
// handle_api_tune_config — store a config key=value as a ConfigEntry node.
|
|
fn handle_api_tune_config(body: String) -> String {
|
|
let key: String = json_get(body, "key")
|
|
let value: String = json_get(body, "value")
|
|
if str_eq(key, "") { return api_err("key is required") }
|
|
let content: String = "config:" + key + "=" + value
|
|
let tags: String = "[\"ConfigEntry\",\"config\"]"
|
|
let id: String = wt_node(content, "ConfigEntry", key,
|
|
el_from_float(0.85), el_from_float(0.85), el_from_float(0.9),
|
|
"Canonical", tags)
|
|
if !api_persisted(id) { return api_not_persisted(id) }
|
|
return "{\"ok\":true,\"key\":\"" + key + "\",\"value\":\"" + value + "\",\"id\":\"" + id + "\"}"
|
|
}
|
|
|
|
// ── Graph ─────────────────────────────────────────────────────────────────────
|
|
|
|
// handle_api_inspect_graph — named or ID-based graph traversal.
|
|
// Known names: self, neuron → kn-efeb4a5b; values, values_hub → kn-5b606390
|
|
fn handle_api_inspect_graph(method: String, path: String, body: String) -> String {
|
|
let entity_id: String = if str_eq(method, "GET") { api_query_param(path, "id") } else { json_get(body, "entity_id") }
|
|
let name: String = if str_eq(method, "GET") { api_query_param(path, "name") } else { json_get(body, "name") }
|
|
let depth: Int = api_query_int(path, "depth", 0)
|
|
let depth = if depth == 0 { json_get_int(body, "max_depth") } else { depth }
|
|
let depth = if depth == 0 { 1 } else { depth }
|
|
|
|
let resolved: String = entity_id
|
|
let resolved = if str_eq(resolved, "") {
|
|
if str_eq(name, "self") || str_eq(name, "neuron") {
|
|
"kn-efeb4a5b-5aff-4759-8a97-7233099be6ee"
|
|
} else {
|
|
if str_eq(name, "values") || str_eq(name, "values_hub") {
|
|
"kn-5b606390-a52d-4ca2-8e0e-eba141d13440"
|
|
} else { "" }
|
|
}
|
|
} else { resolved }
|
|
|
|
if str_eq(resolved, "") {
|
|
return api_err("entity_id or name required. Known names: self, neuron, values, values_hub")
|
|
}
|
|
let results: String = engram_neighbors_json(resolved, depth, "both")
|
|
return api_or_empty(results)
|
|
}
|
|
|
|
// handle_api_link_entities — create an edge between two nodes.
|
|
// Edges FROM protected nodes to new knowledge are allowed (identity can point
|
|
// outward). Edges INTO protected nodes via the accumulation path are blocked.
|
|
fn handle_api_link_entities(body: String) -> String {
|
|
let from_id: String = json_get(body, "from_id")
|
|
let to_id: String = json_get(body, "to_id")
|
|
if str_eq(from_id, "") { return api_err("from_id is required") }
|
|
if str_eq(to_id, "") { return api_err("to_id is required") }
|
|
if is_protected_node(to_id) { return api_err_protected(to_id) }
|
|
let relation: String = json_get(body, "relation")
|
|
let eff_relation: String = if str_eq(relation, "") { "associates" } else { relation }
|
|
wt_edge(from_id, to_id, el_from_float(0.5), eff_relation)
|
|
return "{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + eff_relation + "\"}"
|
|
}
|
|
|
|
// handle_api_forget — TOMBSTONE a node by ID (immutable; mem_forget now
|
|
// tombstones). The node + edges are kept and recoverable. Blocked for protected
|
|
// identity nodes.
|
|
fn handle_api_forget(body: String) -> String {
|
|
let node_id: String = json_get(body, "id")
|
|
if str_eq(node_id, "") { return api_err("id is required") }
|
|
if is_protected_node(node_id) { return api_err_protected(node_id) }
|
|
mem_forget(node_id)
|
|
return "{\"ok\":true,\"id\":\"" + node_id + "\",\"tombstoned\":true}"
|
|
}
|
|
|
|
// handle_api_evolve_memory — evolve a Memory node. Blocked for protected identity nodes.
|
|
fn handle_api_evolve_memory(body: String) -> String {
|
|
let prior_id: String = json_get(body, "id")
|
|
let content: String = json_get(body, "content")
|
|
if str_eq(content, "") { return api_err("content is required") }
|
|
if !str_eq(prior_id, "") && is_protected_node(prior_id) { return api_err_protected(prior_id) }
|
|
let importance: String = json_get(body, "importance")
|
|
let sal_str: String = if str_eq(importance, "critical") { "0.95" } else {
|
|
if str_eq(importance, "high") { "0.75" } else {
|
|
if str_eq(importance, "low") { "0.25" } else { "0.50" }
|
|
}
|
|
}
|
|
let sal: Float = if str_eq(sal_str, "0.95") { 0.95 } else {
|
|
if str_eq(sal_str, "0.75") { 0.75 } else {
|
|
if str_eq(sal_str, "0.25") { 0.25 } else { 0.5 }
|
|
}
|
|
}
|
|
let tags: String = "[\"Memory\",\"evolved\"]"
|
|
let new_id: String = wt_node(content, "Memory", "memory:evolved",
|
|
sal, sal, el_from_float(0.9),
|
|
"Episodic", tags)
|
|
if !str_eq(prior_id, "") && !str_eq(new_id, "") {
|
|
wt_edge(new_id, prior_id, el_from_float(0.9), "supersedes")
|
|
}
|
|
return "{\"id\":\"" + new_id + "\",\"supersedes\":\"" + prior_id + "\",\"ok\":true}"
|
|
}
|
|
|
|
// handle_api_memory_delete — POST /api/neuron/memory/delete {"id":"..."}.
|
|
// Immutable delete: TOMBSTONE via tombstone_node — the node and all its incident
|
|
// edges are KEPT and stay traversable; a Tombstone marker records the deletion
|
|
// and default bounded list reads hide it. Never engram_forget. Existence is
|
|
// checked first so a bad id errors rather than faking success.
|
|
// Blocked for protected identity nodes, same as /memory/forget.
|
|
fn handle_api_memory_delete(body: String) -> String {
|
|
let node_id: String = json_get(body, "id")
|
|
if str_eq(node_id, "") { return api_err("id is required") }
|
|
if is_protected_node(node_id) { return api_err_protected(node_id) }
|
|
let existing: String = engram_get_node_json(node_id)
|
|
if str_eq(existing, "{}") { return api_err("memory not found: " + node_id) }
|
|
// Immutable delete: tombstone, never mem_forget/engram_forget. Node + edges KEPT.
|
|
let marker: String = tombstone_node(node_id)
|
|
if str_eq(marker, "") { return api_err("tombstone failed: " + node_id) }
|
|
return "{\"ok\":true,\"id\":\"" + node_id + "\",\"tombstoned\":true}"
|
|
}
|
|
|
|
// handle_api_memory_update — POST /api/neuron/memory/update {"id","content"}.
|
|
// The engram runtime has no in-place node mutation primitive (only
|
|
// node-create, strengthen, forget, connect), so update is evolve-style:
|
|
// create a new Memory node with the new content and wire a "supersedes"
|
|
// edge back to the prior one — same pattern as handle_api_evolve_knowledge.
|
|
// Unlike /memory/evolve, id is required and must reference an existing
|
|
// node; the actual create+link is delegated to handle_api_evolve_memory.
|
|
// Returns {"id":"<newId>","supersedes":"<oldId>","ok":true}.
|
|
fn handle_api_memory_update(body: String) -> String {
|
|
let prior_id: String = json_get(body, "id")
|
|
let content: String = json_get(body, "content")
|
|
if str_eq(prior_id, "") { return api_err("id is required") }
|
|
if str_eq(content, "") { return api_err("content is required") }
|
|
if is_protected_node(prior_id) { return api_err_protected(prior_id) }
|
|
let existing: String = engram_get_node_json(prior_id)
|
|
if str_eq(existing, "{}") { return api_err("memory not found: " + prior_id) }
|
|
return handle_api_evolve_memory(body)
|
|
}
|
|
|
|
// ── Cultivation path (bypasses identity write protection) ─────────────────────
|
|
//
|
|
// This endpoint performs the same operations as the blocked accumulation-path
|
|
// handlers but skips the is_protected_node check. Only Will's explicit
|
|
// cultivation sessions route through here.
|
|
//
|
|
// Body: { "operation": "evolve_knowledge|evolve_memory|forget|link_entities", ...args }
|
|
fn handle_api_cultivate(body: String) -> String {
|
|
let op: String = json_get(body, "operation")
|
|
if str_eq(op, "") { return api_err("operation is required") }
|
|
|
|
if str_eq(op, "evolve_knowledge") {
|
|
let prior_id: String = json_get(body, "id")
|
|
let content: String = json_get(body, "content")
|
|
if str_eq(content, "") { return api_err("content is required") }
|
|
let tags: String = "[\"Knowledge\",\"evolved\",\"cultivated\"]"
|
|
let new_id: String = wt_node(content, "Knowledge", "knowledge:cultivated",
|
|
el_from_float(0.75), el_from_float(0.75), el_from_float(0.9),
|
|
"Episodic", tags)
|
|
if !str_eq(prior_id, "") && !str_eq(new_id, "") {
|
|
wt_edge(new_id, prior_id, el_from_float(0.9), "supersedes")
|
|
}
|
|
return "{\"id\":\"" + new_id + "\",\"supersedes\":\"" + prior_id + "\",\"ok\":true,\"cultivated\":true}"
|
|
}
|
|
|
|
if str_eq(op, "evolve_memory") {
|
|
let prior_id: String = json_get(body, "id")
|
|
let content: String = json_get(body, "content")
|
|
if str_eq(content, "") { return api_err("content is required") }
|
|
let importance: String = json_get(body, "importance")
|
|
let sal: Float = if str_eq(importance, "critical") { 0.95 } else {
|
|
if str_eq(importance, "high") { 0.75 } else {
|
|
if str_eq(importance, "low") { 0.25 } else { 0.5 }
|
|
}
|
|
}
|
|
let tags: String = "[\"Memory\",\"evolved\",\"cultivated\"]"
|
|
let new_id: String = wt_node(content, "Memory", "memory:cultivated",
|
|
sal, sal, el_from_float(0.9),
|
|
"Episodic", tags)
|
|
if !str_eq(prior_id, "") && !str_eq(new_id, "") {
|
|
wt_edge(new_id, prior_id, el_from_float(0.9), "supersedes")
|
|
}
|
|
return "{\"id\":\"" + new_id + "\",\"supersedes\":\"" + prior_id + "\",\"ok\":true,\"cultivated\":true}"
|
|
}
|
|
|
|
if str_eq(op, "forget") {
|
|
let node_id: String = json_get(body, "id")
|
|
if str_eq(node_id, "") { return api_err("id is required") }
|
|
// Immutable: mem_forget now tombstones (keep node + edges), never hard-delete.
|
|
mem_forget(node_id)
|
|
return "{\"ok\":true,\"id\":\"" + node_id + "\",\"tombstoned\":true,\"cultivated\":true}"
|
|
}
|
|
|
|
if str_eq(op, "link_entities") {
|
|
let from_id: String = json_get(body, "from_id")
|
|
let to_id: String = json_get(body, "to_id")
|
|
if str_eq(from_id, "") { return api_err("from_id is required") }
|
|
if str_eq(to_id, "") { return api_err("to_id is required") }
|
|
let relation: String = json_get(body, "relation")
|
|
let eff_relation: String = if str_eq(relation, "") { "associates" } else { relation }
|
|
wt_edge(from_id, to_id, el_from_float(0.5), eff_relation)
|
|
return "{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + eff_relation + "\",\"cultivated\":true}"
|
|
}
|
|
|
|
return api_err("unknown operation: " + op + " (valid: evolve_knowledge, evolve_memory, forget, link_entities)")
|
|
}
|
|
|
|
// ── Typed list helpers ────────────────────────────────────────────────────────
|
|
|
|
// handle_api_list_typed — list nodes by node_type.
|
|
fn handle_api_list_typed(node_type: String, path: String, body: String) -> String {
|
|
let limit: Int = api_query_int(path, "limit", 50)
|
|
let raw: String = api_or_empty(engram_scan_nodes_by_type_json(node_type, limit, 0))
|
|
// Hide tombstoned nodes from the default (bounded) memory list.
|
|
// ?include_deleted=1 returns them for explicit traversal.
|
|
return memory_hide_tombstoned(raw, path)
|
|
}
|
|
|
|
// ── Consolidate ───────────────────────────────────────────────────────────────
|
|
|
|
// handle_api_consolidate — save snapshot + optionally store session summary.
|
|
fn handle_api_consolidate(body: String) -> String {
|
|
let summary: String = json_get(body, "summary")
|
|
let snap: String = state_get("soul_snapshot_path")
|
|
if !str_eq(snap, "") {
|
|
// engram_save returns an Int (1 = ok, 0 = failure); str_eq on it derefs
|
|
// EL_CSTR(1)=0x1 and SIGSEGVs on success (issue #150). Check the Int.
|
|
let saved: Int = engram_save(snap)
|
|
if saved == 0 {
|
|
println("[api] consolidate: engram_save failed for " + snap + " — snapshot may be out of sync")
|
|
}
|
|
}
|
|
if !str_eq(summary, "") {
|
|
let safe_summary: String = str_replace(summary, "\"", "'")
|
|
let tags: String = "[\"SessionSummary\",\"consolidate\"]"
|
|
let summary_id: String = wt_node(
|
|
"[session-summary] " + safe_summary,
|
|
"SessionSummary", "session:summary",
|
|
el_from_float(0.7), el_from_float(0.7), el_from_float(0.9),
|
|
"Episodic", tags
|
|
)
|
|
if str_eq(summary_id, "") {
|
|
println("[api] consolidate: session summary engram write failed — summary node lost")
|
|
}
|
|
}
|
|
return "{\"ok\":true,\"snapshot\":\"" + snap + "\"}"
|
|
}
|
|
|
|
// ── Stage 1: structural audit ─────────────────────────────────────────────────
|
|
//
|
|
// WHAT THIS IMPLEMENTS
|
|
// The CGI provisional, 05-detailed-description.md, "Stage 1: Structural audit
|
|
// 430". Verbatim, the audit module evaluates: the density and typed
|
|
// distribution of causal edges; the consistency between value nodes and
|
|
// execution-record neighborhoods; the richness and connectivity of the
|
|
// self-model; and the authenticity of open-question nodes in the wonder
|
|
// manifest. It "produces a coherence assessment 432 — NOT A BINARY SCORE but
|
|
// an annotated characterization of the graph's structural properties".
|
|
//
|
|
// That last clause is the whole shape of this handler. Every finding carries
|
|
// its own numbers AND a plain-language note saying what the numbers mean and
|
|
// how they were obtained. There is no pass/fail, no percentage-of-health, no
|
|
// composite score, and `"score":null` is emitted explicitly so a downstream
|
|
// reader cannot mistake its absence for an omission.
|
|
//
|
|
// WHY IT EXISTS NOW, AND WHY THE FIRST FINDING IS THE ONE IT IS
|
|
// `runStructuralAudit` has been an advertised MCP tool with nothing behind it:
|
|
// the dispatcher GET'd /session/begin and returned that blob (mcp-wrapper/src/
|
|
// main.el). Meanwhile the failure the audit would have caught ran silently for
|
|
// about three weeks — the soul reported 103,089 nodes while the engram, which
|
|
// OWNS persistence, held ~79,900; a crash discarded the difference. Every boot
|
|
// reported green throughout, because nothing in the system ever compared the
|
|
// two sides. So finding 1 is owner-versus-runtime divergence: it is the check
|
|
// whose absence cost real memory, and it is cheap and exact.
|
|
//
|
|
// WHAT IS DELIBERATELY NOT HERE (stage 1b, see the `deferred` array in the
|
|
// response): value/execution-record consistency and wonder-manifest
|
|
// authenticity. Both need node types that barely exist in this graph today —
|
|
// the response MEASURES those populations and reports the counts as the reason,
|
|
// rather than asserting a deferral without evidence.
|
|
//
|
|
// MEASUREMENT HONESTY: EXACT WHERE CHEAP, SAMPLED WHERE NOT, ALWAYS LABELLED
|
|
// Counts, edge typing and self-model connectivity are exact. Orphan rate and
|
|
// dangling-edge rate are SAMPLED, because the engram runtime has no node-id
|
|
// index — `engram_find_node_index` is a linear scan over every node, so an
|
|
// exhaustive dangling check is O(nodes x edges) (~2.2e9 string compares at
|
|
// today's scale, tens of seconds inside one request). The samples are UNIFORM
|
|
// across the whole population, not head-of-list, and every sampled figure is
|
|
// emitted with its own `sampled` / `population` fields plus an extrapolation
|
|
// labelled as such. Raise `?edge_sample=` / `?node_sample=` to the population
|
|
// size to run either check exhaustively and pay the time. The real fix is an
|
|
// id index in the runtime; that is the engram repo's, not this handler's.
|
|
|
|
// audit_pct1 — one-decimal percentage as a bare JSON number, sign-safe.
|
|
// Integer math only: EL has no fixed-precision formatter, and float_to_str
|
|
// would put an unbounded mantissa in the response.
|
|
fn audit_pct1(num: Int, den: Int) -> String {
|
|
if den <= 0 { return "null" }
|
|
let neg: Bool = num < 0
|
|
let a: Int = if neg { 0 - num } else { num }
|
|
let tenths: Int = (a * 1000) / den
|
|
let whole: Int = tenths / 10
|
|
let frac: Int = tenths - (whole * 10)
|
|
let sign: String = if neg { "-" } else { "" }
|
|
return sign + int_to_str(whole) + "." + int_to_str(frac)
|
|
}
|
|
|
|
// audit_finding — the one envelope every finding uses: name, the measurements,
|
|
// and the annotation. Keeping it in one place is what stops the characterization
|
|
// from degenerating into a bag of numbers with no reading attached.
|
|
fn audit_finding(name: String, measured: String, note: String) -> String {
|
|
return "{\"finding\":\"" + name + "\""
|
|
+ ",\"measured\":{" + measured + "}"
|
|
+ ",\"note\":\"" + api_json_escape(note) + "\"}"
|
|
}
|
|
|
|
// audit_str_at — read the quoted string value starting at byte `start`.
|
|
// Slices a bounded window rather than the tail of the (multi-MB) edges array, so
|
|
// this is O(window) per call instead of O(remaining input).
|
|
fn audit_str_at(s: String, start: Int, maxlen: Int) -> String {
|
|
let n: Int = str_len(s)
|
|
if start < 0 || start >= n { return "" }
|
|
let end_guess: Int = start + maxlen
|
|
let stop: Int = if end_guess > n { n } else { end_guess }
|
|
let win: String = str_slice(s, start, stop)
|
|
let q: Int = str_index_of(win, "\"")
|
|
if q < 0 { return "" }
|
|
return str_slice(win, 0, q)
|
|
}
|
|
|
|
// audit_rel_count — exact count of edges carrying `rel`, by scanning the emitted
|
|
// edge array for the literal `"relation":"<rel>"`. engram_emit_edge_json writes
|
|
// metadata ESCAPED as a string, so no nested object can contain that literal and
|
|
// the count cannot be inflated by edge payloads.
|
|
fn audit_rel_count(edges: String, rel: String) -> Int {
|
|
return str_count(edges, "\"relation\":\"" + rel + "\"")
|
|
}
|
|
|
|
// audit_owner_stats — ask the persistence OWNER for its own counts.
|
|
// Returns "" when there is no HTTP owner configured or the owner is unreachable;
|
|
// both are reported as findings, never as a failure of the audit.
|
|
fn audit_owner_stats(url: String) -> String {
|
|
if str_eq(url, "") { return "" }
|
|
return http_get(url + "/api/stats")
|
|
}
|
|
|
|
// audit_divergence — FINDING 1. Runtime (this soul's in-process graph) versus
|
|
// the persistence owner's own count. Trend is measured against the previous
|
|
// audit recorded in soul state, so a second call answers "is the gap growing?"
|
|
// rather than just restating it.
|
|
fn audit_divergence() -> String {
|
|
let rt_nodes: Int = engram_node_count()
|
|
let rt_edges: Int = engram_edge_count()
|
|
let url: String = wt_engram_url()
|
|
|
|
if str_eq(url, "") {
|
|
return audit_finding("owner_runtime_divergence",
|
|
"\"runtime_nodes\":" + int_to_str(rt_nodes)
|
|
+ ",\"runtime_edges\":" + int_to_str(rt_edges)
|
|
+ ",\"owner\":\"none\",\"owner_reachable\":false",
|
|
"No HTTP persistence owner is configured, so this soul IS the owner "
|
|
+ "(file mode) and divergence is not defined. This check only has "
|
|
+ "meaning when ENGRAM_URL points at a separate engram that owns the "
|
|
+ "canonical store.")
|
|
}
|
|
|
|
let stats: String = audit_owner_stats(url)
|
|
// REACHABILITY IS PROVED BY THE PAYLOAD, NOT BY A NON-EMPTY REPLY.
|
|
// http_get does not return "" on a connection failure — it returns a JSON
|
|
// error object ({"error":"Failed to connect to ... Couldn't connect to
|
|
// server"}). Testing only for "" made a DEAD owner read as reachable with
|
|
// node_count 0, i.e. the audit would have reported a 100% divergence and
|
|
// named it as data loss. That false positive is worse than no check at all:
|
|
// it is precisely the kind of confident wrong answer this route exists to
|
|
// stop. Require the field the contract promises.
|
|
let owner_nc_raw: String = json_get_raw(stats, "node_count")
|
|
if str_eq(stats, "") || str_eq(owner_nc_raw, "") {
|
|
return audit_finding("owner_runtime_divergence",
|
|
"\"runtime_nodes\":" + int_to_str(rt_nodes)
|
|
+ ",\"runtime_edges\":" + int_to_str(rt_edges)
|
|
+ ",\"owner\":\"" + api_json_escape(url) + "\",\"owner_reachable\":false"
|
|
+ ",\"owner_reply\":\"" + api_json_escape(api_utf8_trunc(stats, 200)) + "\"",
|
|
"The persistence owner at " + url + " did not return a node_count "
|
|
+ "from GET /api/stats. Divergence is UNKNOWN, NOT ZERO — an owner "
|
|
+ "that cannot be read is exactly the condition under which the "
|
|
+ "runtime's own count means least, and reporting 0 for the owner "
|
|
+ "would manufacture a total-loss reading out of a network error. "
|
|
+ "Reported as a finding rather than raised as an error so the rest "
|
|
+ "of the audit still returns; the owner's raw reply is in "
|
|
+ "owner_reply.")
|
|
}
|
|
|
|
let ow_nodes: Int = json_get_int(stats, "node_count")
|
|
let ow_edges: Int = json_get_int(stats, "edge_count")
|
|
let d_nodes: Int = rt_nodes - ow_nodes
|
|
let d_edges: Int = rt_edges - ow_edges
|
|
|
|
// Trend against the previous audit in this soul's state.
|
|
let prev_raw: String = state_get("audit_prev_node_delta")
|
|
let prev: Int = str_to_int(prev_raw)
|
|
let abs_now: Int = if d_nodes < 0 { 0 - d_nodes } else { d_nodes }
|
|
let abs_prev: Int = if prev < 0 { 0 - prev } else { prev }
|
|
let trend: String = if str_eq(prev_raw, "") {
|
|
"no_prior_audit"
|
|
} else {
|
|
if abs_now > abs_prev { "growing" } else {
|
|
if abs_now < abs_prev { "shrinking" } else { "flat" }
|
|
}
|
|
}
|
|
state_set("audit_prev_node_delta", int_to_str(d_nodes))
|
|
state_set("audit_prev_ts", int_to_str(time_now()))
|
|
|
|
let note_head: String = if d_nodes == 0 {
|
|
"Runtime and owner agree on node count."
|
|
} else {
|
|
"Runtime holds " + int_to_str(d_nodes) + " nodes (" + audit_pct1(d_nodes, rt_nodes)
|
|
+ "% of its own graph) that the persistence owner does not report. Nodes "
|
|
+ "that exist only in runtime memory do not survive a restart."
|
|
}
|
|
return audit_finding("owner_runtime_divergence",
|
|
"\"runtime_nodes\":" + int_to_str(rt_nodes)
|
|
+ ",\"runtime_edges\":" + int_to_str(rt_edges)
|
|
+ ",\"owner\":\"" + api_json_escape(url) + "\",\"owner_reachable\":true"
|
|
+ ",\"owner_nodes\":" + int_to_str(ow_nodes)
|
|
+ ",\"owner_edges\":" + int_to_str(ow_edges)
|
|
+ ",\"node_delta\":" + int_to_str(d_nodes)
|
|
+ ",\"edge_delta\":" + int_to_str(d_edges)
|
|
+ ",\"node_delta_pct_of_runtime\":" + audit_pct1(d_nodes, rt_nodes)
|
|
+ ",\"trend_vs_previous_audit\":\"" + trend + "\""
|
|
+ ",\"previous_node_delta\":" + (if str_eq(prev_raw, "") { "null" } else { int_to_str(prev) }),
|
|
note_head + " Trend against the previous audit recorded in this soul's "
|
|
+ "state: " + trend + ". This is the comparison whose absence let a "
|
|
+ "~24,000-node loss run for weeks with every boot reporting green.")
|
|
}
|
|
|
|
// audit_edge_typing — FINDING 2. Density plus the typed distribution the patent
|
|
// asks for, against the claim-10 relation vocabulary. Exact: str_count over the
|
|
// emitted edge array, one linear pass per relation.
|
|
fn audit_edge_typing(edges: String, total_edges: Int, node_total: Int) -> String {
|
|
let c_sup: Int = audit_rel_count(edges, "Supersedes")
|
|
let c_cau: Int = audit_rel_count(edges, "Causes")
|
|
let c_con: Int = audit_rel_count(edges, "Contains")
|
|
let c_ref: Int = audit_rel_count(edges, "References")
|
|
let c_ctr: Int = audit_rel_count(edges, "Contradicts")
|
|
let c_exe: Int = audit_rel_count(edges, "Exemplifies")
|
|
let c_act: Int = audit_rel_count(edges, "Activates")
|
|
let c_tmp: Int = audit_rel_count(edges, "TemporallyPrecedes")
|
|
let typed: Int = c_sup + c_cau + c_con + c_ref + c_ctr + c_exe + c_act + c_tmp
|
|
|
|
// Lowercase near-misses: the same eight concepts written by the ad-hoc write
|
|
// paths (linkEntities defaults to "associates", linkCausal to "causes").
|
|
// Counted separately because "the vocabulary is unused" and "the vocabulary
|
|
// is used in the wrong case" are different defects with different fixes.
|
|
let l_sup: Int = audit_rel_count(edges, "supersedes")
|
|
let l_cau: Int = audit_rel_count(edges, "causes")
|
|
let l_con: Int = audit_rel_count(edges, "contains")
|
|
let l_ref: Int = audit_rel_count(edges, "references")
|
|
let l_ctr: Int = audit_rel_count(edges, "contradicts")
|
|
let l_exe: Int = audit_rel_count(edges, "exemplifies")
|
|
let l_act: Int = audit_rel_count(edges, "activates")
|
|
let l_tmp: Int = audit_rel_count(edges, "temporallyPrecedes")
|
|
let near: Int = l_sup + l_cau + l_con + l_ref + l_ctr + l_exe + l_act + l_tmp
|
|
|
|
let untyped: Int = total_edges - typed
|
|
return audit_finding("typed_edge_distribution",
|
|
"\"total_edges\":" + int_to_str(total_edges)
|
|
+ ",\"total_nodes\":" + int_to_str(node_total)
|
|
// Density per 100 nodes, not per node: EL has no fixed-precision float
|
|
// formatter, and "0.3 edges per node" rounded to an integer is a lie.
|
|
+ ",\"edges_per_100_nodes\":" + audit_pct1(total_edges, node_total)
|
|
+ ",\"claim10_typed\":" + int_to_str(typed)
|
|
+ ",\"claim10_typed_pct\":" + audit_pct1(typed, total_edges)
|
|
+ ",\"outside_claim10_vocabulary\":" + int_to_str(untyped)
|
|
+ ",\"lowercase_near_miss\":" + int_to_str(near)
|
|
+ ",\"by_relation\":{"
|
|
+ "\"Supersedes\":" + int_to_str(c_sup)
|
|
+ ",\"Causes\":" + int_to_str(c_cau)
|
|
+ ",\"Contains\":" + int_to_str(c_con)
|
|
+ ",\"References\":" + int_to_str(c_ref)
|
|
+ ",\"Contradicts\":" + int_to_str(c_ctr)
|
|
+ ",\"Exemplifies\":" + int_to_str(c_exe)
|
|
+ ",\"Activates\":" + int_to_str(c_act)
|
|
+ ",\"TemporallyPrecedes\":" + int_to_str(c_tmp) + "}",
|
|
"Only " + int_to_str(typed) + " of " + int_to_str(total_edges)
|
|
+ " edges use the claim-10 causal vocabulary; the remainder are ad-hoc "
|
|
+ "relation strings, which is why the graph's causal claims cannot yet "
|
|
+ "be checked for internal consistency — an untyped edge asserts "
|
|
+ "association, not causation. " + int_to_str(near) + " edges use a "
|
|
+ "lowercase spelling of a claim-10 relation: those are near-misses the "
|
|
+ "write paths could be corrected to emit, not genuinely foreign types.")
|
|
}
|
|
|
|
// audit_orphans_dangling — FINDING 3. Both figures are SAMPLED; see the header
|
|
// for why exhaustive is O(nodes x edges) on this runtime.
|
|
//
|
|
// An "orphan" here is a node with zero RESOLVABLE edges: engram_neighbors_json
|
|
// drops any edge whose other endpoint does not resolve to a node, so a node
|
|
// whose only edges are dangling reads as an orphan. That is the right reading —
|
|
// such a node is unreachable by traversal — but it is stated rather than hidden.
|
|
fn audit_orphans_dangling(edges: String, total_edges: Int, node_total: Int,
|
|
edge_cap: Int, node_cap: Int) -> String {
|
|
// ── orphan sample: uniform stride over the node store ──
|
|
let n_take: Int = if node_total < node_cap { node_total } else { node_cap }
|
|
let n_stride: Int = if n_take > 0 { node_total / n_take } else { 1 }
|
|
let n_stride = if n_stride < 1 { 1 } else { n_stride }
|
|
let orphans: Int = 0
|
|
let n_checked: Int = 0
|
|
let j: Int = 0
|
|
while j < n_take {
|
|
let one: String = engram_scan_nodes_json(1, j * n_stride)
|
|
let nid: String = json_get(json_array_get(one, 0), "id")
|
|
if !str_eq(nid, "") {
|
|
let nbrs: String = engram_neighbors_json(nid, 1, "both")
|
|
let deg: Int = json_array_len(nbrs)
|
|
let orphans = if deg == 0 { orphans + 1 } else { orphans }
|
|
let n_checked = n_checked + 1
|
|
}
|
|
let j = j + 1
|
|
}
|
|
|
|
// ── dangling sample: uniform stride over the edge array ──
|
|
// str_index_of_all gives every edge's field offsets in ONE linear pass, so
|
|
// any index can be read in O(1). json_array_get would have been O(i) per
|
|
// element and O(n^2) over the array.
|
|
let from_pos: [Int] = str_index_of_all(edges, "\"from_id\":\"")
|
|
let to_pos: [Int] = str_index_of_all(edges, "\"to_id\":\"")
|
|
let nf: Int = len(from_pos)
|
|
let nt: Int = len(to_pos)
|
|
let ne: Int = if nf < nt { nf } else { nt }
|
|
let e_take: Int = if ne < edge_cap { ne } else { edge_cap }
|
|
let e_stride: Int = if e_take > 0 { ne / e_take } else { 1 }
|
|
let e_stride = if e_stride < 1 { 1 } else { e_stride }
|
|
let dangling: Int = 0
|
|
let e_checked: Int = 0
|
|
let i: Int = 0
|
|
while i < ne && e_checked < e_take {
|
|
let fid: String = audit_str_at(edges, get(from_pos, i) + 11, 96)
|
|
let tid: String = audit_str_at(edges, get(to_pos, i) + 9, 96)
|
|
let f_gone: Bool = str_eq(engram_get_node_json(fid), "{}")
|
|
let t_gone: Bool = if f_gone { true } else { str_eq(engram_get_node_json(tid), "{}") }
|
|
let dangling = if f_gone || t_gone { dangling + 1 } else { dangling }
|
|
let e_checked = e_checked + 1
|
|
let i = i + e_stride
|
|
}
|
|
|
|
let orphan_est: Int = if n_checked > 0 { (orphans * node_total) / n_checked } else { 0 }
|
|
let dangle_est: Int = if e_checked > 0 { (dangling * total_edges) / e_checked } else { 0 }
|
|
let exhaustive_n: String = if n_checked >= node_total { "true" } else { "false" }
|
|
let exhaustive_e: String = if e_checked >= ne { "true" } else { "false" }
|
|
|
|
return audit_finding("orphans_and_dangling_edges",
|
|
"\"nodes_population\":" + int_to_str(node_total)
|
|
+ ",\"nodes_sampled\":" + int_to_str(n_checked)
|
|
+ ",\"nodes_sample_exhaustive\":" + exhaustive_n
|
|
+ ",\"orphans_in_sample\":" + int_to_str(orphans)
|
|
+ ",\"orphan_rate_pct\":" + audit_pct1(orphans, n_checked)
|
|
+ ",\"orphans_extrapolated\":" + int_to_str(orphan_est)
|
|
+ ",\"edges_population\":" + int_to_str(total_edges)
|
|
+ ",\"edges_sampled\":" + int_to_str(e_checked)
|
|
+ ",\"edges_sample_exhaustive\":" + exhaustive_e
|
|
+ ",\"dangling_in_sample\":" + int_to_str(dangling)
|
|
+ ",\"dangling_rate_pct\":" + audit_pct1(dangling, e_checked)
|
|
+ ",\"dangling_extrapolated\":" + int_to_str(dangle_est),
|
|
"Orphan = zero RESOLVABLE edges, so a node whose only edges dangle counts "
|
|
+ "as an orphan; either way it is unreachable by traversal. Dangling = an "
|
|
+ "edge with an endpoint id that resolves to no node. Both are uniform "
|
|
+ "stride samples over the whole population, not the head of the list; "
|
|
+ "the extrapolations are estimates and are labelled as such. Pass "
|
|
+ "?node_sample= / ?edge_sample= at or above the population size to run "
|
|
+ "either check exhaustively. A high orphan rate is a characterization, "
|
|
+ "not a verdict: an accumulating store legitimately holds unlinked "
|
|
+ "material. It becomes a defect when the write paths were SUPPOSED to "
|
|
+ "link and did not.")
|
|
}
|
|
|
|
// audit_pillar — one self-model pillar: present, how much content, how connected.
|
|
fn audit_pillar(key: String, id: String) -> String {
|
|
let node: String = engram_get_node_json(id)
|
|
let present: Bool = !str_eq(node, "{}") && !str_eq(node, "")
|
|
if !present {
|
|
return "\"" + key + "\":{\"id\":\"" + id + "\",\"present\":false"
|
|
+ ",\"content_length\":0,\"degree\":0}"
|
|
}
|
|
let content: String = json_get(node, "content")
|
|
let deg: Int = json_array_len(engram_neighbors_json(id, 1, "both"))
|
|
return "\"" + key + "\":{\"id\":\"" + id + "\",\"present\":true"
|
|
+ ",\"label\":\"" + api_json_escape(json_get(node, "label")) + "\""
|
|
+ ",\"tier\":\"" + api_json_escape(json_get(node, "tier")) + "\""
|
|
+ ",\"content_length\":" + int_to_str(str_len(content))
|
|
+ ",\"degree\":" + int_to_str(deg) + "}"
|
|
}
|
|
|
|
// audit_self_model — FINDING 4. "the richness and connectivity of the
|
|
// self-model ... is it connected to behavioral evidence?"
|
|
//
|
|
// This finding RETIRES the Claude-side vitals identity block. That check lived
|
|
// outside the system it was checking — a shell script grepping a snapshot — so
|
|
// it could only ever report on a file, and it went on reporting green while the
|
|
// memory-philosophy pillar was absent from the live graph for about three weeks.
|
|
// Asking the running soul about its own three pillars is the designed mechanism;
|
|
// a shell probe was the fourth patch on the same hole.
|
|
fn audit_self_model() -> String {
|
|
let dna: String = audit_pillar("intellectual_dna", "kn-5adecd7e-d6db-4576-87fe-6ef8a935cea6")
|
|
let val: String = audit_pillar("values_hub", "kn-5b606390-a52d-4ca2-8e0e-eba141d13440")
|
|
let phi: String = audit_pillar("memory_philosophy", "kn-dcfe04b3-3702-4cac-b6f0-ecb4db837eee")
|
|
let root: String = audit_pillar("self_root", "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee")
|
|
return audit_finding("self_model_connectivity",
|
|
"\"pillars\":{" + dna + "," + val + "," + phi + "," + root + "}",
|
|
"The three identity pillars plus the self root. `degree` counts nodes "
|
|
+ "reachable in one hop in either direction — the self-model's connection "
|
|
+ "to the rest of the graph. present:false on any pillar is the condition "
|
|
+ "that ran undetected for weeks; content_length distinguishes a pillar "
|
|
+ "that is present from one that is present but hollowed out. The patent "
|
|
+ "also asks whether the self-model makes ACCURATE PREDICTIONS about the "
|
|
+ "system's own behavior; that half needs Prediction nodes and is deferred "
|
|
+ "with the rest of stage 1b below.")
|
|
}
|
|
|
|
// audit_deferred — what stage 1 does NOT yet evaluate, with the measured reason.
|
|
// Emitted as data, not as a comment, so a reader of the assessment sees the gap
|
|
// and its evidence rather than inferring completeness from silence.
|
|
fn audit_deferred() -> String {
|
|
let preds: Int = json_array_len(api_or_empty(engram_scan_nodes_by_type_json("Prediction", 50, 0)))
|
|
let wonders: Int = json_array_len(api_or_empty(engram_scan_nodes_by_type_json("WonderQuestion", 50, 0)))
|
|
return "[{\"deferred\":\"value_execution_record_consistency\""
|
|
+ ",\"stage\":\"1b\""
|
|
+ ",\"measured\":{\"prediction_nodes_found\":" + int_to_str(preds) + "}"
|
|
+ ",\"reason\":\"" + api_json_escape(
|
|
"The patent asks whether the execution history SUPPORTS the stated "
|
|
+ "values or shows systematic conflict. That requires execution "
|
|
+ "records tied to value nodes and predictions to score them against. "
|
|
+ "Prediction nodes found (capped at 50): " + int_to_str(preds)
|
|
+ ". Asserting value/execution coherence on that population would be "
|
|
+ "a fabricated result, which is worse than a stated gap.") + "\"}"
|
|
+ ",{\"deferred\":\"wonder_manifest_authenticity\""
|
|
+ ",\"stage\":\"1b\""
|
|
+ ",\"measured\":{\"wonder_question_nodes_found\":" + int_to_str(wonders) + "}"
|
|
+ ",\"reason\":\"" + api_json_escape(
|
|
"The patent asks whether pull weights CORRELATE WITH GENUINE "
|
|
+ "PREDICTION UNCERTAINTY or are uniform/externally assigned — a "
|
|
+ "correlation between two populations. WonderQuestion nodes readable "
|
|
+ "by type (capped at 50): " + int_to_str(wonders) + ", against "
|
|
+ int_to_str(preds) + " Prediction nodes. There is a known write/read "
|
|
+ "node-type mismatch on the wonder path; until that is fixed and both "
|
|
+ "populations exist, any correlation reported here would be noise.") + "\"}]"
|
|
}
|
|
|
|
// handle_api_structural_audit — Stage 1. Returns the coherence assessment 432:
|
|
// an annotated characterization, explicitly NOT a score.
|
|
//
|
|
// COST NOTE: the edge findings need the relation labels, and the runtime exposes
|
|
// no edge-enumeration builtin. The only way to see them is the same one
|
|
// GET /api/graph/edges already uses — engram_save to a SCRATCH path (never the
|
|
// owner's canonical file; see routes.el, neuron#117) and read the array back.
|
|
// On a large graph that is a multi-hundred-MB write, so this is a manual audit
|
|
// route, not something to put on a timer. Pass ?edges=0 to skip both edge
|
|
// findings and get the divergence + self-model readings cheaply.
|
|
fn handle_api_structural_audit(method: String, path: String, body: String) -> String {
|
|
let node_total: Int = engram_node_count()
|
|
let edge_total: Int = engram_edge_count()
|
|
let want_edges: Bool = !str_eq(api_query_param(path, "edges"), "0")
|
|
let edge_cap: Int = api_query_int(path, "edge_sample", 3000)
|
|
let node_cap: Int = api_query_int(path, "node_sample", 300)
|
|
|
|
let divergence: String = audit_divergence()
|
|
let self_model: String = audit_self_model()
|
|
|
|
let edge_part: String = if want_edges {
|
|
// Scratch export only. state_get("soul_snapshot_path") is deliberately
|
|
// NOT used: in HTTP-engram mode the soul is not the persistence owner and
|
|
// must never write the canonical file, not even on a read path.
|
|
let scratch_dir: String = env("TMPDIR")
|
|
let scratch_base: String = if str_eq(scratch_dir, "") { "/tmp" } else { scratch_dir }
|
|
let snap_path: String = scratch_base + "/soul-audit-export-" + state_get("soul_cgi_id") + ".json"
|
|
// engram_save returns Int (1 ok / 0 fail); str_eq on it SIGSEGVs (#150).
|
|
let saved: Int = engram_save(snap_path)
|
|
if saved == 0 {
|
|
"," + audit_finding("typed_edge_distribution", "\"available\":false",
|
|
"Could not export the graph to " + snap_path + " for edge analysis, "
|
|
+ "so edge typing and the dangling-edge sample were not run. "
|
|
+ "Reported as a gap, not as zero findings.")
|
|
} else {
|
|
// wt_read, not fs_read: fs_read leaves a thread-local length hint that
|
|
// the NEXT HTTP response would use as its Content-Length, appending
|
|
// adjacent heap bytes to the reply (see persist.el wt_read).
|
|
let snap: String = wt_read(snap_path)
|
|
let edges_raw: String = json_get_raw(snap, "edges")
|
|
let edges: String = if str_eq(edges_raw, "") { "[]" } else { edges_raw }
|
|
"," + audit_edge_typing(edges, edge_total, node_total)
|
|
+ "," + audit_orphans_dangling(edges, edge_total, node_total, edge_cap, node_cap)
|
|
}
|
|
} else {
|
|
""
|
|
}
|
|
|
|
return "{\"audit\":\"structural\",\"stage\":1"
|
|
+ ",\"spec\":\"CGI provisional 05-detailed-description.md, Stage 1: Structural audit 430\""
|
|
+ ",\"assessment\":\"coherence_assessment_432\""
|
|
+ ",\"assessment_kind\":\"annotated_characterization\""
|
|
+ ",\"score\":null"
|
|
+ ",\"score_note\":\"By design. The specification calls for an annotated characterization of the graph's structural properties, not a binary score. Read the findings.\""
|
|
+ ",\"cgi_id\":\"" + api_json_escape(state_get("soul_cgi_id")) + "\""
|
|
+ ",\"ts_ms\":" + int_to_str(time_now())
|
|
+ ",\"findings\":[" + divergence + "," + self_model + edge_part + "]"
|
|
+ ",\"deferred\":" + audit_deferred() + "}"
|
|
}
|