Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ce8656de2 | |||
| 1e49560f1f | |||
| e8f0b5a9de | |||
| 40287c4cfc | |||
| 0481bea44d | |||
| 9d565ca080 | |||
| 4773dd0aa2 | |||
| 6b9d9e6c4a | |||
| b4967af13e | |||
| 2b2a1246e7 |
@@ -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
@@ -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 + "yó" }
|
||||
return stem + "ió"
|
||||
}
|
||||
if slot == 2 { return stem + "ió" }
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
("tú" "pron" "tú" "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")
|
||||
|
||||
)
|
||||
@@ -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"),
|
||||
("tú", "pron", "tú", "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())
|
||||
@@ -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
@@ -81,7 +81,7 @@ jobs:
|
||||
# Link to produce the engram binary
|
||||
- name: Link engram binary
|
||||
run: |
|
||||
cc -std=c11 -O2 \
|
||||
cc -std=c11 -O2 -DHAVE_CURL \
|
||||
-I /usr/local/lib/el \
|
||||
-o dist/engram \
|
||||
dist/engram.c \
|
||||
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
# Link to produce the engram binary
|
||||
- name: Link engram binary
|
||||
run: |
|
||||
cc -std=c11 -O2 \
|
||||
cc -std=c11 -O2 -DHAVE_CURL \
|
||||
-I /usr/local/lib/el \
|
||||
-o dist/engram \
|
||||
dist/engram.c \
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
# Link to produce the engram binary
|
||||
- name: Link engram binary
|
||||
run: |
|
||||
cc -std=c11 -O2 \
|
||||
cc -std=c11 -O2 -DHAVE_CURL \
|
||||
-I /usr/local/lib/el \
|
||||
-o dist/engram \
|
||||
dist/engram.c \
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
+105
-254
@@ -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())));
|
||||
|
||||
+63
-424
@@ -76,112 +76,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 +103,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 +122,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 +170,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 +177,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 +219,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 +241,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 +256,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 +295,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 +321,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 +351,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()))
|
||||
|
||||
@@ -17,6 +17,16 @@
|
||||
// 4. Append dep to order after all its transitive deps
|
||||
// 5. Deduplicate: skip already-ordered vessels
|
||||
|
||||
// ── Cross-module forward declarations ─────────────────────────────────────────
|
||||
// Defined in sibling epm modules; resolved at link time. The `extern fn` decls
|
||||
// give elc the C prototypes so generated install.c compiles cleanly under strict
|
||||
// compilers (gcc>=14 / clang) that reject implicit function declarations.
|
||||
extern fn manifest_name(src: String) -> String // manifest.el
|
||||
extern fn manifest_deps(src: String) -> String // manifest.el
|
||||
extern fn registry_token() -> String // registry.el
|
||||
extern fn registry_find(name: String, version: String) -> String // registry.el
|
||||
extern fn registry_latest_version(name: String) -> String // registry.el
|
||||
|
||||
// ── Install paths ─────────────────────────────────────────────────────────────
|
||||
|
||||
// packages_dir returns the root directory for installed vessels.
|
||||
|
||||
@@ -14,6 +14,15 @@
|
||||
// EPM_REGISTRY_ORG — org name that hosts vessel repos (default: neuron-technologies)
|
||||
// EPM_TOKEN — Gitea personal access token (required for publish)
|
||||
|
||||
// ── Cross-module forward declarations ─────────────────────────────────────────
|
||||
// These symbols are defined in sibling epm modules or the El runtime and are
|
||||
// resolved at link time. The `extern fn` decls give elc the C prototype so the
|
||||
// generated registry.c compiles cleanly under strict compilers (gcc>=14 / clang)
|
||||
// that reject implicit function declarations. Signature arity must match the
|
||||
// definition; return/param types are informational (all lower to el_val_t).
|
||||
extern fn config(key: String) -> String // El runtime builtin
|
||||
extern fn read_installed() -> String // install.el
|
||||
|
||||
// ── Config helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// registry_api_url returns the Gitea API base URL with no trailing slash.
|
||||
|
||||
@@ -6,6 +6,15 @@
|
||||
// Depends on: registry.el (registry_latest_version, registry_find),
|
||||
// install.el (read_installed, install_vessel, installed_version)
|
||||
|
||||
// ── Cross-module forward declarations ─────────────────────────────────────────
|
||||
// Defined in sibling epm modules; resolved at link time. The `extern fn` decls
|
||||
// give elc the C prototypes so generated update.c compiles cleanly under strict
|
||||
// compilers (gcc>=14 / clang) that reject implicit function declarations.
|
||||
extern fn read_installed() -> String // install.el
|
||||
extern fn installed_version(name: String) -> String // install.el
|
||||
extern fn install_vessel(name: String, version: String) -> Bool // install.el
|
||||
extern fn registry_latest_version(name: String) -> String // registry.el
|
||||
|
||||
// ── Semver helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// semver_part extracts the Nth dot-separated component from a semver string.
|
||||
|
||||
@@ -75,6 +75,7 @@ static inline void* el_win_dlsym(void* handle, const char* name) {
|
||||
#include <direct.h> /* _mkdir */
|
||||
#define mkdir(path, mode) _mkdir(path) /* POSIX mkdir(path,mode) → _mkdir(path) */
|
||||
#define timegm _mkgmtime /* UTC tm → time_t */
|
||||
#define fsync(fd) _commit(fd) /* no fsync() on Windows; _commit() (<io.h>) is the equiv */
|
||||
|
||||
/* setenv/unsetenv: not in the Windows CRT; map to _putenv_s / SetEnvironmentVariable. */
|
||||
static inline int setenv(const char* name, const char* value, int overwrite) {
|
||||
|
||||
@@ -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);
|
||||
@@ -2081,18 +1963,19 @@ void http_serve_async(el_val_t port, el_val_t handler) {
|
||||
int sock = socket(AF_INET6, SOCK_STREAM, 0);
|
||||
if (sock < 0) { perror("socket"); return; }
|
||||
int yes = 1; int no = 0;
|
||||
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
||||
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no));
|
||||
/* Win32/mingw setsockopt takes optval as (const char*); the cast is portable on POSIX too. */
|
||||
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&yes, sizeof(yes));
|
||||
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (const char*)&no, sizeof(no));
|
||||
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 +3140,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 +6049,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 ──────────────────────────────────────
|
||||
*
|
||||
@@ -7082,6 +6896,243 @@ static int engram_rank_cmp(const void* a, const void* b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════════
|
||||
* SEMANTIC SEARCH LAYER — nomic-embed-text via Ollama /api/embeddings
|
||||
* ──────────────────────────────────────────────────────────────────────────
|
||||
* Augments the lexical (istr_contains) matcher with dense-vector retrieval.
|
||||
* Node content and the query are embedded through a local Ollama server;
|
||||
* nodes are ranked by cosine similarity and UNIONED with lexical hits. This
|
||||
* lets a paraphrase query surface a node whose words never appear in it.
|
||||
*
|
||||
* DEGRADABLE BY DESIGN. The whole layer is gated on HAVE_CURL plus a one-shot
|
||||
* runtime probe of the embedding endpoint. If curl is not compiled in, or
|
||||
* Ollama is unreachable, or ENGRAM_SEMANTIC=0, every entry point returns
|
||||
* "no semantic signal" and callers fall back to pure lexical behaviour —
|
||||
* byte-for-byte the pre-existing search.
|
||||
*
|
||||
* CACHE. Node embeddings are computed lazily on first use and cached in
|
||||
* process memory keyed by node id, with an FNV-1a content hash for
|
||||
* invalidation (edited content re-embeds). The query is embedded once per
|
||||
* search call. This is what "avoid re-embedding the whole graph every query"
|
||||
* buys us: a warm cache serves cosine from RAM. (A cold process still pays
|
||||
* O(N) embed calls the first time each node is scanned — persisting the cache
|
||||
* to a snapshot sidecar is the documented next step, not done here.)
|
||||
*
|
||||
* nomic task prefixes ("search_query:" / "search_document:") are applied
|
||||
* because nomic-embed-text is trained with them; they materially improve
|
||||
* retrieval separation (empirically: paraphrase 0.72 vs distractors <0.48).
|
||||
*
|
||||
* ENV:
|
||||
* ENGRAM_SEMANTIC "0" disables; unset/other = auto-probe
|
||||
* ENGRAM_EMBED_URL default http://localhost:11434/api/embeddings
|
||||
* ENGRAM_EMBED_MODEL default nomic-embed-text
|
||||
* ENGRAM_SEMANTIC_MIN cosine threshold for a pure-semantic match (def 0.6)
|
||||
* ════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
static double engram_semantic_min(void) {
|
||||
static double v = -1.0;
|
||||
if (v >= 0.0) return v;
|
||||
const char* s = getenv("ENGRAM_SEMANTIC_MIN");
|
||||
double d = 0.6;
|
||||
if (s && *s) { char* e = NULL; double t = strtod(s, &e);
|
||||
if (e != s && t >= 0.0 && t <= 1.0) d = t; }
|
||||
v = d; return v;
|
||||
}
|
||||
|
||||
#ifdef HAVE_CURL
|
||||
|
||||
typedef struct { char* id; uint64_t hash; float* vec; int dim; } EngramEmbEntry;
|
||||
static EngramEmbEntry* g_emb_items = NULL;
|
||||
static int64_t g_emb_count = 0, g_emb_cap = 0;
|
||||
static int g_emb_state = 0; /* 0=unprobed, 1=available, -1=disabled */
|
||||
|
||||
static uint64_t engram_fnv1a(const char* s) {
|
||||
uint64_t h = 1469598103934665603ULL;
|
||||
if (s) for (const unsigned char* p = (const unsigned char*)s; *p; p++) {
|
||||
h ^= *p; h *= 1099511628211ULL;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/* Parse "embedding":[f,f,...] from an Ollama response. malloc'd vec, or NULL. */
|
||||
static float* engram_parse_embedding(const char* json, int* out_dim) {
|
||||
if (!json) return NULL;
|
||||
const char* p = strstr(json, "\"embedding\"");
|
||||
if (!p) return NULL;
|
||||
p = strchr(p, '[');
|
||||
if (!p) return NULL;
|
||||
p++;
|
||||
int cap = 1024, n = 0;
|
||||
float* v = malloc((size_t)cap * sizeof(float));
|
||||
if (!v) return NULL;
|
||||
while (*p && *p != ']') {
|
||||
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++;
|
||||
if (*p == ']' || !*p) break;
|
||||
char* e = NULL;
|
||||
double d = strtod(p, &e);
|
||||
if (e == p) break;
|
||||
if (n >= cap) { cap *= 2; float* nv = realloc(v, (size_t)cap * sizeof(float));
|
||||
if (!nv) { free(v); return NULL; } v = nv; }
|
||||
v[n++] = (float)d;
|
||||
p = e;
|
||||
}
|
||||
if (n == 0) { free(v); return NULL; }
|
||||
*out_dim = n;
|
||||
return v;
|
||||
}
|
||||
|
||||
/* JSON-escape src into a malloc'd buffer (no surrounding quotes). */
|
||||
static char* engram_json_escape(const char* src) {
|
||||
if (!src) src = "";
|
||||
size_t n = strlen(src);
|
||||
char* out = malloc(n * 2 + 1);
|
||||
if (!out) return NULL;
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
unsigned char c = (unsigned char)src[i];
|
||||
if (c == '"') { out[j++] = '\\'; out[j++] = '"'; }
|
||||
else if (c == '\\') { out[j++] = '\\'; out[j++] = '\\'; }
|
||||
else if (c == '\n') { out[j++] = '\\'; out[j++] = 'n'; }
|
||||
else if (c == '\r') { out[j++] = '\\'; out[j++] = 'r'; }
|
||||
else if (c == '\t') { out[j++] = '\\'; out[j++] = 't'; }
|
||||
else if (c < 0x20) { /* drop other control bytes */ }
|
||||
else { out[j++] = (char)c; }
|
||||
}
|
||||
out[j] = '\0';
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Embed `prefix+text` via Ollama. Returns malloc'd vec (caller frees), or NULL. */
|
||||
static float* engram_embed_raw(const char* prefix, const char* text, int* out_dim) {
|
||||
if (!text) return NULL;
|
||||
const char* url = getenv("ENGRAM_EMBED_URL");
|
||||
if (!url || !*url) url = "http://localhost:11434/api/embeddings";
|
||||
const char* model = getenv("ENGRAM_EMBED_MODEL");
|
||||
if (!model || !*model) model = "nomic-embed-text";
|
||||
/* Bound content length to keep latency/memory sane on huge nodes. */
|
||||
char* trunc = NULL;
|
||||
size_t maxlen = 8192;
|
||||
if (strlen(text) > maxlen) {
|
||||
trunc = malloc(maxlen + 1);
|
||||
if (trunc) { memcpy(trunc, text, maxlen); trunc[maxlen] = '\0'; text = trunc; }
|
||||
}
|
||||
char* esc_prefix = engram_json_escape(prefix ? prefix : "");
|
||||
char* esc = engram_json_escape(text);
|
||||
free(trunc);
|
||||
if (!esc || !esc_prefix) { free(esc); free(esc_prefix); return NULL; }
|
||||
size_t blen = strlen(esc) + strlen(esc_prefix) + strlen(model) + 64;
|
||||
char* body = malloc(blen);
|
||||
if (!body) { free(esc); free(esc_prefix); return NULL; }
|
||||
snprintf(body, blen, "{\"model\":\"%s\",\"prompt\":\"%s%s\"}", model, esc_prefix, esc);
|
||||
free(esc); free(esc_prefix);
|
||||
|
||||
CURL* c = curl_easy_init();
|
||||
if (!c) { free(body); return NULL; }
|
||||
HttpBuf rb; httpbuf_init(&rb);
|
||||
struct curl_slist* h = curl_slist_append(NULL, "Content-Type: application/json");
|
||||
char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0';
|
||||
curl_easy_setopt(c, CURLOPT_URL, url);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb);
|
||||
curl_easy_setopt(c, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_POSTFIELDS, body);
|
||||
curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(body));
|
||||
curl_easy_setopt(c, CURLOPT_HTTPHEADER, h);
|
||||
curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms());
|
||||
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf);
|
||||
CURLcode rc = curl_easy_perform(c);
|
||||
curl_slist_free_all(h);
|
||||
curl_easy_cleanup(c);
|
||||
free(body);
|
||||
if (rc != CURLE_OK) { free(rb.data); return NULL; }
|
||||
float* v = engram_parse_embedding(rb.data, out_dim);
|
||||
free(rb.data);
|
||||
return v;
|
||||
}
|
||||
|
||||
/* One-shot probe: is semantic search available? Caches the verdict. */
|
||||
static int engram_semantic_enabled(void) {
|
||||
if (g_emb_state != 0) return g_emb_state == 1;
|
||||
const char* s = getenv("ENGRAM_SEMANTIC");
|
||||
if (s && strcmp(s, "0") == 0) { g_emb_state = -1; return 0; }
|
||||
int dim = 0;
|
||||
float* v = engram_embed_raw("search_query: ", "probe", &dim);
|
||||
if (v && dim > 0) { free(v); g_emb_state = 1; return 1; }
|
||||
free(v);
|
||||
g_emb_state = -1; return 0;
|
||||
}
|
||||
|
||||
/* Embed the query. Returns malloc'd vec (caller frees), or NULL if semantic off. */
|
||||
static float* engram_embed_query(const char* q, int* dim) {
|
||||
if (!engram_semantic_enabled()) return NULL;
|
||||
if (!q || !*q) return NULL;
|
||||
return engram_embed_raw("search_query: ", q, dim);
|
||||
}
|
||||
|
||||
/* Cached node embedding. Returns a pointer OWNED BY THE CACHE — do not free. */
|
||||
static const float* engram_node_vec(EngramNode* n, int* out_dim) {
|
||||
if (!n || !n->id) return NULL;
|
||||
uint64_t h = engram_fnv1a(n->content);
|
||||
for (int64_t i = 0; i < g_emb_count; i++) {
|
||||
if (g_emb_items[i].id && strcmp(g_emb_items[i].id, n->id) == 0) {
|
||||
if (g_emb_items[i].hash == h && g_emb_items[i].vec) {
|
||||
*out_dim = g_emb_items[i].dim; return g_emb_items[i].vec;
|
||||
}
|
||||
/* content changed → re-embed in place */
|
||||
int dim = 0;
|
||||
float* v = engram_embed_raw("search_document: ", n->content ? n->content : "", &dim);
|
||||
if (!v) return NULL;
|
||||
free(g_emb_items[i].vec);
|
||||
g_emb_items[i].vec = v; g_emb_items[i].dim = dim; g_emb_items[i].hash = h;
|
||||
*out_dim = dim; return v;
|
||||
}
|
||||
}
|
||||
int dim = 0;
|
||||
float* v = engram_embed_raw("search_document: ", n->content ? n->content : "", &dim);
|
||||
if (!v) return NULL;
|
||||
if (g_emb_count >= g_emb_cap) {
|
||||
int64_t nc = g_emb_cap ? g_emb_cap * 2 : 256;
|
||||
EngramEmbEntry* ni = realloc(g_emb_items, (size_t)nc * sizeof(EngramEmbEntry));
|
||||
if (!ni) { free(v); return NULL; }
|
||||
g_emb_items = ni; g_emb_cap = nc;
|
||||
}
|
||||
g_emb_items[g_emb_count].id = strdup(n->id);
|
||||
g_emb_items[g_emb_count].hash = h;
|
||||
g_emb_items[g_emb_count].vec = v;
|
||||
g_emb_items[g_emb_count].dim = dim;
|
||||
g_emb_count++;
|
||||
*out_dim = dim; return v;
|
||||
}
|
||||
|
||||
static double engram_cosine(const float* a, const float* b, int dim) {
|
||||
double dot = 0, na = 0, nb = 0;
|
||||
for (int i = 0; i < dim; i++) { dot += (double)a[i] * b[i];
|
||||
na += (double)a[i] * a[i];
|
||||
nb += (double)b[i] * b[i]; }
|
||||
if (na <= 0 || nb <= 0) return 0.0;
|
||||
return dot / (sqrt(na) * sqrt(nb));
|
||||
}
|
||||
|
||||
/* Cosine of node n against the query vector; 0 if unavailable / dim mismatch. */
|
||||
static double engram_node_cosine(EngramNode* n, const float* qvec, int qdim) {
|
||||
if (!qvec || qdim <= 0) return 0.0;
|
||||
int ndim = 0;
|
||||
const float* nv = engram_node_vec(n, &ndim);
|
||||
if (!nv || ndim != qdim) return 0.0;
|
||||
return engram_cosine(qvec, nv, qdim);
|
||||
}
|
||||
|
||||
#else /* !HAVE_CURL — semantic layer compiled out; callers stay pure-lexical.
|
||||
* Only the two boundary functions the always-compiled search/activate
|
||||
* code calls are stubbed; the query embed always yields NULL so every
|
||||
* cosine is 0 and every caller collapses to lexical-only. */
|
||||
static float* engram_embed_query(const char* q, int* dim) { (void)q; (void)dim; return NULL; }
|
||||
static double engram_node_cosine(EngramNode* n, const float* qvec, int qdim) {
|
||||
(void)n; (void)qvec; (void)qdim; return 0.0;
|
||||
}
|
||||
#endif /* HAVE_CURL */
|
||||
|
||||
el_val_t engram_search(el_val_t query, el_val_t limit) {
|
||||
EngramStore* g = engram_get();
|
||||
const char* q = EL_CSTR(query);
|
||||
@@ -7092,8 +7143,15 @@ el_val_t engram_search(el_val_t query, el_val_t limit) {
|
||||
char toks[ENGRAM_MAX_QTOKENS][ENGRAM_QTOK_LEN];
|
||||
int ntok = engram_tokenize_query(q, toks, ENGRAM_MAX_QTOKENS);
|
||||
if (ntok == 0) return lst;
|
||||
/* Semantic augmentation: embed the query once; a node is a hit if it covers
|
||||
* >=1 query token (tokenized-lexical, #66) OR its cosine clears the
|
||||
* threshold (#67). qvec is NULL (cosine 0) when semantic is unavailable →
|
||||
* pure tokenized-lexical, byte-identical to the lexical-only behaviour. */
|
||||
int qdim = 0;
|
||||
float* qvec = engram_embed_query(q, &qdim);
|
||||
double sem_min = engram_semantic_min();
|
||||
EngramRankEntry* hits = malloc((size_t)g->node_count * sizeof(EngramRankEntry));
|
||||
if (!hits) return lst;
|
||||
if (!hits) { free(qvec); return lst; }
|
||||
int64_t nhits = 0;
|
||||
for (int64_t i = 0; i < g->node_count; i++) {
|
||||
EngramNode* n = &g->nodes[i];
|
||||
@@ -7103,20 +7161,24 @@ el_val_t engram_search(el_val_t query, el_val_t limit) {
|
||||
* + 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) {
|
||||
double sem = qvec ? engram_node_cosine(n, qvec, qdim) : 0.0;
|
||||
if (sc > 0 || sem >= sem_min) {
|
||||
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. */
|
||||
/* Rank by distinct tokens matched (desc) then salience (desc), then cap.
|
||||
* Pure-semantic hits (token score 0) sort after every lexical hit — a
|
||||
* lexical ∪ semantic union with lexical precedence. */
|
||||
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]));
|
||||
}
|
||||
free(hits);
|
||||
free(qvec);
|
||||
return lst;
|
||||
}
|
||||
|
||||
@@ -7439,48 +7501,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 +7528,31 @@ 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. */
|
||||
/* Tokenized + semantic seeding: a node seeds if it covers >=1 query token
|
||||
* (tokenized-lexical, #66) OR its cosine clears the threshold (#67). A
|
||||
* lexical seed's activation is scaled by token coverage (fraction of
|
||||
* distinct query tokens covered) so a node matching all words seeds more
|
||||
* strongly than one matching a single word; single-word queries → coverage
|
||||
* 1.0. A pure-semantic seed (no token match) is instead down-weighted by
|
||||
* its cosine so paraphrase matches spread without overpowering exact seeds.
|
||||
* q_vec is NULL (cosine 0) when semantic is unavailable → the seed set is
|
||||
* exactly the tokenized-lexical one. q_vec is freed right after this loop
|
||||
* so the many downstream early-returns need no cleanup change. */
|
||||
char toks[ENGRAM_MAX_QTOKENS][ENGRAM_QTOK_LEN];
|
||||
int ntok = engram_tokenize_query(q, toks, ENGRAM_MAX_QTOKENS);
|
||||
int q_dim = 0;
|
||||
float* q_vec = engram_embed_query(q, &q_dim);
|
||||
double q_sem_min = engram_semantic_min();
|
||||
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) {
|
||||
double sem = q_vec ? engram_node_cosine(n, q_vec, q_dim) : 0.0;
|
||||
if (sc > 0 || sem >= q_sem_min) {
|
||||
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;
|
||||
if (sc > 0) act *= (ntok > 0 ? (double)sc / (double)ntok : 1.0);
|
||||
else act *= sem; /* pure-semantic seed: down-weight by cosine */
|
||||
seeds[seed_count].idx = i;
|
||||
seeds[seed_count].act = act;
|
||||
seeds[seed_count].created_at = n->created_at;
|
||||
@@ -7532,6 +7562,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
reached[i] = 1;
|
||||
}
|
||||
}
|
||||
free(q_vec);
|
||||
/* Compute mean seed created_at for temporal proximity bonus. */
|
||||
int64_t seed_epoch = 0;
|
||||
if (seed_count > 0) {
|
||||
@@ -7686,12 +7717,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 +8094,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;
|
||||
}
|
||||
@@ -8096,14 +8118,16 @@ el_val_t engram_get_node_json(el_val_t id) {
|
||||
* 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.
|
||||
* Used by chat.el to retrieve well-known nodes (e.g. "conv:history",
|
||||
* "session:summary") by their stable label rather than by ID, which is immune
|
||||
* to vector index drift across restarts.
|
||||
*
|
||||
* 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. */
|
||||
* Exact match (strcmp, not istr_contains) because labels like "conv:history"
|
||||
* must not collide with nodes whose content happens to contain that substring.
|
||||
*
|
||||
* Backported verbatim (idiom-adapted to jb_finish) from release runtime
|
||||
* v1.0.0-20260501 to unblock the soul regen link: chat.el references this
|
||||
* native but the current runtime lacked its definition. */
|
||||
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("{}"));
|
||||
@@ -8126,39 +8150,50 @@ el_val_t engram_search_json(el_val_t query, el_val_t limit) {
|
||||
if (lim <= 0) lim = 100;
|
||||
JsonBuf b; jb_init(&b);
|
||||
jb_putc(&b, '[');
|
||||
int first = 1;
|
||||
if (q && *q) {
|
||||
if (q && *q && g->node_count > 0) {
|
||||
/* Collect candidates from the UNION of tokenized-lexical and semantic
|
||||
* matches, score each, rank by score, emit the top `lim`. A node is a
|
||||
* candidate if it covers >=1 query token (tokenized-lexical, #66) OR its
|
||||
* query cosine clears the threshold (#67). Lexical score is the distinct
|
||||
* token count (>=1), so any lexical hit outranks a pure-semantic hit
|
||||
* (cosine < 1); pure-semantic hits are scored by cosine alone. When
|
||||
* semantic is unavailable qvec is NULL, sem is 0, only tokenized-lexical
|
||||
* hits are collected, and the stable insertion sort preserves order. */
|
||||
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++;
|
||||
}
|
||||
int qdim = 0;
|
||||
float* qvec = engram_embed_query(q, &qdim);
|
||||
double sem_min = engram_semantic_min();
|
||||
typedef struct { int64_t idx; double score; } Cand;
|
||||
Cand* cand = malloc((size_t)g->node_count * sizeof(Cand));
|
||||
if (cand) {
|
||||
int64_t nc = 0;
|
||||
for (int64_t i = 0; i < g->node_count; i++) {
|
||||
EngramNode* n = &g->nodes[i];
|
||||
if (engram_layer_is_transparent(n->layer_id)) continue;
|
||||
int sc = engram_node_match_score(n, toks, ntok);
|
||||
double sem = qvec ? engram_node_cosine(n, qvec, qdim) : 0.0;
|
||||
if (sc > 0 || sem >= sem_min) {
|
||||
cand[nc].idx = i;
|
||||
cand[nc].score = (double)sc + sem;
|
||||
nc++;
|
||||
}
|
||||
/* 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);
|
||||
}
|
||||
/* Insertion sort by score desc; stable for equal scores. */
|
||||
for (int64_t i = 1; i < nc; i++) {
|
||||
Cand k = cand[i]; int64_t j = i - 1;
|
||||
while (j >= 0 && cand[j].score < k.score) { cand[j + 1] = cand[j]; j--; }
|
||||
cand[j + 1] = k;
|
||||
}
|
||||
int first = 1;
|
||||
for (int64_t i = 0; i < nc && i < lim; i++) {
|
||||
if (!first) jb_putc(&b, ',');
|
||||
engram_emit_node_json(&b, &g->nodes[cand[i].idx]);
|
||||
first = 0;
|
||||
}
|
||||
free(cand);
|
||||
}
|
||||
free(qvec);
|
||||
}
|
||||
jb_putc(&b, ']');
|
||||
return el_wrap_str(jb_finish(&b));
|
||||
@@ -8676,9 +8711,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;
|
||||
}
|
||||
|
||||
@@ -1072,6 +1072,7 @@ el_val_t __engram_save(el_val_t path) { return engram_save
|
||||
el_val_t __engram_load(el_val_t path) { return engram_load(path); }
|
||||
|
||||
el_val_t __engram_get_node_json(el_val_t id) { return engram_get_node_json(id); }
|
||||
el_val_t __engram_get_node_by_label(el_val_t label) { return engram_get_node_by_label(label); }
|
||||
|
||||
el_val_t __engram_search_json(el_val_t query, el_val_t limit) {
|
||||
return engram_search_json(query, limit);
|
||||
|
||||
@@ -226,6 +226,7 @@ el_val_t __engram_activate(el_val_t query, el_val_t depth);
|
||||
el_val_t __engram_save(el_val_t path);
|
||||
el_val_t __engram_load(el_val_t path);
|
||||
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);
|
||||
|
||||
@@ -2670,6 +2670,7 @@ fn builtin_arity(name: String) -> Int {
|
||||
if str_eq(name, "engram_save") { return 1 }
|
||||
if str_eq(name, "engram_load") { return 1 }
|
||||
if str_eq(name, "engram_get_node_json") { return 1 }
|
||||
if str_eq(name, "engram_get_node_by_label") { return 1 }
|
||||
if str_eq(name, "engram_search_json") { return 2 }
|
||||
if str_eq(name, "engram_scan_nodes_json") { return 2 }
|
||||
if str_eq(name, "engram_neighbors_json") { return 3 }
|
||||
|
||||
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. */
|
||||
|
||||
Reference in New Issue
Block a user