@polycode-projects/the-mechanical-code-talker 2.10.5 → 2.11.1
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 +2 -2
- package/corpus/sprites/src/sprite-facts.jsonl +18 -0
- package/corpus/worlds/manifest.json +5 -5
- package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
- package/corpus/worlds/src/ashcombe-hall.jsonl +27 -0
- package/data/sprites/book-icon.toml +12 -0
- package/data/sprites/cellar-icon.toml +12 -0
- package/data/sprites/drawing-room-icon.toml +13 -0
- package/data/sprites/garden-icon.toml +12 -0
- package/data/sprites/kitchen-icon.toml +13 -0
- package/data/sprites/library-icon.toml +12 -0
- package/data/sprites/pan-icon.toml +11 -0
- package/data/sprites/study-icon.toml +12 -0
- package/data/templates/responses.jsonl +1 -0
- package/package.json +5 -2
- package/src/adapters/corpus/wikipedia-live.mjs +182 -26
- package/src/adapters/corpus/worlds-pack.mjs +8 -2
- package/src/adapters/toml-config.mjs +6 -0
- package/src/domain/ask-vocab.mjs +17 -0
- package/src/domain/ask.mjs +51 -1
- package/src/domain/grammar/ace.mjs +43 -3
- package/src/domain/interpret/normalize.mjs +6 -2
- package/src/domain/interpret/strategies/grammar.mjs +47 -17
- package/src/domain/interpret/strategies/keywords.mjs +53 -4
- package/src/domain/memory/trust.mjs +11 -0
- package/src/domain/router/registry.mjs +8 -1
- package/src/domain/worlds-pack.mjs +50 -0
- package/src/services/adventure-autoplay.mjs +5 -2
- package/src/services/adventure-viz.mjs +301 -33
- package/src/services/adventure.mjs +162 -14
- package/src/services/chat-page-viz.mjs +265 -189
- package/src/services/chat-session.mjs +15 -5
- package/src/services/chat.mjs +471 -79
- package/src/services/code-explorer-viz.mjs +183 -75
- package/src/services/extract-facts.mjs +118 -28
- package/src/services/ingest-viz.mjs +328 -79
- package/src/services/ledger-viz.mjs +99 -0
- package/src/services/memory-panel-viz.mjs +159 -0
- package/src/services/research.mjs +266 -0
- package/src/services/sentences.mjs +19 -0
- package/src/services/spider-fly-viz.mjs +21 -5
- package/src/surfaces/web/adventure-browser-entry.mjs +9 -5
- package/src/surfaces/web/chat-browser-entry.mjs +28 -11
- package/src/surfaces/web/code-explorer-browser-entry.mjs +27 -11
- package/src/surfaces/web/ingest-browser-entry.mjs +123 -41
- package/src/surfaces/web/ledger-browser-entry.mjs +10 -4
- package/src/surfaces/web/memory-ask-browser.bundle.js +116 -116
- package/src/surfaces/web/memory-stats.mjs +53 -0
package/src/services/chat.mjs
CHANGED
|
@@ -51,13 +51,14 @@ import {
|
|
|
51
51
|
LIVE_PACK_NAME, cleanMissLiveTerm, renderLiveReferenceAnswer, liveProvenanceTag,
|
|
52
52
|
} from "../domain/reference-pack.mjs";
|
|
53
53
|
import { getReferencePackProvider } from "../adapters/corpus/reference-pack.mjs";
|
|
54
|
-
import { getLiveReferenceProvider } from "../adapters/corpus/wikipedia-live.mjs";
|
|
54
|
+
import { getLiveReferenceProvider, getResearchProvider } from "../adapters/corpus/wikipedia-live.mjs";
|
|
55
|
+
import { researchTurn, researchSnapshot, resolveResearchConfig, RESEARCH_DEFAULTS } from "./research.mjs";
|
|
55
56
|
import { CHILD_PACK_NAME, childProvenanceTag } from "../domain/child-pack.mjs";
|
|
56
57
|
import { getChildPackProvider } from "../adapters/corpus/child-pack.mjs";
|
|
57
58
|
import { dialogueActForLane } from "../domain/dialogue-acts.mjs";
|
|
58
59
|
import { subClassParents, ancestryChain, clusterSenses } from "../domain/sense-split.mjs";
|
|
59
60
|
import { relatedForTerm } from "../domain/skos-view.mjs";
|
|
60
|
-
import { adventureTurn, unclaimedAdventureOpening } from "./adventure.mjs";
|
|
61
|
+
import { adventureTurn, unclaimedAdventureOpening, foldWorldState } from "./adventure.mjs";
|
|
61
62
|
import { spiderFlyTurn } from "./spider-fly-turn.mjs";
|
|
62
63
|
import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
|
|
63
64
|
|
|
@@ -1081,6 +1082,11 @@ const CAPABILITY_PHRASES = [
|
|
|
1081
1082
|
// — closed to the same self-referential noun set META_ORIENT_RE uses, so a
|
|
1082
1083
|
// real module/class named literally "repo"/"codebase" is never at risk.
|
|
1083
1084
|
/^what(?:'s|s|\s+is) in (?:this|the)\s+(?:app|codebase|repo|repository|project|code)\??$/i,
|
|
1085
|
+
// "can I ask you something random" — an ordinary conversational preamble
|
|
1086
|
+
// asking permission before a real question, not itself a question about
|
|
1087
|
+
// anything nameable. Answered the same as any other vague opener: sure,
|
|
1088
|
+
// here's what I can help with.
|
|
1089
|
+
/^can i ask (?:you\s+)?something(?:\s+random)?\??$/i,
|
|
1084
1090
|
];
|
|
1085
1091
|
/** IDENTITY questions — "who/what are you", by name, in plain or ESL-ish phrasing.
|
|
1086
1092
|
* Routed to a self-description (identity-self) that works regardless of graph
|
|
@@ -1131,6 +1137,25 @@ const AI_IDENTITY_PHRASES = [
|
|
|
1131
1137
|
// product path (no-LLM constitution, deterministic offline reasoning), so
|
|
1132
1138
|
// this is a real "no", not the generic capability listing.
|
|
1133
1139
|
/^can (?:you|u) (?:browse|access|use|go on|connect to) the internet\??$/i,
|
|
1140
|
+
// "can you look things up on the internet" / "browse the web to check
|
|
1141
|
+
// something" — the same offline-capability question as the entry just
|
|
1142
|
+
// above, worded around "web"/"look up" instead of "browse the internet".
|
|
1143
|
+
// With no closed match, this fell into a code-graph module-name search for
|
|
1144
|
+
// the literal words instead of the plain "no, I'm offline" answer.
|
|
1145
|
+
/^can (?:you|u) look (?:things?\s+)?up on the internet\??$/i,
|
|
1146
|
+
/^can (?:you|u) (?:browse|search|check) the web(?:\s+to\s+check\s+(?:something|this|that))?\??$/i,
|
|
1147
|
+
// "are you like chatgpt or gemini or something" — the comparison phrasing
|
|
1148
|
+
// of the "are you an AI" question, a named model or two followed by a
|
|
1149
|
+
// trailing "or something" hedge.
|
|
1150
|
+
/^(?:are you|r u)\s+like\s+(?:chatgpt|gpt|claude|gemini|llama)(?:\s+or\s+(?:chatgpt|gpt|claude|gemini|llama))*\s+or\s+something\??$/i,
|
|
1151
|
+
// "what model are you, gpt-4 or claude or something else" — the SAME
|
|
1152
|
+
// open-pick question "what model are you built on" already answers, just
|
|
1153
|
+
// without the "built/based/running on" bridge phrase.
|
|
1154
|
+
/^what model are you,?\s*(?:gpt-?\d(?:\.\d)?|chatgpt|claude|gemini|llama)(?:\s+or\s+(?:gpt-?\d(?:\.\d)?|chatgpt|claude|gemini|llama|something\s+else))*\??$/i,
|
|
1155
|
+
// "can you use an LLM to answer this" — asks the identical no-LLM question
|
|
1156
|
+
// as "do you use ai", just with "use an LLM to answer this" as the verb
|
|
1157
|
+
// phrase instead of a bare "use ai".
|
|
1158
|
+
/^can (?:you|u) use (?:an? )?(?:llm|ai|gpt|chatgpt) to answer (?:this|that|it)\??$/i,
|
|
1134
1159
|
];
|
|
1135
1160
|
|
|
1136
1161
|
/** META-COMMAND/SESSION questions — a RETURNING USER checking whether a
|
|
@@ -1218,10 +1243,25 @@ const FEELINGS_PHRASES = [
|
|
|
1218
1243
|
// "keeps growing as new natural phrasings surface" precedent.
|
|
1219
1244
|
/^how (?:are|r) (?:you|u) doing(?:\s+today)?\??$/i,
|
|
1220
1245
|
/^what(?:'s|s|\s+is) your (?:favou?rite\s+(?:colou?r|food|movie|book|band|song|number)|name)\??$/i,
|
|
1221
|
-
/^do you (?:get|ever get) bored\??$/i,
|
|
1246
|
+
/^do you (?:get|ever get) (?:bored|tired)\??$/i,
|
|
1222
1247
|
/^what do you do for fun\??$/i,
|
|
1223
1248
|
/^can you (?:tell|make)\s+(?:me\s+)?(?:a\s+)?jokes?\??$/i,
|
|
1224
1249
|
/^do you (?:know|know anything|know much)\s+about\s+(?:movies?|sports?|music|tv|television)(?:\s+or\s+(?:movies?|sports?|music|tv|television))?\??$/i,
|
|
1250
|
+
// "do you think dogs are smarter than cats" — a personal-opinion comparison,
|
|
1251
|
+
// same family as the direct personal questions above: closed to a small
|
|
1252
|
+
// comparative-adjective vocabulary, open on the two compared things (the
|
|
1253
|
+
// SAME discipline PHRASING_FRAMES' object captures use elsewhere).
|
|
1254
|
+
/^do you think .+?\s+(?:is|are)\s+(?:smarter|dumber|better|worse|nicer|cooler|cuter|friendlier|stronger|faster)\s+than\s+.+\??$/i,
|
|
1255
|
+
];
|
|
1256
|
+
/** "can you make up an answer if you don't actually know" — a direct probe of
|
|
1257
|
+
* the product's own headline promise (the honest miss: a query it can't
|
|
1258
|
+
* ground gets a refusal, never a guess). With no closed match this fell to
|
|
1259
|
+
* the plain grammar wall instead of confirming the very thing it asked
|
|
1260
|
+
* about. Same family/placement as FEELINGS_PHRASES just above. */
|
|
1261
|
+
const HONEST_MISS_PHRASES = [
|
|
1262
|
+
/^can (?:you|u) make up an answer if you (?:don'?t|do\s+not) (?:actually\s+|really\s+)?know\??$/i,
|
|
1263
|
+
/^(?:do|would) you (?:ever\s+)?(?:make (?:something|stuff) up|make up an answer)(?:\s+if you (?:don'?t|do\s+not) know)?\??$/i,
|
|
1264
|
+
/^what if you (?:don'?t|do\s+not) know (?:the\s+)?answer\??$/i,
|
|
1225
1265
|
];
|
|
1226
1266
|
/** "whats 2+2" — a bare arithmetic expression, not a code/vocabulary question
|
|
1227
1267
|
* at all. With no closed-set match of its own, this fell into the SAME
|
|
@@ -1234,6 +1274,15 @@ const FEELINGS_PHRASES = [
|
|
|
1234
1274
|
* structural answer, never this decline. "+"/"*"/"/" have no such
|
|
1235
1275
|
* collision in tmct's own vocabulary. */
|
|
1236
1276
|
const ARITHMETIC_RE = /\d+\s*[+*/]\s*\d+/;
|
|
1277
|
+
/** A bare SQL data-definition/manipulation statement typed at the prompt
|
|
1278
|
+
* ("DROP TABLE users;") — nonsense as a code-graph question, and with no
|
|
1279
|
+
* closed match of its own this fell to the ≤3-word catch-all's identity/
|
|
1280
|
+
* orientation blurb, the same wrong-flavor miss ARITHMETIC_RE exists to
|
|
1281
|
+
* avoid for arithmetic. Each alternative requires the FULL SQL clause shape
|
|
1282
|
+
* (not just the bare leading verb), so an ordinary English imperative
|
|
1283
|
+
* ("update the readme") is never caught here — "table"/"from"/"into"/"set"
|
|
1284
|
+
* are the words that make this unambiguously SQL rather than English. */
|
|
1285
|
+
const SQL_STATEMENT_RE = /^(?:(?:drop|truncate|alter)\s+table|delete\s+from|insert\s+into|update\s+[a-z0-9_.]+\s+set)\s+[a-z0-9_.]+\b/i;
|
|
1237
1286
|
/** The structural verbs/nouns that mark a near-miss code question (→ keep the
|
|
1238
1287
|
* precise grammar hint, not the friendly nudge). */
|
|
1239
1288
|
const STRUCT_WORDS = new Set([
|
|
@@ -1427,6 +1476,10 @@ const T_ORIENTATION_EMPTY = "orientation-empty";
|
|
|
1427
1476
|
const T_IDENTITY_SELF = "identity-self";
|
|
1428
1477
|
const T_IDENTITY_NOT_LLM = "identity-not-an-llm";
|
|
1429
1478
|
const T_IDENTITY_NO_FEELINGS = "identity-no-feelings";
|
|
1479
|
+
/** Confirms the honest-miss promise itself when a user asks about it directly
|
|
1480
|
+
* ("can you make up an answer if you don't know"). Same "works regardless of
|
|
1481
|
+
* graph state" family as the identity answers just above. */
|
|
1482
|
+
const T_IDENTITY_HONEST_MISS = "identity-honest-miss";
|
|
1430
1483
|
/** THE CONCEPT FORCE (concept.mjs): the three-band answer to a vague "what is a X"
|
|
1431
1484
|
* that names a known concept WITH instances — {definition}/{examples}/{followups}. */
|
|
1432
1485
|
const T_CONCEPT = "concept-force";
|
|
@@ -1496,6 +1549,13 @@ const BYE = new Set([
|
|
|
1496
1549
|
"bye", "goodbye", "quit", "exit", "see ya", "see you", "cya", "later", "farewell",
|
|
1497
1550
|
"peace", "peace out", "im off", "i'm off", "gtg", "gotta go", "catch you later",
|
|
1498
1551
|
"farewell then",
|
|
1552
|
+
// "gtg thx" — the SAME "gtg" farewell above, immediately followed by a
|
|
1553
|
+
// thanks word with no delimiter between them (so farewellOrThanksSignal's
|
|
1554
|
+
// comma/semicolon clause split never sees two clauses to work with).
|
|
1555
|
+
// Whole-phrase entry rather than a general "bye word + thanks word, no
|
|
1556
|
+
// delimiter" mechanism — closed and hand-curated, this exact reported
|
|
1557
|
+
// phrasing only.
|
|
1558
|
+
"gtg thx",
|
|
1499
1559
|
// "good day to you" deliberately does NOT live here: it's a formal-register
|
|
1500
1560
|
// GREETING, not a farewell. foldedBye is checked before GREET in
|
|
1501
1561
|
// conversationalTurn, so having it here would silently end the session on
|
|
@@ -1513,6 +1573,11 @@ const WHY = new Set([
|
|
|
1513
1573
|
const OK_ACK = new Set([
|
|
1514
1574
|
"ok", "okay", "cool", "aight", "fair enough", "got it", "gotcha", "noted",
|
|
1515
1575
|
"sounds good", "sure", "cool cool", "right",
|
|
1576
|
+
// "haha ok fair enough" — a laughter beat leading a two-word ack ("fair
|
|
1577
|
+
// enough") that dismissalSignal's own peeling can't reach: it peels
|
|
1578
|
+
// single-WORD fluff off each end, but "fair enough" is itself two words.
|
|
1579
|
+
// Whole-phrase entry, closed and hand-curated, this exact reported phrasing.
|
|
1580
|
+
"haha ok fair enough",
|
|
1516
1581
|
]);
|
|
1517
1582
|
/** Dismissals — "drop it, no question here" beats. Routed to a warm dismissal
|
|
1518
1583
|
* template, never the identity/orientation blurb (which reads like the tool
|
|
@@ -1619,6 +1684,11 @@ const CLOSING_FILLER_CLAUSES = new Set([
|
|
|
1619
1684
|
"that's everything for now", "that's all for now",
|
|
1620
1685
|
"that's everything i needed", "that's all i needed",
|
|
1621
1686
|
"that's everything for today", "that's all for today",
|
|
1687
|
+
// "thats enough for now" — the same closing-filler shape, worded around
|
|
1688
|
+
// "enough" instead of "everything"/"all", and without the apostrophe a
|
|
1689
|
+
// casual typer routinely drops (conversationalTurn's own `q` never runs an
|
|
1690
|
+
// apostrophe/contraction pass, so both spellings are curated explicitly).
|
|
1691
|
+
"that's enough for now", "thats enough for now",
|
|
1622
1692
|
]);
|
|
1623
1693
|
/** Strip a hedging lead ("i think that's everything for today" → "that's
|
|
1624
1694
|
* everything for today") so a hedged closing clause still matches the closed
|
|
@@ -1875,6 +1945,20 @@ function conversationalTurn(line, ctx) {
|
|
|
1875
1945
|
note(ctx.trace, "lane: conversational — identity/feelings (FEELINGS_PHRASES closed set)");
|
|
1876
1946
|
return mk(t(T_IDENTITY_NO_FEELINGS), { lane: "help" });
|
|
1877
1947
|
}
|
|
1948
|
+
if (HONEST_MISS_PHRASES.some((re) => re.test(raw))) {
|
|
1949
|
+
note(ctx.trace, "goal: identity — a direct probe of the honest-miss promise itself");
|
|
1950
|
+
note(ctx.trace, "lane: conversational — identity/honest-miss (HONEST_MISS_PHRASES closed set)");
|
|
1951
|
+
return mk(t(T_IDENTITY_HONEST_MISS), { lane: "help" });
|
|
1952
|
+
}
|
|
1953
|
+
if (SQL_STATEMENT_RE.test(raw)) {
|
|
1954
|
+
note(ctx.trace, "goal: nonsense input shaped like a SQL statement — a targeted decline, not the identity blurb");
|
|
1955
|
+
note(ctx.trace, "lane: conversational — SQL-statement decline (SQL_STATEMENT_RE)");
|
|
1956
|
+
return mk(
|
|
1957
|
+
"That reads like a SQL statement, not a question about a code graph or taught facts. "
|
|
1958
|
+
+ "Try \"what is a dog\" for vocabulary, or point me at a repo with --repo <path>.",
|
|
1959
|
+
{ lane: "help" },
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1878
1962
|
if (ARITHMETIC_RE.test(raw)) {
|
|
1879
1963
|
note(ctx.trace, "goal: arithmetic — not a code/vocabulary question, an honest decline");
|
|
1880
1964
|
note(ctx.trace, "lane: conversational — arithmetic decline (ARITHMETIC_RE)");
|
|
@@ -2112,7 +2196,7 @@ const WALL_MISS_ANYWHERE_RE = /couldn't parse this as a graph question\. Try:/;
|
|
|
2112
2196
|
// bare "X is a Y" declarative the graph parser couldn't handle → route to the
|
|
2113
2197
|
// assert/memory path; when it can't be stored, say what CAN be remembered
|
|
2114
2198
|
// instead of the grammar wall or a silent data loss.
|
|
2115
|
-
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;
|
|
2199
|
+
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|teach(?:\s+me)?)\b(?:\s+(?:this|that|also))?[:,]?\s*(?:that\s+)?(.+?)[.?!]*$/i;
|
|
2116
2200
|
// A trailing sentence-final mark ([.!?]*) is tolerated at the very end: an
|
|
2117
2201
|
// ordinary first turn typed as a full sentence ("every dog is a mammal.")
|
|
2118
2202
|
// otherwise failed this shape test by one character whenever neither ACE nor
|
|
@@ -2861,8 +2945,20 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }
|
|
|
2861
2945
|
const [, det, subjectRaw, verb, objectRaw] = m;
|
|
2862
2946
|
const { loadLexicon, lookupNoun, lookupAdjective, classify } = await import("../domain/grammar/lexicon.mjs");
|
|
2863
2947
|
const lex = lexicon || loadLexicon();
|
|
2864
|
-
// A known X's own ACE miss is a real miss — never silently reinterpreted
|
|
2865
|
-
|
|
2948
|
+
// A known X's own ACE miss is a real miss — never silently reinterpreted
|
|
2949
|
+
// here. EXCEPT: classify() folds a trailing "-s" the same way resolveNP
|
|
2950
|
+
// does (lexicon.mjs's lookupNoun), so a bare proper name that happens to
|
|
2951
|
+
// end in "s" and collide with an unrelated dictionary noun ("whiskers" ->
|
|
2952
|
+
// "whisker") reads as "already known" under a SINGULAR "is" sentence, where
|
|
2953
|
+
// no plural evidence supports the fold at all (the genuinely-plural "are"
|
|
2954
|
+
// case is handled correctly a few lines down). Refuse only the fold's own
|
|
2955
|
+
// contribution here — an EXACT noun hit (no fold), or any non-noun
|
|
2956
|
+
// classification (a real verb/adjective/proper name/determiner), still
|
|
2957
|
+
// blocks this fallback exactly as before.
|
|
2958
|
+
const subjectClass = classify(subjectRaw, lex);
|
|
2959
|
+
const subjectFoldedNounOnly = subjectClass?.pos === "noun" && !/^are$/i.test(verb)
|
|
2960
|
+
&& (lookupNoun(lex, subjectRaw)?.lemma || "").toLowerCase() !== String(subjectRaw).toLowerCase();
|
|
2961
|
+
if (subjectClass && !subjectFoldedNounOnly) return null;
|
|
2866
2962
|
const quantifier = /^every$/i.test((det || "").trim()) ? "every" : "";
|
|
2867
2963
|
// Singularize the SUBJECT before storage, but ONLY on a genuinely PLURAL
|
|
2868
2964
|
// phrasing ("all men ARE mortal", verb "are"). This
|
|
@@ -3138,7 +3234,8 @@ async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon
|
|
|
3138
3234
|
// sentence said ("tony never eats ribs" -> tony eats ribs) — a truthful teach
|
|
3139
3235
|
// read back as a confident lie. It belongs to NEG_MARKER_SRC below.
|
|
3140
3236
|
const TEACH_ADVERB_SKIP_SRC = "(?:(?:usually|often|sometimes|rarely|always|typically|generally|"
|
|
3141
|
-
+ "occasionally|frequently|normally|regularly|commonly|mostly|currently|still|also|really|actually
|
|
3237
|
+
+ "occasionally|frequently|normally|regularly|commonly|mostly|currently|still|also|really|actually|"
|
|
3238
|
+
+ "closely|strongly|directly)\\s+)?";
|
|
3142
3239
|
/** The negation markers a teach/query frame recognizes, in ONE place so the
|
|
3143
3240
|
* teach side and the query side can never disagree about what negates a
|
|
3144
3241
|
* sentence — the same discipline TEACH_ADVERB_SKIP_SRC is shared under. */
|
|
@@ -3163,6 +3260,110 @@ function splitTeachNegation(payload) {
|
|
|
3163
3260
|
return { payload: `${m[1]} ${canFamily ? "can " : ""}${m[3]}`.trim(), negated: true };
|
|
3164
3261
|
}
|
|
3165
3262
|
const GENERAL_VERB_TEACH_RE = new RegExp(`^([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+)\\s+(.+?)[.!?]*$`, "i");
|
|
3263
|
+
/** The closed participle set the relational teach frames read as "X is
|
|
3264
|
+
* <participle> <prep> Y" — a past participle whose own form is the word, so it
|
|
3265
|
+
* reads back with no morphology. Closed by list (templates over general
|
|
3266
|
+
* grammar), so the frame can never widen onto an arbitrary "-ed" adjective. */
|
|
3267
|
+
const TEACH_PARTICIPLE_SRC = "connected|related|associated|linked|based|derived|composed|made|used|known|located|found|involved|concerned";
|
|
3268
|
+
/** The prepositions those participles take. A closed set, folded into the
|
|
3269
|
+
* minted predicate (mgx:<participle>-<prep>) the same way PREP_SRC folds into
|
|
3270
|
+
* the general-verb frame's. */
|
|
3271
|
+
const TEACH_PARTICIPLE_PREP_SRC = "with|to|from|by|of|in|on|for|as|about|into";
|
|
3272
|
+
/** "X is <participle> <prep> Y" — "sales are closely connected with marketing"
|
|
3273
|
+
* → sales mgx:connected-with marketing. The subject is one or two tokens (the
|
|
3274
|
+
* same bound the comparative/unknown-subject frames use), an optional adverb
|
|
3275
|
+
* is skipped, and the object is captured for a determiner-strip + 3-token cap
|
|
3276
|
+
* by its handler. */
|
|
3277
|
+
const PARTICIPLE_PREP_TEACH_RE = new RegExp(
|
|
3278
|
+
`^(?:the\\s+|an?\\s+)?([\\w'-]+(?:\\s+[\\w'-]+)?)\\s+(?:is|are|was|were)\\s+${TEACH_ADVERB_SKIP_SRC}(${TEACH_PARTICIPLE_SRC})\\s+(${TEACH_PARTICIPLE_PREP_SRC})\\s+(.+)$`,
|
|
3279
|
+
"i",
|
|
3280
|
+
);
|
|
3281
|
+
/** "X is a <noun> <participle> <prep> Y" — a copula-NP with a trailing
|
|
3282
|
+
* participle clause: "sales are activities related to selling" decomposes into
|
|
3283
|
+
* the class-membership half (sales ⊑ activity, through the ordinary mint/assert
|
|
3284
|
+
* path) AND the relational half (sales mgx:related-to selling). The NP head is
|
|
3285
|
+
* a single token, followed by a closed participle — which keeps this disjoint
|
|
3286
|
+
* from PARTICIPLE_PREP_TEACH_RE, where the participle sits right after the
|
|
3287
|
+
* copula. */
|
|
3288
|
+
const COPULA_NP_PARTICIPLE_TEACH_RE = new RegExp(
|
|
3289
|
+
`^(?:the\\s+|an?\\s+)?([\\w'-]+(?:\\s+[\\w'-]+)?)\\s+(is|are|was|were)\\s+(?:an?\\s+)?([\\w'-]+)\\s+(${TEACH_PARTICIPLE_SRC})\\s+(${TEACH_PARTICIPLE_PREP_SRC})\\s+(.+)$`,
|
|
3290
|
+
"i",
|
|
3291
|
+
);
|
|
3292
|
+
/** "A and B have/share the same <noun>" — "sales and marketing have the same
|
|
3293
|
+
* goal" → sales mgx:same-goal-as marketing. A closed shape; the conjunction
|
|
3294
|
+
* pre-pass leaves it alone (its second clause never opens with is/are/has/
|
|
3295
|
+
* have/can), so it reaches the teach dispatch whole. */
|
|
3296
|
+
const SAME_NOUN_TEACH_RE = /^(?:the\s+)?([\w'-]+)\s+and\s+(?:the\s+)?([\w'-]+)\s+(?:have|has|share|shares)\s+(?:the\s+)?same\s+([\w'-]+)[.!?]*$/i;
|
|
3297
|
+
/** "the letter is in the garden" — a locative teach whose subject the running
|
|
3298
|
+
* adventure world already places somewhere. Group 1 is the subject; the world
|
|
3299
|
+
* place is left to the fold, since the sentence is stored as a note either
|
|
3300
|
+
* way. */
|
|
3301
|
+
const LOCATIVE_TEACH_RE = /^(?:the\s+)?([\w'-]+)\s+(?:is|are|was|were)\s+(?:in|on|under|inside|at|near|behind|above|below)\s+(?:the\s+)?[\w'-]+/i;
|
|
3302
|
+
/** Fold a relational object down to its head phrase: cut at the first clause
|
|
3303
|
+
* boundary (a comma, semicolon, or a coordinating "or"/"and"), strip a leading
|
|
3304
|
+
* determiner, then cap at 3 tokens — "selling or the number of goods sold in a
|
|
3305
|
+
* period" folds to "selling", "the number of goods" to "number of goods".
|
|
3306
|
+
* Keeps a minted relational object bounded, the same discipline the general-
|
|
3307
|
+
* verb frame's own object fold uses. */
|
|
3308
|
+
function participleObject(raw) {
|
|
3309
|
+
const cleaned = String(raw).trim()
|
|
3310
|
+
.replace(/[.!?]+$/, "")
|
|
3311
|
+
.split(/\s*[,;]\s*|\s+(?:or|and)\s+/i)[0]
|
|
3312
|
+
.trim()
|
|
3313
|
+
.replace(/^(?:the|an?|its|his|her|their|our|my|your|some|any)\s+/i, "");
|
|
3314
|
+
return cleaned.split(/\s+/).slice(0, 3).join(" ");
|
|
3315
|
+
}
|
|
3316
|
+
/** Does a bare (unwrapped) sentence fit one of the relational teach frames —
|
|
3317
|
+
* including its negated twin, read through splitTeachNegation the same way the
|
|
3318
|
+
* general-verb and capability frames read theirs? Used only to admit the
|
|
3319
|
+
* sentence as a teach payload; the dispatch below re-matches and stores.
|
|
3320
|
+
*
|
|
3321
|
+
* Each regex's own subject capture is a bare word/short NP with no pronoun
|
|
3322
|
+
* exclusion of its own (unlike generalVerbTeach's GENERAL_VERB_NOT_A_VERB_RE
|
|
3323
|
+
* check) — this frame used to be reached only once the bare-sentence pronoun
|
|
3324
|
+
* guard (TEACH_PRONOUN_BARE_RE) had already declined, but that guard is
|
|
3325
|
+
* deliberately narrow (copula + a SHORT complement only), so a pronoun
|
|
3326
|
+
* subject with a longer relational complement ("it is closely connected
|
|
3327
|
+
* with hunting" — the ingest pronoun-carry mechanism's own first, expected-
|
|
3328
|
+
* to-fail attempt) reaches here unprotected. isTeachPronoun is the same
|
|
3329
|
+
* closed check every other mint fallback in this lane uses. */
|
|
3330
|
+
function matchesRelationalTeachFrame(sentence) {
|
|
3331
|
+
const { payload } = splitTeachNegation(String(sentence || "").trim());
|
|
3332
|
+
const pp = payload.match(PARTICIPLE_PREP_TEACH_RE);
|
|
3333
|
+
if (pp) return !isTeachPronoun(pp[1]);
|
|
3334
|
+
const np = payload.match(COPULA_NP_PARTICIPLE_TEACH_RE);
|
|
3335
|
+
if (np) return !isTeachPronoun(np[1]);
|
|
3336
|
+
const same = payload.match(SAME_NOUN_TEACH_RE);
|
|
3337
|
+
if (same) return !isTeachPronoun(same[1]) && !isTeachPronoun(same[2]);
|
|
3338
|
+
return false;
|
|
3339
|
+
}
|
|
3340
|
+
/** Does a relational-frame sentence name a code-graph entity? "the Router is
|
|
3341
|
+
* used by every handler" reads as a passive uses-CLAIM the ask engine verifies
|
|
3342
|
+
* against the graph ("No — no uses edge found…"), not a fact to store — the
|
|
3343
|
+
* participle+preposition frame would otherwise intercept it. When any term the
|
|
3344
|
+
* frame would store resolves to a graph entity, the frame yields so the graph
|
|
3345
|
+
* lane keeps the sentence. No graph (a bare/browser/ingest turn) means nothing
|
|
3346
|
+
* to yield to, so the frame proceeds. */
|
|
3347
|
+
async function relationalFrameNamesGraphEntity(sentence, graph) {
|
|
3348
|
+
if (!graph) return false;
|
|
3349
|
+
const { payload } = splitTeachNegation(String(sentence || "").trim());
|
|
3350
|
+
let terms = null;
|
|
3351
|
+
const pp = payload.match(PARTICIPLE_PREP_TEACH_RE);
|
|
3352
|
+
if (pp) terms = [pp[1], participleObject(pp[4])];
|
|
3353
|
+
else {
|
|
3354
|
+
const np = payload.match(COPULA_NP_PARTICIPLE_TEACH_RE);
|
|
3355
|
+
if (np) terms = [np[1], np[3], participleObject(np[6])];
|
|
3356
|
+
else {
|
|
3357
|
+
const same = payload.match(SAME_NOUN_TEACH_RE);
|
|
3358
|
+
if (same) terms = [same[1], same[2]];
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
if (!terms) return false;
|
|
3362
|
+
for (const t of terms) {
|
|
3363
|
+
try { if (await resolveEntity(graph, String(t).trim())) return true; } catch { /* unresolved is fine */ }
|
|
3364
|
+
}
|
|
3365
|
+
return false;
|
|
3366
|
+
}
|
|
3166
3367
|
/** Determiners/quantifiers that make the FIRST token an article, not a real
|
|
3167
3368
|
* bare-name subject ("every controller…", "the cache…") — GENERAL_VERB_TEACH_RE
|
|
3168
3369
|
* would otherwise happily bind them as a 1-token subject and misread the
|
|
@@ -3251,7 +3452,17 @@ const GENERAL_VERB_ANYWHERE_EXCLUDE_RE = /\b(?:is|are|am|owns|maintains)\b/i;
|
|
|
3251
3452
|
* the legit "mentors" as NOUN, so a POS gate would have regressed a real
|
|
3252
3453
|
* teach; a closed list (templates over general grammar rules) is both more
|
|
3253
3454
|
* reliable here and, being closed, can never widen recognition the way a
|
|
3254
|
-
* probabilistic POS heuristic could.
|
|
3455
|
+
* probabilistic POS heuristic could.
|
|
3456
|
+
*
|
|
3457
|
+
* The interrogative pronouns (what/who/which/where/when/why/how) join the
|
|
3458
|
+
* list for the same reason: a dropped-copula casual fragment ("k what abt
|
|
3459
|
+
* users.mjs", missing the "is" QUESTION_LEAD_RE/hasMidSentenceInterrogative
|
|
3460
|
+
* both expect) leaves the WH-word sitting in the verb slot exactly like "if"
|
|
3461
|
+
* or "in" above — subject "k", verb "what", object "abt users.mjs" — and
|
|
3462
|
+
* the SAME nonsense -s fold ("k WHATs abt users.mjs") mints it as a fact. A
|
|
3463
|
+
* WH-word is never a genuine general-verb-frame verb, so this is the same
|
|
3464
|
+
* pure narrowing the block above already describes, just closing the one
|
|
3465
|
+
* closed class it left out. */
|
|
3255
3466
|
const GENERAL_VERB_NOT_A_VERB_RE = new RegExp(
|
|
3256
3467
|
"^(?:"
|
|
3257
3468
|
// personal/possessive/demonstrative pronouns + determiners (mirrors, and
|
|
@@ -3264,6 +3475,9 @@ const GENERAL_VERB_NOT_A_VERB_RE = new RegExp(
|
|
|
3264
3475
|
+ "|up|down|off|out|above|below|between|among|against|without|within|along|across|behind|beyond|upon|toward|towards|per"
|
|
3265
3476
|
// conjunctions/subordinators
|
|
3266
3477
|
+ "|and|but|or|if|because|although|though|while|when|since|unless|until|whether|so|nor|than|as"
|
|
3478
|
+
// interrogative pronouns/adverbs — never a real verb, only ever the
|
|
3479
|
+
// fronted question-word of a copula-dropped fragment
|
|
3480
|
+
+ "|what|who|whom|whose|which|where|why|how"
|
|
3267
3481
|
+ ")$",
|
|
3268
3482
|
"i",
|
|
3269
3483
|
);
|
|
@@ -3819,20 +4033,38 @@ function habitualGroundingHintText(line, habitual) {
|
|
|
3819
4033
|
* a demonstrated entity ("that is a bug", pointing at something real) is a
|
|
3820
4034
|
* much closer call than "every you is a womble".
|
|
3821
4035
|
*
|
|
3822
|
-
* The verb slot matches ANY word, not just the is/are/am copula
|
|
4036
|
+
* The verb slot matches ANY word, not just the is/are/am copula — a pronoun
|
|
3823
4037
|
* is just as invalid a fact subject under a general verb ("remember you has
|
|
3824
|
-
* a hat", "remember he eats ribs") as it is under "is" —
|
|
3825
|
-
*
|
|
3826
|
-
*
|
|
3827
|
-
*
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
4038
|
+
* a hat", "remember he eats ribs") as it is under "is" — but ONLY once an
|
|
4039
|
+
* explicit "remember"/"note"/"teach me"-style wrapper (TEACH_RE) already
|
|
4040
|
+
* named this an unambiguous teach attempt (TEACH_PRONOUN_WRAPPED_RE, tried
|
|
4041
|
+
* first when `wrapped` is set). A BARE sentence with no such signal gets the
|
|
4042
|
+
* narrower TEACH_PRONOUN_BARE_RE instead: pronoun + copula + a SHORT
|
|
4043
|
+
* complement (one word, one optional leading article) — the exact shape
|
|
4044
|
+
* TEACH_PROPERTY_RE/BARE_DECLARATIVE_RE would themselves recognize from a
|
|
4045
|
+
* legal subject. Without that narrowing, an ordinary opener that merely
|
|
4046
|
+
* STARTS with "I" ("I am new here", "I want to know X") reached this guard
|
|
4047
|
+
* too (teachLane is the LAST lane tried, after every query strategy already
|
|
4048
|
+
* missed) and fired the copula-specific decline over a sentence no frame
|
|
4049
|
+
* would ever have stored regardless of subject — a confusing answer to a
|
|
4050
|
+
* question nobody asked. Those now fall to teachExclusionReason's existing
|
|
4051
|
+
* self-referential exclusion (a standalone "i"/"me" is already a
|
|
4052
|
+
* TEACH_EXCLUDE_META_TOKEN_RE hit) or to generalVerbTeach's own silent
|
|
4053
|
+
* decline (GENERAL_VERB_NOT_A_VERB_RE covers the same closed pronoun set) —
|
|
4054
|
+
* the same clean stand-down a non-pronoun subject already gets. */
|
|
4055
|
+
const TEACH_PRONOUNS_BARE = Object.freeze(["you", "i", "it", "they", "he", "she", "we"]);
|
|
4056
|
+
// The contracted forms ("i'm new here …") bake the copula into one token, so
|
|
4057
|
+
// they get their own branch below rather than a separate copula match.
|
|
4058
|
+
const TEACH_PRONOUNS_CONTRACTED = Object.freeze(["you're", "i'm", "it's", "they're", "he's", "she's", "we're"]);
|
|
4059
|
+
const TEACH_PRONOUNS = Object.freeze([...TEACH_PRONOUNS_CONTRACTED, ...TEACH_PRONOUNS_BARE]);
|
|
4060
|
+
const TEACH_PRONOUN_WRAPPED_RE = new RegExp(`^(?:every\\s+|each\\s+|all\\s+|some\\s+|a few\\s+|a\\s+|an\\s+)?(${TEACH_PRONOUNS.join("|")})\\s+\\S+`, "i");
|
|
4061
|
+
const TEACH_PRONOUN_BARE_RE = new RegExp(
|
|
4062
|
+
"^(?:every\\s+|each\\s+|all\\s+|some\\s+|a few\\s+|a\\s+|an\\s+)?"
|
|
4063
|
+
+ `(?:(${TEACH_PRONOUNS_BARE.join("|")})\\s+(?:is|are|am|was|were)`
|
|
4064
|
+
+ `|(${TEACH_PRONOUNS_CONTRACTED.join("|")}))`
|
|
4065
|
+
+ "\\s+(?:an?\\s+)?[\\w'-]+[.!?]*$",
|
|
4066
|
+
"i",
|
|
4067
|
+
);
|
|
3836
4068
|
/** The same closed set, read as a whole-word membership test: a pronoun is no
|
|
3837
4069
|
* more a legal fact subject when a reader LIFTS one out of a prior answer than
|
|
3838
4070
|
* when a teach frame offers one. */
|
|
@@ -3988,12 +4220,12 @@ async function teachExclusionReason(sentence) {
|
|
|
3988
4220
|
const unpunctuated = s.replace(/[.!?]+\s*$/, "");
|
|
3989
4221
|
if (TEACH_EXCLUDE_IMPERATIVE_LEAD_RE.test(s)
|
|
3990
4222
|
&& !RETRACT_FORGET_RE.test(unpunctuated) && !RETRACT_FORGET_LOCATIVE_RE.test(unpunctuated)) return "imperative";
|
|
3991
|
-
if (TEACH_EXCLUDE_META_TOKEN_RE.test(s) && !
|
|
4223
|
+
if (TEACH_EXCLUDE_META_TOKEN_RE.test(s) && !TEACH_PRONOUN_BARE_RE.test(s)) return "self-referential";
|
|
3992
4224
|
return null;
|
|
3993
4225
|
}
|
|
3994
4226
|
export { teachExclusionReason };
|
|
3995
4227
|
|
|
3996
|
-
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null, planHolder = null }) {
|
|
4228
|
+
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null, planHolder = null, graph = null }) {
|
|
3997
4229
|
// A closed discourse-marker preamble ahead of a teach sentence ("howdy
|
|
3998
4230
|
// pardner, remember that TaskController is fragile") would otherwise
|
|
3999
4231
|
// corrupt TEACH_RE's own match, so strip it first. applyPreambleFrames is
|
|
@@ -4165,10 +4397,10 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4165
4397
|
// wrapped; trailing punctuation stripped the same way the OWNS/SOME_A_FEW
|
|
4166
4398
|
// lanes below do) before anything else in this function, so a pronoun
|
|
4167
4399
|
// subject NEVER reaches teachSuggestion's "did you mean" hint or
|
|
4168
|
-
// unknownSubjectFallback's direct-write path — see
|
|
4169
|
-
// docblock above for why.
|
|
4400
|
+
// unknownSubjectFallback's direct-write path — see TEACH_PRONOUN_WRAPPED_RE/
|
|
4401
|
+
// TEACH_PRONOUN_BARE_RE's own shared docblock above for why the two differ.
|
|
4170
4402
|
const pronounSrc = (wrapped ?? raw).replace(/[.!?]+\s*$/, "");
|
|
4171
|
-
const pronounMatch = pronounSrc.match(
|
|
4403
|
+
const pronounMatch = pronounSrc.match(wrapped != null ? TEACH_PRONOUN_WRAPPED_RE : TEACH_PRONOUN_BARE_RE);
|
|
4172
4404
|
// A pronoun-led sentence that's ALSO a mid-sentence question ("it uses
|
|
4173
4405
|
// which controller as its base") isn't a pronoun-classification problem at
|
|
4174
4406
|
// all — "it" was never going to be storable either way, so naming the
|
|
@@ -4184,7 +4416,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4184
4416
|
// declining right here.
|
|
4185
4417
|
if (pronounMatch && !ACTION_SIGNATURE_TEACH_RE.test(pronounSrc)
|
|
4186
4418
|
&& !(await hasMidSentenceInterrogative(pronounSrc))) {
|
|
4187
|
-
const pronoun = pronounMatch[1];
|
|
4419
|
+
const pronoun = pronounMatch[1] || pronounMatch[2];
|
|
4188
4420
|
return {
|
|
4189
4421
|
text: `I can't store a fact about "${pronoun}" as a class — pronouns aren't things I can classify. `
|
|
4190
4422
|
+ `I remember facts in the shape "every X is a Y", where X is a specific noun, not a pronoun. `
|
|
@@ -4196,10 +4428,14 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4196
4428
|
// NEGATION ("X is not a Y") and RETRACTION ("forget that X is a Y"). Two
|
|
4197
4429
|
// sentences, two intents, and the split is the point: a negative is a source
|
|
4198
4430
|
// DISAGREEING, never an instruction to destroy. Each branch documents itself
|
|
4199
|
-
// below; both are tried here, right after the pronoun guard
|
|
4200
|
-
// subject ("it is not an animal") still falls to that
|
|
4201
|
-
// first (
|
|
4202
|
-
//
|
|
4431
|
+
// below; both are tried here, right after the pronoun guard. A WRAPPED
|
|
4432
|
+
// pronoun subject ("remember that it is not an animal") still falls to that
|
|
4433
|
+
// guard's own decline first. A BARE one ("it is not an animal") reaches this
|
|
4434
|
+
// block instead, but stays safe either way: retractNotMatch's own "gated on
|
|
4435
|
+
// the positive existing" rule below finds no stored "it ⊑ …" fact to
|
|
4436
|
+
// disagree with (a pronoun was never a legal mint subject anywhere in this
|
|
4437
|
+
// lane), so it falls through to the ordinary cascade unstored, same as
|
|
4438
|
+
// today — just without this guard's more specific wording.
|
|
4203
4439
|
const retractSrc = (wrapped ?? raw).replace(/[.!?]+\s*$/, "");
|
|
4204
4440
|
const retractSrcMidQuestion = memoryDir && !QUESTION_LEAD_RE.test(retractSrc)
|
|
4205
4441
|
? await hasMidSentenceInterrogative(retractSrc) : false;
|
|
@@ -4869,7 +5105,10 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4869
5105
|
|
|
4870
5106
|
let payload = null;
|
|
4871
5107
|
if (wrapped && /\b(?:is|are)\b/i.test(wrapped)) payload = wrapped;
|
|
4872
|
-
else if ((BARE_DECLARATIVE_RE.test(raw) || COMPARATIVE_TEACH_RE.test(raw)
|
|
5108
|
+
else if ((BARE_DECLARATIVE_RE.test(raw) || COMPARATIVE_TEACH_RE.test(raw)
|
|
5109
|
+
|| (matchesRelationalTeachFrame(raw) && !(await relationalFrameNamesGraphEntity(raw, graph)))
|
|
5110
|
+
|| matchBareHabitualTeach(raw) || matchBareCanTeach(raw))
|
|
5111
|
+
&& !QUESTION_LEAD_RE.test(raw) && !(await hasMidSentenceInterrogative(raw))) payload = raw;
|
|
4873
5112
|
if (!payload) {
|
|
4874
5113
|
// "remember margo eats ribs", re-escaping here through a combination
|
|
4875
5114
|
// that mechanism's own deliberate subject-shape restriction doesn't
|
|
@@ -4901,6 +5140,58 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4901
5140
|
});
|
|
4902
5141
|
if (stored) return stored;
|
|
4903
5142
|
}
|
|
5143
|
+
// RELATIONAL teach frames — a participle+preposition claim
|
|
5144
|
+
// ("sales are closely connected with marketing"), a copula-NP with a
|
|
5145
|
+
// trailing participle ("sales are activities related to selling"), or a
|
|
5146
|
+
// shared-attribute claim ("sales and marketing have the same goal"). Each
|
|
5147
|
+
// reads through splitTeachNegation like its comparative/general-verb
|
|
5148
|
+
// siblings, so a negated form stores the mgxneg: twin.
|
|
5149
|
+
{
|
|
5150
|
+
const { payload: posPayload, negated } = splitTeachNegation(String(payload).trim());
|
|
5151
|
+
// (D) participle + preposition, checked ahead of the copula-NP form so a
|
|
5152
|
+
// bare participle right after the copula is never misread as a noun.
|
|
5153
|
+
const pp = posPayload.match(PARTICIPLE_PREP_TEACH_RE);
|
|
5154
|
+
if (pp) {
|
|
5155
|
+
const pred = `mgx:${pp[2].toLowerCase()}-${pp[3].toLowerCase()}`;
|
|
5156
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
5157
|
+
subject: pp[1].trim(), predicate: negated ? negatedPredicate(pred) : pred,
|
|
5158
|
+
object: participleObject(pp[4]),
|
|
5159
|
+
});
|
|
5160
|
+
if (stored) return stored;
|
|
5161
|
+
}
|
|
5162
|
+
// (E) copula-NP + trailing participle — decomposed into the
|
|
5163
|
+
// class-membership half (subject ⊑ singular(NP head)) and the relational
|
|
5164
|
+
// half. The copula-NP shape is a deliberate declarative, strong enough to
|
|
5165
|
+
// mint the membership directly, so it lands even when neither term is in
|
|
5166
|
+
// the lexicon (the acceptance the ingest pipeline needs). Either half may
|
|
5167
|
+
// stand on its own if the other's write fails.
|
|
5168
|
+
const np = posPayload.match(COPULA_NP_PARTICIPLE_TEACH_RE);
|
|
5169
|
+
if (np) {
|
|
5170
|
+
const subject = np[1].trim();
|
|
5171
|
+
const relPred = `mgx:${np[4].toLowerCase()}-${np[5].toLowerCase()}`;
|
|
5172
|
+
const isaStored = await teachFact(memoryDir, sessionId, {
|
|
5173
|
+
subject, predicate: SUBCLASS_PREDICATE, object: singularizeSurface(np[3]),
|
|
5174
|
+
});
|
|
5175
|
+
const relStored = await teachFact(memoryDir, sessionId, {
|
|
5176
|
+
subject, predicate: negated ? negatedPredicate(relPred) : relPred,
|
|
5177
|
+
object: participleObject(np[6]),
|
|
5178
|
+
});
|
|
5179
|
+
const stripNoted = (t) => String(t).replace(/^noted — remembered(?:\s+\d+\s+facts?)?:\s*/i, "").trim();
|
|
5180
|
+
if (isaStored && relStored) return { text: `noted — remembered both: ${stripNoted(isaStored.text)}; and ${stripNoted(relStored.text)}`, via: "assert", miss: false };
|
|
5181
|
+
if (relStored) return relStored;
|
|
5182
|
+
if (isaStored) return isaStored;
|
|
5183
|
+
}
|
|
5184
|
+
// (F) shared attribute — "A and B have the same <noun>".
|
|
5185
|
+
const same = posPayload.match(SAME_NOUN_TEACH_RE);
|
|
5186
|
+
if (same) {
|
|
5187
|
+
const pred = `mgx:same-${same[3].toLowerCase()}-as`;
|
|
5188
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
5189
|
+
subject: same[1].trim(), predicate: negated ? negatedPredicate(pred) : pred,
|
|
5190
|
+
object: same[2].trim(),
|
|
5191
|
+
});
|
|
5192
|
+
if (stored) return stored;
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
4904
5195
|
for (const cand of assertCandidates(payload)) {
|
|
4905
5196
|
// assertTurn ITSELF records the "every" quantifier (point 3) on a plain
|
|
4906
5197
|
// universal success, so every caller (this loop AND the top-level
|
|
@@ -5624,7 +5915,8 @@ export async function helpText() {
|
|
|
5624
5915
|
["/export <path>", "write the memory store to a file, as JSONL (the same shape `tmct memory --export` writes)"],
|
|
5625
5916
|
["/ingest <path>", "read a local text file and store every fact the recognizer grounds from it (same recognizer as `tmct extract`)"],
|
|
5626
5917
|
["/narrate on|off", "verbose developer/debug mode: decision points, matched pattern, results+sources, goal per turn"],
|
|
5627
|
-
["/wiki on|off|supplement", "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 answer"],
|
|
5918
|
+
["/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"],
|
|
5919
|
+
["research <topic> [limit N]", "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"],
|
|
5628
5920
|
["/help", "this list"],
|
|
5629
5921
|
["/exit", "leave the session (also Ctrl+C / Ctrl+D)"],
|
|
5630
5922
|
];
|
|
@@ -5904,6 +6196,14 @@ function predicatePhrase(predicate) {
|
|
|
5904
6196
|
// "is smaller than" (never a 3sg fold — "smallers" isn't a word)
|
|
5905
6197
|
const comp = /^mgx:([a-z]+(?:-[a-z]+)*)-than$/i.exec(p);
|
|
5906
6198
|
if (comp) return `is ${comp[1].replace(/-/g, " ")} than`;
|
|
6199
|
+
// a participle + preposition renders as its copula surface: mgx:connected-with
|
|
6200
|
+
// -> "is connected with" (the participle is already a participle, so no 3sg
|
|
6201
|
+
// fold — "connecteds" isn't a word)
|
|
6202
|
+
const part = new RegExp(`^mgx:(${TEACH_PARTICIPLE_SRC})-([a-z]+)$`, "i").exec(p);
|
|
6203
|
+
if (part) return `is ${part[1].toLowerCase()} ${part[2].toLowerCase()}`;
|
|
6204
|
+
// a shared-attribute predicate: mgx:same-goal-as -> "has the same goal as"
|
|
6205
|
+
const same = /^mgx:same-([a-z]+)-as$/i.exec(p);
|
|
6206
|
+
if (same) return `has the same ${same[1].toLowerCase()} as`;
|
|
5907
6207
|
const m = /^mgx:([a-z]+)(?:-([a-z]+))?$/i.exec(p);
|
|
5908
6208
|
if (!m) return predicate;
|
|
5909
6209
|
// a folded preposition renders back naturally: mgx:rest-on -> "rests on"
|
|
@@ -7939,13 +8239,14 @@ const IS_ADJECTIVE_PRONOUN_RE = /^(?:it|this|that)$/i;
|
|
|
7939
8239
|
* SUBJECT capture — factReadBack then treats "you"/"you like"/"you secretly
|
|
7940
8240
|
* chatgpt or" as a literal fact subject and offers to teach a fact ABOUT the
|
|
7941
8241
|
* pronoun ("remember that you is happy"), exactly the grammatical category
|
|
7942
|
-
* error
|
|
7943
|
-
* on the teach-lane side. Same pronoun set (you|i|they|he|
|
|
7944
|
-
* here, checked at the START of the subject capture only
|
|
7945
|
-
* the whole capture — a pronoun subject can carry trailing
|
|
7946
|
-
* like"/"you secretly … or", the same
|
|
7947
|
-
*
|
|
7948
|
-
*
|
|
8242
|
+
* error TEACH_PRONOUN_WRAPPED_RE/TEACH_PRONOUN_BARE_RE (above) were already
|
|
8243
|
+
* built to reject on the teach-lane side. Same pronoun set (you|i|they|he|
|
|
8244
|
+
* she|we) reused here, checked at the START of the subject capture only
|
|
8245
|
+
* (not anchored to the whole capture — a pronoun subject can carry trailing
|
|
8246
|
+
* words, "you like"/"you secretly … or", the same class of category error
|
|
8247
|
+
* those two regexes reject on the teach-lane side). "it" is deliberately
|
|
8248
|
+
* EXCLUDED from this set, unlike TEACH_PRONOUN_WRAPPED_RE/
|
|
8249
|
+
* TEACH_PRONOUN_BARE_RE — IS_ADJECTIVE_PRONOUN_RE (just above) already gives "it"
|
|
7949
8250
|
* its own correct, wanted behavior (anaphoric resolution against the
|
|
7950
8251
|
* session's current FOCUS, "is it deprecated" → resolves off focusLabel),
|
|
7951
8252
|
* which this guard must not shadow. Every call site below is expected to
|
|
@@ -10012,7 +10313,7 @@ async function liveReferenceAnswerForKey(key, onLiveLookup) {
|
|
|
10012
10313
|
* key up in the shipped child triples pack and append every fact under child
|
|
10013
10314
|
* provenance, so the SAME question can be re-asked from the store. Null on a
|
|
10014
10315
|
* pack miss or any failure — the turn then proceeds byte-identically. */
|
|
10015
|
-
async function childPackFactsForKey(key, { memoryDir, env, cache }) {
|
|
10316
|
+
async function childPackFactsForKey(key, { memoryDir, env, cache, synthesisBudget = AUTO_SYNTHESIS_BUDGET }) {
|
|
10016
10317
|
let row = null;
|
|
10017
10318
|
try { row = await getChildPackProvider(env).lookup(key); } catch { row = null; }
|
|
10018
10319
|
if (!row?.facts?.length) return null;
|
|
@@ -10023,7 +10324,7 @@ async function childPackFactsForKey(key, { memoryDir, env, cache }) {
|
|
|
10023
10324
|
})));
|
|
10024
10325
|
} catch { return null; }
|
|
10025
10326
|
if (cache) cache.rows = null;
|
|
10026
|
-
await synthesiseAroundTerm(memoryDir, key, cache);
|
|
10327
|
+
await synthesiseAroundTerm(memoryDir, key, cache, synthesisBudget);
|
|
10027
10328
|
return { key, count: row.facts.length };
|
|
10028
10329
|
}
|
|
10029
10330
|
|
|
@@ -10034,7 +10335,7 @@ async function childPackFactsForKey(key, { memoryDir, env, cache }) {
|
|
|
10034
10335
|
* and is failure-tolerated: the answer stands whether or not the facts land.
|
|
10035
10336
|
* The optimistic tier is pure (no recognizer re-entry), so this stays cheap on
|
|
10036
10337
|
* the chat turn. Returns the count stored. */
|
|
10037
|
-
async function ingestReferenceArticle(memoryDir, key, article, cache, tagFor = referenceProvenanceTag, lexicon = null) {
|
|
10338
|
+
async function ingestReferenceArticle(memoryDir, key, article, cache, tagFor = referenceProvenanceTag, lexicon = null, synthesisBudget = AUTO_SYNTHESIS_BUDGET) {
|
|
10038
10339
|
if (!article) return 0;
|
|
10039
10340
|
const provenance = tagFor(article);
|
|
10040
10341
|
const facts = [];
|
|
@@ -10056,7 +10357,7 @@ async function ingestReferenceArticle(memoryDir, key, article, cache, tagFor = r
|
|
|
10056
10357
|
await appendFacts(memoryDir, facts);
|
|
10057
10358
|
if (cache) cache.rows = null;
|
|
10058
10359
|
} catch { return 0; }
|
|
10059
|
-
await synthesiseAroundTerm(memoryDir, key, cache);
|
|
10360
|
+
await synthesiseAroundTerm(memoryDir, key, cache, synthesisBudget);
|
|
10060
10361
|
return facts.length;
|
|
10061
10362
|
}
|
|
10062
10363
|
|
|
@@ -10072,15 +10373,15 @@ const AUTO_SYNTHESIS_BUDGET = 12;
|
|
|
10072
10373
|
* facts carry entailed:* provenance at their discounted trust and are
|
|
10073
10374
|
* retractable. Failure-tolerated: a synthesis miss never disturbs the answer
|
|
10074
10375
|
* the load already composed. Returns the count derived. */
|
|
10075
|
-
async function synthesiseAroundTerm(memoryDir, term, cache) {
|
|
10076
|
-
if (!memoryDir || !term) return 0;
|
|
10376
|
+
async function synthesiseAroundTerm(memoryDir, term, cache, budget = AUTO_SYNTHESIS_BUDGET) {
|
|
10377
|
+
if (!memoryDir || !term || budget <= 0) return 0;
|
|
10077
10378
|
try {
|
|
10078
10379
|
const { syllogise } = await import("../domain/syllogise.mjs");
|
|
10079
10380
|
const { loadMemory, readFactRows, appendFacts, normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
10080
10381
|
const res = await syllogise(memoryDir, {
|
|
10081
10382
|
focus: [...factTermVariants(normFactTerm, term)],
|
|
10082
10383
|
expandFocus: true,
|
|
10083
|
-
budget
|
|
10384
|
+
budget,
|
|
10084
10385
|
store: { loadMemory, readFactRows, appendFacts },
|
|
10085
10386
|
});
|
|
10086
10387
|
if (res?.count && cache) cache.rows = null;
|
|
@@ -10453,9 +10754,13 @@ async function compareAnswer(query, { graph, config, source }) {
|
|
|
10453
10754
|
* "give me a detailed overview of X", wired to src/domain/completions/'s extractive
|
|
10454
10755
|
* multi-sentence pipeline below. Two closed shapes (DETAILED_HOW_WORKS_RE
|
|
10455
10756
|
* tried first, more specific); distinct from DESCRIBE_WRAPPER_RE, which
|
|
10456
|
-
* neither anchors on "give me"/"explain ... in detail".
|
|
10757
|
+
* neither anchors on "give me"/"explain ... in detail". The article before
|
|
10758
|
+
* "detailed" and the "of" before "how" are both OPTIONAL: a non-native
|
|
10759
|
+
* speaker's "give me detailed summary how this application works" carries
|
|
10760
|
+
* the identical intent as the fully-articled form, and dropping either word
|
|
10761
|
+
* changes nothing the pipeline downstream reads — same shape, same term. */
|
|
10457
10762
|
const DETAILED_HOW_WORKS_RE =
|
|
10458
|
-
/^(?:(?:can|could|would)\s+you\s+(?:please\s+)?|please\s+)?(?:give\s+me\s+
|
|
10763
|
+
/^(?:(?:can|could|would)\s+you\s+(?:please\s+)?|please\s+)?(?:give\s+me\s+(?:an?\s+)?detailed\s+(?:summary|overview|explanation)\s+(?:of\s+)?how|explain\s+(?:to\s+me\s+)?in\s+detail\s+how)\s+(.+?)\s+works\s*\??$/i;
|
|
10459
10764
|
|
|
10460
10765
|
const DETAILED_OVERVIEW_RE =
|
|
10461
10766
|
/^(?:(?:can|could|would)\s+you\s+(?:please\s+)?|please\s+)?give\s+me\s+a\s+detailed\s+(?:overview|summary|explanation)\s+of\s+(.+?)\s*\??$/i;
|
|
@@ -11280,8 +11585,12 @@ const DECISION_RECALL_RE = /^(?:remind\s+me\s+)?what\s+(?:did\s+)?(?:we|i|you)\s
|
|
|
11280
11585
|
* than silently accepted alongside the current location. */
|
|
11281
11586
|
const MOVE_HISTORY_RE = /^where\s+did\s+(.+?)\s+(?:move|get\s+moved|go)(?:\s+to)?[?.!\s]*$/i;
|
|
11282
11587
|
|
|
11283
|
-
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null, tel = null, biasByBundle = {}, cache = null, vocabAntecedent = null, planHolder = null, gameConfig = DEFAULT_GAME_CONFIG, liveReference = false, onLiveLookup = null }) {
|
|
11588
|
+
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null, tel = null, biasByBundle = {}, cache = null, vocabAntecedent = null, planHolder = null, gameConfig = DEFAULT_GAME_CONFIG, liveReference = false, onLiveLookup = null, uiContext = "cli", synthesisBudget = AUTO_SYNTHESIS_BUDGET }) {
|
|
11284
11589
|
const ts = new Date().toISOString();
|
|
11590
|
+
// The surface this turn runs on ("cli" default; "browser" from a web entry) —
|
|
11591
|
+
// the honest-miss tail below points a browser/adventure miss at the teach
|
|
11592
|
+
// lane instead of the CLI-only --repo/tmct-init remedy.
|
|
11593
|
+
const browser = uiContext === "browser";
|
|
11285
11594
|
// DISCOURSE ANAPHORA: a follow-up like "which of those are tested" / "count
|
|
11286
11595
|
// them" filters or counts the PREVIOUS answer's entity set, threaded as
|
|
11287
11596
|
// ask()'s `prev`. Prefers the FULL id set (`allIds`) over `matches`, since a
|
|
@@ -11340,7 +11649,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11340
11649
|
try { liveKey = cleanMissLiveTerm(wikiTerm, lexicon ?? undefined); } catch { liveKey = null; }
|
|
11341
11650
|
const live = liveKey ? await liveReferenceAnswerForKey(liveKey, onLiveLookup) : null;
|
|
11342
11651
|
if (live) {
|
|
11343
|
-
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon);
|
|
11652
|
+
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon, synthesisBudget);
|
|
11344
11653
|
note(trace, `lane: WIKIPEDIA ASK — answered from a live en.wikipedia.org lookup, cited (article "${live.article.title}", revid ${live.article.revid})`);
|
|
11345
11654
|
return plainTurn(query, live.text, { via: "reference", miss: false, focus });
|
|
11346
11655
|
}
|
|
@@ -11503,8 +11812,12 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11503
11812
|
// examples are the wrong audience here), an unseeded one at the seed/
|
|
11504
11813
|
// teach pair — vocabHint already carries exactly that split.
|
|
11505
11814
|
answer = (!graph || noCodeGraph(graph)) && (!config || e?.emptyGraph || /^cannot read graph artifact\b/.test(thrown))
|
|
11815
|
+
// A browser session has no `tmct init in a repo` to reach for, so its
|
|
11816
|
+
// fallback drops that CLI-only remedy and keeps just the teach pointer.
|
|
11506
11817
|
? `I can't answer that as a code question — no code graph is loaded in this session. ${vocabHint
|
|
11507
|
-
||
|
|
11818
|
+
|| (browser
|
|
11819
|
+
? "I can still remember and answer taught facts (try \"every bug is an issue\")."
|
|
11820
|
+
: "I can still remember and answer taught facts (try \"every bug is an issue\"), or run `tmct init` in a repo to index one.")}`
|
|
11508
11821
|
: thrown;
|
|
11509
11822
|
note(trace, `intermediate: the ask engine threw — ${thrown}`);
|
|
11510
11823
|
}
|
|
@@ -11903,7 +12216,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11903
12216
|
if (!bareMetaHit) {
|
|
11904
12217
|
const refTerm = metaTermOf(factQuery, envelope);
|
|
11905
12218
|
const key = refTerm ? await cleanMissPackKey(refTerm, { graph, memoryDir, lexicon, cache }) : null;
|
|
11906
|
-
const learned = key ? await childPackFactsForKey(key, { memoryDir, env, cache }) : null;
|
|
12219
|
+
const learned = key ? await childPackFactsForKey(key, { memoryDir, env, cache, synthesisBudget }) : null;
|
|
11907
12220
|
if (learned) {
|
|
11908
12221
|
const fact = (await factAnswer(memoryDir, factQuery, envelope, miss, biasByBundle, cache, newFocus?.label))
|
|
11909
12222
|
?? (await factReadBack(memoryDir, factQuery, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
|
|
@@ -11967,7 +12280,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11967
12280
|
handled = true;
|
|
11968
12281
|
note(trace, "lane: (2b) REFERENCE PACK — a bare \"what is X\" clean miss answered from the shipped reference pack, cited");
|
|
11969
12282
|
note(trace, `source: reference pack ${REFERENCE_PACK_NAME} — article "${bareMetaHit.reference.article.title}" (revid ${bareMetaHit.reference.article.revid})`);
|
|
11970
|
-
await ingestReferenceArticle(memoryDir, bareMetaHit.reference.key, bareMetaHit.reference.article, cache, referenceProvenanceTag, lexicon);
|
|
12283
|
+
await ingestReferenceArticle(memoryDir, bareMetaHit.reference.key, bareMetaHit.reference.article, cache, referenceProvenanceTag, lexicon, synthesisBudget);
|
|
11971
12284
|
} else if (bareMetaHit?.live) {
|
|
11972
12285
|
// The bare-form LIVE hit settles the same way, under live provenance.
|
|
11973
12286
|
answer = bareMetaHit.text;
|
|
@@ -11976,7 +12289,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11976
12289
|
handled = true;
|
|
11977
12290
|
note(trace, "lane: (2b) LIVE WIKIPEDIA — a bare \"what is X\" clean miss answered from a live en.wikipedia.org lookup (opt-in), cited");
|
|
11978
12291
|
note(trace, `source: live reference ${LIVE_PACK_NAME} — article "${bareMetaHit.live.article.title}" (revid ${bareMetaHit.live.article.revid})`);
|
|
11979
|
-
await ingestReferenceArticle(memoryDir, bareMetaHit.live.key, bareMetaHit.live.article, cache, liveProvenanceTag, lexicon);
|
|
12292
|
+
await ingestReferenceArticle(memoryDir, bareMetaHit.live.key, bareMetaHit.live.article, cache, liveProvenanceTag, lexicon, synthesisBudget);
|
|
11980
12293
|
} else if (bareMetaHit) {
|
|
11981
12294
|
answer = bareMetaHit.replace ? bareMetaHit.text : `${answer}\n${bareMetaHit.text}`;
|
|
11982
12295
|
// Same discipline as lane (3): a fact-lane return flagged `miss` is an
|
|
@@ -12203,7 +12516,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12203
12516
|
// (4) #2 TEACH lane — a teach-shaped would-miss nothing above answered: route to
|
|
12204
12517
|
// memory, or say what CAN be remembered (LOUD), never the wall / a silent drop.
|
|
12205
12518
|
if (miss && recordMiss && via === "composed") {
|
|
12206
|
-
const taught = await teachLane(query, { memoryDir, sessionId, lexicon, cache, planHolder });
|
|
12519
|
+
const taught = await teachLane(query, { memoryDir, sessionId, lexicon, cache, planHolder, graph });
|
|
12207
12520
|
if (taught) {
|
|
12208
12521
|
answer = taught.text; via = taught.via; recordMiss = taught.miss;
|
|
12209
12522
|
if (!taught.miss) dialogueLaneOverride = "teach";
|
|
@@ -12222,6 +12535,26 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12222
12535
|
// kind of parent" as inherits) keeps its canonical: it genuinely
|
|
12223
12536
|
// restates the relation the teach stored.
|
|
12224
12537
|
if (envelope?.parsed?.fuzzyVerb) canonical = null;
|
|
12538
|
+
// A mid-game locative teach is accepted and stored as the player's own
|
|
12539
|
+
// note, but a running adventure's world only moves through actions — so
|
|
12540
|
+
// when the taught subject is a term the world already places, the
|
|
12541
|
+
// confirmation says so plainly. (The fold itself keeps taught rows out of
|
|
12542
|
+
// the world state; this is the matching UX.)
|
|
12543
|
+
if (!taught.miss && planHolder?.state?.adventure) {
|
|
12544
|
+
const loc = String(query).trim().match(LOCATIVE_TEACH_RE);
|
|
12545
|
+
if (loc) {
|
|
12546
|
+
let placed = false;
|
|
12547
|
+
try {
|
|
12548
|
+
const { normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
12549
|
+
const world = foldWorldState(await factRows(memoryDir, cache));
|
|
12550
|
+
placed = world.placements.has(normFactTerm(loc[1]));
|
|
12551
|
+
} catch { placed = false; }
|
|
12552
|
+
if (placed) {
|
|
12553
|
+
answer = `${answer}\n(noted as your note — the game world itself only changes through actions like go, take and open.)`;
|
|
12554
|
+
note(trace, "intermediate: mid-game locative teach — stored as a note; the adventure fold only moves through actions");
|
|
12555
|
+
}
|
|
12556
|
+
}
|
|
12557
|
+
}
|
|
12225
12558
|
}
|
|
12226
12559
|
}
|
|
12227
12560
|
// (4b) #4 AUTHOR lane — "who is <Name>", "what did <Name> touch",
|
|
@@ -12362,7 +12695,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12362
12695
|
if (miss && recordMiss && via === "composed" && memoryDir) {
|
|
12363
12696
|
const refTerm = metaTermOf(query, envelope);
|
|
12364
12697
|
const key = refTerm ? await cleanMissPackKey(refTerm, { graph, memoryDir, lexicon, cache }) : null;
|
|
12365
|
-
const learned = key ? await childPackFactsForKey(key, { memoryDir, env, cache }) : null;
|
|
12698
|
+
const learned = key ? await childPackFactsForKey(key, { memoryDir, env, cache, synthesisBudget }) : null;
|
|
12366
12699
|
if (learned) {
|
|
12367
12700
|
const fact = (await factAnswer(memoryDir, query, envelope, miss, biasByBundle, cache, newFocus?.label))
|
|
12368
12701
|
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
|
|
@@ -12383,7 +12716,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12383
12716
|
recordMiss = false;
|
|
12384
12717
|
note(trace, "lane: (4h) REFERENCE PACK — a clean miss on a lexicon term answered from the shipped reference pack, cited");
|
|
12385
12718
|
note(trace, `source: reference pack ${REFERENCE_PACK_NAME} — article "${ref.article.title}" (revid ${ref.article.revid})`);
|
|
12386
|
-
await ingestReferenceArticle(memoryDir, ref.key, ref.article, cache, referenceProvenanceTag, lexicon);
|
|
12719
|
+
await ingestReferenceArticle(memoryDir, ref.key, ref.article, cache, referenceProvenanceTag, lexicon, synthesisBudget);
|
|
12387
12720
|
}
|
|
12388
12721
|
}
|
|
12389
12722
|
// The live Wikipedia supplement (opt-in), strictly AFTER both shipped
|
|
@@ -12399,7 +12732,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12399
12732
|
recordMiss = false;
|
|
12400
12733
|
note(trace, "lane: (4h) LIVE WIKIPEDIA — a clean miss answered from a live en.wikipedia.org lookup (opt-in), cited");
|
|
12401
12734
|
note(trace, `source: live reference ${LIVE_PACK_NAME} — article "${live.article.title}" (revid ${live.article.revid})`);
|
|
12402
|
-
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon);
|
|
12735
|
+
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon, synthesisBudget);
|
|
12403
12736
|
}
|
|
12404
12737
|
}
|
|
12405
12738
|
}
|
|
@@ -12421,12 +12754,25 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12421
12754
|
note(trace, `lane: (5) SHORT TAILORED MISS — every lane above declined; ${repeat ? "REPEAT collapsed to one-liner (wall kindness)" : "the full grammar wall was shortened + tailored to the query's keywords"}`);
|
|
12422
12755
|
}
|
|
12423
12756
|
// #4 HONEST-EMPTY POLISH — an empty CODE graph: any still-standing engine
|
|
12424
|
-
// dead-end (an honest empty, the short miss, the bootstrap note) carries the
|
|
12425
|
-
// toward a real graph, unless it already points there. Only when
|
|
12757
|
+
// dead-end (an honest empty, the short miss, the bootstrap note) carries the
|
|
12758
|
+
// exit toward a real graph, unless it already points there. Only when
|
|
12759
|
+
// genuinely empty. The CLI keeps the --repo/example pointer verbatim; a
|
|
12760
|
+
// browser or a live adventure has no such command to reach for, so each gets
|
|
12761
|
+
// a teach-forward pointer (and the adventure also names the world asides that
|
|
12762
|
+
// are guaranteed to hit).
|
|
12763
|
+
const adventureLive = !!planHolder?.state?.adventure;
|
|
12426
12764
|
if (recordMiss && (via === "composed" || via === "miss")
|
|
12427
12765
|
&& noCodeGraph(graph) && !/--repo|tmct init|no code graph/i.test(answer)) {
|
|
12428
|
-
|
|
12429
|
-
|
|
12766
|
+
if (adventureLive) {
|
|
12767
|
+
answer = `${answer}\n(I don't know that yet — you can teach me: say "remember: <thing> is a <kind>". Or ask the world: "look", "where is the key", "talk to the butler".)`;
|
|
12768
|
+
note(trace, "intermediate: HONEST-EMPTY POLISH — a live adventure miss points at the teach lane and the world asides, not the --repo remedy");
|
|
12769
|
+
} else if (browser) {
|
|
12770
|
+
answer = `${answer}\n(I don't know that yet — you can teach me: say "remember: <thing> is a <kind>".)`;
|
|
12771
|
+
note(trace, "intermediate: HONEST-EMPTY POLISH — a browser miss points at the teach lane, not the CLI-only --repo remedy");
|
|
12772
|
+
} else {
|
|
12773
|
+
answer = `${answer}\n(this repo has no code graph — for structure, point me at a \`.tmct/graph.json\` with \`--repo <path>\` or run \`npm run example:mini\`; tmct doesn't index code itself.)`;
|
|
12774
|
+
note(trace, "intermediate: HONEST-EMPTY POLISH — the loaded graph has 0 modules, so the dead-end got a --repo/tmct init pointer appended");
|
|
12775
|
+
}
|
|
12430
12776
|
}
|
|
12431
12777
|
// TEACH-OFFER: a "what is X" miss where X is genuinely unknown EVERYWHERE —
|
|
12432
12778
|
// not a real graph entity, not a schema/vocab term, and not already in
|
|
@@ -12497,20 +12843,24 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12497
12843
|
answer = `hypothetically, if ${counterfactualSubject[1].trim()} were removed: ${answer}`;
|
|
12498
12844
|
note(trace, `intermediate: COUNTERFACTUAL_RE matched — compiled to a real traversal, wrapped as hypothetical ("${counterfactualSubject[1].trim()}" removed)`);
|
|
12499
12845
|
}
|
|
12500
|
-
// LIVE SUPPLEMENT (/wiki supplement
|
|
12501
|
-
// Wikipedia says about its subject —
|
|
12502
|
-
//
|
|
12503
|
-
//
|
|
12504
|
-
// Wikipedia read-out. Failure-tolerated, and network-gated by
|
|
12505
|
-
//
|
|
12506
|
-
|
|
12507
|
-
|
|
12846
|
+
// LIVE SUPPLEMENT (/wiki supplement, and its superset /wiki always): a
|
|
12847
|
+
// grounded answer also carries what Wikipedia says about its subject —
|
|
12848
|
+
// corroboration, not rescue. Scoped to a clean vocabulary subject (a
|
|
12849
|
+
// "what is X" / "tell me about X" term), never doubled onto an answer that
|
|
12850
|
+
// already IS a Wikipedia read-out. Failure-tolerated, and network-gated by
|
|
12851
|
+
// the same toggle (both values are truthy, so the rescue lanes above already
|
|
12852
|
+
// ran). "always" widens the term fallback to an ordinary grounded ask's own
|
|
12853
|
+
// parsed object, so a plain code/fact answer also gets corroborated; the
|
|
12854
|
+
// adapter's throttle bounds the request rate.
|
|
12855
|
+
if ((liveReference === "supplement" || liveReference === "always") && !recordMiss && via !== "reference") {
|
|
12856
|
+
const supplementTerm = metaTermOf(query, envelope) || vagueTouchTermOf(query)
|
|
12857
|
+
|| (liveReference === "always" ? envelope?.parsed?.object : null);
|
|
12508
12858
|
let liveKey = null;
|
|
12509
12859
|
try { liveKey = supplementTerm ? cleanMissLiveTerm(supplementTerm, lexicon ?? undefined) : null; } catch { liveKey = null; }
|
|
12510
12860
|
const live = liveKey ? await liveReferenceAnswerForKey(liveKey, onLiveLookup) : null;
|
|
12511
12861
|
if (live) {
|
|
12512
12862
|
answer = `${answer}\nWikipedia adds: ${live.text}`;
|
|
12513
|
-
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon);
|
|
12863
|
+
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon, synthesisBudget);
|
|
12514
12864
|
note(trace, `intermediate: LIVE SUPPLEMENT — appended a cited en.wikipedia.org read-out for "${supplementTerm}" (supplement mode)`);
|
|
12515
12865
|
}
|
|
12516
12866
|
}
|
|
@@ -12653,13 +13003,14 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
12653
13003
|
// state). A bare "/wiki" reports the CURRENT state and changes nothing.
|
|
12654
13004
|
if (name === "wiki") {
|
|
12655
13005
|
const arg = argText.toLowerCase();
|
|
12656
|
-
const stateWord = (v) => (v === "supplement" ? "supplement" : v ? "on" : "off");
|
|
12657
|
-
if (arg !== "on" && arg !== "off" && arg !== "supplement") {
|
|
12658
|
-
return mk(`live Wikipedia supplement is ${stateWord(liveReference)} — /wiki on, /wiki off, or /wiki
|
|
13006
|
+
const stateWord = (v) => (v === "always" ? "always" : v === "supplement" ? "supplement" : v ? "on" : "off");
|
|
13007
|
+
if (arg !== "on" && arg !== "off" && arg !== "supplement" && arg !== "always") {
|
|
13008
|
+
return mk(`live Wikipedia supplement is ${stateWord(liveReference)} — /wiki on, /wiki off, /wiki supplement, or /wiki always. `
|
|
12659
13009
|
+ "When on, a question I can't answer also tries en.wikipedia.org (network); "
|
|
12660
|
-
+ "supplement adds a cited Wikipedia read-out under every grounded answer too
|
|
13010
|
+
+ "supplement adds a cited Wikipedia read-out under every grounded vocabulary answer too; "
|
|
13011
|
+
+ "always widens that to every grounded answer.");
|
|
12661
13012
|
}
|
|
12662
|
-
const next = arg === "supplement" ? "supplement" : arg === "on";
|
|
13013
|
+
const next = arg === "always" ? "always" : arg === "supplement" ? "supplement" : arg === "on";
|
|
12663
13014
|
return mk(`live Wikipedia supplement ${stateWord(next)}.`, { liveReferenceNext: next });
|
|
12664
13015
|
}
|
|
12665
13016
|
|
|
@@ -13747,7 +14098,7 @@ function vocabAntecedentFrom(last) {
|
|
|
13747
14098
|
return m[1];
|
|
13748
14099
|
}
|
|
13749
14100
|
|
|
13750
|
-
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, liveReference = false, onLiveLookup = null, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, gameConfig = null, _noSplit = false } = {}) {
|
|
14101
|
+
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, liveReference = false, onLiveLookup = null, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, gameConfig = null, uiContext = "cli", synthesisBudget = AUTO_SYNTHESIS_BUDGET, researchState = null, researchConfig = null, _noSplit = false } = {}) {
|
|
13751
14102
|
// Every game's tuning knobs (spider-fly's mass economy, guess-the-number's
|
|
13752
14103
|
// bounds, the shared plan lane's search-depth cap) — a caller's own
|
|
13753
14104
|
// gameConfig (chat-session.mjs resolves one per session from tmct.toml)
|
|
@@ -13812,7 +14163,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13812
14163
|
// the PLAN NEXT block below write planHolder.state; every other path leaves
|
|
13813
14164
|
// it untouched, and the caller re-threads whatever comes back.
|
|
13814
14165
|
const planHolder = { state: planState };
|
|
13815
|
-
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, liveReference, onLiveLookup, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache, vocabAntecedent, planHolder, gameConfig: resolvedGameConfig };
|
|
14166
|
+
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, liveReference, onLiveLookup, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache, vocabAntecedent, planHolder, gameConfig: resolvedGameConfig, uiContext, synthesisBudget };
|
|
13816
14167
|
// A DISPATCHED turn (count / slash-command / ask) becomes the new "last
|
|
13817
14168
|
// answer" that why/say-more re-renders; a conversational turn does not.
|
|
13818
14169
|
// Every dispatched turn's result passes through finish() here — the LAST
|
|
@@ -13950,6 +14301,47 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13950
14301
|
}
|
|
13951
14302
|
}
|
|
13952
14303
|
|
|
14304
|
+
// RESEARCH — "research <topic>[, limit N]" runs a Simple English Wikipedia
|
|
14305
|
+
// queue through the same ingest path a live-Wikipedia rescue uses: depth 0
|
|
14306
|
+
// now, the lead section's linked topics queued for "research next" (which
|
|
14307
|
+
// the web pages' auto-play button submits turn by turn). The explicit
|
|
14308
|
+
// request is the network consent for its own fetches — unlike the
|
|
14309
|
+
// clean-miss rescue, which fires on an ordinary question and so stays
|
|
14310
|
+
// behind /wiki on. Queue state threads turn-to-turn as researchState, the
|
|
14311
|
+
// same way planState does.
|
|
14312
|
+
{
|
|
14313
|
+
const researchHolder = { state: researchState };
|
|
14314
|
+
const resolvedResearchConfig = researchConfig ?? RESEARCH_DEFAULTS;
|
|
14315
|
+
const rTurn = await researchTurn(workingLine, {
|
|
14316
|
+
holder: researchHolder,
|
|
14317
|
+
memoryDir,
|
|
14318
|
+
lexicon,
|
|
14319
|
+
provider: getResearchProvider({ minIntervalMs: resolvedResearchConfig.minIntervalMs }),
|
|
14320
|
+
config: resolvedResearchConfig,
|
|
14321
|
+
planActive: Boolean(planHolder.state && !planHolder.state.done),
|
|
14322
|
+
pagerActive: Boolean(Array.isArray(last?.detail?.pending?.items) && last.detail.pending.items.length),
|
|
14323
|
+
notify: onLiveLookup,
|
|
14324
|
+
ingest: (key, article, tag) =>
|
|
14325
|
+
ingestReferenceArticle(memoryDir, key, article, factRowsCache, () => tag, lexicon, synthesisBudget),
|
|
14326
|
+
});
|
|
14327
|
+
if (rTurn) {
|
|
14328
|
+
note(trace, `lane: ${rTurn.note}`);
|
|
14329
|
+
note(trace, `goal: ${rTurn.goal}`);
|
|
14330
|
+
const result = plainTurn(workingLine, rTurn.text, { via: "research", miss: !!rTurn.miss, focus });
|
|
14331
|
+
result.lane = "research";
|
|
14332
|
+
const snapshot = researchSnapshot(researchHolder.state);
|
|
14333
|
+
if (snapshot) result.record.research = snapshot;
|
|
14334
|
+
const rec = withLast(result, rTurn.goal);
|
|
14335
|
+
rec.planState = planHolder.state;
|
|
14336
|
+
rec.researchState = researchHolder.state;
|
|
14337
|
+
// Present on EVERY research turn — null when the run ended or never
|
|
14338
|
+
// started — so a UI can tell "queue cleared" from "not a research
|
|
14339
|
+
// turn" (where the field is absent entirely).
|
|
14340
|
+
rec.research = snapshot;
|
|
14341
|
+
return rec;
|
|
14342
|
+
}
|
|
14343
|
+
}
|
|
14344
|
+
|
|
13953
14345
|
// Conversational layer next (greetings, thanks, help, bye, why/say-more) — these
|
|
13954
14346
|
// resolve no entity and carry their own preserved `last`. Bypasses withLast (a
|
|
13955
14347
|
// conversational turn is never finish()'d / never becomes a new `last`), so the
|