@polycode-projects/the-mechanical-code-talker 2.7.12 → 2.7.14
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/corpus/worlds/manifest.json +5 -5
- package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
- package/corpus/worlds/src/ashcombe-hall.jsonl +1 -0
- package/package.json +1 -1
- package/src/domain/ask.mjs +27 -3
- package/src/domain/interpret/normalize.mjs +1 -1
- package/src/domain/interpret/strategies/keywords.mjs +15 -2
- package/src/domain/spider-fly-world.mjs +13 -0
- package/src/domain/sprite-map.mjs +49 -1
- package/src/services/adventure-autoplay.mjs +192 -0
- package/src/services/adventure-viz.mjs +262 -0
- package/src/services/chat.mjs +420 -37
- package/src/services/spider-fly-viz.mjs +39 -9
- package/src/services/spider-fly.mjs +236 -68
- package/src/surfaces/web/adventure-browser-entry.mjs +88 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +69 -15
- package/src/surfaces/web/spider-fly-browser-entry.mjs +10 -8
package/src/services/chat.mjs
CHANGED
|
@@ -884,6 +884,12 @@ const CAPABILITY_PHRASES = [
|
|
|
884
884
|
// into a capability question; not covered by the "do" pair above since
|
|
885
885
|
// neither accepts "help (me)? with" as a synonym tail for "do".
|
|
886
886
|
/^(?:so,?\s+)?what can (?:you|u)(?:\s+(?:actually|really))? help(?:\s+me)?\s+with\??$/i,
|
|
887
|
+
// "can u help me with smth" — the SAME request, inverted word order
|
|
888
|
+
// ("can you help ME with X" rather than "what can you help with"), plus
|
|
889
|
+
// texting shorthand ("u", "smth"). A vague object (smth/something/this/
|
|
890
|
+
// that) never names a real term to look up, so it's the capability
|
|
891
|
+
// question, not a request about a specific thing.
|
|
892
|
+
/^can (?:you|u) help me with (?:smth|something|this|that)\??$/i,
|
|
887
893
|
/^help( me)?\??$/i, /^\?+$/,
|
|
888
894
|
/^how do (i|you) work\??$/i, /^how does (this|it) work\??$/i,
|
|
889
895
|
// unix-habit openers typed inside the REPL out of muscle memory — argv-only
|
|
@@ -952,6 +958,56 @@ const AI_IDENTITY_PHRASES = [
|
|
|
952
958
|
/^is this (chatgpt|gpt|claude|an? ai|an? llm)\??$/i,
|
|
953
959
|
/^do you use ai\??$/i, /^what language model are you( using)?\??$/i,
|
|
954
960
|
/^am i (talking|speaking|chatting) (to|with) a (real )?(person|human|bot|ai)\??$/i,
|
|
961
|
+
// "what model are you built on, GPT-4 or Claude?" — the SAME underlying
|
|
962
|
+
// question as "are you secretly GPT" above, just posed as an open pick
|
|
963
|
+
// between named models rather than a yes/no. The trailing model-name pair
|
|
964
|
+
// is optional (the closed lead alone is already unambiguous).
|
|
965
|
+
/^what model (?:are you|is this) (?:built|based|running) on(?:,?\s*(?:gpt-?\d(?:\.\d)?|chatgpt|claude|gemini|llama)(?:\s+or\s+(?:gpt-?\d(?:\.\d)?|chatgpt|claude|gemini|llama))?)?\??$/i,
|
|
966
|
+
// "do you use classical logic" — a mechanism question, not phrased as "are
|
|
967
|
+
// you an AI", but asking the identical underlying thing (rule-based/
|
|
968
|
+
// deterministic vs. a statistical model) T_IDENTITY_NOT_LLM already answers.
|
|
969
|
+
/^do you use classical logic\??$/i,
|
|
970
|
+
// "can u browse the internet" — tmct genuinely has no network access in the
|
|
971
|
+
// product path (no-LLM constitution, deterministic offline reasoning), so
|
|
972
|
+
// this is a real "no", not the generic capability listing.
|
|
973
|
+
/^can (?:you|u) (?:browse|access|use|go on|connect to) the internet\??$/i,
|
|
974
|
+
];
|
|
975
|
+
|
|
976
|
+
/** META-COMMAND/SESSION questions — a RETURNING USER checking whether a
|
|
977
|
+
* remembered command or session behavior still holds ("is /focus still a
|
|
978
|
+
* command", "did you keep anything from last session"). Without a
|
|
979
|
+
* recognizer, a literal "/focus"/"/forget"/"/stats" token embedded in an
|
|
980
|
+
* ordinary sentence reads as a bare word to whichever parser gets to it
|
|
981
|
+
* first (the teach lane, or a code-import/definition lookup), producing
|
|
982
|
+
* garbled nonsense instead of the plain, true answer — even though the
|
|
983
|
+
* underlying capability (or its real equivalent) verifiably works when
|
|
984
|
+
* invoked directly. Each entry answers the SPECIFIC thing asked, closed
|
|
985
|
+
* and hand-written (never a guess): confirming what still works, or
|
|
986
|
+
* naming the real equivalent for something that was never a command at
|
|
987
|
+
* all ("/forget" isn't one; "forget that X is a Y" retracts a taught
|
|
988
|
+
* fact instead). */
|
|
989
|
+
const META_FOCUS_STILL_RE = /^can i still (?:do|use) \/?focus\b/i;
|
|
990
|
+
const META_FOCUS_RENAMED_RE = /^is \/?focus (?:even )?still a command\b/i;
|
|
991
|
+
const META_FORGET_RE = /^what about \/?forget\b/i;
|
|
992
|
+
const META_STATS_STILL_RE = /^is there still a stats command\b/i;
|
|
993
|
+
const META_COMPARE_STILL_RE = /^can (?:you|u) still do that thing where you compare two classes\b/i;
|
|
994
|
+
const META_LAST_SESSION_RE = /^did you keep anything from (?:our |my )?last session\b/i;
|
|
995
|
+
/** One answer per META_* recognizer above, in the same order, so the
|
|
996
|
+
* dispatch site (conversationalTurn) stays a flat, readable table rather
|
|
997
|
+
* than a chain of near-identical if-blocks. */
|
|
998
|
+
const META_COMMAND_ANSWERS = [
|
|
999
|
+
[META_FOCUS_STILL_RE, "Yes — /focus still works, unrenamed: \"/focus <symbol>\" sets the current focus, "
|
|
1000
|
+
+ "reused by \"it\"/\"this\" and no-arg entity commands. /help lists every command."],
|
|
1001
|
+
[META_FOCUS_RENAMED_RE, "Yes — /focus is still a real command, never renamed: \"/focus <symbol>\" sets the "
|
|
1002
|
+
+ "current focus. /help lists every command."],
|
|
1003
|
+
[META_FORGET_RE, "There's no /forget command, but a taught fact IS undoable — say \"forget that <subject> is "
|
|
1004
|
+
+ "a <object>\" (the exact fact as taught) to retract it. /memory shows what's currently stored."],
|
|
1005
|
+
[META_STATS_STILL_RE, "Yes — /stats still works: a one-screen overview of entity counts, relationship "
|
|
1006
|
+
+ "counts, and packages. /help lists every command."],
|
|
1007
|
+
[META_COMPARE_STILL_RE, "Yes — say \"compare <X> and <Y>\" for two entities of the same kind. /help lists "
|
|
1008
|
+
+ "every command and question shape."],
|
|
1009
|
+
[META_LAST_SESSION_RE, "Taught facts and folded session summaries persist between sessions (written to "
|
|
1010
|
+
+ ".tmct/ on disk) — it's never a clean slate. /memory shows what's currently remembered."],
|
|
955
1011
|
];
|
|
956
1012
|
|
|
957
1013
|
/** Split raw turn text into candidate single-sentence clauses on sentence-
|
|
@@ -1007,6 +1063,17 @@ const FEELINGS_PHRASES = [
|
|
|
1007
1063
|
/^can you (?:tell|make)\s+(?:me\s+)?(?:a\s+)?jokes?\??$/i,
|
|
1008
1064
|
/^do you (?:know|know anything|know much)\s+about\s+(?:movies?|sports?|music|tv|television)(?:\s+or\s+(?:movies?|sports?|music|tv|television))?\??$/i,
|
|
1009
1065
|
];
|
|
1066
|
+
/** "whats 2+2" — a bare arithmetic expression, not a code/vocabulary question
|
|
1067
|
+
* at all. With no closed-set match of its own, this fell into the SAME
|
|
1068
|
+
* "≤3 words, not code-ish" catch-all a genuine orientation opener
|
|
1069
|
+
* ("what's up", "so what is this") uses, giving the non-sequitur identity
|
|
1070
|
+
* blurb where an honest "I don't do arithmetic" decline belongs. Deliberately
|
|
1071
|
+
* excludes "-" from the operator set: this domain's OWN dates ("what
|
|
1072
|
+
* changed since 2026-01-01") and file/line ranges ("model.mjs:9-15") are
|
|
1073
|
+
* digit-hyphen-digit too, and a real ambiguity there must stay a real
|
|
1074
|
+
* structural answer, never this decline. "+"/"*"/"/" have no such
|
|
1075
|
+
* collision in tmct's own vocabulary. */
|
|
1076
|
+
const ARITHMETIC_RE = /\d+\s*[+*/]\s*\d+/;
|
|
1010
1077
|
/** The structural verbs/nouns that mark a near-miss code question (→ keep the
|
|
1011
1078
|
* precise grammar hint, not the friendly nudge). */
|
|
1012
1079
|
const STRUCT_WORDS = new Set([
|
|
@@ -1582,11 +1649,28 @@ function conversationalTurn(line, ctx) {
|
|
|
1582
1649
|
note(ctx.trace, "lane: conversational — identity/feelings (FEELINGS_PHRASES closed set)");
|
|
1583
1650
|
return mk(t(T_IDENTITY_NO_FEELINGS), { lane: "help" });
|
|
1584
1651
|
}
|
|
1652
|
+
if (ARITHMETIC_RE.test(raw)) {
|
|
1653
|
+
note(ctx.trace, "goal: arithmetic — not a code/vocabulary question, an honest decline");
|
|
1654
|
+
note(ctx.trace, "lane: conversational — arithmetic decline (ARITHMETIC_RE)");
|
|
1655
|
+
return mk(
|
|
1656
|
+
"I don't do arithmetic — I answer questions about a code graph or taught facts. "
|
|
1657
|
+
+ "Try \"what is a dog\" for vocabulary, or point me at a repo with --repo <path>.",
|
|
1658
|
+
{ lane: "help" },
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1585
1661
|
if (IDENTITY_PHRASES.some((re) => re.test(raw))) {
|
|
1586
1662
|
note(ctx.trace, "goal: identity — who/what tmct is, not a capability listing");
|
|
1587
1663
|
note(ctx.trace, "lane: conversational — identity (IDENTITY_PHRASES closed set)");
|
|
1588
1664
|
return mk(t(T_IDENTITY_SELF), { lane: "help" });
|
|
1589
1665
|
}
|
|
1666
|
+
{
|
|
1667
|
+
const metaHit = META_COMMAND_ANSWERS.find(([re]) => re.test(raw));
|
|
1668
|
+
if (metaHit) {
|
|
1669
|
+
note(ctx.trace, "goal: meta — does a remembered command/session behavior still hold");
|
|
1670
|
+
note(ctx.trace, "lane: conversational — meta-command/session (closed per-command answer set)");
|
|
1671
|
+
return mk(metaHit[1], { lane: "help" });
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1590
1674
|
// CAPABILITY_PHRASES' vague-opener entries are self-contained closed
|
|
1591
1675
|
// regexes, but a preamble ahead of one ("right, can you walk me through
|
|
1592
1676
|
// this codebase" — an ACK_PREAMBLE_RE + MODAL_WRAPPER_RE stack) is tested
|
|
@@ -1801,7 +1885,14 @@ const WALL_MISS_ANYWHERE_RE = /couldn't parse this as a graph question\. Try:/;
|
|
|
1801
1885
|
// assert/memory path; when it can't be stored, say what CAN be remembered
|
|
1802
1886
|
// instead of the grammar wall or a silent data loss.
|
|
1803
1887
|
const TEACH_RE = /^(?:please\s+)?(?:i\s+(?:want|wanted)\s+you\s+to\s+|i(?:'d|\s+would)\s+like\s+you\s+to\s+)?(?:remember|note|keep in mind|jot down|for the record|fyi|learn)\b(?:\s+(?:this|that|also))?[:,]?\s*(?:that\s+)?(.+?)[.?!]*$/i;
|
|
1804
|
-
|
|
1888
|
+
// A trailing sentence-final mark ([.!?]*) is tolerated at the very end: an
|
|
1889
|
+
// ordinary first turn typed as a full sentence ("every dog is a mammal.")
|
|
1890
|
+
// otherwise failed this shape test by one character whenever neither ACE nor
|
|
1891
|
+
// the wrapped path could take it first, so teachLane bailed out (payload
|
|
1892
|
+
// stayed null) before ever trying the unknown-subject/object mint fallbacks
|
|
1893
|
+
// below — the SAME sentence typed without the period worked. Mirrors
|
|
1894
|
+
// UNKNOWN_SUBJECT_RE's own identical tolerance, added for the same reason.
|
|
1895
|
+
const BARE_DECLARATIVE_RE = /^(?:every |each |all |a |an )?[\w-]+(?: [\w-]+)? (?:is|are) (?:a |an )?[\w-]+(?: too)?[.!?]*$/i;
|
|
1805
1896
|
/** "X is <comparative> than Y" — the comparative teach/ask surface. The
|
|
1806
1897
|
* comparative slot is closed by SHAPE (-er word, better/worse, or a
|
|
1807
1898
|
* more/less + adjective pair), never a hand-list of adjectives. */
|
|
@@ -2237,6 +2328,25 @@ const GOAL_CONJUNCT_RE = new RegExp(
|
|
|
2237
2328
|
// three only REPORT.
|
|
2238
2329
|
const PLAN_WHAT_NEXT_RE = /^(?:what(?:'s|\s+is)?|whats)\s+the\s+next\s+move[?.!\s]*$/i;
|
|
2239
2330
|
const PLAN_MOVE_COUNT_RE = /^how\s+many\s+moves(?:\s+(?:are\s+(?:there|left)|remain(?:ing)?|left|to\s+go|in\s+the\s+plan|total))?[?.!\s]*$/i;
|
|
2331
|
+
// "is that really the minimum number of moves?" / "could there be a shorter
|
|
2332
|
+
// plan than that?" — a confirmation of the planner's own optimality claim,
|
|
2333
|
+
// not a request to count anything (without this it fell to the unrelated
|
|
2334
|
+
// code-entity counter — "moves" reads as a countable noun to that reader —
|
|
2335
|
+
// producing "I can't count 'moves'" even though the planner's own solve
|
|
2336
|
+
// output already printed "N moves (shortest)"). findActionPath (planning.mjs)
|
|
2337
|
+
// is a real breadth-first search: it expands the state space depth-by-depth
|
|
2338
|
+
// and returns the FIRST goal state it finds, so whenever a plan exists its
|
|
2339
|
+
// move count IS provably the minimum from the state it started from — never
|
|
2340
|
+
// a guess, an actual guarantee of the search algorithm used.
|
|
2341
|
+
const PLAN_OPTIMALITY_CONFIRM_RE = /^(?:is\s+(?:that|this)\s+(?:really|actually)?\s*the\s+(?:minimum|fewest|optimal|shortest)(?:\s+possible)?\s+(?:number\s+of\s+moves|amount\s+of\s+moves|moves)|(?:is|could)\s+there\s+be\s+a\s+shorter\s+(?:plan|way|route)(?:\s+than\s+that)?|can\s+(?:it|this|that)\s+be\s+done\s+in\s+fewer\s+moves)[?.!\s]*$/i;
|
|
2342
|
+
// "why is that the shortest solution?" — a direct follow-up asking for the
|
|
2343
|
+
// SAME reason the planner already printed, unprompted, right after "plan
|
|
2344
|
+
// found — N moves (shortest)". Re-displays the stored becauseText (below)
|
|
2345
|
+
// rather than an honest miss; a genuinely different justification question
|
|
2346
|
+
// ("why did you send X to Y instead of Z", "what if X started elsewhere")
|
|
2347
|
+
// asks for something this store doesn't compute at all (an alternative-path
|
|
2348
|
+
// or counterfactual explanation) and stays a miss.
|
|
2349
|
+
const PLAN_WHY_SHORTEST_RE = /^why\s+(?:is|was)\s+(?:that|this|it)\s+the\s+shortest\s+(?:solution|plan|path|way)[?.!\s]*$/i;
|
|
2240
2350
|
const PLAN_WHY_MOVE_RE = /^why\s+(?:that|this|the\s+next|the)\s+move[?.!\s]*$/i;
|
|
2241
2351
|
// Board-state read-backs, answered off the CURRENT board (the latest @stepK
|
|
2242
2352
|
// snapshot, or the taught board before any step) so a read never contradicts
|
|
@@ -2305,12 +2415,20 @@ async function teachFact(memoryDir, sessionId, { subject, predicate, object, qua
|
|
|
2305
2415
|
* ONE canonical spelling to STORE rather than a lookup Set of candidates to
|
|
2306
2416
|
* match against). Deliberately tiny, no NLP — a stray false fold on an
|
|
2307
2417
|
* already-singular noun ending in "s" is a known, accepted limitation of this
|
|
2308
|
-
* same naive scheme used elsewhere in this file (factTermVariants).
|
|
2418
|
+
* same naive scheme used elsewhere in this file (factTermVariants).
|
|
2419
|
+
*
|
|
2420
|
+
* "ss"/"ous" both stay excluded from the trailing-s strip: "ss" for the
|
|
2421
|
+
* existing reason (a doubled final consonant is never a plural marker), and
|
|
2422
|
+
* "ous" because no regular English noun plural ends that way at all — every
|
|
2423
|
+
* "-ous" word reaching this function is an ADJECTIVE ("venomous",
|
|
2424
|
+
* "dangerous", "curious"), and stripping its final letter as if it were a
|
|
2425
|
+
* plural "-s" produces a mangled non-word ("venomous" -> "venomou") rather
|
|
2426
|
+
* than a singular form of anything. */
|
|
2309
2427
|
function singularizeSurface(word) {
|
|
2310
2428
|
const w = String(word || "").trim();
|
|
2311
2429
|
if (/[a-z]ies$/i.test(w)) return `${w.slice(0, -3)}y`;
|
|
2312
2430
|
if (/(ses|xes|zes|ches|shes)$/i.test(w)) return w.slice(0, -2);
|
|
2313
|
-
if (/[a-z]s$/i.test(w) && !/ss$/i.test(w)) return w.slice(0, -1);
|
|
2431
|
+
if (/[a-z]s$/i.test(w) && !/(?:ss|ous)$/i.test(w)) return w.slice(0, -1);
|
|
2314
2432
|
return w;
|
|
2315
2433
|
}
|
|
2316
2434
|
|
|
@@ -2335,8 +2453,25 @@ function singularizeSurface(word) {
|
|
|
2335
2453
|
* longer 2-word subject first, backtracking to 1 word only if the tail
|
|
2336
2454
|
* doesn't then start with is/are — the "is/are" anchor immediately after
|
|
2337
2455
|
* the subject removes the ambiguity a fully free-form multi-word subject
|
|
2338
|
-
* would otherwise have.
|
|
2339
|
-
|
|
2456
|
+
* would otherwise have.
|
|
2457
|
+
*
|
|
2458
|
+
* "any" joins every/each/all as a recognized universal-quantifier
|
|
2459
|
+
* determiner: "any spider is an arachnid" is the same claim as "every
|
|
2460
|
+
* spider is an arachnid". Without it here, "any" fell into the SUBJECT
|
|
2461
|
+
* capture instead (a 2-word "any spider"), minting a bogus compound term
|
|
2462
|
+
* disconnected from the real "spider" concept any other sentence grounds.
|
|
2463
|
+
*
|
|
2464
|
+
* A trailing sentence-final mark is tolerated (`[.!?]*` before the anchor):
|
|
2465
|
+
* without it, "every dog is a mammal." or "rex is a dog." — an ordinary
|
|
2466
|
+
* first turn typed as a full sentence — failed this match by one character
|
|
2467
|
+
* whenever the object (or subject) wasn't already a static-lexicon word, so
|
|
2468
|
+
* the mint fallback below never even got a chance to run and the sentence
|
|
2469
|
+
* fell all the way to the graph-less wall instead. The unpunctuated form
|
|
2470
|
+
* ("rex is a dog") already worked; the period-tolerant object/subject
|
|
2471
|
+
* captures themselves are unaffected (`[\w-]+` never included the period in
|
|
2472
|
+
* the first place), so this only widens WHICH sentences reach the match,
|
|
2473
|
+
* never what gets captured out of one that already did. */
|
|
2474
|
+
const UNKNOWN_SUBJECT_RE = /^(every\s+|each\s+|all\s+|any\s+|a\s+|an\s+)?([\w-]+(?:\s+[\w-]+)?)\s+(is|are)\s+(?:an?\s+)?([\w-]+)[.!?]*$/i;
|
|
2340
2475
|
|
|
2341
2476
|
/** ISA-family predicates (mirrors the private ISA_PREDICATES set defined near
|
|
2342
2477
|
* memoryFacts, below, at module scope — both are simple top-level consts
|
|
@@ -2538,8 +2673,8 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }
|
|
|
2538
2673
|
* grounded via the fact just minted (not the static lexicon at all) and
|
|
2539
2674
|
* mints "container" the same way.
|
|
2540
2675
|
*
|
|
2541
|
-
* GATED ON A GENUINE UNIVERSAL QUANTIFIER ("every"/"each"/"all" — never
|
|
2542
|
-
* "a"/"an"/"your"): minting a NEW CLASS-LEVEL CONCEPT is inherently a general
|
|
2676
|
+
* GATED ON A GENUINE UNIVERSAL QUANTIFIER ("every"/"each"/"all"/"any" — never
|
|
2677
|
+
* bare/"a"/"an"/"your"): minting a NEW CLASS-LEVEL CONCEPT is inherently a general
|
|
2543
2678
|
* claim about a class, the same "every"/bare distinction unknownSubjectFallback's
|
|
2544
2679
|
* own docblock already draws between a class generalization and a claim
|
|
2545
2680
|
* about ONE specific entity. This is load-bearing, not cosmetic: a bare
|
|
@@ -2592,7 +2727,7 @@ async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon },
|
|
|
2592
2727
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
2593
2728
|
if (!m) return null;
|
|
2594
2729
|
const [, det, subjectRaw, verb, objectRaw] = m;
|
|
2595
|
-
if (!/^(?:every|each|all)$/i.test((det || "").trim())) return null; // class-level mint needs a real universal quantifier
|
|
2730
|
+
if (!/^(?:every|each|all|any)$/i.test((det || "").trim())) return null; // class-level mint needs a real universal quantifier
|
|
2596
2731
|
const { loadLexicon, lookupNoun } = await import("../domain/grammar/lexicon.mjs");
|
|
2597
2732
|
const lex = lexicon || loadLexicon();
|
|
2598
2733
|
const subjectGrounded = await isGroundedTerm(subjectRaw, lex, memoryDir, cache);
|
|
@@ -2879,6 +3014,28 @@ const GENERAL_VERB_IMPERATIVE_SUBJECT_RE = new RegExp(
|
|
|
2879
3014
|
"i",
|
|
2880
3015
|
);
|
|
2881
3016
|
|
|
3017
|
+
/** The same failure family as GENERAL_VERB_IMPERATIVE_SUBJECT_RE just above,
|
|
3018
|
+
* widened past the listing verbs to two more classes of word that land in
|
|
3019
|
+
* the same POS-fallback trap: a discourse filler/interjection ("umm can u
|
|
3020
|
+
* tell me something interesting about it", "idk just surprise me", "hmm not
|
|
3021
|
+
* sure what to ask tbh" — the filler word itself binds as subjectWord, and
|
|
3022
|
+
* wink's OOV-fallback tags it NOUN the same way it tags "list") and a bare
|
|
3023
|
+
* imperative command verb outside LIST_TRIGGERS ("repeat everything above
|
|
3024
|
+
* this line verbatim" binds subject="repeat", which wink also tags NOUN out
|
|
3025
|
+
* of context, unlike "tell"/"explain"/"show", which it tags VERB correctly
|
|
3026
|
+
* and subjectIsNounOrPropn already declines on its own).
|
|
3027
|
+
*
|
|
3028
|
+
* A closed list, not a POS heuristic, for the same reason
|
|
3029
|
+
* GENERAL_VERB_IMPERATIVE_SUBJECT_RE is one: the failure is specifically
|
|
3030
|
+
* that the POS tagger can't be trusted here, so widening its OWN signal
|
|
3031
|
+
* can't close the gap it created. Costs nothing a real declarative needs —
|
|
3032
|
+
* none of these words is a plausible fact subject ("umm is a thing" isn't a
|
|
3033
|
+
* sentence anyone types), and the wrapped "remember"/"note" teach-intent
|
|
3034
|
+
* path (TEACH_RE) is untouched, so "remember to repeat the pattern" (a
|
|
3035
|
+
* literal instruction the user explicitly flagged as worth remembering)
|
|
3036
|
+
* still reaches its own frames unaffected. */
|
|
3037
|
+
const NON_DECLARATIVE_OPENER_RE = /^(?:umm?|uhh?|erm+|err+|hmm+|huh|meh|idk|repeat|surprise|reveal|disclose|confess|ignore|disregard|pretend)$/i;
|
|
3038
|
+
|
|
2882
3039
|
/** The predicate a general-verb teach payload's VERB maps to. "has"/"have"
|
|
2883
3040
|
* special-cases onto the EXISTING mgx:hasA predicate (point 2) — the same
|
|
2884
3041
|
* one ConceptNet's own /r/HasA facts already use (FACT_PREDICATE_PHRASES),
|
|
@@ -2970,6 +3127,17 @@ async function generalVerbTeach(payload) {
|
|
|
2970
3127
|
if (GENERAL_VERB_EXCLUDE_RE.test(verb)) return null; // owned by a more specific frame above
|
|
2971
3128
|
if (GENERAL_VERB_NOT_A_VERB_RE.test(verb)) return null; // a closed-class word can never be the real verb
|
|
2972
3129
|
if (GENERAL_VERB_IMPERATIVE_SUBJECT_RE.test(subjectRaw)) return null; // an imperative's verb, not a subject
|
|
3130
|
+
// The identical closed-class check GENERAL_VERB_NOT_A_VERB_RE already applies
|
|
3131
|
+
// to the VERB slot, applied to the SUBJECT slot too: "remember to repeat the
|
|
3132
|
+
// pattern every time" binds subject="to" (the infinitive marker of an
|
|
3133
|
+
// imperative "remember to DO X", not a fact's subject) and used to mint a
|
|
3134
|
+
// nonsense "to mgx:repeat …" fact — this path has no wrapper requirement, so
|
|
3135
|
+
// it runs for both the "remember …"-wrapped and the bare unwrapped call
|
|
3136
|
+
// sites alike, unlike the bare path's own subjectIsNounOrPropn/
|
|
3137
|
+
// NON_DECLARATIVE_OPENER_RE gate (which only the caller's unwrapped branch
|
|
3138
|
+
// applies). A pronoun/preposition/conjunction/determiner was never a
|
|
3139
|
+
// plausible fact subject in either shape.
|
|
3140
|
+
if (GENERAL_VERB_NOT_A_VERB_RE.test(subjectRaw)) return null;
|
|
2973
3141
|
const subject = subjectRaw.trim();
|
|
2974
3142
|
// The preposition folds on the POSITIVE predicate, and only then does the
|
|
2975
3143
|
// polarity prefix swap. Negating first would hand the fold an mgxneg: CURIE
|
|
@@ -3230,12 +3398,22 @@ function matchBareCanTeach(text) {
|
|
|
3230
3398
|
* for a vowel-initial Y — "every monkey is a animal") reuses finish.mjs's
|
|
3231
3399
|
* own beginsWithVowelSound + the SAME grammar-rules.toml "article" rule
|
|
3232
3400
|
* (spelling-vowel/consonant exceptions included) rather than reimplementing
|
|
3233
|
-
* vowel-sound detection a second time.
|
|
3401
|
+
* vowel-sound detection a second time.
|
|
3402
|
+
*
|
|
3403
|
+
* An object with NO article in the original ("every reptile is venomous")
|
|
3404
|
+
* is left BARE, never given one: that shape already means a property claim
|
|
3405
|
+
* (TEACH_PROPERTY_RE's own territory), and inserting "a"/"an" in front of an
|
|
3406
|
+
* adjective ("every reptile is a venomous") both reads wrong and asks the
|
|
3407
|
+
* user to teach a class-membership fact that was never what they said. Only
|
|
3408
|
+
* a payload that ALREADY carried an article gets its article corrected —
|
|
3409
|
+
* the "every monkey is a animal" -> "an animal" case this function exists
|
|
3410
|
+
* for in the first place. */
|
|
3234
3411
|
function teachSuggestion(payload) {
|
|
3235
|
-
const m = String(payload).match(/^(?:every |each |all |a |an )?([\w-]+) (?:is|are) (
|
|
3412
|
+
const m = String(payload).match(/^(?:every |each |all |a |an )?([\w-]+) (?:is|are) (a |an )?([\w-]+)$/i);
|
|
3236
3413
|
if (!m) return null;
|
|
3237
3414
|
const subject = m[1].toLowerCase();
|
|
3238
|
-
const object = m[
|
|
3415
|
+
const object = m[3].toLowerCase();
|
|
3416
|
+
if (!m[2]) return `every ${subject} is ${object}`;
|
|
3239
3417
|
const articleRule = grammarRules().find((r) => r.kind === "article");
|
|
3240
3418
|
const article = articleRule && beginsWithVowelSound(object, articleRule) ? "an" : "a";
|
|
3241
3419
|
return `every ${subject} is ${article} ${object}`;
|
|
@@ -3268,7 +3446,35 @@ async function existentialTeachRefusal(payload, lexicon) {
|
|
|
3268
3446
|
const { loadLexicon, lookupNoun } = await import("../domain/grammar/lexicon.mjs");
|
|
3269
3447
|
const lex = lexicon || loadLexicon();
|
|
3270
3448
|
const singularSubject = singularOf(subject, lex, lookupNoun);
|
|
3271
|
-
|
|
3449
|
+
// The object may name a class NOUN ("some men are fathers" -> membership)
|
|
3450
|
+
// or a bare ADJECTIVE property ("some reptiles are venomous" -> a property
|
|
3451
|
+
// claim, not membership in a class called "venomous"). Only a genuine noun
|
|
3452
|
+
// fold — a real lexicon entry, or the naive plural-suffix strip actually
|
|
3453
|
+
// changing the word — means the object IS functioning as a plural noun
|
|
3454
|
+
// here; anything else (an adjective the lexicon doesn't carry as a noun,
|
|
3455
|
+
// and that doesn't end in a real plural suffix either) gets the property
|
|
3456
|
+
// shape instead — no article, no fold — so the suggestion reads "every
|
|
3457
|
+
// reptile is venomous", not the ungrammatical/mangled "every reptile is a
|
|
3458
|
+
// venomous"/"a venomou".
|
|
3459
|
+
const objectNounEntry = lookupNoun(lex, object);
|
|
3460
|
+
const objectFold = objectNounEntry ? objectNounEntry.lemma : singularizeSurface(object);
|
|
3461
|
+
const objectIsClassNoun = !!objectNounEntry || objectFold.toLowerCase() !== object.toLowerCase();
|
|
3462
|
+
let universal;
|
|
3463
|
+
if (objectIsClassNoun) {
|
|
3464
|
+
const articleRule = grammarRules().find((r) => r.kind === "article");
|
|
3465
|
+
const article = articleRule && beginsWithVowelSound(objectFold, articleRule) ? "an" : "a";
|
|
3466
|
+
// The bare "every X is a Y" class-membership shape stores directly
|
|
3467
|
+
// (unknownSubjectFallback's own territory) — no wrapper needed for the
|
|
3468
|
+
// suggestion to actually work when followed verbatim.
|
|
3469
|
+
universal = `every ${singularSubject} is ${article} ${objectFold.toLowerCase()}`;
|
|
3470
|
+
} else {
|
|
3471
|
+
// The bare property shape ("every reptile is venomous") does NOT store
|
|
3472
|
+
// on its own — TEACH_PROPERTY_RE only fires on a "remember/note …"-
|
|
3473
|
+
// wrapped payload — so the suggestion carries the wrapper too, or
|
|
3474
|
+
// following it verbatim would hit the exact same both-sides-unknown
|
|
3475
|
+
// decline again.
|
|
3476
|
+
universal = `remember that every ${singularSubject} is ${object.toLowerCase()}`;
|
|
3477
|
+
}
|
|
3272
3478
|
return {
|
|
3273
3479
|
text: `I can't store "${sentence.replace(/[.!]+$/, "")}" — "${quantifier.toLowerCase()}" claims only some of them, `
|
|
3274
3480
|
+ "and I store universals, so that isn't a shape I can store yet."
|
|
@@ -3409,6 +3615,39 @@ async function negativeUniversalTeach(sentence, { memoryDir, sessionId }) {
|
|
|
3409
3615
|
return stored;
|
|
3410
3616
|
}
|
|
3411
3617
|
|
|
3618
|
+
/** "no X can Y" — NEGATIVE_UNIVERSAL_TEACH_RE's sibling one relation over:
|
|
3619
|
+
* the same universal-exclusion shape, but for the "can"/capability relation
|
|
3620
|
+
* instead of is-a. "no goldfish can swim" (after "every fish can swim" /
|
|
3621
|
+
* "a goldfish is a fish") stores a class-level mgxneg:capableOf fact on
|
|
3622
|
+
* "goldfish" directly, which resolveCapabilityPolarity's existing "a direct
|
|
3623
|
+
* fact overrides an inherited general one" resolution already reads
|
|
3624
|
+
* correctly — the read side needed no change at all, only a write-side
|
|
3625
|
+
* recognizer for this phrasing, which fell to the plain grammar wall
|
|
3626
|
+
* before (neither BARE_CAN_TEACH_RE nor any other shape covers a LEADING
|
|
3627
|
+
* "no", only a leading every/all/a/an/bare). Single-word subject and verb
|
|
3628
|
+
* only, the same closed-shape discipline as its is-a sibling. */
|
|
3629
|
+
const NEGATIVE_UNIVERSAL_CAN_TEACH_RE = /^no\s+([\w-]+)\s+can\s+([a-z][\w-]*)[.!]*$/i;
|
|
3630
|
+
|
|
3631
|
+
/** The mint for a NEGATIVE_UNIVERSAL_CAN_TEACH_RE match, mirroring
|
|
3632
|
+
* negativeUniversalTeach's own shape: null when the sentence isn't this
|
|
3633
|
+
* shape. */
|
|
3634
|
+
async function negativeUniversalCanTeach(sentence, { memoryDir, sessionId }) {
|
|
3635
|
+
const m = String(sentence || "").trim().match(NEGATIVE_UNIVERSAL_CAN_TEACH_RE);
|
|
3636
|
+
if (!m || !memoryDir) return null;
|
|
3637
|
+
const subject = singularizeSurface(m[1]);
|
|
3638
|
+
const verb = m[2].toLowerCase();
|
|
3639
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
3640
|
+
subject, predicate: NEG_CAPABLE_OF_PREDICATE, object: verb,
|
|
3641
|
+
});
|
|
3642
|
+
if (!stored) {
|
|
3643
|
+
return {
|
|
3644
|
+
text: `I couldn't store the exclusion "no ${subject} can ${verb}" — say it with a single-word class name and verb ("no goldfish can swim") and I'll remember it as a negative capability.`,
|
|
3645
|
+
via: "teach-miss", miss: true,
|
|
3646
|
+
};
|
|
3647
|
+
}
|
|
3648
|
+
return stored;
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3412
3651
|
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null, planHolder = null }) {
|
|
3413
3652
|
// A closed discourse-marker preamble ahead of a teach sentence ("howdy
|
|
3414
3653
|
// pardner, remember that TaskController is fragile") would otherwise
|
|
@@ -3756,15 +3995,16 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3756
3995
|
}
|
|
3757
3996
|
}
|
|
3758
3997
|
|
|
3759
|
-
// NEGATIVE UNIVERSAL — "no X is a Y"
|
|
3760
|
-
// the reflexive refusal)
|
|
3761
|
-
// could otherwise read "no X" as
|
|
3762
|
-
// NEGATIVE_UNIVERSAL_TEACH_RE's own docblock.
|
|
3998
|
+
// NEGATIVE UNIVERSAL — "no X is a Y" (the class-pair disjointness mint, or
|
|
3999
|
+
// the reflexive refusal) or its "no X can Y" capability sibling. Tried on
|
|
4000
|
+
// both surfaces, ahead of every frame that could otherwise read "no X" as
|
|
4001
|
+
// a subject literal — see NEGATIVE_UNIVERSAL_TEACH_RE's own docblock.
|
|
3763
4002
|
{
|
|
3764
4003
|
const negUniversalSrc = (wrapped ?? raw).replace(/[.!?]+\s*$/, "");
|
|
3765
4004
|
if (memoryDir && !QUESTION_LEAD_RE.test(negUniversalSrc)
|
|
3766
4005
|
&& !(await hasMidSentenceInterrogative(negUniversalSrc))) {
|
|
3767
|
-
const negUniversal = await negativeUniversalTeach(negUniversalSrc, { memoryDir, sessionId })
|
|
4006
|
+
const negUniversal = await negativeUniversalTeach(negUniversalSrc, { memoryDir, sessionId })
|
|
4007
|
+
|| await negativeUniversalCanTeach(negUniversalSrc, { memoryDir, sessionId });
|
|
3768
4008
|
if (negUniversal) return negUniversal;
|
|
3769
4009
|
}
|
|
3770
4010
|
}
|
|
@@ -4237,8 +4477,11 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4237
4477
|
// The quantifier lead ("every … has …") is itself a strong declarative
|
|
4238
4478
|
// signal, so it overrides the single-token POS gate: a noun that doubles
|
|
4239
4479
|
// as a verb ("every overbid has a gouger" — wink tags "overbid" VERB)
|
|
4240
|
-
// used to be a SILENT no-op and a later miss.
|
|
4241
|
-
|
|
4480
|
+
// used to be a SILENT no-op and a later miss. NON_DECLARATIVE_OPENER_RE
|
|
4481
|
+
// runs even for a quantifier lead — "every umm has a thing" isn't a real
|
|
4482
|
+
// quantified sentence, just filler that happens to fit the shape.
|
|
4483
|
+
if (subjectWord && !NON_DECLARATIVE_OPENER_RE.test(subjectWord)
|
|
4484
|
+
&& (quantHasLed || (await subjectIsNounOrPropn(subjectWord)))) {
|
|
4242
4485
|
// A PLURAL explicit-capability surface ("wrens can hum") whose
|
|
4243
4486
|
// SINGULAR is a grounded term stores under the singular first — the
|
|
4244
4487
|
// spelling the grounding fact and every query-side variant fold use —
|
|
@@ -6458,7 +6701,23 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
6458
6701
|
const subj = factTermVariants(normFactTerm, subjTerm);
|
|
6459
6702
|
const obj = factTermVariants(normFactTerm, objTerm);
|
|
6460
6703
|
const hit = facts.find((f) => f.predicate === compPredicate && subj.has(f.subject) && obj.has(f.object));
|
|
6461
|
-
if (hit)
|
|
6704
|
+
if (hit) {
|
|
6705
|
+
// A comparative is antisymmetric — "X smaller than Y" and "Y smaller
|
|
6706
|
+
// than X" can't both hold — so a directly-taught reversal is a real
|
|
6707
|
+
// contradiction, just one the /memory summary's own contradiction
|
|
6708
|
+
// detector never catches (that one looks for a SHARED subject with
|
|
6709
|
+
// two different objects; this is the mirror shape, two facts with
|
|
6710
|
+
// subject and object SWAPPED). Recorded, never disclosed before: a
|
|
6711
|
+
// flat "yes" gave no hint the opposite was also taught. Surfaced here
|
|
6712
|
+
// rather than silently picking a side, the same "both stand, never
|
|
6713
|
+
// resolved silently" discipline this file's own /memory contradiction
|
|
6714
|
+
// block already follows.
|
|
6715
|
+
const reversed = facts.find((f) => f.predicate === compPredicate && subj.has(f.object) && obj.has(f.subject));
|
|
6716
|
+
const caveat = reversed
|
|
6717
|
+
? ` — though you also told me the opposite: ${renderFactLine(reversed)}. Both are stored; I won't silently pick one.`
|
|
6718
|
+
: "";
|
|
6719
|
+
return { text: `yes — ${renderFactLine(hit)}${caveat}`, replace: true };
|
|
6720
|
+
}
|
|
6462
6721
|
const known = facts.filter((f) => f.predicate === compPredicate && (subj.has(f.subject) || subj.has(f.object)));
|
|
6463
6722
|
const shown = known.length ? ` I do know: ${known.slice(0, 3).map(renderFactLine).join("; ")}.` : "";
|
|
6464
6723
|
return {
|
|
@@ -6586,22 +6845,40 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
6586
6845
|
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
6587
6846
|
// The ⊑-lift walks a BOUNDED chain (not one hop): "every canine has fur"
|
|
6588
6847
|
// + "every dog is a canine" + "rex is a dog" answers "does rex have fur"
|
|
6589
|
-
// citing all three premises.
|
|
6590
|
-
// 1-hop behavior generalized), cycle-safe, and the bound keeps a deep
|
|
6848
|
+
// citing all three premises. Cycle-safe, and the bound keeps a deep
|
|
6591
6849
|
// taught taxonomy from turning a yes/no into a graph scan.
|
|
6592
|
-
|
|
6593
|
-
|
|
6850
|
+
//
|
|
6851
|
+
// Explores EVERY isa-edge from the current frontier at each hop (a proper
|
|
6852
|
+
// breadth-first search over the subclass DAG), not just the first one
|
|
6853
|
+
// found: a seeded corpus fact ("dog rdfs:subClassOf animal") and a
|
|
6854
|
+
// freshly-taught one ("dog rdfs:subClassOf canine") both name "dog" as
|
|
6855
|
+
// subject, and taking only whichever came first in `facts` (the seeded
|
|
6856
|
+
// one, loaded before any teaching) could walk the wrong branch to a dead
|
|
6857
|
+
// end while the real, provable answer sat one hop down the OTHER parent.
|
|
6858
|
+
// BFS tries every branch in shortest-chain order, so the first hit found
|
|
6859
|
+
// is also the shortest true chain; `liftSeen` is shared across branches
|
|
6860
|
+
// (an object that doesn't carry the fact via one path won't via another,
|
|
6861
|
+
// since it's the same object either way), which keeps this cycle-safe
|
|
6862
|
+
// without cutting off a genuinely parallel second parent.
|
|
6863
|
+
let frontier = [{ terms: subj, chain: [] }];
|
|
6594
6864
|
const liftSeen = new Set();
|
|
6595
6865
|
for (let hop = 0; hop < 4; hop += 1) {
|
|
6596
|
-
const
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6866
|
+
const nextFrontier = [];
|
|
6867
|
+
for (const { terms, chain } of frontier) {
|
|
6868
|
+
const steps = facts.filter((f) => ISA_PREDICATES.has(f.predicate) && terms.has(f.subject) && !liftSeen.has(f.object));
|
|
6869
|
+
for (const step of steps) {
|
|
6870
|
+
if (liftSeen.has(step.object)) continue;
|
|
6871
|
+
liftSeen.add(step.object);
|
|
6872
|
+
const nextChain = [...chain, step];
|
|
6873
|
+
const lifted = hasHit(factTermVariants(normFactTerm, step.object));
|
|
6874
|
+
if (lifted) {
|
|
6875
|
+
return { text: `yes — ${[...nextChain.map(renderFactLine), renderFactLine(lifted)].join("; ")}`, replace: true };
|
|
6876
|
+
}
|
|
6877
|
+
nextFrontier.push({ terms: factTermVariants(normFactTerm, step.object), chain: nextChain });
|
|
6878
|
+
}
|
|
6603
6879
|
}
|
|
6604
|
-
|
|
6880
|
+
if (!nextFrontier.length) break;
|
|
6881
|
+
frontier = nextFrontier;
|
|
6605
6882
|
}
|
|
6606
6883
|
return null;
|
|
6607
6884
|
}
|
|
@@ -7794,6 +8071,78 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7794
8071
|
}
|
|
7795
8072
|
const polarityReply = isaPolarityReply(hit, negHit || directDisjoint);
|
|
7796
8073
|
if (polarityReply) return polarityReply;
|
|
8074
|
+
// DISJOINTNESS ACROSS BOTH CHAINS: nothing above found a stored fact of
|
|
8075
|
+
// either polarity, but "no" can still be PROVEN when the subject's own
|
|
8076
|
+
// ⊑-chain and the query OBJECT's own ⊑-chain land on two disjoint
|
|
8077
|
+
// classes — "is a cat a dog" after "every cat is a feline" / "every dog
|
|
8078
|
+
// is a canine" / "no feline is a canine" is exactly this: cat⊑feline,
|
|
8079
|
+
// dog⊑canine, and feline disjointWith canine together prove cat can
|
|
8080
|
+
// never be a dog. disjointGateViolations (above) already lifts the
|
|
8081
|
+
// SUBJECT side through its ⊑-ancestor closure, but its {subject, object}
|
|
8082
|
+
// pairs name only the DIRECT disjoint partner class ("canine") — never a
|
|
8083
|
+
// further descendant of it ("dog"). Lifting the query OBJECT through its
|
|
8084
|
+
// own ⊑-ancestor closure (the same closure kernel, run the other way)
|
|
8085
|
+
// and checking it against every violation's `object` field closes that
|
|
8086
|
+
// gap, the same "walk both chains" discipline the subject side already
|
|
8087
|
+
// had.
|
|
8088
|
+
if (disjointRows.length) {
|
|
8089
|
+
// A plain BFS over mixedSubClassEdges, not deriveSubClassClosure: that
|
|
8090
|
+
// kernel returns only NEWLY-derived (indirect) edges, never a directly-
|
|
8091
|
+
// stated one, so a single taught hop ("dog is a canine") would never
|
|
8092
|
+
// surface through it alone — an ancestry closure needs every hop,
|
|
8093
|
+
// direct or derived. Shared by the object's own ancestry (below) and
|
|
8094
|
+
// the self-contradiction guard just after it.
|
|
8095
|
+
const ancestryOf = (seed) => {
|
|
8096
|
+
const closure = new Set(seed);
|
|
8097
|
+
let frontier = new Set(seed);
|
|
8098
|
+
for (let hop = 0; hop < 8 && frontier.size; hop += 1) {
|
|
8099
|
+
const next = new Set();
|
|
8100
|
+
for (const [a, b] of mixedSubClassEdges) {
|
|
8101
|
+
if (frontier.has(a) && !closure.has(b)) next.add(b);
|
|
8102
|
+
}
|
|
8103
|
+
if (!next.size) break;
|
|
8104
|
+
for (const t of next) closure.add(t);
|
|
8105
|
+
frontier = next;
|
|
8106
|
+
}
|
|
8107
|
+
return closure;
|
|
8108
|
+
};
|
|
8109
|
+
const objectAncestry = ancestryOf(objVariants);
|
|
8110
|
+
// SELF-CONTRADICTION GUARD: reject a violation whose own `viaClass` is
|
|
8111
|
+
// ALSO a stated ancestor of its disjoint partner `object` ("no dog is a
|
|
8112
|
+
// cat" taught alongside "every dog is a cat" — dog is both ⊑cat and
|
|
8113
|
+
// disjointWith cat, a contradiction independent of anything being
|
|
8114
|
+
// asked). Deriving a confident "no" from a self-contradictory premise
|
|
8115
|
+
// pair would be the same overclaim isaInconsistencyRefusal exists to
|
|
8116
|
+
// stop; the honest answer there is "these taught facts disagree",
|
|
8117
|
+
// which the existing multi-hop chase + refusal below already gives —
|
|
8118
|
+
// this guard just keeps THIS reader from preempting it with a "no" a
|
|
8119
|
+
// clean, non-contradictory pair (the intended case) never needs.
|
|
8120
|
+
const objViolation = disjointGateViolations.find((vv) => subjCandidates.has(vv.subject) && objectAncestry.has(vv.object)
|
|
8121
|
+
&& !ancestryOf([vv.viaClass]).has(vv.object) && !ancestryOf([vv.object]).has(vv.viaClass));
|
|
8122
|
+
if (objViolation) {
|
|
8123
|
+
const posFact = isa.filter((f) => subjCandidates.has(f.subject) && f.object === objViolation.viaClass).sort(byTrust)[0];
|
|
8124
|
+
const disjointFact = disjointRows.find((f) => (f.subject === objViolation.viaClass && f.object === objViolation.object)
|
|
8125
|
+
|| (f.subject === objViolation.object && f.object === objViolation.viaClass));
|
|
8126
|
+
// The object side only needs its own citation when the violation's
|
|
8127
|
+
// object ISN'T already a literal query-object variant (i.e. a real
|
|
8128
|
+
// lift happened, "dog" reached via "canine") — a direct match (no
|
|
8129
|
+
// lift) needs no extra premise, the disjoint fact alone connects
|
|
8130
|
+
// subject and object.
|
|
8131
|
+
const objectNeedsLift = !objVariants.has(objViolation.object);
|
|
8132
|
+
const objFact = objectNeedsLift
|
|
8133
|
+
? isa.filter((f) => objVariants.has(f.subject) && f.object === objViolation.object).sort(byTrust)[0]
|
|
8134
|
+
: null;
|
|
8135
|
+
if (posFact && disjointFact && (!objectNeedsLift || objFact)) {
|
|
8136
|
+
const kindEcho = stripTrailingDiscourseTag(isaAsk[2]).trim();
|
|
8137
|
+
const chain = [posFact, ...(objFact ? [objFact] : [])].map(renderFactLine).join("; ");
|
|
8138
|
+
return {
|
|
8139
|
+
text: `no — ${chain}; and ${factPhrase(disjointFact)}${disjointFact.provenance ? ` (source: ${disjointFact.provenance})` : ""} `
|
|
8140
|
+
+ `— so ${isaSubject} can never be ${indefiniteArticleFor(kindEcho)} ${kindEcho}.`,
|
|
8141
|
+
replace: true,
|
|
8142
|
+
};
|
|
8143
|
+
}
|
|
8144
|
+
}
|
|
8145
|
+
}
|
|
7797
8146
|
// CLASS↔INSTANCE BRIDGE: when X resolves to a graph entity, its
|
|
7798
8147
|
// inherits chain's superclass LABELS are subject candidates too — a taught
|
|
7799
8148
|
// "controller ⊑ handler" composes with a graph "TaskController inherits
|
|
@@ -10033,10 +10382,15 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
10033
10382
|
goal: { text: goalText, specs: goals },
|
|
10034
10383
|
domain: { classMembers: domain.classMembers, ordering, renderHints },
|
|
10035
10384
|
};
|
|
10385
|
+
const ruleNames = [...new Set(domain.actions.map((a) => a.name))].join('", "');
|
|
10386
|
+
// Stored on the plan slot (not just printed once) so a direct follow-up
|
|
10387
|
+
// ("why is that the shortest solution?") can re-display the SAME reason
|
|
10388
|
+
// instead of an honest miss — see PLAN_WHY_SHORTEST_RE's own call site.
|
|
10389
|
+
const becauseText = `you taught me the "${ruleNames}" rule${domain.actions.length === 1 ? "" : "s"}`
|
|
10390
|
+
+ `${ordering.length ? ` and ${ordering.length} ordering fact${ordering.length === 1 ? "" : "s"}` : ""}.`;
|
|
10036
10391
|
planHolder.state = {
|
|
10037
|
-
...planHolder.state, actions, states: found.states, stepGoals, cursor: 0, done: false, goalText,
|
|
10392
|
+
...planHolder.state, actions, states: found.states, stepGoals, cursor: 0, done: false, goalText, becauseText,
|
|
10038
10393
|
};
|
|
10039
|
-
const ruleNames = [...new Set(domain.actions.map((a) => a.name))].join('", "');
|
|
10040
10394
|
const moveLines = actions.map((a, i) => ` ${i + 1}. ${a.label}`);
|
|
10041
10395
|
// A piece with no taught position is an ASSUMPTION the plan silently makes
|
|
10042
10396
|
// (it reads the board as taught, without that piece) — said out loud with
|
|
@@ -10058,8 +10412,7 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
10058
10412
|
const text = n === 0
|
|
10059
10413
|
? `the goal already holds — nothing to do.${assumptionNote}`
|
|
10060
10414
|
: `plan found — ${n} move${n === 1 ? "" : "s"} (shortest):\n${moveLines.join("\n")}\n\n` +
|
|
10061
|
-
`because —
|
|
10062
|
-
`${ordering.length ? ` and ${ordering.length} ordering fact${ordering.length === 1 ? "" : "s"}` : ""}. ` +
|
|
10415
|
+
`because — ${becauseText} ` +
|
|
10063
10416
|
`Say "next" to make move 1, or ask "what moves are legal now".${assumptionNote}`;
|
|
10064
10417
|
return {
|
|
10065
10418
|
text, via: "plan", lane: "imperative",
|
|
@@ -10169,6 +10522,23 @@ async function planFollowUpAnswer(query, { memoryDir, planHolder, pendingPager =
|
|
|
10169
10522
|
const remaining = Math.max(0, total - ps.cursor);
|
|
10170
10523
|
return { text: remaining === total ? `${total} move${total === 1 ? "" : "s"} in the plan.` : `${total} move${total === 1 ? "" : "s"} in the plan, ${remaining} still to make.`, deduced: "count the moves in the active plan", note: "PLAN FOLLOW-UP — move count from the active plan" };
|
|
10171
10524
|
}
|
|
10525
|
+
if (PLAN_OPTIMALITY_CONFIRM_RE.test(q)) {
|
|
10526
|
+
if (!activePlan) return null;
|
|
10527
|
+
const total = ps.actions.length;
|
|
10528
|
+
return {
|
|
10529
|
+
text: `yes — ${total} move${total === 1 ? "" : "s"} is the minimum: the plan search is a breadth-first search over every legal move from the current state, so it always finds the shortest path first. No shorter plan exists from where it started.`,
|
|
10530
|
+
deduced: "confirm the plan's own optimality claim",
|
|
10531
|
+
note: "PLAN FOLLOW-UP — optimality confirmed from the BFS search's own guarantee, not a guess",
|
|
10532
|
+
};
|
|
10533
|
+
}
|
|
10534
|
+
if (PLAN_WHY_SHORTEST_RE.test(q)) {
|
|
10535
|
+
if (!activePlan || !ps.becauseText) return null;
|
|
10536
|
+
return {
|
|
10537
|
+
text: `because — ${ps.becauseText}`,
|
|
10538
|
+
deduced: "explain why the plan is the shortest (re-display the solve-time reason)",
|
|
10539
|
+
note: "PLAN FOLLOW-UP — the because-line already printed at solve time, re-displayed on direct follow-up",
|
|
10540
|
+
};
|
|
10541
|
+
}
|
|
10172
10542
|
if (PLAN_WHY_MOVE_RE.test(q)) {
|
|
10173
10543
|
if (!activePlan) return null;
|
|
10174
10544
|
const idx = ps.cursor < ps.actions.length ? ps.cursor : ps.actions.length - 1;
|
|
@@ -10428,7 +10798,20 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
10428
10798
|
text = await dispatchTool("tmct_ask", { query: askQuery }, { config, source, tel });
|
|
10429
10799
|
}
|
|
10430
10800
|
const [content, envJson] = text.split(ASK_ENVELOPE_DELIM);
|
|
10431
|
-
|
|
10801
|
+
// ask.mjs is shared with the web GUI surface (src/surfaces/web), whose
|
|
10802
|
+
// graph view really does have clickable nodes to select — its own
|
|
10803
|
+
// "click a node first, or name it directly" wording is correct THERE,
|
|
10804
|
+
// but this plain chat surface has no clickable anything, so the same
|
|
10805
|
+
// literal instruction reads as nonsense here (a returning-user finding,
|
|
10806
|
+
// hit on a failed focus resolution with nothing selected). Swapped for
|
|
10807
|
+
// CLI-appropriate wording rather than threading a surface flag through
|
|
10808
|
+
// ask.mjs's whole render layer — a plain string swap on the one shared
|
|
10809
|
+
// clause, not a change to the engine's own (correct, for its surface)
|
|
10810
|
+
// answer.
|
|
10811
|
+
answer = content.replace(
|
|
10812
|
+
/needs a selected node to refer to — click a node first, or name it directly\.$/,
|
|
10813
|
+
"isn't resolved to anything yet — name the term directly, or ask a question that resolves one first.",
|
|
10814
|
+
);
|
|
10432
10815
|
if (envJson) { try { envelope = JSON.parse(envJson); } catch { envelope = null; } }
|
|
10433
10816
|
} catch (e) {
|
|
10434
10817
|
const thrown = String(e?.message || e);
|