Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5743568bf1 | |||
| 9fd8c11670 | |||
| be0f9d1afe | |||
| 1742d0b575 |
@@ -63,6 +63,16 @@ jobs:
|
||||
cp vendor/el-runtime/v1.0.0-20260501/el_runtime.h /opt/el/runtime/el_runtime.h
|
||||
echo "El runtime PINNED to v1.0.0-20260501: $(ls /opt/el/runtime/)"
|
||||
|
||||
# neuron#133: CI compiles dist/soul.c, NOT the .el sources. On 2026-08-07 a
|
||||
# build off main would have shipped an engine with none of five merged fixes,
|
||||
# including a P0 safety fix, while main's source read as correct. The runner
|
||||
# cannot regenerate the amalgam (elc needs 24GB+ virtual memory), but it can
|
||||
# refuse to compile a stale one. Fails loudly with the recipe in the message.
|
||||
- name: Verify dist/soul.c matches the sources
|
||||
run: |
|
||||
chmod +x tools/soulc-stamp.sh
|
||||
./tools/soulc-stamp.sh --check
|
||||
|
||||
- name: Build neuron soul binary
|
||||
run: |
|
||||
RUNTIME=/opt/el/runtime
|
||||
|
||||
+101
-2
@@ -974,6 +974,10 @@ el_val_t wt_commit(el_val_t id);
|
||||
el_val_t tier_working(void);
|
||||
el_val_t tier_episodic(void);
|
||||
el_val_t tier_canonical(void);
|
||||
el_val_t mem_assoc_skip_label(el_val_t label);
|
||||
el_val_t mem_assoc_ok(el_val_t cand_id, el_val_t cand_label, el_val_t self_id);
|
||||
el_val_t mem_assoc_slot(el_val_t results, el_val_t idx, el_val_t new_id);
|
||||
el_val_t mem_associate(el_val_t new_id, el_val_t content, el_val_t label);
|
||||
el_val_t mem_store(el_val_t content, el_val_t label, el_val_t tags);
|
||||
el_val_t mem_remember(el_val_t content, el_val_t tags);
|
||||
el_val_t mem_recall(el_val_t query, el_val_t depth);
|
||||
@@ -25573,6 +25577,97 @@ el_val_t tier_canonical(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t mem_assoc_skip_label(el_val_t label) {
|
||||
if (str_contains(label, EL_STR("state-event"))) {
|
||||
return 1;
|
||||
}
|
||||
if (str_contains(label, EL_STR("soul-response"))) {
|
||||
return 1;
|
||||
}
|
||||
if (str_contains(label, EL_STR("soul-outbox"))) {
|
||||
return 1;
|
||||
}
|
||||
if (str_contains(label, EL_STR("boot_count"))) {
|
||||
return 1;
|
||||
}
|
||||
if (str_contains(label, EL_STR("loop-outcome"))) {
|
||||
return 1;
|
||||
}
|
||||
if (str_contains(label, EL_STR("search-result"))) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t mem_assoc_ok(el_val_t cand_id, el_val_t cand_label, el_val_t self_id) {
|
||||
if (str_eq(cand_id, EL_STR(""))) {
|
||||
return 0;
|
||||
}
|
||||
if (str_eq(cand_id, self_id)) {
|
||||
return 0;
|
||||
}
|
||||
el_val_t lab = str_lower(cand_label);
|
||||
if (str_starts_with(lab, EL_STR("self"))) {
|
||||
return 0;
|
||||
}
|
||||
if (str_starts_with(lab, EL_STR("value"))) {
|
||||
return 0;
|
||||
}
|
||||
if (str_contains(lab, EL_STR("values"))) {
|
||||
return 0;
|
||||
}
|
||||
if (str_contains(lab, EL_STR("identity"))) {
|
||||
return 0;
|
||||
}
|
||||
if (mem_assoc_skip_label(cand_label)) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t mem_assoc_slot(el_val_t results, el_val_t idx, el_val_t new_id) {
|
||||
if (idx >= json_array_len(results)) {
|
||||
return 0;
|
||||
}
|
||||
el_val_t cand = json_array_get(results, idx);
|
||||
el_val_t cid = json_get(cand, EL_STR("id"));
|
||||
el_val_t clabel = json_get(cand, EL_STR("label"));
|
||||
el_val_t ctype = json_get(cand, EL_STR("node_type"));
|
||||
if (str_eq(ctype, EL_STR("Value"))) {
|
||||
return 0;
|
||||
}
|
||||
if (str_eq(ctype, EL_STR("DharmaSelf"))) {
|
||||
return 0;
|
||||
}
|
||||
if (str_eq(ctype, EL_STR("Safety"))) {
|
||||
return 0;
|
||||
}
|
||||
if (mem_assoc_ok(cid, clabel, new_id)) {
|
||||
wt_edge(new_id, cid, el_from_float(0.5), EL_STR("related"));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t mem_associate(el_val_t new_id, el_val_t content, el_val_t label) {
|
||||
if (str_eq(new_id, EL_STR(""))) {
|
||||
return 0;
|
||||
}
|
||||
if (mem_assoc_skip_label(label)) {
|
||||
return 0;
|
||||
}
|
||||
el_val_t probe = str_slice(content, 0, 400);
|
||||
el_val_t results = engram_recall_json(probe, 4);
|
||||
if (str_eq(results, EL_STR(""))) {
|
||||
return 0;
|
||||
}
|
||||
mem_assoc_slot(results, 0, new_id);
|
||||
mem_assoc_slot(results, 1, new_id);
|
||||
mem_assoc_slot(results, 2, new_id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
el_val_t mem_store(el_val_t content, el_val_t label, el_val_t tags) {
|
||||
el_val_t id = wt_node(content, EL_STR("Memory"), label, el_from_float(0.5), el_from_float(0.5), el_from_float(0.8), EL_STR("Working"), tags);
|
||||
if (str_eq(id, EL_STR(""))) {
|
||||
@@ -25580,6 +25675,7 @@ el_val_t mem_store(el_val_t content, el_val_t label, el_val_t tags) {
|
||||
return EL_STR("");
|
||||
}
|
||||
el_val_t durable = wt_commit(id);
|
||||
mem_associate(id, content, label);
|
||||
if (durable) {
|
||||
println(el_str_concat(el_str_concat(el_str_concat(EL_STR("[memory] write persisted at owner: "), id), EL_STR(" label=")), label));
|
||||
} else {
|
||||
@@ -29709,7 +29805,9 @@ el_val_t handle_api_begin_session(el_val_t body) {
|
||||
el_val_t state_events = api_compact_node_array(state_events_raw, 5, 500);
|
||||
el_val_t recent_raw = engram_scan_nodes_json(10, 0);
|
||||
el_val_t recent = api_compact_node_array(recent_raw, 10, 240);
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"stats\":"), stats), EL_STR(",\"recent\":")), recent), EL_STR(",\"activated\":")), activated), EL_STR(",\"self_neighbors\":[]")), EL_STR(",\"recent_state_events\":")), state_events), EL_STR("}"));
|
||||
el_val_t self_raw = engram_neighbors_json(EL_STR("kn-efeb4a5b-5aff-4759-8a97-7233099be6ee"), 1, EL_STR("both"));
|
||||
el_val_t self_slice = api_compact_node_array(self_raw, 24, 240);
|
||||
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"stats\":"), stats), EL_STR(",\"recent\":")), recent), EL_STR(",\"activated\":")), activated), EL_STR(",\"self_neighbors\":")), self_slice), EL_STR(",\"recent_state_events\":")), state_events), EL_STR("}"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -29739,6 +29837,7 @@ el_val_t handle_api_remember(el_val_t body) {
|
||||
if (!api_persisted(id)) {
|
||||
return api_not_persisted(id);
|
||||
}
|
||||
mem_associate(id, content, EL_STR("memory:remembered"));
|
||||
return el_str_concat(el_str_concat(EL_STR("{\"id\":\""), id), EL_STR("\",\"ok\":true}"));
|
||||
return 0;
|
||||
}
|
||||
@@ -29830,7 +29929,7 @@ el_val_t handle_api_recall(el_val_t method, el_val_t path, el_val_t body) {
|
||||
if (str_eq(eff_q, EL_STR(""))) {
|
||||
return api_or_empty(engram_scan_nodes_json(limit, 0));
|
||||
}
|
||||
el_val_t results = engram_search_json(eff_q, limit);
|
||||
el_val_t results = engram_recall_json(eff_q, limit);
|
||||
return api_or_empty(results);
|
||||
return 0;
|
||||
}
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# soul.c.stamp — fingerprint of the .el sources dist/soul.c was generated from.
|
||||
# Written by tools/soulc-stamp.sh --write. Do not hand-edit.
|
||||
# generated_amalgam_sha256 e31f760de5f6629cd37bcfd26172f92be3ca5d6ff36480f6adafa1d4a1de5a66
|
||||
# generated_amalgam_bytes 1179122
|
||||
f8597e10546654bce3fbbe40461b2da59d0e06dbf1b038d1d362d24f949e3911 awareness.el
|
||||
b6f3d14ca0c26017a2d617399a6d3754dabb0905e4d5f52eb75d25c4ad18d3c5 chat.el
|
||||
42288c212cbf72fb1e8ecbd4d9900e4e9ee1cfa475b7974295c7637f1bf2939f elp-input.el
|
||||
b3f77f49d6086932c38bd17fe7a5eaf8bce25685f6fc3e1750f05729c6b49b9e imprint.el
|
||||
fba8ffdb9ba72bca5b09ca1c93a520edc52f3f4d8aec2c7585fe9b17e06420b2 manifest.el
|
||||
550a72e234ae8cec1f33e02108fd365353f45edd88513da90b792e79b6c0e5f0 memory.el
|
||||
77640783df5c38066149dcd11a11aea7e36f66e40e3df6aedf6723be8b9e0e1e neuron-api.el
|
||||
03c47c451e0e87f2c252cadb4b765867943962a804f548dd53adeef0520912c8 persist.el
|
||||
541b9309c59c33ee619d393714871b820ec4151b721d8b2aebcfdb7ce8eb2ffa routes.el
|
||||
c28e36952ec56525963a0bdf29455ab097d3b0c5653d19c25fbb005e1069a1f7 safety.el
|
||||
fd3ab91d0ae0ea26639e21bef2f8f94054dc4b02eae68b19e3fe689d2769aad4 sessions.el
|
||||
0f1cf43904a98a5a646cce5a07e0e96162ced662692fbc13357d9b67d9a8ac3d soul.el
|
||||
30337940905171a9645b0929f0a412ce6b3dccb1246495070c553bca0bbae6cd stewardship.el
|
||||
e105dc5990e6adbf39db9dc0462cd8bcf6e6c3dfd03709059227ecfad2bbab29 studio.el
|
||||
@@ -4,6 +4,86 @@ fn tier_working() -> String { return "Working" }
|
||||
fn tier_episodic() -> String { return "Episodic" }
|
||||
fn tier_canonical() -> String { return "Canonical" }
|
||||
|
||||
// ── Association on write ──────────────────────────────────────────────────────
|
||||
// DESIGN: "promotion integrates candidate nodes by linking them to existing nodes
|
||||
// using typed semantic edges RATHER THAN APPENDING AS UNLINKED CONTENT" (CCR
|
||||
// claim 29). Unlinked append is the explicitly rejected behaviour — and it is the
|
||||
// only behaviour this system had. Measured 2026-08-09 on Tim's graph: 14,214 edges
|
||||
// across 80,936 nodes, 5% of nodes connected to anything, and NO edge created by
|
||||
// any write since 2026-07-19 while 27,000+ nodes were added. A memory that forms
|
||||
// no connections cannot be reached by spreading activation, so retrieval silently
|
||||
// degrades to literal matching.
|
||||
//
|
||||
// BOUNDS, each one bought with a specific failure:
|
||||
// * max 3 edges per memory — link_memories.py's cap, precision over spray
|
||||
// * never link to identity (self/*, Value): the existing policy is explicit that
|
||||
// "memories must not pollute the self traversal by similarity; only an explicit
|
||||
// citation may touch identity". Similarity is not citation.
|
||||
// * never link telemetry (state-event, soul-response, boot_count, loop-outcome):
|
||||
// these are ~97% of daily write volume (1,020 vs 31 real memories on 08-08).
|
||||
// Linking them would add ~3,000 noise edges a day and re-flatten the graph in
|
||||
// the name of connecting it.
|
||||
// * fail-soft: a failed association never fails the write.
|
||||
// Edges go through wt_edge so they reach the owner and survive restart.
|
||||
fn mem_assoc_skip_label(label: String) -> Bool {
|
||||
if str_contains(label, "state-event") { return true }
|
||||
if str_contains(label, "soul-response") { return true }
|
||||
if str_contains(label, "soul-outbox") { return true }
|
||||
if str_contains(label, "boot_count") { return true }
|
||||
if str_contains(label, "loop-outcome") { return true }
|
||||
if str_contains(label, "search-result") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
// A candidate is linkable only if it is a real, distinct, non-identity node.
|
||||
fn mem_assoc_ok(cand_id: String, cand_label: String, self_id: String) -> Bool {
|
||||
if str_eq(cand_id, "") { return false }
|
||||
if str_eq(cand_id, self_id) { return false }
|
||||
// CASE MATTERS — measured 2026-08-09. A lowercase-only check let a memory link
|
||||
// to "Self — Values (grounded)", i.e. it polluted the self traversal, which is
|
||||
// the one thing this policy exists to prevent. My verification had the same
|
||||
// blind spot and printed PASS. Check every casing the graph actually uses, and
|
||||
// exclude identity node TYPES as well as labels.
|
||||
let lab: String = str_lower(cand_label)
|
||||
if str_starts_with(lab, "self") { return false }
|
||||
if str_starts_with(lab, "value") { return false }
|
||||
if str_contains(lab, "values") { return false }
|
||||
if str_contains(lab, "identity") { return false }
|
||||
if mem_assoc_skip_label(cand_label) { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
// One slot of the association. Manual unroll rather than a loop: EL's codegen
|
||||
// mis-emits accumulating while-loops (documented at soul.el:212, which unrolled
|
||||
// three affective slots for the same reason).
|
||||
fn mem_assoc_slot(results: String, idx: Int, new_id: String) -> Void {
|
||||
if idx >= json_array_len(results) { return }
|
||||
let cand: String = json_array_get(results, idx)
|
||||
let cid: String = json_get(cand, "id")
|
||||
let clabel: String = json_get(cand, "label")
|
||||
let ctype: String = json_get(cand, "node_type")
|
||||
if str_eq(ctype, "Value") { return }
|
||||
if str_eq(ctype, "DharmaSelf") { return }
|
||||
if str_eq(ctype, "Safety") { return }
|
||||
if mem_assoc_ok(cid, clabel, new_id) {
|
||||
wt_edge(new_id, cid, el_from_float(0.5), "related")
|
||||
}
|
||||
}
|
||||
|
||||
// mem_associate — connect a freshly written memory to what it is about.
|
||||
fn mem_associate(new_id: String, content: String, label: String) -> Void {
|
||||
if str_eq(new_id, "") { return }
|
||||
if mem_assoc_skip_label(label) { return }
|
||||
// Ask the graph what this memory resembles. Now that the store carries
|
||||
// meaning-vectors this is semantic, not merely lexical.
|
||||
let probe: String = str_slice(content, 0, 400)
|
||||
let results: String = engram_recall_json(probe, 4)
|
||||
if str_eq(results, "") { return }
|
||||
mem_assoc_slot(results, 0, new_id)
|
||||
mem_assoc_slot(results, 1, new_id)
|
||||
mem_assoc_slot(results, 2, new_id)
|
||||
}
|
||||
|
||||
fn mem_store(content: String, label: String, tags: String) -> String {
|
||||
let id: String = wt_node(
|
||||
content,
|
||||
@@ -31,6 +111,9 @@ fn mem_store(content: String, label: String, tags: String) -> String {
|
||||
// rather than claiming a save that did not happen. The id is still returned:
|
||||
// the local write DID succeed, and the queued delta will be retried.
|
||||
let durable: Bool = wt_commit(id)
|
||||
// Associate AFTER the node is durable: an edge to a node that did not persist
|
||||
// is a dangling edge, which is the defect the 2026-08-09 cleanup removed 830 of.
|
||||
mem_associate(id, content, label)
|
||||
if durable {
|
||||
println("[memory] write persisted at owner: " + id + " label=" + label)
|
||||
} else {
|
||||
|
||||
+33
-1
@@ -304,10 +304,35 @@ fn handle_api_begin_session(body: String) -> String {
|
||||
let state_events: String = api_compact_node_array(state_events_raw, 5, 500)
|
||||
let recent_raw: String = engram_scan_nodes_json(10, 0)
|
||||
let recent: String = api_compact_node_array(recent_raw, 10, 240)
|
||||
// SELF-SEEDED SLICE (2026-08-09). The design is explicit: "Every compilation
|
||||
// query begins at the self-model node and traverses outward... structural
|
||||
// reachability from the self-model node is a precondition for any node to
|
||||
// appear in compiled context" (will-anderson patents/drafts/engram-claims.md,
|
||||
// Self-Seeded Activation; DRAFT, not a filed provisional — cite it as such).
|
||||
//
|
||||
// Measured 2026-08-09 before this change: compiled context contained 0-1
|
||||
// identity records out of 10, because compilation seeds from a hardcoded
|
||||
// TEXT STRING, never from the self. Even an explicit "my values identity who
|
||||
// I am" query returned a boot counter and state-events.
|
||||
//
|
||||
// This restores the designed behaviour WITHOUT repeating the failure that got
|
||||
// self_neighbors set to [] in the first place: that was an UNBOUNDED ~90KB
|
||||
// neighbour dump which closed the socket on every call. Same bound as every
|
||||
// other list here — cap 8, 240-char snippets. The self root has 34 direct
|
||||
// neighbours of which 23 are identity records, so depth 1 is dense enough to
|
||||
// be worth seeding and small enough to stay cheap.
|
||||
let self_raw: String = engram_neighbors_json("kn-efeb4a5b-5aff-4759-8a97-7233099be6ee", 1, "both")
|
||||
// Cap 24, not 8: measured 2026-08-09, the self root's first 8 neighbours are
|
||||
// TAG nodes ("neuron", "tier:note", "disposition:experimental", "imprint",
|
||||
// "traversal") which crowd out the substantive identity records behind them.
|
||||
// The root has 34 neighbours of which 23 are identity; 24 captures them while
|
||||
// staying bounded. Cost measured at ~+4KB on a ~12KB response, nowhere near
|
||||
// the ~90KB unbounded dump that closed sockets and got this set to [].
|
||||
let self_slice: String = api_compact_node_array(self_raw, 24, 240)
|
||||
return "{\"stats\":" + stats
|
||||
+ ",\"recent\":" + recent
|
||||
+ ",\"activated\":" + activated
|
||||
+ ",\"self_neighbors\":[]"
|
||||
+ ",\"self_neighbors\":" + self_slice
|
||||
+ ",\"recent_state_events\":" + state_events + "}"
|
||||
}
|
||||
|
||||
@@ -355,6 +380,13 @@ fn handle_api_remember(body: String) -> String {
|
||||
sal, sal, el_from_float(0.9),
|
||||
"Episodic", final_tags)
|
||||
if !api_persisted(id) { return api_not_persisted(id) }
|
||||
// Associate on write (2026-08-09). THIS CALL MUST BE HERE, not only in mem_store.
|
||||
// The HTTP memory route writes via wt_node directly; mem_store serves only the
|
||||
// awareness paths (soul-response, search-result, activation-result) which are
|
||||
// exactly the telemetry we refuse to link. Hooking mem_store alone produced
|
||||
// ZERO edges across four real writes — measured, not assumed, which is the only
|
||||
// reason it was caught before shipping.
|
||||
mem_associate(id, content, "memory:remembered")
|
||||
return "{\"id\":\"" + id + "\",\"ok\":true}"
|
||||
}
|
||||
|
||||
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# build-soul-from-dist.sh — build a deployable soul from the SAME input CI compiles.
|
||||
#
|
||||
# THE PROBLEM THIS CLOSES: until now, deploys were built by build-soul.sh, which
|
||||
# compiles a scratch amalgam and never touches dist/soul.c. CI compiles dist/soul.c.
|
||||
# Two lineages. On 2026-08-09 the committed input fell 2,761 bytes behind the sources
|
||||
# while three binaries built the other way were installed on the operator machine —
|
||||
# so "what runs" and "what the repo says builds" were different artifacts again,
|
||||
# which is the whole of #133 and #111 wearing new clothes.
|
||||
#
|
||||
# This builds from dist/soul.c with CI's own flags, after asserting that dist/soul.c
|
||||
# actually matches the .el sources, and writes a provenance sidecar so a deployer can
|
||||
# refuse anything of unknown origin.
|
||||
#
|
||||
# -rdynamic and -DHAVE_CURL are copied from .gitea/workflows/ci.yaml deliberately.
|
||||
# The CI comment explains -rdynamic: without it the runtime cannot resolve its HTTP
|
||||
# handler by name via dlsym and the binary serves nothing on every route.
|
||||
#
|
||||
# usage: build-soul-from-dist.sh <out-binary>
|
||||
set -u
|
||||
OUT="${1:?usage: build-soul-from-dist.sh <out-binary>}"
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
RUNTIME="$ROOT/vendor/el-runtime/v1.0.0-20260501"
|
||||
|
||||
cd "$ROOT" || exit 2
|
||||
|
||||
echo "[build-from-dist] GATE: does dist/soul.c match the sources?"
|
||||
if ! ./tools/soulc-stamp.sh --check; then
|
||||
echo "[build-from-dist] REFUSING — the build input is stale. Regenerate and stamp first." >&2
|
||||
exit 9
|
||||
fi
|
||||
|
||||
[ -f "$RUNTIME/el_runtime.c" ] || { echo "pinned runtime missing at $RUNTIME" >&2; exit 2; }
|
||||
|
||||
echo "[build-from-dist] compiling dist/soul.c with CI's flags"
|
||||
cc -O2 -DHAVE_CURL -rdynamic \
|
||||
-I"$RUNTIME" \
|
||||
dist/soul.c \
|
||||
"$RUNTIME/el_runtime.c" \
|
||||
-lcurl -lpthread -lm \
|
||||
-o "$OUT" || { echo "[build-from-dist] COMPILE FAILED" >&2; exit 3; }
|
||||
|
||||
# Provenance sidecar: what a deployer checks before installing anything.
|
||||
SRC_SHA="$(shasum -a 256 dist/soul.c | awk '{print $1}')"
|
||||
STAMP_SHA="$(shasum -a 256 dist/soul.c.stamp | awk '{print $1}')"
|
||||
COMMIT="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||
DIRTY="clean"; [ -n "$(git status --porcelain -- '*.el' dist/soul.c 2>/dev/null)" ] && DIRTY="DIRTY"
|
||||
cat > "$OUT.provenance" <<EOF
|
||||
{"built_from":"dist/soul.c",
|
||||
"dist_soul_c_sha256":"$SRC_SHA",
|
||||
"stamp_sha256":"$STAMP_SHA",
|
||||
"git_commit":"$COMMIT",
|
||||
"worktree":"$DIRTY",
|
||||
"runtime":"vendor/el-runtime/v1.0.0-20260501",
|
||||
"flags":"-O2 -DHAVE_CURL -rdynamic"}
|
||||
EOF
|
||||
|
||||
echo "[build-from-dist] OK -> $OUT ($(wc -c < "$OUT" | tr -d ' ') bytes)"
|
||||
echo "[build-from-dist] provenance -> $OUT.provenance (commit ${COMMIT:0:8}, worktree $DIRTY)"
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
# soulc-stamp.sh — make it impossible for dist/soul.c to drift from the sources
|
||||
# in silence.
|
||||
#
|
||||
# THE PROBLEM (neuron#133, and its own words): "Nothing in the tree regenerates
|
||||
# this file. Only a human running the recipe. It lags in batches, never
|
||||
# per-change, and it will drift again."
|
||||
#
|
||||
# It drifted. On 2026-08-07 a CI or GKE build off main would have shipped an
|
||||
# engine with NONE of five merged fixes — including a P0 safety fix — while
|
||||
# main's source read as correct. CI compiles dist/soul.c, not the .el files, so
|
||||
# the source being right is not the same as the build being right.
|
||||
#
|
||||
# WHY A STAMP AND NOT AUTO-REGENERATION: the CI workflow says elc cannot run on
|
||||
# the runner ("elb on Linux would OOM the runner (elc uses 24GB+ virtual memory
|
||||
# on a 16GB host)"). So the build cannot regenerate the file itself. What it CAN
|
||||
# do, for free and with no compiler, is refuse to compile a stale one.
|
||||
#
|
||||
# The stamp records a fingerprint of every .el source that feeds the amalgam at
|
||||
# the moment it was generated. --check recomputes and compares. Divergence is a
|
||||
# build failure with the recipe in the message, not a silent ship.
|
||||
#
|
||||
# soulc-stamp.sh --write after regenerating dist/soul.c (records the fingerprint)
|
||||
# soulc-stamp.sh --check in CI, before the compile (fails on drift)
|
||||
set -u
|
||||
MODE="${1:---check}"
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
STAMP="$ROOT/dist/soul.c.stamp"
|
||||
AMALGAM="$ROOT/dist/soul.c"
|
||||
|
||||
# Every .el at the repo root is an input to the amalgam. Sorted so the hash is
|
||||
# order-independent; content-only so timestamps and checkouts do not perturb it.
|
||||
fingerprint() {
|
||||
(
|
||||
cd "$ROOT" || exit 1
|
||||
for f in $(ls -1 *.el 2>/dev/null | sort); do
|
||||
printf '%s %s\n' "$(shasum -a 256 "$f" | awk '{print $1}')" "$f"
|
||||
done
|
||||
)
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
--write)
|
||||
[ -f "$AMALGAM" ] || { echo "no dist/soul.c to stamp — regenerate it first" >&2; exit 2; }
|
||||
{
|
||||
echo "# soul.c.stamp — fingerprint of the .el sources dist/soul.c was generated from."
|
||||
echo "# Written by tools/soulc-stamp.sh --write. Do not hand-edit."
|
||||
echo "# generated_amalgam_sha256 $(shasum -a 256 "$AMALGAM" | awk '{print $1}')"
|
||||
echo "# generated_amalgam_bytes $(wc -c < "$AMALGAM" | tr -d ' ')"
|
||||
fingerprint
|
||||
} > "$STAMP"
|
||||
echo "stamped $(fingerprint | wc -l | tr -d ' ') sources -> dist/soul.c.stamp"
|
||||
;;
|
||||
|
||||
--check)
|
||||
if [ ! -f "$STAMP" ]; then
|
||||
echo "FAIL: dist/soul.c.stamp is missing — the build input is unverifiable." >&2
|
||||
echo " Regenerate the amalgam, then: tools/soulc-stamp.sh --write" >&2
|
||||
exit 1
|
||||
fi
|
||||
RECORDED="$(grep -v '^#' "$STAMP")"
|
||||
CURRENT="$(fingerprint)"
|
||||
if [ "$RECORDED" = "$CURRENT" ]; then
|
||||
echo "soulc-stamp: OK — dist/soul.c matches the .el sources"
|
||||
exit 0
|
||||
fi
|
||||
echo "FAIL: dist/soul.c is STALE. It does not match the current .el sources." >&2
|
||||
echo "" >&2
|
||||
echo "CI compiles dist/soul.c, not the .el files. Shipping this means shipping" >&2
|
||||
echo "an engine that does not contain the merged source. That is neuron#133," >&2
|
||||
echo "which once hid five merged fixes including a P0 safety fix." >&2
|
||||
echo "" >&2
|
||||
echo "Sources that changed since the amalgam was generated:" >&2
|
||||
diff <(printf '%s\n' "$RECORDED") <(printf '%s\n' "$CURRENT") \
|
||||
| grep -E '^[<>]' | awk '{print " " $1 " " $3}' | sort -u >&2
|
||||
echo "" >&2
|
||||
echo "Fix: regenerate the amalgam, then tools/soulc-stamp.sh --write" >&2
|
||||
exit 1
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "usage: soulc-stamp.sh [--check|--write]" >&2; exit 2 ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user