diff --git a/Makefile b/Makefile index 6dddd96..8e38e44 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,12 @@ EL_ROOT ?= $(HOME)/Development/neuron-technologies/foundation/el EL_LIB ?= $(EL_ROOT)/el-compiler/runtime ELC ?= $(EL_ROOT)/dist/platform/elc CC ?= cc -CFLAGS := -std=c11 -O2 -I$(EL_LIB) +# -DHAVE_CURL: the el-compiler runtime guards its libcurl client behind this +# macro, so a build without it links fine and then answers every http_get / +# http_post_json with {"error":"not built with HAVE_CURL"} — forge is entirely +# HTTP, so it fails at the first engram call. The SDK release runtime CI pulls +# has no such guard; defining it is a no-op there. +CFLAGS := -std=c11 -O2 -DHAVE_CURL -I$(EL_LIB) .PHONY: build clean install diff --git a/registry.json b/registry.json index 9616b20..fc3028b 100644 --- a/registry.json +++ b/registry.json @@ -1,7 +1,4 @@ -{ - "version": "1.0", - "imprints": [ - { +{"version":"1.0","imprints":[{ "subject": "Bobby Anderson", "slug": "bobby-anderson", "seed_file": "seeds/bobby-anderson-seed.json", @@ -13,8 +10,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-bobby-anderson-2026", "soul_daemon_url": "http://localhost:8901" - }, - { + },{ "subject": "Alan Turing", "slug": "alan-turing", "seed_file": "seeds/alan-turing-seed.json", @@ -26,8 +22,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-alan-turing-2026", "soul_daemon_url": "http://localhost:8902" - }, - { + },{ "subject": "Albert Einstein", "slug": "albert-einstein", "seed_file": "seeds/albert-einstein-seed.json", @@ -39,8 +34,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-albert-einstein-2026", "soul_daemon_url": "http://localhost:8903" - }, - { + },{ "subject": "Nikola Tesla", "slug": "nikola-tesla", "seed_file": "seeds/nikola-tesla-seed.json", @@ -52,8 +46,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-nikola-tesla-2026", "soul_daemon_url": "http://localhost:8904" - }, - { + },{ "subject": "Leonardo da Vinci", "slug": "leonardo-da-vinci", "seed_file": "seeds/leonardo-da-vinci-seed.json", @@ -65,8 +58,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-leonardo-da-vinci-2026", "soul_daemon_url": "http://localhost:8905" - }, - { + },{ "subject": "Richard Feynman", "slug": "richard-feynman", "seed_file": "seeds/richard-feynman-seed.json", @@ -78,8 +70,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-richard-feynman-2026", "soul_daemon_url": "http://localhost:8906" - }, - { + },{ "subject": "Carl Sagan", "slug": "carl-sagan", "seed_file": "seeds/carl-sagan-seed.json", @@ -91,8 +82,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-carl-sagan-2026", "soul_daemon_url": "http://localhost:8907" - }, - { + },{ "subject": "Ren\u00e9 Descartes", "slug": "rene-descartes", "seed_file": "seeds/rene-descartes-seed.json", @@ -104,8 +94,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-rene-descartes-2026", "soul_daemon_url": "http://localhost:8908" - }, - { + },{ "subject": "Robin Williams", "slug": "robin-williams", "seed_file": "seeds/robin-williams-seed.json", @@ -117,8 +106,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-robin-williams-2026", "soul_daemon_url": "http://localhost:8909" - }, - { + },{ "subject": "Frederick Douglass", "slug": "frederick-douglass", "seed_file": "seeds/frederick-douglass-seed.json", @@ -130,8 +118,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-frederick-douglass-2026", "soul_daemon_url": "http://localhost:8910" - }, - { + },{ "subject": "Marcus Aurelius", "slug": "marcus-aurelius", "seed_file": "seeds/marcus-aurelius-seed.json", @@ -143,8 +130,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-marcus-aurelius-2026", "soul_daemon_url": "http://localhost:8911" - }, - { + },{ "subject": "Friedrich Nietzsche", "slug": "friedrich-nietzsche", "seed_file": "seeds/friedrich-nietzsche-seed.json", @@ -156,8 +142,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-friedrich-nietzsche-2026", "soul_daemon_url": "http://localhost:8912" - }, - { + },{ "subject": "James Baldwin", "slug": "james-baldwin", "seed_file": "seeds/james-baldwin-seed.json", @@ -169,8 +154,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-james-baldwin-2026", "soul_daemon_url": "http://localhost:8913" - }, - { + },{ "subject": "Ada Lovelace", "slug": "ada-lovelace", "seed_file": "seeds/ada-lovelace-seed.json", @@ -182,8 +166,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-ada-lovelace-2026", "soul_daemon_url": "http://localhost:8914" - }, - { + },{ "subject": "Harriet Tubman", "slug": "harriet-tubman", "seed_file": "seeds/harriet-tubman-seed.json", @@ -195,8 +178,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-harriet-tubman-2026", "soul_daemon_url": "http://localhost:8915" - }, - { + },{ "subject": "Virginia Woolf", "slug": "virginia-woolf", "seed_file": "seeds/virginia-woolf-seed.json", @@ -208,8 +190,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-virginia-woolf-2026", "soul_daemon_url": "http://localhost:8916" - }, - { + },{ "subject": "Hedy Lamarr", "slug": "hedy-lamarr", "seed_file": "seeds/hedy-lamarr-seed.json", @@ -221,8 +202,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-hedy-lamarr-2026", "soul_daemon_url": "http://localhost:8917" - }, - { + },{ "subject": "Marie Curie", "slug": "marie-curie", "seed_file": "seeds/marie-curie-seed.json", @@ -234,8 +214,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-marie-curie-2026", "soul_daemon_url": "http://localhost:8918" - }, - { + },{ "subject": "Helen Keller", "slug": "helen-keller", "seed_file": "seeds/helen-keller-seed.json", @@ -247,8 +226,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-helen-keller-2026", "soul_daemon_url": "http://localhost:8919" - }, - { + },{ "subject": "Superman", "slug": "superman", "seed_file": "seeds/superman-seed.json", @@ -260,8 +238,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-superman-2026", "soul_daemon_url": "http://localhost:8920" - }, - { + },{ "subject": "Stephen King", "slug": "stephen-king", "seed_file": "/Users/will/Development/neuron-technologies/forge/seeds/stephen-king-seed.json", @@ -273,8 +250,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-stephen-king-2026", "soul_daemon_url": "http://localhost:8921" - }, - { + },{ "subject": "Jon Stewart", "slug": "jon-stewart", "seed_file": "/Users/will/Development/neuron-technologies/forge/seeds/jon-stewart-seed.json", @@ -286,8 +262,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-jon-stewart-2026", "soul_daemon_url": "http://localhost:8922" - }, - { + },{ "subject": "Jimmy Kimmel", "slug": "jimmy-kimmel", "seed_file": "/Users/will/Development/neuron-technologies/forge/seeds/jimmy-kimmel-seed.json", @@ -299,8 +274,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-jimmy-kimmel-2026", "soul_daemon_url": "http://localhost:8923" - }, - { + },{ "subject": "John Oliver", "slug": "john-oliver", "seed_file": "/Users/will/Development/neuron-technologies/forge/seeds/john-oliver-seed.json", @@ -312,8 +286,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-john-oliver-2026", "soul_daemon_url": "http://localhost:8924" - }, - { + },{ "subject": "Stephen Colbert", "slug": "stephen-colbert", "seed_file": "/Users/will/Development/neuron-technologies/forge/seeds/stephen-colbert-seed.json", @@ -325,8 +298,7 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-stephen-colbert-2026", "soul_daemon_url": "http://localhost:8925" - }, - { + },{ "subject": "Carl Jung", "slug": "carl-jung", "seed_file": "seeds/carl-jung-seed.json", @@ -338,6 +310,4 @@ "installed_at": "2026-05-03", "engram_api_key": "ntn-carl-jung-2026", "soul_daemon_url": "http://localhost:8926" - } - ] -} \ No newline at end of file + },{"subject":"Abraham Lincoln","slug":"abraham-lincoln","seed_file":"/Users/will/Development/neuron-technologies/foundation/forge/seeds/abraham-lincoln-seed.json","engram_db_path":"imprints/abraham-lincoln","engram_port":8827,"engram_url":"http://localhost:8827","engram_root_id":"93800f4c-3bbc-4b2d-8d13-dce498b55e2c","installed":true,"installed_at":"2026-05-03","engram_api_key":"ntn-abraham-lincoln-2026","soul_daemon_url":"http://localhost:8927"},{"subject":"Isaac Newton","slug":"isaac-newton","seed_file":"isaac-newton-seed.json","engram_db_path":"imprints/isaac-newton","engram_port":8828,"engram_url":"http://localhost:8828","engram_root_id":"097d86b3-d3a6-4977-8c9c-ab25a4702d72","installed":true,"installed_at":"2026-05-03","engram_api_key":"ntn-isaac-newton-2026","soul_daemon_url":"http://localhost:8928"},{"subject":"Pierre de Fermat","slug":"pierre-de-fermat","seed_file":"/Users/will/Development/neuron-technologies/forge/seeds/pierre-de-fermat-seed.json","engram_db_path":"imprints/pierre-de-fermat","engram_port":8829,"engram_url":"http://localhost:8829","engram_root_id":"c75c505a-3937-45a2-a79c-6f9aee2f8650","installed":true,"installed_at":"2026-05-03","engram_api_key":"ntn-pierre-de-fermat-2026","soul_daemon_url":"http://localhost:8929"},{"subject":"Neuron","slug":"neuron","seed_file":"/Users/will/Development/neuron-technologies/foundation/forge/seeds/neuron-genesis-seed.json","engram_db_path":"/private/tmp/claude-501/-Users-will/9da71693-1057-42a0-a4b4-98fc45859d8d/scratchpad/sbx/wt-data","engram_port":8994,"engram_url":"http://localhost:8994","engram_root_id":"kn-efeb4a5b-5aff-4759-8a97-7233099be6ee","installed":true,"installed_at":"2026-08-10","engram_api_key":"","soul_daemon_url":"http://localhost:7770","seed_type":"genesis"}]} \ No newline at end of file diff --git a/src/install.el b/src/install.el index 72966f8..18d5167 100644 --- a/src/install.el +++ b/src/install.el @@ -152,6 +152,333 @@ fn install_reasoning_pattern(target_url: String, root_id: String, pattern: Strin return node_id } +// ── Genesis seeds ───────────────────────────────────────────────────────────── +// +// A PERSONA seed is generative: it declares values/biography/relationships as +// prose and the installer mints a node id for each one. The ids are incidental +// — nothing outside the graph refers to them. +// +// A GENESIS seed is declarative. It ships identity_nodes[] with explicit ids +// and edges[] that reference those ids, and the ids ARE the artifact: +// is_protected_node() in neuron-api.el keys on exact ids, and every edge in the +// seed is written in terms of them. Installing a genesis seed through the +// persona path mints fresh ids and produces a graph that is structurally +// unrelated to the one the seed describes — which is why 87 of Neuron's 102 +// identity nodes were simply absent. +// +// So the genesis path never mints. It writes the declared nodes and edges into +// a snapshot-format file and hands that to the engram, which honors the ids: +// POST /api/nodes/reseed — create-at-id, plus in-place replace of the ids +// named in "replace" (edge-preserving) +// POST /api/load-merge — create-at-id only; the fallback when the engram +// predates the reseed route +// Both skip nodes already present and dedup edges by (from,to,relation), so a +// second run is a no-op by construction. + +// genesis_ts — millisecond timestamp string for created_at/updated_at fields. +fn genesis_ts() -> String { + return int_to_str(unix_timestamp()) + "000" +} + +// genesis_tags — flatten a seed node's tags[] array plus its role into the +// comma-separated tag string the engram stores. Matches the convention already +// used by the resident kn- identity nodes ("tier:note,disposition:experimental"). +fn genesis_tags(node_obj: String) -> String { + let tags_raw: String = json_get_raw(node_obj, "tags") + let n: Int = json_array_len(tags_raw) + let out: String = "" + let i: Int = 0 + while i < n { + let t: String = json_array_get_string(tags_raw, i) + if !str_eq(t, "") { + if str_eq(out, "") { + let out = str_escape_json(t) + } else { + let out = out + "," + str_escape_json(t) + } + } + let i = i + 1 + } + let role: String = json_get_string(node_obj, "role") + if !str_eq(role, "") { + if str_eq(out, "") { + let out = "role:" + str_escape_json(role) + } else { + let out = out + ",role:" + str_escape_json(role) + } + } + return out +} + +// genesis_node_record — render one seed node as a snapshot-format node object. +// +// id/content/label/node_type are spliced in as their RAW JSON tokens rather +// than decoded and re-escaped. The seed bodies are multi-KB markdown carrying +// newlines, quotes and \u escapes; a decode/re-encode round-trip through +// str_escape_json would have to reproduce every escape the seed author wrote, +// and any escape it does not know about silently corrupts the body. Copying the +// token verbatim cannot corrupt what it never decodes. +fn genesis_node_record(node_obj: String, ts: String) -> String { + let id_raw: String = json_get_raw(node_obj, "id") + let content_raw: String = json_get_raw(node_obj, "content") + let label_raw: String = json_get_raw(node_obj, "label") + let type_raw: String = json_get_raw(node_obj, "node_type") + if str_eq(id_raw, "") { return "" } + if str_eq(content_raw, "") { return "" } + let ntype: String = if str_eq(type_raw, "") { "\"Knowledge\"" } else { type_raw } + let nlabel: String = if str_eq(label_raw, "") { content_raw } else { label_raw } + return "{\"id\":" + id_raw + + ",\"content\":" + content_raw + + ",\"node_type\":" + ntype + + ",\"label\":" + nlabel + + ",\"tier\":\"Semantic\"" + + ",\"tags\":\"" + genesis_tags(node_obj) + "\"" + + ",\"metadata\":\"{}\"" + + ",\"salience\":1.0,\"importance\":0.9,\"confidence\":1.0" + + ",\"temporal_decay_rate\":0,\"activation_count\":0,\"last_activated\":0" + + ",\"created_at\":" + ts + ",\"updated_at\":" + ts + + ",\"background_activation\":0,\"working_memory_weight\":0" + + ",\"suppression_count\":0,\"layer_id\":1}" +} + +// genesis_edge_record — render one seed edge as a snapshot-format edge object. +// Seed edges use from/to; the snapshot format uses from_id/to_id. +fn genesis_edge_record(edge_obj: String, slug: String, index: Int, ts: String) -> String { + let from_raw: String = json_get_raw(edge_obj, "from") + let to_raw: String = json_get_raw(edge_obj, "to") + let rel_raw: String = json_get_raw(edge_obj, "relation") + if str_eq(from_raw, "") { return "" } + if str_eq(to_raw, "") { return "" } + let relation: String = if str_eq(rel_raw, "") { "\"associates\"" } else { rel_raw } + let w_raw: String = json_get_raw(edge_obj, "weight") + let weight: String = if str_eq(w_raw, "") { "1.0" } else { w_raw } + return "{\"id\":\"ge-" + slug + "-" + int_to_str(index) + "\"" + + ",\"from_id\":" + from_raw + + ",\"to_id\":" + to_raw + + ",\"relation\":" + relation + + ",\"metadata\":\"{}\"" + + ",\"weight\":" + weight + + ",\"confidence\":1.0" + + ",\"created_at\":" + ts + ",\"updated_at\":" + ts + + ",\"last_fired\":0,\"inhibitory\":0,\"layer_id\":1}" +} + +// genesis_live_content_len — decoded length of the node's content in the live +// graph. Returns -1 when the node is absent (engram answers "{}" for a miss). +fn genesis_live_content_len(target_url: String, id: String) -> Int { + let resp: String = http_get(target_url + "/api/nodes/" + id) + if str_eq(resp, "") { return -1 } + if str_eq(str_trim(resp), "{}") { return -1 } + let live_id: String = json_get_string(resp, "id") + if str_eq(live_id, "") { return -1 } + return str_len(json_get_string(resp, "content")) +} + +// genesis_is_hollow — the failure signature this whole path exists to fix. +// +// A hollow node has the right id and no body: `content` holds only the node's +// own label. Observed at 30 bytes against a 4263-byte seed body and 22 against +// 2590. The test is deliberately conservative — under 40% of the declared body +// AND under 400 bytes — so a node whose body legitimately grew or was edited in +// the graph is left alone. Genesis restores what is missing; it does not +// overwrite what the graph has learned. +fn genesis_is_hollow(live_len: Int, seed_len: Int) -> Bool { + if live_len < 0 { return false } + if seed_len < 300 { return false } + if live_len >= 400 { return false } + if live_len * 10 >= seed_len * 4 { return false } + return true +} + +// install_genesis — install a declarative genesis seed into target_url. +// Returns the traversal root id, or "" if the install could not proceed. +fn install_genesis(seed_json: String, seed_file: String, subject: String, slug: String, target_url: String, api_key: String) -> String { + let root_id: String = json_get_string(seed_json, "traversal_root") + if str_eq(root_id, "") { + println("[forge] error: genesis seed has no traversal_root") + return "" + } + + let nodes_raw: String = json_get_raw(seed_json, "identity_nodes") + let edges_raw: String = json_get_raw(seed_json, "edges") + let n_nodes: Int = json_array_len(nodes_raw) + let n_edges: Int = json_array_len(edges_raw) + if n_nodes == 0 { + println("[forge] error: genesis seed declares no identity_nodes") + return "" + } + + println("[forge] genesis seed: " + int_to_str(n_nodes) + " identity nodes, " + int_to_str(n_edges) + " edges") + println("[forge] traversal root: " + root_id) + println("[forge] engram: " + target_url) + println("[forge] classifying declared nodes against the live graph...") + + // ── Pass 1: classify every declared id ──────────────────────────────────── + let ts: String = genesis_ts() + let node_parts: String = "" + let replace_ids: String = "" + let n_absent: Int = 0 + let n_hollow: Int = 0 + let n_intact: Int = 0 + let i: Int = 0 + while i < n_nodes { + let node_obj: String = json_array_get(nodes_raw, i) + let id: String = json_get_string(node_obj, "id") + if !str_eq(id, "") { + let seed_len: Int = str_len(json_get_string(node_obj, "content")) + let live_len: Int = genesis_live_content_len(target_url, id) + let hollow: Bool = genesis_is_hollow(live_len, seed_len) + + if live_len < 0 { + let n_absent = n_absent + 1 + } else { + if hollow { + let n_hollow = n_hollow + 1 + println("[forge] hollow: " + id + " (" + int_to_str(live_len) + " bytes live vs " + int_to_str(seed_len) + " declared)") + if str_eq(replace_ids, "") { + let replace_ids = "\"" + id + "\"" + } else { + let replace_ids = replace_ids + ",\"" + id + "\"" + } + } else { + let n_intact = n_intact + 1 + } + } + + // Intact nodes are still written to the merge file — the engram + // skips ids it already holds, so including them costs nothing and + // keeps the file a faithful copy of the seed. + let rec: String = genesis_node_record(node_obj, ts) + if !str_eq(rec, "") { + if str_eq(node_parts, "") { + let node_parts = rec + } else { + let node_parts = node_parts + "," + rec + } + } + } + let i = i + 1 + } + + println("[forge] absent: " + int_to_str(n_absent) + " hollow: " + int_to_str(n_hollow) + " intact: " + int_to_str(n_intact)) + + // ── Pass 2: render edges ────────────────────────────────────────────────── + let edge_parts: String = "" + let j: Int = 0 + while j < n_edges { + let edge_obj: String = json_array_get(edges_raw, j) + let erec: String = genesis_edge_record(edge_obj, slug, j, ts) + if !str_eq(erec, "") { + if str_eq(edge_parts, "") { + let edge_parts = erec + } else { + let edge_parts = edge_parts + "," + erec + } + } + let j = j + 1 + } + + // ── Pass 3: write the merge file and hand it to the engram ──────────────── + let stage_dir: String = FORGE_DIR + "/imprints/" + slug + exec("mkdir -p " + stage_dir + " 2>/dev/null") + let merge_path: String = stage_dir + "/.genesis-merge.json" + let merge_doc: String = "{\"nodes\":[" + node_parts + "],\"edges\":[" + edge_parts + "]}" + let wrote: Int = fs_write(merge_path, merge_doc) + if wrote == 0 { + println("[forge] error: could not stage merge file at " + merge_path) + return "" + } + println("[forge] staged: " + merge_path) + + let reseed_body: String = "{\"path\":\"" + merge_path + "\",\"replace\":[" + replace_ids + "],\"_auth\":\"" + api_key + "\"}" + let resp: String = http_post_json(target_url + "/api/nodes/reseed", reseed_body) + let blocked: Int = 0 + if str_contains(resp, "not found") { + // Engram predates the reseed route. /api/load-merge still creates every + // absent node at its declared id and lays every edge — it just cannot + // replace a hollow body. Do the part that is safe and say so loudly + // rather than reaching for a destructive substitute. + println("[forge] note: engram has no /api/nodes/reseed — falling back to /api/load-merge") + let merge_body: String = "{\"path\":\"" + merge_path + "\",\"_auth\":\"" + api_key + "\"}" + let resp = http_post_json(target_url + "/api/load-merge", merge_body) + let blocked = n_hollow + } + if str_eq(resp, "") { + println("[forge] error: engram did not respond at " + target_url) + return "" + } + if str_contains(resp, "\"error\"") { + println("[forge] error: engram rejected the merge: " + resp) + return "" + } + println("[forge] engram: " + resp) + + // ── Pass 4: verify by re-reading every declared id ──────────────────────── + // + // The bug this path replaces reported success while delivering empty + // bodies, so success is not what the write call returned — it is what the + // graph now holds. Re-read all of it and audit the content lengths. + println("[forge] verifying...") + let ok_nodes: Int = 0 + let failed_ids: String = "" + let n_failed: Int = 0 + let k: Int = 0 + while k < n_nodes { + let node_obj: String = json_array_get(nodes_raw, k) + let id: String = json_get_string(node_obj, "id") + if !str_eq(id, "") { + let seed_len: Int = str_len(json_get_string(node_obj, "content")) + let live_len: Int = genesis_live_content_len(target_url, id) + if live_len < 0 { + let n_failed = n_failed + 1 + if str_eq(failed_ids, "") { + let failed_ids = id + " (absent)" + } else { + let failed_ids = failed_ids + ", " + id + " (absent)" + } + } else { + if genesis_is_hollow(live_len, seed_len) { + let n_failed = n_failed + 1 + if str_eq(failed_ids, "") { + let failed_ids = id + " (hollow)" + } else { + let failed_ids = failed_ids + ", " + id + " (hollow)" + } + } else { + let ok_nodes = ok_nodes + 1 + } + } + } + let k = k + 1 + } + + // ── Manifest ────────────────────────────────────────────────────────────── + let filled: Int = n_hollow - blocked + println("") + println("[forge] ── genesis manifest ─────────────────────────────") + println("[forge] created: " + int_to_str(n_absent)) + println("[forge] filled: " + int_to_str(filled)) + println("[forge] skipped: " + int_to_str(n_intact)) + println("[forge] failed: " + int_to_str(n_failed)) + if blocked > 0 { + println("[forge] blocked: " + int_to_str(blocked) + " hollow node(s) — engram lacks /api/nodes/reseed") + } + if !str_eq(failed_ids, "") { + println("[forge] failing ids: " + failed_ids) + } + println("[forge] nodes verified: " + int_to_str(ok_nodes) + "/" + int_to_str(n_nodes)) + println("[forge] edges declared: " + int_to_str(n_edges) + " (engram dedups by from/to/relation)") + println("[forge] ─────────────────────────────────────────────────") + + log_event("install_genesis", subject, + "root=" + root_id + " created=" + int_to_str(n_absent) + + " filled=" + int_to_str(filled) + " skipped=" + int_to_str(n_intact) + + " failed=" + int_to_str(n_failed) + " edges=" + int_to_str(n_edges)) + + if n_failed > 0 { return "" } + return root_id +} + // ── Registry helpers ────────────────────────────────────────────────────────── // find_max_port — scan registry and return the highest engram_port found. @@ -283,6 +610,111 @@ fn registry_update_root_id(subject: String, slug: String, root_id: String, seed_ registry_replace_slug(slug, new_entry) } +// registry_update_genesis — record a genesis install. Same entry shape as every +// other imprint, with three differences that are inherent to what a genesis seed +// is: engram_root_id is the seed's declared traversal_root (not a minted id), +// engram_db_path points at the engram's own data dir rather than an +// imprints/ directory forge provisioned, and engram_api_key is left empty +// — the key for a pre-existing engram belongs to that engram's launchd +// environment, not to a file in this repo. +fn registry_update_genesis(subject: String, slug: String, root_id: String, seed_file: String, target_url: String, port: Int, db_path: String, soul_url: String, installed: Bool) -> Void { + let inst: String = if installed { "true" } else { "false" } + let inst_at: String = if installed { genesis_today() } else { "" } + let new_entry: String = "{\"subject\":\"" + str_escape_json(subject) + + "\",\"slug\":\"" + slug + + "\",\"seed_file\":\"" + str_escape_json(seed_file) + + "\",\"engram_db_path\":\"" + str_escape_json(db_path) + + "\",\"engram_port\":" + int_to_str(port) + + ",\"engram_url\":\"" + target_url + + "\",\"engram_root_id\":\"" + root_id + + "\",\"installed\":" + inst + ",\"installed_at\":\"" + inst_at + "\"" + + ",\"engram_api_key\":\"\"" + + ",\"soul_daemon_url\":\"" + soul_url + + "\",\"seed_type\":\"genesis\"}" + registry_replace_slug(slug, new_entry) +} + +// genesis_today — installed_at stamp. The persona path hardcodes a date; this +// derives one so a reinstall records when it actually happened. +fn genesis_today() -> String { + return str_trim(exec("date -u +%Y-%m-%d")) +} + +// install_genesis_main — resolve the target engram for a genesis seed, gate the +// write, run the install, record it. +// +// Target resolution differs from the persona path on purpose. A persona seed +// gets a port allocated and an Engram provisioned for it; a genesis seed +// installs INTO an engram that already exists and is owned by something else +// (Neuron's own engram is a launchd agent, ai.neuron.engram). So forge resolves +// a target — ENGRAM_URL, else the registry entry, else the default — and never +// provisions one. The ":8742 refusal" that guards the persona path is also +// inverted here: that guard exists to stop a foreign soul landing in Neuron's +// engram, and Neuron's own identity graph is the one thing that belongs there. +// +// In its place, GENESIS_CONFIRM. A genesis install rewrites the graph the +// running CGI reads itself out of; it should never happen because someone typed +// a familiar command with an unfamiliar file. +fn install_genesis_main(seed_json: String, seed_file: String, subject: String, slug: String) -> String { + let reg_json: String = fs_read(FORGE_DIR + "/registry.json") + let entry: String = registry_find_by_slug(reg_json, slug) + + let target_url: String = ENGRAM_DEFAULT_URL + let reg_url: String = json_get_string(entry, "engram_url") + if !str_eq(reg_url, "") { let target_url = reg_url } + let env_url: String = env("ENGRAM_URL") + if !str_eq(env_url, "") { let target_url = env_url } + + let api_key: String = env("ENGRAM_API_KEY") + if str_eq(api_key, "") { let api_key = json_get_string(entry, "engram_api_key") } + + let colon_pos: Int = str_last_index_of(target_url, ":") + let port: Int = 0 + if colon_pos > 0 { + let port = str_to_int(str_slice(target_url, colon_pos + 1, str_len(target_url))) + } + + let db_path: String = env("ENGRAM_DATA_DIR") + if str_eq(db_path, "") { + let db_path = json_get_string(entry, "engram_db_path") + } + if str_eq(db_path, "") { let db_path = "imprints/" + slug } + + let soul_url: String = env("SOUL_URL") + if str_eq(soul_url, "") { let soul_url = "http://localhost:7770" } + + println("[forge] seed_type: genesis") + + if str_eq(env("GENESIS_CONFIRM"), "") { + println("[forge] refusing to install a genesis seed without confirmation.") + println("[forge] target: " + target_url) + println("[forge] a genesis seed writes an identity graph into a resident engram at") + println("[forge] declared node ids. Re-run with GENESIS_CONFIRM=1 once the target is") + println("[forge] the one you mean (set ENGRAM_URL to aim at a sandbox).") + return "" + } + + // Never provision — only wait for what is already meant to be there. + let stats: String = http_get(target_url + "/stats") + if str_eq(stats, "") { + println("[forge] error: no engram responding at " + target_url) + println("[forge] genesis installs into an existing engram; forge will not start one.") + return "" + } + + let root_id: String = install_genesis(seed_json, seed_file, subject, slug, target_url, api_key) + if str_eq(root_id, "") { + println("[forge] genesis install did not complete cleanly — registry not updated") + return "" + } + + registry_update_genesis(subject, slug, root_id, seed_file, target_url, port, db_path, soul_url, true) + println("[forge] registry: recorded " + slug + " (root " + root_id + ")") + println("") + println("[forge] traverse from: " + target_url + "/api/neighbors/" + root_id) + return root_id +} + // ── Main ────────────────────────────────────────────────────────────────────── fn install_main() -> String { @@ -315,6 +747,19 @@ fn install_main() -> String { let slug: String = str_replace(slug_raw, " ", "-") println("[forge] slug: " + slug) + // ── Step 3b: Genesis seeds take a different path entirely ───────────────── + // + // Everything below this point is the persona pipeline: allocate a port, + // provision a launchd Engram for a soul that does not have one, mint a root + // node, mint a node per value/biography/relationship. None of that applies + // to a genesis seed, which installs a declared graph into an engram that + // already exists. Branch before any of it runs. Seeds with no seed_type are + // persona seeds and reach the identical code they always did. + let seed_type: String = json_get_string(seed_json, "seed_type") + if str_eq(seed_type, "genesis") { + return install_genesis_main(seed_json, seed_file, subject, slug) + } + // ── Step 4: Resolve or create registry entry ────────────────────────────── let reg_json: String = fs_read(FORGE_DIR + "/registry.json") let target_url: String = ""