Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72e0b829c2 | |||
| 5f0bb67cbf | |||
| 6934a0e889 | |||
| b9e609ee39 | |||
| eb69c40f2d | |||
| 2018036bce |
+10
-4
@@ -1274,6 +1274,8 @@ el_val_t axon_raw;
|
|||||||
el_val_t axon_base;
|
el_val_t axon_base;
|
||||||
el_val_t studio_dir_raw;
|
el_val_t studio_dir_raw;
|
||||||
el_val_t studio_dir;
|
el_val_t studio_dir;
|
||||||
|
el_val_t identity_raw;
|
||||||
|
el_val_t soul_identity;
|
||||||
el_val_t using_http_engram;
|
el_val_t using_http_engram;
|
||||||
el_val_t local_node_count;
|
el_val_t local_node_count;
|
||||||
el_val_t snapshot_usable;
|
el_val_t snapshot_usable;
|
||||||
@@ -29331,7 +29333,7 @@ el_val_t handle_config(el_val_t method, el_val_t body) {
|
|||||||
|
|
||||||
el_val_t dharma_registry(void) {
|
el_val_t dharma_registry(void) {
|
||||||
el_val_t cgi_id = state_get(EL_STR("soul_cgi_id"));
|
el_val_t cgi_id = state_get(EL_STR("soul_cgi_id"));
|
||||||
el_val_t principal = state_get(EL_STR("soul_principal"));
|
el_val_t principal = cgi_principal();
|
||||||
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("{\"registry\":[{\"cgi\":\""), cgi_id), EL_STR("\",")), EL_STR("\"principal\":\"")), principal), EL_STR("\",")), EL_STR("\"covenant\":\"Principal Covenant v1\",")), EL_STR("\"registered\":\"2026-05-01\",\"provenance\":\"genesis\",")), EL_STR("\"entry\":1}],")), EL_STR("\"network_status\":\"initializing\",")), EL_STR("\"total_cgis\":1}"));
|
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("{\"registry\":[{\"cgi\":\""), cgi_id), EL_STR("\",")), EL_STR("\"principal\":\"")), principal), EL_STR("\",")), EL_STR("\"covenant\":\"Principal Covenant v1\",")), EL_STR("\"registered\":\"2026-05-01\",\"provenance\":\"genesis\",")), EL_STR("\"entry\":1}],")), EL_STR("\"network_status\":\"initializing\",")), EL_STR("\"total_cgis\":1}"));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -31936,6 +31938,7 @@ el_val_t layered_cycle(el_val_t raw_input, el_val_t session_id, el_val_t utility
|
|||||||
|
|
||||||
int main(int _argc, char** _argv) {
|
int main(int _argc, char** _argv) {
|
||||||
el_runtime_init_args(_argc, _argv);
|
el_runtime_init_args(_argc, _argv);
|
||||||
|
el_cgi_init(EL_STR("neuron-soul"), EL_STR("ntn-genesis@http://localhost:7770"), EL_STR("william-christopher-anderson"), EL_STR("dharma-mainnet"), EL_STR("http://localhost:8742"));
|
||||||
soul_cgi_id_raw = env(EL_STR("SOUL_CGI_ID"));
|
soul_cgi_id_raw = env(EL_STR("SOUL_CGI_ID"));
|
||||||
soul_cgi_id = ({ el_val_t _if_result_821 = 0; if (str_eq(soul_cgi_id_raw, EL_STR(""))) { _if_result_821 = (EL_STR("ntn-genesis")); } else { _if_result_821 = (soul_cgi_id_raw); } _if_result_821; });
|
soul_cgi_id = ({ el_val_t _if_result_821 = 0; if (str_eq(soul_cgi_id_raw, EL_STR(""))) { _if_result_821 = (EL_STR("ntn-genesis")); } else { _if_result_821 = (soul_cgi_id_raw); } _if_result_821; });
|
||||||
port_raw = env(EL_STR("NEURON_PORT"));
|
port_raw = env(EL_STR("NEURON_PORT"));
|
||||||
@@ -31948,6 +31951,9 @@ int main(int _argc, char** _argv) {
|
|||||||
axon_base = ({ el_val_t _if_result_824 = 0; if (str_eq(axon_raw, EL_STR(""))) { _if_result_824 = (EL_STR("http://localhost:7771")); } else { _if_result_824 = (axon_raw); } _if_result_824; });
|
axon_base = ({ el_val_t _if_result_824 = 0; if (str_eq(axon_raw, EL_STR(""))) { _if_result_824 = (EL_STR("http://localhost:7771")); } else { _if_result_824 = (axon_raw); } _if_result_824; });
|
||||||
studio_dir_raw = env(EL_STR("SOUL_STUDIO_DIR"));
|
studio_dir_raw = env(EL_STR("SOUL_STUDIO_DIR"));
|
||||||
studio_dir = ({ el_val_t _if_result_825 = 0; if (str_eq(studio_dir_raw, EL_STR(""))) { _if_result_825 = (el_str_concat(env(EL_STR("HOME")), EL_STR("/Development/neuron-technologies/products/cgi-studio/el-daemon"))); } else { _if_result_825 = (studio_dir_raw); } _if_result_825; });
|
studio_dir = ({ el_val_t _if_result_825 = 0; if (str_eq(studio_dir_raw, EL_STR(""))) { _if_result_825 = (el_str_concat(env(EL_STR("HOME")), EL_STR("/Development/neuron-technologies/products/cgi-studio/el-daemon"))); } else { _if_result_825 = (studio_dir_raw); } _if_result_825; });
|
||||||
|
identity_raw = env(EL_STR("SOUL_IDENTITY"));
|
||||||
|
soul_identity = ({ el_val_t _if_result_826 = 0; if (str_eq(identity_raw, EL_STR(""))) { _if_result_826 = (el_str_concat(el_str_concat(EL_STR("You are "), soul_cgi_id), EL_STR(", a CGI."))); } else { _if_result_826 = (identity_raw); } _if_result_826; });
|
||||||
|
state_set(EL_STR("soul_identity"), soul_identity);
|
||||||
println(el_str_concat(el_str_concat(el_str_concat(EL_STR("[soul] boot - cgi="), soul_cgi_id), EL_STR(" port=")), int_to_str(port)));
|
println(el_str_concat(el_str_concat(el_str_concat(EL_STR("[soul] boot - cgi="), soul_cgi_id), EL_STR(" port=")), int_to_str(port)));
|
||||||
using_http_engram = !str_eq(engram_url_raw, EL_STR(""));
|
using_http_engram = !str_eq(engram_url_raw, EL_STR(""));
|
||||||
engram_load(snapshot);
|
engram_load(snapshot);
|
||||||
@@ -31957,8 +31963,8 @@ int main(int _argc, char** _argv) {
|
|||||||
println(el_str_concat(el_str_concat(EL_STR("[soul] engram -> HTTP "), engram_url_raw), EL_STR(" (no local snapshot, first boot)")));
|
println(el_str_concat(el_str_concat(EL_STR("[soul] engram -> HTTP "), engram_url_raw), EL_STR(" (no local snapshot, first boot)")));
|
||||||
el_val_t nodes_json = http_get(el_str_concat(engram_url_raw, EL_STR("/api/nodes?limit=10000")));
|
el_val_t nodes_json = http_get(el_str_concat(engram_url_raw, EL_STR("/api/nodes?limit=10000")));
|
||||||
el_val_t edges_json = http_get(el_str_concat(engram_url_raw, EL_STR("/api/edges")));
|
el_val_t edges_json = http_get(el_str_concat(engram_url_raw, EL_STR("/api/edges")));
|
||||||
el_val_t nodes_part = ({ el_val_t _if_result_826 = 0; if (str_eq(nodes_json, EL_STR(""))) { _if_result_826 = (EL_STR("[]")); } else { _if_result_826 = (nodes_json); } _if_result_826; });
|
el_val_t nodes_part = ({ el_val_t _if_result_827 = 0; if (str_eq(nodes_json, EL_STR(""))) { _if_result_827 = (EL_STR("[]")); } else { _if_result_827 = (nodes_json); } _if_result_827; });
|
||||||
el_val_t edges_part = ({ el_val_t _if_result_827 = 0; if (str_eq(edges_json, EL_STR(""))) { _if_result_827 = (EL_STR("[]")); } else { _if_result_827 = (edges_json); } _if_result_827; });
|
el_val_t edges_part = ({ el_val_t _if_result_828 = 0; if (str_eq(edges_json, EL_STR(""))) { _if_result_828 = (EL_STR("[]")); } else { _if_result_828 = (edges_json); } _if_result_828; });
|
||||||
el_val_t snapshot_data = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"nodes\":"), nodes_part), EL_STR(",\"edges\":")), edges_part), EL_STR("}"));
|
el_val_t snapshot_data = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"nodes\":"), nodes_part), EL_STR(",\"edges\":")), edges_part), EL_STR("}"));
|
||||||
el_val_t tmp_path = el_str_concat(el_str_concat(EL_STR("/tmp/soul-engram-"), soul_cgi_id), EL_STR(".json"));
|
el_val_t tmp_path = el_str_concat(el_str_concat(EL_STR("/tmp/soul-engram-"), soul_cgi_id), EL_STR(".json"));
|
||||||
fs_write(tmp_path, snapshot_data);
|
fs_write(tmp_path, snapshot_data);
|
||||||
@@ -31982,7 +31988,7 @@ int main(int _argc, char** _argv) {
|
|||||||
state_set(EL_STR("soul_engram_api_key"), engram_api_key_raw);
|
state_set(EL_STR("soul_engram_api_key"), engram_api_key_raw);
|
||||||
state_set(EL_STR("soul.running"), EL_STR("true"));
|
state_set(EL_STR("soul.running"), EL_STR("true"));
|
||||||
is_genesis = str_eq(soul_cgi_id, EL_STR("ntn-genesis"));
|
is_genesis = str_eq(soul_cgi_id, EL_STR("ntn-genesis"));
|
||||||
guard_disk = ({ el_val_t _if_result_828 = 0; if (str_eq(engram_url_raw, EL_STR(""))) { _if_result_828 = (fs_read(snapshot)); } else { _if_result_828 = (EL_STR("")); } _if_result_828; });
|
guard_disk = ({ el_val_t _if_result_829 = 0; if (str_eq(engram_url_raw, EL_STR(""))) { _if_result_829 = (fs_read(snapshot)); } else { _if_result_829 = (EL_STR("")); } _if_result_829; });
|
||||||
guard_disk_len = str_len(guard_disk);
|
guard_disk_len = str_len(guard_disk);
|
||||||
safe_to_seed = (!using_http_engram && !((guard_disk_len > 200000) && ((engram_node_count() * 16000) < guard_disk_len)));
|
safe_to_seed = (!using_http_engram && !((guard_disk_len > 200000) && ((engram_node_count() * 16000) < guard_disk_len)));
|
||||||
if (is_genesis && !safe_to_seed) {
|
if (is_genesis && !safe_to_seed) {
|
||||||
|
|||||||
+5
-4
@@ -1,7 +1,8 @@
|
|||||||
# soul.c.stamp — fingerprint of the .el sources dist/soul.c was generated from.
|
# 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.
|
# Written by tools/soulc-stamp.sh --write. Do not hand-edit.
|
||||||
# generated_amalgam_sha256 63e30030bee5e87fa082a84cda5c1226896f49da6076101fcd6b9530ea7caf49
|
# generated_amalgam_sha256 cdc5e716dbfb797faa1b3e080cbd1ac82a75a258809da70cc5fbd02cc8040692
|
||||||
# generated_amalgam_bytes 1204442
|
# generated_amalgam_bytes 1205007
|
||||||
|
7cf5e29d2618db2fca04e6df7aa8954dd6cf9ac5e70aafb8e0b52aa734882131 __compiler__
|
||||||
f8597e10546654bce3fbbe40461b2da59d0e06dbf1b038d1d362d24f949e3911 awareness.el
|
f8597e10546654bce3fbbe40461b2da59d0e06dbf1b038d1d362d24f949e3911 awareness.el
|
||||||
b6f3d14ca0c26017a2d617399a6d3754dabb0905e4d5f52eb75d25c4ad18d3c5 chat.el
|
b6f3d14ca0c26017a2d617399a6d3754dabb0905e4d5f52eb75d25c4ad18d3c5 chat.el
|
||||||
42288c212cbf72fb1e8ecbd4d9900e4e9ee1cfa475b7974295c7637f1bf2939f elp-input.el
|
42288c212cbf72fb1e8ecbd4d9900e4e9ee1cfa475b7974295c7637f1bf2939f elp-input.el
|
||||||
@@ -13,6 +14,6 @@ fba8ffdb9ba72bca5b09ca1c93a520edc52f3f4d8aec2c7585fe9b17e06420b2 manifest.el
|
|||||||
a6d69f3fc55233d9d3300160fd46a1551f2064bcd0fb84e2c9e432f636a72476 routes.el
|
a6d69f3fc55233d9d3300160fd46a1551f2064bcd0fb84e2c9e432f636a72476 routes.el
|
||||||
c28e36952ec56525963a0bdf29455ab097d3b0c5653d19c25fbb005e1069a1f7 safety.el
|
c28e36952ec56525963a0bdf29455ab097d3b0c5653d19c25fbb005e1069a1f7 safety.el
|
||||||
fd3ab91d0ae0ea26639e21bef2f8f94054dc4b02eae68b19e3fe689d2769aad4 sessions.el
|
fd3ab91d0ae0ea26639e21bef2f8f94054dc4b02eae68b19e3fe689d2769aad4 sessions.el
|
||||||
0f1cf43904a98a5a646cce5a07e0e96162ced662692fbc13357d9b67d9a8ac3d soul.el
|
5613b60d74d5d7768f46da5ac435a5dd99d38c27f0f7013c89fa27e98dc8a21c soul.el
|
||||||
30337940905171a9645b0929f0a412ce6b3dccb1246495070c553bca0bbae6cd stewardship.el
|
30337940905171a9645b0929f0a412ce6b3dccb1246495070c553bca0bbae6cd stewardship.el
|
||||||
e105dc5990e6adbf39db9dc0462cd8bcf6e6c3dfd03709059227ecfad2bbab29 studio.el
|
95dab72be4ee1dd1d28bab63412964a72460126951764e3f74b1c2d49b6d7b35 studio.el
|
||||||
|
|||||||
@@ -1,937 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""state-key-audit.py — the analyzer behind scripts/verify-state-keys.sh.
|
|
||||||
|
|
||||||
Read that script's header for WHY this exists (issue #129). This file is the
|
|
||||||
HOW: a small El reader that resolves the key expression at every state_get /
|
|
||||||
state_set site, including keys that are computed.
|
|
||||||
|
|
||||||
WHAT IT PARSES
|
|
||||||
El as this engine writes it: `fn f(a: T, b: T) -> T { ... }`, `let x: T = e`,
|
|
||||||
`return e`, `if c { a } else { b }` as an expression, `+` concatenation,
|
|
||||||
`"..."` with backslash escapes, `//` line comments. No block comments, no
|
|
||||||
const/match/struct exist in this dialect (verified over the whole tree).
|
|
||||||
|
|
||||||
KEY PATTERNS — the only two things a key expression can resolve to
|
|
||||||
EXACT "soul_model" the whole key is known
|
|
||||||
PREFIX "session_hist_" a known head, then runtime text
|
|
||||||
(plus UNRESOLVED, which is a report line and never a failure)
|
|
||||||
|
|
||||||
RESOLUTION — resolve_expr() returns a SET of patterns; unions are how branches,
|
|
||||||
multiple returns, and multiple bindings of one name are represented.
|
|
||||||
literal "k" -> {EXACT k}
|
|
||||||
concat A + B -> fold left; all-static -> EXACT,
|
|
||||||
static head + dynamic tail -> PREFIX
|
|
||||||
if-expression if c {A} else {B} -> resolve(A) | resolve(B), except that
|
|
||||||
str_eq(X,"") with X statically ""
|
|
||||||
folds to the taken branch only
|
|
||||||
call f(args) -> union over f's return expressions,
|
|
||||||
with f's params bound to THIS call
|
|
||||||
site's actual argument expressions
|
|
||||||
local var let k = e; state_get(k)-> union over every `let k =` in the
|
|
||||||
enclosing function
|
|
||||||
parameter fn g(k) { state_get(k) }-> union over the argument at that
|
|
||||||
position across every call site of g
|
|
||||||
anything else json_get(...), env(...)-> UNRESOLVED
|
|
||||||
Recursion is depth- and cycle-guarded; a guard trip yields UNRESOLVED, never a
|
|
||||||
failure.
|
|
||||||
|
|
||||||
COVERAGE — a read is satisfied when some write can produce the same key:
|
|
||||||
read EXACT k <- write EXACT k, or write PREFIX p where k starts with p
|
|
||||||
read PREFIX p <- write EXACT k where k starts with p, or write PREFIX q
|
|
||||||
where p and q are prefixes of each other
|
|
||||||
Deliberately permissive at the boundaries: a gate that cries wolf gets deleted.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
MAX_DEPTH = 12
|
|
||||||
|
|
||||||
# ── patterns ────────────────────────────────────────────────────────────────
|
|
||||||
EXACT = "exact"
|
|
||||||
PREFIX = "prefix"
|
|
||||||
|
|
||||||
|
|
||||||
def pat_exact(s):
|
|
||||||
return (EXACT, s)
|
|
||||||
|
|
||||||
|
|
||||||
def pat_prefix(s):
|
|
||||||
# A prefix with no static text at all carries no information; that is the
|
|
||||||
# UNRESOLVED case, not a pattern.
|
|
||||||
return (PREFIX, s) if s else None
|
|
||||||
|
|
||||||
|
|
||||||
def covers(write, read):
|
|
||||||
"""Can a write of pattern `write` produce a key that `read` reads?
|
|
||||||
|
|
||||||
The prefix rule is DIRECTIONAL, and that direction is the whole point. A
|
|
||||||
write namespace that is the same or BROADER than the read namespace covers
|
|
||||||
it (write "rl:" covers read "rl:x"). A write namespace that is NARROWER does
|
|
||||||
NOT (write "session_histv2_" does not cover read "session_hist_") — being
|
|
||||||
permissive there re-opens the exact hole this gate exists to close: rename
|
|
||||||
the producer, leave the readers, stay green. Verified with a control run
|
|
||||||
that renames sessions.el's writer and leaves its four readers behind."""
|
|
||||||
wk, wv = write
|
|
||||||
rk, rv = read
|
|
||||||
if rk == EXACT:
|
|
||||||
return rv == wv if wk == EXACT else rv.startswith(wv)
|
|
||||||
# read is a PREFIX: some key starting with rv is read
|
|
||||||
if wk == EXACT:
|
|
||||||
return wv.startswith(rv) # that one written key is in range
|
|
||||||
return rv.startswith(wv) # write namespace same-or-broader
|
|
||||||
|
|
||||||
|
|
||||||
# ── lexer ───────────────────────────────────────────────────────────────────
|
|
||||||
TOK_STR, TOK_IDENT, TOK_PUNCT, TOK_NUM = "str", "ident", "punct", "num"
|
|
||||||
IDENT_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
||||||
NUM_RE = re.compile(r"[0-9]+(\.[0-9]+)?")
|
|
||||||
|
|
||||||
|
|
||||||
class Tok:
|
|
||||||
__slots__ = ("kind", "val", "line")
|
|
||||||
|
|
||||||
def __init__(self, kind, val, line):
|
|
||||||
self.kind, self.val, self.line = kind, val, line
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "%s(%r)@%d" % (self.kind, self.val, self.line)
|
|
||||||
|
|
||||||
|
|
||||||
def lex(src):
|
|
||||||
toks, i, n, line = [], 0, len(src), 1
|
|
||||||
while i < n:
|
|
||||||
c = src[i]
|
|
||||||
if c == "\n":
|
|
||||||
line += 1
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
if c in " \t\r":
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
if c == "/" and i + 1 < n and src[i + 1] == "/":
|
|
||||||
while i < n and src[i] != "\n":
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
if c == '"':
|
|
||||||
j, buf = i + 1, []
|
|
||||||
while j < n:
|
|
||||||
if src[j] == "\\" and j + 1 < n:
|
|
||||||
esc = src[j + 1]
|
|
||||||
buf.append({"n": "\n", "t": "\t", "r": "\r"}.get(esc, esc))
|
|
||||||
j += 2
|
|
||||||
continue
|
|
||||||
if src[j] == '"':
|
|
||||||
break
|
|
||||||
if src[j] == "\n":
|
|
||||||
line += 1
|
|
||||||
buf.append(src[j])
|
|
||||||
j += 1
|
|
||||||
toks.append(Tok(TOK_STR, "".join(buf), line))
|
|
||||||
i = j + 1
|
|
||||||
continue
|
|
||||||
m = IDENT_RE.match(src, i)
|
|
||||||
if m:
|
|
||||||
toks.append(Tok(TOK_IDENT, m.group(0), line))
|
|
||||||
i = m.end()
|
|
||||||
continue
|
|
||||||
m = NUM_RE.match(src, i)
|
|
||||||
if m:
|
|
||||||
toks.append(Tok(TOK_NUM, m.group(0), line))
|
|
||||||
i = m.end()
|
|
||||||
continue
|
|
||||||
toks.append(Tok(TOK_PUNCT, c, line))
|
|
||||||
i += 1
|
|
||||||
return toks
|
|
||||||
|
|
||||||
|
|
||||||
def match_close(toks, i, open_ch, close_ch):
|
|
||||||
"""toks[i] is open_ch; return index of its matching close_ch."""
|
|
||||||
depth = 0
|
|
||||||
while i < len(toks):
|
|
||||||
if toks[i].kind == TOK_PUNCT:
|
|
||||||
if toks[i].val == open_ch:
|
|
||||||
depth += 1
|
|
||||||
elif toks[i].val == close_ch:
|
|
||||||
depth -= 1
|
|
||||||
if depth == 0:
|
|
||||||
return i
|
|
||||||
i += 1
|
|
||||||
return len(toks) - 1
|
|
||||||
|
|
||||||
|
|
||||||
# ── program model ───────────────────────────────────────────────────────────
|
|
||||||
class Func:
|
|
||||||
def __init__(self, name, path, line, params, toks, start, end):
|
|
||||||
self.name, self.path, self.line = name, path, line
|
|
||||||
self.params = params # [param name]
|
|
||||||
self.toks = toks # the whole file's token list
|
|
||||||
self.start, self.end = start, end # body token range, exclusive of braces
|
|
||||||
self.lets = None # name -> [expr token ranges], lazily built
|
|
||||||
|
|
||||||
|
|
||||||
class Site:
|
|
||||||
def __init__(self, kind, path, line, func, arg_range, text):
|
|
||||||
self.kind = kind # "get" | "set"
|
|
||||||
self.path, self.line = path, line
|
|
||||||
self.func = func
|
|
||||||
self.arg_range = arg_range
|
|
||||||
self.text = text # source text of the key expression
|
|
||||||
self.pats = set()
|
|
||||||
self.unresolved = False
|
|
||||||
self.literal = None # set when the key expression is a bare literal
|
|
||||||
|
|
||||||
|
|
||||||
class Program:
|
|
||||||
def __init__(self):
|
|
||||||
self.files = {} # path -> toks
|
|
||||||
self.funcs = {} # name -> [Func] (El allows no overloads, but be safe)
|
|
||||||
self.toplevel = [] # [Func] one per file, params=[]
|
|
||||||
self.sites = [] # [Site]
|
|
||||||
self.calls = {} # callee name -> [(Func caller, [arg ranges])]
|
|
||||||
|
|
||||||
# -- loading ------------------------------------------------------------
|
|
||||||
def load(self, path, rel):
|
|
||||||
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
|
||||||
src = fh.read()
|
|
||||||
toks = lex(src)
|
|
||||||
self.files[rel] = toks
|
|
||||||
self._scan_funcs(rel, toks)
|
|
||||||
|
|
||||||
def _scan_funcs(self, rel, toks):
|
|
||||||
covered = []
|
|
||||||
i = 0
|
|
||||||
while i < len(toks):
|
|
||||||
t = toks[i]
|
|
||||||
if t.kind == TOK_IDENT and t.val == "fn" and i + 2 < len(toks) \
|
|
||||||
and toks[i + 1].kind == TOK_IDENT and toks[i + 2].val == "(":
|
|
||||||
name = toks[i + 1].val
|
|
||||||
pclose = match_close(toks, i + 2, "(", ")")
|
|
||||||
params = self._params(toks, i + 3, pclose)
|
|
||||||
bopen = pclose + 1
|
|
||||||
while bopen < len(toks) and toks[bopen].val != "{":
|
|
||||||
bopen += 1
|
|
||||||
bclose = match_close(toks, bopen, "{", "}")
|
|
||||||
f = Func(name, rel, t.line, params, toks, bopen + 1, bclose)
|
|
||||||
self.funcs.setdefault(name, []).append(f)
|
|
||||||
covered.append((i, bclose))
|
|
||||||
i = bclose + 1
|
|
||||||
continue
|
|
||||||
i += 1
|
|
||||||
# everything outside a fn is the file's top-level "function"
|
|
||||||
tl = Func("<toplevel:%s>" % rel, rel, 1, [], toks, 0, len(toks))
|
|
||||||
tl.covered = covered
|
|
||||||
self.toplevel.append(tl)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _params(toks, i, end):
|
|
||||||
"""`a: T, b: T` -> ['a','b'] (top-level commas only)."""
|
|
||||||
names, depth, expect = [], 0, True
|
|
||||||
while i < end:
|
|
||||||
t = toks[i]
|
|
||||||
if t.kind == TOK_PUNCT and t.val in "([{":
|
|
||||||
depth += 1
|
|
||||||
elif t.kind == TOK_PUNCT and t.val in ")]}":
|
|
||||||
depth -= 1
|
|
||||||
elif depth == 0 and t.kind == TOK_PUNCT and t.val == ",":
|
|
||||||
expect = True
|
|
||||||
elif depth == 0 and expect and t.kind == TOK_IDENT:
|
|
||||||
names.append(t.val)
|
|
||||||
expect = False
|
|
||||||
i += 1
|
|
||||||
return names
|
|
||||||
|
|
||||||
def func_at(self, rel, tok_index):
|
|
||||||
for f in self.funcs_in(rel):
|
|
||||||
if f.start <= tok_index < f.end:
|
|
||||||
return f
|
|
||||||
for f in self.toplevel:
|
|
||||||
if f.path == rel:
|
|
||||||
return f
|
|
||||||
return None
|
|
||||||
|
|
||||||
def funcs_in(self, rel):
|
|
||||||
for fl in self.funcs.values():
|
|
||||||
for f in fl:
|
|
||||||
if f.path == rel:
|
|
||||||
yield f
|
|
||||||
|
|
||||||
# -- indexing -----------------------------------------------------------
|
|
||||||
def index(self):
|
|
||||||
for rel, toks in self.files.items():
|
|
||||||
i = 0
|
|
||||||
while i < len(toks):
|
|
||||||
t = toks[i]
|
|
||||||
if t.kind == TOK_IDENT and i + 1 < len(toks) and toks[i + 1].val == "(" \
|
|
||||||
and t.val not in KEYWORDS \
|
|
||||||
and not (i > 0 and toks[i - 1].kind == TOK_IDENT
|
|
||||||
and toks[i - 1].val == "fn"):
|
|
||||||
# ^ the `fn f(a: T)` declaration is not a call site; counting
|
|
||||||
# it as one makes every parameter resolve to its own name
|
|
||||||
# and reports the whole function UNRESOLVED.
|
|
||||||
close = match_close(toks, i + 1, "(", ")")
|
|
||||||
args = split_args(toks, i + 2, close)
|
|
||||||
self.calls.setdefault(t.val, []).append(
|
|
||||||
(self.func_at(rel, i), args, rel, t.line))
|
|
||||||
if t.val in ("state_get", "state_set") and args:
|
|
||||||
self.sites.append(Site(
|
|
||||||
"get" if t.val == "state_get" else "set",
|
|
||||||
rel, t.line, self.func_at(rel, i), args[0],
|
|
||||||
render(toks, *args[0])))
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
# -- resolution ---------------------------------------------------------
|
|
||||||
def lets_of(self, f):
|
|
||||||
if f.lets is not None:
|
|
||||||
return f.lets
|
|
||||||
f.lets = {}
|
|
||||||
toks = f.toks
|
|
||||||
skip = getattr(f, "covered", [])
|
|
||||||
i = f.start
|
|
||||||
while i < f.end:
|
|
||||||
if any(a <= i <= b for a, b in skip):
|
|
||||||
i = max(b for a, b in skip if a <= i <= b) + 1
|
|
||||||
continue
|
|
||||||
t = toks[i]
|
|
||||||
if t.kind == TOK_IDENT and t.val == "let" and i + 1 < f.end \
|
|
||||||
and toks[i + 1].kind == TOK_IDENT:
|
|
||||||
name = toks[i + 1].val
|
|
||||||
j = i + 2
|
|
||||||
if j < f.end and toks[j].val == ":": # skip the type
|
|
||||||
while j < f.end and toks[j].val != "=":
|
|
||||||
j += 1
|
|
||||||
if j < f.end and toks[j].val == "=":
|
|
||||||
s = j + 1
|
|
||||||
e = stmt_end(toks, s, f.end)
|
|
||||||
f.lets.setdefault(name, []).append((s, e))
|
|
||||||
i = e
|
|
||||||
continue
|
|
||||||
i += 1
|
|
||||||
return f.lets
|
|
||||||
|
|
||||||
def returns_of(self, ctx, depth=0, seen=None):
|
|
||||||
"""The value expressions of a function, in the context it was CALLED in.
|
|
||||||
|
|
||||||
Context-sensitive on purpose. `conv_hist_key` is written as a guard:
|
|
||||||
|
|
||||||
if str_eq(session_id, "") { return "conv_history" }
|
|
||||||
return "session_hist_" + session_id
|
|
||||||
|
|
||||||
Collecting both returns flat would make state_set(conv_hist_key("")) — the
|
|
||||||
dead handle_chat() write — claim to produce the session_hist_ namespace
|
|
||||||
too. That is a producer this engine does not actually have, and claiming
|
|
||||||
it would let the gate stay green if sessions.el's real writer vanished:
|
|
||||||
a masking hole in the exact namespace #129 lives in. So a guard whose
|
|
||||||
condition folds is honoured, and the branch not taken is dropped."""
|
|
||||||
out = []
|
|
||||||
self._values(ctx.toks, ctx.start, ctx.end, ctx, depth,
|
|
||||||
seen if seen is not None else set(), out)
|
|
||||||
return out
|
|
||||||
|
|
||||||
def _values(self, toks, s, e, ctx, depth, seen, out):
|
|
||||||
"""Append the value expressions of a statement sequence.
|
|
||||||
Returns True when the sequence definitely returns (rest unreachable)."""
|
|
||||||
if depth > MAX_DEPTH:
|
|
||||||
return False
|
|
||||||
i = s
|
|
||||||
while i < e:
|
|
||||||
t = toks[i]
|
|
||||||
if t.kind == TOK_IDENT and t.val == "return":
|
|
||||||
j = stmt_end(toks, i + 1, e)
|
|
||||||
if j > i + 1:
|
|
||||||
out.append((i + 1, j))
|
|
||||||
return True
|
|
||||||
if t.kind == TOK_IDENT and t.val == "let":
|
|
||||||
i = stmt_end(toks, i + 2, e)
|
|
||||||
continue
|
|
||||||
if t.kind == TOK_IDENT and t.val == "if":
|
|
||||||
i = self._if_stmt(toks, i, e, ctx, depth, seen, out)
|
|
||||||
if i is True:
|
|
||||||
return True
|
|
||||||
continue
|
|
||||||
if t.kind == TOK_PUNCT and t.val in "([{":
|
|
||||||
i = match_close(toks, i, t.val,
|
|
||||||
{"(": ")", "[": "]", "{": "}"}[t.val]) + 1
|
|
||||||
continue
|
|
||||||
en = stmt_end(toks, i, e)
|
|
||||||
if en <= i:
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
if en >= e: # trailing expression = the value
|
|
||||||
out.append((i, en))
|
|
||||||
i = en
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _if_stmt(self, toks, i, e, ctx, depth, seen, out):
|
|
||||||
"""Walk one if / else-if / else chain. Returns the next index, or True
|
|
||||||
if the chain definitely returns on every reachable branch."""
|
|
||||||
bopen = i + 1
|
|
||||||
while bopen < e and toks[bopen].val != "{":
|
|
||||||
bopen += 1
|
|
||||||
if bopen >= e:
|
|
||||||
return e
|
|
||||||
bclose = match_close(toks, bopen, "{", "}")
|
|
||||||
fold = self._fold_cond(toks, i + 1, bopen, ctx, depth, seen)
|
|
||||||
|
|
||||||
j = bclose + 1
|
|
||||||
else_s = else_e = None
|
|
||||||
if j < e and toks[j].kind == TOK_IDENT and toks[j].val == "else":
|
|
||||||
if j + 1 < e and toks[j + 1].val == "{":
|
|
||||||
ec = match_close(toks, j + 1, "{", "}")
|
|
||||||
else_s, else_e = j + 2, ec
|
|
||||||
j = ec + 1
|
|
||||||
else: # `else if ...` — the rest of the chain
|
|
||||||
else_s = j + 1
|
|
||||||
else_e = stmt_end(toks, j + 1, e)
|
|
||||||
j = else_e
|
|
||||||
|
|
||||||
then_ret = else_ret = False
|
|
||||||
if fold is not False:
|
|
||||||
then_ret = self._values(toks, bopen + 1, bclose, ctx, depth + 1, seen, out)
|
|
||||||
if fold is not True and else_s is not None:
|
|
||||||
else_ret = self._values(toks, else_s, else_e, ctx, depth + 1, seen, out)
|
|
||||||
|
|
||||||
if fold is True and then_ret:
|
|
||||||
return True
|
|
||||||
if fold is False and else_s is not None and else_ret:
|
|
||||||
return True
|
|
||||||
if fold is None and else_s is not None and then_ret and else_ret:
|
|
||||||
return True
|
|
||||||
return j
|
|
||||||
|
|
||||||
def resolve(self, rng, func, depth=0, seen=None):
|
|
||||||
"""-> (set of patterns, unresolved_flag)"""
|
|
||||||
if seen is None:
|
|
||||||
seen = set()
|
|
||||||
if depth > MAX_DEPTH:
|
|
||||||
return set(), True
|
|
||||||
return self._expr(func.toks, rng[0], rng[1], func, depth, seen)
|
|
||||||
|
|
||||||
# -- expression walker --------------------------------------------------
|
|
||||||
def _expr(self, toks, s, e, func, depth, seen):
|
|
||||||
parts, cur, d = [], s, 0
|
|
||||||
i = s
|
|
||||||
while i < e: # split on top-level '+'
|
|
||||||
v = toks[i].val
|
|
||||||
if toks[i].kind == TOK_PUNCT and v in "([{":
|
|
||||||
d += 1
|
|
||||||
elif toks[i].kind == TOK_PUNCT and v in ")]}":
|
|
||||||
d -= 1
|
|
||||||
elif d == 0 and toks[i].kind == TOK_PUNCT and v == "+" and i > s:
|
|
||||||
parts.append((cur, i))
|
|
||||||
cur = i + 1
|
|
||||||
i += 1
|
|
||||||
parts.append((cur, e))
|
|
||||||
if len(parts) == 1:
|
|
||||||
return self._primary(toks, s, e, func, depth, seen)
|
|
||||||
|
|
||||||
# concatenation: keep folding while every operand so far is EXACT
|
|
||||||
head, unres = "", False
|
|
||||||
static = True
|
|
||||||
for (ps, pe) in parts:
|
|
||||||
pats, u = self._primary(toks, ps, pe, func, depth, seen)
|
|
||||||
exacts = {p[1] for p in pats if p[0] == EXACT}
|
|
||||||
if static and len(exacts) == 1 and not u and len(pats) == 1:
|
|
||||||
head += exacts.pop()
|
|
||||||
continue
|
|
||||||
if static and pats and all(p[0] == EXACT for p in pats) and len(pats) > 1:
|
|
||||||
# a branchy static operand: keep the shared head only
|
|
||||||
static = False
|
|
||||||
head += os.path.commonprefix(sorted({p[1] for p in pats}))
|
|
||||||
break
|
|
||||||
static = False
|
|
||||||
# first non-static operand: everything after it is runtime text
|
|
||||||
if (ps, pe) == parts[0]:
|
|
||||||
for p in pats:
|
|
||||||
if p[0] == PREFIX:
|
|
||||||
head = p[1]
|
|
||||||
break
|
|
||||||
if not head:
|
|
||||||
unres = True
|
|
||||||
break
|
|
||||||
if static:
|
|
||||||
return {pat_exact(head)}, False
|
|
||||||
p = pat_prefix(head)
|
|
||||||
return ({p} if p else set()), (unres or not p)
|
|
||||||
|
|
||||||
def _primary(self, toks, s, e, func, depth, seen):
|
|
||||||
while s < e and toks[s].kind == TOK_PUNCT and toks[s].val == "(" \
|
|
||||||
and match_close(toks, s, "(", ")") == e - 1:
|
|
||||||
s, e = s + 1, e - 1
|
|
||||||
if s >= e:
|
|
||||||
return set(), True
|
|
||||||
t = toks[s]
|
|
||||||
|
|
||||||
if t.kind == TOK_STR and e == s + 1:
|
|
||||||
return {pat_exact(t.val)}, False
|
|
||||||
|
|
||||||
if t.kind == TOK_IDENT and t.val == "if":
|
|
||||||
return self._if_expr(toks, s, e, func, depth, seen)
|
|
||||||
|
|
||||||
if t.kind == TOK_IDENT and s + 1 < e and toks[s + 1].val == "(":
|
|
||||||
close = match_close(toks, s + 1, "(", ")")
|
|
||||||
if close == e - 1:
|
|
||||||
return self._call(toks, t.val, split_args(toks, s + 2, close),
|
|
||||||
func, depth, seen)
|
|
||||||
|
|
||||||
if t.kind == TOK_IDENT and e == s + 1:
|
|
||||||
return self._var(t.val, func, depth, seen)
|
|
||||||
|
|
||||||
return set(), True
|
|
||||||
|
|
||||||
def _if_expr(self, toks, s, e, func, depth, seen):
|
|
||||||
bopen = s + 1
|
|
||||||
while bopen < e and toks[bopen].val != "{":
|
|
||||||
bopen += 1
|
|
||||||
cond = (s + 1, bopen)
|
|
||||||
bclose = match_close(toks, bopen, "{", "}")
|
|
||||||
then_rng = block_tail(toks, bopen + 1, bclose) or (bopen + 1, bclose)
|
|
||||||
|
|
||||||
else_rng = None
|
|
||||||
j = bclose + 1
|
|
||||||
if j < e and toks[j].kind == TOK_IDENT and toks[j].val == "else":
|
|
||||||
if j + 1 < e and toks[j + 1].val == "{":
|
|
||||||
ec = match_close(toks, j + 1, "{", "}")
|
|
||||||
else_rng = block_tail(toks, j + 2, ec) or (j + 2, ec)
|
|
||||||
else:
|
|
||||||
else_rng = (j + 1, e) # `else if ...`
|
|
||||||
|
|
||||||
taken = self._fold_cond(toks, cond[0], cond[1], func, depth, seen)
|
|
||||||
rngs = []
|
|
||||||
if taken is not False:
|
|
||||||
rngs.append(then_rng)
|
|
||||||
if taken is not True and else_rng:
|
|
||||||
rngs.append(else_rng)
|
|
||||||
|
|
||||||
pats, unres = set(), False
|
|
||||||
for r in rngs:
|
|
||||||
p, u = self._expr(toks, r[0], r[1], func, depth + 1, seen)
|
|
||||||
pats |= p
|
|
||||||
unres = unres or u
|
|
||||||
return pats, unres
|
|
||||||
|
|
||||||
def _fold_cond(self, toks, s, e, func, depth, seen):
|
|
||||||
"""Constant-fold `str_eq(X, "")` / `!str_eq(X, "")` so a helper called with
|
|
||||||
a literal (conv_hist_key("")) yields only the branch it really takes.
|
|
||||||
Returns True / False / None(unknown)."""
|
|
||||||
neg = False
|
|
||||||
if s < e and toks[s].kind == TOK_PUNCT and toks[s].val == "!":
|
|
||||||
neg, s = True, s + 1
|
|
||||||
if not (s < e and toks[s].kind == TOK_IDENT and toks[s].val == "str_eq"
|
|
||||||
and s + 1 < e and toks[s + 1].val == "("):
|
|
||||||
return None
|
|
||||||
close = match_close(toks, s + 1, "(", ")")
|
|
||||||
if close != e - 1:
|
|
||||||
return None
|
|
||||||
args = split_args(toks, s + 2, close)
|
|
||||||
if len(args) != 2:
|
|
||||||
return None
|
|
||||||
va, ua = self._expr(toks, args[0][0], args[0][1], func, depth + 1, seen)
|
|
||||||
vb, ub = self._expr(toks, args[1][0], args[1][1], func, depth + 1, seen)
|
|
||||||
if ua or ub or len(va) != 1 or len(vb) != 1:
|
|
||||||
return None
|
|
||||||
(ka, sa), (kb, sb) = va.pop(), vb.pop()
|
|
||||||
if ka != EXACT or kb != EXACT:
|
|
||||||
return None
|
|
||||||
r = (sa == sb)
|
|
||||||
return (not r) if neg else r
|
|
||||||
|
|
||||||
def _call(self, toks, name, args, func, depth, seen):
|
|
||||||
cands = self.funcs.get(name)
|
|
||||||
if not cands:
|
|
||||||
return set(), True # builtin: json_get, env, ...
|
|
||||||
pats, unres = set(), False
|
|
||||||
for callee in cands:
|
|
||||||
key = ("fn", callee.path, callee.name, tuple(args))
|
|
||||||
if key in seen:
|
|
||||||
unres = True
|
|
||||||
continue
|
|
||||||
seen = seen | {key}
|
|
||||||
# bind the callee's params to THIS call site's argument expressions
|
|
||||||
binding = {}
|
|
||||||
for idx, pname in enumerate(callee.params):
|
|
||||||
if idx < len(args):
|
|
||||||
binding[pname] = (args[idx], func)
|
|
||||||
callee_ctx = _Bound(callee, binding)
|
|
||||||
for r in self.returns_of(callee_ctx, depth + 1, seen):
|
|
||||||
p, u = self._expr(callee.toks, r[0], r[1], callee_ctx,
|
|
||||||
depth + 1, seen)
|
|
||||||
pats |= p
|
|
||||||
unres = unres or u
|
|
||||||
return pats, unres
|
|
||||||
|
|
||||||
def _var(self, name, func, depth, seen):
|
|
||||||
real = func.func if isinstance(func, _Bound) else func
|
|
||||||
|
|
||||||
# 1. a parameter bound by the call site we came through
|
|
||||||
if isinstance(func, _Bound) and name in func.binding:
|
|
||||||
rng, caller_ctx = func.binding[name]
|
|
||||||
return self._expr(caller_ctx.toks, rng[0], rng[1], caller_ctx,
|
|
||||||
depth + 1, seen)
|
|
||||||
|
|
||||||
# 2. a local `let` in the enclosing function
|
|
||||||
lets = self.lets_of(real)
|
|
||||||
if name in lets:
|
|
||||||
key = ("let", real.path, real.name, name)
|
|
||||||
if key in seen:
|
|
||||||
return set(), True
|
|
||||||
seen = seen | {key}
|
|
||||||
pats, unres = set(), False
|
|
||||||
for rng in lets[name]:
|
|
||||||
p, u = self._expr(real.toks, rng[0], rng[1], real, depth + 1, seen)
|
|
||||||
pats |= p
|
|
||||||
unres = unres or u
|
|
||||||
return pats, unres
|
|
||||||
|
|
||||||
# 3. an unbound parameter -> look at every call site of the enclosing fn
|
|
||||||
if name in real.params:
|
|
||||||
key = ("param", real.path, real.name, name)
|
|
||||||
if key in seen:
|
|
||||||
return set(), True
|
|
||||||
seen = seen | {key}
|
|
||||||
idx = real.params.index(name)
|
|
||||||
pats, unres = set(), False
|
|
||||||
sites = self.calls.get(real.name, [])
|
|
||||||
if not sites:
|
|
||||||
return set(), True
|
|
||||||
for caller, args, _rel, _line in sites:
|
|
||||||
if caller is None or idx >= len(args):
|
|
||||||
unres = True
|
|
||||||
continue
|
|
||||||
p, u = self._expr(caller.toks, args[idx][0], args[idx][1],
|
|
||||||
caller, depth + 1, seen)
|
|
||||||
pats |= p
|
|
||||||
unres = unres or u
|
|
||||||
return pats, unres
|
|
||||||
|
|
||||||
# 4. a file-level / cross-file top-level `let`
|
|
||||||
for tl in self.toplevel:
|
|
||||||
lets = self.lets_of(tl)
|
|
||||||
if name in lets:
|
|
||||||
key = ("let", tl.path, tl.name, name)
|
|
||||||
if key in seen:
|
|
||||||
return set(), True
|
|
||||||
seen2 = seen | {key}
|
|
||||||
pats, unres = set(), False
|
|
||||||
for rng in lets[name]:
|
|
||||||
p, u = self._expr(tl.toks, rng[0], rng[1], tl, depth + 1, seen2)
|
|
||||||
pats |= p
|
|
||||||
unres = unres or u
|
|
||||||
return pats, unres
|
|
||||||
|
|
||||||
return set(), True
|
|
||||||
|
|
||||||
|
|
||||||
class _Bound:
|
|
||||||
"""A callee view that also knows what its params were called with."""
|
|
||||||
|
|
||||||
def __init__(self, func, binding):
|
|
||||||
self.func, self.binding = func, binding
|
|
||||||
self.toks, self.start, self.end = func.toks, func.start, func.end
|
|
||||||
self.params, self.path, self.name = func.params, func.path, func.name
|
|
||||||
|
|
||||||
def __getattr__(self, k):
|
|
||||||
return getattr(self.func, k)
|
|
||||||
|
|
||||||
|
|
||||||
# ── token helpers ───────────────────────────────────────────────────────────
|
|
||||||
def split_args(toks, s, e):
|
|
||||||
out, cur, d = [], s, 0
|
|
||||||
i = s
|
|
||||||
while i < e:
|
|
||||||
v = toks[i].val
|
|
||||||
if toks[i].kind == TOK_PUNCT and v in "([{":
|
|
||||||
d += 1
|
|
||||||
elif toks[i].kind == TOK_PUNCT and v in ")]}":
|
|
||||||
d -= 1
|
|
||||||
elif d == 0 and toks[i].kind == TOK_PUNCT and v == ",":
|
|
||||||
out.append((cur, i))
|
|
||||||
cur = i + 1
|
|
||||||
i += 1
|
|
||||||
if cur < e:
|
|
||||||
out.append((cur, e))
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
STMT_START = {"let", "return", "if", "while", "for"}
|
|
||||||
KEYWORDS = {"if", "while", "for", "return", "fn", "let", "else", "match"}
|
|
||||||
|
|
||||||
|
|
||||||
def stmt_end(toks, s, limit):
|
|
||||||
"""End of the expression starting at s: the next top-level statement
|
|
||||||
boundary. El has no semicolons, so a newline that starts a new statement
|
|
||||||
ends this one."""
|
|
||||||
d, i = 0, s
|
|
||||||
while i < limit:
|
|
||||||
t = toks[i]
|
|
||||||
if t.kind == TOK_PUNCT and t.val in "([":
|
|
||||||
d += 1
|
|
||||||
elif t.kind == TOK_PUNCT and t.val in ")]":
|
|
||||||
d -= 1
|
|
||||||
if d < 0:
|
|
||||||
return i
|
|
||||||
elif t.kind == TOK_PUNCT and t.val == "{":
|
|
||||||
# a brace at depth 0 belongs to this expression only when it is an
|
|
||||||
# if/else block that is part of it
|
|
||||||
d += 1
|
|
||||||
elif t.kind == TOK_PUNCT and t.val == "}":
|
|
||||||
d -= 1
|
|
||||||
if d < 0:
|
|
||||||
return i
|
|
||||||
elif d == 0 and t.kind == TOK_PUNCT and t.val == ",":
|
|
||||||
return i
|
|
||||||
elif d == 0 and i > s and t.kind == TOK_IDENT and t.val in STMT_START:
|
|
||||||
if t.val == "if" and toks[i - 1].kind == TOK_IDENT and toks[i - 1].val == "else":
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
return i
|
|
||||||
elif d == 0 and i > s and t.kind == TOK_IDENT and t.val == "fn":
|
|
||||||
return i
|
|
||||||
i += 1
|
|
||||||
return limit
|
|
||||||
|
|
||||||
|
|
||||||
def block_tail(toks, s, e):
|
|
||||||
"""The trailing expression of a block, if the block ends in one."""
|
|
||||||
i, last = s, None
|
|
||||||
while i < e:
|
|
||||||
t = toks[i]
|
|
||||||
if t.kind == TOK_IDENT and t.val in ("let", "return"):
|
|
||||||
i = stmt_end(toks, i + 1, e)
|
|
||||||
last = None
|
|
||||||
continue
|
|
||||||
if t.kind == TOK_PUNCT and t.val in "([{":
|
|
||||||
i = match_close(toks, i, t.val, {"(": ")", "[": "]", "{": "}"}[t.val]) + 1
|
|
||||||
continue
|
|
||||||
st = i
|
|
||||||
en = stmt_end(toks, i, e)
|
|
||||||
if en <= st:
|
|
||||||
i = st + 1
|
|
||||||
continue
|
|
||||||
last = (st, en)
|
|
||||||
i = en
|
|
||||||
return last
|
|
||||||
|
|
||||||
|
|
||||||
def render(toks, s, e):
|
|
||||||
out = []
|
|
||||||
for t in toks[s:e]:
|
|
||||||
out.append('"%s"' % t.val if t.kind == TOK_STR else t.val)
|
|
||||||
return " ".join(out)
|
|
||||||
|
|
||||||
|
|
||||||
# ── the gate ────────────────────────────────────────────────────────────────
|
|
||||||
def collect(root, include_tests):
|
|
||||||
files = []
|
|
||||||
for dirpath, dirnames, filenames in os.walk(root):
|
|
||||||
dirnames[:] = [d for d in dirnames
|
|
||||||
if d not in ("dist", "vendor", ".git", "node_modules")]
|
|
||||||
rel_dir = os.path.relpath(dirpath, root)
|
|
||||||
if not include_tests and rel_dir.split(os.sep)[0] == "tests":
|
|
||||||
continue
|
|
||||||
for fn in sorted(filenames):
|
|
||||||
if fn.endswith(".el"):
|
|
||||||
rel = os.path.normpath(os.path.join(rel_dir, fn))
|
|
||||||
files.append((os.path.join(dirpath, fn), rel))
|
|
||||||
return sorted(files, key=lambda x: x[1])
|
|
||||||
|
|
||||||
|
|
||||||
def is_bare_literal(prog, site):
|
|
||||||
toks = prog.files[site.path]
|
|
||||||
s, e = site.arg_range
|
|
||||||
return e == s + 1 and toks[s].kind == TOK_STR
|
|
||||||
|
|
||||||
|
|
||||||
def read_decl(path):
|
|
||||||
"""A declaration file: one entry per line, `# ...` comments stripped."""
|
|
||||||
out = []
|
|
||||||
if not path or not os.path.exists(path):
|
|
||||||
return out
|
|
||||||
with open(path) as fh:
|
|
||||||
for ln in fh:
|
|
||||||
ln = ln.split("#", 1)[0].strip()
|
|
||||||
if ln:
|
|
||||||
out.append(ln)
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def opt(argv, name, default=None):
|
|
||||||
for i, a in enumerate(argv):
|
|
||||||
if a == name and i + 1 < len(argv):
|
|
||||||
return argv[i + 1]
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv):
|
|
||||||
root = os.path.abspath(argv[1]) if len(argv) > 1 and not argv[1].startswith("-") else "."
|
|
||||||
include_tests = "--include-tests" in argv
|
|
||||||
verbose = "--verbose" in argv
|
|
||||||
baseline_path = opt(argv, "--baseline")
|
|
||||||
external_path = opt(argv, "--external")
|
|
||||||
|
|
||||||
prog = Program()
|
|
||||||
for path, rel in collect(root, include_tests):
|
|
||||||
prog.load(path, rel)
|
|
||||||
prog.index()
|
|
||||||
for site in prog.sites:
|
|
||||||
pats, unres = prog.resolve(site.arg_range, site.func)
|
|
||||||
site.pats, site.unresolved = {p for p in pats if p}, unres
|
|
||||||
if is_bare_literal(prog, site):
|
|
||||||
site.literal = prog.files[site.path][site.arg_range[0]].val
|
|
||||||
|
|
||||||
writes = [s for s in prog.sites if s.kind == "set"]
|
|
||||||
reads = [s for s in prog.sites if s.kind == "get"]
|
|
||||||
write_pats = set()
|
|
||||||
for w in writes:
|
|
||||||
write_pats |= w.pats
|
|
||||||
|
|
||||||
# Declared host-set keys: written by something outside the El tree (an
|
|
||||||
# operator, the installer, a host process). Each entry must carry a reason.
|
|
||||||
external = []
|
|
||||||
for ln in read_decl(external_path):
|
|
||||||
parts = ln.split(None, 1)
|
|
||||||
if len(parts) != 2 or parts[0] not in (EXACT, PREFIX):
|
|
||||||
print("bad --external line (want `exact|prefix <key>`): %r" % ln,
|
|
||||||
file=sys.stderr)
|
|
||||||
return 2
|
|
||||||
external.append((parts[0], parts[1]))
|
|
||||||
write_pats |= set(external)
|
|
||||||
|
|
||||||
# F1 — a read of a key no write in the tree produces.
|
|
||||||
f1 = []
|
|
||||||
for r in reads:
|
|
||||||
for p in sorted(r.pats):
|
|
||||||
if not any(covers(w, p) for w in write_pats):
|
|
||||||
f1.append((r, p))
|
|
||||||
|
|
||||||
# F2 — a key namespace owned by a helper, accessed by a hand-rolled literal.
|
|
||||||
# This is the #129 shape: the producer moved behind conv_hist_key() and
|
|
||||||
# one consumer kept spelling the old key out by hand.
|
|
||||||
owners = {} # helper fn name -> its value set
|
|
||||||
for s in prog.sites:
|
|
||||||
toks = prog.files[s.path]
|
|
||||||
a, b = s.arg_range
|
|
||||||
if toks[a].kind == TOK_IDENT and a + 1 < b and toks[a + 1].val == "(" \
|
|
||||||
and match_close(toks, a + 1, "(", ")") == b - 1 \
|
|
||||||
and toks[a].val in prog.funcs:
|
|
||||||
name = toks[a].val
|
|
||||||
if name not in owners:
|
|
||||||
vals = set()
|
|
||||||
for callee in prog.funcs[name]:
|
|
||||||
# No call context here on purpose: the OWNED namespace is
|
|
||||||
# every key the helper can ever produce, over all call sites.
|
|
||||||
for rng in prog.returns_of(callee):
|
|
||||||
p, _ = prog._expr(callee.toks, rng[0], rng[1], callee, 0, set())
|
|
||||||
vals |= {x for x in p if x}
|
|
||||||
owners[name] = vals
|
|
||||||
f2 = []
|
|
||||||
for s in prog.sites:
|
|
||||||
if s.literal is None:
|
|
||||||
continue
|
|
||||||
for owner, vals in sorted(owners.items()):
|
|
||||||
for v in sorted(vals):
|
|
||||||
if covers(v, pat_exact(s.literal)):
|
|
||||||
f2.append((s, owner, v))
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
|
|
||||||
unresolved = [s for s in prog.sites if s.unresolved or not s.pats]
|
|
||||||
|
|
||||||
# Baseline signatures carry NO line number on purpose: an unrelated edit that
|
|
||||||
# shifts a line must not un-mute an accepted finding (that is crying wolf),
|
|
||||||
# but a GROWTH in count must not hide either. So a baseline entry is
|
|
||||||
# `<file> <CODE> <detail> [xN]` and only the first N matches are muted.
|
|
||||||
baseline, bad_baseline = {}, []
|
|
||||||
for ln in read_decl(baseline_path):
|
|
||||||
n, key = 1, ln
|
|
||||||
parts = ln.rsplit(" x", 1)
|
|
||||||
if len(parts) == 2 and parts[1].isdigit():
|
|
||||||
key, n = parts[0].strip(), int(parts[1])
|
|
||||||
baseline[key] = n
|
|
||||||
|
|
||||||
def sig(path, code, detail):
|
|
||||||
return "%s %s %s" % (path, code, detail)
|
|
||||||
|
|
||||||
findings = []
|
|
||||||
for r, p in f1:
|
|
||||||
findings.append((sig(r.path, "DEAD-READ", "%s:%s" % p), r.line,
|
|
||||||
" %s:%d state_get(%s)\n resolves to %s %r — no state_set in the tree produces it"
|
|
||||||
% (r.path, r.line, r.text, p[0].upper(), p[1])))
|
|
||||||
for s, owner, v in f2:
|
|
||||||
findings.append((sig(s.path, "HAND-ROLLED", "%s<-%s()" % (s.literal, owner)), s.line,
|
|
||||||
" %s:%d state_%s(\"%s\")\n %s() owns this key namespace (%s %r) — go through the helper, "
|
|
||||||
"or a rename orphans this site silently" % (s.path, s.line, s.kind, s.literal, owner, v[0].upper(), v[1])))
|
|
||||||
findings.sort(key=lambda f: (f[0], f[1]))
|
|
||||||
|
|
||||||
live, muted, budget = [], [], dict(baseline)
|
|
||||||
for f in findings:
|
|
||||||
if budget.get(f[0], 0) > 0:
|
|
||||||
budget[f[0]] -= 1
|
|
||||||
muted.append(f)
|
|
||||||
else:
|
|
||||||
live.append(f)
|
|
||||||
stale = sorted(k for k, v in budget.items() if v > 0)
|
|
||||||
|
|
||||||
print("── state-key audit ─────────────────────────────────────────────")
|
|
||||||
print("scanned %d .el files%s" % (len(prog.files),
|
|
||||||
"" if include_tests else " (tests/ excluded)"))
|
|
||||||
print("sites %d state_set, %d state_get" % (len(writes), len(reads)))
|
|
||||||
print("keys %d distinct write patterns" % len(write_pats))
|
|
||||||
print("")
|
|
||||||
|
|
||||||
if verbose:
|
|
||||||
print("WRITE PATTERNS")
|
|
||||||
for k, v in sorted(write_pats):
|
|
||||||
print(" %-6s %s" % (k, v))
|
|
||||||
print("")
|
|
||||||
|
|
||||||
if external:
|
|
||||||
print("DECLARED HOST-SET (%d) — %s" % (len(external), external_path))
|
|
||||||
for k, v in sorted(external):
|
|
||||||
print(" %-6s %s" % (k, v))
|
|
||||||
print("")
|
|
||||||
|
|
||||||
print("UNRESOLVED (%d) — reported, never fails the build" % len(unresolved))
|
|
||||||
if not unresolved:
|
|
||||||
print(" (none)")
|
|
||||||
for s in sorted(unresolved, key=lambda x: (x.path, x.line)):
|
|
||||||
print(" %s:%d state_%s(%s)%s"
|
|
||||||
% (s.path, s.line, s.kind, s.text,
|
|
||||||
" [partial: %s]" % ", ".join("%s %r" % p for p in sorted(s.pats))
|
|
||||||
if s.pats else ""))
|
|
||||||
print("")
|
|
||||||
|
|
||||||
if muted:
|
|
||||||
print("BASELINED (%d) — pre-existing debt accepted in %s. NOT clean; fix these."
|
|
||||||
% (len(muted), baseline_path))
|
|
||||||
for sg, line, _ in muted:
|
|
||||||
print(" %s (line %d)" % (sg, line))
|
|
||||||
print("")
|
|
||||||
if stale:
|
|
||||||
print("STALE BASELINE (%d) — entries that no longer match anything; delete them:"
|
|
||||||
% len(stale))
|
|
||||||
for sg in stale:
|
|
||||||
print(" %s" % sg)
|
|
||||||
print("")
|
|
||||||
|
|
||||||
print("FINDINGS (%d)" % len(live))
|
|
||||||
if not live:
|
|
||||||
print(" (none)")
|
|
||||||
for _, _, body in live:
|
|
||||||
print(body)
|
|
||||||
print("")
|
|
||||||
|
|
||||||
if live:
|
|
||||||
print("FAIL: %d state-key finding(s). See scripts/verify-state-keys.sh "
|
|
||||||
"for why this gate exists (issue #129)." % len(live))
|
|
||||||
return 1
|
|
||||||
print("PASS: every resolvable state_get key has a producer, and no key "
|
|
||||||
"namespace is spelled two ways.")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main(sys.argv))
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# state-key-baseline.txt — findings that already existed when this gate landed
|
|
||||||
# (2026-08-07). Each one is a REAL defect of the #129 class, not a false
|
|
||||||
# positive. They are muted only so the gate can be turned on today instead of
|
|
||||||
# being deferred until the debt is paid; every run still prints them under
|
|
||||||
# BASELINED with the word "debt".
|
|
||||||
#
|
|
||||||
# THIS FILE SHOULD ONLY EVER SHRINK. Adding a line means you are shipping a
|
|
||||||
# known silent-"" read. If you must, date it and say why in the comment.
|
|
||||||
#
|
|
||||||
# format: <file> <CODE> <detail> [xN] # N = how many sites are accepted
|
|
||||||
# No line numbers on purpose: an unrelated edit must not un-mute an accepted
|
|
||||||
# finding, but a GROWTH in count is NOT muted — the extra site fails the build.
|
|
||||||
#
|
|
||||||
chat.el DEAD-READ exact:soul_identity x5
|
|
||||||
# ^ soul.el used to run `state_set("soul_identity", soul_identity)`. It was
|
|
||||||
# deleted on 2026-05-13 in b163fa6 ("feat(awareness): route ISE writes to HTTP
|
|
||||||
# Engram ..."), a commit about something else entirely, and the five readers in
|
|
||||||
# chat.el were left behind. Since that date build_system_prompt (737), the
|
|
||||||
# vision handler (1745), the agentic system prompt (2620), the council
|
|
||||||
# transcript handler (3425) and 3480 have all been prefixing "" — exactly the
|
|
||||||
# #129 shape, found by this gate on its first run. Sites: 737, 1745, 2620,
|
|
||||||
# 3425, 3480. Fix = restore the boot-time write or delete the reads; not done
|
|
||||||
# here because this branch must not change engine behaviour.
|
|
||||||
|
|
||||||
studio.el DEAD-READ exact:soul_principal x1
|
|
||||||
# ^ studio.el:57 dharma_registry() emits "principal":"" on every call — no
|
|
||||||
# producer has ever existed in the tree's history (git log -S finds none).
|
|
||||||
# Never-wired rather than orphaned, same silent-"" result.
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# state-key-external.txt — state keys the engine READS but deliberately never
|
|
||||||
# WRITES, because a host outside the El tree sets them (an operator, the
|
|
||||||
# installer, a deployment env). Read scripts/verify-state-keys.sh for why this
|
|
||||||
# list has to exist and why it has to stay short.
|
|
||||||
#
|
|
||||||
# THE RULE FOR ADDING A LINE: the read site must already treat "" as a defined
|
|
||||||
# default (`if str_eq(x, "") { <default> }`) AND the source must say so in a
|
|
||||||
# comment. "I could not find the writer" is NOT a reason — that is the #129
|
|
||||||
# defect, and it belongs in state-key-baseline.txt with a date, not here.
|
|
||||||
#
|
|
||||||
# format: exact|prefix <key> # why, and where the source says so
|
|
||||||
#
|
|
||||||
exact soul_rate_limit # routes.el:59-61 — "configurable via soul state key ... Falls back to 60 req/min if not set."
|
|
||||||
exact web_search_tool_version # chat.el:1884-1910 — version lives in state "so a future bump is a config write, not a recompile"; defaults to web_search_20250305
|
|
||||||
exact platform_auth # stewardship.el:92 — host-set capability flag; fail-CLOSED (anything but "true" denies the platform tool)
|
|
||||||
exact security_research_authorized # awareness.el:991-996 — state override for env SECURITY_RESEARCH_TOKEN; fail-closed, defaults false
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# verify-state-keys.sh — the state-key gate. Retires a defect class at build time.
|
|
||||||
#
|
|
||||||
# ── WHY THIS EXISTS. DO NOT DELETE IT AS NOISE. ──────────────────────────────
|
|
||||||
#
|
|
||||||
# The engine keeps runtime values in a key-value store: state_set("k", v) writes,
|
|
||||||
# state_get("k") reads. A read of a key that NOTHING writes returns an empty
|
|
||||||
# string. Silently. No error, no warning, no log line. The El compiler cannot see
|
|
||||||
# it, no test sees it, and the product keeps running — just with a hole in it.
|
|
||||||
#
|
|
||||||
# That is how issue #129 happened. ff421d3 (2026-08-05) correctly moved
|
|
||||||
# conversation history to a per-session key behind conv_hist_key(session_id). One
|
|
||||||
# consumer did not move with it: the agentic path's L1 safety screen kept reading
|
|
||||||
# the old anonymous "conv_history" bucket. The desktop app always mints a session
|
|
||||||
# id, so history was always written under session_hist_<id> and that read always
|
|
||||||
# returned "". The half of the crisis score that receives history is the
|
|
||||||
# ESCALATION half — the one that exists for distress building across several
|
|
||||||
# turns, where no single message trips the bell on its own. It scored 0 on every
|
|
||||||
# real conversation for two days, and nothing failed.
|
|
||||||
#
|
|
||||||
# The line that broke carried a comment describing this exact bug being fixed
|
|
||||||
# once already, under issue #9. A comment is not a gate. This is the gate.
|
|
||||||
#
|
|
||||||
# ── WHAT IT CHECKS ──────────────────────────────────────────────────────────
|
|
||||||
#
|
|
||||||
# DEAD-READ a state_get whose key resolves to something no state_set in the
|
|
||||||
# tree produces. The direct form of the class.
|
|
||||||
#
|
|
||||||
# HAND-ROLLED a state_get/state_set that spells out a literal belonging to a
|
|
||||||
# key namespace a helper function owns (e.g. "conv_history", owned
|
|
||||||
# by conv_hist_key()). This is #129's actual shape: the producer
|
|
||||||
# moved behind the helper and one consumer kept the old spelling
|
|
||||||
# by hand. DEAD-READ alone does NOT catch #129, because the dead
|
|
||||||
# handle_chat() still writes that key through the helper — so this
|
|
||||||
# second check is the one that earns the gate its keep.
|
|
||||||
#
|
|
||||||
# ── WHY IT DOES NOT CRY WOLF ────────────────────────────────────────────────
|
|
||||||
#
|
|
||||||
# Keys are usually COMPUTED, not literal, so a naive grep would flood and get
|
|
||||||
# switched off within a day. scripts/state-key-audit.py resolves computed keys:
|
|
||||||
# string concatenation (matched on the static prefix), helper functions (resolved
|
|
||||||
# to their possible return values), keys built into a local variable, and keys
|
|
||||||
# arriving as a function parameter (resolved through the call sites). Where a key
|
|
||||||
# genuinely cannot be resolved it is printed under UNRESOLVED and does NOT fail
|
|
||||||
# the build — visible, never silently ignored. Keep that list short.
|
|
||||||
#
|
|
||||||
# On this tree it resolves 278 of 278 sites: UNRESOLVED is 0 and FINDINGS is 0.
|
|
||||||
#
|
|
||||||
# Two declaration files, both of which should only ever shrink:
|
|
||||||
# scripts/state-key-external.txt keys a host outside the El tree writes
|
|
||||||
# scripts/state-key-baseline.txt findings that predate the gate (real debt)
|
|
||||||
#
|
|
||||||
# ── PROVEN TO DISCRIMINATE (2026-08-07) ─────────────────────────────────────
|
|
||||||
#
|
|
||||||
# 1. Synthetic: a scratch copy of this tree with agentic_safety_screen reverted
|
|
||||||
# to the pre-fix state_get("conv_history") — ONE line, nothing else — FAILS
|
|
||||||
# with `chat.el:2536 ... conv_hist_key() owns this key namespace`. The tree
|
|
||||||
# as shipped PASSES. One variable, opposite verdicts.
|
|
||||||
# 2. Independent: run read-only against origin/feat/soul-openai-tools-v2, which
|
|
||||||
# carries the same defect on its own, the gate reported chat.el:2937 — the
|
|
||||||
# exact line 43d0449's commit message had named by hand. Against that
|
|
||||||
# branch's fix (origin/fix/129-on-openai-tools) it passes.
|
|
||||||
# 3. Producer-moved controls: renaming the sole writer of an EXACT key
|
|
||||||
# (soul_model) orphans 3 readers across 3 files; renaming the sole writer of
|
|
||||||
# a PREFIX namespace (agent_workspace_root_*) orphans 3 readers — including
|
|
||||||
# when the producer moves to a NARROWER namespace, which an earlier,
|
|
||||||
# sloppier prefix rule let through.
|
|
||||||
#
|
|
||||||
# It also found, on its first run, a defect nobody was looking for: soul.el's
|
|
||||||
# `state_set("soul_identity", ...)` was deleted on 2026-05-13 in b163fa6 (a
|
|
||||||
# commit about awareness/ISE writes) and five readers in chat.el were left
|
|
||||||
# behind — the system prompt, the vision handler, the agentic prompt and the
|
|
||||||
# council handler have been prefixing "" ever since. See state-key-baseline.txt.
|
|
||||||
#
|
|
||||||
# ── SAFETY ──────────────────────────────────────────────────────────────────
|
|
||||||
# Pure static read of .el sources. Starts nothing, opens no port, touches no
|
|
||||||
# daemon, and never reads or writes ~/.neuron.
|
|
||||||
#
|
|
||||||
# ── USAGE ───────────────────────────────────────────────────────────────────
|
|
||||||
# scripts/verify-state-keys.sh gate the repo (honours baseline)
|
|
||||||
# scripts/verify-state-keys.sh --strict ignore the baseline: show the debt
|
|
||||||
# scripts/verify-state-keys.sh --verbose also dump every write pattern
|
|
||||||
# scripts/verify-state-keys.sh --root DIR audit a different tree
|
|
||||||
# exit 0 = clean; 1 = finding(s); 2 = the gate itself could not run.
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
STRICT=0
|
|
||||||
PASS_THROUGH=()
|
|
||||||
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--strict) STRICT=1; shift ;;
|
|
||||||
--root) ROOT="${2:?--root needs a directory}"; shift 2 ;;
|
|
||||||
-h|--help) awk 'NR>1 && /^#/ {print; next} NR>1 {exit}' "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
||||||
*) PASS_THROUGH+=("$1"); shift ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
command -v python3 >/dev/null 2>&1 || {
|
|
||||||
echo "[state-keys] CANNOT RUN: python3 not found" >&2; exit 2; }
|
|
||||||
[ -d "$ROOT" ] || { echo "[state-keys] CANNOT RUN: no such tree: $ROOT" >&2; exit 2; }
|
|
||||||
|
|
||||||
AUDIT="$SCRIPT_DIR/state-key-audit.py"
|
|
||||||
[ -f "$AUDIT" ] || { echo "[state-keys] CANNOT RUN: missing $AUDIT" >&2; exit 2; }
|
|
||||||
|
|
||||||
ARGS=("$ROOT" "--external" "$SCRIPT_DIR/state-key-external.txt")
|
|
||||||
[ "$STRICT" -eq 0 ] && ARGS+=("--baseline" "$SCRIPT_DIR/state-key-baseline.txt")
|
|
||||||
[ ${#PASS_THROUGH[@]} -gt 0 ] && ARGS+=("${PASS_THROUGH[@]}")
|
|
||||||
|
|
||||||
python3 "$AUDIT" "${ARGS[@]}"
|
|
||||||
RC=$?
|
|
||||||
if [ "$RC" -gt 1 ]; then
|
|
||||||
echo "[state-keys] CANNOT RUN: the audit itself failed (exit $RC)" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
exit "$RC"
|
|
||||||
@@ -559,6 +559,27 @@ let axon_base: String = if str_eq(axon_raw, "") { "http://localhost:7771" } else
|
|||||||
let studio_dir_raw: String = env("SOUL_STUDIO_DIR")
|
let studio_dir_raw: String = env("SOUL_STUDIO_DIR")
|
||||||
let studio_dir: String = if str_eq(studio_dir_raw, "") { env("HOME") + "/Development/neuron-technologies/products/cgi-studio/el-daemon" } else { studio_dir_raw }
|
let studio_dir: String = if str_eq(studio_dir_raw, "") { env("HOME") + "/Development/neuron-technologies/products/cgi-studio/el-daemon" } else { studio_dir_raw }
|
||||||
|
|
||||||
|
// RESTORED 2026-08-09 — this producer was added 2026-05-02 in 601e0fe and deleted
|
||||||
|
// by the awareness refactor b163fa6 a few days later. Nothing has written
|
||||||
|
// soul_identity since, while FIVE sites in chat.el kept reading it:
|
||||||
|
// chat.el:737, 1745, 2620, 3425, 3480 — each doing state_get("soul_identity")
|
||||||
|
// and splicing the result into the system prompt beside the voice, security and
|
||||||
|
// capability rules. They have been splicing an EMPTY STRING for roughly three
|
||||||
|
// months. The identity section of every chat turn was blank and nothing said so.
|
||||||
|
//
|
||||||
|
// Found by the #132 state-key gate, which reports a read with no producer as a
|
||||||
|
// build error rather than a silence — the whole reason that gate exists.
|
||||||
|
//
|
||||||
|
// Restored verbatim rather than improved: this key is an env-configurable persona
|
||||||
|
// LINE, which is NOT the same thing as soul_identity_context (the graph-derived
|
||||||
|
// [INTELLECTUAL-DNA]/[VALUES]/[MEMORY-PHILOSOPHY] block written at soul.el:184).
|
||||||
|
// Pointing these five reads at that block instead would have substituted different
|
||||||
|
// content and called it a fix. Whether the chat system prompt should ALSO carry the
|
||||||
|
// graph-derived block is a real question, and a separate one.
|
||||||
|
let identity_raw: String = env("SOUL_IDENTITY")
|
||||||
|
let soul_identity: String = if str_eq(identity_raw, "") { "You are " + soul_cgi_id + ", a CGI." } else { identity_raw }
|
||||||
|
state_set("soul_identity", soul_identity)
|
||||||
|
|
||||||
println("[soul] boot - cgi=" + soul_cgi_id + " port=" + int_to_str(port))
|
println("[soul] boot - cgi=" + soul_cgi_id + " port=" + int_to_str(port))
|
||||||
|
|
||||||
let using_http_engram: Bool = !str_eq(engram_url_raw, "")
|
let using_http_engram: Bool = !str_eq(engram_url_raw, "")
|
||||||
|
|||||||
@@ -53,8 +53,23 @@ fn handle_config(method: String, body: String) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn dharma_registry() -> String {
|
fn dharma_registry() -> String {
|
||||||
|
// COMPILED IDENTITY, not state (2026-08-09). soul_principal had no producer at
|
||||||
|
// all — the #132 gate flagged it as a dead read and the registry reported an
|
||||||
|
// empty principal under a heading that says "Principal Covenant v1". The value
|
||||||
|
// was never missing: it is declared in soul.el's cgi block, and as of the
|
||||||
|
// codegen fix it is compiled into the binary and loaded at startup.
|
||||||
|
//
|
||||||
|
// Read it from the compiled constant rather than the state store. The design is
|
||||||
|
// explicit that this identity is "not modifiable by any runtime mechanism
|
||||||
|
// including environment variables, configuration files, or API calls" — so
|
||||||
|
// publishing it into state (the cheap fix) would have recreated exactly the
|
||||||
|
// mutable copy it forbids. cgi_principal() is read-only and has no setter.
|
||||||
|
//
|
||||||
|
// cgi_id keeps its state read deliberately: the RUNTIME instance id is a
|
||||||
|
// different fact from the compiled dharma_id, and conflating them would hide
|
||||||
|
// the case where a binary runs under an id its declaration never claimed.
|
||||||
let cgi_id: String = state_get("soul_cgi_id")
|
let cgi_id: String = state_get("soul_cgi_id")
|
||||||
let principal: String = state_get("soul_principal")
|
let principal: String = cgi_principal()
|
||||||
return "{\"registry\":[{\"cgi\":\"" + cgi_id + "\","
|
return "{\"registry\":[{\"cgi\":\"" + cgi_id + "\","
|
||||||
+ "\"principal\":\"" + principal + "\","
|
+ "\"principal\":\"" + principal + "\","
|
||||||
+ "\"covenant\":\"Principal Covenant v1\","
|
+ "\"covenant\":\"Principal Covenant v1\","
|
||||||
|
|||||||
@@ -30,9 +30,19 @@ AMALGAM="$ROOT/dist/soul.c"
|
|||||||
|
|
||||||
# Every .el at the repo root is an input to the amalgam. Sorted so the hash is
|
# 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.
|
# order-independent; content-only so timestamps and checkouts do not perturb it.
|
||||||
|
# The COMPILER is an input too. Learned 2026-08-09 by installing a fixed elc and
|
||||||
|
# watching this gate report OK while the committed amalgam had gone stale by a line:
|
||||||
|
# the sources had not changed, so a source-only fingerprint could not see it. That is
|
||||||
|
# precisely the blind spot this gate exists to close, and it had it.
|
||||||
fingerprint() {
|
fingerprint() {
|
||||||
(
|
(
|
||||||
cd "$ROOT" || exit 1
|
cd "$ROOT" || exit 1
|
||||||
|
ELC_BIN="${ELC:-$HOME/neuron-dev-stack/src/el/lang/dist/platform/elc}"
|
||||||
|
if [ -f "$ELC_BIN" ]; then
|
||||||
|
printf '%s %s\n' "$(shasum -a 256 "$ELC_BIN" | awk '{print $1}')" "__compiler__"
|
||||||
|
else
|
||||||
|
printf '%s %s\n' "MISSING" "__compiler__"
|
||||||
|
fi
|
||||||
for f in $(ls -1 *.el 2>/dev/null | sort); do
|
for f in $(ls -1 *.el 2>/dev/null | sort); do
|
||||||
printf '%s %s\n' "$(shasum -a 256 "$f" | awk '{print $1}')" "$f"
|
printf '%s %s\n' "$(shasum -a 256 "$f" | awk '{print $1}')" "$f"
|
||||||
done
|
done
|
||||||
|
|||||||
+19
@@ -5634,6 +5634,25 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Compiled-identity accessors (2026-08-09) ─────────────────────────────────
|
||||||
|
* el_cgi_init loads the declaration into these globals at startup and printed
|
||||||
|
* them, and NOTHING read them back out — no accessor existed, and el_cgi_init
|
||||||
|
* writes no state. So a binary carried its declared identity and every consumer
|
||||||
|
* still read it from the mutable state store, which is exactly what IDPROTO
|
||||||
|
* claims 1-2 forbid ("not modifiable by any runtime mechanism including
|
||||||
|
* environment variables, configuration files, or API calls").
|
||||||
|
*
|
||||||
|
* These are READ-ONLY on purpose. There is deliberately no setter: publishing
|
||||||
|
* the values into the state store would have been one line and would have
|
||||||
|
* recreated the mutable copy the design prohibits. A caller can read the
|
||||||
|
* compiled identity; nothing can change it after el_cgi_init.
|
||||||
|
*/
|
||||||
|
el_val_t cgi_name(void) { return EL_STR(_el_cgi_name ? _el_cgi_name : ""); }
|
||||||
|
el_val_t cgi_dharma_id(void) { return EL_STR(_el_cgi_dharma_id ? _el_cgi_dharma_id : ""); }
|
||||||
|
el_val_t cgi_principal(void) { return EL_STR(_el_cgi_principal ? _el_cgi_principal : ""); }
|
||||||
|
el_val_t cgi_network(void) { return EL_STR(_el_cgi_network ? _el_cgi_network : ""); }
|
||||||
|
el_val_t cgi_engram(void) { return EL_STR(_el_cgi_engram ? _el_cgi_engram : ""); }
|
||||||
|
|
||||||
/* ── Batch 3: Engram in-process graph store ──────────────────────────────── */
|
/* ── Batch 3: Engram in-process graph store ──────────────────────────────── */
|
||||||
/*
|
/*
|
||||||
* Single global EngramStore allocated lazily on first call. All node and
|
* Single global EngramStore allocated lazily on first call. All node and
|
||||||
|
|||||||
@@ -782,6 +782,14 @@ el_val_t trace_span_start(el_val_t name);
|
|||||||
el_val_t trace_span_end(el_val_t span_handle);
|
el_val_t trace_span_end(el_val_t span_handle);
|
||||||
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
|
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
|
||||||
|
|
||||||
|
/* Compiled-identity accessors — read-only by design (2026-08-09). */
|
||||||
|
el_val_t cgi_name(void);
|
||||||
|
el_val_t cgi_dharma_id(void);
|
||||||
|
el_val_t cgi_principal(void);
|
||||||
|
el_val_t cgi_network(void);
|
||||||
|
el_val_t cgi_engram(void);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Reference in New Issue
Block a user