@polycode-projects/the-mechanical-code-talker 5.0.5 → 5.0.7
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 +78 -19
- package/bin/tmct.mjs +63 -2
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +23 -0
- package/src/domain/ask-vocab.mjs +19 -0
- package/src/domain/ask.mjs +10 -5
- package/src/domain/codegraph.mjs +23 -9
- package/src/domain/game-config.mjs +12 -0
- 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-viz.mjs +5 -2
- package/src/services/adventure.mjs +8 -1
- package/src/services/chat-page-viz.mjs +123 -25
- package/src/services/chat-session.mjs +60 -10
- package/src/services/chat.mjs +328 -36
- package/src/services/code-explorer-viz.mjs +3 -2
- package/src/services/extract-facts.mjs +47 -7
- package/src/services/ingest-viz.mjs +113 -29
- package/src/services/ledger-viz.mjs +9 -4
- package/src/services/memory-panel-viz.mjs +44 -0
- package/src/services/mud-viz.mjs +21 -3
- package/src/services/mudiii-scene.mjs +407 -36
- package/src/services/mudiii-turn.mjs +65 -9
- package/src/services/mudiii-viz.mjs +810 -157
- package/src/services/p2p-room.mjs +1 -1
- package/src/services/plan-viz.mjs +26 -4
- package/src/services/predator-prey.mjs +141 -37
- package/src/services/research-viz.mjs +17 -23
- package/src/services/spider-fly-turn.mjs +7 -1
- package/src/services/spider-fly-viz.mjs +13 -5
- package/src/services/sprite-catalog-viz.mjs +3 -2
- package/src/services/viz-theme.mjs +20 -0
- 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/mudiii-browser-entry.mjs +70 -34
- 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".`;
|
|
@@ -1413,6 +1425,35 @@ export function isConversational(query) {
|
|
|
1413
1425
|
return expandContractions(q).split(/\s+/).filter(Boolean).length <= 3 && !codeish;
|
|
1414
1426
|
}
|
|
1415
1427
|
|
|
1428
|
+
/** The words that, in the slot of a short "what is X" / "who is X", name the
|
|
1429
|
+
* SESSION rather than a term to look up ("what is this", "what is up").
|
|
1430
|
+
* Closed and hand-curated, the same discipline every small-talk set in this
|
|
1431
|
+
* file uses: a word listed here keeps today's orientation card, and anything
|
|
1432
|
+
* else is a question about a term. */
|
|
1433
|
+
const SESSION_REFERENT_TERMS = new Set([
|
|
1434
|
+
"this", "that", "it", "they", "them", "you", "u", "yours", "tmct",
|
|
1435
|
+
"up", "new", "next", "now", "here", "there", "left", "wrong", "more", "else",
|
|
1436
|
+
"all", "everything", "anything", "something", "nothing",
|
|
1437
|
+
"going", "happening", "possible", "available", "supported", "included",
|
|
1438
|
+
]);
|
|
1439
|
+
|
|
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.
|
|
1442
|
+
*
|
|
1443
|
+
* isConversational's catch-all counts words, so "what is grelb" (three) took
|
|
1444
|
+
* the orientation card while "what is a grelb" (four, one article apart)
|
|
1445
|
+
* refused — the same question answered two different ways, and the shorter
|
|
1446
|
+
* one answered with a paragraph about tmct that reads as if the term had been
|
|
1447
|
+
* looked up. A term question that resolves to nothing belongs on the miss
|
|
1448
|
+
* wall; only the closed set above is really about the product. */
|
|
1449
|
+
function shortTermQuestionTerm(query) {
|
|
1450
|
+
const m = String(query).trim().replace(/[?.!]+\s*$/, "")
|
|
1451
|
+
.match(/^(?:(?:what|who)\s+(?:is|are|was|were)|define|definition\s+of)\s+([a-z][\w'-]*)$/i);
|
|
1452
|
+
if (!m) return null;
|
|
1453
|
+
const term = m[1].toLowerCase();
|
|
1454
|
+
return SESSION_REFERENT_TERMS.has(term) ? null : term;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1416
1457
|
/** The tmct tools dispatchTool can back (the set a tool-emitting caller may use).
|
|
1417
1458
|
* A declared tool outside this set is never emitted — the request falls through
|
|
1418
1459
|
* to a text answer. The COMMANDS map names the richer graph tools; TOOLS names
|
|
@@ -1650,6 +1691,21 @@ const BYE = new Set([
|
|
|
1650
1691
|
const WHY = new Set([
|
|
1651
1692
|
"why", "how", "how so", "how come", "explain", "say more", "go on",
|
|
1652
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",
|
|
1653
1709
|
]);
|
|
1654
1710
|
/** Bare acknowledgements — routed identically to THANKS (an "ok"/"cool" after an
|
|
1655
1711
|
* answer reads the same as a thanks, not a new question). Kept separate from
|
|
@@ -1973,6 +2029,11 @@ function conversationalTurn(line, ctx) {
|
|
|
1973
2029
|
// mid-game. The exact/closed-set farewell just above and below this guard
|
|
1974
2030
|
// stays live either way (a real "bye"/"exit" is unambiguous, never a guess).
|
|
1975
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;
|
|
1976
2037
|
const t = (id, slots = {}) => tRender(ctx.templates, id, slots) ?? TEMPLATES_UNAVAILABLE;
|
|
1977
2038
|
const mk = (answer, { end = false, miss = false, via = "template", lane = null } = {}) => {
|
|
1978
2039
|
const ts = new Date().toISOString();
|
|
@@ -2456,16 +2517,41 @@ function buildAliasSubClassTrees(rows, predicate = SUBCLASS_PREDICATE) {
|
|
|
2456
2517
|
broadEdges.push([f.subject, f.object]);
|
|
2457
2518
|
if (isOperatorTaught(f)) strictEdges.push([f.subject, f.object]);
|
|
2458
2519
|
}
|
|
2459
|
-
|
|
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 };
|
|
2460
2536
|
}
|
|
2461
2537
|
|
|
2462
2538
|
/** Chase `role` toward `targetSet` over the strict (taught-only) tree first,
|
|
2463
2539
|
* falling back to the broad tree only when the strict chase comes up empty
|
|
2464
2540
|
* — the strict attempt is tried first specifically so a hop resolvable
|
|
2465
|
-
* 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. */
|
|
2466
2548
|
function chaseAliasEitherTree(chaseFn, role, targetSet, trees, opts) {
|
|
2467
|
-
|
|
2468
|
-
|
|
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);
|
|
2469
2555
|
}
|
|
2470
2556
|
|
|
2471
2557
|
/** "<Name> owns/maintains <X>" — the ownership teach declarative. <Name> is one
|
|
@@ -2959,6 +3045,23 @@ function singularizeSurface(word) {
|
|
|
2959
3045
|
|
|
2960
3046
|
export { singularizeSurface };
|
|
2961
3047
|
|
|
3048
|
+
/** Does the complement of this payload wear its own article ("all foxes are A
|
|
3049
|
+
* SPECIES")? An article marks the complement singular already, so the plural
|
|
3050
|
+
* fold below must leave it alone. */
|
|
3051
|
+
const objectCarriesArticle = (payload) => /\s(?:is|are)\s+(?:an?\s+)/i.test(String(payload || ""));
|
|
3052
|
+
|
|
3053
|
+
/** The complement as it should be STORED. The subject of a plural membership
|
|
3054
|
+
* sentence has always folded ("all foxes are mammals" stores "fox"); the
|
|
3055
|
+
* object had not, so the pair landed as fox ⊑ mammals and the next turn's
|
|
3056
|
+
* "is a fox a mammal" looked up a class spelled a different way and missed
|
|
3057
|
+
* the fact it had just been told. Gated exactly as the subject fold is: only
|
|
3058
|
+
* a genuinely plural "are" phrasing, and only where no article already marks
|
|
3059
|
+
* the complement singular. */
|
|
3060
|
+
function storedObjectTerm(objectRaw, { verb, payload, lex, lookupNoun }) {
|
|
3061
|
+
if (!/^are$/i.test(verb) || objectCarriesArticle(payload)) return objectRaw;
|
|
3062
|
+
return lookupNoun(lex, objectRaw)?.lemma || singularizeSurface(objectRaw);
|
|
3063
|
+
}
|
|
3064
|
+
|
|
2962
3065
|
/** "(every|each|all|a|an )?X is/are (a|an )?Y" — the shape the unknown-subject
|
|
2963
3066
|
* fallback recognizes (group 2 = X, group 4 = Y); group 1 (when present)
|
|
2964
3067
|
* names the determiner, so the caller can tell a genuine "every" universal
|
|
@@ -3103,19 +3206,25 @@ async function ungroundedPairHint(payload, lexicon, memoryDir, cache = null, gra
|
|
|
3103
3206
|
if (!memoryDir) return "";
|
|
3104
3207
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
3105
3208
|
if (!m) return "";
|
|
3106
|
-
const [, , subjectRaw, , objectRaw] = m;
|
|
3107
|
-
const { loadLexicon } = await import("../domain/grammar/lexicon.mjs");
|
|
3209
|
+
const [, , subjectRaw, verb, objectRaw] = m;
|
|
3210
|
+
const { loadLexicon, lookupNoun } = await import("../domain/grammar/lexicon.mjs");
|
|
3108
3211
|
const lex = lexicon || loadLexicon();
|
|
3109
3212
|
if (await isGroundedTerm(subjectRaw, lex, memoryDir, cache, graph)) return "";
|
|
3110
3213
|
if (await isGroundedTerm(objectRaw, lex, memoryDir, cache, graph)) return "";
|
|
3214
|
+
// The sentences suggested here have to be the ones that actually store, so
|
|
3215
|
+
// both sides fold to the singular a plural surface named ("all zorps are
|
|
3216
|
+
// florbs" → "every zorp is a thing"). Following the plural verbatim teaches
|
|
3217
|
+
// a second class under a spelling nothing else uses.
|
|
3218
|
+
const subject = /^are$/i.test(verb) ? singularOf(subjectRaw, lex, lookupNoun) : subjectRaw;
|
|
3219
|
+
const object = storedObjectTerm(objectRaw, { verb, payload, lex, lookupNoun });
|
|
3111
3220
|
// Chaining the second term UNDER the first's now-grounded proper name
|
|
3112
3221
|
// ("every man is a john") is technically accepted by the grammar (once
|
|
3113
3222
|
// "john" is grounded, ANY term can be taught as a kind of it), but reads as
|
|
3114
3223
|
// nonsense to a human, since a proper name is never a category. Ground both
|
|
3115
3224
|
// sides independently instead — two clear, parallel, semantically sane
|
|
3116
3225
|
// suggestions, not a confusing chain through an arbitrary first term.
|
|
3117
|
-
return ` I don't know "${
|
|
3118
|
-
+ `"every ${
|
|
3226
|
+
return ` I don't know "${subject}" or "${object}" yet. Try grounding each one first, e.g. `
|
|
3227
|
+
+ `"every ${subject} is a thing" and "every ${object} is a thing", then re-teach the`
|
|
3119
3228
|
+ ` original fact.`;
|
|
3120
3229
|
}
|
|
3121
3230
|
|
|
@@ -3192,7 +3301,12 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon,
|
|
|
3192
3301
|
if (lookupNoun(lex, objectRaw) || GENERIC_ANCHOR_NOUNS.has(String(objectRaw).toLowerCase())
|
|
3193
3302
|
|| (await isGroundedByFact(objectRaw, memoryDir, cache))) {
|
|
3194
3303
|
return teachFact(memoryDir, sessionId, {
|
|
3195
|
-
subject,
|
|
3304
|
+
subject,
|
|
3305
|
+
predicate: SUBCLASS_PREDICATE,
|
|
3306
|
+
object: storedObjectTerm(objectRaw, { verb, payload, lex, lookupNoun }),
|
|
3307
|
+
quantifier,
|
|
3308
|
+
observedAt,
|
|
3309
|
+
dateText,
|
|
3196
3310
|
});
|
|
3197
3311
|
}
|
|
3198
3312
|
if (lookupAdjective(lex, objectRaw)) {
|
|
@@ -3321,7 +3435,12 @@ async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon, c
|
|
|
3321
3435
|
? (lookupNoun(lex, subjectRaw)?.lemma || singularizeSurface(subjectRaw))
|
|
3322
3436
|
: subjectRaw;
|
|
3323
3437
|
return teachFact(memoryDir, sessionId, {
|
|
3324
|
-
subject,
|
|
3438
|
+
subject,
|
|
3439
|
+
predicate: SUBCLASS_PREDICATE,
|
|
3440
|
+
object: storedObjectTerm(objectRaw, { verb, payload, lex, lookupNoun }),
|
|
3441
|
+
quantifier,
|
|
3442
|
+
observedAt,
|
|
3443
|
+
dateText,
|
|
3325
3444
|
});
|
|
3326
3445
|
}
|
|
3327
3446
|
|
|
@@ -4160,9 +4279,20 @@ function matchBareCanTeach(text) {
|
|
|
4160
4279
|
function teachSuggestion(payload) {
|
|
4161
4280
|
const m = String(payload).match(/^(?:every |each |all |a |an )?([\w-]+) (is|are) (a |an )?([\w-]+)[.!?]*$/i);
|
|
4162
4281
|
if (!m) return null;
|
|
4163
|
-
const
|
|
4164
|
-
const
|
|
4165
|
-
|
|
4282
|
+
const plural = /^are$/i.test(m[2]);
|
|
4283
|
+
const subject = (plural ? singularizeSurface(m[1]) : m[1]).toLowerCase();
|
|
4284
|
+
let object = m[4].toLowerCase();
|
|
4285
|
+
let articled = !!m[3];
|
|
4286
|
+
// A plural complement with no article of its own ("all lynxes are mammals")
|
|
4287
|
+
// folds like the subject does, and the fold itself is what proves it was a
|
|
4288
|
+
// plural NOUN rather than a bare adjective — so it earns the article the
|
|
4289
|
+
// singular membership sentence needs. Without this the repair suggested the
|
|
4290
|
+
// very spelling the same sentence had just called unrecognized.
|
|
4291
|
+
if (plural && !articled) {
|
|
4292
|
+
const folded = singularizeSurface(object);
|
|
4293
|
+
if (folded !== object) { object = folded; articled = true; }
|
|
4294
|
+
}
|
|
4295
|
+
if (!articled) return `every ${subject} is ${object}`;
|
|
4166
4296
|
const articleRule = grammarRules().find((r) => r.kind === "article");
|
|
4167
4297
|
const article = articleRule && beginsWithVowelSound(object, articleRule) ? "an" : "a";
|
|
4168
4298
|
return `every ${subject} is ${article} ${object}`;
|
|
@@ -4327,14 +4457,43 @@ const RETRACT_NOT_A_RE = /^(?:a\s+|an\s+)?([\w-]+(?:\s+[\w-]+)?)\s+(?:(?:is|are)
|
|
|
4327
4457
|
* remember/note/keep in mind/…), so this is matched against the RAW
|
|
4328
4458
|
* (unwrapped) sentence, unlike RETRACT_NOT_A_RE above which is tried against
|
|
4329
4459
|
* the remember-wrapped surface too. */
|
|
4330
|
-
|
|
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
|
+
}
|
|
4331
4490
|
/** The locative teach shape ("disk-1 rests on peg-b") — the board-fact
|
|
4332
4491
|
* surface, shared by the mid-plan write guard and the locative forget. */
|
|
4333
4492
|
const BOARD_TEACH_LOCATIVE_RE = new RegExp(`^([\\w-]+)\\s+([a-z]+)s\\s+(${PREP_SRC})\\s+([\\w-]+)$`, "i");
|
|
4334
4493
|
/** "forget that disk-1 rests on peg-b" — the locative twin of
|
|
4335
4494
|
* RETRACT_FORGET_RE: a plain minted mgx:<verb>-<prep> fact has no entailment
|
|
4336
4495
|
* cascade, so removing the one row IS the retraction. */
|
|
4337
|
-
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");
|
|
4338
4497
|
|
|
4339
4498
|
/** The closed related-to pair — "X relates to Y" / "X is related to Y" —
|
|
4340
4499
|
* minted onto mgx:relatedTo (the SKOS view's skos:related source), so the
|
|
@@ -4461,7 +4620,8 @@ async function teachExclusionReason(sentence) {
|
|
|
4461
4620
|
if (TEACH_EXCLUDE_REQUEST_LEAD_RE.test(s) || (await hasMidSentenceInterrogative(s))) return "interrogative";
|
|
4462
4621
|
const unpunctuated = s.replace(/[.!?]+\s*$/, "");
|
|
4463
4622
|
if (TEACH_EXCLUDE_IMPERATIVE_LEAD_RE.test(s)
|
|
4464
|
-
&& !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";
|
|
4465
4625
|
if (TEACH_EXCLUDE_META_TOKEN_RE.test(s) && !TEACH_PRONOUN_BARE_RE.test(s)) return "self-referential";
|
|
4466
4626
|
return null;
|
|
4467
4627
|
}
|
|
@@ -4799,9 +4959,11 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4799
4959
|
// the SHAPE of a retraction sentence — it says nothing about whether
|
|
4800
4960
|
// subject⊑object was ever actually taught, or taught by THIS session.
|
|
4801
4961
|
// retractSubClassOf is asked for real and is the only thing that decides:
|
|
4802
|
-
// - found:false → subject⊑object was never a stored fact,
|
|
4803
|
-
//
|
|
4804
|
-
//
|
|
4962
|
+
// - found:false → subject⊑object was never a stored fact, so there is
|
|
4963
|
+
// nothing to withdraw and the turn says exactly that. It must NOT
|
|
4964
|
+
// fall through to the rest of the cascade: the teach frames below
|
|
4965
|
+
// read the unconsumed sentence from the top, take "forget bertha" as
|
|
4966
|
+
// a two-word subject, and confirm a fact the user asked to destroy.
|
|
4805
4967
|
// - found:true, ownRecord:false → some OTHER source taught it; this
|
|
4806
4968
|
// session has nothing of its own to withdraw.
|
|
4807
4969
|
// - found:true, stillStands:true → this session's own record is gone,
|
|
@@ -4849,7 +5011,38 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4849
5011
|
via: "retract", miss: false,
|
|
4850
5012
|
};
|
|
4851
5013
|
}
|
|
4852
|
-
|
|
5014
|
+
return {
|
|
5015
|
+
text: `"${retractSubject} is a kind of ${retractObject}" isn't stored, so there's nothing to forget.`,
|
|
5016
|
+
via: "retract", miss: true,
|
|
5017
|
+
};
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
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 */ }
|
|
4853
5046
|
}
|
|
4854
5047
|
}
|
|
4855
5048
|
|
|
@@ -6231,6 +6424,31 @@ async function presuppositionNudge(query, { graph, memoryDir }) {
|
|
|
6231
6424
|
return { text: holds ? `${verdict}. ${subjEnt.label} does ${split.verb} ${objEnt.label}.` : `${verdict} — the premise doesn't hold.` };
|
|
6232
6425
|
}
|
|
6233
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
|
+
|
|
6234
6452
|
/** The wall-repeat one-liner. MUST NOT match
|
|
6235
6453
|
* WALL_MISS_RE: the suppression keys on the PREVIOUS answer matching it, so this
|
|
6236
6454
|
* text self-limits — a third consecutive miss re-offers the tailored hint. */
|
|
@@ -6290,6 +6508,8 @@ export async function helpText() {
|
|
|
6290
6508
|
["/syllogise <term>", "work out and remember what follows from the facts about a term (needed for chains longer than 2 hops)"],
|
|
6291
6509
|
["/export <path>", "write the memory store to a file, as JSONL (the same shape `tmct memory --export` writes)"],
|
|
6292
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"],
|
|
6293
6513
|
["/narrate on|off", "verbose developer/debug mode: decision points, matched pattern, results+sources, goal per turn"],
|
|
6294
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"],
|
|
6295
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"],
|
|
@@ -7883,6 +8103,15 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7883
8103
|
// tail, verbatim.
|
|
7884
8104
|
if (m) metaTerm = stripTrailingScopeFiller(m[1]);
|
|
7885
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
|
+
}
|
|
7886
8115
|
// An ambiguous parse tie ({ambiguousParse}) reaches this lane with
|
|
7887
8116
|
// envelope.parsed nulled and miss=false, so NEITHER branch above arms —
|
|
7888
8117
|
// but when one tied reading is META and memory holds facts for its term
|
|
@@ -8322,18 +8551,24 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
8322
8551
|
const capable = uniqueFacts(facts.filter((f) => f.predicate === "mgx:capableOf" && verbVariants.has(f.object)))
|
|
8323
8552
|
.filter((f) => resolveCapabilityPolarity(new Set([f.subject]), verbVariants, facts).verdict === "yes");
|
|
8324
8553
|
if (capable.length) {
|
|
8325
|
-
const {
|
|
8554
|
+
const {
|
|
8555
|
+
findIsaChain, buildSubClassSuccessors: buildKindSuccessors,
|
|
8556
|
+
SUBCLASS_PREDICATE: SC_PRED, TYPE_PREDICATE: TYPE_PRED,
|
|
8557
|
+
} = await import("../domain/syllogise.mjs");
|
|
8326
8558
|
const subClassRows = facts.filter((f) => f.predicate === SC_PRED);
|
|
8327
8559
|
const typeRows = facts.filter((f) => f.predicate === TYPE_PRED);
|
|
8328
8560
|
const subClassEdges = subClassRows.map((f) => [f.subject, f.object]);
|
|
8329
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);
|
|
8330
8565
|
const rowForStep = (step) => (step.predicate === SC_PRED ? subClassRows : typeRows)
|
|
8331
8566
|
.find((g) => g.subject === step.subject && g.object === step.object);
|
|
8332
8567
|
const chainBySubject = new Map();
|
|
8333
8568
|
const inKind = capable.filter((f) => {
|
|
8334
8569
|
if (kindVariants.has(f.subject)) return true;
|
|
8335
8570
|
if (!chainBySubject.has(f.subject)) {
|
|
8336
|
-
chainBySubject.set(f.subject, findIsaChain(f.subject, kindVariants, typeEdges,
|
|
8571
|
+
chainBySubject.set(f.subject, findIsaChain(f.subject, kindVariants, typeEdges, subClassSucc, { maxHops: 3 }));
|
|
8337
8572
|
}
|
|
8338
8573
|
return !!chainBySubject.get(f.subject);
|
|
8339
8574
|
});
|
|
@@ -9442,7 +9677,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9442
9677
|
const noun = await entityClassNoun(graph, isaSubject);
|
|
9443
9678
|
if (noun) for (const v of factTermVariants(normFactTerm, noun)) subjCandidates.add(v);
|
|
9444
9679
|
const {
|
|
9445
|
-
findIsaChain, deriveDisjointViolations,
|
|
9680
|
+
findIsaChain, buildSubClassSuccessors: buildChaseSuccessors, deriveDisjointViolations,
|
|
9446
9681
|
SUBCLASS_PREDICATE: SC_PREDICATE, TYPE_PREDICATE: RDF_TYPE_PREDICATE, DISJOINT_PREDICATE,
|
|
9447
9682
|
} = await import("../domain/syllogise.mjs");
|
|
9448
9683
|
const isTaught = isOperatorTaught;
|
|
@@ -9454,6 +9689,10 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9454
9689
|
const mixedTypeRows = isa.filter((f) => f.predicate === RDF_TYPE_PREDICATE);
|
|
9455
9690
|
const mixedTypeEdges = mixedTypeRows.map((f) => [f.subject, f.object]);
|
|
9456
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);
|
|
9457
9696
|
const disjointRows = rows.filter((f) => f.predicate === DISJOINT_PREDICATE && isTaught(f));
|
|
9458
9697
|
const disjointEdges = disjointRows.map((f) => [f.subject, f.object]);
|
|
9459
9698
|
// CAX-DW GATE, COMPUTED BEFORE ANY "YES" MAY RETURN: every taught
|
|
@@ -9615,7 +9854,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9615
9854
|
const factForStep = (step) => (step.predicate === SC_PREDICATE ? chainSubClassRows : chainTypeRows)
|
|
9616
9855
|
.find((f) => f.subject === step.subject && f.object === step.object);
|
|
9617
9856
|
for (const subj of subjCandidates) {
|
|
9618
|
-
const chain = findIsaChain(subj, objVariants, chainTypeEdges,
|
|
9857
|
+
const chain = findIsaChain(subj, objVariants, chainTypeEdges, chainSubClassSucc, { maxHops: 2 });
|
|
9619
9858
|
if (!chain) continue;
|
|
9620
9859
|
const chainRefusal = disjointRefusalFor(subj);
|
|
9621
9860
|
if (chainRefusal) return chainRefusal;
|
|
@@ -9636,7 +9875,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9636
9875
|
const mixedFactForStep = (step) => (step.predicate === SC_PREDICATE ? mixedSubClassRows : mixedTypeRows)
|
|
9637
9876
|
.find((f) => f.subject === step.subject && f.object === step.object);
|
|
9638
9877
|
for (const subj of subjCandidates) {
|
|
9639
|
-
const chain = findIsaChain(subj, objVariants, mixedTypeEdges,
|
|
9878
|
+
const chain = findIsaChain(subj, objVariants, mixedTypeEdges, mixedSubClassSucc, { maxHops: 2 });
|
|
9640
9879
|
if (!chain) continue;
|
|
9641
9880
|
const chainRefusal = disjointRefusalFor(subj);
|
|
9642
9881
|
if (chainRefusal) return chainRefusal;
|
|
@@ -9766,7 +10005,9 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9766
10005
|
? deriveSomeValuesFromSubsumption(restrictionEdges, chainSubClassEdges, { budget: 10 })
|
|
9767
10006
|
: [];
|
|
9768
10007
|
if (svfSubsumption.length) {
|
|
9769
|
-
const
|
|
10008
|
+
const enlargedSubClassSucc = buildChaseSuccessors(
|
|
10009
|
+
chainSubClassEdges.concat(svfSubsumption.map((d) => [d.subject, d.object])),
|
|
10010
|
+
);
|
|
9770
10011
|
// The SAME `min(premiseTrusts) x
|
|
9771
10012
|
// ruleConfidence` discipline syllogise()'s own batch pass now applies
|
|
9772
10013
|
// to scm-svf1 (src/domain/syllogise.mjs), computed here for this LIVE,
|
|
@@ -9814,7 +10055,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9814
10055
|
: undefined;
|
|
9815
10056
|
};
|
|
9816
10057
|
for (const subj of subjCandidates) {
|
|
9817
|
-
const chain = findIsaChain(subj, objVariants, chainTypeEdges,
|
|
10058
|
+
const chain = findIsaChain(subj, objVariants, chainTypeEdges, enlargedSubClassSucc, { maxHops: 3 });
|
|
9818
10059
|
if (!chain) continue;
|
|
9819
10060
|
const premises = chain.map(factForStepOrSvf);
|
|
9820
10061
|
if (premises.every(Boolean)) {
|
|
@@ -9856,7 +10097,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9856
10097
|
// edge lists the chases use, and /syllogise closes over a superset of
|
|
9857
10098
|
// them, so a chain found here is one it can really materialize.
|
|
9858
10099
|
const deeperChainExists = [...subjCandidates].some(
|
|
9859
|
-
(subj) => findIsaChain(subj, objVariants, chainTypeEdges,
|
|
10100
|
+
(subj) => findIsaChain(subj, objVariants, chainTypeEdges, chainSubClassSucc, { maxHops: DEEP_CHAIN_PROBE_HOPS }),
|
|
9860
10101
|
);
|
|
9861
10102
|
if (knownSubjectIsa.length) {
|
|
9862
10103
|
const shown = knownSubjectIsa.slice(0, 3).map(renderFactLine).join("; ");
|
|
@@ -10803,6 +11044,12 @@ const BARE_WHATIS_RE = /^what\s+(?:is|are)\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
|
10803
11044
|
* subject or a relation object); with no such facts it returns null and the
|
|
10804
11045
|
* turn falls through to the author/relation who-readers unchanged. */
|
|
10805
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;
|
|
10806
11053
|
|
|
10807
11054
|
/** The meta term a "what is a X" / "what is X" / "what does X mean" / "define X"
|
|
10808
11055
|
* question asks about — from the parse when present, else recognized directly
|
|
@@ -10821,7 +11068,7 @@ function metaTermOf(query, envelope) {
|
|
|
10821
11068
|
const q = String(query).trim();
|
|
10822
11069
|
const m = q.match(BARE_WHATIS_RE)
|
|
10823
11070
|
|| q.match(/^what\s+(?:does|do)\s+(?:an?\s+)?(.+?)\s+means?[?.!\s]*$/i)
|
|
10824
|
-
|| q.match(
|
|
11071
|
+
|| q.match(DEFINE_IMPERATIVE_RE);
|
|
10825
11072
|
return m ? stripTrailingScopeFiller(stripTrailingDiscourseTag(m[1].trim())) : null;
|
|
10826
11073
|
}
|
|
10827
11074
|
|
|
@@ -13122,6 +13369,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13122
13369
|
// that blurb (a dispatched turn) then becomes `last`, wiping the very
|
|
13123
13370
|
// antecedent the next pronoun turn needs.
|
|
13124
13371
|
const isConversationalCandidate = conversationalCandidateBaseGate && !vocabAntecedent && isConversational(query);
|
|
13372
|
+
const liveGameOwnWord = gameOwnWord(query, planHolder);
|
|
13125
13373
|
// "what is X" with NO article ("what is john") is BOTH conversational-shaped
|
|
13126
13374
|
// (isConversational() would claim it) AND a legitimate bare meta/fact-lookup
|
|
13127
13375
|
// form (BARE_WHATIS_RE). Diverts ONLY when a REAL fact actually resolves for
|
|
@@ -13259,6 +13507,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13259
13507
|
const fallback = metaFallbackEntityAnswer(graph, String(query).trim());
|
|
13260
13508
|
if (fallback) bareMetaHit = { text: fallback.text, replace: true };
|
|
13261
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;
|
|
13262
13514
|
const coldPronounDecline = focus?.label ? null : coldPronounDeclineText(query);
|
|
13263
13515
|
let selfContainedMiss = false;
|
|
13264
13516
|
if (bareMetaHit?.reference) {
|
|
@@ -13322,7 +13574,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13322
13574
|
via = "template"; handled = true;
|
|
13323
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");
|
|
13324
13576
|
note(trace, "goal: resolve a pronoun to a subject (nothing named yet)");
|
|
13325
|
-
} else if (isConversationalCandidate && planHolder?.state?.game) {
|
|
13577
|
+
} else if ((isConversationalCandidate || liveGameOwnWord) && planHolder?.state?.game) {
|
|
13326
13578
|
// MID-GAME: a short line that parsed as nothing ("you said lower", "is it
|
|
13327
13579
|
// warm in here") stays INSIDE the game frame with a nudge naming the
|
|
13328
13580
|
// state — the identity card answers a question nobody asked, and it used
|
|
@@ -13336,9 +13588,26 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13336
13588
|
dialogueLaneOverride = "game-inform";
|
|
13337
13589
|
note(trace, "lane: (2) MID-GAME NUDGE — an unparsed short turn stayed inside the live game frame instead of the identity card");
|
|
13338
13590
|
note(trace, "goal: keep the running guess-the-number game on track");
|
|
13339
|
-
} 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)) {
|
|
13340
13607
|
// A conversational miss (a greeting, "what can you do", a very short non-code
|
|
13341
13608
|
// line) gets the friendly orientation (module-aware: empty → --repo/tmct init).
|
|
13609
|
+
// A short question naming a TERM is excluded: nothing above resolved it, so
|
|
13610
|
+
// it belongs on the miss wall its four-word twin already reaches.
|
|
13342
13611
|
// This branch carries via:"template" and never reaches the composed-only
|
|
13343
13612
|
// wall-shortening gate below, so it needs its own repeat collapse,
|
|
13344
13613
|
// mirroring WALL_REPEAT_ONELINER.
|
|
@@ -13413,6 +13682,16 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13413
13682
|
deduced = TAUGHT_FACT_LOOKUP_GOAL;
|
|
13414
13683
|
note(trace, `goal: ${deduced} (revised — a general-verb direct-question fact lookup answered this turn)`);
|
|
13415
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
|
+
}
|
|
13416
13695
|
} else if (miss) {
|
|
13417
13696
|
// W2: after the honest miss is composed, consult the folded-session memory. A
|
|
13418
13697
|
// relevant enough block ANSWERS — recalled Q/A framed + cited first, with the
|
|
@@ -13530,7 +13809,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13530
13809
|
// all, so it would otherwise be confidently WRONG or silently absent.
|
|
13531
13810
|
// Every successfully-recognized teach attempt gets this consistent goal
|
|
13532
13811
|
// line instead.
|
|
13533
|
-
|
|
13812
|
+
// A retraction reaches this lane too, and its goal is the opposite one:
|
|
13813
|
+
// saying "teach/remember a new fact" under a sentence asking for a fact
|
|
13814
|
+
// to go describes the turn backwards.
|
|
13815
|
+
deduced = taught.via === "retract" ? "withdraw a remembered fact" : "teach/remember a new fact";
|
|
13534
13816
|
note(trace, `goal: ${deduced} (revised — the teach lane recognized this shape where the raw structural parse never should have)`);
|
|
13535
13817
|
// A canonical whose verb only matched through the fuzzy edit-distance
|
|
13536
13818
|
// tier ("disk-1 rests on peg-a." read as an ask about "tests") restates
|
|
@@ -13791,7 +14073,12 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13791
14073
|
// does. Both shapes below anchor on the written-out copula.
|
|
13792
14074
|
const offerSrc = expandContractions(String(query).trim());
|
|
13793
14075
|
const knowAboutTerm = offerSrc.match(KNOW_ABOUT_RE)?.[1]?.trim();
|
|
13794
|
-
|
|
14076
|
+
// "who is grelb" asks about a term the same way "what is grelb" does, and
|
|
14077
|
+
// a name is exactly what a person teaches next — without this the who-form
|
|
14078
|
+
// dead-ends on the bare grammar wall while its what-form twin offers the
|
|
14079
|
+
// teach phrasing.
|
|
14080
|
+
const whoTerm = shortTermQuestionTerm(offerSrc) ? offerSrc.match(WHO_IS_BARE_RE)?.[1]?.trim() : null;
|
|
14081
|
+
const offerTerm = knowAboutTerm || metaTermOf(offerSrc, envelope) || whoTerm;
|
|
13795
14082
|
// A term that LEADS with a bindable anaphor ("it used for", from an
|
|
13796
14083
|
// unresolved "what is it used for") is a pronoun that failed to bind,
|
|
13797
14084
|
// not a teachable subject — offering to learn facts about it would echo
|
|
@@ -13836,9 +14123,14 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13836
14123
|
} else if (browser) {
|
|
13837
14124
|
answer = `${answer}\n(I don't know that yet — you can teach me: say "remember: <thing> is a <kind>".)`;
|
|
13838
14125
|
note(trace, "intermediate: HONEST-EMPTY POLISH — a browser miss points at the teach lane, not the CLI-only --repo remedy");
|
|
13839
|
-
} else {
|
|
14126
|
+
} else if (looksCodeish(String(query), String(query).toLowerCase())) {
|
|
13840
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\`.)`;
|
|
13841
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");
|
|
13842
14134
|
}
|
|
13843
14135
|
}
|
|
13844
14136
|
if (teachOffer) {
|