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 };