File size: 2,971 Bytes
8494d00 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | // 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 ", <rest>" — 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 };
|