From 335298a518d7233e18157343a607876b659173fc Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Thu, 13 Aug 2026 14:56:57 -0500 Subject: [PATCH] =?UTF-8?q?elp(propositions):=20native-el=20READ=20primiti?= =?UTF-8?q?ve=20=E2=80=94=20memory=20text=20->=20SACRED=20triples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 piece 1. Ports propositions.py off spaCy: the dependency-parser role is now the el-native parser (parse_spec), and each memory sentence's meaning-spec IS the triple (subject, predicate, object, modifiers, polarity, tense, source, confidence). Sentence segmentation + repr parity with propositions.py. NEGATION SACRED: polarity flows straight from the spec, never dropped/inverted. Gate (propositions_gate.el): 4/4 SACRED polarity correct on extraction; multi-sentence memory splits one triple per sentence in reading order with negation preserved. Built bounded (elc rc=0 peak 24MB, cc rc=0). Gap (honest): English regular-verb lemmatizer does not restore silent-e (stores->stor); coreference/passive normalization from the reference not yet ported (shallow pronoun subject kept as surface). --- elp/manifest.el | 1 + elp/src/propositions.el | 140 +++++++++++++++++++++++++++++++++ elp/tests/propositions_gate.el | 52 ++++++++++++ 3 files changed, 193 insertions(+) create mode 100644 elp/src/propositions.el create mode 100644 elp/tests/propositions_gate.el diff --git a/elp/manifest.el b/elp/manifest.el index 3d212fb..0a1f939 100644 --- a/elp/manifest.el +++ b/elp/manifest.el @@ -81,6 +81,7 @@ build { "src/realizer.el", "src/semantics.el", "src/comprehend.el", + "src/propositions.el", "src/elp.el", ] } diff --git a/elp/src/propositions.el b/elp/src/propositions.el new file mode 100644 index 0000000..86d6da5 --- /dev/null +++ b/elp/src/propositions.el @@ -0,0 +1,140 @@ +// propositions.el - the READ primitive over the engram's OWN memories, native el. +// +// Free memory text -> structured PROPOSITIONS (triples): +// (subject, predicate, object, modifiers, polarity, tense, source, confidence) +// +// This is comprehension turned inward: the Python reference (propositions.py) ran +// spaCy's dependency parser over each memory sentence and walked the arcs. Here +// the spaCy role is filled by the el-native parser (comprehend.el / parse_spec): +// each sentence is parsed to a meaning-spec, and the spec's roles ARE the triple. +// Nothing generates text. NEGATION IS SACRED: polarity flows straight from the +// spec's polarity field and is never dropped or inverted. +// +// Depends on: comprehend (parse_spec / parse_spec_lang), grammar (slots_get). + +// ── sentence segmentation ───────────────────────────────────────────────────── +// Split on sentence-final punctuation (. ! ?) and hard newlines. Markdown/long +// memories are handled shallowly (the reference caps + ranks by query overlap; +// that ranking belongs to the dialogue layer, not here). + +fn prop_is_boundary(c: String) -> Bool { + if str_eq(c, ".") { return true } + if str_eq(c, "!") { return true } + if str_eq(c, "?") { return true } + if str_eq(c, "\n") { return true } + return false +} + +fn prop_split_sentences(text: String) -> [String] { + let out: [String] = native_list_empty() + let n: Int = str_len(text) + let start: Int = 0 + let i: Int = 0 + while i < n { + let c: String = str_slice(text, i, i + 1) + if prop_is_boundary(c) { + let seg: String = str_slice(text, start, i + 1) + let trimmed: String = cp_trim_punct(seg) + if !str_eq(trimmed, "") { + let out = native_list_append(out, seg) + } + let start = i + 1 + } + let i = i + 1 + } + if start < n { + let seg: String = str_slice(text, start, n) + let trimmed: String = cp_trim_punct(seg) + if !str_eq(trimmed, "") { + let out = native_list_append(out, seg) + } + } + return out +} + +// ── spec -> proposition record ──────────────────────────────────────────────── +// A proposition is a slot map (same [String] shape as the spec) with the READ +// contract keys. Modifiers fold the spec's location + iobj adjuncts. + +fn prop_confidence(subject: String, predicate: String, object: String) -> String { + if str_eq(predicate, "") { return "0.0" } + if str_eq(subject, "") { return "0.4" } + if str_eq(object, "") { return "0.7" } + return "1.0" +} + +fn prop_modifiers(spec: [String]) -> String { + let loc: String = slots_get(spec, "location") + let iobj: String = slots_get(spec, "iobj") + let parts: [String] = native_list_empty() + if !str_eq(loc, "") { let parts = native_list_append(parts, loc) } + if !str_eq(iobj, "") { let parts = native_list_append(parts, "to " + iobj) } + return str_join(parts, "; ") +} + +fn prop_from_spec(spec: [String], source_id: String) -> [String] { + let subject: String = slots_get(spec, "agent") + let predicate: String = slots_get(spec, "predicate") + let object: String = slots_get(spec, "patient") + let polarity: String = slots_get(spec, "polarity") + let tense: String = slots_get(spec, "tense") + let mods: String = prop_modifiers(spec) + let conf: String = prop_confidence(subject, predicate, object) + + let p: [String] = native_list_empty() + let p = native_list_append(p, "subject"); let p = native_list_append(p, subject) + let p = native_list_append(p, "predicate"); let p = native_list_append(p, predicate) + let p = native_list_append(p, "object"); let p = native_list_append(p, object) + let p = native_list_append(p, "modifiers"); let p = native_list_append(p, mods) + let p = native_list_append(p, "polarity"); let p = native_list_append(p, polarity) + let p = native_list_append(p, "tense"); let p = native_list_append(p, tense) + let p = native_list_append(p, "source"); let p = native_list_append(p, source_id) + let p = native_list_append(p, "confidence"); let p = native_list_append(p, conf) + return p +} + +// Extract one proposition from a single sentence (given language). +fn prop_extract_one_lang(sentence: String, lang: String, source_id: String) -> [String] { + let spec: [String] = parse_spec_lang(sentence, lang) + return prop_from_spec(spec, source_id) +} + +fn prop_extract_one(sentence: String, source_id: String) -> [String] { + return prop_extract_one_lang(sentence, "en", source_id) +} + +// Render a proposition as a compact trace line (repr parity with propositions.py). +fn prop_repr(p: [String]) -> String { + let neg: String = "" + if str_eq(slots_get(p, "polarity"), "neg") { let neg = "NOT " } + let mods: String = slots_get(p, "modifiers") + let modstr: String = "" + if !str_eq(mods, "") { let modstr = " [" + mods + "]" } + let s: String = "(" + slots_get(p, "subject") + " -" + neg + slots_get(p, "predicate") + let s = s + "-> " + slots_get(p, "object") + modstr + let s = s + " conf=" + slots_get(p, "confidence") + ")" + return s +} + +// Extract all propositions from a memory's text (one per sentence). Returns a +// flat [String] whose entries are the prop_repr trace lines, in reading order. +fn prop_extract_lang(text: String, lang: String, source_id: String) -> [String] { + let sents: [String] = prop_split_sentences(text) + let m: Int = native_list_len(sents) + let out: [String] = native_list_empty() + let i: Int = 0 + while i < m { + let sent: String = native_list_get(sents, i) + let p: [String] = prop_extract_one_lang(sent, lang, source_id) + // drop empty parses (no predicate recovered): honest partial, not noise. + if !str_eq(slots_get(p, "predicate"), "") { + let out = native_list_append(out, prop_repr(p)) + } + let i = i + 1 + } + return out +} + +fn prop_extract(text: String, source_id: String) -> [String] { + return prop_extract_lang(text, "en", source_id) +} diff --git a/elp/tests/propositions_gate.el b/elp/tests/propositions_gate.el new file mode 100644 index 0000000..2f048ad --- /dev/null +++ b/elp/tests/propositions_gate.el @@ -0,0 +1,52 @@ +// propositions_gate.el - the READ primitive over memory text (native el). +// Proves triples are recovered from free memory text and that SACRED polarity +// survives extraction (a negative memory must yield a NOT-triple). + +fn pg_check(text: String, want_pol: String) -> String { + let p: [String] = prop_extract_one(text, "nd-test") + let pol: String = slots_get(p, "polarity") + let ok: String = "MISMATCH" + if str_eq(pol, want_pol) { let ok = "ok" } + return " " + prop_repr(p) + " pol=" + pol + " expected=" + want_pol + " (" + ok + ")\n" +} + +fn pg_pol_ok(text: String, want_pol: String) -> Int { + let p: [String] = prop_extract_one(text, "nd-test") + if str_eq(slots_get(p, "polarity"), want_pol) { return 1 } + return 0 +} + +fn run_prop_gate() -> String { + let m1: String = "Neuron stores memories in SQLite." + let m2: String = "The engram does not delete a memory." + let m3: String = "Salience never drops the negation." + let m4: String = "The teacher gives the book to the children." + + let rep: String = "==== ELP proposition extraction (memory text -> triples) ====\n" + let rep = rep + pg_check(m1, "aff") + let rep = rep + pg_check(m2, "neg") + let rep = rep + pg_check(m3, "neg") + let rep = rep + pg_check(m4, "aff") + + // multi-sentence memory: one triple per sentence, order preserved + let doc: String = "Neuron persists learning. It does not forget the library." + let props: [String] = prop_extract(doc, "nd-doc") + let rep = rep + " --- multi-sentence doc (" + int_to_str(native_list_len(props)) + " props) ---\n" + let di: Int = 0 + while di < native_list_len(props) { + let rep = rep + " " + native_list_get(props, di) + "\n" + let di = di + 1 + } + + let ok: Int = 0 + if pg_pol_ok(m1, "aff") == 1 { let ok = ok + 1 } + if pg_pol_ok(m2, "neg") == 1 { let ok = ok + 1 } + if pg_pol_ok(m3, "neg") == 1 { let ok = ok + 1 } + if pg_pol_ok(m4, "aff") == 1 { let ok = ok + 1 } + let rep = rep + "-----------------------------------------------------------------\n" + let rep = rep + "SACRED polarity correct on extraction: " + int_to_str(ok) + "/4\n" + if ok == 4 { let rep = rep + "PROP GATE: PASS\n" } else { let rep = rep + "PROP GATE: FAIL\n" } + return rep +} + +println(run_prop_gate())