Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2d8a07c7b | |||
| 710bea174d |
@@ -0,0 +1,764 @@
|
||||
// ingest.el — the native EL AFFERENT INGEST ORGAN
|
||||
//
|
||||
// The source-polymorphic ingest(source) primitive: point it at a directory,
|
||||
// file, url, llm-query, structured-primitive set, or stream; it EXTRACTS the
|
||||
// real content faithfully (no invention), TRANSDUCES it into a DISCRETE
|
||||
// MANIFOLD (multiple nodes + internal edges — meaning-structure, never a
|
||||
// single blob; the conversion from extracted surface content into geometry
|
||||
// is automatic and invisible to the caller, the way digestion is invisible
|
||||
// to the one who chose to eat — ingest is the conscious act, transduce is
|
||||
// the mechanism underneath it, and it is no less real for being unseen),
|
||||
// and MERGES that manifold into the engram geometry: shared
|
||||
// meanings DEDUP onto existing nodes (search + exact/cosine match), genuinely
|
||||
// new meanings add nodes, relations add edges. Every node enters with
|
||||
// PROVENANCE + grounding-level + stewardship class from the moment of entry.
|
||||
//
|
||||
// It is a pure HTTP CLIENT of the engram server — it links only el_runtime.c
|
||||
// via fs/http/json/string builtins; it never links el_seed.c or the engram
|
||||
// engine. This is the general afferent metabolism the migration / reseed /
|
||||
// fetch_fact / conversation / multimodal-learning all ride on.
|
||||
//
|
||||
// Build (canonical runtime):
|
||||
// ELC=lang/dist/platform/elc ; RT=lang/releases/v1.0.0-20260501
|
||||
// $ELC ingest/src/ingest.el > ingest/build/ingest.c
|
||||
// cc -std=c11 -O2 -I $RT -o ingest/build/ingest ingest/build/ingest.c $RT/el_runtime.c -lcurl -lpthread
|
||||
//
|
||||
// Run (against an nsbx sandbox clone — NEVER the live :8742):
|
||||
// ENGRAM_URL=http://127.0.0.1:8902 ENGRAM_KEY=sbx-ingest-test \
|
||||
// INGEST_KIND=file INGEST_ARG=/abs/path.md ./ingest/build/ingest
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// SECTION A — JSON helpers (self-defined; canonical runtime does not export
|
||||
// json_build_object / json_escape_string, so we own them here)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
fn j_esc(s: String) -> String {
|
||||
let a: String = str_replace(s, "\\", "\\\\")
|
||||
let b: String = str_replace(a, "\"", "\\\"")
|
||||
let c: String = str_replace(b, "\n", "\\n")
|
||||
let d: String = str_replace(c, "\r", "\\r")
|
||||
let e: String = str_replace(d, "\t", "\\t")
|
||||
return e
|
||||
}
|
||||
|
||||
// a quoted, escaped JSON string literal
|
||||
fn j_q(s: String) -> String {
|
||||
return "\"" + j_esc(s) + "\""
|
||||
}
|
||||
|
||||
// Extract the top-level keys of a JSON object string. A thin, self-contained
|
||||
// scanner (FLAGGED: the one non-trivial parser in this organ — everything else
|
||||
// is faithful text handling). Tracks string state + brace/bracket depth; a key
|
||||
// is a string at object-interior depth 1 immediately followed by ':'.
|
||||
fn json_object_keys(obj: String) -> [String] {
|
||||
let keys: [String] = el_list_empty()
|
||||
let n: Int = str_len(obj)
|
||||
let i: Int = 0
|
||||
let depth: Int = 0
|
||||
let in_str: Bool = false
|
||||
let esc: Bool = false
|
||||
let str_start: Int = -1
|
||||
let cur: String = ""
|
||||
let have_key: Bool = false
|
||||
while i < n {
|
||||
let c: String = str_char_at(obj, i)
|
||||
if in_str {
|
||||
if esc {
|
||||
esc = false
|
||||
} else {
|
||||
if str_eq(c, "\\") {
|
||||
esc = true
|
||||
} else {
|
||||
if str_eq(c, "\"") {
|
||||
in_str = false
|
||||
cur = str_slice(obj, str_start + 1, i)
|
||||
have_key = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if str_eq(c, "\"") {
|
||||
in_str = true
|
||||
str_start = i
|
||||
}
|
||||
if str_eq(c, "{") { depth = depth + 1 }
|
||||
if str_eq(c, "}") { depth = depth - 1 }
|
||||
if str_eq(c, "[") { depth = depth + 1 }
|
||||
if str_eq(c, "]") { depth = depth - 1 }
|
||||
if str_eq(c, ":") {
|
||||
if have_key {
|
||||
if depth == 1 {
|
||||
keys = el_list_append(keys, cur)
|
||||
}
|
||||
}
|
||||
have_key = false
|
||||
}
|
||||
if str_eq(c, ",") { have_key = false }
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// SECTION B — engram HTTP client (provenance-carrying afferent LOAD)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
fn eg_base() -> String {
|
||||
let u: String = env("ENGRAM_URL")
|
||||
if !str_eq(u, "") { return u }
|
||||
let s: String = env("SBX_URL")
|
||||
if !str_eq(s, "") { return s }
|
||||
return "http://127.0.0.1:8902"
|
||||
}
|
||||
|
||||
fn eg_key() -> String {
|
||||
let k: String = env("ENGRAM_KEY")
|
||||
if !str_eq(k, "") { return k }
|
||||
let s: String = env("SBX_KEY")
|
||||
if !str_eq(s, "") { return s }
|
||||
return ""
|
||||
}
|
||||
|
||||
// POST a JSON body (auth _auth injected) to an engram path.
|
||||
fn eg_post(path: String, body_inner: String) -> String {
|
||||
let key: String = eg_key()
|
||||
let auth: String = if str_eq(key, "") { "" } else { ",\"_auth\":" + j_q(key) }
|
||||
let body: String = "{" + body_inner + auth + "}"
|
||||
return http_post_json(eg_base() + path, body)
|
||||
}
|
||||
|
||||
fn eg_get(path: String) -> String {
|
||||
return http_get(eg_base() + path)
|
||||
}
|
||||
|
||||
// crystallize a node with full provenance-bearing metadata (server-confirmed
|
||||
// write — unlike the local FORM decision in merge_manifold, this is real);
|
||||
// returns the new node id. Not currently called by any live path (dead code,
|
||||
// kept for a future single-node ad-hoc write use case) — 2026-08-15.
|
||||
fn eg_crystallize_node(content: String, ntype: String, tier: String,
|
||||
sal: String, imp: String, conf: String, tags: String) -> String {
|
||||
let inner: String =
|
||||
"\"content\":" + j_q(content) +
|
||||
",\"node_type\":" + j_q(ntype) +
|
||||
",\"label\":" + j_q(str_slice(content, 0, 80)) +
|
||||
",\"tier\":" + j_q(tier) +
|
||||
",\"salience\":" + sal +
|
||||
",\"importance\":" + imp +
|
||||
",\"confidence\":" + conf +
|
||||
",\"tags\":" + j_q(tags)
|
||||
let resp: String = eg_post("/api/nodes", inner)
|
||||
return json_get_string(resp, "id")
|
||||
}
|
||||
|
||||
// search the existing geometry (lexical token-overlap rank); returns JSON array
|
||||
fn eg_search(query: String, limit: Int) -> String {
|
||||
let inner: String = "\"query\":" + j_q(query) + ",\"limit\":" + int_to_str(limit)
|
||||
return eg_post("/api/search", inner)
|
||||
}
|
||||
|
||||
// cosine similarity between two existing (embedded) nodes; -2 if not comparable
|
||||
fn eg_similarity(a: String, b: String) -> Float {
|
||||
let resp: String = eg_get("/api/similarity?a=" + a + "&b=" + b)
|
||||
return json_get_float(resp, "cosine")
|
||||
}
|
||||
|
||||
fn eg_embed_backfill(n: Int) -> String {
|
||||
return eg_get("/api/embed-backfill?n=" + int_to_str(n))
|
||||
}
|
||||
|
||||
fn eg_forget(id: String) -> String {
|
||||
return http_delete(eg_base() + "/api/nodes/" + id)
|
||||
}
|
||||
|
||||
// ── DEDUP probe: is this meaning already in the graph? ──────────────────────
|
||||
// TIER 1 (deterministic, no embedding needed): search by content tokens, then
|
||||
// exact normalized-content match among the candidates. Returns the existing
|
||||
// node id, or "" if the meaning is genuinely new.
|
||||
fn find_existing_by_content(content: String) -> String {
|
||||
let want: String = str_trim(content)
|
||||
if str_eq(want, "") { return "" }
|
||||
let arr: String = eg_search(content, 8)
|
||||
let n: Int = json_array_len(arr)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let hit: String = json_array_get(arr, i)
|
||||
let hc: String = str_trim(json_get_string(hit, "content"))
|
||||
if str_eq(hc, want) {
|
||||
return json_get_string(hit, "id")
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// SECTION C — manifold representation (nodes + internal edges, in memory)
|
||||
// A NODE is a JSON obj {lid, content, ntype, tier, sal, imp, conf, tags}.
|
||||
// An EDGE is a JSON obj {from, rel, to}. lid = local id within this manifold.
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
fn mk_node(lid: String, content: String, ntype: String, tier: String,
|
||||
sal: String, imp: String, conf: String, tags: String) -> String {
|
||||
return "{\"lid\":" + j_q(lid) +
|
||||
",\"content\":" + j_q(content) +
|
||||
",\"ntype\":" + j_q(ntype) +
|
||||
",\"tier\":" + j_q(tier) +
|
||||
",\"sal\":" + j_q(sal) +
|
||||
",\"imp\":" + j_q(imp) +
|
||||
",\"conf\":" + j_q(conf) +
|
||||
",\"tags\":" + j_q(tags) + "}"
|
||||
}
|
||||
|
||||
fn mk_edge(ef: String, rel: String, et: String) -> String {
|
||||
return "{\"from\":" + j_q(ef) + ",\"rel\":" + j_q(rel) + ",\"to\":" + j_q(et) + "}"
|
||||
}
|
||||
|
||||
// linear lookup in parallel lid/real lists
|
||||
fn lid_lookup(lids: [String], reals: [String], lid: String) -> String {
|
||||
let n: Int = el_list_len(lids)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
if str_eq(el_list_get(lids, i), lid) {
|
||||
return el_list_get(reals, i)
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// SECTION D — the MERGE: resolve each manifold node (dedup or create), then
|
||||
// wire the internal edges onto the resolved real ids. This is the
|
||||
// merge boundary: shared meanings collapse onto existing nodes;
|
||||
// genuinely-new meanings add nodes; relations add edges. Structure
|
||||
// grows, size saturates.
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// within-run content dedup: has this exact meaning already been resolved in
|
||||
// THIS manifold? returns its real id, or "".
|
||||
fn lookup_content(contents: [String], reals: [String], content: String) -> String {
|
||||
let n: Int = el_list_len(contents)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
if str_eq(el_list_get(contents, i), content) {
|
||||
return el_list_get(reals, i)
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn ingest_snap_path() -> String {
|
||||
let p: String = env("INGEST_SNAP")
|
||||
if !str_eq(p, "") { return p }
|
||||
return "/tmp/ingest-organ-snap.json"
|
||||
}
|
||||
|
||||
// The MERGE. Resolve every manifold node against (1) already-resolved nodes in
|
||||
// this run and (2) the existing graph (search + exact content match). Shared
|
||||
// meanings collapse onto an existing id (DEDUP); genuinely-new meanings get a
|
||||
// fresh id and go into the snapshot (CREATE). Then wire the internal edges onto
|
||||
// resolved ids. LOAD is ONE snapshot merged via /api/load-merge — a single
|
||||
// write (scales to the migration), the sanctioned rail. Structure grows, size
|
||||
// saturates: re-ingesting adds ~0 nodes, only edges/strengthening.
|
||||
fn merge_manifold(nodes: [String], edges: [String]) -> String {
|
||||
let nn: Int = el_list_len(nodes)
|
||||
let lids: [String] = el_list_empty()
|
||||
let reals: [String] = el_list_empty()
|
||||
let contents: [String] = el_list_empty()
|
||||
let snap_nodes: String = "["
|
||||
let sn_count: Int = 0
|
||||
let created: Int = 0
|
||||
let deduped: Int = 0
|
||||
|
||||
let i: Int = 0
|
||||
while i < nn {
|
||||
let node: String = el_list_get(nodes, i)
|
||||
let lid: String = json_get_string(node, "lid")
|
||||
let content: String = json_get_string(node, "content")
|
||||
let ntype: String = json_get_string(node, "ntype")
|
||||
let tier: String = json_get_string(node, "tier")
|
||||
let sal: String = json_get_string(node, "sal")
|
||||
let imp: String = json_get_string(node, "imp")
|
||||
let conf: String = json_get_string(node, "conf")
|
||||
let tags: String = json_get_string(node, "tags")
|
||||
|
||||
let real: String = ""
|
||||
let prior: String = lookup_content(contents, reals, content)
|
||||
if !str_eq(prior, "") {
|
||||
real = prior
|
||||
deduped = deduped + 1
|
||||
println(" DEDUP* " + real + " :: " + head80(content))
|
||||
} else {
|
||||
let existing: String = find_existing_by_content(content)
|
||||
if !str_eq(existing, "") {
|
||||
real = existing
|
||||
deduped = deduped + 1
|
||||
println(" DEDUP " + real + " :: " + head80(content))
|
||||
} else {
|
||||
real = uuid_v4()
|
||||
// provenance + grounding + stewardship: searchable in tags,
|
||||
// structured in metadata — carried from the moment of entry.
|
||||
let meta: String = "{\"provenance\":" + j_q(tags) + ",\"ingest_organ\":\"native-el\"}"
|
||||
let njson: String = "{\"id\":" + j_q(real) +
|
||||
",\"content\":" + j_q(content) +
|
||||
",\"node_type\":" + j_q(ntype) +
|
||||
",\"label\":" + j_q(head80(content)) +
|
||||
",\"tier\":" + j_q(tier) +
|
||||
",\"tags\":" + j_q(tags) +
|
||||
",\"metadata\":" + j_q(meta) +
|
||||
",\"salience\":" + sal +
|
||||
",\"importance\":" + imp +
|
||||
",\"confidence\":" + conf + "}"
|
||||
let sep: String = if sn_count == 0 { "" } else { "," }
|
||||
snap_nodes = snap_nodes + sep + njson
|
||||
sn_count = sn_count + 1
|
||||
created = created + 1
|
||||
println(" FORM " + real + " :: " + head80(content))
|
||||
}
|
||||
}
|
||||
lids = el_list_append(lids, lid)
|
||||
reals = el_list_append(reals, real)
|
||||
contents = el_list_append(contents, content)
|
||||
i = i + 1
|
||||
}
|
||||
snap_nodes = snap_nodes + "]"
|
||||
|
||||
// resolve internal edges onto real ids
|
||||
let ne: Int = el_list_len(edges)
|
||||
let snap_edges: String = "["
|
||||
let ec: Int = 0
|
||||
let j: Int = 0
|
||||
while j < ne {
|
||||
let edge: String = el_list_get(edges, j)
|
||||
let flid: String = json_get_string(edge, "from")
|
||||
let tlid: String = json_get_string(edge, "to")
|
||||
let rel: String = json_get_string(edge, "rel")
|
||||
let fr: String = lid_lookup(lids, reals, flid)
|
||||
let tr: String = lid_lookup(lids, reals, tlid)
|
||||
if !str_eq(fr, "") {
|
||||
if !str_eq(tr, "") {
|
||||
let eid: String = uuid_v4()
|
||||
let ejson: String = "{\"id\":" + j_q(eid) +
|
||||
",\"from_id\":" + j_q(fr) + ",\"to_id\":" + j_q(tr) +
|
||||
",\"relation\":" + j_q(rel) + ",\"weight\":0.6}"
|
||||
let sep: String = if ec == 0 { "" } else { "," }
|
||||
snap_edges = snap_edges + sep + ejson
|
||||
ec = ec + 1
|
||||
println(" EDGE " + fr + " -" + rel + "-> " + tr)
|
||||
}
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
snap_edges = snap_edges + "]"
|
||||
|
||||
// LOAD: one snapshot, one merge (single write).
|
||||
let snap: String = "{\"nodes\":" + snap_nodes + ",\"edges\":" + snap_edges + "}"
|
||||
let path: String = ingest_snap_path()
|
||||
fs_write(path, snap)
|
||||
let resp: String = eg_post("/api/load-merge", "\"path\":" + j_q(path))
|
||||
|
||||
// HONESTY GATE: the local FORM/DEDUP/EDGE decisions above are real (they
|
||||
// describe what this manifold contains), but they are NOT confirmation of
|
||||
// a server write — only this response is. If the server returned an error
|
||||
// (bad auth, network failure, anything), nodes_added/edges_added silently
|
||||
// default to 0 via json_get_int, which reads identically to "everything
|
||||
// was already known" — a real failure and a benign no-op must never look
|
||||
// the same. Surface the distinction explicitly rather than let a caller
|
||||
// (or a human) infer success from a quiet zero.
|
||||
let srv_err: String = json_get_string(resp, "error")
|
||||
if !str_eq(srv_err, "") {
|
||||
return "{\"error\":" + j_q("load-merge failed: " + srv_err) +
|
||||
",\"nodes_formed_locally\":" + int_to_str(created) +
|
||||
",\"nodes_deduped_locally\":" + int_to_str(deduped) +
|
||||
",\"manifold_nodes\":" + int_to_str(nn) +
|
||||
",\"manifold_edges\":" + int_to_str(ne) +
|
||||
",\"note\":" + j_q("nothing below this manifold was confirmed persisted by the server") + "}"
|
||||
}
|
||||
|
||||
let nadd: Int = json_get_int(resp, "nodes_added")
|
||||
let eadd: Int = json_get_int(resp, "edges_added")
|
||||
|
||||
return "{\"nodes_created\":" + int_to_str(created) +
|
||||
",\"nodes_deduped\":" + int_to_str(deduped) +
|
||||
",\"new_in_snapshot\":" + int_to_str(sn_count) +
|
||||
",\"nodes_added\":" + int_to_str(nadd) +
|
||||
",\"edges_resolved\":" + int_to_str(ec) +
|
||||
",\"edges_added\":" + int_to_str(eadd) +
|
||||
",\"manifold_nodes\":" + int_to_str(nn) +
|
||||
",\"manifold_edges\":" + int_to_str(ne) + "}"
|
||||
}
|
||||
|
||||
fn head80(s: String) -> String {
|
||||
let t: String = str_trim(s)
|
||||
if str_len(t) <= 80 { return t }
|
||||
return str_slice(t, 0, 80) + "..."
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// SECTION E — EXTRACTORS (faithful; no invention). Each returns a manifold by
|
||||
// APPENDING to the nodes/edges accumulators via a returned struct.
|
||||
// We accumulate into module-level lists carried by the caller.
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// PROSE: chunk text into a discrete manifold. Split on blank lines into
|
||||
// paragraphs; every non-empty paragraph is its own node (NEVER one blob).
|
||||
// Edges: doc-root -contains-> chunk; chunk -precedes-> next chunk;
|
||||
// most-recent-heading -section_of-> chunk. Content is verbatim (substring of
|
||||
// the source) — pure extraction of ground truth.
|
||||
fn transduce_prose(nodes: [String], edges: [String], text: String,
|
||||
prov: String, ground: String, steward: String,
|
||||
root_lid: String, root_title: String) -> [String] {
|
||||
// returns [nodes_json_list_encoded, edges_json_list_encoded] is awkward in
|
||||
// EL; instead we mutate by returning a 2-list. We package results as a
|
||||
// single JSON array string carrying {nodes:[...],edges:[...]} additions.
|
||||
// (Kept simple: caller passes empty lists and receives the packaged pair.)
|
||||
let tagbase: String = "prov:" + prov + " ground:" + ground + " steward:" + steward
|
||||
// root node
|
||||
nodes = el_list_append(nodes, mk_node(root_lid, "document: " + root_title,
|
||||
"Concept", "Semantic", "0.6", "0.6", "0.9", tagbase + " kind:document"))
|
||||
|
||||
let paras: [String] = str_split(text, "\n\n")
|
||||
let np: Int = el_list_len(paras)
|
||||
let idx: Int = 0
|
||||
let last_chunk: String = ""
|
||||
let last_heading: String = ""
|
||||
let ci: Int = 0
|
||||
while idx < np {
|
||||
let raw: String = str_trim(el_list_get(paras, idx))
|
||||
if !str_eq(raw, "") {
|
||||
let lid: String = root_lid + ":c" + int_to_str(ci)
|
||||
let is_heading: Bool = str_starts_with(raw, "#")
|
||||
let kind: String = if is_heading { "kind:heading" } else { "kind:doc-chunk" }
|
||||
nodes = el_list_append(nodes, mk_node(lid, raw,
|
||||
"Knowledge", "Semantic", "0.55", "0.55", "0.9", tagbase + " " + kind))
|
||||
// containment: document root -contains-> chunk
|
||||
edges = el_list_append(edges, mk_edge(root_lid, "contains", lid))
|
||||
// sequence: previous chunk -precedes-> this chunk
|
||||
if !str_eq(last_chunk, "") {
|
||||
edges = el_list_append(edges, mk_edge(last_chunk, "precedes", lid))
|
||||
}
|
||||
// sectioning: most-recent heading -section_of-> this chunk
|
||||
if is_heading {
|
||||
last_heading = lid
|
||||
} else {
|
||||
if !str_eq(last_heading, "") {
|
||||
edges = el_list_append(edges, mk_edge(last_heading, "section_of", lid))
|
||||
}
|
||||
}
|
||||
last_chunk = lid
|
||||
ci = ci + 1
|
||||
}
|
||||
idx = idx + 1
|
||||
}
|
||||
// package: we return the two lists concatenated via a sentinel; but EL
|
||||
// lists can't nest heterogeneously here, so we instead return nodes and
|
||||
// rely on the caller holding edges by reference is not possible — so we
|
||||
// encode both into one list: [ "N" + nodejson ... , "E" + edgejson ... ].
|
||||
let packed: [String] = el_list_empty()
|
||||
let a: Int = 0
|
||||
let an: Int = el_list_len(nodes)
|
||||
while a < an { packed = el_list_append(packed, "N" + el_list_get(nodes, a)) a = a + 1 }
|
||||
let b: Int = 0
|
||||
let bn: Int = el_list_len(edges)
|
||||
while b < bn { packed = el_list_append(packed, "E" + el_list_get(edges, b)) b = b + 1 }
|
||||
return packed
|
||||
}
|
||||
|
||||
// STRUCTURED / RAW-GEOMETRY: ingest structured primitives (phonetics/formants,
|
||||
// instrument signatures, scene primitives) as GEOMETRY, faithfully. Normalized
|
||||
// input shape:
|
||||
// {"dataset":"<name>","primitive_type":"<t>",
|
||||
// "records":[{"key":"<id>","features":{...categorical...},"attributes":{...}}]}
|
||||
// Each record -> a primitive node; each categorical feature -> a SHARED feature
|
||||
// node (deduped across records: many primitives -> one feature node = real
|
||||
// connective geometry, meaning saturates); numeric attributes fold into the
|
||||
// primitive's content (unique values, no dedup benefit). This is knowledge
|
||||
// represented as geometry, not prose — the path speech/music/image ingest on.
|
||||
fn transduce_structured(nodes: [String], edges: [String], js: String,
|
||||
prov: String, ground: String, steward: String,
|
||||
root_lid: String) -> [String] {
|
||||
// grounding integrity: the SOURCE may declare its own epistemic grounding
|
||||
// (measured / derived / convention / ...) via a top-level "grounding" field;
|
||||
// honor it faithfully over the ingest-time default. This keeps the per-node
|
||||
// ground: facet consistent with the source's honest self-description.
|
||||
let src_ground: String = json_get_string(js, "grounding")
|
||||
let use_ground: String = if str_eq(src_ground, "") { ground } else { src_ground }
|
||||
let tagbase: String = "prov:" + prov + " ground:" + use_ground + " steward:" + steward
|
||||
let dsname: String = json_get_string(js, "dataset")
|
||||
let ptype: String = json_get_string(js, "primitive_type")
|
||||
// capture the source's own scholarly provenance citation (verbatim) onto
|
||||
// the dataset root — faithful attribution, retrievable, reachable from every
|
||||
// primitive via its -contains- edge back to the root.
|
||||
let src_cite: String = json_get_string(js, "provenance")
|
||||
let root_content: String = "dataset: " + dsname + " (" + ptype + ")"
|
||||
if !str_eq(src_cite, "") { root_content = root_content + " | provenance: " + src_cite }
|
||||
nodes = el_list_append(nodes, mk_node(root_lid, root_content,
|
||||
"Concept", "Semantic", "0.6", "0.6", "0.9", tagbase + " kind:dataset"))
|
||||
|
||||
let recs: String = json_get_raw(js, "records")
|
||||
let nr: Int = json_array_len(recs)
|
||||
let r: Int = 0
|
||||
while r < nr {
|
||||
let rec: String = json_array_get(recs, r)
|
||||
let rkey: String = json_get_string(rec, "key")
|
||||
let attrs: String = json_get_raw(rec, "attributes")
|
||||
// faithful compact serialization of the primitive's numeric signature
|
||||
let attr_str: String = flatten_pairs(attrs)
|
||||
let content: String = ptype + " " + rkey
|
||||
if !str_eq(attr_str, "") { content = content + " | " + attr_str }
|
||||
let plid: String = root_lid + ":" + rkey
|
||||
nodes = el_list_append(nodes, mk_node(plid, content,
|
||||
"Concept", "Semantic", "0.6", "0.6", "0.92",
|
||||
tagbase + " kind:primitive primitive:" + ptype + " key:" + rkey))
|
||||
edges = el_list_append(edges, mk_edge(root_lid, "contains", plid))
|
||||
|
||||
// categorical features -> SHARED (deduped) feature nodes + labelled edges
|
||||
let feats: String = json_get_raw(rec, "features")
|
||||
let fkeys: [String] = json_object_keys(feats)
|
||||
let fk: Int = el_list_len(fkeys)
|
||||
let k: Int = 0
|
||||
while k < fk {
|
||||
let fname: String = el_list_get(fkeys, k)
|
||||
let fval: String = json_get_string(feats, fname)
|
||||
// shared feature node: content is the feature=value pair; identical
|
||||
// pairs across records dedup onto ONE node (the geometry).
|
||||
let flid: String = "feat:" + fname + "=" + fval
|
||||
let fcontent: String = fname + "=" + fval
|
||||
nodes = el_list_append(nodes, mk_node(flid, fcontent,
|
||||
"Concept", "Semantic", "0.5", "0.5", "0.9",
|
||||
tagbase + " kind:feature feature:" + fname))
|
||||
edges = el_list_append(edges, mk_edge(plid, fname, flid))
|
||||
k = k + 1
|
||||
}
|
||||
r = r + 1
|
||||
}
|
||||
let packed: [String] = el_list_empty()
|
||||
let a: Int = 0
|
||||
let an: Int = el_list_len(nodes)
|
||||
while a < an { packed = el_list_append(packed, "N" + el_list_get(nodes, a)) a = a + 1 }
|
||||
let b: Int = 0
|
||||
let bn: Int = el_list_len(edges)
|
||||
while b < bn { packed = el_list_append(packed, "E" + el_list_get(edges, b)) b = b + 1 }
|
||||
return packed
|
||||
}
|
||||
|
||||
// flatten a flat JSON object of scalar fields into "k=v k=v" (faithful; values
|
||||
// verbatim). Used for numeric attribute signatures.
|
||||
fn flatten_pairs(obj: String) -> String {
|
||||
if str_eq(obj, "") { return "" }
|
||||
let keys: [String] = json_object_keys(obj)
|
||||
let n: Int = el_list_len(keys)
|
||||
let out: String = ""
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let k: String = el_list_get(keys, i)
|
||||
// json_get_raw returns the raw token — works for NUMBERS (bare, e.g.
|
||||
// "270") where json_get_string yields "" for non-string values. Strip
|
||||
// surrounding quotes if the value happens to be a string token.
|
||||
let raw: String = json_get_raw(obj, k)
|
||||
let v: String = str_replace(raw, "\"", "")
|
||||
let sep: String = if i == 0 { "" } else { " " }
|
||||
out = out + sep + k + "=" + v
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// unpack the "N"/"E"-prefixed packed list back into two lists, then merge
|
||||
fn merge_packed(packed: [String]) -> String {
|
||||
let nodes: [String] = el_list_empty()
|
||||
let edges: [String] = el_list_empty()
|
||||
let n: Int = el_list_len(packed)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let item: String = el_list_get(packed, i)
|
||||
let tag: String = str_slice(item, 0, 1)
|
||||
let rest: String = str_slice(item, 1, str_len(item))
|
||||
if str_eq(tag, "N") { nodes = el_list_append(nodes, rest) }
|
||||
if str_eq(tag, "E") { edges = el_list_append(edges, rest) }
|
||||
i = i + 1
|
||||
}
|
||||
return merge_manifold(nodes, edges)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// SECTION F — DISPATCH on source kind
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
fn basename(path: String) -> String {
|
||||
let parts: [String] = str_split(path, "/")
|
||||
let n: Int = el_list_len(parts)
|
||||
if n == 0 { return path }
|
||||
return el_list_get(parts, n - 1)
|
||||
}
|
||||
|
||||
fn ends_with_ci(s: String, suf: String) -> Bool {
|
||||
return str_ends_with(str_to_lower(s), suf)
|
||||
}
|
||||
|
||||
fn is_text_file(path: String) -> Bool {
|
||||
return ends_with_ci(path, ".md") || ends_with_ci(path, ".txt")
|
||||
|| ends_with_ci(path, ".markdown") || ends_with_ci(path, ".text")
|
||||
}
|
||||
|
||||
// default ingestion grounding; overridable per-invocation via INGEST_GROUND.
|
||||
// Note: a source's OWN top-level "grounding" field (structured) takes precedence
|
||||
// over this — the author's honest self-description wins.
|
||||
fn default_ground() -> String {
|
||||
let g: String = env("INGEST_GROUND")
|
||||
if str_eq(g, "") { return "extracted" }
|
||||
return g
|
||||
}
|
||||
|
||||
fn default_steward() -> String {
|
||||
let s: String = env("INGEST_STEWARD")
|
||||
if str_eq(s, "") { return "local-private" }
|
||||
return s
|
||||
}
|
||||
|
||||
// ingest one file -> report JSON
|
||||
fn ingest_file(path: String) -> String {
|
||||
let text: String = fs_read(path)
|
||||
if str_eq(text, "") {
|
||||
return "{\"error\":\"empty or unreadable\",\"path\":" + j_q(path) + "}"
|
||||
}
|
||||
let prov: String = "file:" + path
|
||||
if ends_with_ci(path, ".json") {
|
||||
let packed: [String] = transduce_structured(el_list_empty(), el_list_empty(),
|
||||
text, prov, default_ground(), default_steward(), "ds:" + basename(path))
|
||||
return merge_packed(packed)
|
||||
}
|
||||
let packed: [String] = transduce_prose(el_list_empty(), el_list_empty(),
|
||||
text, prov, default_ground(), default_steward(),
|
||||
"doc:" + basename(path), basename(path))
|
||||
return merge_packed(packed)
|
||||
}
|
||||
|
||||
// ingest a directory: walk one level, ingest each supported file, aggregate
|
||||
fn ingest_dir(path: String) -> String {
|
||||
let entries: [String] = fs_list(path)
|
||||
let n: Int = el_list_len(entries)
|
||||
let tot_created: Int = 0
|
||||
let tot_deduped: Int = 0
|
||||
let tot_edges: Int = 0
|
||||
let files: Int = 0
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let name: String = str_trim(el_list_get(entries, i))
|
||||
if !str_eq(name, "") {
|
||||
let full: String = path + "/" + name
|
||||
if is_text_file(full) || ends_with_ci(full, ".json") {
|
||||
println("FILE " + full)
|
||||
let rep: String = ingest_file(full)
|
||||
tot_created = tot_created + json_get_int(rep, "nodes_created")
|
||||
tot_deduped = tot_deduped + json_get_int(rep, "nodes_deduped")
|
||||
tot_edges = tot_edges + json_get_int(rep, "edges_added")
|
||||
files = files + 1
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return "{\"kind\":\"directory\",\"path\":" + j_q(path) +
|
||||
",\"files_ingested\":" + int_to_str(files) +
|
||||
",\"nodes_created\":" + int_to_str(tot_created) +
|
||||
",\"nodes_deduped\":" + int_to_str(tot_deduped) +
|
||||
",\"edges_accepted\":" + int_to_str(tot_edges) + "}"
|
||||
}
|
||||
|
||||
// ingest a url: fetch, treat body as prose (faithful extraction of what's there)
|
||||
fn ingest_url(url: String) -> String {
|
||||
let body: String = http_get(url)
|
||||
if str_eq(body, "") { return "{\"error\":\"empty fetch\",\"url\":" + j_q(url) + "}" }
|
||||
let packed: [String] = transduce_prose(el_list_empty(), el_list_empty(),
|
||||
body, "url:" + url, "extracted", "public-web",
|
||||
"url:" + url, url)
|
||||
return merge_packed(packed)
|
||||
}
|
||||
|
||||
// ingest an llm-query: pose the query to the local guide model, take the answer
|
||||
// as a CANDIDATE (provisional, guide-sourced grounding) — never believe-the-
|
||||
// model. The answer is ingested faithfully as what the model said, marked.
|
||||
fn ingest_llm(query: String) -> String {
|
||||
let model: String = if str_eq(env("INGEST_MODEL"), "") { "qwen3:1.7b" } else { env("INGEST_MODEL") }
|
||||
let body: String = "{\"model\":" + j_q(model) + ",\"prompt\":" + j_q(query) + ",\"stream\":false}"
|
||||
let resp: String = http_post_json("http://127.0.0.1:11434/api/generate", body)
|
||||
let answer: String = json_get_string(resp, "response")
|
||||
if str_eq(answer, "") { return "{\"error\":\"no model response\"}" }
|
||||
let packed: [String] = transduce_prose(el_list_empty(), el_list_empty(),
|
||||
answer, "llm:" + model + ":" + query, "candidate-provisional", "guide-provisional",
|
||||
"llm:" + query, "guide answer: " + query)
|
||||
return merge_packed(packed)
|
||||
}
|
||||
|
||||
// ingest a stream: a file whose lines are turns; each line a node, sequence
|
||||
// edges — the conversational-manifold degenerate case (continuous metabolism).
|
||||
fn ingest_stream(path: String) -> String {
|
||||
let text: String = fs_read(path)
|
||||
if str_eq(text, "") { return "{\"error\":\"empty stream\"}" }
|
||||
let lines: [String] = str_split(text, "\n")
|
||||
let nodes: [String] = el_list_empty()
|
||||
let edges: [String] = el_list_empty()
|
||||
let prov: String = "stream:" + path
|
||||
let tagbase: String = "prov:" + prov + " ground:extracted steward:local-private"
|
||||
nodes = el_list_append(nodes, mk_node("stream", "stream: " + basename(path),
|
||||
"Concept", "Semantic", "0.6", "0.6", "0.9", tagbase + " kind:stream"))
|
||||
let n: Int = el_list_len(lines)
|
||||
let i: Int = 0
|
||||
let prev: String = ""
|
||||
let ci: Int = 0
|
||||
while i < n {
|
||||
let ln: String = str_trim(el_list_get(lines, i))
|
||||
if !str_eq(ln, "") {
|
||||
let lid: String = "stream:t" + int_to_str(ci)
|
||||
nodes = el_list_append(nodes, mk_node(lid, ln,
|
||||
"Memory", "Episodic", "0.5", "0.5", "0.85", tagbase + " kind:turn"))
|
||||
edges = el_list_append(edges, mk_edge("stream", "contains", lid))
|
||||
if !str_eq(prev, "") { edges = el_list_append(edges, mk_edge(prev, "precedes", lid)) }
|
||||
prev = lid
|
||||
ci = ci + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
return merge_manifold(nodes, edges)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// SECTION G — ENTRY
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
let kind: String = env("INGEST_KIND")
|
||||
let arg: String = env("INGEST_ARG")
|
||||
|
||||
println("[ingest] organ online — engram=" + eg_base() + " kind=" + kind)
|
||||
println("[ingest] source=" + arg)
|
||||
|
||||
let report: String = ""
|
||||
if str_eq(kind, "dir") {
|
||||
report = ingest_dir(arg)
|
||||
} else {
|
||||
if str_eq(kind, "file") {
|
||||
report = ingest_file(arg)
|
||||
} else {
|
||||
if str_eq(kind, "structured") {
|
||||
report = ingest_file(arg)
|
||||
} else {
|
||||
if str_eq(kind, "url") {
|
||||
report = ingest_url(arg)
|
||||
} else {
|
||||
if str_eq(kind, "llm") {
|
||||
report = ingest_llm(arg)
|
||||
} else {
|
||||
if str_eq(kind, "stream") {
|
||||
report = ingest_stream(arg)
|
||||
} else {
|
||||
report = "{\"error\":\"unknown INGEST_KIND: " + kind + "\"}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println("REPORT " + report)
|
||||
@@ -1,120 +0,0 @@
|
||||
# sandbox — the Neuron STACK sandbox
|
||||
|
||||
**Work on a whole stack at once, not one repo at a time.** `sandbox` assembles every
|
||||
constituent repo of a named stack into **one combined worktree workspace**, wired so
|
||||
they build and run **together**, on an isolated clean base — then tears it all down
|
||||
cleanly. The live soul/engram (`:7770` / `:8742`) are never touched.
|
||||
|
||||
It is the multi-repo sibling of [`nsbx`](./README.md): where `nsbx dev` stands up
|
||||
**one** repo's worktree + an isolated engram, `sandbox` stands up **every** repo of a
|
||||
stack as sibling git worktrees under a single workspace.
|
||||
|
||||
```bash
|
||||
export PATH="$PWD:$PATH" # or symlink `sandbox` onto your PATH
|
||||
|
||||
sandbox neuron-stack tim # el + neuron soul + NeuronUI, assembled together
|
||||
cd ~/Development/neuron-technologies/stack-worktrees/neuron-stack-tim
|
||||
source .stack-env # EL_REPO + PATH now point at the SANDBOX el
|
||||
./build.sh # engram compiles, soul compiles, UI present & buildable
|
||||
sandbox down neuron-stack tim # remove every worktree; live untouched
|
||||
```
|
||||
|
||||
## The two profiles
|
||||
|
||||
### `el-stack` — the whole EL kit
|
||||
The compiler + language + framework + tooling are **all one repo** (`foundation/el`:
|
||||
`lang/` = elc/elb + runtime, `engram/src/server.el`, `elp/` = NLG, `ui/` = the **el-ui
|
||||
framework**, plus `ql`, `ide`, `epm`, `arbor`, `tools`). Its downstream SDK consumers
|
||||
come along so a change to `elc` can be proven end-to-end across the kit.
|
||||
|
||||
| repo | required | role |
|
||||
|------|----------|------|
|
||||
| `foundation/el` | ✓ | elc + elb compiler, el_runtime, engram source, **el-ui framework**, elp/ql/ide/epm tooling |
|
||||
| `engram-language` | | language-faculty reference POC (Python) — being ported into `el/elp` |
|
||||
| `foundation/forge` | | downstream SDK consumer — `make build` |
|
||||
| `foundation/dharma` | | downstream SDK consumer — CGI provenance registry |
|
||||
|
||||
`build.sh` proves it: `elc` compiles a real stack source and `cc` links it against the
|
||||
runtime into a native binary (elc + runtime build together), and — if present — `forge`
|
||||
builds against the freshly-assembled SDK.
|
||||
|
||||
### `neuron-stack` — the full product
|
||||
Substrate + soul + UI. **Engram is not a separate repo** — its source lives inside
|
||||
`foundation/el`.
|
||||
|
||||
| repo | required | role |
|
||||
|------|----------|------|
|
||||
| `foundation/el` | ✓ | substrate: elc + el_runtime + engram source + the `elp` NLG the soul imports |
|
||||
| `neuron` | ✓ | the soul (`:7770`) + engram build; `soul.el` imports `../foundation/el/elp/src/elp.el` |
|
||||
| `products/NeuronUI` | ✓ | the app/UI (Kotlin/Compose desktop client; bundles the soul binary) |
|
||||
| `products/web` | | marketing site + interactive soul-demo |
|
||||
|
||||
`build.sh` proves it: **engram** builds (`elc engram/src/server.el` → `cc … el_runtime.c`
|
||||
→ native binary), the **soul** compiles with its cross-repo `../foundation/el` import
|
||||
resolving to the *sandbox* el, and the **UI** is present with its build entry.
|
||||
|
||||
## Why it works — mirrored-layout wiring
|
||||
|
||||
The repos reference each other by **relative sibling paths** (e.g. the soul imports
|
||||
`../foundation/el/elp/src/elp.el`). So `sandbox` lays every worktree out at its **natural
|
||||
relative path** inside the workspace:
|
||||
|
||||
```
|
||||
stack-worktrees/neuron-stack-tim/
|
||||
├── foundation/el/ ← worktree of foundation/el (the sandbox el)
|
||||
├── neuron/ ← worktree of neuron
|
||||
└── products/NeuronUI/ ← worktree of products/NeuronUI
|
||||
```
|
||||
|
||||
From `neuron/`, `../foundation/el` resolves to `…/neuron-stack-tim/foundation/el` — the
|
||||
**sandbox** copy, never the live tree. No symlinks, no path rewriting: the layout *is*
|
||||
the wiring. `.stack-env` additionally pins `EL_REPO` and prepends the sandbox `elc`/`elb`
|
||||
to `PATH`.
|
||||
|
||||
## Commands
|
||||
|
||||
| command | does |
|
||||
|---------|------|
|
||||
| `sandbox el-stack <name> [--minimal]` | assemble the EL kit (`--minimal` = required repos only) |
|
||||
| `sandbox neuron-stack <name> [--minimal]` | assemble the full product |
|
||||
| `sandbox build <profile> <name>` | run the workspace's combined `build.sh` |
|
||||
| `sandbox status <profile> <name>` | per-repo head + clean/dirty |
|
||||
| `sandbox list` | list assembled workspaces |
|
||||
| `sandbox down <profile> <name> [--delete-branch]` | remove every worktree + drop the workspace (branch kept unless `--delete-branch`) |
|
||||
|
||||
Flags: `--minimal` (required repos only), `--branch B` (branch name; default
|
||||
`sandbox/<profile>-<name>`), `--base REF` (fork point; default each repo's committed
|
||||
HEAD).
|
||||
|
||||
## Rails (always)
|
||||
|
||||
- **Clean base** — worktrees fork off each repo's **committed HEAD**; the dirty state of
|
||||
the live checkout is deliberately *not* carried in.
|
||||
- **Persistent** — the workspace lives under `NSBX_STACK_ROOT` (default
|
||||
`~/Development/neuron-technologies/stack-worktrees`), **never `/tmp`** (ablated on
|
||||
compaction).
|
||||
- **Never touches live** — `sandbox` only does `git worktree` + offline `cc`. It never
|
||||
binds `:8742`/`:7770`, never `launchctl`, never `pkill`. Bringing up an **isolated
|
||||
engram** is delegated, opt-in, to `nsbx` (which guards the live store and refuses the
|
||||
live ports).
|
||||
- **Idempotent & safe** — refuses to clobber an existing workspace; a failed assembly
|
||||
rolls back its partial worktrees; teardown removes worktrees through their origin repo
|
||||
and prunes.
|
||||
- **Own-the-core** — pure bash + `git worktree`. No new dependencies.
|
||||
|
||||
## Env knobs
|
||||
|
||||
`NSBX_STACK_ROOT` (workspace root), `NEURON_DEV_ROOT` (the dir holding all the peer
|
||||
repos, default `~/Development/neuron-technologies`).
|
||||
|
||||
## Isolated engram for `neuron-stack` (opt-in)
|
||||
|
||||
`sandbox` gets the code building together; to run the soul against an **isolated** engram
|
||||
(never live), delegate to `nsbx` from inside the workspace:
|
||||
|
||||
```bash
|
||||
source .stack-env
|
||||
nsbx create $STACK_NAME --source "$EL_REPO" # clone live store onto a non-default port
|
||||
nsbx up $STACK_NAME
|
||||
nsbx status $STACK_NAME # prints the isolated engram URL
|
||||
```
|
||||
@@ -1,505 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# sandbox — the Neuron STACK sandbox: assemble a WHOLE stack of repos into ONE
|
||||
# combined worktree workspace, wired so they build/run TOGETHER, on an isolated
|
||||
# clean base — so an agent (or Will) can work on the full stack at once instead of
|
||||
# one repo at a time.
|
||||
#
|
||||
# It is the multi-repo generalisation of `nsbx` (this same directory): where
|
||||
# `nsbx dev` stands up ONE repo's worktree + an isolated engram, `sandbox` stands
|
||||
# up EVERY constituent repo of a named stack as sibling git worktrees under a
|
||||
# single workspace, mirroring their on-disk relative layout so the cross-repo
|
||||
# `../foundation/el` imports resolve to the SANDBOX copy — never the live tree.
|
||||
#
|
||||
# sandbox el-stack <name> # elc compiler + EL language + framework + tooling (+ consumers)
|
||||
# sandbox neuron-stack <name> # runtime/soul + engram + app/UI (the full product)
|
||||
# sandbox list # list assembled stack workspaces
|
||||
# sandbox status <profile> <name> # inspect one
|
||||
# sandbox build <profile> <name> # run the workspace's combined build.sh
|
||||
# sandbox down <profile> <name> # tear down: remove every worktree, drop the workspace
|
||||
#
|
||||
# RAILS (always):
|
||||
# * worktrees fork off each repo's COMMITTED HEAD -> a clean, reproducible base
|
||||
# (the dirty state of the live checkout is deliberately NOT carried in).
|
||||
# * the workspace lives at a PERSISTENT path (never /tmp — ablated on compaction).
|
||||
# * NEVER touches the live soul/engram (:7770 / :8742). It only creates git
|
||||
# worktrees + a build script; bringing up an isolated engram is delegated,
|
||||
# opt-in, to `nsbx` (which already guards the live store & ports).
|
||||
# * idempotent & safe: refuses to clobber an existing workspace; teardown removes
|
||||
# worktrees through their origin repo and prunes — branches are kept by default.
|
||||
# * own-the-core: pure bash + git worktree. No new dependencies.
|
||||
set -uo pipefail
|
||||
|
||||
# ---------------------------------------------------------------- constants ----
|
||||
# Root that holds all the peer repos (neuron, foundation/el, products/*, ...).
|
||||
DEV_ROOT="${NEURON_DEV_ROOT:-$HOME/Development/neuron-technologies}"
|
||||
# Where assembled stack workspaces live (persistent; sibling to el-worktrees/).
|
||||
STACK_ROOT="${NSBX_STACK_ROOT:-$DEV_ROOT/stack-worktrees}"
|
||||
EL_REPO_REL="foundation/el"
|
||||
LIVE_ENGRAM_PORT=8742 # live engram — sandbox must never bind it
|
||||
LIVE_SOUL_PORT=7770 # live soul — sandbox must never bind it
|
||||
# nsbx (single-repo isolated-engram tool) lives next to this script.
|
||||
NSBX="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)/nsbx"
|
||||
|
||||
C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YEL=$'\033[33m'; C_CYN=$'\033[36m'; C_DIM=$'\033[2m'; C_BLD=$'\033[1m'; C_0=$'\033[0m'
|
||||
|
||||
# ---------------------------------------------------------------- helpers ------
|
||||
die(){ printf '%serror:%s %s\n' "$C_RED" "$C_0" "$*" >&2; exit 1; }
|
||||
log(){ printf '%s==>%s %s\n' "$C_BLD" "$C_0" "$*" >&2; }
|
||||
info(){ printf ' %s\n' "$*" >&2; }
|
||||
ok(){ printf ' %s%s%s\n' "$C_GRN" "$*" "$C_0" >&2; }
|
||||
warn(){ printf ' %s%s%s\n' "$C_YEL" "$*" "$C_0" >&2; }
|
||||
need(){ command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1"; }
|
||||
|
||||
# ---------------------------------------------------------------- profiles -----
|
||||
# profile_repos <profile> : emit one line per constituent repo:
|
||||
# <relpath-under-DEV_ROOT> | <required|optional> | <role>
|
||||
# The relpath is preserved INSIDE the workspace, so all cross-repo `../foundation/el`
|
||||
# references resolve to the sandbox copy automatically (mirrored-layout wiring).
|
||||
profile_repos(){
|
||||
case "$1" in
|
||||
el-stack)
|
||||
# The compiler+language+framework+tooling are all ONE repo (foundation/el).
|
||||
# Its downstream SDK consumers (forge, dharma) + the language-faculty POC come
|
||||
# along so a change to elc can be proven end-to-end across the kit.
|
||||
cat <<'EOF'
|
||||
foundation/el | required | elc + elb compiler, el_runtime, engram source, el-ui framework, elp/ql/ide/epm tooling
|
||||
engram-language | optional | language-faculty reference POC (Python) — ported into el/elp
|
||||
foundation/forge | optional | downstream SDK consumer — `make build` (imprint forge CLI)
|
||||
foundation/dharma | optional | downstream SDK consumer — CGI provenance registry
|
||||
EOF
|
||||
;;
|
||||
neuron-stack)
|
||||
# The full product: substrate (el) + soul + UI. Engram is NOT a separate repo
|
||||
# (its source lives in foundation/el/engram/src/server.el).
|
||||
cat <<'EOF'
|
||||
foundation/el | required | substrate: elc + el_runtime + engram source + elp NLG the soul imports
|
||||
neuron | required | the soul (:7770) + engram build; soul.el imports ../foundation/el/elp/src/elp.el
|
||||
products/NeuronUI | required | the app/UI (Kotlin/Compose desktop client; bundles the soul binary)
|
||||
products/web | optional | marketing site + interactive soul-demo
|
||||
EOF
|
||||
;;
|
||||
*) return 1;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_profile(){ profile_repos "$1" >/dev/null 2>&1; }
|
||||
|
||||
ws_dir(){ printf '%s/%s-%s' "$STACK_ROOT" "$1" "$2"; } # <root>/<profile>-<name>
|
||||
ws_branch(){ printf 'sandbox/%s-%s' "$1" "$2"; } # branch name used in each repo
|
||||
manifest(){ printf '%s/.stack-manifest.json' "$1"; } # <ws>/.stack-manifest.json
|
||||
|
||||
# ================================================================ up ===========
|
||||
cmd_up(){
|
||||
local profile="$1"; shift
|
||||
local name="" branch="" base_override="" minimal=0
|
||||
[ $# -gt 0 ] && [ "${1#-}" = "$1" ] && { name="$1"; shift; } || die "usage: sandbox $profile <name> [--minimal] [--branch B] [--base REF]"
|
||||
while [ $# -gt 0 ]; do case "$1" in
|
||||
--minimal) minimal=1; shift;;
|
||||
--branch) branch="$2"; shift 2;;
|
||||
--base) base_override="$2"; shift 2;;
|
||||
*) die "unknown flag: $1";;
|
||||
esac; done
|
||||
need git
|
||||
is_profile "$profile" || die "unknown profile: $profile (try: el-stack | neuron-stack)"
|
||||
|
||||
local ws; ws="$(ws_dir "$profile" "$name")"
|
||||
[ -n "$branch" ] || branch="$(ws_branch "$profile" "$name")"
|
||||
|
||||
# -------- pre-flight (fail before creating anything) --------
|
||||
case "$ws" in /tmp/*|/private/tmp/*|/var/tmp/*)
|
||||
die "refusing workspace under a temp dir ($ws) — temp dirs are ablated on compaction; set NSBX_STACK_ROOT to a persistent path";;
|
||||
esac
|
||||
[ -e "$ws" ] && die "workspace already exists: $ws (sandbox down $profile $name first)"
|
||||
|
||||
# resolve + validate every repo, and pick a base sha per repo, BEFORE touching disk
|
||||
local -a rels roles bases origins wts
|
||||
local line rel role_extra role req origin base wt
|
||||
while IFS= read -r line; do
|
||||
[ -z "${line// }" ] && continue
|
||||
rel="$(printf '%s' "$line" | cut -d'|' -f1 | xargs)"
|
||||
req="$(printf '%s' "$line" | cut -d'|' -f2 | xargs)"
|
||||
role="$(printf '%s' "$line" | cut -d'|' -f3- | sed 's/^ *//')"
|
||||
[ "$minimal" -eq 1 ] && [ "$req" = "optional" ] && continue
|
||||
origin="$DEV_ROOT/$rel"
|
||||
git -C "$origin" rev-parse --git-dir >/dev/null 2>&1 || {
|
||||
[ "$req" = "required" ] && die "required repo missing or not a git repo: $origin"
|
||||
warn "skipping optional repo (missing): $rel"; continue; }
|
||||
if [ -n "$base_override" ]; then base="$base_override"; else base="$(git -C "$origin" rev-parse HEAD)"; fi
|
||||
wt="$ws/$rel"
|
||||
[ -e "$wt" ] && die "target worktree path already exists: $wt"
|
||||
rels+=("$rel"); roles+=("$role"); origins+=("$origin"); bases+=("$base"); wts+=("$wt")
|
||||
done < <(profile_repos "$profile")
|
||||
[ "${#rels[@]}" -gt 0 ] || die "no repos resolved for profile $profile"
|
||||
|
||||
log "assembling '$profile' workspace '$name'"
|
||||
info "workspace: $ws"
|
||||
info "branch: $branch (created in each repo, off its committed HEAD)"
|
||||
mkdir -p "$ws"
|
||||
|
||||
# -------- create a worktree per repo (mirrored relpath layout) --------
|
||||
local i n="${#rels[@]}"
|
||||
SB_DONE_WTS=(); SB_DONE_ORIGINS=()
|
||||
for ((i=0; i<n; i++)); do
|
||||
rel="${rels[$i]}"; origin="${origins[$i]}"; base="${bases[$i]}"; wt="${wts[$i]}"
|
||||
mkdir -p "$(dirname "$wt")"
|
||||
local gerr
|
||||
if git -C "$origin" show-ref --verify --quiet "refs/heads/$branch"; then
|
||||
gerr="$(git -C "$origin" worktree add "$wt" "$branch" 2>&1)" \
|
||||
|| { _rollback; die "git worktree add failed for $rel (existing branch $branch):"$'\n'" $gerr"; }
|
||||
else
|
||||
gerr="$(git -C "$origin" worktree add -b "$branch" "$wt" "$base" 2>&1)" \
|
||||
|| { _rollback; die "git worktree add -b $branch failed for $rel (base $base):"$'\n'" $gerr"; }
|
||||
fi
|
||||
SB_DONE_WTS+=("$wt"); SB_DONE_ORIGINS+=("$origin")
|
||||
ok "worktree: $rel -> ${wt#$ws/} (branch $branch @ ${base:0:9})"
|
||||
done
|
||||
|
||||
local el_ws="$ws/$EL_REPO_REL"
|
||||
_write_env "$ws" "$profile" "$name" "$branch" "$el_ws"
|
||||
_write_manifest "$ws" "$profile" "$name" "$branch"
|
||||
_write_build "$ws" "$profile" "$el_ws"
|
||||
_write_readme "$ws" "$profile" "$name" "$branch" "$el_ws"
|
||||
|
||||
# -------- summary --------
|
||||
echo >&2
|
||||
printf '%s STACK WORKSPACE READY — %s / %s%s\n' "$C_BLD" "$profile" "$name" "$C_0" >&2
|
||||
printf ' %-11s %s\n' "workspace" "$ws" >&2
|
||||
printf ' %-11s %s\n' "branch" "$branch (in each repo)" >&2
|
||||
printf ' %-11s %s\n' "repos" "$n worktrees, mirrored layout" >&2
|
||||
echo >&2
|
||||
info "get in: cd $ws && source .stack-env"
|
||||
info "build all: sandbox build $profile $name # (or: cd $ws && ./build.sh)"
|
||||
if [ "$profile" = "neuron-stack" ]; then
|
||||
info "isolated engram (opt-in, via nsbx):"
|
||||
info " nsbx create $profile-$name --source $el_ws && nsbx up $profile-$name"
|
||||
fi
|
||||
info "tear down: sandbox down $profile $name # removes all worktrees; branches kept"
|
||||
}
|
||||
|
||||
# _rollback : remove any worktrees already created this run (globals set by cmd_up)
|
||||
SB_DONE_WTS=(); SB_DONE_ORIGINS=()
|
||||
_rollback(){
|
||||
local j
|
||||
[ "${#SB_DONE_WTS[@]}" -gt 0 ] && warn "rolling back ${#SB_DONE_WTS[@]} partial worktree(s)"
|
||||
for ((j=${#SB_DONE_WTS[@]}-1; j>=0; j--)); do
|
||||
git -C "${SB_DONE_ORIGINS[$j]}" worktree remove --force "${SB_DONE_WTS[$j]}" 2>/dev/null || rm -rf "${SB_DONE_WTS[$j]}"
|
||||
git -C "${SB_DONE_ORIGINS[$j]}" worktree prune 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- writers ------
|
||||
_write_env(){
|
||||
local ws="$1" profile="$2" name="$3" branch="$4" el_ws="$5"
|
||||
local elc_dir="$el_ws/lang/dist/platform"
|
||||
cat > "$ws/.stack-env" <<ENV
|
||||
# stack env for '$profile/$name' — SOURCE this to work the whole stack together.
|
||||
# Pins EL_REPO + PATH at the SANDBOX copy of foundation/el, so elc/elb/runtime and
|
||||
# every cross-repo ../foundation/el import resolve INSIDE this workspace.
|
||||
# The live mind (:$LIVE_ENGRAM_PORT engram / :$LIVE_SOUL_PORT soul) is deliberately NOT referenced.
|
||||
export STACK_NAME="$profile-$name"
|
||||
export STACK_PROFILE="$profile"
|
||||
export STACK_ROOT_WS="$ws"
|
||||
export EL_REPO="$el_ws"
|
||||
export PATH="$elc_dir:\$PATH" # elc, elb (darwin/linux prebuilt) from the sandbox el
|
||||
ENV
|
||||
if [ "$profile" = "neuron-stack" ]; then
|
||||
cat >> "$ws/.stack-env" <<ENV
|
||||
export NEURON_REPO="$ws/neuron"
|
||||
export NEURONUI_REPO="$ws/products/NeuronUI"
|
||||
# engram/soul are NOT bound here — bring up an ISOLATED engram via nsbx when needed
|
||||
# (nsbx guards the live store & refuses ports :$LIVE_ENGRAM_PORT/:$LIVE_SOUL_PORT):
|
||||
# nsbx create $profile-$name --source \$EL_REPO && nsbx up $profile-$name
|
||||
# nsbx status $profile-$name # prints the isolated engram URL to point the soul at
|
||||
ENV
|
||||
fi
|
||||
# direnv convenience
|
||||
[ -e "$ws/.envrc" ] || printf 'source_env .stack-env 2>/dev/null || source .stack-env\n' > "$ws/.envrc"
|
||||
}
|
||||
|
||||
_write_manifest(){
|
||||
local ws="$1" profile="$2" name="$3" branch="$4"
|
||||
# emit worktree records from git's own worktree list, filtered to this workspace
|
||||
python3 - "$ws" "$profile" "$name" "$branch" "$DEV_ROOT" <<'PY'
|
||||
import json, os, subprocess, sys
|
||||
ws, profile, name, branch, dev = sys.argv[1:6]
|
||||
repos = []
|
||||
for rel in sorted(os.listdir(ws)) if False else []:
|
||||
pass
|
||||
# discover worktrees by walking one level of relpaths we created
|
||||
def git(root, *a):
|
||||
return subprocess.run(["git","-C",root,*a], capture_output=True, text=True).stdout.strip()
|
||||
for dirpath, dirnames, filenames in os.walk(ws):
|
||||
if ".git" in filenames or ".git" in dirnames:
|
||||
rel = os.path.relpath(dirpath, ws)
|
||||
toplevel = git(dirpath, "rev-parse", "--show-toplevel")
|
||||
common = git(dirpath, "rev-parse", "--git-common-dir")
|
||||
origin = os.path.realpath(os.path.join(common, ".."))
|
||||
head = git(dirpath, "rev-parse", "HEAD")
|
||||
repos.append({"rel": rel, "worktree": dirpath, "origin": origin,
|
||||
"branch": branch, "head": head})
|
||||
dirnames[:] = [] # don't descend into a repo
|
||||
repos.sort(key=lambda r: r["rel"])
|
||||
json.dump({"profile": profile, "name": name, "branch": branch,
|
||||
"workspace": ws, "repos": repos},
|
||||
open(os.path.join(ws, ".stack-manifest.json"), "w"), indent=2)
|
||||
PY
|
||||
}
|
||||
|
||||
_write_build(){
|
||||
local ws="$1" profile="$2" el_ws="$3"
|
||||
cat > "$ws/build.sh" <<'BUILD'
|
||||
#!/usr/bin/env bash
|
||||
# build.sh — build the assembled stack together, in dependency order.
|
||||
# Generated by `sandbox`. Run from the workspace root (it sources .stack-env).
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")"; source ./.stack-env
|
||||
say(){ printf '\033[1m==>\033[0m %s\n' "$*"; }
|
||||
ok(){ printf ' \033[32m%s\033[0m\n' "$*"; }
|
||||
bad(){ printf ' \033[31m%s\033[0m\n' "$*"; }
|
||||
|
||||
# locate an elc that runs on THIS machine (darwin-arm64 / linux-amd64), from the sandbox el
|
||||
find_elc(){
|
||||
local d="$EL_REPO/lang/dist/platform"
|
||||
case "$(uname -s)-$(uname -m)" in
|
||||
Darwin-arm64) echo "$d/elc-darwin-arm64";;
|
||||
Linux-x86_64) echo "$d/elc-linux-amd64";;
|
||||
*) echo "$d/elc";;
|
||||
esac
|
||||
}
|
||||
ELC="$(find_elc)"; [ -x "$ELC" ] || ELC="$EL_REPO/lang/dist/platform/elc"
|
||||
say "elc: $ELC"
|
||||
[ -x "$ELC" ] && ok "$("$ELC" 2>&1 | head -1 || echo present)" || { bad "elc not executable"; exit 1; }
|
||||
|
||||
# canonical runtime C to link (CI-published release copy; ~8 copies exist in-tree)
|
||||
RT="$EL_REPO/lang/releases/v1.0.0-20260501"
|
||||
[ -f "$RT/el_runtime.c" ] || RT="$EL_REPO/lang/el-compiler/runtime"
|
||||
[ -f "$RT/el_runtime.c" ] && ok "el_runtime: $RT/el_runtime.c" || bad "no el_runtime.c found under $EL_REPO/lang"
|
||||
BUILD
|
||||
|
||||
if [ "$profile" = "el-stack" ]; then
|
||||
cat >> "$ws/build.sh" <<'BUILD'
|
||||
|
||||
# ---- EL STACK: prove elc + the el stuff (incl. the el-ui framework) build together ----
|
||||
say "el-ui framework present: $EL_REPO/ui"
|
||||
[ -d "$EL_REPO/ui" ] && ok "framework dir present ($(ls "$EL_REPO/ui" | tr '\n' ' '))" || bad "no ui/ dir"
|
||||
|
||||
# end-to-end compiler proof: elc compiles a real, substantial stack source to C,
|
||||
# then cc links it against the runtime -> a working native binary.
|
||||
B="$(mktemp -d)"
|
||||
say "elc end-to-end: compile engram/src/server.el and link a native binary"
|
||||
if "$ELC" "$EL_REPO/engram/src/server.el" > "$B/x.c" 2>"$B/elc.err"; then
|
||||
ok "elc -> C ($(wc -c <"$B/x.c" | tr -d ' ') bytes)"
|
||||
if cc -std=c11 -O2 -w -I "$RT" -o "$B/x" "$B/x.c" "$RT/el_runtime.c" -lcurl -lpthread -lm 2>"$B/cc.err"; then
|
||||
ok "cc link ok -> native binary $(ls -lh "$B/x" | awk '{print $5}') (elc + runtime build together)"
|
||||
else
|
||||
bad "cc link failed:"; grep -i 'error:' "$B/cc.err" | sort -u | head | sed 's/^/ /'
|
||||
fi
|
||||
else
|
||||
bad "elc compile failed:"; sed 's/^/ /' "$B/elc.err" | head
|
||||
fi
|
||||
|
||||
# optional downstream consumer: forge builds on the SDK (make build) — proves the
|
||||
# freshly-assembled el SDK still compiles a real downstream repo.
|
||||
FORGE="$STACK_ROOT_WS/foundation/forge"
|
||||
if [ -f "$FORGE/Makefile" ]; then
|
||||
say "downstream consumer: foundation/forge (make build)"
|
||||
( cd "$FORGE" && EL_REPO="$EL_REPO" PATH="$EL_REPO/lang/dist/platform:$PATH" make build ) \
|
||||
&& ok "forge built against the sandbox SDK" || bad "forge build failed (see above)"
|
||||
fi
|
||||
say "el-stack build complete"
|
||||
BUILD
|
||||
else
|
||||
cat >> "$ws/build.sh" <<'BUILD'
|
||||
|
||||
# ---- 1) engram (elc engram/src/server.el -> cc engram.c el_runtime.c), from the sandbox el ----
|
||||
say "build engram from $EL_REPO/engram/src/server.el"
|
||||
B="$(mktemp -d)"
|
||||
if "$ELC" "$EL_REPO/engram/src/server.el" > "$B/engram.c" 2>"$B/elc.err"; then
|
||||
ok "elc -> engram.c ($(wc -c <"$B/engram.c" | tr -d ' ') bytes)"
|
||||
if cc -std=c11 -O2 -w -I "$RT" -o "$B/engram" \
|
||||
"$B/engram.c" "$RT/el_runtime.c" -lcurl -lpthread -lm 2>"$B/cc.err"; then
|
||||
ok "engram binary built: $(ls -lh "$B/engram" | awk '{print $5}')"
|
||||
else
|
||||
bad "engram cc link failed:"; grep -i 'error:' "$B/cc.err" | sort -u | head | sed 's/^/ /'
|
||||
fi
|
||||
else
|
||||
bad "engram elc transpile failed:"; sed 's/^/ /' "$B/elc.err"
|
||||
fi
|
||||
|
||||
# ---- 2) soul (imports ../foundation/el/elp/src/elp.el — resolves to SANDBOX el) ----
|
||||
say "soul present + cross-repo import resolves inside the sandbox"
|
||||
[ -f "$NEURON_REPO/soul.el" ] && ok "neuron/soul.el present" || bad "no soul.el"
|
||||
if [ -f "$EL_REPO/elp/src/elp.el" ]; then
|
||||
ok "../foundation/el/elp/src/elp.el resolves -> $EL_REPO/elp/src/elp.el (sandbox copy)"
|
||||
else
|
||||
bad "elp NLG source missing under sandbox el"
|
||||
fi
|
||||
# soul is a heavy single-TU compile; prove elc parses it rather than a full link
|
||||
if "$ELC" "$NEURON_REPO/soul.el" > "$B/soul.c" 2>"$B/soul.err"; then
|
||||
ok "elc compiled soul.el -> $(wc -c <"$B/soul.c" | tr -d ' ') bytes of C (cross-repo imports resolved)"
|
||||
else
|
||||
bad "soul.el elc compile failed:"; sed 's/^/ /' "$B/soul.err" | head
|
||||
fi
|
||||
|
||||
# ---- 3) UI (present + buildable; gradle/JDK21 is heavy so we don't run it here) ----
|
||||
say "app/UI present + buildable"
|
||||
if [ -f "$NEURONUI_REPO/build.sh" ] || [ -f "$NEURONUI_REPO/gradlew" ]; then
|
||||
ok "NeuronUI build entry present (./build.sh / ./gradlew — needs JDK21; run: cd $NEURONUI_REPO && ./gradlew run)"
|
||||
else
|
||||
bad "no NeuronUI build entry"
|
||||
fi
|
||||
say "neuron-stack build complete (engram compiled, soul compiled, UI present & buildable)"
|
||||
BUILD
|
||||
fi
|
||||
chmod +x "$ws/build.sh"
|
||||
}
|
||||
|
||||
_write_readme(){
|
||||
local ws="$1" profile="$2" name="$3" branch="$4" el_ws="$5"
|
||||
cat > "$ws/README.md" <<MD
|
||||
# $profile / $name — combined stack workspace
|
||||
|
||||
Assembled by \`sandbox\`. Every constituent repo is a **git worktree** on branch
|
||||
\`$branch\`, forked off its origin repo's committed HEAD, laid out at its natural
|
||||
relative path so cross-repo \`../foundation/el\` imports resolve **inside this
|
||||
workspace** (the sandbox el), never the live tree.
|
||||
|
||||
## Get in
|
||||
\`\`\`bash
|
||||
cd $ws
|
||||
source .stack-env # EL_REPO + PATH now point at the sandbox el
|
||||
./build.sh # build the stack together (or: sandbox build $profile $name)
|
||||
\`\`\`
|
||||
|
||||
## Layout
|
||||
__STACK_LAYOUT__
|
||||
|
||||
## Isolation
|
||||
- Worktrees only; the live soul/engram (:$LIVE_SOUL_PORT / :$LIVE_ENGRAM_PORT) are never touched.
|
||||
- To run against an **isolated engram**, delegate to \`nsbx\` (guards the live store/ports):
|
||||
\`\`\`bash
|
||||
nsbx create $profile-$name --source \$EL_REPO && nsbx up $profile-$name
|
||||
\`\`\`
|
||||
|
||||
## Tear down
|
||||
\`\`\`bash
|
||||
sandbox down $profile $name # remove every worktree; branch '$branch' kept
|
||||
sandbox down $profile $name --delete-branch
|
||||
\`\`\`
|
||||
MD
|
||||
# fill the layout list from the manifest without embedding backticks in the heredoc
|
||||
python3 - "$ws" <<'PY'
|
||||
import json, os, sys
|
||||
ws = sys.argv[1]
|
||||
d = json.load(open(os.path.join(ws, ".stack-manifest.json")))
|
||||
lines = ["- `%s` <- worktree of %s" % (r["rel"], r["origin"]) for r in d["repos"]]
|
||||
p = os.path.join(ws, "README.md")
|
||||
txt = open(p).read().replace("__STACK_LAYOUT__", "\n".join(lines))
|
||||
open(p, "w").write(txt)
|
||||
PY
|
||||
}
|
||||
|
||||
# ================================================================ down =========
|
||||
cmd_down(){
|
||||
local profile="$1" name="$2"; shift 2 || true
|
||||
local del_branch=0
|
||||
while [ $# -gt 0 ]; do case "$1" in
|
||||
--delete-branch) del_branch=1; shift;;
|
||||
*) die "unknown flag: $1";;
|
||||
esac; done
|
||||
need git
|
||||
local ws; ws="$(ws_dir "$profile" "$name")"
|
||||
[ -d "$ws" ] || die "no such workspace: $ws"
|
||||
local mf; mf="$(manifest "$ws")"
|
||||
[ -f "$mf" ] || die "no manifest in $ws (refusing to guess); remove it by hand if intended"
|
||||
local branch; branch="$(python3 -c "import json;print(json.load(open('$mf'))['branch'])")"
|
||||
|
||||
log "tearing down '$profile/$name' ($ws)"
|
||||
# remove each worktree through its origin repo
|
||||
python3 -c "import json;[print(r['origin']+'\t'+r['worktree']) for r in json.load(open('$mf'))['repos']]" \
|
||||
| while IFS=$'\t' read -r origin wt; do
|
||||
if [ -d "$wt" ]; then
|
||||
git -C "$origin" worktree remove --force "$wt" 2>/dev/null || rm -rf "$wt"
|
||||
git -C "$origin" worktree prune 2>/dev/null || true
|
||||
ok "removed worktree: ${wt#$ws/}"
|
||||
fi
|
||||
if [ "$del_branch" -eq 1 ]; then
|
||||
git -C "$origin" branch -D "$branch" 2>/dev/null && ok "deleted branch $branch in ${origin#$DEV_ROOT/}" || true
|
||||
fi
|
||||
done
|
||||
# drop the (now worktree-free) workspace tree
|
||||
rm -rf "$ws"
|
||||
ok "workspace removed: $ws"
|
||||
[ "$del_branch" -eq 1 ] || info "branch '$branch' kept in each repo (use --delete-branch to drop)"
|
||||
ok "down '$profile/$name' complete (live untouched)"
|
||||
}
|
||||
|
||||
# ================================================================ build ========
|
||||
cmd_build(){
|
||||
local profile="$1" name="$2"; local ws; ws="$(ws_dir "$profile" "$name")"
|
||||
[ -x "$ws/build.sh" ] || die "no build.sh in $ws (is it assembled? sandbox $profile $name)"
|
||||
exec "$ws/build.sh"
|
||||
}
|
||||
|
||||
# ================================================================ list/status ==
|
||||
cmd_list(){
|
||||
[ -d "$STACK_ROOT" ] || { info "no stack workspaces (root $STACK_ROOT absent)"; return 0; }
|
||||
local mf found=0
|
||||
for mf in "$STACK_ROOT"/*/.stack-manifest.json; do
|
||||
[ -f "$mf" ] || continue; found=1
|
||||
python3 -c "import json;d=json.load(open('$mf'));print(' %-22s %-8s %2d repos branch=%s'%(d['profile']+'/'+d['name'],'',len(d['repos']),d['branch']))" 2>/dev/null
|
||||
done
|
||||
[ "$found" -eq 1 ] || info "no assembled stack workspaces under $STACK_ROOT"
|
||||
}
|
||||
|
||||
cmd_status(){
|
||||
local profile="$1" name="$2"; local ws; ws="$(ws_dir "$profile" "$name")"
|
||||
local mf; mf="$(manifest "$ws")"; [ -f "$mf" ] || die "no such workspace: $ws"
|
||||
log "stack '$profile/$name'"; info "workspace: $ws"
|
||||
python3 - "$mf" <<'PY'
|
||||
import json,sys,subprocess
|
||||
d=json.load(open(sys.argv[1]))
|
||||
print(f" branch: {d['branch']}")
|
||||
for r in d['repos']:
|
||||
st=subprocess.run(["git","-C",r["worktree"],"status","--porcelain"],capture_output=True,text=True).stdout
|
||||
n=len([l for l in st.splitlines() if l.strip()])
|
||||
print(f" {r['rel']:<20} {r['head'][:9]} {'clean' if n==0 else str(n)+' changed'}")
|
||||
PY
|
||||
}
|
||||
|
||||
# ================================================================ usage/main ===
|
||||
usage(){ cat >&2 <<EOF
|
||||
${C_BLD}sandbox${C_0} — assemble a WHOLE Neuron stack into one combined worktree workspace,
|
||||
wired to build together on an isolated clean base. Multi-repo sibling of ${C_BLD}nsbx${C_0}.
|
||||
|
||||
${C_CYN}sandbox el-stack <name>${C_0} [--minimal] elc + EL language + el-ui framework + tooling (+ SDK consumers)
|
||||
${C_CYN}sandbox neuron-stack <name>${C_0} [--minimal] runtime/soul + engram + app/UI (the full product)
|
||||
${C_CYN}sandbox build <profile> <name>${C_0} build the assembled stack together (runs its build.sh)
|
||||
${C_CYN}sandbox status <profile> <name>${C_0} inspect one workspace
|
||||
${C_CYN}sandbox list${C_0} list assembled workspaces
|
||||
${C_CYN}sandbox down <profile> <name>${C_0} [--delete-branch] tear down (remove worktrees; branch kept)
|
||||
|
||||
Flags: --minimal only the required repos --branch B branch name --base REF fork point
|
||||
Env: NSBX_STACK_ROOT (workspace root, default \$DEV_ROOT/stack-worktrees) NEURON_DEV_ROOT
|
||||
|
||||
Each constituent repo becomes a git worktree at its natural relpath inside the
|
||||
workspace, so cross-repo ../foundation/el imports resolve to the SANDBOX el. The
|
||||
live soul/engram (:$LIVE_SOUL_PORT / :$LIVE_ENGRAM_PORT) are never touched; isolated-engram
|
||||
bring-up is delegated to nsbx.
|
||||
EOF
|
||||
}
|
||||
|
||||
main(){
|
||||
local cmd="${1:-}"; shift || true
|
||||
case "$cmd" in
|
||||
el-stack|neuron-stack) cmd_up "$cmd" "$@";;
|
||||
up) [ $# -ge 1 ] || die "usage: sandbox up <profile> <name>"; local p="$1"; shift; cmd_up "$p" "$@";;
|
||||
down) [ $# -ge 2 ] || die "usage: sandbox down <profile> <name>"; cmd_down "$@";;
|
||||
build) [ $# -ge 2 ] || die "usage: sandbox build <profile> <name>"; cmd_build "$@";;
|
||||
status) [ $# -ge 2 ] || die "usage: sandbox status <profile> <name>"; cmd_status "$@";;
|
||||
list|ls) cmd_list "$@";;
|
||||
""|-h|--help|help) usage;;
|
||||
*) die "unknown command: $cmd (try: sandbox help)";;
|
||||
esac
|
||||
}
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user