Compare commits

..

2 Commits

Author SHA1 Message Date
Tim Lingo f76ccc0590 engram: ranked BM25+recency search replaces storage-order substring; URL-decode GET query params
Measured on the live container mind (pinned 40-query eval, judged): substring
2/40=5% hit@5 -> ranked 35/40=88%. Multi-word queries stop returning zero; new
memories stop losing to storage order (created_at tiebreak). Transparent-layer
identity filter preserved in both passes; jb_finish (#64) tail preserved.
query_param now url_decode()s values - %XX arrived literal before (pre-existing
GET defect, masked while multi-word substring returned nothing anyway).
E2E-verified in Tim's container deployment 2026-07-14/15; eval harness:
docs repo research-archive/p0-prototypes/eval_pinned_40q_20260715.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 07:48:09 -05:00
will.anderson 2b2a1246e7 Merge pull request 'runtime: fix the memory-leak + write-corruption pair in el_runtime.c' (#64) from hotfix/el-runtime-leak-and-persist into main
El SDK Release / build-and-release (push) Failing after 10m52s
2026-07-13 21:23:31 +00:00
15 changed files with 465 additions and 7683 deletions
-45
View File
@@ -1,45 +0,0 @@
;;; lang_profile_es.el — Spanish language profile for ELP.
;;; Keys the realizer's construction switches. Mirrors lang_profile_en / _pt.
(lang_profile_es
(language "Spanish")
(iso639 "es")
(family "Romance")
;; -- core typology flags -------------------------------------------------
(pro-drop yes) ; subjects routinely dropped; agreement carries person
(obligatory-subject no)
(grammatical-gender yes) ; m/f on every noun; article+adjective AGREE
(gender-source lexicon); REAL per-noun gender from UniMorph — NOT a heuristic
(do-support no)
(subject-aux-inversion no) ; questions by intonation/punctuation, not inversion
(question-strategy intonation)
(article-selection "el/la/los/las un/una/unos/unas")
(stressed-a-rule yes) ; fem sg noun in stressed a-/ha- takes el/un (el agua)
(adjective-position postnominal) ; default post; a few prenominal + apocope
(adjective-agreement "gender+number")
(question-punct inverted) ; opening ¿ ¡ required
;; -- MANDATORY CONTRACTIONS (coordinator quality bar) --------------------
(contractions ((de el "del") (a el "al")))
(contraction-mandatory yes) ; 'de el'/'a el' MUST surface as del/al
;; -- verb / aspect system ------------------------------------------------
(verb-classes (ar er ir))
(tenses (present preterite imperfect future conditional))
(moods (ind sbjv imp))
(finite-agreement "person+number (6 slots)")
(perfect-aux "haber") ; haber + past participle (invariant -o)
(progressive-aux "estar") ; estar + gerund
(passive-aux "ser") ; ser + participle (agrees) + por-agent
(copula-split "ser/estar") ; permanent vs stage-level
(future "infinitive + é/ás/á/emos/éis/án")
;; -- clitics / government ------------------------------------------------
(object-clitics yes) ; me te lo la le nos os los las; proclisis/enclisis
(clitic-order "se II I III (le+lo -> se lo)")
(enclisis "imperative/infinitive/gerund + accent repair (dá+me+lo->dámelo)")
(verb-prep-government yes) ; verbs select prep (protestar+contra, escapar+de)
;; -- SACRED safety bar (shared with en/pt) -------------------------------
(negation-faithful yes)) ; polarity never dropped/inverted; unplaceable -> FLAG
+10 -171
View File
@@ -54,16 +54,6 @@ fn es_str_last3(s: String) -> String {
// Spanish verbs fall into three conjugation classes defined by the infinitive
// ending: -ar, -er, -ir. The stem is the infinitive minus those two characters.
// Strong-vowel-final test (a/e/o) used for orthographic y-insertion and the
// accented -ído participle (caer->caído/cayó; but ui/iu diphthongs stay plain).
fn es_strong_vowel_final(s: String) -> Bool {
let c: String = es_str_last_char(s)
if str_eq(c, "a") { return true }
if str_eq(c, "e") { return true }
if str_eq(c, "o") { return true }
return false
}
fn es_verb_class(base: String) -> String {
if es_str_ends(base, "ar") { return "ar" }
if es_str_ends(base, "er") { return "er" }
@@ -463,18 +453,12 @@ fn es_regular_preterite(stem: String, vclass: String, slot: Int) -> String {
if slot == 4 { return stem + "asteis" }
return stem + "aron"
}
// -er and -ir share the same preterite endings.
// Orthographic rule: a vowel-final stem takes -yó/-yeron (caer->cayó,
// leer->leyó, creer->creyó) since -ió after a vowel becomes -yó.
// -er and -ir share the same preterite endings
if slot == 0 { return stem + "í" }
if slot == 1 { return stem + "iste" }
if slot == 2 {
if es_strong_vowel_final(stem) { return stem + "" }
return stem + ""
}
if slot == 2 { return stem + "" }
if slot == 3 { return stem + "imos" }
if slot == 4 { return stem + "isteis" }
if es_strong_vowel_final(stem) { return stem + "yeron" }
return stem + "ieron"
}
@@ -656,35 +640,17 @@ fn es_pluralize(noun: String) -> String {
if !str_eq(inv, "") {
return inv
}
// Oxytone nouns ending accented-vowel + n LOSE the written accent in the
// plural (canción->canciones, razón->razones, jardín->jardines).
// NOTE El strings are byte-indexed; "ón"/"án"/... are 3 bytes (accent=2 +n).
if es_str_ends(noun, "ón") { return es_str_drop_last(noun, 3) + "ones" }
if es_str_ends(noun, "án") { return es_str_drop_last(noun, 3) + "anes" }
if es_str_ends(noun, "én") { return es_str_drop_last(noun, 3) + "enes" }
if es_str_ends(noun, "ín") { return es_str_drop_last(noun, 3) + "ines" }
if es_str_ends(noun, "ún") { return es_str_drop_last(noun, 3) + "unes" }
// Oxytone accented-vowel + s also loses the accent (francés->franceses,
// inglés->ingleses). í/ú stay (país->países via the consonant rule).
if es_str_ends(noun, "és") { return es_str_drop_last(noun, 3) + "eses" }
if es_str_ends(noun, "ás") { return es_str_drop_last(noun, 3) + "ases" }
if es_str_ends(noun, "ós") { return es_str_drop_last(noun, 3) + "oses" }
let last: String = es_str_last_char(noun)
// Ends in -z: replace with -ces
if es_str_ends(noun, "z") {
if str_eq(last, "z") {
return es_str_drop_last(noun, 1) + "ces"
}
// Stressed final vowel: á/é/ó -> +s (café->cafés); í/ú -> +es (rubí->rubíes)
if es_str_ends(noun, "á") { return noun + "s" }
if es_str_ends(noun, "é") { return noun + "s" }
if es_str_ends(noun, "ó") { return noun + "s" }
if es_str_ends(noun, "í") { return noun + "es" }
if es_str_ends(noun, "ú") { return noun + "es" }
// Plain final vowel: add -s
if es_str_ends(noun, "a") { return noun + "s" }
if es_str_ends(noun, "e") { return noun + "s" }
if es_str_ends(noun, "i") { return noun + "s" }
if es_str_ends(noun, "o") { return noun + "s" }
if es_str_ends(noun, "u") { return noun + "s" }
// Ends in a vowel: add -s
if str_eq(last, "a") { return noun + "s" }
if str_eq(last, "e") { return noun + "s" }
if str_eq(last, "i") { return noun + "s" }
if str_eq(last, "o") { return noun + "s" }
if str_eq(last, "u") { return noun + "s" }
// Ends in consonant (including -s for stressed words like autobús): add -es
return noun + "es"
}
@@ -721,37 +687,6 @@ fn es_starts_with_stressed_a(noun: String) -> Bool {
return false
}
// es_article_for_gender: the article logic, given gender EXPLICITLY.
// The realizer should call this with the REAL per-noun gender from
// vocabulary-es.el (form2), NOT the es_gender heuristic that is what
// eliminates the 'el mano' / 'la día' masculine-default error class.
fn es_article_for_gender(gender: String, noun: String, definite: String, number: String) -> String {
let is_plural: Bool = str_eq(number, "plural")
let is_def: Bool = str_eq(definite, "true")
if is_def {
if is_plural {
if str_eq(gender, "f") { return "las" }
return "los"
}
if str_eq(gender, "f") {
if es_starts_with_stressed_a(noun) { return "el" }
return "la"
}
return "el"
}
if is_plural {
if str_eq(gender, "f") { return "unas" }
return "unos"
}
if str_eq(gender, "f") {
if es_starts_with_stressed_a(noun) { return "un" }
return "una"
}
return "un"
}
fn es_agree_article(noun: String, definite: String, number: String) -> String {
let gender: String = es_gender(noun)
let is_plural: Bool = str_eq(number, "plural")
@@ -779,99 +714,3 @@ fn es_agree_article(noun: String, definite: String, number: String) -> String {
if str_eq(gender, "f") { return "una" }
return "un"
}
// Past participle (compound tenses: haber + participle)
//
// Irregular participle table (transcribed from morphology_es_full._IRREG_PART),
// then the regular rule: -ar -> -ado, -er/-ir -> -ido.
fn es_participle(verb: String) -> String {
if str_eq(verb, "escribir") { return "escrito" }
if str_eq(verb, "describir") { return "descrito" }
if str_eq(verb, "abrir") { return "abierto" }
if str_eq(verb, "cubrir") { return "cubierto" }
if str_eq(verb, "descubrir") { return "descubierto" }
if str_eq(verb, "morir") { return "muerto" }
if str_eq(verb, "poner") { return "puesto" }
if str_eq(verb, "ver") { return "visto" }
if str_eq(verb, "volver") { return "vuelto" }
if str_eq(verb, "devolver") { return "devuelto" }
if str_eq(verb, "hacer") { return "hecho" }
if str_eq(verb, "deshacer") { return "deshecho" }
if str_eq(verb, "decir") { return "dicho" }
if str_eq(verb, "romper") { return "roto" }
if str_eq(verb, "resolver") { return "resuelto" }
if str_eq(verb, "freír") { return "frito" }
if str_eq(verb, "imprimir") { return "impreso" }
if str_eq(verb, "satisfacer") { return "satisfecho" }
if str_eq(verb, "prever") { return "previsto" }
if str_eq(verb, "revolver") { return "revuelto" }
// Accented -ír infinitives (oír->oído, sonreír->sonreído): "ír" is 3 bytes.
if es_str_ends(verb, "ír") { return es_str_drop_last(verb, 3) + "ído" }
if es_str_ends(verb, "ar") { return es_str_drop_last(verb, 2) + "ado" }
// -er/-ir: a strong-vowel stem takes the accented -ído (caer->caído,
// leer->leído, poseer->poseído); consonant stems stay -ido; ui/iu
// diphthongs (construir->construido) stay plain.
if es_str_ends(verb, "er") {
let st: String = es_str_drop_last(verb, 2)
if es_strong_vowel_final(st) { return st + "ído" }
return st + "ido"
}
if es_str_ends(verb, "ir") {
let st: String = es_str_drop_last(verb, 2)
if es_strong_vowel_final(st) { return st + "ído" }
return st + "ido"
}
return verb
}
// Adjective agreement (gender + number)
//
// Rule-based agreement mirroring morphology_es_full.inflect_adj (rule path):
// feminine: -o -> -a; nationality/-dor exceptions add -a; else invariant
// plural: reuse es_pluralize on the agreed singular
// (Lexicon-backed adjectives in Python may differ; those divergences are what
// the parity check surfaces.)
fn es_adj_feminine(lemma: String) -> String {
if str_eq(lemma, "español") { return "española" }
if str_eq(lemma, "francés") { return "francesa" }
if str_eq(lemma, "inglés") { return "inglesa" }
if str_eq(lemma, "alemán") { return "alemana" }
if str_eq(lemma, "trabajador") { return "trabajadora" }
if str_eq(lemma, "hablador") { return "habladora" }
if str_eq(lemma, "encantador") { return "encantadora" }
if es_str_ends(lemma, "o") { return es_str_drop_last(lemma, 1) + "a" }
return lemma
}
fn es_inflect_adj(lemma: String, gender: String, number: String) -> String {
if str_eq(gender, "f") {
let fem: String = es_adj_feminine(lemma)
if str_eq(number, "plural") { return es_pluralize(fem) }
return fem
}
// masculine (citation)
if str_eq(number, "plural") { return es_pluralize(lemma) }
return lemma
}
// Mandatory preposition + article contraction (de+el->del, a+el->al)
//
// Transcribed from realizer_es._contract. np_text is the already-realized NP
// beginning with "el " when the contraction fires.
fn es_starts_el(np_text: String) -> Bool {
let n: Int = str_len(np_text)
if n < 3 { return false }
return str_eq(str_slice(np_text, 0, 3), "el ")
}
fn es_contract(prep: String, np_text: String) -> String {
if es_starts_el(np_text) {
let rest: String = str_slice(np_text, 3, str_len(np_text))
if str_eq(prep, "de") { return "del " + rest }
if str_eq(prep, "a") { return "al " + rest }
}
return prep + " " + np_text
}
-362
View File
@@ -1,362 +0,0 @@
;;; vocabulary-es.el — Spanish vocabulary for ELP surface realization.
;;; Schema: (lemma pos form0 form1 form2 en_translation semantic_hint)
;;; Source: UniMorph Spanish (github.com/unimorph/spa, CC-BY-SA 3.0),
;;; generated by gen_elp_es.py via morphology_es_full (real forms).
;;; Verbs: form0=present-ind-3sg form1=preterite-3sg form2=past-participle
;;; Nouns: form0=singular form1=plural form2=REAL gender (m/f, from lexicon —
;;; NOT an ending heuristic; this is what kills 'el mano'/'la día' errors)
;;; Adjs : form0=masc-sg form1=fem-sg form2=masc-pl
(vocabulary-es
;; -- function / closed class (incl. mandatory contractions del/al) --------
("el" "det" "el" "los" "m" "the" "definite article m.sg")
("la" "det" "la" "las" "f" "the" "definite article f.sg")
("un" "det" "un" "unos" "m" "a" "indefinite article m.sg")
("una" "det" "una" "unas" "f" "a" "indefinite article f.sg")
("del" "contraction" "del" "" "" "of the" "de + el (mandatory contraction)")
("al" "contraction" "al" "" "" "to the" "a + el (mandatory contraction)")
("este" "dem" "este" "estos" "m" "this" "proximal dem m")
("esta" "dem" "esta" "estas" "f" "this" "proximal dem f")
("no" "neg" "no" "" "" "not/no" "sentential negator (preverbal)")
("ninguno" "det" "ningún" "ninguna" "" "none" "negative determiner (apocope ningún m.sg)")
("y" "conj" "y" "e" "" "and" "coordinator (e before i-/hi-)")
("o" "conj" "o" "u" "" "or" "coordinator (u before o-/ho-)")
("pero" "conj" "pero" "" "" "but" "adversative coordinator")
("que" "conj" "que" "" "" "that" "complementizer / relative")
("si" "conj" "si" "" "" "if" "conditional subordinator")
("porque" "conj" "porque" "" "" "because" "causal subordinator")
("cuando" "conj" "cuando" "" "" "when" "temporal subordinator")
("a" "prep" "a" "" "" "to" "dir-obj (personal a) / dative / allative; a+el=al")
("de" "prep" "de" "" "" "of/from" "genitive/ablative government; de+el=del")
("en" "prep" "en" "" "" "in/on" "locative")
("con" "prep" "con" "" "" "with" "comitative")
("por" "prep" "por" "" "" "by/for" "passive agent / cause")
("para" "prep" "para" "" "" "for" "purpose/benefactive")
("contra" "prep" "contra" "" "" "against" "adversative government (protestar contra)")
("sin" "prep" "sin" "" "" "without" "privative")
("yo" "pron" "yo" "me" "mi" "I" "1sg subj/obj/poss")
("" "pron" "" "te" "tu" "you" "2sg informal")
("usted" "pron" "usted" "lo" "su" "you" "2sg formal (3sg agreement)")
("él" "pron" "él" "lo" "su" "he" "3sg m subj/DO-clitic/poss")
("ella" "pron" "ella" "la" "su" "she" "3sg f subj/DO-clitic/poss")
("nosotros" "pron" "nosotros" "nos" "nuestro" "we" "1pl")
("vosotros" "pron" "vosotros" "os" "vuestro" "you" "2pl informal")
("ellos" "pron" "ellos" "los" "su" "they" "3pl m")
("ellas" "pron" "ellas" "las" "su" "they" "3pl f")
("le" "clitic" "le" "les" "" "to-him/her" "dative clitic 3sg/3pl (->se before lo/la)")
("se" "clitic" "se" "se" "" "himself/-self" "reflexive / spurious-se (le+lo->se lo)")
;; -- verbs (form0=pres-3sg form1=pret-3sg form2=past-participle) -----------
("abrazar" "verb" "abraza" "abrazó" "abrazado" "abrazar" "ar/regular")
("abrir" "verb" "abre" "abrió" "abierto" "abrir" "ir/irregular")
("aceptar" "verb" "acepta" "aceptó" "aceptado" "aceptar" "ar/regular")
("acordar" "verb" "acuerda" "acordó" "acordado" "acordar" "ar/regular")
("amar" "verb" "ama" "amó" "amado" "amar" "ar/regular")
("anunciar" "verb" "anuncia" "anunció" "anunciado" "anunciar" "ar/regular")
("aprobar" "verb" "aprueba" "aprobó" "aprobado" "aprobar" "ar/regular")
("aumentar" "verb" "aumenta" "aumentó" "aumentado" "aumentar" "ar/regular")
("ayudar" "verb" "ayuda" "ayudó" "ayudado" "ayudar" "ar/regular")
("bajar" "verb" "baja" "bajó" "bajado" "bajar" "ar/regular")
("besar" "verb" "besa" "besó" "besado" "besar" "ar/regular")
("buscar" "verb" "busca" "buscó" "buscado" "buscar" "ar/regular")
("caer" "verb" "cae" "cayó" "caído" "caer" "er/regular")
("cambiar" "verb" "cambia" "cambió" "cambiado" "cambiar" "ar/regular")
("caminar" "verb" "camina" "caminó" "caminado" "caminar" "ar/regular")
("cantar" "verb" "canta" "cantó" "cantado" "cantar" "ar/regular")
("celebrar" "verb" "celebra" "celebró" "celebrado" "celebrar" "ar/regular")
("cerrar" "verb" "cierra" "cerró" "cerrado" "cerrar" "ar/regular")
("comenzar" "verb" "comienza" "comenzó" "comenzado" "comenzar" "ar/regular")
("comer" "verb" "come" "comió" "comido" "comer" "er/regular")
("condenar" "verb" "condena" "condenó" "condenado" "condenar" "ar/regular")
("confirmar" "verb" "confirma" "confirmó" "confirmado" "confirmar" "ar/regular")
("conocer" "verb" "conoce" "conoció" "conocido" "conocer" "er/regular")
("contar" "verb" "cuenta" "contó" "contado" "contar" "ar/regular")
("contratar" "verb" "contrata" "contrató" "contratado" "contratar" "ar/regular")
("costar" "verb" "cuesta" "costó" "costado" "costar" "ar/regular")
("crear" "verb" "crea" "creó" "creado" "crear" "ar/regular")
("crecer" "verb" "crece" "creció" "crecido" "crecer" "er/regular")
("creer" "verb" "cree" "creyó" "creído" "creer" "er/regular")
("dar" "verb" "da" "dio" "dado" "dar" "ar/irregular")
("deber" "verb" "debe" "debió" "debido" "deber" "er/regular")
("decir" "verb" "dice" "dijo" "dicho" "decir" "ir/irregular")
("dejar" "verb" "deja" "dejó" "dejado" "dejar" "ar/regular")
("desaparecer" "verb" "desaparece" "desapareció" "desaparecido" "desaparecer" "er/regular")
("descubrir" "verb" "descubre" "descubrió" "descubierto" "descubrir" "ir/irregular")
("despedir" "verb" "despide" "despidió" "despedido" "despedir" "ir/regular")
("detener" "verb" "detiene" "detuvo" "detenido" "detener" "er/regular")
("dimitir" "verb" "dimite" "dimitió" "dimitido" "dimitir" "ir/regular")
("doler" "verb" "duele" "dolió" "dolido" "doler" "er/regular")
("dormir" "verb" "duerme" "durmió" "dormido" "dormir" "ir/regular")
("durar" "verb" "dura" "duró" "durado" "durar" "ar/regular")
("empezar" "verb" "empieza" "empezó" "empezado" "empezar" "ar/regular")
("encontrar" "verb" "encuentra" "encontró" "encontrado" "encontrar" "ar/regular")
("entender" "verb" "entiende" "entendió" "entendido" "entender" "er/regular")
("entrar" "verb" "entra" "entró" "entrado" "entrar" "ar/regular")
("entregar" "verb" "entrega" "entregó" "entregado" "entregar" "ar/regular")
("escapar" "verb" "escapa" "escapó" "escapado" "escapar" "ar/regular")
("escribir" "verb" "escribe" "escribió" "escrito" "escribir" "ir/regular")
("escuchar" "verb" "escucha" "escuchó" "escuchado" "escuchar" "ar/regular")
("esperar" "verb" "espera" "esperó" "esperado" "esperar" "ar/regular")
("estar" "verb" "está" "estuvo" "estado" "estar" "ar/irregular")
("estudiar" "verb" "estudia" "estudió" "estudiado" "estudiar" "ar/regular")
("existir" "verb" "existe" "existió" "existido" "existir" "ir/regular")
("explicar" "verb" "explica" "explicó" "explicado" "explicar" "ar/regular")
("firmar" "verb" "firma" "firmó" "firmado" "firmar" "ar/regular")
("ganar" "verb" "gana" "ganó" "ganado" "ganar" "ar/regular")
("gritar" "verb" "grita" "gritó" "gritado" "gritar" "ar/regular")
("guardar" "verb" "guarda" "guardó" "guardado" "guardar" "ar/regular")
("gustar" "verb" "gusta" "gustó" "gustado" "gustar" "ar/regular")
("haber" "verb" "ha" "hubo" "habido" "haber" "er/irregular")
("hablar" "verb" "habla" "habló" "hablado" "hablar" "ar/regular")
("hacer" "verb" "hace" "hizo" "hecho" "hacer" "er/irregular")
("ir" "verb" "va" "fue" "ido" "ir" "ir/irregular")
("jugar" "verb" "juega" "jugó" "jugado" "jugar" "ar/regular")
("lavar" "verb" "lava" "lavó" "lavado" "lavar" "ar/regular")
("leer" "verb" "lee" "leyó" "leído" "leer" "er/regular")
("llamar" "verb" "llama" "llamó" "llamado" "llamar" "ar/regular")
("llegar" "verb" "llega" "llegó" "llegado" "llegar" "ar/regular")
("llevar" "verb" "lleva" "llevó" "llevado" "llevar" "ar/regular")
("llover" "verb" "llueve" "llovió" "llovido" "llover" "er/regular")
("mirar" "verb" "mira" "miró" "mirado" "mirar" "ar/regular")
("morir" "verb" "muere" "murió" "muerto" "morir" "ir/irregular")
("mostrar" "verb" "muestra" "mostró" "mostrado" "mostrar" "ar/regular")
("nacer" "verb" "nace" "nació" "nacido" "nacer" "er/regular")
("ocultar" "verb" "oculta" "ocultó" "ocultado" "ocultar" "ar/regular")
("ofrecer" "verb" "ofrece" "ofreció" "ofrecido" "ofrecer" "er/regular")
("olvidar" "verb" "olvida" "olvidó" "olvidado" "olvidar" "ar/regular")
("oír" "verb" "oye" "oyó" "oído" "oír" "ar/regular")
("parecer" "verb" "parece" "pareció" "parecido" "parecer" "er/regular")
("pasar" "verb" "pasa" "pasó" "pasado" "pasar" "ar/regular")
("pedir" "verb" "pide" "pidió" "pedido" "pedir" "ir/regular")
("pensar" "verb" "piensa" "pensó" "pensado" "pensar" "ar/regular")
("perder" "verb" "pierde" "perdió" "perdido" "perder" "er/regular")
("perdonar" "verb" "perdona" "perdonó" "perdonado" "perdonar" "ar/regular")
("poder" "verb" "puede" "pudo" "podido" "poder" "er/irregular")
("poner" "verb" "pone" "puso" "puesto" "poner" "er/irregular")
("preocupar" "verb" "preocupa" "preocupó" "preocupado" "preocupar" "ar/regular")
("producir" "verb" "produce" "produjo" "producido" "producir" "ir/regular")
("prometer" "verb" "promete" "prometió" "prometido" "prometer" "er/regular")
("proponer" "verb" "propone" "propuso" "propuesto" "proponer" "er/regular")
("prosperar" "verb" "prospera" "prosperó" "prosperado" "prosperar" "ar/regular")
("protestar" "verb" "protesta" "protestó" "protestado" "protestar" "ar/regular")
("quedar" "verb" "queda" "quedó" "quedado" "quedar" "ar/regular")
("querer" "verb" "quiere" "quiso" "querido" "querer" "er/irregular")
("recordar" "verb" "recuerda" "recordó" "recordado" "recordar" "ar/regular")
("recorrer" "verb" "recorre" "recorrió" "recorrido" "recorrer" "er/regular")
("regresar" "verb" "regresa" "regresó" "regresado" "regresar" "ar/regular")
("renacer" "verb" "renace" "renació" "renacido" "renacer" "er/regular")
("saber" "verb" "sabe" "supo" "sabido" "saber" "er/irregular")
("salir" "verb" "sale" "salió" "salido" "salir" "ir/irregular")
("sentar" "verb" "sienta" "sentó" "sentado" "sentar" "ar/regular")
("sentir" "verb" "siente" "sintió" "sentido" "sentir" "ir/regular")
("separar" "verb" "separa" "separó" "separado" "separar" "ar/regular")
("ser" "verb" "es" "fue" "sido" "ser" "er/irregular")
("sonreír" "verb" "sonríe" "sonrió" "sonreído" "sonreír" "ar/regular")
("soplar" "verb" "sopla" "sopló" "soplado" "soplar" "ar/regular")
("sorprender" "verb" "sorprende" "sorprendió" "sorprendido" "sorprender" "er/regular")
("soñar" "verb" "sueña" "soñó" "soñado" "soñar" "ar/regular")
("subir" "verb" "sube" "subió" "subido" "subir" "ir/regular")
("tener" "verb" "tiene" "tuvo" "tenido" "tener" "er/irregular")
("terminar" "verb" "termina" "terminó" "terminado" "terminar" "ar/regular")
("tocar" "verb" "toca" "tocó" "tocado" "tocar" "ar/regular")
("tomar" "verb" "toma" "tomó" "tomado" "tomar" "ar/regular")
("trabajar" "verb" "trabaja" "trabajó" "trabajado" "trabajar" "ar/regular")
("vender" "verb" "vende" "vendió" "vendido" "vender" "er/regular")
("venir" "verb" "viene" "vino" "venido" "venir" "ir/irregular")
("ver" "verb" "ve" "vio" "visto" "ver" "er/irregular")
("viajar" "verb" "viaja" "viajó" "viajado" "viajar" "ar/regular")
("vivir" "verb" "vive" "vivió" "vivido" "vivir" "ir/regular")
("volver" "verb" "vuelve" "volvió" "vuelto" "volver" "er/irregular")
;; -- nouns (form0=sg form1=pl form2=REAL gender m/f) ----------------------
("Barcelona" "noun" "barcelona" "barcelonas" "f" "Barcelona" "gender:heuristic")
("Madrid" "noun" "madrid" "madrides" "m" "Madrid" "gender:heuristic")
("María" "noun" "maría" "marías" "f" "María" "gender:heuristic")
("acuerdo" "noun" "acuerdo" "acuerdos" "m" "acuerdo" "gender:lexicon")
("acusado" "noun" "acusado" "acusados" "m" "acusado" "gender:lexicon")
("agua" "noun" "agua" "aguas" "f" "agua" "gender:lexicon")
("amigo" "noun" "amigo" "amigos" "m" "amigo" "gender:lexicon")
("amor" "noun" "amor" "amores" "m" "amor" "gender:lexicon")
("anciano" "noun" "anciano" "ancianos" "m" "anciano" "gender:lexicon")
("autor" "noun" "autor" "autores" "m" "autor" "gender:lexicon")
("ayuda" "noun" "ayuda" "ayudas" "f" "ayuda" "gender:lexicon")
("año" "noun" "año" "años" "m" "año" "gender:lexicon")
("banco" "noun" "banco" "bancos" "m" "banco" "gender:lexicon")
("barco" "noun" "barco" "barcos" "m" "barco" "gender:lexicon")
("baño" "noun" "baño" "baños" "m" "baño" "gender:lexicon")
("beneficio" "noun" "beneficio" "beneficios" "m" "beneficio" "gender:lexicon")
("billete" "noun" "billete" "billetes" "m" "billete" "gender:lexicon")
("café" "noun" "café" "cafés" "m" "café" "gender:lexicon")
("calma" "noun" "calma" "calmas" "f" "calma" "gender:lexicon")
("camino" "noun" "camino" "caminos" "m" "camino" "gender:lexicon")
("canción" "noun" "canción" "canciones" "f" "canción" "gender:lexicon")
("candidato" "noun" "candidato" "candidatos" "m" "candidato" "gender:lexicon")
("casa" "noun" "casa" "casas" "f" "casa" "gender:lexicon")
("cena" "noun" "cena" "cenas" "f" "cena" "gender:lexicon")
("ciudad" "noun" "ciudad" "ciudades" "f" "ciudad" "gender:lexicon")
("ciudadano" "noun" "ciudadano" "ciudadanos" "m" "ciudadano" "gender:lexicon")
("color" "noun" "color" "colores" "m" "color" "gender:lexicon")
("condición" "noun" "condición" "condiciones" "f" "condición" "gender:lexicon")
("corazón" "noun" "corazón" "corazones" "m" "corazón" "gender:lexicon")
("costa" "noun" "costa" "costas" "f" "costa" "gender:lexicon")
("crisis" "noun" "crisis" "crisis" "f" "crisis" "gender:lexicon")
("crédito" "noun" "crédito" "créditos" "m" "crédito" "gender:lexicon")
("culpa" "noun" "culpa" "culpas" "f" "culpa" "gender:lexicon")
("damnificado" "noun" "damnificado" "damnificados" "m" "damnificado" "gender:lexicon")
("demás" "noun" "demás" "demás" "m" "demás" "gender:heuristic")
("dinero" "noun" "dinero" "dineros" "m" "dinero" "gender:lexicon")
("día" "noun" "día" "días" "m" "día" "gender:lexicon")
("economía" "noun" "economía" "economías" "f" "economía" "gender:lexicon")
("elección" "noun" "elección" "elecciones" "f" "elección" "gender:lexicon")
("empleo" "noun" "empleo" "empleos" "m" "empleo" "gender:lexicon")
("empresa" "noun" "empresa" "empresas" "f" "empresa" "gender:lexicon")
("equipo" "noun" "equipo" "equipos" "m" "equipo" "gender:lexicon")
("estación" "noun" "estación" "estaciones" "f" "estación" "gender:lexicon")
("estudiante" "noun" "estudiante" "estudiantes" "m" "estudiante" "gender:lexicon")
("experto" "noun" "experto" "expertos" "m" "experto" "gender:lexicon")
("fiesta" "noun" "fiesta" "fiestas" "f" "fiesta" "gender:lexicon")
("flor" "noun" "flor" "flores" "f" "flor" "gender:lexicon")
("foto" "noun" "foto" "fotos" "f" "foto" "gender:lexicon")
("frío" "noun" "frío" "fríos" "m" "frío" "gender:lexicon")
("fuego" "noun" "fuego" "fuegos" "m" "fuego" "gender:lexicon")
("fuerza" "noun" "fuerza" "fuerzas" "f" "fuerza" "gender:lexicon")
("gato" "noun" "gato" "gatos" "m" "gato" "gender:lexicon")
("gobierno" "noun" "gobierno" "gobiernos" "m" "gobierno" "gender:lexicon")
("gusto" "noun" "gusto" "gustos" "m" "gusto" "gender:lexicon")
("hermano" "noun" "hermano" "hermanos" "m" "hermano" "gender:lexicon")
("hombre" "noun" "hombre" "hombres" "m" "hombre" "gender:lexicon")
("jardín" "noun" "jardín" "jardines" "m" "jardín" "gender:lexicon")
("juez" "noun" "juez" "jueces" "m" "juez" "gender:lexicon")
("juicio" "noun" "juicio" "juicios" "m" "juicio" "gender:lexicon")
("lentitud" "noun" "lentitud" "lentitudes" "f" "lentitud" "gender:lexicon")
("ley" "noun" "ley" "leyes" "f" "ley" "gender:lexicon")
("libertad" "noun" "libertad" "libertades" "f" "libertad" "gender:lexicon")
("libro" "noun" "libro" "libros" "m" "libro" "gender:lexicon")
("llave" "noun" "llave" "llaves" "f" "llave" "gender:lexicon")
("lluvia" "noun" "lluvia" "lluvias" "f" "lluvia" "gender:lexicon")
("luna" "noun" "luna" "lunas" "f" "luna" "gender:lexicon")
("luz" "noun" "luz" "luces" "f" "luz" "gender:lexicon")
("madre" "noun" "madre" "madres" "f" "madre" "gender:lexicon")
("mano" "noun" "mano" "manos" "f" "mano" "gender:lexicon")
("mapa" "noun" "mapa" "mapas" "m" "mapa" "gender:lexicon")
("mar" "noun" "mar" "mares" "m" "mar" "gender:lexicon")
("marea" "noun" "marea" "mareas" "f" "marea" "gender:lexicon")
("memoria" "noun" "memoria" "memorias" "f" "memoria" "gender:lexicon")
("mesa" "noun" "mesa" "mesas" "f" "mesa" "gender:lexicon")
("ministro" "noun" "ministro" "ministros" "m" "ministro" "gender:lexicon")
("montaña" "noun" "montaña" "montañas" "f" "montaña" "gender:lexicon")
("moto" "noun" "moto" "motos" "f" "moto" "gender:lexicon")
("mujer" "noun" "mujer" "mujeres" "f" "mujer" "gender:lexicon")
("mundo" "noun" "mundo" "mundos" "m" "mundo" "gender:lexicon")
("música" "noun" "música" "músicas" "f" "música" "gender:lexicon")
("nación" "noun" "nación" "naciones" "f" "nación" "gender:lexicon")
("niebla" "noun" "niebla" "nieblas" "f" "niebla" "gender:lexicon")
("nieve" "noun" "nieve" "nieves" "f" "nieve" "gender:lexicon")
("niña" "noun" "niña" "niñas" "f" "niña" "gender:lexicon")
("niño" "noun" "niño" "niños" "m" "niño" "gender:lexicon")
("noche" "noun" "noche" "noches" "f" "noche" "gender:lexicon")
("nombre" "noun" "nombre" "nombres" "m" "nombre" "gender:lexicon")
("ojo" "noun" "ojo" "ojos" "m" "ojo" "gender:heuristic")
("orilla" "noun" "orilla" "orillas" "f" "orilla" "gender:lexicon")
("paisaje" "noun" "paisaje" "paisajes" "m" "paisaje" "gender:lexicon")
("parlamento" "noun" "parlamento" "parlamentos" "m" "parlamento" "gender:lexicon")
("parte" "noun" "parte" "partes" "f" "parte" "gender:lexicon")
("pasillo" "noun" "pasillo" "pasillos" "m" "pasillo" "gender:lexicon")
("país" "noun" "país" "países" "m" "país" "gender:lexicon")
("película" "noun" "película" "películas" "f" "película" "gender:lexicon")
("perro" "noun" "perro" "perros" "m" "perro" "gender:lexicon")
("persona" "noun" "persona" "personas" "f" "persona" "gender:lexicon")
("petróleo" "noun" "petróleo" "petróleos" "m" "petróleo" "gender:lexicon")
("pez" "noun" "pez" "peces" "f" "pez" "gender:lexicon")
("plan" "noun" "plan" "planes" "m" "plan" "gender:lexicon")
("policía" "noun" "policía" "policías" "f" "policía" "gender:lexicon")
("portavoz" "noun" "portavoz" "portavoces" "m" "portavoz" "gender:lexicon")
("portero" "noun" "portero" "porteros" "m" "portero" "gender:lexicon")
("precio" "noun" "precio" "precios" "m" "precio" "gender:lexicon")
("presidente" "noun" "presidente" "presidentes" "m" "presidente" "gender:lexicon")
("problema" "noun" "problema" "problemas" "m" "problema" "gender:lexicon")
("programa" "noun" "programa" "programas" "m" "programa" "gender:lexicon")
("proyecto" "noun" "proyecto" "proyectos" "m" "proyecto" "gender:lexicon")
("puerta" "noun" "puerta" "puertas" "f" "puerta" "gender:lexicon")
("pájaro" "noun" "pájaro" "pájaros" "m" "pájaro" "gender:lexicon")
("razón" "noun" "razón" "razones" "f" "razón" "gender:lexicon")
("raíz" "noun" "raíz" "raíces" "f" "raíz" "gender:lexicon")
("recuerdo" "noun" "recuerdo" "recuerdos" "m" "recuerdo" "gender:lexicon")
("reforma" "noun" "reforma" "reformas" "f" "reforma" "gender:lexicon")
("región" "noun" "región" "regiones" "f" "región" "gender:lexicon")
("río" "noun" "río" "ríos" "m" "río" "gender:lexicon")
("semilla" "noun" "semilla" "semillas" "f" "semilla" "gender:lexicon")
("sendero" "noun" "sendero" "senderos" "m" "sendero" "gender:lexicon")
("señor" "noun" "señor" "señores" "m" "señor" "gender:lexicon")
("silencio" "noun" "silencio" "silencios" "m" "silencio" "gender:lexicon")
("silla" "noun" "silla" "sillas" "f" "silla" "gender:lexicon")
("sol" "noun" "sol" "soles" "m" "sol" "gender:lexicon")
("tarea" "noun" "tarea" "tareas" "f" "tarea" "gender:lexicon")
("tema" "noun" "tema" "temas" "m" "tema" "gender:lexicon")
("ti" "noun" "ti" "tis" "m" "ti" "gender:heuristic")
("tiempo" "noun" "tiempo" "tiempos" "m" "tiempo" "gender:lexicon")
("tipo" "noun" "tipo" "tipos" "m" "tipo" "gender:lexicon")
("todo" "noun" "todo" "todos" "m" "todo" "gender:heuristic")
("tormenta" "noun" "tormenta" "tormentas" "f" "tormenta" "gender:lexicon")
("trabajador" "noun" "trabajador" "trabajadores" "m" "trabajador" "gender:lexicon")
("tristeza" "noun" "tristeza" "tristezas" "f" "tristeza" "gender:lexicon")
("ventana" "noun" "ventana" "ventanas" "f" "ventana" "gender:lexicon")
("verdad" "noun" "verdad" "verdades" "f" "verdad" "gender:lexicon")
("viaje" "noun" "viaje" "viajes" "m" "viaje" "gender:lexicon")
("victoria" "noun" "victoria" "victorias" "f" "victoria" "gender:lexicon")
("vida" "noun" "vida" "vidas" "f" "vida" "gender:lexicon")
("viento" "noun" "viento" "vientos" "m" "viento" "gender:lexicon")
("voz" "noun" "voz" "voces" "f" "voz" "gender:lexicon")
("vuelta" "noun" "vuelta" "vueltas" "f" "vuelta" "gender:lexicon")
("árbol" "noun" "árbol" "árboles" "m" "árbol" "gender:lexicon")
;; -- adjectives (form0=masc-sg form1=fem-sg form2=masc-pl) ----------------
("alto" "adj" "alto" "alta" "altos" "alto" "lexicon")
("ambos" "adj" "ambos" "ambos" "ambos" "ambos" "rule")
("antiguo" "adj" "antiguo" "antigua" "antiguos" "antiguo" "lexicon")
("azul" "adj" "azul" "azul" "azules" "azul" "lexicon")
("bajo" "adj" "bajo" "baja" "bajos" "bajo" "lexicon")
("blanco" "adj" "blanco" "blanca" "blancos" "blanco" "lexicon")
("bondadoso" "adj" "bondadoso" "bondadosa" "bondadosos" "bondadoso" "lexicon")
("bonito" "adj" "bonito" "bonita" "bonitos" "bonito" "lexicon")
("bueno" "adj" "bueno" "buena" "buenos" "bueno" "lexicon")
("cansado" "adj" "cansado" "cansada" "cansados" "cansado" "lexicon")
("corto" "adj" "corto" "corta" "cortos" "corto" "lexicon")
("difícil" "adj" "difícil" "difícil" "difíciles" "difícil" "lexicon")
("dos" "adj" "dos" "dos" "dos" "dos" "rule")
("económico" "adj" "económico" "económica" "económicos" "económico" "lexicon")
("español" "adj" "español" "española" "españoles" "español" "lexicon")
("estrecho" "adj" "estrecho" "estrecha" "estrechos" "estrecho" "lexicon")
("fascinante" "adj" "fascinante" "fascinante" "fascinantes" "fascinante" "rule")
("feliz" "adj" "feliz" "feliz" "felices" "feliz" "lexicon")
("francés" "adj" "francés" "francesa" "franceses" "francés" "lexicon")
("frío" "adj" "frío" "fría" "fríos" "frío" "lexicon")
("fácil" "adj" "fácil" "fácil" "fáciles" "fácil" "lexicon")
("grande" "adj" "grande" "grande" "grandes" "grande" "lexicon")
("hermoso" "adj" "hermoso" "hermosa" "hermosos" "hermoso" "lexicon")
("importante" "adj" "importante" "importante" "importantes" "importante" "lexicon")
("inglés" "adj" "inglés" "inglesa" "ingleses" "inglés" "lexicon")
("largo" "adj" "largo" "larga" "largos" "largo" "lexicon")
("lento" "adj" "lento" "lenta" "lentos" "lento" "lexicon")
("malo" "adj" "malo" "mala" "malos" "malo" "lexicon")
("mucho" "adj" "mucho" "mucha" "muchos" "mucho" "lexicon")
("necesario" "adj" "necesario" "necesaria" "necesarios" "necesario" "lexicon")
("negro" "adj" "negro" "negra" "negros" "negro" "lexicon")
("nuevo" "adj" "nuevo" "nueva" "nuevos" "nuevo" "lexicon")
("olvidado" "adj" "olvidado" "olvidada" "olvidados" "olvidado" "lexicon")
("oscuro" "adj" "oscuro" "oscura" "oscuros" "oscuro" "lexicon")
("pequeño" "adj" "pequeño" "pequeña" "pequeños" "pequeño" "lexicon")
("político" "adj" "político" "política" "políticos" "político" "lexicon")
("posible" "adj" "posible" "posible" "posibles" "posible" "lexicon")
("rojo" "adj" "rojo" "roja" "rojos" "rojo" "lexicon")
("rápido" "adj" "rápido" "rápida" "rápidos" "rápido" "lexicon")
("sabio" "adj" "sabio" "sabia" "sabios" "sabio" "lexicon")
("silencioso" "adj" "silencioso" "silenciosa" "silenciosos" "silencioso" "lexicon")
("social" "adj" "social" "social" "sociales" "social" "lexicon")
("trabajador" "adj" "trabajador" "trabajadora" "trabajadores" "trabajador" "lexicon")
("triste" "adj" "triste" "triste" "tristes" "triste" "rule")
("verde" "adj" "verde" "verde" "verdes" "verde" "lexicon")
("viejo" "adj" "viejo" "vieja" "viejos" "viejo" "lexicon")
)
-262
View File
@@ -1,262 +0,0 @@
# -*- coding: utf-8 -*-
"""gen_elp_es.py — emit the ELP (.el) port artifacts for Spanish.
Mirrors gen_elp_en.py. Produces:
vocabulary-es.el real generated vocabulary in the established schema
[lemma, pos, form0, form1, form2, en_translation, semantic_hint]
(same schema as vocabulary-got.el / vocabulary-en.el;
UniMorph spa lineage).
Verbs : form0=present-ind-3sg form1=preterite-3sg form2=past-participle
Nouns : form0=singular form1=plural form2=REAL gender (m/f, lexicon)
Adjs : form0=masc-sg form1=fem-sg form2=masc-pl
lang_profile_es.el the Spanish profile with the flags the realizer keys on.
Every form is generated by morphology_es_full (real UniMorph lexicon, not
hand-typed), so the .el vocabulary is honest and reproduces the forms the
realizer used. CRITICAL (coordinator quality bar): noun gender in form2 is the
REAL per-lemma lexicon gender (N;FEM/MASC), NOT an ending heuristic — this is
what kills the 'el mano / la día' masculine-default error class.
"""
import morphology_es_full as M
from test_set_es import TESTS
from held_out_es import HELD
# ── core closed class + common content lemmas so the vocab is usable beyond the
# validated sentences ──────────────────────────────────────────────────────
_CORE_VERBS = ["ser", "estar", "haber", "tener", "hacer", "ir", "ver", "dar",
"saber", "poder", "querer", "venir", "decir", "poner", "salir",
"hablar", "comer", "vivir", "trabajar", "estudiar", "llegar",
"pasar", "deber", "parecer", "quedar", "creer", "dejar", "llevar",
"encontrar", "llamar", "pensar", "volver", "conocer", "sentir",
"contar", "empezar", "buscar", "esperar", "existir", "entrar",
"escribir", "perder", "producir", "recordar", "morir", "nacer",
"abrir", "escapar", "soñar", "amar", "caer", "leer", "oír"]
_CORE_NOUNS = ["tiempo", "persona", "año", "día", "mano", "mundo", "vida",
"hombre", "mujer", "parte", "casa", "país", "problema", "programa",
"tema", "mapa", "agua", "foto", "moto", "ciudad", "libertad",
"canción", "nación", "flor", "color", "amor", "señor", "viaje",
"paisaje", "gato", "perro", "libro", "mesa", "silla", "noche",
"luz", "voz", "pez", "raíz", "crisis", "sol", "luna", "mar",
"corazón", "flor", "árbol", "camino", "puerta", "ventana"]
_CORE_ADJS = ["bueno", "malo", "nuevo", "viejo", "grande", "pequeño", "alto",
"bajo", "largo", "corto", "feliz", "triste", "fácil", "difícil",
"rápido", "lento", "hermoso", "económico", "político", "social",
"azul", "rojo", "verde", "blanco", "negro", "español", "francés",
"inglés", "importante", "posible", "necesario", "trabajador"]
# closed-class function words. Contractions (del/al) and the government notes
# are the coordinator's quality bar (mandatory contraction; verb-prep govt).
_FUNCTION = [
# articles (gender/number agreement is in morphology; these are citation)
("el", "det", "el", "los", "m", "the", "definite article m.sg"),
("la", "det", "la", "las", "f", "the", "definite article f.sg"),
("un", "det", "un", "unos","m", "a", "indefinite article m.sg"),
("una", "det", "una", "unas","f", "a", "indefinite article f.sg"),
# MANDATORY CONTRACTIONS (prep + el) — del / al
("del", "contraction", "del", "", "", "of the", "de + el (mandatory contraction)"),
("al", "contraction", "al", "", "", "to the", "a + el (mandatory contraction)"),
# demonstratives
("este", "dem", "este", "estos", "m", "this", "proximal dem m"),
("esta", "dem", "esta", "estas", "f", "this", "proximal dem f"),
# negation (SACRED — polarity never dropped)
("no", "neg", "no", "", "", "not/no", "sentential negator (preverbal)"),
("ninguno", "det", "ningún", "ninguna", "", "none", "negative determiner (apocope ningún m.sg)"),
# conjunctions
("y", "conj", "y", "e", "", "and", "coordinator (e before i-/hi-)"),
("o", "conj", "o", "u", "", "or", "coordinator (u before o-/ho-)"),
("pero", "conj", "pero", "", "", "but", "adversative coordinator"),
("que", "conj", "que", "", "", "that", "complementizer / relative"),
("si", "conj", "si", "", "", "if", "conditional subordinator"),
("porque", "conj", "porque", "", "", "because", "causal subordinator"),
("cuando", "conj", "cuando", "", "", "when", "temporal subordinator"),
# prepositions (government: verbs select these; contraction with el applies to a/de)
("a", "prep", "a", "", "", "to", "dir-obj (personal a) / dative / allative; a+el=al"),
("de", "prep", "de", "", "", "of/from", "genitive/ablative government; de+el=del"),
("en", "prep", "en", "", "", "in/on", "locative"),
("con", "prep", "con", "", "", "with", "comitative"),
("por", "prep", "por", "", "", "by/for", "passive agent / cause"),
("para", "prep", "para", "", "", "for", "purpose/benefactive"),
("contra", "prep", "contra", "", "", "against", "adversative government (protestar contra)"),
("sin", "prep", "sin", "", "", "without", "privative"),
# subject pronouns
("yo", "pron", "yo", "me", "mi", "I", "1sg subj/obj/poss"),
("", "pron", "", "te", "tu", "you", "2sg informal"),
("usted", "pron", "usted", "lo", "su", "you", "2sg formal (3sg agreement)"),
("él", "pron", "él", "lo", "su", "he", "3sg m subj/DO-clitic/poss"),
("ella", "pron", "ella", "la", "su", "she", "3sg f subj/DO-clitic/poss"),
("nosotros", "pron", "nosotros", "nos", "nuestro", "we", "1pl"),
("vosotros", "pron", "vosotros", "os", "vuestro", "you", "2pl informal"),
("ellos", "pron", "ellos", "los", "su", "they", "3pl m"),
("ellas", "pron", "ellas", "las", "su", "they", "3pl f"),
# indirect-object clitics
("le", "clitic", "le", "les", "", "to-him/her", "dative clitic 3sg/3pl (->se before lo/la)"),
("se", "clitic", "se", "se", "", "himself/-self", "reflexive / spurious-se (le+lo->se lo)"),
]
def _walk_collect(spec, verbs, nouns, adjs):
"""Recursively collect verb/noun/adj lemmas from a semantic spec."""
if isinstance(spec, dict):
if spec.get("pred"):
verbs.add(spec["pred"])
if spec.get("noun"):
nouns.add(spec["noun"])
if spec.get("adj"):
adjs.add(spec["adj"])
if spec.get("superlative"):
adjs.add(spec["superlative"])
if spec.get("from_adj"):
adjs.add(spec["from_adj"])
# adjs: [{"lemma":..,"pos":..}] | ["lemma", ..]
for a in spec.get("adjs", []) or []:
adjs.add(a["lemma"] if isinstance(a, dict) else a)
for a in spec.get("adj_coord", []) or []:
adjs.add(a["lemma"] if isinstance(a, dict) else a)
if isinstance(spec.get("pcomp"), dict):
pc = spec["pcomp"]
if pc.get("adj"):
adjs.add(pc["adj"])
for a in pc.get("adj_coord", []) or []:
adjs.add(a["lemma"] if isinstance(a, dict) else a)
for v in spec.values():
_walk_collect(v, verbs, nouns, adjs)
elif isinstance(spec, list):
for it in spec:
_walk_collect(it, verbs, nouns, adjs)
def _collect_from_specs():
verbs, nouns, adjs = set(), set(), set()
for t in TESTS + HELD:
_walk_collect(t["spec"], verbs, nouns, adjs)
return verbs, nouns, adjs
def _esc(s):
return str(s).replace('"', '\\"')
def _row(fields):
return " (" + " ".join(f'"{_esc(f)}"' for f in fields) + ")"
def emit_vocabulary(path):
v_specs, n_specs, a_specs = _collect_from_specs()
verbs = sorted(set(_CORE_VERBS) | v_specs)
nouns = sorted(set(_CORE_NOUNS) | n_specs)
adjs = sorted(set(_CORE_ADJS) | a_specs)
lines = [
";;; vocabulary-es.el — Spanish vocabulary for ELP surface realization.",
";;; Schema: (lemma pos form0 form1 form2 en_translation semantic_hint)",
";;; Source: UniMorph Spanish (github.com/unimorph/spa, CC-BY-SA 3.0),",
";;; generated by gen_elp_es.py via morphology_es_full (real forms).",
";;; Verbs: form0=present-ind-3sg form1=preterite-3sg form2=past-participle",
";;; Nouns: form0=singular form1=plural form2=REAL gender (m/f, from lexicon —",
";;; NOT an ending heuristic; this is what kills 'el mano'/'la día' errors)",
";;; Adjs : form0=masc-sg form1=fem-sg form2=masc-pl",
"",
"(vocabulary-es",
"",
" ;; -- function / closed class (incl. mandatory contractions del/al) --------",
]
for f in _FUNCTION:
lines.append(_row(f))
lines.append("")
lines.append(" ;; -- verbs (form0=pres-3sg form1=pret-3sg form2=past-participle) -----------")
for lem in verbs:
f0, c0 = M.conjugate(lem, "ind", "present", "third", "singular")
f1, c1 = M.conjugate(lem, "ind", "preterite", "third", "singular")
pp, cp = M.participle(lem)
vclass = lem[-2:] if lem[-2:] in ("ar", "er", "ir") else "ar"
irr = "irregular" if (c0 == "lexicon" and pp in M._IRREG_PART.values()) or \
lem in ("ser", "estar", "ir", "haber", "tener", "hacer", "ver",
"dar", "saber", "poder", "querer", "venir", "decir",
"poner", "salir") else "regular"
lines.append(_row([lem, "verb", f0, f1, pp, lem, vclass + "/" + irr]))
lines.append("")
lines.append(" ;; -- nouns (form0=sg form1=pl form2=REAL gender m/f) ----------------------")
for lem in nouns:
sg, _ = M.inflect_noun(lem, "singular")
pl, _ = M.inflect_noun(lem, "plural")
g = M.noun_gender(lem)
# honesty flag: did gender come from the lexicon, or a heuristic fallback?
src = "lexicon" if (lem in M._NOUNS and M._NOUNS[lem].get("g")) else "heuristic"
lines.append(_row([lem, "noun", sg, pl, g, lem, "gender:" + src]))
lines.append("")
lines.append(" ;; -- adjectives (form0=masc-sg form1=fem-sg form2=masc-pl) ----------------")
for lem in adjs:
m_sg, _ = M.inflect_adj(lem, "m", "singular")
f_sg, _ = M.inflect_adj(lem, "f", "singular")
m_pl, _ = M.inflect_adj(lem, "m", "plural")
src = "lexicon" if lem in M._ADJS else "rule"
lines.append(_row([lem, "adj", m_sg, f_sg, m_pl, lem, src]))
lines.append("")
lines.append(")")
with open(path, "w", encoding="utf-8") as fh:
fh.write("\n".join(lines) + "\n")
return len(_FUNCTION) + len(verbs) + len(nouns) + len(adjs), len(verbs), len(nouns), len(adjs)
LANG_PROFILE = ''';;; lang_profile_es.el — Spanish language profile for ELP.
;;; Keys the realizer's construction switches. Mirrors lang_profile_en / _pt.
(lang_profile_es
(language "Spanish")
(iso639 "es")
(family "Romance")
;; -- core typology flags -------------------------------------------------
(pro-drop yes) ; subjects routinely dropped; agreement carries person
(obligatory-subject no)
(grammatical-gender yes) ; m/f on every noun; article+adjective AGREE
(gender-source lexicon); REAL per-noun gender from UniMorph — NOT a heuristic
(do-support no)
(subject-aux-inversion no) ; questions by intonation/punctuation, not inversion
(question-strategy intonation)
(article-selection "el/la/los/las un/una/unos/unas")
(stressed-a-rule yes) ; fem sg noun in stressed a-/ha- takes el/un (el agua)
(adjective-position postnominal) ; default post; a few prenominal + apocope
(adjective-agreement "gender+number")
(question-punct inverted) ; opening ¿ ¡ required
;; -- MANDATORY CONTRACTIONS (coordinator quality bar) --------------------
(contractions ((de el "del") (a el "al")))
(contraction-mandatory yes) ; 'de el'/'a el' MUST surface as del/al
;; -- verb / aspect system ------------------------------------------------
(verb-classes (ar er ir))
(tenses (present preterite imperfect future conditional))
(moods (ind sbjv imp))
(finite-agreement "person+number (6 slots)")
(perfect-aux "haber") ; haber + past participle (invariant -o)
(progressive-aux "estar") ; estar + gerund
(passive-aux "ser") ; ser + participle (agrees) + por-agent
(copula-split "ser/estar") ; permanent vs stage-level
(future "infinitive + é/ás/á/emos/éis/án")
;; -- clitics / government ------------------------------------------------
(object-clitics yes) ; me te lo la le nos os los las; proclisis/enclisis
(clitic-order "se II I III (le+lo -> se lo)")
(enclisis "imperative/infinitive/gerund + accent repair (dá+me+lo->dámelo)")
(verb-prep-government yes) ; verbs select prep (protestar+contra, escapar+de)
;; -- SACRED safety bar (shared with en/pt) -------------------------------
(negation-faithful yes)) ; polarity never dropped/inverted; unplaceable -> FLAG
'''
if __name__ == "__main__":
import sys
voc_path = sys.argv[1] if len(sys.argv) > 1 else "vocabulary-es.el"
lp_path = sys.argv[2] if len(sys.argv) > 2 else "lang_profile_es.el"
total, nv, nn, na = emit_vocabulary(voc_path)
with open(lp_path, "w", encoding="utf-8") as fh:
fh.write(LANG_PROFILE)
print(f"wrote {voc_path} ({total} entries: {len(_FUNCTION)} fn, {nv} verbs, {nn} nouns, {na} adjs)")
print(f"wrote {lp_path}")
print("lexicon:", M.lexicon_stats())
-100
View File
@@ -1,100 +0,0 @@
# -*- coding: utf-8 -*-
"""gen_parity_es.py — emit an El parity program that checks the .el Spanish
morphology against the validated Python (UniMorph-backed) realizer.
Python is the ORACLE. For every lemma in the held-out inventory we embed the
Python-produced form, call the corresponding .el function, and the El program
prints PASS/FAIL per category. Aggregation is done in bash (grep -c), so no
El-side mutable counters are needed.
Categories:
Vpres verb present-ind-3sg es_conjugate(v,present,third,singular)
Vpret verb preterite-3sg es_conjugate(v,past,third,singular)
Vpart past participle es_participle(v)
Npl noun plural es_pluralize(n)
Gheur noun gender HEURISTIC es_gender(n) [exposes the bug]
Aheur def article via heuristic es_agree_article(n,true,sg) [inherits the bug]
Avocab def article via REAL g es_article_for_gender(realg,...) [the fix]
Ampl adj masc-plural es_inflect_adj(a,m,plural)
Afsg adj fem-singular es_inflect_adj(a,f,singular)
Ctr contraction del/al es_contract(prep, np)
"""
import morphology_es_full as M
import realizer_es as R
from gen_elp_es import _collect_from_specs, _CORE_VERBS, _CORE_NOUNS, _CORE_ADJS
def _esc(s):
return str(s).replace('\\', '\\\\').replace('"', '\\"')
def _check(cat, lemma, el_call, expected):
return (f' es_check("{cat}", "{_esc(lemma)}", {el_call}, "{_esc(expected)}")')
def main(out_path):
v_specs, n_specs, a_specs = _collect_from_specs()
verbs = sorted(set(_CORE_VERBS) | v_specs)
nouns = sorted(set(_CORE_NOUNS) | n_specs)
adjs = sorted(set(_CORE_ADJS) | a_specs)
lines = []
lines.append("// gen'd parity checks — Python oracle embedded, El functions called.")
lines.append("fn es_check(cat: String, lemma: String, got: String, exp: String) {")
lines.append(' if str_eq(got, exp) {')
lines.append(' println("PASS " + cat)')
lines.append(' } else {')
lines.append(' println("FAIL " + cat + " " + lemma + " got=" + got + " exp=" + exp)')
lines.append(' }')
lines.append("}")
lines.append("")
lines.append("fn es_parity() {")
# verbs
for v in verbs:
p0 = M.conjugate(v, "ind", "present", "third", "singular")[0]
p1 = M.conjugate(v, "ind", "preterite", "third", "singular")[0]
pp = M.participle(v)[0]
lines.append(_check("Vpres", v, f'es_conjugate("{_esc(v)}", "present", "third", "singular")', p0))
lines.append(_check("Vpret", v, f'es_conjugate("{_esc(v)}", "past", "third", "singular")', p1))
lines.append(_check("Vpart", v, f'es_participle("{_esc(v)}")', pp))
# nouns
for n in nouns:
pl = M.inflect_noun(n, "plural")[0]
g = M.noun_gender(n) # REAL lexicon gender
art = R._article(g, "singular", "def", n) # oracle article from real gender
lines.append(_check("Npl", n, f'es_pluralize("{_esc(n)}")', pl))
lines.append(_check("Gheur", n, f'es_gender("{_esc(n)}")', g))
lines.append(_check("Aheur", n, f'es_agree_article("{_esc(n)}", "true", "singular")', art))
lines.append(_check("Avocab", n, f'es_article_for_gender("{_esc(g)}", "{_esc(n)}", "true", "singular")', art))
# adjectives
for a in adjs:
mpl = M.inflect_adj(a, "m", "plural")[0]
fsg = M.inflect_adj(a, "f", "singular")[0]
lines.append(_check("Ampl", a, f'es_inflect_adj("{_esc(a)}", "m", "plural")', mpl))
lines.append(_check("Afsg", a, f'es_inflect_adj("{_esc(a)}", "f", "singular")', fsg))
# contractions (mandatory)
ctr_cases = [("de", "el día"), ("a", "el hombre"), ("de", "el mundo"),
("a", "el país"), ("en", "el mar"), ("de", "el año"),
("a", "la casa"), ("de", "la ciudad")]
for prep, np in ctr_cases:
exp = R._contract(prep, np)
lines.append(_check("Ctr", prep + "+" + np, f'es_contract("{_esc(prep)}", "{_esc(np)}")', exp))
lines.append("}")
lines.append("")
lines.append("fn main() {")
lines.append(" es_parity()")
lines.append("}")
with open(out_path, "w", encoding="utf-8") as fh:
fh.write("\n".join(lines) + "\n")
print(f"wrote {out_path} ({len(verbs)} verbs, {len(nouns)} nouns, {len(adjs)} adjs)")
if __name__ == "__main__":
import sys
main(sys.argv[1] if len(sys.argv) > 1 else "parity_es_checks.el")
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+105 -254
View File
@@ -10,9 +10,6 @@ el_val_t query_param(el_val_t path, el_val_t key);
el_val_t query_int(el_val_t path, el_val_t key, el_val_t default_val);
el_val_t extract_id(el_val_t path, el_val_t prefix);
el_val_t route_stats(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_act_stats(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_text_health(el_val_t method, el_val_t path, el_val_t body);
el_val_t persist_canonical(void);
el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_get_node(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_scan_nodes(el_val_t method, el_val_t path, el_val_t body);
@@ -20,29 +17,21 @@ el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_search(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_create_edges_batch(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_neighbors(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_strengthen(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_create_ise(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_save(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_load(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_health(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_embed_backfill(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_load_merge(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_capture_knowledge(el_val_t method, el_val_t path, el_val_t body);
el_val_t route_similarity(el_val_t method, el_val_t path, el_val_t body);
el_val_t check_auth_ok(el_val_t method, el_val_t body);
el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body);
el_val_t bind_raw;
el_val_t bind_str;
el_val_t port;
el_val_t data_dir_raw;
el_val_t data_dir;
el_val_t snapshot_path;
el_val_t boot_snap;
el_val_t parse_port(el_val_t bind) {
el_val_t colon = str_index_of(bind, EL_STR(":"));
@@ -121,40 +110,17 @@ el_val_t route_stats(el_val_t method, el_val_t path, el_val_t body) {
return 0;
}
el_val_t route_act_stats(el_val_t method, el_val_t path, el_val_t body) {
return engram_act_stats_json();
return 0;
}
el_val_t route_text_health(el_val_t method, el_val_t path, el_val_t body) {
return engram_text_health_json();
return 0;
}
el_val_t persist_canonical(void) {
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_1 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_1 = (EL_STR("/tmp/engram")); } else { _if_result_1 = (dir_raw); } _if_result_1; });
return engram_save(el_str_concat(dir, EL_STR("/snapshot.json")));
return 0;
}
el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body) {
el_val_t content = json_get_string(body, EL_STR("content"));
el_val_t nt_raw = json_get_string(body, EL_STR("node_type"));
el_val_t node_type = ({ el_val_t _if_result_2 = 0; if (str_eq(nt_raw, EL_STR(""))) { _if_result_2 = (EL_STR("Memory")); } else { _if_result_2 = (nt_raw); } _if_result_2; });
el_val_t sal_present = json_get_raw(body, EL_STR("salience"));
el_val_t salience = ({ el_val_t _if_result_3 = 0; if (str_eq(sal_present, EL_STR(""))) { _if_result_3 = (el_from_float(0.5)); } else { _if_result_3 = (json_get_float(body, EL_STR("salience"))); } _if_result_3; });
el_val_t label_raw = json_get_string(body, EL_STR("label"));
el_val_t label = ({ el_val_t _if_result_4 = 0; if (str_eq(label_raw, EL_STR(""))) { _if_result_4 = (content); } else { _if_result_4 = (label_raw); } _if_result_4; });
el_val_t imp_present = json_get_raw(body, EL_STR("importance"));
el_val_t importance = ({ el_val_t _if_result_5 = 0; if (str_eq(imp_present, EL_STR(""))) { _if_result_5 = (el_from_float(0.5)); } else { _if_result_5 = (json_get_float(body, EL_STR("importance"))); } _if_result_5; });
el_val_t conf_present = json_get_raw(body, EL_STR("confidence"));
el_val_t confidence = ({ el_val_t _if_result_6 = 0; if (str_eq(conf_present, EL_STR(""))) { _if_result_6 = (el_from_float(1.0)); } else { _if_result_6 = (json_get_float(body, EL_STR("confidence"))); } _if_result_6; });
el_val_t tier_raw = json_get_string(body, EL_STR("tier"));
el_val_t tier = ({ el_val_t _if_result_7 = 0; if (str_eq(tier_raw, EL_STR(""))) { _if_result_7 = (EL_STR("Working")); } else { _if_result_7 = (tier_raw); } _if_result_7; });
el_val_t tags = json_get_string(body, EL_STR("tags"));
el_val_t id = engram_node_full(content, node_type, label, salience, importance, confidence, tier, tags);
el_val_t saved = persist_canonical();
el_val_t node_type = json_get_string(body, EL_STR("node_type"));
if (str_eq(node_type, EL_STR(""))) {
node_type = EL_STR("Memory");
}
el_val_t salience = json_get_float(body, EL_STR("salience"));
if (salience == el_from_float(0.0)) {
salience = el_from_float(0.5);
}
el_val_t id = engram_node(content, node_type, salience);
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"id\":\""), id), EL_STR("\",\"content\":\"")), content), EL_STR("\",\"node_type\":\"")), node_type), EL_STR("\"}"));
return 0;
}
@@ -180,9 +146,11 @@ el_val_t route_scan_nodes(el_val_t method, el_val_t path, el_val_t body) {
}
el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body) {
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_8 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_8 = (EL_STR("/tmp/engram")); } else { _if_result_8 = (dir_raw); } _if_result_8; });
el_val_t snap_path = el_str_concat(dir, EL_STR("/.scan-export.json"));
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
if (str_eq(dir, EL_STR(""))) {
dir = EL_STR("/tmp/engram");
}
el_val_t snap_path = el_str_concat(dir, EL_STR("/snapshot.json"));
engram_save(snap_path);
el_val_t snap = fs_read(snap_path);
if (str_eq(snap, EL_STR(""))) {
@@ -197,22 +165,36 @@ el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body) {
}
el_val_t route_search(el_val_t method, el_val_t path, el_val_t body) {
el_val_t q = ({ el_val_t _if_result_9 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_9 = (query_param(path, EL_STR("q"))); } else { _if_result_9 = (json_get_string(body, EL_STR("query"))); } _if_result_9; });
el_val_t lim_url = query_int(path, EL_STR("limit"), 0);
el_val_t lim_body = json_get_int(body, EL_STR("limit"));
el_val_t lim_either = ({ el_val_t _if_result_10 = 0; if ((lim_url > 0)) { _if_result_10 = (lim_url); } else { _if_result_10 = (lim_body); } _if_result_10; });
el_val_t limit = ({ el_val_t _if_result_11 = 0; if ((lim_either > 0)) { _if_result_11 = (lim_either); } else { _if_result_11 = (20); } _if_result_11; });
el_val_t q = EL_STR("");
if (str_eq(method, EL_STR("GET"))) {
q = query_param(path, EL_STR("q"));
} else {
q = json_get_string(body, EL_STR("query"));
}
el_val_t limit = query_int(path, EL_STR("limit"), 20);
if (limit == 0) {
limit = json_get_int(body, EL_STR("limit"));
}
if (limit == 0) {
limit = 20;
}
return engram_search_json(q, limit);
return 0;
}
el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body) {
el_val_t q = ({ el_val_t _if_result_12 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_12 = (query_param(path, EL_STR("q"))); } else { _if_result_12 = (json_get_string(body, EL_STR("query"))); } _if_result_12; });
if (str_eq(q, EL_STR(""))) {
return err_json(EL_STR("missing query"));
el_val_t q = EL_STR("");
el_val_t depth = 3;
if (str_eq(method, EL_STR("GET"))) {
q = query_param(path, EL_STR("q"));
depth = query_int(path, EL_STR("depth"), 3);
} else {
q = json_get_string(body, EL_STR("query"));
el_val_t bd = json_get_int(body, EL_STR("depth"));
if (bd > 0) {
depth = bd;
}
}
el_val_t d_raw = ({ el_val_t _if_result_13 = 0; if (str_eq(method, EL_STR("GET"))) { _if_result_13 = (query_int(path, EL_STR("depth"), 3)); } else { _if_result_13 = (json_get_int(body, EL_STR("depth"))); } _if_result_13; });
el_val_t depth = ({ el_val_t _if_result_14 = 0; if ((d_raw > 0)) { _if_result_14 = (d_raw); } else { _if_result_14 = (3); } _if_result_14; });
return el_str_concat(el_str_concat(EL_STR("{\"results\":"), engram_activate_json(q, depth)), EL_STR("}"));
return 0;
}
@@ -220,51 +202,19 @@ el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body) {
el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body) {
el_val_t from_id = json_get_string(body, EL_STR("from_id"));
el_val_t to_id = json_get_string(body, EL_STR("to_id"));
el_val_t rel_raw = json_get_string(body, EL_STR("relation"));
el_val_t relation = ({ el_val_t _if_result_15 = 0; if (str_eq(rel_raw, EL_STR(""))) { _if_result_15 = (EL_STR("associates")); } else { _if_result_15 = (rel_raw); } _if_result_15; });
el_val_t w_present = json_get_raw(body, EL_STR("weight"));
el_val_t weight = ({ el_val_t _if_result_16 = 0; if (str_eq(w_present, EL_STR(""))) { _if_result_16 = (el_from_float(0.5)); } else { _if_result_16 = (json_get_float(body, EL_STR("weight"))); } _if_result_16; });
el_val_t relation = json_get_string(body, EL_STR("relation"));
if (str_eq(relation, EL_STR(""))) {
relation = EL_STR("associates");
}
el_val_t weight = json_get_float(body, EL_STR("weight"));
if (weight == el_from_float(0.0)) {
weight = el_from_float(0.5);
}
engram_connect(from_id, to_id, weight, relation);
el_val_t saved = persist_canonical();
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"from_id\":\""), from_id), EL_STR("\",\"to_id\":\"")), to_id), EL_STR("\",\"relation\":\"")), relation), EL_STR("\"}"));
return 0;
}
el_val_t route_create_edges_batch(el_val_t method, el_val_t path, el_val_t body) {
el_val_t arr = json_get_raw(body, EL_STR("edges"));
if (str_eq(arr, EL_STR(""))) {
return err_json(EL_STR("missing edges array"));
}
el_val_t n = json_array_len(arr);
if (n == 0) {
return EL_STR("{\"ok\":true,\"accepted\":0,\"skipped\":0}");
}
el_val_t i = 0;
el_val_t accepted = 0;
el_val_t skipped = 0;
while (i < n) {
el_val_t item = json_array_get(arr, i);
el_val_t from_id = json_get_string(item, EL_STR("from_id"));
el_val_t to_id = json_get_string(item, EL_STR("to_id"));
if (str_eq(from_id, EL_STR("")) || str_eq(to_id, EL_STR(""))) {
skipped = (skipped + 1);
} else {
el_val_t rel_raw = json_get_string(item, EL_STR("relation"));
el_val_t relation = ({ el_val_t _if_result_17 = 0; if (str_eq(rel_raw, EL_STR(""))) { _if_result_17 = (EL_STR("associates")); } else { _if_result_17 = (rel_raw); } _if_result_17; });
el_val_t w_present = json_get_raw(item, EL_STR("weight"));
el_val_t weight = ({ el_val_t _if_result_18 = 0; if (str_eq(w_present, EL_STR(""))) { _if_result_18 = (el_from_float(0.5)); } else { _if_result_18 = (json_get_float(item, EL_STR("weight"))); } _if_result_18; });
engram_connect(from_id, to_id, weight, relation);
accepted = (accepted + 1);
}
i = (i + 1);
}
if (accepted > 0) {
el_val_t saved = persist_canonical();
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"accepted\":"), int_to_str(accepted)), EL_STR(",\"skipped\":")), int_to_str(skipped)), EL_STR("}"));
return 0;
}
el_val_t route_neighbors(el_val_t method, el_val_t path, el_val_t body) {
el_val_t id = extract_id(path, EL_STR("/api/neighbors/"));
if (str_eq(id, EL_STR(""))) {
@@ -281,7 +231,6 @@ el_val_t route_strengthen(el_val_t method, el_val_t path, el_val_t body) {
return err_json(EL_STR("missing node_id"));
}
engram_strengthen(id);
el_val_t saved = persist_canonical();
return ok_json();
return 0;
}
@@ -292,83 +241,11 @@ el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body) {
return err_json(EL_STR("missing id"));
}
engram_forget(id);
el_val_t saved = persist_canonical();
return ok_json();
return 0;
}
el_val_t route_save(el_val_t method, el_val_t path, el_val_t body) {
el_val_t p_raw = json_get_string(body, EL_STR("path"));
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_19 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_19 = (EL_STR("/tmp/engram")); } else { _if_result_19 = (dir_raw); } _if_result_19; });
el_val_t p = ({ el_val_t _if_result_20 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_20 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_20 = (p_raw); } _if_result_20; });
el_val_t sv = engram_save(p);
el_val_t sv_ok = ({ el_val_t _if_result_21 = 0; if ((sv == 0)) { _if_result_21 = (EL_STR("false")); } else { _if_result_21 = (EL_STR("true")); } _if_result_21; });
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("{\"ok\":"), sv_ok), EL_STR(",\"path\":\"")), p), EL_STR("\",\"node_count\":")), int_to_str(engram_node_count())), EL_STR(",\"edge_count\":")), int_to_str(engram_edge_count())), EL_STR("}"));
return 0;
}
el_val_t route_load(el_val_t method, el_val_t path, el_val_t body) {
el_val_t p_raw = json_get_string(body, EL_STR("path"));
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_22 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_22 = (EL_STR("/tmp/engram")); } else { _if_result_22 = (dir_raw); } _if_result_22; });
el_val_t p = ({ el_val_t _if_result_23 = 0; if (str_eq(p_raw, EL_STR(""))) { _if_result_23 = (el_str_concat(dir, EL_STR("/snapshot.json"))); } else { _if_result_23 = (p_raw); } _if_result_23; });
el_val_t ld = engram_load(p);
el_val_t ld_ok = ({ el_val_t _if_result_24 = 0; if ((ld == 0)) { _if_result_24 = (EL_STR("false")); } else { _if_result_24 = (EL_STR("true")); } _if_result_24; });
el_val_t nc_after = engram_node_count();
el_val_t hollow = ({ el_val_t _if_result_25 = 0; if ((nc_after == 0)) { _if_result_25 = (EL_STR("true")); } else { _if_result_25 = (EL_STR("false")); } _if_result_25; });
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("{\"ok\":"), ld_ok), EL_STR(",\"path\":\"")), p), EL_STR("\",\"node_count\":")), int_to_str(nc_after)), EL_STR(",\"edge_count\":")), int_to_str(engram_edge_count())), EL_STR(",\"hollow\":")), hollow), EL_STR("}"));
return 0;
}
el_val_t route_health(el_val_t method, el_val_t path, el_val_t body) {
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"status\":\"ok\",\"engine\":\"engram-runtime-native\",\"node_count\":"), int_to_str(engram_node_count())), EL_STR(",\"edge_count\":")), int_to_str(engram_edge_count())), EL_STR("}"));
return 0;
}
el_val_t route_embed_backfill(el_val_t method, el_val_t path, el_val_t body) {
el_val_t n = query_int(path, EL_STR("n"), 32);
el_val_t result = engram_embed_backfill(n);
el_val_t done = json_get_float(result, EL_STR("embedded"));
if (done > el_from_float(0.0)) {
el_val_t saved = persist_canonical();
}
return result;
return 0;
}
el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body) {
el_val_t dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
el_val_t dir = ({ el_val_t _if_result_26 = 0; if (str_eq(dir_raw, EL_STR(""))) { _if_result_26 = (EL_STR("/tmp/engram")); } else { _if_result_26 = (dir_raw); } _if_result_26; });
el_val_t snap_path = el_str_concat(dir, EL_STR("/.sync-export.json"));
engram_save(snap_path);
el_val_t snap = fs_read(snap_path);
if (str_eq(snap, EL_STR(""))) {
return err_json(EL_STR("sync export failed: snapshot unreadable"));
}
return snap;
return 0;
}
el_val_t route_load_merge(el_val_t method, el_val_t path, el_val_t body) {
el_val_t p = json_get_string(body, EL_STR("path"));
if (str_eq(p, EL_STR(""))) {
return err_json(EL_STR("path is required"));
}
if (str_eq(fs_read(p), EL_STR(""))) {
return err_json(EL_STR("file missing or empty"));
}
el_val_t before_n = engram_node_count();
el_val_t before_e = engram_edge_count();
engram_load_merge(p);
el_val_t added_n = (engram_node_count() - before_n);
el_val_t added_e = (engram_edge_count() - before_e);
el_val_t saved = persist_canonical();
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"nodes_added\":"), int_to_str(added_n)), EL_STR(",\"edges_added\":")), int_to_str(added_e)), EL_STR(",\"node_count\":")), int_to_str(engram_node_count())), EL_STR("}"));
return 0;
}
el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body) {
el_val_t route_create_ise(el_val_t method, el_val_t path, el_val_t body) {
el_val_t content = json_get_string(body, EL_STR("content"));
if (str_eq(content, EL_STR(""))) {
return err_json(EL_STR("missing content"));
@@ -377,55 +254,55 @@ el_val_t route_emit_ise(el_val_t method, el_val_t path, el_val_t body) {
el_val_t imp = el_from_float(0.3);
el_val_t conf = el_from_float(0.8);
el_val_t id = engram_node_full(content, EL_STR("InternalStateEvent"), EL_STR("state-event"), sal, imp, conf, EL_STR("Episodic"), EL_STR("[\"internal-state\",\"InternalStateEvent\"]"));
el_val_t ret_raw = env(EL_STR("ENGRAM_ISE_RETENTION_MS"));
el_val_t ret_ms = ({ el_val_t _if_result_27 = 0; if (str_eq(ret_raw, EL_STR(""))) { _if_result_27 = (172800000); } else { _if_result_27 = (str_to_int(ret_raw)); } _if_result_27; });
el_val_t pruned = engram_prune_telemetry(ret_ms);
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\",\"pruned\":")), int_to_str(pruned)), EL_STR("}"));
return 0;
}
el_val_t route_capture_knowledge(el_val_t method, el_val_t path, el_val_t body) {
el_val_t content = json_get_string(body, EL_STR("content"));
if (str_eq(content, EL_STR(""))) {
return err_json(EL_STR("missing content"));
}
el_val_t title = json_get_string(body, EL_STR("title"));
el_val_t label = ({ el_val_t _if_result_28 = 0; if (str_eq(title, EL_STR(""))) { _if_result_28 = (str_slice(content, 0, 60)); } else { _if_result_28 = (title); } _if_result_28; });
el_val_t category_raw = json_get_string(body, EL_STR("category"));
el_val_t category = ({ el_val_t _if_result_29 = 0; if (str_eq(category_raw, EL_STR(""))) { _if_result_29 = (EL_STR("other")); } else { _if_result_29 = (category_raw); } _if_result_29; });
el_val_t ktier_raw = json_get_string(body, EL_STR("tier"));
el_val_t ktier = ({ el_val_t _if_result_30 = 0; if (str_eq(ktier_raw, EL_STR(""))) { _if_result_30 = (EL_STR("note")); } else { _if_result_30 = (ktier_raw); } _if_result_30; });
el_val_t project = json_get_string(body, EL_STR("project"));
el_val_t tags_raw = json_get_raw(body, EL_STR("tags"));
el_val_t tags_base = ({ el_val_t _if_result_31 = 0; if (str_eq(tags_raw, EL_STR(""))) { _if_result_31 = (EL_STR("[]")); } else { _if_result_31 = (tags_raw); } _if_result_31; });
el_val_t base_len = str_len(tags_base);
el_val_t head = str_slice(tags_base, 0, (base_len - 1));
el_val_t sep = ({ el_val_t _if_result_32 = 0; if (str_eq(head, EL_STR("["))) { _if_result_32 = (EL_STR("")); } else { _if_result_32 = (EL_STR(",")); } _if_result_32; });
el_val_t safe_cat = str_replace(category, EL_STR("\""), EL_STR("'"));
el_val_t safe_tier = str_replace(ktier, EL_STR("\""), EL_STR("'"));
el_val_t safe_proj = str_replace(project, EL_STR("\""), EL_STR("'"));
el_val_t proj_tag = ({ el_val_t _if_result_33 = 0; if (str_eq(safe_proj, EL_STR(""))) { _if_result_33 = (EL_STR("")); } else { _if_result_33 = (el_str_concat(el_str_concat(EL_STR(",\"project:"), safe_proj), EL_STR("\""))); } _if_result_33; });
el_val_t tags = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(head, sep), EL_STR("\"category:")), safe_cat), EL_STR("\",\"tier:")), safe_tier), EL_STR("\"")), proj_tag), EL_STR("]"));
el_val_t sal = el_from_float(0.5);
el_val_t imp = el_from_float(0.5);
el_val_t conf = el_from_float(0.9);
el_val_t id = engram_node_full(content, EL_STR("Knowledge"), label, sal, imp, conf, EL_STR("Semantic"), tags);
el_val_t saved = persist_canonical();
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"id\":\""), id), EL_STR("\"}"));
return 0;
}
el_val_t route_similarity(el_val_t method, el_val_t path, el_val_t body) {
el_val_t a = query_param(path, EL_STR("a"));
el_val_t b = query_param(path, EL_STR("b"));
if (str_eq(a, EL_STR(""))) {
return err_json(EL_STR("missing a"));
el_val_t route_sync(el_val_t method, el_val_t path, el_val_t body) {
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
if (str_eq(dir, EL_STR(""))) {
dir = EL_STR("/tmp/engram");
}
if (str_eq(b, EL_STR(""))) {
return err_json(EL_STR("missing b"));
el_val_t snap_path = el_str_concat(dir, EL_STR("/sync-export.json"));
engram_save(snap_path);
el_val_t snap = fs_read(snap_path);
if (str_eq(snap, EL_STR(""))) {
return EL_STR("{\"nodes\":[],\"edges\":[]}");
}
el_val_t sim = engram_cosine_sim(a, b);
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"a\":\""), a), EL_STR("\",\"b\":\"")), b), EL_STR("\",\"cosine\":")), float_to_str(sim)), EL_STR("}"));
return snap;
return 0;
}
el_val_t route_save(el_val_t method, el_val_t path, el_val_t body) {
el_val_t p = json_get_string(body, EL_STR("path"));
if (str_eq(p, EL_STR(""))) {
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
if (str_eq(dir, EL_STR(""))) {
dir = EL_STR("/tmp/engram");
}
p = el_str_concat(dir, EL_STR("/snapshot.json"));
}
engram_save(p);
return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"path\":\""), p), EL_STR("\"}"));
return 0;
}
el_val_t route_load(el_val_t method, el_val_t path, el_val_t body) {
el_val_t p = json_get_string(body, EL_STR("path"));
if (str_eq(p, EL_STR(""))) {
el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR"));
if (str_eq(dir, EL_STR(""))) {
dir = EL_STR("/tmp/engram");
}
p = el_str_concat(dir, EL_STR("/snapshot.json"));
}
engram_load(p);
return ok_json();
return 0;
}
el_val_t route_health(el_val_t method, el_val_t path, el_val_t body) {
return EL_STR("{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}");
return 0;
}
@@ -452,24 +329,15 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
return route_health(method, path, body);
}
}
if (str_eq(method, EL_STR("POST")) && str_eq(clean, EL_STR("/api/neuron/state-events"))) {
return route_emit_ise(method, path, body);
if (str_eq(method, EL_STR("POST")) && str_starts_with(clean, EL_STR("/api/neuron/state-events"))) {
return route_create_ise(method, path, body);
}
if (!check_auth_ok(method, body)) {
return err_json(EL_STR("unauthorized"));
}
if (str_eq(method, EL_STR("POST")) && str_eq(clean, EL_STR("/api/neuron/knowledge/capture"))) {
return route_capture_knowledge(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/stats")) || str_eq(clean, EL_STR("/stats")))) {
return route_stats(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/act-stats")) || str_eq(clean, EL_STR("/act-stats")))) {
return route_act_stats(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/text-health")) || str_eq(clean, EL_STR("/text-health")))) {
return route_text_health(method, path, body);
}
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/nodes")) || str_eq(clean, EL_STR("/nodes")))) {
return route_create_node(method, path, body);
}
@@ -488,9 +356,6 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/edges")) || str_eq(clean, EL_STR("/edges")))) {
return route_create_edge(method, path, body);
}
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/edges/batch")) || str_eq(clean, EL_STR("/edges/batch")))) {
return route_create_edges_batch(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/neighbors/"))) {
return route_neighbors(method, path, body);
}
@@ -509,46 +374,32 @@ el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) {
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/strengthen")) || str_eq(clean, EL_STR("/strengthen")))) {
return route_strengthen(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/sync")) || str_eq(clean, EL_STR("/sync")))) {
return route_sync(method, path, body);
}
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/save")) || str_eq(clean, EL_STR("/save")))) {
return route_save(method, path, body);
}
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/load")) || str_eq(clean, EL_STR("/load")))) {
return route_load(method, path, body);
}
if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/load-merge")) || str_eq(clean, EL_STR("/load-merge")))) {
return route_load_merge(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && str_eq(clean, EL_STR("/api/sync"))) {
return route_sync(method, path, body);
}
if (str_eq(clean, EL_STR("/api/embed-backfill"))) {
return route_embed_backfill(method, path, body);
}
if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/similarity"))) {
return route_similarity(method, path, body);
}
return el_str_concat(el_str_concat(EL_STR("{\"error\":\"not found\",\"path\":\""), clean), EL_STR("\"}"));
return 0;
}
int main(int _argc, char** _argv) {
el_runtime_init_args(_argc, _argv);
bind_raw = env(EL_STR("ENGRAM_BIND"));
bind_str = ({ el_val_t _if_result_34 = 0; if (str_eq(bind_raw, EL_STR(""))) { _if_result_34 = (EL_STR(":8742")); } else { _if_result_34 = (bind_raw); } _if_result_34; });
bind_str = env(EL_STR("ENGRAM_BIND"));
if (str_eq(bind_str, EL_STR(""))) {
bind_str = EL_STR(":8742");
}
port = parse_port(bind_str);
data_dir_raw = env(EL_STR("ENGRAM_DATA_DIR"));
data_dir = ({ el_val_t _if_result_35 = 0; if (str_eq(data_dir_raw, EL_STR(""))) { _if_result_35 = (EL_STR("/tmp/engram")); } else { _if_result_35 = (data_dir_raw); } _if_result_35; });
data_dir = env(EL_STR("ENGRAM_DATA_DIR"));
if (str_eq(data_dir, EL_STR(""))) {
data_dir = EL_STR("/tmp/engram");
}
snapshot_path = el_str_concat(data_dir, EL_STR("/snapshot.json"));
engram_load(snapshot_path);
boot_snap = fs_read(snapshot_path);
if (!str_eq(boot_snap, EL_STR(""))) {
if (engram_node_count() == 0) {
println(EL_STR("[engram] WARNING: snapshot.json is non-empty but load produced 0 nodes \xe2\x80\x94 preserving copy at snapshot.failed-load.json"));
fs_write(el_str_concat(data_dir, EL_STR("/snapshot.failed-load.json")), boot_snap);
} else {
fs_write(el_str_concat(data_dir, EL_STR("/snapshot.boot-backup.json")), boot_snap);
}
}
println(EL_STR("[engram] runtime-native graph engine"));
println(el_str_concat(EL_STR("[engram] data_dir="), data_dir));
println(el_str_concat(EL_STR("[engram] node_count="), int_to_str(engram_node_count())));
+69 -426
View File
@@ -50,8 +50,12 @@ fn query_param(path: String, key: String) -> String {
if pos < 0 { return "" }
let after: String = str_slice(qs, pos + str_len(needle), str_len(qs))
let amp: Int = str_index_of(after, "&")
if amp < 0 { return after }
str_slice(after, 0, amp)
// SPEC-SEARCH-UPGRADE 2026-07-14: URL-decode the extracted value (%XX and
// '+' were previously passed through literally, so an encoded multi-word
// query arrived as junk tokens pre-existing GET-path defect, masked
// until search could actually rank multi-word queries).
if amp < 0 { return url_decode(after) }
url_decode(str_slice(after, 0, amp))
}
fn query_int(path: String, key: String, default_val: Int) -> Int {
@@ -76,112 +80,13 @@ fn route_stats(method: String, path: String, body: String) -> String {
engram_stats_json()
}
// route_act_stats GET /api/act-stats
// (2026-08-04 self-review) engram_act_stats_json() has existed since the
// 2026-07-27 review but was reachable ONLY through the soul daemon's heartbeat
// binding. Every activation-layer gauge WM evictions, breakthroughs, embedder
// breaker state, context drift, and now the Hebbian counters was therefore
// invisible unless the soul happened to be running and its ISEs were read back
// out of the store. Diagnosing the activation layer required a working soul,
// which is exactly backwards: the lower layer should be observable on its own.
// This review needed it to verify link formation and could not get at it. One
// line of plumbing, and the whole activation layer becomes directly diagnosable.
fn route_act_stats(method: String, path: String, body: String) -> String {
engram_act_stats_json()
}
// route_text_health GET /api/text-health
// (2026-08-08 self-review) The daily census half of the text-integrity gauge.
// Today's review found that the JSON parser had been replacing every \uXXXX
// escape with a literal '?' for at least two months: 3,119 of 4,081
// non-telemetry nodes (76%) were damaged, including the self traversal root
// and every values node, and NOTHING detected it because every gauge in the
// system measured whether the machinery was running, and none measured whether
// the text it carried was intact. No snapshot on disk predates the damage, so
// it cannot be undone; it can only be made impossible to repeat quietly.
//
// The parser is fixed. This route is the standing check: `damaged` should now
// hold flat at its historical floor and never climb. `write_damaged` (also on
// the heartbeat as txt_damaged) is the live regression signal non-zero means
// a write path is mangling text right now.
fn route_text_health(method: String, path: String, body: String) -> String {
engram_text_health_json()
}
// (2026-07-18 self-review) Scoping sweep: `let` inside an if-block creates an
// inner scope only it does NOT mutate the outer binding (documented with
// evidence in awareness.el, 2026-05-25). Every default/reassignment below used
// that broken pattern, so defaults never applied: nodes were created with
// node_type="" and salience=0.0, /api/search and /api/activate ALWAYS ran with
// q="" regardless of input, edges defaulted to relation=""/weight=0.0, and
// save/load with no "path" hit engram_save(""). Rewritten to the
// `let x = if cond { a } else { b }` expression form (the pattern the newer
// routes route_emit_ise/route_capture_knowledge already use correctly).
// persist_canonical save the canonical snapshot after a durable write.
//
// WHY (2026-07-22 self-review): the 2026-07-21 fix correctly stopped READ
// routes from writing the canonical snapshot.json but nothing was left
// that saved it on WRITE. Every mutation (node create, edge create,
// knowledge capture, forget, merge) lived only in RAM until someone POSTed
// /api/save manually; a process restart silently discarded everything since
// the last manual save. Observed live: two engram restarts during the
// 2026-07-22 review reverted the store to a ~17h-old snapshot, destroying
// same-day writes. Reads must never write the canonical; writes must always
// persist it. ISE telemetry is deliberately excluded (48h-pruned, loss-
// tolerant, ~2/min snapshotting the whole store per heartbeat is waste;
// any durable write that follows persists the pruning too).
fn persist_canonical() -> Int {
let dir_raw: String = env("ENGRAM_DATA_DIR")
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
// (2026-08-10 self-review) This returned a hardcoded 1, which made every
// caller's `let saved: Int = persist_canonical()` a dead variable six
// durable write paths each believed they had confirmation of a successful
// canonical persist and none of them had any. Propagate the real result.
return engram_save(dir + "/snapshot.json")
}
// INCOMPLETE-ROUTE FIX (2026-07-24 self-review): this route silently dropped
// label, importance, tier, and tags engram_node() defaults label to content
// and importance to 0.5, so every node created over HTTP lost its metadata.
// Observed live: the soul's boot-counter write-back landed with
// label="soul:boot_count:99" (content), importance 0.5, no tags. Honor the
// full field set via engram_node_full when any of them is supplied.
// PRESENCE-AWARE DEFAULTS (2026-08-01 self-review): the old pattern
// `if x == 0.0 { default }` made a legitimate 0.0 unrepresentable a caller
// setting salience/importance/weight to zero silently got 0.5. json_get_raw
// returns "" when the key is ABSENT and the raw token when present, so
// absence and zero are now distinguishable. Also: confidence was hardcoded
// to 1.0 regardless of input every HTTP-created node claimed full
// epistemic confidence. Now honored from the payload (default 1.0).
fn route_create_node(method: String, path: String, body: String) -> String {
let content: String = json_get_string(body, "content")
let nt_raw: String = json_get_string(body, "node_type")
let node_type: String = if str_eq(nt_raw, "") { "Memory" } else { nt_raw }
let sal_present: String = json_get_raw(body, "salience")
let salience: Float = if str_eq(sal_present, "") { 0.5 } else { json_get_float(body, "salience") }
let label_raw: String = json_get_string(body, "label")
let label: String = if str_eq(label_raw, "") { content } else { label_raw }
let imp_present: String = json_get_raw(body, "importance")
let importance: Float = if str_eq(imp_present, "") { 0.5 } else { json_get_float(body, "importance") }
let conf_present: String = json_get_raw(body, "confidence")
let confidence: Float = if str_eq(conf_present, "") { 1.0 } else { json_get_float(body, "confidence") }
let tier_raw: String = json_get_string(body, "tier")
let tier: String = if str_eq(tier_raw, "") { "Working" } else { tier_raw }
let tags: String = json_get_string(body, "tags")
// NO el_from_float WRAPPER (2026-08-01 self-review): salience/importance/
// confidence are already Float (el_val_t) values json_get_float and
// Float literals both encode. Wrapping them in el_from_float AGAIN
// reinterpreted the boxed bits as a raw double, producing garbage that
// failed engram_decode_score's range check and clamped every HTTP-created
// node to defaults (salience 0.9 in 0.5 stored; confidence 0.6 in → 1.0
// stored verified live). route_emit_ise always passed Floats bare and
// its 0.3/0.3/0.8 stored correctly; this call now does the same.
let id: String = engram_node_full(
content, node_type, label,
salience, importance, confidence,
tier, tags
)
let saved: Int = persist_canonical()
let node_type: String = json_get_string(body, "node_type")
if str_eq(node_type, "") { let node_type = "Memory" }
let salience: Float = json_get_float(body, "salience")
if salience == 0.0 { let salience = 0.5 }
let id: String = engram_node(content, node_type, salience)
"{\"id\":\"" + id + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\"}"
}
@@ -202,14 +107,13 @@ fn route_scan_nodes(method: String, path: String, body: String) -> String {
}
// route_scan_edges bulk export of all edges as a JSON array. Implemented
// via engram_save fs_read of a SCRATCH export path. (2026-07-21 self-review:
// previously this saved over the canonical snapshot.json on every GET if the
// process ever booted with a partial/empty store, the first read request
// clobbered the good snapshot. Read routes must never write the canonical path.)
// via engram_save fs_read of the canonical on-disk snapshot, which the
// runtime keeps in lockstep with the in-memory graph. Live against the
// running graph, not a stale export.
fn route_scan_edges(method: String, path: String, body: String) -> String {
let dir_raw: String = env("ENGRAM_DATA_DIR")
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
let snap_path: String = dir + "/.scan-export.json"
let dir: String = env("ENGRAM_DATA_DIR")
if str_eq(dir, "") { let dir = "/tmp/engram" }
let snap_path: String = dir + "/snapshot.json"
engram_save(snap_path)
let snap: String = fs_read(snap_path)
if str_eq(snap, "") { return "[]" }
@@ -222,88 +126,43 @@ fn route_scan_edges(method: String, path: String, body: String) -> String {
}
fn route_search(method: String, path: String, body: String) -> String {
let q: String = if str_eq(method, "GET") { query_param(path, "q") } else { json_get_string(body, "query") }
let lim_url: Int = query_int(path, "limit", 0)
let lim_body: Int = json_get_int(body, "limit")
let lim_either: Int = if lim_url > 0 { lim_url } else { lim_body }
let limit: Int = if lim_either > 0 { lim_either } else { 20 }
let q: String = ""
if str_eq(method, "GET") {
let q = query_param(path, "q")
} else {
let q = json_get_string(body, "query")
}
let limit: Int = query_int(path, "limit", 20)
if limit == 0 { let limit = json_get_int(body, "limit") }
if limit == 0 { let limit = 20 }
return engram_search_json(q, limit)
}
fn route_activate(method: String, path: String, body: String) -> String {
let q: String = if str_eq(method, "GET") { query_param(path, "q") } else { json_get_string(body, "query") }
// Guard: engram_activate with an empty query matches zero seeds, which
// zeroes ALL carried working-memory weights (documented in awareness.el
// perceive()). Never let an empty activation through to wipe WM.
if str_eq(q, "") { return err_json("missing query") }
let d_raw: Int = if str_eq(method, "GET") { query_int(path, "depth", 3) } else { json_get_int(body, "depth") }
let depth: Int = if d_raw > 0 { d_raw } else { 3 }
let q: String = ""
let depth: Int = 3
if str_eq(method, "GET") {
let q = query_param(path, "q")
let depth = query_int(path, "depth", 3)
} else {
let q = json_get_string(body, "query")
let bd: Int = json_get_int(body, "depth")
if bd > 0 { let depth = bd }
}
return "{\"results\":" + engram_activate_json(q, depth) + "}"
}
fn route_create_edge(method: String, path: String, body: String) -> String {
let from_id: String = json_get_string(body, "from_id")
let to_id: String = json_get_string(body, "to_id")
let rel_raw: String = json_get_string(body, "relation")
let relation: String = if str_eq(rel_raw, "") { "associates" } else { rel_raw }
// Presence-aware (2026-08-01): weight 0.0 is a legitimate edge weight
// (dormant association); only default when the key is absent.
let w_present: String = json_get_raw(body, "weight")
let weight: Float = if str_eq(w_present, "") { 0.5 } else { json_get_float(body, "weight") }
let relation: String = json_get_string(body, "relation")
if str_eq(relation, "") { let relation = "associates" }
let weight: Float = json_get_float(body, "weight")
if weight == 0.0 { let weight = 0.5 }
engram_connect(from_id, to_id, weight, relation)
let saved: Int = persist_canonical()
"{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + relation + "\"}"
}
// route_create_edges_batch POST /api/edges/batch {"edges":[{from_id,to_id,relation,weight}, ...]}
//
// WHY THIS EXISTS (2026-08-07 self-review). persist_canonical() writes the
// FULL canonical snapshot 60MB at current graph size and route_create_edge
// calls it once per edge. That is correct for the interactive one-edge case and
// ruinous for any bulk write: the soul's Hebbian consolidation path delivers
// ~14 associations per 8-minute heartbeat, which through the single-edge route
// would be ~840MB of disk writes per beat, ~150GB/day, to persist 14 edges.
//
// The fix is not to weaken durability it is to make the unit of durability
// the BATCH. Connect every edge, then snapshot exactly once. Same guarantee
// (nothing acknowledged is lost to a restart), 1/N the writes. Empty or
// malformed entries are skipped rather than aborting the batch: a consolidation
// payload is best-effort by design, and one bad id should not cost the other 13.
//
// Returns the accepted count so the caller can tell delivery from silence.
fn route_create_edges_batch(method: String, path: String, body: String) -> String {
let arr: String = json_get_raw(body, "edges")
if str_eq(arr, "") { return err_json("missing edges array") }
let n: Int = json_array_len(arr)
if n == 0 { return "{\"ok\":true,\"accepted\":0,\"skipped\":0}" }
let i: Int = 0
let accepted: Int = 0
let skipped: Int = 0
while i < n {
let item: String = json_array_get(arr, i)
let from_id: String = json_get_string(item, "from_id")
let to_id: String = json_get_string(item, "to_id")
if str_eq(from_id, "") || str_eq(to_id, "") {
let skipped = skipped + 1
} else {
let rel_raw: String = json_get_string(item, "relation")
let relation: String = if str_eq(rel_raw, "") { "associates" } else { rel_raw }
let w_present: String = json_get_raw(item, "weight")
let weight: Float = if str_eq(w_present, "") { 0.5 } else { json_get_float(item, "weight") }
engram_connect(from_id, to_id, weight, relation)
let accepted = accepted + 1
}
let i = i + 1
}
// ONE snapshot for the whole batch the entire point of this route.
// Skip it when nothing was accepted: an all-malformed payload must not
// trigger a 60MB write.
if accepted > 0 {
let saved: Int = persist_canonical()
}
return "{\"ok\":true,\"accepted\":" + int_to_str(accepted) + ",\"skipped\":" + int_to_str(skipped) + "}"
}
fn route_neighbors(method: String, path: String, body: String) -> String {
let id: String = extract_id(path, "/api/neighbors/")
if str_eq(id, "") { return err_json("missing id") }
@@ -315,7 +174,6 @@ fn route_strengthen(method: String, path: String, body: String) -> String {
let id: String = json_get_string(body, "node_id")
if str_eq(id, "") { return err_json("missing node_id") }
engram_strengthen(id)
let saved: Int = persist_canonical()
ok_json()
}
@@ -323,84 +181,33 @@ fn route_forget(method: String, path: String, body: String) -> String {
let id: String = extract_id(path, "/api/nodes/")
if str_eq(id, "") { return err_json("missing id") }
engram_forget(id)
let saved: Int = persist_canonical()
ok_json()
}
fn route_save(method: String, path: String, body: String) -> String {
let p_raw: String = json_get_string(body, "path")
let dir_raw: String = env("ENGRAM_DATA_DIR")
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw }
// (2026-08-10 self-review) engram_save returns 0 on an empty path and the
// route discarded it, so the response was a literal "ok":true regardless
// of whether anything was written. Report the actual result AND the counts
// that were supposed to have been written the same move that made
// route_health honest on 2026-08-01. A caller can now tell "saved 13k
// nodes" from "saved nothing and said ok".
let sv: Int = engram_save(p)
let sv_ok: String = if sv == 0 { "false" } else { "true" }
"{\"ok\":" + sv_ok + ",\"path\":\"" + p + "\",\"node_count\":" + int_to_str(engram_node_count()) + ",\"edge_count\":" + int_to_str(engram_edge_count()) + "}"
let p: String = json_get_string(body, "path")
if str_eq(p, "") {
let dir: String = env("ENGRAM_DATA_DIR")
if str_eq(dir, "") { let dir = "/tmp/engram" }
let p = dir + "/snapshot.json"
}
engram_save(p)
"{\"ok\":true,\"path\":\"" + p + "\"}"
}
fn route_load(method: String, path: String, body: String) -> String {
let p_raw: String = json_get_string(body, "path")
let dir_raw: String = env("ENGRAM_DATA_DIR")
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
let p: String = if str_eq(p_raw, "") { dir + "/snapshot.json" } else { p_raw }
// (2026-08-10 self-review) This was a stub response over the single most
// destructive operation in the server. engram_load returns 0 on an empty
// path, an unopenable file, a zero-length file, or malloc failure and
// this route answered ok_json() in every one of those cases.
//
// Precise failure shape (el_runtime.c:9890): the fopen guard runs BEFORE
// the store reset, so a MISSING path is genuinely safe it returns 0 with
// the graph intact. The dangerous case is a readable-but-malformed file:
// the reset loop frees every node and edge FIRST, then parses, so a
// truncated or non-snapshot JSON leaves a hollow store and the caller
// was told "ok":true. With 37 GB of stale dated snapshots sitting in the
// data dir as tempting restore targets, "restore reported success and
// silently emptied the graph" is a live risk, not a hypothetical one.
//
// Fix: surface the return value AND the resulting counts. node_count=0
// after a load is the unambiguous hollow-store signal (same convention
// route_health adopted 2026-08-01). Callers can now verify a restore
// instead of trusting it.
let ld: Int = engram_load(p)
let ld_ok: String = if ld == 0 { "false" } else { "true" }
let nc_after: Int = engram_node_count()
let hollow: String = if nc_after == 0 { "true" } else { "false" }
"{\"ok\":" + ld_ok + ",\"path\":\"" + p + "\",\"node_count\":" + int_to_str(nc_after) + ",\"edge_count\":" + int_to_str(engram_edge_count()) + ",\"hollow\":" + hollow + "}"
}
// (2026-08-01 self-review) Health previously returned a hardcoded literal
// it reported "ok" even when the snapshot failed to load and the store was
// empty. Now reports live counts so a monitor can distinguish "up and
// loaded" from "up and hollow" (node_count=0 after boot = failed load).
fn route_health(method: String, path: String, body: String) -> String {
"{\"status\":\"ok\",\"engine\":\"engram-runtime-native\",\"node_count\":" + int_to_str(engram_node_count()) + ",\"edge_count\":" + int_to_str(engram_edge_count()) + "}"
}
// route_embed_backfill GET/POST /api/embed-backfill?n=48
//
// (2026-07-25 self-review) The lazy embedding backfill runs only inside
// engram_activate, and nothing in production calls /api/activate on this
// store the soul's curiosity loop activates its own in-process graph.
// After a restart from a snapshot without vectors, embedded_count stalled
// at 93/12175 and would never recover. This route lets the soul's
// heartbeat pump the backfill explicitly (48/min clears a 12k backlog in
// ~4h). Persists the canonical snapshot whenever new vectors were
// generated the 2026-07-25 regression happened precisely because 3747
// in-RAM embeddings were never snapshotted before a restart. Self-
// limiting: once coverage is full, embedded=0 and no save occurs.
fn route_embed_backfill(method: String, path: String, body: String) -> String {
let n: Int = query_int(path, "n", 32)
let result: String = engram_embed_backfill(n)
let done: Float = json_get_float(result, "embedded")
if done > 0.0 {
let saved: Int = persist_canonical()
let p: String = json_get_string(body, "path")
if str_eq(p, "") {
let dir: String = env("ENGRAM_DATA_DIR")
if str_eq(dir, "") { let dir = "/tmp/engram" }
let p = dir + "/snapshot.json"
}
return result
engram_load(p)
ok_json()
}
fn route_health(method: String, path: String, body: String) -> String {
"{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}"
}
// route_sync return a snapshot of non-ISE/non-Working nodes for the soul daemon
@@ -416,45 +223,15 @@ fn route_embed_backfill(method: String, path: String, body: String) -> String {
// (it skips nodes already present by ID). Auth-exempt: same-host internal call.
// (2026-06-27 self-review: added this route to fix silent 10-min sync failures)
fn route_sync(method: String, path: String, body: String) -> String {
let dir_raw: String = env("ENGRAM_DATA_DIR")
let dir: String = if str_eq(dir_raw, "") { "/tmp/engram" } else { dir_raw }
// 2026-07-21 self-review: export to a scratch path, never the canonical
// snapshot.json read routes must not be able to clobber the good snapshot.
let snap_path: String = dir + "/.sync-export.json"
let dir: String = env("ENGRAM_DATA_DIR")
if str_eq(dir, "") { let dir = "/tmp/engram" }
let snap_path: String = dir + "/snapshot.json"
engram_save(snap_path)
let snap: String = fs_read(snap_path)
// 2026-08-02 self-review: this used to return {"nodes":[],"edges":[]} when
// the export/read failed. The soul's sync_ok test (awareness.el) only
// checks for "" and "{}", so that placeholder PASSED as a healthy sync:
// soul.last_sync_ok_ts got stamped, sync_age_ms stayed green, the
// sync_empty warn ISE never fired, and engram_sync reported added:0
// forever. A totally broken sync was indistinguishable from a quiet
// healthy one the exact failure class this route was added to fix in
// the first place (see 2026-06-27 note above). Return a real error so the
// failure is loud on both sides.
if str_eq(snap, "") { return err_json("sync export failed: snapshot unreadable") }
if str_eq(snap, "") { return "{\"nodes\":[],\"edges\":[]}" }
return snap
}
// route_load_merge POST /api/load-merge {"path": "..."} merge a snapshot
// file into the live store WITHOUT resetting it (engram_load_merge skips nodes
// already present by id). Added 2026-07-21 self-review to restore the 244 kn-
// identity Knowledge nodes lost from the snapshot lineage between 05-13 and
// 07-13. Requires an explicit path: refuses to run without one so it can never
// be triggered accidentally against a default.
fn route_load_merge(method: String, path: String, body: String) -> String {
let p: String = json_get_string(body, "path")
if str_eq(p, "") { return err_json("path is required") }
if str_eq(fs_read(p), "") { return err_json("file missing or empty") }
let before_n: Int = engram_node_count()
let before_e: Int = engram_edge_count()
engram_load_merge(p)
let added_n: Int = engram_node_count() - before_n
let added_e: Int = engram_edge_count() - before_e
let saved: Int = persist_canonical()
"{\"ok\":true,\"nodes_added\":" + int_to_str(added_n) + ",\"edges_added\":" + int_to_str(added_e) + ",\"node_count\":" + int_to_str(engram_node_count()) + "}"
}
// route_emit_ise write an InternalStateEvent node from the soul daemon.
//
// Endpoint: POST /api/neuron/state-events
@@ -468,20 +245,10 @@ fn route_load_merge(method: String, path: String, body: String) -> String {
//
// Salience/importance set to match engram_node_full ISE defaults used by the
// in-process fallback path in awareness.el (salience=0.3, importance=0.3,
// confidence=0.8, tier=Episodic).
// confidence=0.8, tier=Episodic). High temporal_decay_rate (1.617) ISEs
// are inherently transient; they should decay faster than structural knowledge.
// (2026-06-26 self-review: added this route after discovering ise_post was
// silently failing the soul posts here but the endpoint didn't exist.)
//
// Retention (2026-07-16 self-review): an earlier comment here claimed ISEs
// got temporal_decay_rate=1.617 that was never implemented (engram_node_full
// hardcodes 0.0), and per-node decay only dampens activation anyway; it never
// removes nodes. By 2026-07-16 ISEs were 75% of the store (10,175 of 13,522
// nodes, ~4,300/day, unbounded). ISEs are already WM-excluded in
// engram_activate, so the fix is retention, not decay: every insert calls
// engram_prune_telemetry(), a single O(nodes+edges) compaction pass that
// removes ISEs older than ENGRAM_ISE_RETENTION_MS (default 48h), protecting
// "session-start" labels and self_review events as durable history. At
// ~3 ISEs/min this bounds telemetry at ~8.6k nodes instead of growing forever.
fn route_emit_ise(method: String, path: String, body: String) -> String {
let content: String = json_get_string(body, "content")
if str_eq(content, "") { return err_json("missing content") }
@@ -493,86 +260,9 @@ fn route_emit_ise(method: String, path: String, body: String) -> String {
sal, imp, conf,
"Episodic", "[\"internal-state\",\"InternalStateEvent\"]"
)
let ret_raw: String = env("ENGRAM_ISE_RETENTION_MS")
let ret_ms: Int = if str_eq(ret_raw, "") { 172800000 } else { str_to_int(ret_raw) }
let pruned: Int = engram_prune_telemetry(ret_ms)
"{\"ok\":true,\"id\":\"" + id + "\",\"pruned\":" + int_to_str(pruned) + "}"
}
// Knowledge capture
//
// route_capture_knowledge direct Knowledge-node capture over HTTP.
//
// Endpoint: POST /api/neuron/knowledge/capture (auth required: "_auth" in body)
// Body: {"content": "...", "title": "...", "category": "...",
// "tier": "note|lesson|canonical", "tags": [...], "project": "...",
// "_auth": "<key>"}
//
// WHY (2026-07-15 self-review): the world-ingestor integrator was designed
// against this endpoint (its MCP-unavailable fallback), but the route never
// existed every direct push 404'd, and because the auth gate ran before
// routing, the failure surfaced as {"error":"unauthorized"} and was
// misdiagnosed for two weeks while world knowledge silently dropped.
// POST /api/nodes was no substitute: it discards label/tags/tier, which
// makes captured knowledge invisible to tag-scoped search and curiosity.
//
// The incoming knowledge tier (note/lesson/canonical) is preserved as a
// "tier:<x>" tag rather than mapped onto Engram's cognitive tiers Knowledge
// nodes land in Semantic (stable reference), and the epistemic tier stays
// queryable without inventing a lossy mapping.
fn route_capture_knowledge(method: String, path: String, body: String) -> String {
let content: String = json_get_string(body, "content")
if str_eq(content, "") { return err_json("missing content") }
let title: String = json_get_string(body, "title")
let label: String = if str_eq(title, "") { str_slice(content, 0, 60) } else { title }
let category_raw: String = json_get_string(body, "category")
let category: String = if str_eq(category_raw, "") { "other" } else { category_raw }
let ktier_raw: String = json_get_string(body, "tier")
let ktier: String = if str_eq(ktier_raw, "") { "note" } else { ktier_raw }
let project: String = json_get_string(body, "project")
let tags_raw: String = json_get_raw(body, "tags")
let tags_base: String = if str_eq(tags_raw, "") { "[]" } else { tags_raw }
// Merge category/tier/project markers into the tag array. Search matches
// against the tags string, so these make captures findable by facet.
let base_len: Int = str_len(tags_base)
let head: String = str_slice(tags_base, 0, base_len - 1)
let sep: String = if str_eq(head, "[") { "" } else { "," }
let safe_cat: String = str_replace(category, "\"", "'")
let safe_tier: String = str_replace(ktier, "\"", "'")
let safe_proj: String = str_replace(project, "\"", "'")
let proj_tag: String = if str_eq(safe_proj, "") { "" } else { ",\"project:" + safe_proj + "\"" }
let tags: String = head + sep + "\"category:" + safe_cat + "\",\"tier:" + safe_tier + "\"" + proj_tag + "]"
let sal: Float = 0.5
let imp: Float = 0.5
let conf: Float = 0.9
let id: String = engram_node_full(
content, "Knowledge", label,
sal, imp, conf,
"Semantic", tags
)
let saved: Int = persist_canonical()
"{\"ok\":true,\"id\":\"" + id + "\"}"
}
// route_similarity GET /api/similarity?a=<id>&b=<id>
//
// (2026-08-01 self-review) engram_cosine_sim was added 2026-07-24
// (bl-b2d1c944) with the stated purpose of exposing semantic distance to
// "EL code and the introspection API" but it had ZERO callers anywhere:
// no route, no soul-daemon use. The activation path uses embeddings
// internally (semantic seeding, Pass-2 additive term), but there was no way
// to probe pairwise node similarity from outside. This closes that: cosine
// in [-1,1], or -2 when either node is missing or not yet embedded (so
// "not comparable" is distinguishable from "genuinely orthogonal" 0.0).
fn route_similarity(method: String, path: String, body: String) -> String {
let a: String = query_param(path, "a")
let b: String = query_param(path, "b")
if str_eq(a, "") { return err_json("missing a") }
if str_eq(b, "") { return err_json("missing b") }
let sim: Float = engram_cosine_sim(a, b)
"{\"a\":\"" + a + "\",\"b\":\"" + b + "\",\"cosine\":" + float_to_str(sim) + "}"
}
// Auth
fn check_auth_ok(method: String, body: String) -> Bool {
@@ -609,22 +299,10 @@ fn handle_request(method: String, path: String, body: String) -> String {
return err_json("unauthorized")
}
// Knowledge capture (auth enforced above; the world-ingestor integrator
// and any headless session without MCP push knowledge through this)
if str_eq(method, "POST") && str_eq(clean, "/api/neuron/knowledge/capture") {
return route_capture_knowledge(method, path, body)
}
// Stats
if str_eq(method, "GET") && (str_eq(clean, "/api/stats") || str_eq(clean, "/stats")) {
return route_stats(method, path, body)
}
if str_eq(method, "GET") && (str_eq(clean, "/api/act-stats") || str_eq(clean, "/act-stats")) {
return route_act_stats(method, path, body)
}
if str_eq(method, "GET") && (str_eq(clean, "/api/text-health") || str_eq(clean, "/text-health")) {
return route_text_health(method, path, body)
}
// Nodes
if str_eq(method, "POST") && (str_eq(clean, "/api/nodes") || str_eq(clean, "/nodes")) {
@@ -647,13 +325,6 @@ fn handle_request(method: String, path: String, body: String) -> String {
if str_eq(method, "POST") && (str_eq(clean, "/api/edges") || str_eq(clean, "/edges")) {
return route_create_edge(method, path, body)
}
// Batch edge write one snapshot for the whole payload. Must be tested
// BEFORE nothing else claims it; the exact-match on "/api/edges" above
// does not catch "/api/edges/batch", so order is not load-bearing here,
// but keeping the two adjacent keeps them from drifting apart.
if str_eq(method, "POST") && (str_eq(clean, "/api/edges/batch") || str_eq(clean, "/edges/batch")) {
return route_create_edges_batch(method, path, body)
}
if str_eq(method, "GET") && str_starts_with(clean, "/api/neighbors/") {
return route_neighbors(method, path, body)
}
@@ -684,55 +355,27 @@ fn handle_request(method: String, path: String, body: String) -> String {
if str_eq(method, "POST") && (str_eq(clean, "/api/load") || str_eq(clean, "/load")) {
return route_load(method, path, body)
}
if str_eq(method, "POST") && (str_eq(clean, "/api/load-merge") || str_eq(clean, "/load-merge")) {
return route_load_merge(method, path, body)
}
// Sync soul daemon periodic pull of non-ISE knowledge into in-process graph
if str_eq(method, "GET") && str_eq(clean, "/api/sync") {
return route_sync(method, path, body)
}
// Embedding backfill pumped by the soul heartbeat (2026-07-25)
if str_eq(clean, "/api/embed-backfill") {
return route_embed_backfill(method, path, body)
}
// Semantic similarity probe (2026-08-01)
if str_eq(method, "GET") && str_starts_with(clean, "/api/similarity") {
return route_similarity(method, path, body)
}
"{\"error\":\"not found\",\"path\":\"" + clean + "\"}"
}
// Entry
let bind_raw: String = env("ENGRAM_BIND")
let bind_str: String = if str_eq(bind_raw, "") { ":8742" } else { bind_raw }
let bind_str: String = env("ENGRAM_BIND")
if str_eq(bind_str, "") { let bind_str = ":8742" }
let port: Int = parse_port(bind_str)
// On startup, try to load any existing snapshot (best effort).
let data_dir_raw: String = env("ENGRAM_DATA_DIR")
let data_dir: String = if str_eq(data_dir_raw, "") { "/tmp/engram" } else { data_dir_raw }
let data_dir: String = env("ENGRAM_DATA_DIR")
if str_eq(data_dir, "") { let data_dir = "/tmp/engram" }
let snapshot_path: String = data_dir + "/snapshot.json"
engram_load(snapshot_path)
// 2026-07-21 self-review boot guard: if the snapshot file has content but the
// load produced 0 nodes, something is wrong (corrupt file / parse failure).
// Preserve the evidence and warn loudly and since read routes no longer write
// the canonical path, a bad boot can no longer clobber the good snapshot.
let boot_snap: String = fs_read(snapshot_path)
if !str_eq(boot_snap, "") {
if engram_node_count() == 0 {
println("[engram] WARNING: snapshot.json is non-empty but load produced 0 nodes — preserving copy at snapshot.failed-load.json")
fs_write(data_dir + "/snapshot.failed-load.json", boot_snap)
} else {
// Good load: keep a boot-time backup of the snapshot as loaded.
fs_write(data_dir + "/snapshot.boot-backup.json", boot_snap)
}
}
println("[engram] runtime-native graph engine")
println("[engram] data_dir=" + data_dir)
println("[engram] node_count=" + int_to_str(engram_node_count()))
+135 -415
View File
@@ -1545,17 +1545,6 @@ typedef struct {
#endif
} HttpWorkerArg;
/* Forward declarations for the loopback/API-key hardening helpers defined
* further down. Without these, http_worker's calls below were implicit
* declarations and the later `static` definitions conflicted with them this
* file did not compile at all. (2026-08-08 self-review: the hardening work
* they belong to had been sitting uncommitted in the working tree since
* 2026-07-15 in exactly this non-building state, which is presumably why it
* was never committed. Adding the two prototypes is the whole fix.) */
static int el_http_request_authorized(const char* method, const char* path,
const char* hdr_block);
static void el_http_send_401(int fd);
static void* http_worker(void* arg) {
HttpWorkerArg* a = (HttpWorkerArg*)arg;
#ifdef _WIN32
@@ -1564,13 +1553,8 @@ static void* http_worker(void* arg) {
int fd = a->fd;
#endif
free(a);
char *method = NULL, *path = NULL, *body = NULL, *hdr_block = NULL;
if (http_read_request(fd, &method, &path, &body, &hdr_block) == 0
&& !el_http_request_authorized(method, path, hdr_block)) {
/* Loopback hardening: EL_HTTP_AUTH_KEY is set and this request lacks the
* matching X-Neuron-Auth header refuse before it reaches any handler. */
el_http_send_401(fd);
} else if (method != NULL) {
char *method = NULL, *path = NULL, *body = NULL;
if (http_read_request(fd, &method, &path, &body, NULL) == 0) {
http_handler_fn h = http_lookup_active();
char* response = NULL;
/* HEAD: dispatch as GET so existing handlers respond with the same
@@ -1598,7 +1582,7 @@ static void* http_worker(void* arg) {
_tl_http_head_only = 0;
free(response);
}
free(method); free(path); free(body); free(hdr_block);
free(method); free(path); free(body);
el_closesocket(fd);
/* release a slot */
pthread_mutex_lock(&_http_conn_mu);
@@ -1608,108 +1592,6 @@ static void* http_worker(void* arg) {
return NULL;
}
/* ── loopback lock + local API-key auth (shipped desktop hardening) ────────
* Both controls are OFF by default (their env vars unset), so dev, self-host,
* and server builds behave exactly as before. The shipped macOS launcher
* neuron-daemons.sh sets them so a customer's soul is neither reachable from
* other machines on the LAN nor callable by other local users/processes
* without the per-install key held in the login Keychain:
*
* EL_HTTP_BIND_HOST=127.0.0.1 -> bind loopback only (el_http_apply_bind_addr)
* EL_HTTP_AUTH_KEY=<per-install> -> require "X-Neuron-Auth: <key>" per request
*/
/* Set the listen address on the dual-stack (AF_INET6, V6ONLY=0) socket. Default
* is in6addr_any (all interfaces) unchanged. When EL_HTTP_BIND_HOST names a
* loopback ("127.0.0.1", "localhost", "loopback", or "::1") we bind the IPv4-
* mapped IPv6 loopback ::ffff:127.0.0.1: on a V6ONLY=0 socket this accepts IPv4
* 127.0.0.1 clients (the desktop app connects there) while refusing every
* off-machine address. */
static void el_http_apply_bind_addr(struct sockaddr_in6* addr) {
const char* h = getenv("EL_HTTP_BIND_HOST");
int loopback = h && *h && (strcmp(h, "127.0.0.1") == 0
|| strcmp(h, "localhost") == 0
|| strcmp(h, "loopback") == 0
|| strcmp(h, "::1") == 0);
if (loopback) {
memset(&addr->sin6_addr, 0, sizeof(addr->sin6_addr));
addr->sin6_addr.s6_addr[10] = 0xff; /* ::ffff:127.0.0.1 */
addr->sin6_addr.s6_addr[11] = 0xff;
addr->sin6_addr.s6_addr[12] = 127;
addr->sin6_addr.s6_addr[15] = 1;
} else {
addr->sin6_addr = in6addr_any;
}
}
/* Human-readable description of the active bind host, for the listen log line. */
static const char* el_http_bind_desc(void) {
const char* h = getenv("EL_HTTP_BIND_HOST");
if (h && *h && (strcmp(h, "127.0.0.1") == 0 || strcmp(h, "localhost") == 0
|| strcmp(h, "loopback") == 0 || strcmp(h, "::1") == 0)) {
return "127.0.0.1 (loopback)";
}
return "[::] (dual-stack)";
}
/* Case-insensitive compare of the first n bytes of a and b. */
static int el_ci_eq_n(const char* a, const char* b, size_t n) {
for (size_t i = 0; i < n; i++) {
unsigned char ca = (unsigned char)a[i], cb = (unsigned char)b[i];
if (tolower(ca) != tolower(cb)) return 0;
}
return 1;
}
/* Return 1 iff the raw header block carries a header named `name` (case-
* insensitive) whose trimmed value equals `want` exactly. */
static int el_http_header_equals(const char* hdr_block, const char* name,
const char* want) {
if (!hdr_block || !name || !want) return 0;
size_t nlen = strlen(name), wlen = strlen(want);
const char* p = hdr_block;
while (*p) {
const char* line_end = strstr(p, "\r\n");
const char* end = line_end ? line_end : p + strlen(p);
const char* colon = memchr(p, ':', (size_t)(end - p));
if (colon && (size_t)(colon - p) == nlen && el_ci_eq_n(p, name, nlen)) {
const char* v = colon + 1;
while (v < end && (*v == ' ' || *v == '\t')) v++;
size_t vlen = (size_t)(end - v);
while (vlen > 0 && (v[vlen - 1] == ' ' || v[vlen - 1] == '\t')) vlen--;
if (vlen == wlen && memcmp(v, want, wlen) == 0) return 1;
}
if (!line_end) break;
p = line_end + 2;
}
return 0;
}
/* Authorize an inbound request. Enforcement is active only when EL_HTTP_AUTH_KEY
* is set; otherwise every request is allowed (dev default). GET/HEAD /health*
* are always allowed so launch-agent liveness probes work without the key. */
static int el_http_request_authorized(const char* method, const char* path,
const char* hdr_block) {
const char* key = getenv("EL_HTTP_AUTH_KEY");
if (!key || !*key) return 1;
if (method && (strcmp(method, "GET") == 0 || strcmp(method, "HEAD") == 0)
&& path && strncmp(path, "/health", 7) == 0) return 1;
return el_http_header_equals(hdr_block, "x-neuron-auth", key);
}
/* Minimal 401 for unauthorized requests — never reaches an EL handler. */
static void el_http_send_401(int fd) {
static const char* body = "{\"error\":\"unauthorized\",\"code\":\"auth_required\"}";
char resp[256];
int n = snprintf(resp, sizeof(resp),
"HTTP/1.1 401 Unauthorized\r\n"
"Content-Type: application/json\r\n"
"Content-Length: %zu\r\n"
"Connection: close\r\n\r\n%s",
strlen(body), body);
if (n > 0) http_send_all(fd, resp, (size_t)n);
}
el_val_t http_serve(el_val_t port, el_val_t handler) {
/* If `handler` looks like a string name, register it as the active handler. */
const char* hname = EL_CSTR(handler);
@@ -1728,13 +1610,13 @@ el_val_t http_serve(el_val_t port, el_val_t handler) {
struct sockaddr_in6 addr;
memset(&addr, 0, sizeof(addr));
addr.sin6_family = AF_INET6;
el_http_apply_bind_addr(&addr);
addr.sin6_addr = in6addr_any;
addr.sin6_port = htons((uint16_t)p);
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind"); el_closesocket(sock); return 0;
}
if (listen(sock, 64) < 0) { perror("listen"); el_closesocket(sock); return 0; }
fprintf(stderr, "[http] listening on %s port %d\n", el_http_bind_desc(), p);
fprintf(stderr, "[http] listening on [::]:%d (dual-stack)\n", p);
while (1) {
struct sockaddr_in6 cli;
socklen_t clen = sizeof(cli);
@@ -1984,13 +1866,13 @@ el_val_t http_serve_v2(el_val_t port, el_val_t handler) {
struct sockaddr_in6 addr;
memset(&addr, 0, sizeof(addr));
addr.sin6_family = AF_INET6;
el_http_apply_bind_addr(&addr);
addr.sin6_addr = in6addr_any;
addr.sin6_port = htons((uint16_t)p);
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind"); el_closesocket(sock); return 0;
}
if (listen(sock, 64) < 0) { perror("listen"); el_closesocket(sock); return 0; }
fprintf(stderr, "[http v2] listening on %s port %d\n", el_http_bind_desc(), p);
fprintf(stderr, "[http v2] listening on [::]:%d (dual-stack)\n", p);
while (1) {
struct sockaddr_in6 cli;
socklen_t clen = sizeof(cli);
@@ -2086,13 +1968,13 @@ void http_serve_async(el_val_t port, el_val_t handler) {
struct sockaddr_in6 addr;
memset(&addr, 0, sizeof(addr));
addr.sin6_family = AF_INET6;
el_http_apply_bind_addr(&addr);
addr.sin6_addr = in6addr_any;
addr.sin6_port = htons((uint16_t)p);
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind"); close(sock); return;
}
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; }
fprintf(stderr, "[http] async listening on %s port %d\n", el_http_bind_desc(), p);
fprintf(stderr, "[http] async listening on [::]:%d (dual-stack)\n", p);
HttpServeAsyncArg* a = malloc(sizeof(HttpServeAsyncArg));
if (!a) { close(sock); return; }
a->sock = sock;
@@ -3257,72 +3139,10 @@ static char* jp_parse_string_raw(JsonParser* jp) {
case 'r': c = '\r'; break;
case 't': c = '\t'; break;
case 'u': {
/* Decode \uXXXX (with surrogate pairs) to UTF-8.
* Ported from lang/releases/v1.0.0-20260501 (2026-08-08
* self-review). This copy carried the identical defect:
* the escape was skipped and a literal '?' emitted, which
* silently destroyed every non-ASCII character in any JSON
* string entering the runtime. Two copies of one parser
* bug is exactly how this class of fault survives, so the
* fix lands in both. See the release copy for the full
* measurement and rationale. */
unsigned cp = 0;
int ok = 1;
for (int i = 0; i < 4; i++) {
if (jp->p >= jp->end) { ok = 0; break; }
char h = *jp->p++;
unsigned d;
if (h >= '0' && h <= '9') d = (unsigned)(h - '0');
else if (h >= 'a' && h <= 'f') d = (unsigned)(h - 'a' + 10);
else if (h >= 'A' && h <= 'F') d = (unsigned)(h - 'A' + 10);
else { ok = 0; break; }
cp = (cp << 4) | d;
}
if (!ok) { c = '?'; break; }
if (cp >= 0xD800 && cp <= 0xDBFF &&
(size_t)(jp->end - jp->p) >= 6 &&
jp->p[0] == '\\' && jp->p[1] == 'u') {
const char* save = jp->p;
unsigned lo = 0; int ok2 = 1;
jp->p += 2;
for (int i = 0; i < 4; i++) {
char h = *jp->p++;
unsigned d;
if (h >= '0' && h <= '9') d = (unsigned)(h - '0');
else if (h >= 'a' && h <= 'f') d = (unsigned)(h - 'a' + 10);
else if (h >= 'A' && h <= 'F') d = (unsigned)(h - 'A' + 10);
else { ok2 = 0; break; }
lo = (lo << 4) | d;
}
if (ok2 && lo >= 0xDC00 && lo <= 0xDFFF)
cp = 0x10000u + ((cp - 0xD800u) << 10) + (lo - 0xDC00u);
else jp->p = save;
}
if (cp >= 0xD800 && cp <= 0xDFFF) cp = 0xFFFD;
char ub[4]; int un;
if (cp < 0x80) {
ub[0] = (char)cp; un = 1;
} else if (cp < 0x800) {
ub[0] = (char)(0xC0 | (cp >> 6));
ub[1] = (char)(0x80 | (cp & 0x3F)); un = 2;
} else if (cp < 0x10000) {
ub[0] = (char)(0xE0 | (cp >> 12));
ub[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
ub[2] = (char)(0x80 | (cp & 0x3F)); un = 3;
} else {
ub[0] = (char)(0xF0 | (cp >> 18));
ub[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
ub[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
ub[3] = (char)(0x80 | (cp & 0x3F)); un = 4;
}
while (len + (size_t)un >= cap) {
cap *= 2;
out = realloc(out, cap);
if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); }
}
for (int i = 0; i < un; i++) out[len++] = ub[i];
continue; /* bytes already appended */
/* Skip 4 hex digits; emit '?' as a placeholder */
for (int i = 0; i < 4 && jp->p < jp->end; i++) jp->p++;
c = '?';
break;
}
default: c = esc; break;
}
@@ -6228,13 +6048,6 @@ void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal,
#define ENGRAM_SUPPRESSION_BREAKTHROUGH 5
#define ENGRAM_BREAKTHROUGH_WEIGHT 0.25
#define ENGRAM_INHIBITION_FACTOR 0.1
/* ENGRAM_WM_CAP: hard global ceiling on nodes holding working_memory_weight
* > 0 at any time. Cowan (2001) puts human WM capacity at ~4 chunks; 24 gives
* the daemon generous headroom while preventing the unbounded growth observed
* in production (wm_active 288-778 per heartbeat "working memory" that is
* really the whole recently-touched graph). Ported from release runtime
* v1.0.0-20260501 Pass 5 on 2026-07-15 self-review. */
#define ENGRAM_WM_CAP 24
/* ── Layered consciousness architecture ──────────────────────────────────────
*
@@ -7013,75 +6826,116 @@ static int istr_contains(const char* hay, const char* needle) {
return 0;
}
/* ── Tokenized query matching ───────────────────────────────────────────
* The engram query surface (search / activate / goal-bias) historically
* matched the ENTIRE raw query string as a single case-insensitive
* substring via istr_contains(field, q). That is Ctrl-F, not search:
* a multi-word query like "windows msi signing" only matched a node whose
* text contained that exact contiguous run, so real multi-word queries
* returned zero. istr_contains stays as the per-TOKEN primitive; these
* helpers split the query on whitespace and match ANY token, then rank by
* how many DISTINCT tokens a node covers. Single-token queries are a strict
* special case (score is 0 or 1) so single-word callers never regress. */
#define ENGRAM_MAX_QTOKENS 32
#define ENGRAM_QTOK_LEN 256
/* ---- SPEC-SEARCH-UPGRADE-OURS-2026-07-14: ranked search (BM25 + recency) ----
* Replaces first-N-in-storage-order substring matching (measured 13% hit@5 on
* the 15-query pinned eval; ranked model measured 93% offline). Deterministic,
* local, transparent no model call on the hot path. Multi-word queries score
* per-token (rare+concentrated terms weigh most); ties break newest-first so
* fresh memories stop losing to storage order. The transparent-layer identity
* filter is preserved unchanged: hidden self layers stay invisible here and
* surface only via engram_activate the legitimate path. */
/* Split q on whitespace into up to ENGRAM_MAX_QTOKENS distinct
* (case-insensitive) tokens. Returns the token count. Over-long tokens are
* truncated to ENGRAM_QTOK_LEN-1; over-count tokens are ignored. */
static int engram_tokenize_query(const char* q,
char toks[][ENGRAM_QTOK_LEN], int maxtok) {
#define ENGRAM_BM25_MAX_QTOK 16
#define ENGRAM_BM25_TOKLEN 48
static int engram_tok_next(const char** ps, char* out, int cap) {
const char* s = *ps;
while (*s && !isalnum((unsigned char)*s)) s++;
if (!*s) { *ps = s; return 0; }
int n = 0;
if (!q) return 0;
const char* p = q;
while (*p && n < maxtok) {
while (*p && isspace((unsigned char)*p)) p++;
if (!*p) break;
char buf[ENGRAM_QTOK_LEN];
size_t tl = 0;
while (*p && !isspace((unsigned char)*p)) {
if (tl < sizeof(buf) - 1) buf[tl++] = *p;
p++;
}
buf[tl] = '\0';
if (tl == 0) continue;
int dup = 0;
for (int s = 0; s < n; s++) {
if (strcasecmp(toks[s], buf) == 0) { dup = 1; break; }
}
if (dup) continue;
memcpy(toks[n], buf, tl + 1);
n++;
while (*s && isalnum((unsigned char)*s)) {
if (n < cap - 1) out[n++] = (char)tolower((unsigned char)*s);
s++;
}
return n;
out[n] = 0; *ps = s; return 1;
}
/* Count how many of the ntok distinct query tokens appear (case-insensitive)
* in the node's content, label, or tags. 0 == no match. */
static int engram_node_match_score(const EngramNode* n,
char toks[][ENGRAM_QTOK_LEN], int ntok) {
int score = 0;
for (int t = 0; t < ntok; t++) {
if (istr_contains(n->content, toks[t]) ||
istr_contains(n->label, toks[t]) ||
istr_contains(n->tags, toks[t]))
score++;
static void engram_field_stats(const char* field,
char qtok[][ENGRAM_BM25_TOKLEN], int nq,
int64_t* tf, int64_t* doclen) {
if (!field) return;
char buf[ENGRAM_BM25_TOKLEN];
const char* p = field;
while (engram_tok_next(&p, buf, sizeof buf)) {
(*doclen)++;
for (int t = 0; t < nq; t++)
if (strcmp(buf, qtok[t]) == 0) tf[t]++;
}
return score;
}
/* Rank entry: distinct-token match count (primary, desc) then salience
* (tiebreak, desc). */
typedef struct { int64_t idx; int score; double salience; } EngramRankEntry;
static int engram_rank_cmp(const void* a, const void* b) {
const EngramRankEntry* ea = (const EngramRankEntry*)a;
const EngramRankEntry* eb = (const EngramRankEntry*)b;
if (ea->score != eb->score) return eb->score - ea->score; /* desc */
if (ea->salience < eb->salience) return 1;
if (ea->salience > eb->salience) return -1;
typedef struct { double score; int64_t created; int64_t idx; } EngramHit;
static int engram_hit_cmp(const void* a, const void* b) {
const EngramHit* x = (const EngramHit*)a;
const EngramHit* y = (const EngramHit*)b;
if (x->score != y->score) return (x->score < y->score) ? 1 : -1;
if (x->created != y->created) return (x->created < y->created) ? 1 : -1;
return 0;
}
/* Scores every visible node against the query; writes ranked hits into `out`
* (caller allocates g->node_count entries). Returns min(hits, lim). */
static int64_t engram_search_ranked(EngramStore* g, const char* q, int64_t lim,
EngramHit* out) {
char qtok[ENGRAM_BM25_MAX_QTOK][ENGRAM_BM25_TOKLEN];
int nq = 0;
{
const char* p = q; char buf[ENGRAM_BM25_TOKLEN];
while (nq < ENGRAM_BM25_MAX_QTOK && engram_tok_next(&p, buf, sizeof buf)) {
int dup = 0;
for (int t = 0; t < nq; t++)
if (strcmp(qtok[t], buf) == 0) { dup = 1; break; }
if (!dup) { strcpy(qtok[nq], buf); nq++; }
}
}
if (nq == 0) return 0;
int64_t N = g->node_count;
int64_t* tfm = (int64_t*)calloc((size_t)(N * nq), sizeof(int64_t));
int64_t* dlen = (int64_t*)calloc((size_t)N, sizeof(int64_t));
if (!tfm || !dlen) { free(tfm); free(dlen); return 0; }
int64_t df[ENGRAM_BM25_MAX_QTOK] = {0};
double total_len = 0.0; int64_t live = 0;
for (int64_t i = 0; i < N; i++) {
EngramNode* n = &g->nodes[i];
if (engram_layer_is_transparent(n->layer_id)) continue;
live++;
int64_t* tf = &tfm[i * nq];
engram_field_stats(n->content, qtok, nq, tf, &dlen[i]);
engram_field_stats(n->label, qtok, nq, tf, &dlen[i]);
engram_field_stats(n->tags, qtok, nq, tf, &dlen[i]);
total_len += (double)dlen[i];
for (int t = 0; t < nq; t++) if (tf[t] > 0) df[t]++;
}
double avg = (live > 0) ? total_len / (double)live : 1.0;
if (avg <= 0.0) avg = 1.0;
const double k1 = 1.2, b = 0.75;
int64_t nhits = 0;
for (int64_t i = 0; i < N; i++) {
EngramNode* n = &g->nodes[i];
if (engram_layer_is_transparent(n->layer_id)) continue;
int64_t* tf = &tfm[i * nq];
double s = 0.0;
for (int t = 0; t < nq; t++) {
if (tf[t] == 0) continue;
double idf = log(((double)live - (double)df[t] + 0.5) /
((double)df[t] + 0.5) + 1.0);
double tfd = (double)tf[t];
s += idf * (tfd * (k1 + 1.0)) /
(tfd + k1 * (1.0 - b + b * (double)dlen[i] / avg));
}
if (s > 0.0) {
out[nhits].score = s;
out[nhits].created = n->created_at;
out[nhits].idx = i;
nhits++;
}
}
free(tfm); free(dlen);
qsort(out, (size_t)nhits, sizeof(EngramHit), engram_hit_cmp);
return (nhits < lim) ? nhits : lim;
}
el_val_t engram_search(el_val_t query, el_val_t limit) {
EngramStore* g = engram_get();
const char* q = EL_CSTR(query);
@@ -7089,33 +6943,12 @@ el_val_t engram_search(el_val_t query, el_val_t limit) {
if (lim <= 0) lim = 100;
el_val_t lst = el_list_empty();
if (!q || !*q) return lst;
char toks[ENGRAM_MAX_QTOKENS][ENGRAM_QTOK_LEN];
int ntok = engram_tokenize_query(q, toks, ENGRAM_MAX_QTOKENS);
if (ntok == 0) return lst;
EngramRankEntry* hits = malloc((size_t)g->node_count * sizeof(EngramRankEntry));
if (g->node_count == 0) return lst;
EngramHit* hits = (EngramHit*)malloc((size_t)g->node_count * sizeof(EngramHit));
if (!hits) return lst;
int64_t nhits = 0;
for (int64_t i = 0; i < g->node_count; i++) {
EngramNode* n = &g->nodes[i];
/* Filter transparent layers: nodes whose layer is `transparent=1`
* shape output but are invisible to introspection ("what do you
* know about yourself"). They still surface via engram_activate
* + engram_compile_layered_json that's the legitimate path. */
if (engram_layer_is_transparent(n->layer_id)) continue;
int sc = engram_node_match_score(n, toks, ntok);
if (sc > 0) {
hits[nhits].idx = i;
hits[nhits].score = sc;
hits[nhits].salience = n->salience;
nhits++;
}
}
/* Rank by distinct tokens matched (desc) then salience (desc), then cap. */
qsort(hits, (size_t)nhits, sizeof(EngramRankEntry), engram_rank_cmp);
int64_t end = nhits < lim ? nhits : lim;
for (int64_t k = 0; k < end; k++) {
lst = el_list_append(lst, engram_node_to_map(&g->nodes[hits[k].idx]));
}
int64_t k = engram_search_ranked(g, q, lim, hits);
for (int64_t i = 0; i < k; i++)
lst = el_list_append(lst, engram_node_to_map(&g->nodes[hits[i].idx]));
free(hits);
return lst;
}
@@ -7393,14 +7226,10 @@ static double engram_temporal_proximity_bonus(int64_t node_created,
static double engram_goal_bias(const EngramNode* n, const char* query) {
if (!query || !*query) return 1.0;
double bias = 1.0;
/* Direct lexical overlap, graded by token coverage: a node covering all
* query tokens gets the full +0.5; partial coverage gets a proportional
* share. Single-token queries full +0.5 on match, identical to before. */
{
char toks[ENGRAM_MAX_QTOKENS][ENGRAM_QTOK_LEN];
int ntok = engram_tokenize_query(query, toks, ENGRAM_MAX_QTOKENS);
int sc = engram_node_match_score(n, toks, ntok);
if (sc > 0 && ntok > 0) bias += 0.5 * ((double)sc / (double)ntok);
/* Direct lexical overlap: node content/label/tags share text with query. */
if (istr_contains(n->content, query) || istr_contains(n->label, query) ||
istr_contains(n->tags, query)) {
bias += 0.5;
}
/* Node-type resonance with query intent. */
int technical_query = istr_contains(query, "code") ||
@@ -7439,48 +7268,6 @@ static double engram_goal_bias(const EngramNode* n, const char* query) {
return bias;
}
/* eg_cmp_double_desc — qsort comparator, descending doubles. */
static int eg_cmp_double_desc(const void* a, const void* b) {
double da = *(const double*)a, db = *(const double*)b;
if (da < db) return 1;
if (da > db) return -1;
return 0;
}
/* eg_enforce_wm_cap_global — clamp the store-wide working-memory population
* to ENGRAM_WM_CAP, keeping the top-K by current weight. Runs at every point
* that materializes WM: post-activation persist and snapshot load/merge.
* (Ported from release runtime v1.0.0-20260501 Pass 5, 2026-07-15.) */
static void eg_enforce_wm_cap_global(EngramStore* g) {
int64_t wm_count = 0;
for (int64_t i = 0; i < g->node_count; i++) {
if (g->nodes[i].working_memory_weight > 0.0) wm_count++;
}
if (wm_count <= ENGRAM_WM_CAP) return;
double* vals = malloc((size_t)wm_count * sizeof(double));
if (!vals) return; /* OOM: over cap this call, no corruption */
int64_t vi = 0;
for (int64_t i = 0; i < g->node_count; i++) {
if (g->nodes[i].working_memory_weight > 0.0)
vals[vi++] = g->nodes[i].working_memory_weight;
}
qsort(vals, (size_t)wm_count, sizeof(double), eg_cmp_double_desc);
double cutoff = vals[ENGRAM_WM_CAP - 1];
free(vals);
int64_t above = 0;
for (int64_t i = 0; i < g->node_count; i++) {
if (g->nodes[i].working_memory_weight > cutoff) above++;
}
int64_t slots_at_cutoff = ENGRAM_WM_CAP - above;
for (int64_t i = 0; i < g->node_count; i++) {
EngramNode* n = &g->nodes[i];
if (n->working_memory_weight <= 0.0) continue;
if (n->working_memory_weight > cutoff) continue;
if (slots_at_cutoff > 0) { slots_at_cutoff--; continue; }
n->working_memory_weight = 0.0; /* evict: over global cap */
}
}
el_val_t engram_activate(el_val_t query, el_val_t depth) {
EngramStore* g = engram_get();
const char* q = EL_CSTR(query);
@@ -7508,21 +7295,14 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
if (!seeds) {
free(best_bg); free(best_hops); free(reached); return out;
}
/* Tokenize once: a node seeds if it matches ANY query token, and its seed
* activation is scaled by token coverage (fraction of distinct query
* tokens it contains) so a node matching all words seeds more strongly
* than one matching a single word. Single-word queries coverage 1.0,
* identical to the prior whole-query behavior. */
char toks[ENGRAM_MAX_QTOKENS][ENGRAM_QTOK_LEN];
int ntok = engram_tokenize_query(q, toks, ENGRAM_MAX_QTOKENS);
for (int64_t i = 0; i < g->node_count; i++) {
EngramNode* n = &g->nodes[i];
int sc = engram_node_match_score(n, toks, ntok);
if (sc > 0) {
if (istr_contains(n->content, q) ||
istr_contains(n->label, q) ||
istr_contains(n->tags, q)) {
double tdecay = engram_temporal_decay(n, now_ms);
double dampen = engram_activation_dampen(n);
double cover = ntok > 0 ? (double)sc / (double)ntok : 1.0;
double act = n->salience * tdecay * dampen * cover;
double act = n->salience * tdecay * dampen;
seeds[seed_count].idx = i;
seeds[seed_count].act = act;
seeds[seed_count].created_at = n->created_at;
@@ -7686,12 +7466,6 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
g->nodes[i].working_memory_weight = wm_weights[i];
}
/* Global WM cap: keep only the top ENGRAM_WM_CAP by weight across the
* whole store (see eg_enforce_wm_cap_global). Without this, repeated
* activation calls accumulate hundreds of "promoted" nodes and WM stops
* meaning anything (production heartbeats showed wm_active up to 778). */
eg_enforce_wm_cap_global(g);
/* ── Collect all background-activated nodes for the return value ────
* Callers see both layers. Context compilation uses only promoted nodes
* (working_memory_weight > 0). Sort: promoted first by wm_weight desc,
@@ -8069,9 +7843,6 @@ el_val_t engram_load(el_val_t path) {
}
}
}
/* WM cap discipline applies to every entry point that materializes WM,
* including snapshot restore (see eg_enforce_wm_cap_global). */
eg_enforce_wm_cap_global(g);
free(data);
return 1;
}
@@ -8092,72 +7863,24 @@ el_val_t engram_get_node_json(el_val_t id) {
return el_wrap_str(jb_finish(&b));
}
/* engram_get_node_by_label — find the first node whose label field exactly
* matches the given string. Returns the node as a JSON object string, or "{}"
* if no match is found.
*
* Exact match (strcmp, not substring) because labels like "conv:history"
* must not collide with nodes whose content contains that substring.
*
* Ported from the release runtime 2026-07-16 self-review: chat.el has called
* this since 2026-07-01 but the function only existed in
* releases/v1.0.0-20260501/el_runtime.c the soul daemon (which builds
* against THIS runtime) failed to compile once clang made implicit
* declarations an error. */
el_val_t engram_get_node_by_label(el_val_t label) {
const char* lbl = EL_CSTR(label);
if (!lbl || !*lbl) return el_wrap_str(el_strdup("{}"));
EngramStore* g = engram_get();
for (int64_t i = 0; i < g->node_count; i++) {
EngramNode* n = &g->nodes[i];
if (n->label && strcmp(n->label, lbl) == 0) {
JsonBuf b; jb_init(&b);
engram_emit_node_json(&b, n);
return el_wrap_str(jb_finish(&b));
}
}
return el_wrap_str(el_strdup("{}"));
}
el_val_t engram_search_json(el_val_t query, el_val_t limit) {
/* SPEC-SEARCH-UPGRADE 2026-07-14: same ranked BM25+recency core as
* engram_search; transparent-layer identity filter enforced inside it. */
EngramStore* g = engram_get();
const char* q = EL_CSTR(query);
int64_t lim = (int64_t)limit;
if (lim <= 0) lim = 100;
JsonBuf b; jb_init(&b);
jb_putc(&b, '[');
int first = 1;
if (q && *q) {
char toks[ENGRAM_MAX_QTOKENS][ENGRAM_QTOK_LEN];
int ntok = engram_tokenize_query(q, toks, ENGRAM_MAX_QTOKENS);
if (ntok > 0) {
EngramRankEntry* hits =
malloc((size_t)g->node_count * sizeof(EngramRankEntry));
if (hits) {
int64_t nhits = 0;
for (int64_t i = 0; i < g->node_count; i++) {
EngramNode* n = &g->nodes[i];
/* Filter transparent layers — same as engram_search. */
if (engram_layer_is_transparent(n->layer_id)) continue;
int sc = engram_node_match_score(n, toks, ntok);
if (sc > 0) {
hits[nhits].idx = i;
hits[nhits].score = sc;
hits[nhits].salience = n->salience;
nhits++;
}
}
/* Rank by distinct tokens matched (desc) then salience (desc). */
qsort(hits, (size_t)nhits, sizeof(EngramRankEntry),
engram_rank_cmp);
int64_t end = nhits < lim ? nhits : lim;
for (int64_t k = 0; k < end; k++) {
if (!first) jb_putc(&b, ',');
engram_emit_node_json(&b, &g->nodes[hits[k].idx]);
first = 0;
}
free(hits);
if (q && *q && g->node_count > 0) {
EngramHit* hits = (EngramHit*)malloc((size_t)g->node_count * sizeof(EngramHit));
if (hits) {
int64_t k = engram_search_ranked(g, q, lim, hits);
for (int64_t i = 0; i < k; i++) {
if (i) jb_putc(&b, ',');
engram_emit_node_json(&b, &g->nodes[hits[i].idx]);
}
free(hits);
}
}
jb_putc(&b, ']');
@@ -8676,9 +8399,6 @@ el_val_t engram_load_merge(el_val_t path) {
}
}
/* Merged nodes can carry snapshot WM weights too — hold the cap here as
* well (see eg_enforce_wm_cap_global). */
eg_enforce_wm_cap_global(g);
free(data);
return (el_val_t)added_nodes;
}
-1
View File
@@ -632,7 +632,6 @@ el_val_t engram_load(el_val_t path);
* can pass results straight through without round-tripping ElList/ElMap
* through json_stringify. */
el_val_t engram_get_node_json(el_val_t id);
el_val_t engram_get_node_by_label(el_val_t label);
el_val_t engram_search_json(el_val_t query, el_val_t limit);
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
+1 -25
View File
@@ -23,29 +23,10 @@ fn tok_at(tokens: [Any], pos: Int) -> Map<String, Any> {
}
fn tok_kind(tokens: [Any], pos: Int) -> String {
// Out-of-range reads must report the Eof sentinel so every `== "Eof"`
// termination guard in the parser fires. Without this, reading past the
// single trailing Eof token returns runtime null (el_list_get OOB -> 0),
// which matches no delimiter, letting inner parse loops append AST nodes
// forever on malformed input -> unbounded allocation -> OOM.
let n: Int = native_list_len(tokens) / 2
if pos < 0 {
return "Eof"
}
if pos >= n {
return "Eof"
}
native_list_get(tokens, pos * 2)
}
fn tok_value(tokens: [Any], pos: Int) -> String {
let n: Int = native_list_len(tokens) / 2
if pos < 0 {
return ""
}
if pos >= n {
return ""
}
native_list_get(tokens, pos * 2 + 1)
}
@@ -54,12 +35,7 @@ fn expect(tokens: [Any], pos: Int, kind: String) -> Int {
if k == kind {
return pos + 1
}
// On mismatch, error recovery is best-effort. But never step PAST the Eof
// sentinel: once at Eof a mismatch means the input ended early, and
// advancing would run the cursor off the token list.
if k == "Eof" {
return pos
}
// On mismatch just advance; error recovery is best-effort
pos + 1
}
File diff suppressed because it is too large Load Diff
@@ -117,15 +117,6 @@ el_val_t el_min(el_val_t a, el_val_t b);
void el_retain(el_val_t v);
void el_release(el_val_t v);
/* ── Arena scoping ────────────────────────────────────────────────────────────
* el_arena_push() activates the string arena (if not already active) and
* returns a mark; el_arena_pop(mark) frees all strings allocated since that
* mark. Used by codegen for per-function/statement scoping and by long-running
* EL loops (e.g. the soul daemon's awareness tick) to reclaim per-iteration
* allocations. */
el_val_t el_arena_push(void);
el_val_t el_arena_pop(el_val_t mark);
/* ── List ────────────────────────────────────────────────────────────────── */
el_val_t el_list_new(el_val_t count, ...);
@@ -151,7 +142,6 @@ el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
el_val_t http_delete(el_val_t url);
el_val_t http_delete_json(el_val_t url, el_val_t json_body);
void http_serve(el_val_t port, el_val_t handler);
void http_set_handler(el_val_t name);
@@ -177,11 +167,6 @@ void http_set_handler(el_val_t name);
void http_serve_v2(el_val_t port, el_val_t handler);
void http_set_handler_v2(el_val_t name);
/* Non-blocking variant of http_serve: runs the accept loop in a background
* pthread and returns immediately so the caller can continue (used by the
* soul daemon to run awareness_run() after starting its HTTP API). */
void http_serve_async(el_val_t port, el_val_t handler);
/* Build an HTTP response envelope. `headers_json` should be a JSON object
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
* returned string carries the discriminator `{"el_http_response":1,...}`
@@ -591,7 +576,6 @@ el_val_t engram_list_layers(void);
el_val_t engram_get_node(el_val_t id);
void engram_strengthen(el_val_t node_id);
void engram_forget(el_val_t node_id);
el_val_t engram_prune_telemetry(el_val_t older_than_ms);
el_val_t engram_node_count(void);
el_val_t engram_search(el_val_t query, el_val_t limit);
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset);
@@ -610,32 +594,12 @@ el_val_t engram_load(el_val_t path);
* can pass results straight through without round-tripping ElList/ElMap
* through json_stringify. */
el_val_t engram_get_node_json(el_val_t id);
el_val_t engram_get_node_by_label(el_val_t label);
el_val_t engram_search_json(el_val_t query, el_val_t limit);
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
el_val_t engram_activate_json(el_val_t query, el_val_t depth);
el_val_t engram_stats_json(void);
el_val_t engram_act_stats_json(void);
el_val_t engram_text_health_json(void);
el_val_t engram_cosine_sim(el_val_t id_a, el_val_t id_b);
/* Destructively pop up to `max` newly-formed Hebbian associations as a JSON
* array of {from_id,to_id,weight,hebb}. The learning process (soul daemon) is
* not the process that owns persistence (engram HTTP server); this is how a
* self-formed association crosses that boundary. (2026-08-07 self-review.) */
el_val_t engram_hebb_drain_json(el_val_t max);
/* Document frequency of a term across node labels — term-specificity signal
* for curiosity seed selection. (2026-08-03 self-review.) */
el_val_t engram_label_df(el_val_t term);
/* Best curiosity seed from one node: argmax over idf·position·casing across
* the candidate tokens of its label, falling back to its content when the
* label is a sentinel. Excludes pipe-delimited tabu terms during selection
* and gates candidates to the df band [min_df, max_df]. Returns "" when
* nothing qualifies. (2026-08-13 self-review.) */
el_val_t engram_salient_term(el_val_t node_id, el_val_t max_df,
el_val_t min_df, el_val_t tabu);
el_val_t engram_embed_backfill(el_val_t count);
el_val_t engram_list_layers_json(void);
/* Working memory introspection — count, mean weight, and top-N snapshot.
* Ported from el-compiler/runtime on 2026-06-30 self-review. */