@polycode-projects/the-mechanical-code-talker 2.11.10 → 2.11.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -118,6 +118,13 @@ const COPULA_OF_READ_THROUGH = new Set(["type", "kind", "sort", "form", "class",
118
118
  // bare copula does, unlike any other verb after "is".
119
119
  const COPULA_NAMING_PARTICIPLES = new Set(["termed", "known", "defined", "described", "referred", "called", "classified"]);
120
120
  const COPULA_PARTITIVE_HEADS = new Set(["body", "mass", "group", "collection", "set", "series", "number", "amount", "piece", "part", "lot", "pair", "bunch", "pile"]);
121
+ // The relative pronouns that open a clause predicating about the SENTENCE
122
+ // subject: "a mountain that has lava" is a fact about the volcano, so the
123
+ // relative clause's verb binds to the copula's own subject, not to its object.
124
+ const RELATIVE_PRONOUNS = new Set(["that", "which", "who", "whom", "whose"]);
125
+ // At most this many triples from one sentence — a bound so a run-on can never
126
+ // shatter into noise, not a first-wins cap.
127
+ const MAX_TRIPLES_PER_SENTENCE = 4;
121
128
 
122
129
  /** Fold an entity surface to its stored key: a lexicon noun's lemma, else the
123
130
  * word's own normFactTerm (the optimistic tier mints unlisted content nouns
@@ -144,6 +151,7 @@ function optimisticTriplesPos(sentence, lexicon, nlp) {
144
151
  // folded — "a string instrument" is the class "string instrument", never
145
152
  // its modifier "string"; a single-word run keeps the plain lemma fold.
146
153
  const isNounish = (i) => pos[i] === "NOUN" || pos[i] === "PROPN";
154
+ const runLoOf = (i) => { let lo = i; while (lo - 1 >= 0 && isNounish(lo - 1)) lo -= 1; return lo; };
147
155
  const entityRunAt = (i) => {
148
156
  let lo = i;
149
157
  let hi = i;
@@ -153,18 +161,63 @@ function optimisticTriplesPos(sentence, lexicon, nlp) {
153
161
  const head = lookupNoun(lexicon, String(values[hi]).toLowerCase());
154
162
  return normFactTerm([...values.slice(lo, hi), head ? head.lemma : values[hi]].join(" "));
155
163
  };
156
- const nearestEntity = (idx, step, blocked = null) => {
164
+ const nearestEntityIndex = (idx, step, blocked = null) => {
157
165
  for (let i = idx + step; i >= 0 && i < values.length; i += step) {
158
166
  if (pos[i] === "PUNCT") break;
159
167
  if (blocked && blocked.has(pos[i])) break;
160
- if (isNounish(i)) return entityRunAt(i);
168
+ if (isNounish(i)) return i;
161
169
  }
162
170
  return null;
163
171
  };
164
- const tripleAt = (i, predicate, blocked = null) => {
165
- const subject = nearestEntity(i, -1, blocked);
166
- const object = nearestEntity(i, +1, blocked);
167
- return subject && object && subject !== object ? { subject, predicate, object } : null;
172
+ const nearestEntity = (idx, step, blocked = null) => {
173
+ const i = nearestEntityIndex(idx, step, blocked);
174
+ return i === null ? null : entityRunAt(i);
175
+ };
176
+ // The subject-side mirror of the copula-object of-chain rule: when a found
177
+ // subject run is the inner noun of an of-chain ("the weight of all of the
178
+ // snow …"), climb to the outer run's nominal head ("weight"), bounded to two
179
+ // hops. A classifier head (type/kind/sort/…) reads THROUGH — a "kind of X"
180
+ // outer never becomes the subject, so the inner noun is kept. When the run is
181
+ // governed by "of" but no readable noun heads the chain (a mis-tagged head,
182
+ // e.g. "the top of the mountain …"), return null: an honest abstain, never the
183
+ // inner-noun confusion ("mountain", "snow"). A run not governed by "of" is
184
+ // returned unchanged. Returns a run-lo index to fold, or null to abstain.
185
+ const ofChainSkip = (k) => {
186
+ const p = pos[k];
187
+ return p === "DET" || p === "ADJ" || p === "ADV" || p === "NUM";
188
+ };
189
+ const climbSubjectRun = (found) => {
190
+ let lo = runLoOf(found);
191
+ for (let hop = 0; hop < 2; hop += 1) {
192
+ let g = lo - 1;
193
+ while (g >= 0 && ofChainSkip(g)) g -= 1;
194
+ if (g < 0 || values[g]?.toLowerCase() !== "of") return lo; // not an of-chain object
195
+ let k = g - 1;
196
+ while (k >= 0 && !isNounish(k) && (ofChainSkip(k) || values[k]?.toLowerCase() === "of")) k -= 1;
197
+ if (k < 0 || !isNounish(k)) return null; // no readable head — abstain
198
+ if (COPULA_OF_READ_THROUGH.has(String(values[k]).toLowerCase())) return lo; // classifier reads through
199
+ lo = runLoOf(k);
200
+ }
201
+ return lo;
202
+ };
203
+ // The subject resolution shared by the relation-verb tiers: a run climbed
204
+ // through its of-chain and folded, or null when the of-chain has no readable
205
+ // head (abstain rather than store the inner-noun confusion).
206
+ const climbedSubjectAt = (idx) => {
207
+ const found = nearestEntityIndex(idx, -1);
208
+ if (found === null) return null;
209
+ const climbed = climbSubjectRun(found);
210
+ return climbed === null ? null : entityRunAt(climbed);
211
+ };
212
+ // A relation verb whose nearest content token leftward (skipping adverbs and
213
+ // the auxiliaries of its own verb complex) is a relative pronoun sits in a
214
+ // "that/which …" relative clause — its subject is the sentence subject.
215
+ const inRelativeFrame = (i) => {
216
+ for (let k = i - 1; k >= 0; k -= 1) {
217
+ if (pos[k] === "ADV" || pos[k] === "AUX") continue;
218
+ return RELATIVE_PRONOUNS.has(String(values[k]).toLowerCase());
219
+ }
220
+ return false;
168
221
  };
169
222
  // An isa needs a CLEAN copula frame: only determiners/adjectives/adverbs/
170
223
  // numerals may sit between each entity and the copula. Crossing a verb or
@@ -191,36 +244,87 @@ function optimisticTriplesPos(sentence, lexicon, nlp) {
191
244
  while (hi + 1 < values.length && isNounish(hi + 1)) hi += 1;
192
245
  const headWord = String(values[hi]).toLowerCase();
193
246
  const nextIsOf = values[hi + 1]?.toLowerCase() === "of";
194
- if (!nextIsOf) return entityRunAt(j);
247
+ if (!nextIsOf) return { label: entityRunAt(j), hi };
195
248
  if (COPULA_OF_READ_THROUGH.has(headWord)) { i = hi + 1; j = hi + 1; continue; }
196
249
  if (COPULA_PARTITIVE_HEADS.has(headWord)) return null;
197
- return entityRunAt(j);
250
+ return { label: entityRunAt(j), hi };
198
251
  }
199
252
  return null;
200
253
  };
201
254
  // The copula's own modal chain ("can be", "may be") is part of one verb
202
255
  // complex — the subject scan starts left of it, while a free-standing VERB
203
- // on the way still voids the frame.
256
+ // on the way still voids the frame. An of-chain subject climbs to its head
257
+ // ("the weight of the snow is …" → weight); a mis-headed of-chain abstains.
204
258
  const copulaSubjectAt = (i) => {
205
259
  let k = i - 1;
206
260
  while (k >= 0 && pos[k] === "AUX") k -= 1;
207
- return nearestEntity(k + 1, -1, COPULA_FRAME_BLOCKERS);
261
+ const found = nearestEntityIndex(k + 1, -1, COPULA_FRAME_BLOCKERS);
262
+ if (found === null) return null;
263
+ const climbed = climbSubjectRun(found);
264
+ return climbed === null ? null : entityRunAt(climbed);
208
265
  };
266
+
267
+ const triples = [];
268
+ const seen = new Set();
269
+ const push = (subject, predicate, object) => {
270
+ if (!(subject && object && subject !== object)) return;
271
+ const key = `${subject}\0${predicate}\0${object}`;
272
+ if (seen.has(key) || triples.length >= MAX_TRIPLES_PER_SENTENCE) return;
273
+ seen.add(key);
274
+ triples.push({ subject, predicate, object });
275
+ };
276
+
277
+ // Pass 1 — the first clean copula frame yields the isa (all guards unchanged);
278
+ // its subject and object-run end anchor the relative-clause continuation.
279
+ let copulaSubject = null;
280
+ let copulaObjHi = -1;
209
281
  for (let i = 1; i < values.length - 1; i += 1) {
210
282
  if (pos[i] === "AUX" && OPTIMISTIC_COPULAS.has(values[i].toLowerCase())) {
211
283
  const subject = copulaSubjectAt(i);
212
284
  const object = copulaObjectAt(i);
213
- if (subject && object && subject !== object) return [{ subject, predicate: "rdfs:subClassOf", object }];
285
+ if (subject && object && subject !== object.label) {
286
+ push(subject, "rdfs:subClassOf", object.label);
287
+ copulaSubject = subject;
288
+ copulaObjHi = object.hi;
289
+ break;
290
+ }
291
+ }
292
+ }
293
+
294
+ // Pass 2a — with a copula isa in hand, CONTINUE past its object for relation
295
+ // verbs (has/creates/…), so one sentence contributes every fact it grounds.
296
+ // A "that/which <verb>" clause right after the object predicates about the
297
+ // SENTENCE subject ("a mountain that has lava" → volcano has lava); any other
298
+ // relation verb keeps its nearest-entity-leftward subject. AUX relation verbs
299
+ // ("has") count here — but only inside a copula frame that already resolved,
300
+ // so a bare "… is that Earth has …" complement never mints "earth has lot".
301
+ if (copulaSubject) {
302
+ for (let i = copulaObjHi + 1; i < values.length; i += 1) {
303
+ if (pos[i] !== "VERB" && pos[i] !== "AUX") continue;
304
+ const word = values[i].toLowerCase();
305
+ if (OPTIMISTIC_COPULAS.has(word)) continue;
306
+ const verb = lookupVerb(lexicon, word);
307
+ if (!verb) continue;
308
+ const subject = inRelativeFrame(i) ? copulaSubject : climbedSubjectAt(i);
309
+ if (subject === null) continue;
310
+ push(subject, predicateOf(verb), nearestEntity(i, +1));
214
311
  }
312
+ return triples;
215
313
  }
314
+
315
+ // Pass 2b — no copula isa: the relation-verb tier over the whole sentence,
316
+ // climbing an of-chain subject to its head ("the weight of the snow creates
317
+ // pressure" → weight creates pressure, not snow). VERB-tagged only, so a bare
318
+ // AUX ("Earth has …") in a non-frame sentence stays an honest miss.
216
319
  for (let i = 1; i < values.length - 1; i += 1) {
217
320
  if (pos[i] !== "VERB") continue;
218
321
  const verb = lookupVerb(lexicon, values[i].toLowerCase());
219
322
  if (!verb) continue;
220
- const t = tripleAt(i, predicateOf(verb));
221
- if (t) return [t];
323
+ const subject = climbedSubjectAt(i);
324
+ if (subject === null) continue;
325
+ push(subject, predicateOf(verb), nearestEntity(i, +1));
222
326
  }
223
- return [];
327
+ return triples;
224
328
  }
225
329
 
226
330
  /** The lexical fallback for a checkout with no wink model: a copula flanked by
@@ -254,11 +358,15 @@ function optimisticTriplesLexical(sentence, lexicon) {
254
358
  }
255
359
 
256
360
  /**
257
- * A bounded triple candidate from a sentence the strict recognizer skipped: a
258
- * copula (→ rdfs:subClassOf) or a lexicon-known relation verb (→ its predicate)
259
- * flanked by two entities. At most one triple per sentence; [] when nothing
260
- * resolves both sides no guessing past the shape. Uses wink POS tags when a
261
- * model is available (the precise tier), else a narrower lexicon-only fallback.
361
+ * The bounded triple candidates from a sentence the strict recognizer skipped:
362
+ * a copula (→ rdfs:subClassOf) and, past its object, the relation verbs it
363
+ * grounds (→ their predicates), so one sentence contributes every fact it holds
364
+ * ("a volcano is a mountain that has lava" volcano mountain AND volcano has
365
+ * lava). Every triple passes the same entity/guard checks on its own, deduped,
366
+ * capped at MAX_TRIPLES_PER_SENTENCE so a run-on never shatters into noise; []
367
+ * when nothing resolves both sides — no guessing past the shape. Uses wink POS
368
+ * tags when a model is available (the precise tier), else a narrower
369
+ * lexicon-only fallback.
262
370
  *
263
371
  * opts.lexicon a loaded lexicon (the core vocabulary when absent).
264
372
  * opts.nlp a wink instance (winkInstance() when absent); null forces the