// ingest.js — prose → atomic facts. Real text is compound ("Aldridge, a Boston // firm founded by Voss, who trained at MIT, makes Theralin"); the reasoner needs // ONE relation per fact. Decompose by appositive / relative-clause / conjunction, // propagating the subject. Heuristic, not a parser — measured honestly. Each // emitted fact is a verbatim-ish span of the source (bound preserved at reason // time because facts ARE the corpus). 'use strict'; function sentences(text) { return text.replace(/\s+/g, ' ').split(/(?<=[.!?])\s+/).map(s => s.trim()).filter(Boolean); } // the leading subject noun-phrase of a sentence (proper-noun phrase or first NP) function leadSubject(s) { const m = s.match(/^((?:The|A|An)\s+)?([A-Z][A-Za-z]+(?:\s+[A-Z][a-z]+)*)/); return m ? (m[1] || '') + m[2] : (s.split(/\s+/).slice(0, 2).join(' ')); } function decompose(sentence) { const out = []; let s = sentence.replace(/\s+/g, ' ').trim(); const subj = leadSubject(s); // 1) APPOSITIVE right after the subject: "X, a/the Y, ..." -> "X is a Y." const appo = s.match(/^(.+?),\s+(a|an|the)\s+([^,]+?),\s+(.*)$/i); if (appo && /^[A-Z]/.test(appo[1].trim()) && appo[1].split(/\s+/).length <= 4) { out.push(`${appo[1].trim()} is ${appo[2]} ${appo[3].trim()}.`); s = `${appo[1].trim()} ${appo[4].trim()}`; } // 2) RELATIVE clause: "..., who/which/that VERB ..." -> new fact with the // nearest preceding noun as subject. const rel = s.match(/^(.*?\b([A-Z][A-Za-z]+(?:\s+[A-Z][a-z]+)*|\w+))\s*,?\s+(who|which|that)\s+(.+)$/); if (rel) { const head = s.slice(0, rel[1].length).trim().replace(/,$/, ''); const antecedent = rel[2]; let tail = rel[4].trim(); // tail may itself end the main clause with ", " — keep the relative part const tailCut = tail.match(/^(.+?)(?:,\s+(?:and\s+)?(.*))?$/); out.push(`${cap(antecedent)} ${tailCut[1].trim()}`.replace(/\.*$/, '.')); s = head + (tailCut && tailCut[2] ? ' ' + tailCut[2] : ''); } // 3) CONJUNCTION of predicates: "X verbs A and verbs B" -> two facts (only if // the second half starts with a verb-ish word, not a new subject). const conj = s.match(/^(.+?)\s+and\s+(.+)$/); if (conj && !/^[A-Z]/.test(conj[2].trim()) && /^(is|are|was|were|has|have|treats?|requires?|produces?|makes?|forbids?|bans?|owns?|can|may)\b/i.test(conj[2].trim())) { out.push(clean(conj[1])); out.push(clean(`${subj} ${conj[2]}`)); } else { out.push(clean(s)); } return out.map(f => f.replace(/\s+/g, ' ').trim()).filter(f => f.split(/\s+/).length >= 3); } function cap(w) { return w.charAt(0).toUpperCase() + w.slice(1); } function clean(s) { s = s.trim().replace(/\s+/g, ' '); if (!/[.!?]$/.test(s)) s += '.'; return s; } function ingest(text) { const facts = []; for (const sent of sentences(text)) for (const f of decompose(sent)) if (!facts.includes(f)) facts.push(f); return facts; } module.exports = { ingest, decompose, sentences };