elp(propositions): native-el READ primitive — memory text -> SACRED triples
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).
This commit is contained in:
@@ -81,6 +81,7 @@ build {
|
||||
"src/realizer.el",
|
||||
"src/semantics.el",
|
||||
"src/comprehend.el",
|
||||
"src/propositions.el",
|
||||
"src/elp.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)
|
||||
}
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user