@polycode-projects/the-mechanical-code-talker 2.11.9 → 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.
- package/README.md +52 -51
- package/package.json +1 -1
- package/src/adapters/research-queue-store.mjs +76 -0
- package/src/domain/grammar/ace.mjs +8 -1
- package/src/services/adventure-viz.mjs +156 -6
- package/src/services/adventure.mjs +65 -6
- package/src/services/chat.mjs +16 -2
- package/src/services/extract-facts.mjs +127 -19
- package/src/surfaces/web/memory-ask-browser.bundle.js +105 -105
|
@@ -556,6 +556,44 @@ export function worldDigestRows(rows, state) {
|
|
|
556
556
|
return out;
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
/** The physical-property lines a close "look <object>" states: the object's
|
|
560
|
+
* own world facts, phrased through the SAME worldDigestRows view a room look
|
|
561
|
+
* reads (so mgx:knows-*, the NPC schedule, is-objective and the rest of the
|
|
562
|
+
* puzzle wiring are already excluded there), minus its bare rdf:type line —
|
|
563
|
+
* the class hierarchy renders that as its own is-a chain instead. A carried
|
|
564
|
+
* object surfaces through the "carries the" line the digest already produces.
|
|
565
|
+
* Pure. */
|
|
566
|
+
export function objectLookProperties(rows, state, object) {
|
|
567
|
+
const subjectCased = sentenceCase(object);
|
|
568
|
+
return worldDigestRows(rows, state)
|
|
569
|
+
.filter((r) => (r.subject === subjectCased && r.predicate !== "is a" && r.predicate !== "is an")
|
|
570
|
+
|| (r.predicate === "carries the" && r.object === object))
|
|
571
|
+
.map((r) => `${r.subject} ${r.predicate} ${r.object}.`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** An object's class hierarchy as an is-a chain, nearest-first and opening
|
|
575
|
+
* with the object itself ("housekeeper → person"): a breadth-first walk up
|
|
576
|
+
* the world's OWN rdf:type and rdfs:subClassOf edges (worldActionRows, so a
|
|
577
|
+
* merged corpus's taxonomy for the same word never joins the chain), the same
|
|
578
|
+
* upward-class rendering chat's "what do you know about X" shows. Pure. */
|
|
579
|
+
export function objectClassChain(rows, object) {
|
|
580
|
+
const worldRows = worldActionRows(rows);
|
|
581
|
+
const parentsOf = (node) => worldRows
|
|
582
|
+
.filter((r) => r.subject === node && (r.predicate === "rdf:type" || r.predicate === "rdfs:subClassOf"))
|
|
583
|
+
.map((r) => r.object);
|
|
584
|
+
const seen = new Set([object]);
|
|
585
|
+
const chain = [object];
|
|
586
|
+
const queue = [...parentsOf(object)];
|
|
587
|
+
while (queue.length) {
|
|
588
|
+
const node = queue.shift();
|
|
589
|
+
if (seen.has(node)) continue;
|
|
590
|
+
seen.add(node);
|
|
591
|
+
chain.push(node);
|
|
592
|
+
queue.push(...parentsOf(node));
|
|
593
|
+
}
|
|
594
|
+
return chain;
|
|
595
|
+
}
|
|
596
|
+
|
|
559
597
|
async function worldDigest(prompt, { memoryDir, memory, rows, state, graph }) {
|
|
560
598
|
const view = worldDigestRows(rows, state);
|
|
561
599
|
const store = {
|
|
@@ -689,7 +727,7 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
|
|
|
689
727
|
);
|
|
690
728
|
}
|
|
691
729
|
|
|
692
|
-
if (cmd.verb === "look") {
|
|
730
|
+
if (cmd.verb === "look" && !cmd.object) {
|
|
693
731
|
const digest = await worldDigest(here, { memoryDir, memory, rows, state, graph });
|
|
694
732
|
const actions = roomAffordances(rows, state, here);
|
|
695
733
|
return answer(
|
|
@@ -699,13 +737,13 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
|
|
|
699
737
|
);
|
|
700
738
|
}
|
|
701
739
|
|
|
702
|
-
if (cmd.verb === "examine" || cmd.verb === "talk") {
|
|
740
|
+
if (cmd.verb === "examine" || cmd.verb === "talk" || cmd.verb === "look") {
|
|
703
741
|
const object = cmd.object;
|
|
704
742
|
// A carried object has no room to be "visible in" (visibleRoomOf returns
|
|
705
|
-
// null for anything held by the player) — examine still
|
|
706
|
-
// the same way "what am I carrying" already reads inventory contents.
|
|
743
|
+
// null for anything held by the player) — examine and look still apply to
|
|
744
|
+
// it, the same way "what am I carrying" already reads inventory contents.
|
|
707
745
|
// talk has no carried exception: NPCs are never portable.
|
|
708
|
-
const carried = cmd.verb === "examine" && carriedByPlayer(state, object);
|
|
746
|
+
const carried = (cmd.verb === "examine" || cmd.verb === "look") && carriedByPlayer(state, object);
|
|
709
747
|
// The room the player is standing in is never the SUBJECT of a placement
|
|
710
748
|
// fact (only ever the OBJECT other things are placed in), so
|
|
711
749
|
// visibleRoomOf(object) can never equal `here` for a room's own name —
|
|
@@ -730,7 +768,7 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
|
|
|
730
768
|
{ miss: true },
|
|
731
769
|
);
|
|
732
770
|
}
|
|
733
|
-
if (notHere && !(cmd.verb === "examine" && backgroundOnlyMention(rows, state, object))) {
|
|
771
|
+
if (notHere && !((cmd.verb === "examine" || cmd.verb === "look") && backgroundOnlyMention(rows, state, object))) {
|
|
734
772
|
return answer(
|
|
735
773
|
`I don't see a ${object} here.`,
|
|
736
774
|
noteFor(`${cmd.verb} — ${object} isn't visible in the ${here}; declined, hidden things stay hidden`),
|
|
@@ -738,6 +776,27 @@ async function runWorldCommand(cmd, { world, memoryDir, env, graph, cache }) {
|
|
|
738
776
|
);
|
|
739
777
|
}
|
|
740
778
|
const person = isTyped(rows, object, "person");
|
|
779
|
+
// "look <object>" on a real placed prop is the grounded close look: every
|
|
780
|
+
// physical fact the world writes about the thing (its placement, its
|
|
781
|
+
// within-room position, any datatype property — all via the SAME
|
|
782
|
+
// worldDigestRows view that already drops the puzzle wiring and the
|
|
783
|
+
// staff-knowledge pointers), plus its class hierarchy as an is-a chain,
|
|
784
|
+
// plus a container's open/locked state. A background-only mention has no
|
|
785
|
+
// placed facts of its own, so it falls through to the examine digest below
|
|
786
|
+
// (the same general-knowledge answer "what is a flower" gives).
|
|
787
|
+
if (cmd.verb === "look" && !backgroundOnlyMention(rows, state, object)) {
|
|
788
|
+
const propLines = objectLookProperties(rows, state, object);
|
|
789
|
+
const chain = objectClassChain(rows, object);
|
|
790
|
+
const parts = [`you look closely at the ${object}.`];
|
|
791
|
+
if (propLines.length) parts.push(propLines.join(" "));
|
|
792
|
+
if (chain.length > 1) parts.push(`Class: ${chain.join(" → ")}.`);
|
|
793
|
+
if (!person && isContainer(rows, object)) parts.push(containerStatusPhrase(object, { state }));
|
|
794
|
+
return answer(
|
|
795
|
+
parts.join(" "),
|
|
796
|
+
noteFor(`look at ${object} — its world-fact properties (via worldDigestRows, knows-*/puzzle-wiring excluded) and its rdf:type/subClassOf is-a chain`),
|
|
797
|
+
{ goal: `take a closer look at the ${object}` },
|
|
798
|
+
);
|
|
799
|
+
}
|
|
741
800
|
// Talking to a person is the game's reveal channel: the staff share what
|
|
742
801
|
// they know (a hiding place, the quest, a topic) and report their own
|
|
743
802
|
// room, all resolved from the live fold this turn.
|
package/src/services/chat.mjs
CHANGED
|
@@ -52,7 +52,8 @@ import {
|
|
|
52
52
|
} from "../domain/reference-pack.mjs";
|
|
53
53
|
import { getReferencePackProvider } from "../adapters/corpus/reference-pack.mjs";
|
|
54
54
|
import { getLiveReferenceProvider, getResearchProvider } from "../adapters/corpus/wikipedia-live.mjs";
|
|
55
|
-
import { researchTurn, researchSnapshot, resolveResearchConfig, RESEARCH_DEFAULTS } from "./research.mjs";
|
|
55
|
+
import { researchTurn, researchSnapshot, resolveResearchConfig, RESEARCH_DEFAULTS, parseResearchRequest } from "./research.mjs";
|
|
56
|
+
import { loadResearchQueue, saveResearchQueue } from "../adapters/research-queue-store.mjs";
|
|
56
57
|
import { CHILD_PACK_NAME, childProvenanceTag } from "../domain/child-pack.mjs";
|
|
57
58
|
import { getChildPackProvider } from "../adapters/corpus/child-pack.mjs";
|
|
58
59
|
import { dialogueActForLane } from "../domain/dialogue-acts.mjs";
|
|
@@ -14548,7 +14549,16 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
14548
14549
|
// behind /wiki on. Queue state threads turn-to-turn as researchState, the
|
|
14549
14550
|
// same way planState does.
|
|
14550
14551
|
{
|
|
14551
|
-
|
|
14552
|
+
// A fresh CLI session carries no in-memory queue, so a research-family line
|
|
14553
|
+
// arriving with none resumes the queue persisted under .tmct/ — that is
|
|
14554
|
+
// what makes "research next"/"status"/"stop" work across process restarts.
|
|
14555
|
+
// The gate keeps ordinary turns off the disk (only a parsed research line
|
|
14556
|
+
// loads), and a store with no path (the browser) simply reads back null.
|
|
14557
|
+
let priorResearchState = researchState;
|
|
14558
|
+
if (!priorResearchState && parseResearchRequest(workingLine)) {
|
|
14559
|
+
priorResearchState = await loadResearchQueue(memoryDir);
|
|
14560
|
+
}
|
|
14561
|
+
const researchHolder = { state: priorResearchState };
|
|
14552
14562
|
const resolvedResearchConfig = researchConfig ?? RESEARCH_DEFAULTS;
|
|
14553
14563
|
const rTurn = await researchTurn(workingLine, {
|
|
14554
14564
|
holder: researchHolder,
|
|
@@ -14569,6 +14579,10 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
14569
14579
|
result.lane = "research";
|
|
14570
14580
|
const snapshot = researchSnapshot(researchHolder.state);
|
|
14571
14581
|
if (snapshot) result.record.research = snapshot;
|
|
14582
|
+
// Write-through: persist the queue this turn just mutated (start, next,
|
|
14583
|
+
// skip), and clear the file when it ended (stop, or a failed start that
|
|
14584
|
+
// left no run). A store with no path no-ops, so the browser is untouched.
|
|
14585
|
+
await saveResearchQueue(memoryDir, researchHolder.state);
|
|
14572
14586
|
const rec = withLast(result, rTurn.goal);
|
|
14573
14587
|
rec.planState = planHolder.state;
|
|
14574
14588
|
rec.researchState = researchHolder.state;
|
|
@@ -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
|
|
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
|
|
168
|
+
if (isNounish(i)) return i;
|
|
161
169
|
}
|
|
162
170
|
return null;
|
|
163
171
|
};
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
221
|
-
if (
|
|
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
|
-
*
|
|
258
|
-
* copula (→ rdfs:subClassOf)
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
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
|