@polycode-projects/the-mechanical-code-talker 5.0.6 → 5.0.8
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 +21 -0
- package/bin/tmct.mjs +63 -2
- package/package.json +3 -2
- package/src/adapters/memory/core.mjs +23 -0
- package/src/domain/ask-vocab.mjs +19 -0
- package/src/domain/ask.mjs +8 -1
- package/src/domain/interpret/strategies/keywords.mjs +30 -1
- package/src/domain/memory/capability.mjs +15 -11
- package/src/domain/router/drive.mjs +36 -17
- package/src/domain/router/resolver.mjs +63 -17
- package/src/domain/spider-fly-world.mjs +2 -2
- package/src/domain/sprite-templates.mjs +19 -7
- package/src/domain/syllogise.mjs +16 -6
- package/src/domain/town-square-world.mjs +1 -1
- package/src/services/adventure.mjs +8 -1
- package/src/services/chat-page-viz.mjs +118 -23
- package/src/services/chat-session.mjs +60 -10
- package/src/services/chat.mjs +228 -24
- package/src/services/extract-facts.mjs +47 -7
- package/src/services/ingest-viz.mjs +108 -25
- package/src/services/ledger-viz.mjs +4 -2
- package/src/services/memory-panel-viz.mjs +44 -0
- package/src/services/mud-viz.mjs +17 -0
- package/src/services/mudiii-scene.mjs +271 -24
- package/src/services/mudiii-turn.mjs +65 -9
- package/src/services/mudiii-viz.mjs +302 -125
- package/src/services/plan-viz.mjs +23 -2
- package/src/services/predator-prey.mjs +82 -33
- package/src/services/research-viz.mjs +12 -19
- package/src/services/spider-fly-turn.mjs +7 -1
- package/src/services/spider-fly-viz.mjs +10 -3
- package/src/services/viz-ticker.mjs +15 -2
- package/src/surfaces/http/server-http.mjs +90 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +125 -125
- package/src/surfaces/web/mud-browser-entry.mjs +33 -1
- package/src/surfaces/web/tmct-surface.mjs +18 -6
- package/src/tools/handlers/tmct-ask.mjs +15 -2
- package/src/tools/server.mjs +31 -2
package/src/services/chat.mjs
CHANGED
|
@@ -491,6 +491,11 @@ const countClass = (graph, cls) => (cls === "Package"
|
|
|
491
491
|
? packageCounts(modulesOf(graph)).size
|
|
492
492
|
: graph.individuals.filter((i) => (i.class || "") === cls).length);
|
|
493
493
|
|
|
494
|
+
/** Every kind the counter answers to, as a human list — what it can count once a
|
|
495
|
+
* graph is loaded, as opposed to countableKinds, which is what THIS graph holds. */
|
|
496
|
+
const COUNTABLE_KIND_WORDS = [...new Set(Object.values(COUNT_NOUNS))]
|
|
497
|
+
.map((cls) => CLASS_LABELS[cls]?.[1] ?? `${cls}s`);
|
|
498
|
+
|
|
494
499
|
/** The classes this graph can actually count, as a human list ("classes, functions, …"). */
|
|
495
500
|
function countableKinds(graph) {
|
|
496
501
|
const present = new Set(graph.individuals.map((i) => i.class).filter(Boolean));
|
|
@@ -623,8 +628,15 @@ export function answerCount(graph, query, { uiContext = "cli" } = {}) {
|
|
|
623
628
|
if (uiContext === "browser") {
|
|
624
629
|
return `I can't count "${noun}" — this page holds taught facts only, so there's no code structure to count.`;
|
|
625
630
|
}
|
|
626
|
-
|
|
627
|
-
|
|
631
|
+
// Two true things, said as two. The REASON is the noun: nothing here
|
|
632
|
+
// counts it, and no amount of indexing would, so "no code graph is
|
|
633
|
+
// loaded, so there's nothing to count" was the wrong cause and read as a
|
|
634
|
+
// promise that indexing would let it count moons. The REMEDY is about the
|
|
635
|
+
// session, and a terminal can act on it, so it stays — worded as its own
|
|
636
|
+
// fact rather than as the answer to what was asked.
|
|
637
|
+
return `I can't count "${noun}" — I count the kinds a code graph holds: ${COUNTABLE_KIND_WORDS.join(", ")}. `
|
|
638
|
+
+ `This session has no code graph loaded either — index this repo with "tmct index", `
|
|
639
|
+
+ `point me at another with --repo, or run "npm run example:mini".`;
|
|
628
640
|
}
|
|
629
641
|
return `I can't count "${noun}". I count: ${kinds.join(", ")}. ` +
|
|
630
642
|
`Try "how many classes are there".`;
|
|
@@ -1425,8 +1437,8 @@ const SESSION_REFERENT_TERMS = new Set([
|
|
|
1425
1437
|
"going", "happening", "possible", "available", "supported", "included",
|
|
1426
1438
|
]);
|
|
1427
1439
|
|
|
1428
|
-
/** The term a short "what is X" / "who is X" asks about, or null
|
|
1429
|
-
* isn't that shape or names the session itself.
|
|
1440
|
+
/** The term a short "what is X" / "who is X" / "define X" asks about, or null
|
|
1441
|
+
* when the line isn't that shape or names the session itself.
|
|
1430
1442
|
*
|
|
1431
1443
|
* isConversational's catch-all counts words, so "what is grelb" (three) took
|
|
1432
1444
|
* the orientation card while "what is a grelb" (four, one article apart)
|
|
@@ -1436,7 +1448,7 @@ const SESSION_REFERENT_TERMS = new Set([
|
|
|
1436
1448
|
* wall; only the closed set above is really about the product. */
|
|
1437
1449
|
function shortTermQuestionTerm(query) {
|
|
1438
1450
|
const m = String(query).trim().replace(/[?.!]+\s*$/, "")
|
|
1439
|
-
.match(/^(?:what|who)\s+(?:is|are|was|were)\s+([a-z][\w'-]*)$/i);
|
|
1451
|
+
.match(/^(?:(?:what|who)\s+(?:is|are|was|were)|define|definition\s+of)\s+([a-z][\w'-]*)$/i);
|
|
1440
1452
|
if (!m) return null;
|
|
1441
1453
|
const term = m[1].toLowerCase();
|
|
1442
1454
|
return SESSION_REFERENT_TERMS.has(term) ? null : term;
|
|
@@ -1679,6 +1691,21 @@ const BYE = new Set([
|
|
|
1679
1691
|
const WHY = new Set([
|
|
1680
1692
|
"why", "how", "how so", "how come", "explain", "say more", "go on",
|
|
1681
1693
|
"elaborate", "tell me more", "more detail", "expand",
|
|
1694
|
+
// Provenance follow-ups. Grounding is the pitch, so "how do you know" is a
|
|
1695
|
+
// likely next line after any answer, and the answer it follows already
|
|
1696
|
+
// carries its own source citation and traversal receipt — the same two
|
|
1697
|
+
// things a verbose re-render prints. Without these the same ask landed on
|
|
1698
|
+
// the orientation card ("prove it"), the grammar wall ("how do you know"),
|
|
1699
|
+
// or a vocabulary lookup of the words themselves ("what is your source").
|
|
1700
|
+
"how do you know", "how do you know that", "how do you know this",
|
|
1701
|
+
"how do you know it", "how do u know", "how did you know that",
|
|
1702
|
+
"prove it", "prove that", "are you sure", "are you sure about that",
|
|
1703
|
+
"you sure", "show your working", "show your work", "show me your working",
|
|
1704
|
+
"what is your source", "what's your source", "whats your source",
|
|
1705
|
+
"what are your sources", "what's your evidence", "whats your evidence",
|
|
1706
|
+
"what is your evidence", "says who", "based on what", "on what basis",
|
|
1707
|
+
"where did you get that", "where did you learn that", "where did that come from",
|
|
1708
|
+
"how did you get that", "who told you that", "who told you",
|
|
1682
1709
|
]);
|
|
1683
1710
|
/** Bare acknowledgements — routed identically to THANKS (an "ok"/"cool" after an
|
|
1684
1711
|
* answer reads the same as a thanks, not a new question). Kept separate from
|
|
@@ -2002,6 +2029,11 @@ function conversationalTurn(line, ctx) {
|
|
|
2002
2029
|
// mid-game. The exact/closed-set farewell just above and below this guard
|
|
2003
2030
|
// stays live either way (a real "bye"/"exit" is unambiguous, never a guess).
|
|
2004
2031
|
const gameActive = Boolean(ctx.planHolder?.state?.adventure || ctx.planHolder?.state?.spiderFly || ctx.planHolder?.state?.game);
|
|
2032
|
+
// A bare word the live game owns is a move or a hint, never small talk. "help"
|
|
2033
|
+
// mid-adventure asks what the world takes, not what tmct's commands are, and
|
|
2034
|
+
// the orientation card used to front it. Falling through leaves it to the
|
|
2035
|
+
// game's own nudge downstream.
|
|
2036
|
+
if (gameOwnWord(raw, ctx.planHolder)) return null;
|
|
2005
2037
|
const t = (id, slots = {}) => tRender(ctx.templates, id, slots) ?? TEMPLATES_UNAVAILABLE;
|
|
2006
2038
|
const mk = (answer, { end = false, miss = false, via = "template", lane = null } = {}) => {
|
|
2007
2039
|
const ts = new Date().toISOString();
|
|
@@ -2485,16 +2517,41 @@ function buildAliasSubClassTrees(rows, predicate = SUBCLASS_PREDICATE) {
|
|
|
2485
2517
|
broadEdges.push([f.subject, f.object]);
|
|
2486
2518
|
if (isOperatorTaught(f)) strictEdges.push([f.subject, f.object]);
|
|
2487
2519
|
}
|
|
2488
|
-
|
|
2520
|
+
// The chase runs once per candidate row over these same edges, so the
|
|
2521
|
+
// adjacency the search walks is built HERE, once, and handed to findIsaChain
|
|
2522
|
+
// ready-made — rebuilt per call it costs the whole edge set for a search that
|
|
2523
|
+
// usually touches a handful of nodes. `reachableHeads` is every node an edge
|
|
2524
|
+
// points at: the only nodes a chain can finish on.
|
|
2525
|
+
const strictSucc = new Map();
|
|
2526
|
+
const broadSucc = new Map();
|
|
2527
|
+
const reachableHeads = new Set();
|
|
2528
|
+
const link = (succ, a, b) => {
|
|
2529
|
+
if (!a || !b || a === b) return;
|
|
2530
|
+
if (!succ.has(a)) succ.set(a, new Set());
|
|
2531
|
+
succ.get(a).add(b);
|
|
2532
|
+
};
|
|
2533
|
+
for (const [a, b] of broadEdges) { link(broadSucc, a, b); if (b) reachableHeads.add(b); }
|
|
2534
|
+
for (const [a, b] of strictEdges) link(strictSucc, a, b);
|
|
2535
|
+
return { strictEdges, broadEdges, strictSucc, broadSucc, reachableHeads };
|
|
2489
2536
|
}
|
|
2490
2537
|
|
|
2491
2538
|
/** Chase `role` toward `targetSet` over the strict (taught-only) tree first,
|
|
2492
2539
|
* falling back to the broad tree only when the strict chase comes up empty
|
|
2493
2540
|
* — the strict attempt is tried first specifically so a hop resolvable
|
|
2494
|
-
* either way still cites via the (fuller-provenance) taught path.
|
|
2541
|
+
* either way still cites via the (fuller-provenance) taught path.
|
|
2542
|
+
*
|
|
2543
|
+
* A chain has to END on one of `targetSet`, so a target no subClassOf edge
|
|
2544
|
+
* points at can never be reached and the two searches are skipped outright.
|
|
2545
|
+
* That is the whole cost of a question about an unknown relation name: this
|
|
2546
|
+
* chase runs once per stored fact, and over a seeded store that is tens of
|
|
2547
|
+
* thousands of searches whose answer was fixed before the first one started. */
|
|
2495
2548
|
function chaseAliasEitherTree(chaseFn, role, targetSet, trees, opts) {
|
|
2496
|
-
|
|
2497
|
-
|
|
2549
|
+
const heads = trees.reachableHeads;
|
|
2550
|
+
let anyReachable = false;
|
|
2551
|
+
for (const t of targetSet) if (heads.has(t)) { anyReachable = true; break; }
|
|
2552
|
+
if (!anyReachable) return null;
|
|
2553
|
+
return chaseFn(role, targetSet, [], trees.strictSucc, opts)
|
|
2554
|
+
|| chaseFn(role, targetSet, [], trees.broadSucc, opts);
|
|
2498
2555
|
}
|
|
2499
2556
|
|
|
2500
2557
|
/** "<Name> owns/maintains <X>" — the ownership teach declarative. <Name> is one
|
|
@@ -4400,14 +4457,43 @@ const RETRACT_NOT_A_RE = /^(?:a\s+|an\s+)?([\w-]+(?:\s+[\w-]+)?)\s+(?:(?:is|are)
|
|
|
4400
4457
|
* remember/note/keep in mind/…), so this is matched against the RAW
|
|
4401
4458
|
* (unwrapped) sentence, unlike RETRACT_NOT_A_RE above which is tried against
|
|
4402
4459
|
* the remember-wrapped surface too. */
|
|
4403
|
-
|
|
4460
|
+
/** The verbs that lead a retraction. "forget" is the one /help names; the other
|
|
4461
|
+
* three are what people type meaning the same thing, and each landed on the
|
|
4462
|
+
* parse wall. None is a TEACH_RE lead verb, so widening the set here cannot
|
|
4463
|
+
* make a teach sentence read as a retraction. */
|
|
4464
|
+
const RETRACT_LEAD_VERBS = "forget|unlearn|delete|remove";
|
|
4465
|
+
const RETRACT_FORGET_RE = new RegExp(
|
|
4466
|
+
`^(?:${RETRACT_LEAD_VERBS})\\s+(?:that\\s+)?(?:a\\s+|an\\s+)?([\\w-]+(?:\\s+[\\w-]+)?)`
|
|
4467
|
+
+ "\\s+(?:is|are)\\s+(?:an?\\s+)?(?:(?:kind|type)\\s+of\\s+)?([\\w-]+)$",
|
|
4468
|
+
"i",
|
|
4469
|
+
);
|
|
4470
|
+
/** "forget mira" / "forget about mira" — a retraction that names the subject and
|
|
4471
|
+
* no fact. There is nothing to remove without knowing WHICH fact, so this never
|
|
4472
|
+
* deletes; it answers with what is stored about the subject and the phrasing
|
|
4473
|
+
* that removes one. Two tokens wide, matching the retraction subject above.
|
|
4474
|
+
* Narrower than the full-sentence lead set: "delete the readme" is a plausible
|
|
4475
|
+
* request about a file, and reading it as a memory retraction would answer a
|
|
4476
|
+
* question nobody asked. */
|
|
4477
|
+
const RETRACT_BARE_SUBJECT_RE = /^(?:forget|unlearn)\s+(?:about\s+)?(?:the\s+)?([\w-]+(?:\s+[\w-]+)?)$/i;
|
|
4478
|
+
|
|
4479
|
+
/** The subject a bare retraction names, or null when the line isn't that shape
|
|
4480
|
+
* or names no subject at all — "forget it"/"nevermind" is someone dropping the
|
|
4481
|
+
* thread, and a pronoun names nothing the store can look up. */
|
|
4482
|
+
function bareRetractSubject(query) {
|
|
4483
|
+
const q = String(query).trim().replace(/[?.!]+\s*$/, "");
|
|
4484
|
+
if (DISMISSAL.has(q.toLowerCase())) return null;
|
|
4485
|
+
const m = q.match(RETRACT_BARE_SUBJECT_RE);
|
|
4486
|
+
if (!m) return null;
|
|
4487
|
+
const subject = m[1].trim();
|
|
4488
|
+
return (isTeachPronoun(subject) || SESSION_REFERENT_TERMS.has(subject.toLowerCase())) ? null : subject;
|
|
4489
|
+
}
|
|
4404
4490
|
/** The locative teach shape ("disk-1 rests on peg-b") — the board-fact
|
|
4405
4491
|
* surface, shared by the mid-plan write guard and the locative forget. */
|
|
4406
4492
|
const BOARD_TEACH_LOCATIVE_RE = new RegExp(`^([\\w-]+)\\s+([a-z]+)s\\s+(${PREP_SRC})\\s+([\\w-]+)$`, "i");
|
|
4407
4493
|
/** "forget that disk-1 rests on peg-b" — the locative twin of
|
|
4408
4494
|
* RETRACT_FORGET_RE: a plain minted mgx:<verb>-<prep> fact has no entailment
|
|
4409
4495
|
* cascade, so removing the one row IS the retraction. */
|
|
4410
|
-
const RETRACT_FORGET_LOCATIVE_RE = new RegExp(`^
|
|
4496
|
+
const RETRACT_FORGET_LOCATIVE_RE = new RegExp(`^(?:${RETRACT_LEAD_VERBS})\\s+(?:that\\s+)?([\\w-]+)\\s+([a-z]+)s\\s+(${PREP_SRC})\\s+([\\w-]+)$`, "i");
|
|
4411
4497
|
|
|
4412
4498
|
/** The closed related-to pair — "X relates to Y" / "X is related to Y" —
|
|
4413
4499
|
* minted onto mgx:relatedTo (the SKOS view's skos:related source), so the
|
|
@@ -4534,7 +4620,8 @@ async function teachExclusionReason(sentence) {
|
|
|
4534
4620
|
if (TEACH_EXCLUDE_REQUEST_LEAD_RE.test(s) || (await hasMidSentenceInterrogative(s))) return "interrogative";
|
|
4535
4621
|
const unpunctuated = s.replace(/[.!?]+\s*$/, "");
|
|
4536
4622
|
if (TEACH_EXCLUDE_IMPERATIVE_LEAD_RE.test(s)
|
|
4537
|
-
&& !RETRACT_FORGET_RE.test(unpunctuated) && !RETRACT_FORGET_LOCATIVE_RE.test(unpunctuated)
|
|
4623
|
+
&& !RETRACT_FORGET_RE.test(unpunctuated) && !RETRACT_FORGET_LOCATIVE_RE.test(unpunctuated)
|
|
4624
|
+
&& !bareRetractSubject(unpunctuated)) return "imperative";
|
|
4538
4625
|
if (TEACH_EXCLUDE_META_TOKEN_RE.test(s) && !TEACH_PRONOUN_BARE_RE.test(s)) return "self-referential";
|
|
4539
4626
|
return null;
|
|
4540
4627
|
}
|
|
@@ -4931,6 +5018,34 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4931
5018
|
}
|
|
4932
5019
|
}
|
|
4933
5020
|
|
|
5021
|
+
// BARE RETRACTION — "forget mira", with no fact named. Nothing is removed:
|
|
5022
|
+
// which fact to drop is exactly what the sentence leaves out. Answering with
|
|
5023
|
+
// what IS stored about the subject, plus the phrasing that removes one, beats
|
|
5024
|
+
// the orientation card the short form used to get.
|
|
5025
|
+
{
|
|
5026
|
+
const subject = memoryDir && !QUESTION_LEAD_RE.test(forgetSrc) ? bareRetractSubject(forgetSrc) : null;
|
|
5027
|
+
if (subject) {
|
|
5028
|
+
try {
|
|
5029
|
+
const { loadMemory: loadMemForBare, normFactTerm: normTermForBare, readFactRows: readRowsForBare } = await import("../adapters/memory/core.mjs");
|
|
5030
|
+
const variants = factTermVariants(normTermForBare, subject);
|
|
5031
|
+
const held = readRowsForBare(await loadMemForBare(memoryDir)).filter((r) => variants.has(r.subject));
|
|
5032
|
+
if (held.length) {
|
|
5033
|
+
const shown = held.slice(0, 3).map((r) => `"${r.subject} ${predicatePhrase(r.predicate)} ${r.object}"`).join(", ");
|
|
5034
|
+
const more = held.length > 3 ? `, and ${held.length - 3} more` : "";
|
|
5035
|
+
return {
|
|
5036
|
+
text: `I need to know which fact to drop. About ${subject} I hold ${shown}${more}. `
|
|
5037
|
+
+ `Say "forget that ${subject} is a <kind>" to remove one.`,
|
|
5038
|
+
via: "retract", miss: true,
|
|
5039
|
+
};
|
|
5040
|
+
}
|
|
5041
|
+
return {
|
|
5042
|
+
text: `I hold nothing about "${subject}", so there's nothing to forget.`,
|
|
5043
|
+
via: "retract", miss: true,
|
|
5044
|
+
};
|
|
5045
|
+
} catch { /* store unavailable — fall through to the ordinary cascade */ }
|
|
5046
|
+
}
|
|
5047
|
+
}
|
|
5048
|
+
|
|
4934
5049
|
// MID-PLAN BOARD TEACH — a locative fact about a piece the LIVE plan's moves
|
|
4935
5050
|
// touch is accepted and the plan is re-searched from the moved board, never
|
|
4936
5051
|
// confirmed-then-contradicted by the next move. The change is written as a
|
|
@@ -6309,6 +6424,31 @@ async function presuppositionNudge(query, { graph, memoryDir }) {
|
|
|
6309
6424
|
return { text: holds ? `${verdict}. ${subjEnt.label} does ${split.verb} ${objEnt.label}.` : `${verdict} — the premise doesn't hold.` };
|
|
6310
6425
|
}
|
|
6311
6426
|
|
|
6427
|
+
/** The bare words a live game owns. Typed mid-game they are that game's own
|
|
6428
|
+
* hints and controls, so a vocabulary lookup on them answers a question nobody
|
|
6429
|
+
* asked: "lower" in a running guessing game came back with a ConceptNet
|
|
6430
|
+
* synonym for the word, and "watch"/"step" on the spider-and-fly board read as
|
|
6431
|
+
* questions about clocks and stairs. Each game's own turn handler runs first,
|
|
6432
|
+
* so a word listed here only reaches this check when that handler declined it.
|
|
6433
|
+
* Closed and per-game — a real aside ("what is a dog") is untouched. */
|
|
6434
|
+
const GAME_OWN_WORDS = Object.freeze({
|
|
6435
|
+
game: ["higher", "lower", "warmer", "colder", "hotter", "up", "down", "guess"],
|
|
6436
|
+
spiderFly: ["watch", "step", "tick", "move", "board", "web"],
|
|
6437
|
+
adventure: ["help", "xyzzy", "wait", "hint", "plugh"],
|
|
6438
|
+
});
|
|
6439
|
+
|
|
6440
|
+
/** Which live game claims this bare word, or null. */
|
|
6441
|
+
function gameOwnWord(query, planHolder) {
|
|
6442
|
+
const state = planHolder?.state;
|
|
6443
|
+
if (!state) return null;
|
|
6444
|
+
const word = String(query).trim().toLowerCase().replace(/[?.!]+$/, "");
|
|
6445
|
+
if (!word || /\s/.test(word)) return null;
|
|
6446
|
+
for (const kind of ["game", "spiderFly", "adventure"]) {
|
|
6447
|
+
if (state[kind] && GAME_OWN_WORDS[kind].includes(word)) return kind;
|
|
6448
|
+
}
|
|
6449
|
+
return null;
|
|
6450
|
+
}
|
|
6451
|
+
|
|
6312
6452
|
/** The wall-repeat one-liner. MUST NOT match
|
|
6313
6453
|
* WALL_MISS_RE: the suppression keys on the PREVIOUS answer matching it, so this
|
|
6314
6454
|
* text self-limits — a third consecutive miss re-offers the tailored hint. */
|
|
@@ -6368,6 +6508,8 @@ export async function helpText() {
|
|
|
6368
6508
|
["/syllogise <term>", "work out and remember what follows from the facts about a term (needed for chains longer than 2 hops)"],
|
|
6369
6509
|
["/export <path>", "write the memory store to a file, as JSONL (the same shape `tmct memory --export` writes)"],
|
|
6370
6510
|
["/ingest <path>", "read a local text file and store every fact the recognizer grounds from it (same recognizer as `tmct extract`)"],
|
|
6511
|
+
["remember <X> is a <Y>", "teach a fact in plain English (\"every X is a Y\" and a bare \"X is a Y\" work too)"],
|
|
6512
|
+
["forget that <X> is a <Y>", "withdraw a fact you taught, and anything derived from it — the phrasing the retract lane reads"],
|
|
6371
6513
|
["/narrate on|off", "verbose developer/debug mode: decision points, matched pattern, results+sources, goal per turn"],
|
|
6372
6514
|
["/wiki on|off|supplement|always", "live Wikipedia (default off): on tries en.wikipedia.org when I can't answer (network), cited; supplement also adds a read-out under every grounded vocabulary answer; always widens that to every grounded answer"],
|
|
6373
6515
|
["research <topic> [limit N] [depth D]", "fetch the topic from Simple English Wikipedia (the explicit ask is the network consent), store what it grounds, and queue its linked topics — \"research next\" steps the queue; also status/stop. limit N caps the links queued per topic, depth D how many hops the queue follows (1 by default); a run also stops at its total node budget"],
|
|
@@ -7961,6 +8103,15 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7961
8103
|
// tail, verbatim.
|
|
7962
8104
|
if (m) metaTerm = stripTrailingScopeFiller(m[1]);
|
|
7963
8105
|
}
|
|
8106
|
+
// "define X" parses cleanly as the code lane's reverse-defines query, so it
|
|
8107
|
+
// OWNS the turn and the guard above never arms — a person typing "define dog"
|
|
8108
|
+
// at a vocabulary chatbot got told no module matches. Read as a vocabulary
|
|
8109
|
+
// ask only once the code lane has actually missed, so "define parseQuery"
|
|
8110
|
+
// over an indexed repo keeps the module answer it parsed to.
|
|
8111
|
+
if (!metaTerm && miss) {
|
|
8112
|
+
const d = q.match(DEFINE_IMPERATIVE_RE);
|
|
8113
|
+
if (d) metaTerm = stripTrailingScopeFiller(stripTrailingDiscourseTag(d[1].trim()));
|
|
8114
|
+
}
|
|
7964
8115
|
// An ambiguous parse tie ({ambiguousParse}) reaches this lane with
|
|
7965
8116
|
// envelope.parsed nulled and miss=false, so NEITHER branch above arms —
|
|
7966
8117
|
// but when one tied reading is META and memory holds facts for its term
|
|
@@ -8400,18 +8551,24 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
8400
8551
|
const capable = uniqueFacts(facts.filter((f) => f.predicate === "mgx:capableOf" && verbVariants.has(f.object)))
|
|
8401
8552
|
.filter((f) => resolveCapabilityPolarity(new Set([f.subject]), verbVariants, facts).verdict === "yes");
|
|
8402
8553
|
if (capable.length) {
|
|
8403
|
-
const {
|
|
8554
|
+
const {
|
|
8555
|
+
findIsaChain, buildSubClassSuccessors: buildKindSuccessors,
|
|
8556
|
+
SUBCLASS_PREDICATE: SC_PRED, TYPE_PREDICATE: TYPE_PRED,
|
|
8557
|
+
} = await import("../domain/syllogise.mjs");
|
|
8404
8558
|
const subClassRows = facts.filter((f) => f.predicate === SC_PRED);
|
|
8405
8559
|
const typeRows = facts.filter((f) => f.predicate === TYPE_PRED);
|
|
8406
8560
|
const subClassEdges = subClassRows.map((f) => [f.subject, f.object]);
|
|
8407
8561
|
const typeEdges = typeRows.map((f) => [f.subject, f.object]);
|
|
8562
|
+
// One chase per capable subject over the same edges, so the adjacency is
|
|
8563
|
+
// built here instead of inside every search.
|
|
8564
|
+
const subClassSucc = buildKindSuccessors(subClassEdges);
|
|
8408
8565
|
const rowForStep = (step) => (step.predicate === SC_PRED ? subClassRows : typeRows)
|
|
8409
8566
|
.find((g) => g.subject === step.subject && g.object === step.object);
|
|
8410
8567
|
const chainBySubject = new Map();
|
|
8411
8568
|
const inKind = capable.filter((f) => {
|
|
8412
8569
|
if (kindVariants.has(f.subject)) return true;
|
|
8413
8570
|
if (!chainBySubject.has(f.subject)) {
|
|
8414
|
-
chainBySubject.set(f.subject, findIsaChain(f.subject, kindVariants, typeEdges,
|
|
8571
|
+
chainBySubject.set(f.subject, findIsaChain(f.subject, kindVariants, typeEdges, subClassSucc, { maxHops: 3 }));
|
|
8415
8572
|
}
|
|
8416
8573
|
return !!chainBySubject.get(f.subject);
|
|
8417
8574
|
});
|
|
@@ -9520,7 +9677,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9520
9677
|
const noun = await entityClassNoun(graph, isaSubject);
|
|
9521
9678
|
if (noun) for (const v of factTermVariants(normFactTerm, noun)) subjCandidates.add(v);
|
|
9522
9679
|
const {
|
|
9523
|
-
findIsaChain, deriveDisjointViolations,
|
|
9680
|
+
findIsaChain, buildSubClassSuccessors: buildChaseSuccessors, deriveDisjointViolations,
|
|
9524
9681
|
SUBCLASS_PREDICATE: SC_PREDICATE, TYPE_PREDICATE: RDF_TYPE_PREDICATE, DISJOINT_PREDICATE,
|
|
9525
9682
|
} = await import("../domain/syllogise.mjs");
|
|
9526
9683
|
const isTaught = isOperatorTaught;
|
|
@@ -9532,6 +9689,10 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9532
9689
|
const mixedTypeRows = isa.filter((f) => f.predicate === RDF_TYPE_PREDICATE);
|
|
9533
9690
|
const mixedTypeEdges = mixedTypeRows.map((f) => [f.subject, f.object]);
|
|
9534
9691
|
const mixedSubClassEdges = mixedSubClassRows.map((f) => [f.subject, f.object]);
|
|
9692
|
+
// Both chases below run once per candidate subject over these same edges,
|
|
9693
|
+
// so the adjacency is built here rather than inside each search.
|
|
9694
|
+
const chainSubClassSucc = buildChaseSuccessors(chainSubClassEdges);
|
|
9695
|
+
const mixedSubClassSucc = buildChaseSuccessors(mixedSubClassEdges);
|
|
9535
9696
|
const disjointRows = rows.filter((f) => f.predicate === DISJOINT_PREDICATE && isTaught(f));
|
|
9536
9697
|
const disjointEdges = disjointRows.map((f) => [f.subject, f.object]);
|
|
9537
9698
|
// CAX-DW GATE, COMPUTED BEFORE ANY "YES" MAY RETURN: every taught
|
|
@@ -9693,7 +9854,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9693
9854
|
const factForStep = (step) => (step.predicate === SC_PREDICATE ? chainSubClassRows : chainTypeRows)
|
|
9694
9855
|
.find((f) => f.subject === step.subject && f.object === step.object);
|
|
9695
9856
|
for (const subj of subjCandidates) {
|
|
9696
|
-
const chain = findIsaChain(subj, objVariants, chainTypeEdges,
|
|
9857
|
+
const chain = findIsaChain(subj, objVariants, chainTypeEdges, chainSubClassSucc, { maxHops: 2 });
|
|
9697
9858
|
if (!chain) continue;
|
|
9698
9859
|
const chainRefusal = disjointRefusalFor(subj);
|
|
9699
9860
|
if (chainRefusal) return chainRefusal;
|
|
@@ -9714,7 +9875,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9714
9875
|
const mixedFactForStep = (step) => (step.predicate === SC_PREDICATE ? mixedSubClassRows : mixedTypeRows)
|
|
9715
9876
|
.find((f) => f.subject === step.subject && f.object === step.object);
|
|
9716
9877
|
for (const subj of subjCandidates) {
|
|
9717
|
-
const chain = findIsaChain(subj, objVariants, mixedTypeEdges,
|
|
9878
|
+
const chain = findIsaChain(subj, objVariants, mixedTypeEdges, mixedSubClassSucc, { maxHops: 2 });
|
|
9718
9879
|
if (!chain) continue;
|
|
9719
9880
|
const chainRefusal = disjointRefusalFor(subj);
|
|
9720
9881
|
if (chainRefusal) return chainRefusal;
|
|
@@ -9844,7 +10005,9 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9844
10005
|
? deriveSomeValuesFromSubsumption(restrictionEdges, chainSubClassEdges, { budget: 10 })
|
|
9845
10006
|
: [];
|
|
9846
10007
|
if (svfSubsumption.length) {
|
|
9847
|
-
const
|
|
10008
|
+
const enlargedSubClassSucc = buildChaseSuccessors(
|
|
10009
|
+
chainSubClassEdges.concat(svfSubsumption.map((d) => [d.subject, d.object])),
|
|
10010
|
+
);
|
|
9848
10011
|
// The SAME `min(premiseTrusts) x
|
|
9849
10012
|
// ruleConfidence` discipline syllogise()'s own batch pass now applies
|
|
9850
10013
|
// to scm-svf1 (src/domain/syllogise.mjs), computed here for this LIVE,
|
|
@@ -9892,7 +10055,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9892
10055
|
: undefined;
|
|
9893
10056
|
};
|
|
9894
10057
|
for (const subj of subjCandidates) {
|
|
9895
|
-
const chain = findIsaChain(subj, objVariants, chainTypeEdges,
|
|
10058
|
+
const chain = findIsaChain(subj, objVariants, chainTypeEdges, enlargedSubClassSucc, { maxHops: 3 });
|
|
9896
10059
|
if (!chain) continue;
|
|
9897
10060
|
const premises = chain.map(factForStepOrSvf);
|
|
9898
10061
|
if (premises.every(Boolean)) {
|
|
@@ -9934,7 +10097,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9934
10097
|
// edge lists the chases use, and /syllogise closes over a superset of
|
|
9935
10098
|
// them, so a chain found here is one it can really materialize.
|
|
9936
10099
|
const deeperChainExists = [...subjCandidates].some(
|
|
9937
|
-
(subj) => findIsaChain(subj, objVariants, chainTypeEdges,
|
|
10100
|
+
(subj) => findIsaChain(subj, objVariants, chainTypeEdges, chainSubClassSucc, { maxHops: DEEP_CHAIN_PROBE_HOPS }),
|
|
9938
10101
|
);
|
|
9939
10102
|
if (knownSubjectIsa.length) {
|
|
9940
10103
|
const shown = knownSubjectIsa.slice(0, 3).map(renderFactLine).join("; ");
|
|
@@ -10881,6 +11044,12 @@ const BARE_WHATIS_RE = /^what\s+(?:is|are)\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
|
10881
11044
|
* subject or a relation object); with no such facts it returns null and the
|
|
10882
11045
|
* turn falls through to the author/relation who-readers unchanged. */
|
|
10883
11046
|
const WHO_IS_BARE_RE = /^who\s+(?:is|are|was|were)\s+(?:an?\s+|the\s+)?([\w'-]+)[?.!\s]*$/i;
|
|
11047
|
+
/** "define X" / "please define a dog" / "definition of X" — the imperative twin
|
|
11048
|
+
* of "what does X mean". Read as a vocabulary ask only where the code lane has
|
|
11049
|
+
* already missed, so "define parseQuery" over an indexed repo keeps the module
|
|
11050
|
+
* answer it parses to. */
|
|
11051
|
+
const DEFINE_IMPERATIVE_RE =
|
|
11052
|
+
/^(?:please\s+|can\s+you\s+|could\s+you\s+)*(?:define|definition\s+of|give\s+me\s+the\s+definition\s+of)\s+(?:the\s+(?:term|word)\s+)?(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i;
|
|
10884
11053
|
|
|
10885
11054
|
/** The meta term a "what is a X" / "what is X" / "what does X mean" / "define X"
|
|
10886
11055
|
* question asks about — from the parse when present, else recognized directly
|
|
@@ -10899,7 +11068,7 @@ function metaTermOf(query, envelope) {
|
|
|
10899
11068
|
const q = String(query).trim();
|
|
10900
11069
|
const m = q.match(BARE_WHATIS_RE)
|
|
10901
11070
|
|| q.match(/^what\s+(?:does|do)\s+(?:an?\s+)?(.+?)\s+means?[?.!\s]*$/i)
|
|
10902
|
-
|| q.match(
|
|
11071
|
+
|| q.match(DEFINE_IMPERATIVE_RE);
|
|
10903
11072
|
return m ? stripTrailingScopeFiller(stripTrailingDiscourseTag(m[1].trim())) : null;
|
|
10904
11073
|
}
|
|
10905
11074
|
|
|
@@ -13200,6 +13369,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13200
13369
|
// that blurb (a dispatched turn) then becomes `last`, wiping the very
|
|
13201
13370
|
// antecedent the next pronoun turn needs.
|
|
13202
13371
|
const isConversationalCandidate = conversationalCandidateBaseGate && !vocabAntecedent && isConversational(query);
|
|
13372
|
+
const liveGameOwnWord = gameOwnWord(query, planHolder);
|
|
13203
13373
|
// "what is X" with NO article ("what is john") is BOTH conversational-shaped
|
|
13204
13374
|
// (isConversational() would claim it) AND a legitimate bare meta/fact-lookup
|
|
13205
13375
|
// form (BARE_WHATIS_RE). Diverts ONLY when a REAL fact actually resolves for
|
|
@@ -13337,6 +13507,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13337
13507
|
const fallback = metaFallbackEntityAnswer(graph, String(query).trim());
|
|
13338
13508
|
if (fallback) bareMetaHit = { text: fallback.text, replace: true };
|
|
13339
13509
|
}
|
|
13510
|
+
// A word the live game owns is a move or a hint, not a term to define — drop
|
|
13511
|
+
// whatever the vocabulary lanes found for it so the turn lands on the game's
|
|
13512
|
+
// own nudge below.
|
|
13513
|
+
if (liveGameOwnWord) bareMetaHit = null;
|
|
13340
13514
|
const coldPronounDecline = focus?.label ? null : coldPronounDeclineText(query);
|
|
13341
13515
|
let selfContainedMiss = false;
|
|
13342
13516
|
if (bareMetaHit?.reference) {
|
|
@@ -13400,7 +13574,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13400
13574
|
via = "template"; handled = true;
|
|
13401
13575
|
note(trace, "lane: (2) COLD PRONOUN — a subject pronoun with no antecedent bound and no focus standing; named the pronoun instead of the orientation card");
|
|
13402
13576
|
note(trace, "goal: resolve a pronoun to a subject (nothing named yet)");
|
|
13403
|
-
} else if (isConversationalCandidate && planHolder?.state?.game) {
|
|
13577
|
+
} else if ((isConversationalCandidate || liveGameOwnWord) && planHolder?.state?.game) {
|
|
13404
13578
|
// MID-GAME: a short line that parsed as nothing ("you said lower", "is it
|
|
13405
13579
|
// warm in here") stays INSIDE the game frame with a nudge naming the
|
|
13406
13580
|
// state — the identity card answers a question nobody asked, and it used
|
|
@@ -13414,7 +13588,22 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13414
13588
|
dialogueLaneOverride = "game-inform";
|
|
13415
13589
|
note(trace, "lane: (2) MID-GAME NUDGE — an unparsed short turn stayed inside the live game frame instead of the identity card");
|
|
13416
13590
|
note(trace, "goal: keep the running guess-the-number game on track");
|
|
13417
|
-
} else if (
|
|
13591
|
+
} else if (liveGameOwnWord === "spiderFly") {
|
|
13592
|
+
answer = 'that word belongs to the board, and nothing on it takes it as a move. Say "tick" to advance a turn, '
|
|
13593
|
+
+ 'or address one, e.g. "@spider the fly is east". "stop watching" ends it.';
|
|
13594
|
+
via = "game"; handled = true;
|
|
13595
|
+
dialogueLaneOverride = "game-inform";
|
|
13596
|
+
note(trace, "lane: (2) MID-GAME NUDGE — a word the live spider-and-fly board owns stayed in the game frame");
|
|
13597
|
+
note(trace, "goal: keep the running spider-and-fly board on track");
|
|
13598
|
+
} else if (liveGameOwnWord === "adventure") {
|
|
13599
|
+
answer = 'we\'re mid-adventure, and that word isn\'t one the world takes. Try "look", "inventory", '
|
|
13600
|
+
+ '"go north" or "take the lamp" — "stop playing" leaves.';
|
|
13601
|
+
via = "game"; handled = true;
|
|
13602
|
+
dialogueLaneOverride = "game-inform";
|
|
13603
|
+
note(trace, "lane: (2) MID-GAME NUDGE — a word the live adventure owns stayed in the game frame");
|
|
13604
|
+
note(trace, "goal: keep the running adventure on track");
|
|
13605
|
+
} else if (isConversationalCandidate && !shortTermQuestionTerm(gateQuery) && !shortTermQuestionTerm(query)
|
|
13606
|
+
&& !bareRetractSubject(gateQuery) && !bareRetractSubject(query)) {
|
|
13418
13607
|
// A conversational miss (a greeting, "what can you do", a very short non-code
|
|
13419
13608
|
// line) gets the friendly orientation (module-aware: empty → --repo/tmct init).
|
|
13420
13609
|
// A short question naming a TERM is excluded: nothing above resolved it, so
|
|
@@ -13493,6 +13682,16 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13493
13682
|
deduced = TAUGHT_FACT_LOOKUP_GOAL;
|
|
13494
13683
|
note(trace, `goal: ${deduced} (revised — a general-verb direct-question fact lookup answered this turn)`);
|
|
13495
13684
|
}
|
|
13685
|
+
// Same revision for "define X" answered out of the vocabulary store: the
|
|
13686
|
+
// parsed AST is the code lane's reverse-defines shape, so a goal line read
|
|
13687
|
+
// off it names a module search this turn never made.
|
|
13688
|
+
const definedTerm = !fact.miss && DEFINE_IMPERATIVE_RE.test(String(query).trim())
|
|
13689
|
+
? metaTermOf(query, null)
|
|
13690
|
+
: null;
|
|
13691
|
+
if (definedTerm) {
|
|
13692
|
+
deduced = deduceGoalFromParsed({ shape: "meta", object: definedTerm });
|
|
13693
|
+
note(trace, `goal: ${deduced} (revised — "define X" answered from the vocabulary store, not the code index)`);
|
|
13694
|
+
}
|
|
13496
13695
|
} else if (miss) {
|
|
13497
13696
|
// W2: after the honest miss is composed, consult the folded-session memory. A
|
|
13498
13697
|
// relevant enough block ANSWERS — recalled Q/A framed + cited first, with the
|
|
@@ -13924,9 +14123,14 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13924
14123
|
} else if (browser) {
|
|
13925
14124
|
answer = `${answer}\n(I don't know that yet — you can teach me: say "remember: <thing> is a <kind>".)`;
|
|
13926
14125
|
note(trace, "intermediate: HONEST-EMPTY POLISH — a browser miss points at the teach lane, not the CLI-only --repo remedy");
|
|
13927
|
-
} else {
|
|
14126
|
+
} else if (looksCodeish(String(query), String(query).toLowerCase())) {
|
|
13928
14127
|
answer = `${answer}\n(this repo has no code graph — index it with \`tmct index\`, point me at a \`.tmct/graph.json\` with \`--repo <path>\`, or run \`npm run example:mini\`.)`;
|
|
13929
14128
|
note(trace, "intermediate: HONEST-EMPTY POLISH — the loaded graph has 0 modules, so the dead-end got a tmct index/--repo pointer appended");
|
|
14129
|
+
} else {
|
|
14130
|
+
// Nothing in the question is code-shaped, so indexing a repo would not
|
|
14131
|
+
// help. "who won the 2031 world cup" used to carry the pointer anyway,
|
|
14132
|
+
// which reads as a remedy for a question the remedy cannot touch.
|
|
14133
|
+
note(trace, "intermediate: HONEST-EMPTY POLISH — held back: nothing in the question is code-shaped, so the index/--repo remedy would not apply");
|
|
13930
14134
|
}
|
|
13931
14135
|
}
|
|
13932
14136
|
if (teachOffer) {
|
|
@@ -92,12 +92,23 @@ export function parseArgs(argv) {
|
|
|
92
92
|
*/
|
|
93
93
|
async function runSentence(sentence, { config, memoryDir }) {
|
|
94
94
|
const before = readFactRows(await loadMemory(memoryDir));
|
|
95
|
-
const { record } = await runTurn(sentence, { config, memoryDir, sessionId: uuidv7() });
|
|
96
|
-
if (record?.via !== "assert" || record?.miss) return { recognized: false, rows: [] };
|
|
95
|
+
const { record, answer } = await runTurn(sentence, { config, memoryDir, sessionId: uuidv7() });
|
|
96
|
+
if (record?.via !== "assert" || record?.miss) return { recognized: false, rows: [], decline: String(answer || "") };
|
|
97
97
|
const after = readFactRows(await loadMemory(memoryDir));
|
|
98
98
|
return { recognized: true, rows: touchedFactRows(before, after) };
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/** The recognizer's own words for why it turned a sentence down, when it named
|
|
102
|
+
* an ungrounded term rather than the sentence's shape. "A wombat is a
|
|
103
|
+
* marsupial." is the same shape as "A kestrel is a bird.", which IS recognized;
|
|
104
|
+
* the difference is that "bird" is in the vocabulary and "marsupial" is not, so
|
|
105
|
+
* reporting every skip as an unrecognized shape blames the wrong thing. */
|
|
106
|
+
const UNGROUNDED_TERM_DECLINE_RE = /I don't recognize ((?:"[^"]+"(?:\s+(?:and|or)\s+)?)+) as (?:a )?words? I know/i;
|
|
107
|
+
const ungroundedTermsIn = (decline) => {
|
|
108
|
+
const m = String(decline || "").match(UNGROUNDED_TERM_DECLINE_RE);
|
|
109
|
+
return m ? m[1].match(/"([^"]+)"/g).map((q) => q.slice(1, -1)) : null;
|
|
110
|
+
};
|
|
111
|
+
|
|
101
112
|
// The copula lemmas that read as class membership; a following noun phrase is
|
|
102
113
|
// the class the subject is-a. "has/have" and other verbs are relations, not isa.
|
|
103
114
|
const OPTIMISTIC_COPULAS = new Set(["is", "are", "was", "were", "be", "been", "being", "am"]);
|
|
@@ -541,11 +552,17 @@ export async function ingestText(text, {
|
|
|
541
552
|
let sentenceCount = 0;
|
|
542
553
|
let recognizedSentences = 0;
|
|
543
554
|
let optimisticSentences = 0;
|
|
555
|
+
// The terms a skipped sentence named that nothing grounds yet. Reported with
|
|
556
|
+
// the skip count so the summary names the real obstacle instead of blaming
|
|
557
|
+
// the sentence's shape for a shape it actually recognizes.
|
|
558
|
+
const ungroundedTerms = new Set();
|
|
544
559
|
|
|
545
560
|
// One recognized read of some text form: null when the strict recognizer
|
|
546
561
|
// grounds nothing, else the Fact rows it touched.
|
|
562
|
+
let lastDecline = "";
|
|
547
563
|
const strictRows = async (form) => {
|
|
548
|
-
const { recognized, rows } = await runSentence(form, { config: cfg, memoryDir: dir });
|
|
564
|
+
const { recognized, rows, decline } = await runSentence(form, { config: cfg, memoryDir: dir });
|
|
565
|
+
if (!recognized) lastDecline = decline || lastDecline;
|
|
549
566
|
return recognized && rows.length ? rows : null;
|
|
550
567
|
};
|
|
551
568
|
|
|
@@ -557,6 +574,7 @@ export async function ingestText(text, {
|
|
|
557
574
|
let carrySubject = null;
|
|
558
575
|
for (const sentence of splitSentencesPreservingPaths(paragraph)) {
|
|
559
576
|
sentenceCount += 1;
|
|
577
|
+
lastDecline = "";
|
|
560
578
|
const cleaned = stripCitationResidue(sentence);
|
|
561
579
|
// Whole sentence first, then each closed-marker clause as a fallback.
|
|
562
580
|
let rows = null;
|
|
@@ -592,6 +610,8 @@ export async function ingestText(text, {
|
|
|
592
610
|
}
|
|
593
611
|
continue;
|
|
594
612
|
}
|
|
613
|
+
const ungrounded = ungroundedTermsIn(lastDecline);
|
|
614
|
+
if (ungrounded) for (const term of ungrounded) ungroundedTerms.add(term);
|
|
595
615
|
if (!optimistic) continue;
|
|
596
616
|
const candidates = optimisticTriples(cleaned, { lexicon: lex, nlp });
|
|
597
617
|
if (!candidates.length) continue;
|
|
@@ -612,6 +632,7 @@ export async function ingestText(text, {
|
|
|
612
632
|
extracted,
|
|
613
633
|
optimistic: optimisticFacts,
|
|
614
634
|
skipped: sentenceCount - recognizedSentences - optimisticSentences,
|
|
635
|
+
ungroundedTerms: [...ungroundedTerms],
|
|
615
636
|
};
|
|
616
637
|
if (canonical) {
|
|
617
638
|
result.canonical = canonicalLines([...extracted, ...optimisticFacts], readFactRows(await loadMemory(dir)));
|
|
@@ -641,9 +662,23 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
641
662
|
const filePath = resolve(process.cwd(), file);
|
|
642
663
|
const text = await readFile(filePath, "utf8");
|
|
643
664
|
const sourceTag = basename(filePath);
|
|
644
|
-
|
|
665
|
+
// A repo path is not a store handle. Resolve the SAME backend every other verb
|
|
666
|
+
// reads back through, or the facts land in the retired flat file and the
|
|
667
|
+
// "facts written into …" line below reports a write chat can never see.
|
|
668
|
+
const repoRoot = repo ? resolve(process.cwd(), repo) : null;
|
|
669
|
+
const { openConfiguredMemoryBackend } = await import("../adapters/memory/core.mjs");
|
|
670
|
+
const store = repoRoot ? await openConfiguredMemoryBackend(repoRoot) : null;
|
|
645
671
|
|
|
646
|
-
|
|
672
|
+
let result;
|
|
673
|
+
try {
|
|
674
|
+
result = await ingestText(text, {
|
|
675
|
+
memoryDir: store ? store.dir : null,
|
|
676
|
+
config: repoRoot ? loadConfig(process.env, repoRoot) : null,
|
|
677
|
+
sourceTag, optimistic, canonical,
|
|
678
|
+
});
|
|
679
|
+
} finally {
|
|
680
|
+
if (store) await store.close();
|
|
681
|
+
}
|
|
647
682
|
const emitted = optimistic ? [...result.extracted, ...result.optimistic] : result.extracted;
|
|
648
683
|
|
|
649
684
|
if (out) {
|
|
@@ -663,9 +698,14 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
663
698
|
+ `(${result.extracted.length} fact row${result.extracted.length === 1 ? "" : "s"})`
|
|
664
699
|
+ (optimistic ? `, ${optimisticCount} optimistic candidate${optimisticCount === 1 ? "" : "s"}` : "")
|
|
665
700
|
+ `, ${result.skipped} skipped — not a recognized declarative shape (an honest, expected gap; this is `
|
|
666
|
-
+ `an attempt, not full NLU)
|
|
701
|
+
+ `an attempt, not full NLU).`
|
|
702
|
+
+ (result.ungroundedTerms?.length
|
|
703
|
+
? `\nSome of those skips were shapes I do read, held up by terms nothing grounds yet: `
|
|
704
|
+
+ `${result.ungroundedTerms.map((t) => `"${t}"`).join(", ")}. `
|
|
705
|
+
+ `Ground one side first (e.g. "every ${result.ungroundedTerms[0]} is a thing") and re-run.`
|
|
706
|
+
: ""),
|
|
667
707
|
);
|
|
668
|
-
if (repo) console.error(`facts written into ${
|
|
708
|
+
if (repo) console.error(`facts written into ${repoRoot}'s tmct memory, tagged ${sourceTag}`);
|
|
669
709
|
if (out) console.error(`facts written to ${out}`);
|
|
670
710
|
return { sentences, recognized, extracted: result.extracted, optimistic: result.optimistic, skipped: result.skipped };
|
|
671
711
|
}
|