@polycode-projects/the-mechanical-code-talker 2.10.0 → 2.10.2
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 +42 -4
- package/bin/tmct.mjs +16 -1
- package/data/templates/responses.jsonl +1 -0
- package/package.json +6 -1
- package/src/adapters/memory/export-jsonl.mjs +38 -0
- package/src/domain/ask.mjs +27 -2
- package/src/domain/cli-verbs.mjs +2 -1
- package/src/domain/code-explorer-hints.mjs +176 -0
- package/src/services/adventure-viz.mjs +29 -21
- package/src/services/chat-page-viz.mjs +39 -1
- package/src/services/chat.mjs +436 -26
- package/src/services/code-explorer-viz.mjs +343 -0
- package/src/services/import-file.mjs +69 -7
- package/src/services/spider-fly-viz.mjs +59 -3
- package/src/services/spider-fly.mjs +5 -2
- package/src/surfaces/web/chat-browser-entry.mjs +12 -1
- package/src/surfaces/web/code-explorer-browser-entry.mjs +79 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +119 -112
- package/src/tools/definitions.mjs +7 -0
- package/src/tools/handlers/index.mjs +2 -0
- package/src/tools/handlers/tmct-export.mjs +26 -0
package/src/services/chat.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { join, dirname } from "node:path";
|
|
|
21
21
|
import { dispatchTool, loadGraph, TOOLS } from "../tools/server.mjs";
|
|
22
22
|
import { ToolError } from "../adapters/config.mjs";
|
|
23
23
|
import { parseEntities, edgesOfKind, moduleCountOf, renderAuthorCard, renderAuthorTouches, renderCommitAuthor, resolveSymbol, renderCompare } from "../domain/codegraph.mjs";
|
|
24
|
-
import { classDisplayName } from "../domain/ask.mjs";
|
|
24
|
+
import { classDisplayName, DYNAMIC_TAIL_OK_RE } from "../domain/ask.mjs";
|
|
25
25
|
import { uuidv7 } from "../adapters/uuid.mjs";
|
|
26
26
|
import * as defaultSource from "../adapters/source.mjs";
|
|
27
27
|
import { loadTemplates, render as renderTemplate } from "../adapters/corpus/templates.mjs";
|
|
@@ -811,7 +811,7 @@ async function answerMemoryCount(memoryDir, query) {
|
|
|
811
811
|
// the bare "how many do you know" (no explicit noun) defaults to remembered facts
|
|
812
812
|
if (/\bhow many(?:\s+(?:things?|facts?))?\s+(?:do|d'?)\s+(?:you|u)\s+know\b/.test(q)) cls = "Fact";
|
|
813
813
|
if (!cls) {
|
|
814
|
-
const m = q.match(/\b(?:how many|number of|count(?:\s+the)?)\s+([a-z]+)\b(.*)$/);
|
|
814
|
+
const m = q.match(/\b(?:how many|number of|count(?:\s+the)?)\s+(?:all\s+)?([a-z]+)\b(.*)$/);
|
|
815
815
|
if (m) {
|
|
816
816
|
cls = MEMORY_COUNT_NOUNS[m[1]] || null;
|
|
817
817
|
tail = m[2].trim();
|
|
@@ -844,6 +844,160 @@ async function answerMemoryCount(memoryDir, query) {
|
|
|
844
844
|
return said((mem.individuals || []).filter((i) => (i.class || "") === cls).length);
|
|
845
845
|
}
|
|
846
846
|
|
|
847
|
+
// ---- memory-store LIST + meta-class count ("list facts", "list utterances",
|
|
848
|
+
// "how many sessions are there") — the same reified individuals answerMemoryCount
|
|
849
|
+
// tallies, but enumerated, and reaching the meta-classes (Session/Source/Rule) the
|
|
850
|
+
// count lane skips. dynamicClassQuery (ask.mjs) already answers these when handed a
|
|
851
|
+
// memory-shaped graph, but the chat path hands ask() the CODE graph, so the store's
|
|
852
|
+
// own individuals were never reachable from a chat turn. This reads the store
|
|
853
|
+
// directly, mirroring answerMemoryCount's own lazy/failure-tolerated load. ----
|
|
854
|
+
|
|
855
|
+
/** Chat-phrasing nouns → the memory-store class they name. Fact/Utterance are
|
|
856
|
+
* shared with answerMemoryCount (which owns their counts); the meta-classes are
|
|
857
|
+
* reachable only here. */
|
|
858
|
+
const MEMORY_CLASS_QUERY_NOUNS = {
|
|
859
|
+
fact: "Fact", facts: "Fact",
|
|
860
|
+
utterance: "Utterance", utterances: "Utterance",
|
|
861
|
+
session: "Session", sessions: "Session",
|
|
862
|
+
source: "Source", sources: "Source",
|
|
863
|
+
rule: "Rule", rules: "Rule",
|
|
864
|
+
};
|
|
865
|
+
const MEMORY_CLASS_PLURALS = {
|
|
866
|
+
Fact: "facts", Utterance: "utterances", Session: "sessions", Source: "sources", Rule: "rules",
|
|
867
|
+
};
|
|
868
|
+
const MEMORY_CLASS_LIST_TRIGGER_RE = /^(?:list|show(?:\s+me)?)\s+(?:all\s+|the\s+)?([a-z][a-z-]*)\s*(.*)$/i;
|
|
869
|
+
const MEMORY_CLASS_COUNT_TRIGGER_RE = /^(?:how\s+many|number\s+of|count(?:\s+the)?)\s+(?:all\s+)?([a-z][a-z-]*)\s*(.*)$/i;
|
|
870
|
+
|
|
871
|
+
/** One display line per stored individual of a class: a Fact reads back through
|
|
872
|
+
* the same renderFactLine every other fact list uses; the other classes show
|
|
873
|
+
* their own label. */
|
|
874
|
+
function memoryClassLine(cls, ind, factByLabel) {
|
|
875
|
+
if (cls === "Fact") {
|
|
876
|
+
const row = factByLabel.get(ind.id);
|
|
877
|
+
if (row) return renderFactLine(row);
|
|
878
|
+
}
|
|
879
|
+
return String(ind.label || ind.id || "").trim();
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Recognise "list <memory-class>" (any class) and "how many <meta-class>"
|
|
883
|
+
* (Session/Source/Rule — Fact/Utterance counts stay with answerMemoryCount) and
|
|
884
|
+
* answer off the store. Returns { text, pending } or null (→ the next lane owns
|
|
885
|
+
* it). A real restrictor tail declines rather than answering a shorter question
|
|
886
|
+
* nobody asked. */
|
|
887
|
+
async function answerMemoryClassQuery(memoryDir, query) {
|
|
888
|
+
if (!memoryDir) return null;
|
|
889
|
+
const q = String(query).trim();
|
|
890
|
+
const listM = q.match(MEMORY_CLASS_LIST_TRIGGER_RE);
|
|
891
|
+
const countM = listM ? null : q.match(MEMORY_CLASS_COUNT_TRIGGER_RE);
|
|
892
|
+
const m = listM || countM;
|
|
893
|
+
if (!m) return null;
|
|
894
|
+
const cls = MEMORY_CLASS_QUERY_NOUNS[m[1].toLowerCase()];
|
|
895
|
+
if (!cls) return null;
|
|
896
|
+
// Fact/Utterance counts carry answerMemoryCount's own about-tail discipline;
|
|
897
|
+
// never re-answer them from this simpler lane.
|
|
898
|
+
if (countM && (cls === "Fact" || cls === "Utterance")) return null;
|
|
899
|
+
const plural = MEMORY_CLASS_PLURALS[cls];
|
|
900
|
+
const tail = (m[2] || "").trim();
|
|
901
|
+
if (!DYNAMIC_TAIL_OK_RE.test(tail)) {
|
|
902
|
+
const verb = listM ? "list" : "count";
|
|
903
|
+
return {
|
|
904
|
+
text: `I can ${verb} the ${plural} I hold, but not the "${tail}" part of that question — `
|
|
905
|
+
+ `so I won't answer as if you hadn't asked it. `
|
|
906
|
+
+ `Ask "${verb} ${plural}" for all of them.`,
|
|
907
|
+
miss: true,
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
let loadMemory;
|
|
911
|
+
let readFactRows;
|
|
912
|
+
try { ({ loadMemory, readFactRows } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
913
|
+
let mem;
|
|
914
|
+
try { mem = await loadMemory(memoryDir); } catch { return null; }
|
|
915
|
+
const inds = (mem.individuals || []).filter((i) => (i.class || "") === cls);
|
|
916
|
+
if (countM) return { text: `${inds.length} ${inds.length === 1 ? plural.replace(/s$/, "") : plural}.` };
|
|
917
|
+
if (!inds.length) return { text: `I don't have any ${plural} stored yet.`, miss: true };
|
|
918
|
+
const factByLabel = cls === "Fact" ? new Map(readFactRows(mem).map((r) => [r.id, r])) : new Map();
|
|
919
|
+
const lines = inds.map((ind) => memoryClassLine(cls, ind, factByLabel));
|
|
920
|
+
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
921
|
+
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
922
|
+
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
923
|
+
return {
|
|
924
|
+
text: shown.join("\n") + extra,
|
|
925
|
+
...(rest.length ? { pending: { items: rest, noun: plural } } : {}),
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// "how many animals are there" — a real count of a TAUGHT class's members
|
|
930
|
+
// (every "X is a kind of animal" fact), distinct from answerQuantifierRecall's
|
|
931
|
+
// literal quantifier lookup. Placed ahead of it in runTurn: HOW_MANY_ARE_RE
|
|
932
|
+
// reads "there" as a second noun and answers "I was never told a quantifier",
|
|
933
|
+
// stealing the phrasing before a member count ever runs.
|
|
934
|
+
const TAUGHT_CLASS_COUNT_RE = /^how\s+many\s+([a-z][\w-]*)\s*(.*)$/i;
|
|
935
|
+
|
|
936
|
+
/** Count the taught members of a class named by a plain noun ("how many animals
|
|
937
|
+
* are there" → every "X is a kind of animal"). Declines (null) for a real
|
|
938
|
+
* code-countable class (answerCount owns it) or a class nothing was taught
|
|
939
|
+
* about, so structural counts and the quantifier lane are unaffected. */
|
|
940
|
+
async function answerTaughtClassCount(memoryDir, query, biasByBundle = {}, cache = null) {
|
|
941
|
+
if (!memoryDir) return null;
|
|
942
|
+
const m = String(query).trim().match(TAUGHT_CLASS_COUNT_RE);
|
|
943
|
+
if (!m) return null;
|
|
944
|
+
if (!DYNAMIC_TAIL_OK_RE.test((m[2] || "").trim())) return null;
|
|
945
|
+
const asked = m[1].toLowerCase();
|
|
946
|
+
if (COUNT_NOUNS[asked]) return null; // a real graph-countable class — answerCount owns it
|
|
947
|
+
let normFactTerm;
|
|
948
|
+
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
949
|
+
const rows = await factRows(memoryDir, cache);
|
|
950
|
+
const isa = rows.filter((f) => ISA_PREDICATES.has(f.predicate));
|
|
951
|
+
const variants = factTermVariants(normFactTerm, asked);
|
|
952
|
+
const members = rankByBiasThenTrust(isa.filter((f) => variants.has(f.object)), biasByBundle);
|
|
953
|
+
if (!members.length) return null; // nothing taught under this class name — later lanes own it
|
|
954
|
+
// A member whose SUBJECT is itself a countable graph class ("every class is a
|
|
955
|
+
// component") is an asserted-vocabulary cardinality, not a member enumeration —
|
|
956
|
+
// countFromFacts counts the real class, so defer to it rather than tallying the
|
|
957
|
+
// one class-level fact.
|
|
958
|
+
if (members.some((f) => COUNT_NOUNS[String(f.subject).toLowerCase()])) return null;
|
|
959
|
+
return `${members.length} ${members.length === 1 ? asked.replace(/s$/, "") : asked}.`;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// "list all animals" / "list the animals" — enumerate a taught class's members,
|
|
963
|
+
// with its OWN trigger rather than the "what is an animal" definition lane's
|
|
964
|
+
// leftovers: at scale the definition lane fills its cap with forward corpus facts
|
|
965
|
+
// before the reverse-membership listing ever shows, and the conversational
|
|
966
|
+
// orientation lane claims the bare "list …" phrasing before factReadBack runs.
|
|
967
|
+
const MEMBERSHIP_LIST_RE = /^(?:list|show(?:\s+me)?)\s+(?:all\s+|the\s+)?([a-z][\w-]*)\s*(.*)$/i;
|
|
968
|
+
|
|
969
|
+
/** List the taught members of a class named by a plain noun ("list all animals"
|
|
970
|
+
* → every "X is a kind of animal"). Declines (null) for a code-countable class
|
|
971
|
+
* or a class nothing was taught about; declines with a message for a real
|
|
972
|
+
* restrictor tail rather than answering as if it weren't there. */
|
|
973
|
+
async function answerMembershipList(memoryDir, query, biasByBundle = {}, cache = null) {
|
|
974
|
+
if (!memoryDir) return null;
|
|
975
|
+
const m = String(query).trim().match(MEMBERSHIP_LIST_RE);
|
|
976
|
+
if (!m) return null;
|
|
977
|
+
const asked = m[1].toLowerCase();
|
|
978
|
+
if (COUNT_NOUNS[asked]) return null; // a real graph-countable class — the code list lane owns it
|
|
979
|
+
const tail = (m[2] || "").trim();
|
|
980
|
+
let normFactTerm;
|
|
981
|
+
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
982
|
+
const rows = await factRows(memoryDir, cache);
|
|
983
|
+
const isa = rows.filter((f) => ISA_PREDICATES.has(f.predicate));
|
|
984
|
+
const variants = factTermVariants(normFactTerm, asked);
|
|
985
|
+
const members = rankByBiasThenTrust(isa.filter((f) => variants.has(f.object)), biasByBundle);
|
|
986
|
+
if (!members.length) return null; // nothing taught under this class name — later lanes own it
|
|
987
|
+
if (!DYNAMIC_TAIL_OK_RE.test(tail)) {
|
|
988
|
+
return {
|
|
989
|
+
text: `I can list the ${asked}, but not the "${tail}" part of that question — `
|
|
990
|
+
+ `so I won't answer as if you hadn't asked it. Ask "list ${asked}" for all of them.`,
|
|
991
|
+
miss: true,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
const lines = members.map(renderFactLine);
|
|
995
|
+
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
996
|
+
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
997
|
+
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
998
|
+
return { text: shown.join("\n") + extra, ...(rest.length ? { pending: { items: rest, noun: asked } } : {}) };
|
|
999
|
+
}
|
|
1000
|
+
|
|
847
1001
|
/** `/stats`: a one-screen overview of the graph — class counts, relationship
|
|
848
1002
|
* (predicate) counts, and module/package totals — read straight off the header. */
|
|
849
1003
|
export function renderStats(graph) {
|
|
@@ -915,7 +1069,7 @@ const CAPABILITY_PHRASES = [
|
|
|
915
1069
|
// as new natural phrasings surface, never a general "any long question is
|
|
916
1070
|
// an orientation request" rule.
|
|
917
1071
|
/^(?:can you\s+)?walk me through (?:this|the)\s+(?:app|codebase|repo|repository|project|code)\??$/i,
|
|
918
|
-
/^what(?:'s|s|\s+is) the big picture(?:\s+here)?\??$/i,
|
|
1072
|
+
/^(?:what(?:'s|s|\s+is)|give me|show me|gimme) the big picture(?:\s+(?:here|(?:on|of|for|about)\s+(?:this|the)\s+(?:app|codebase|repo|repository|project|code)))?\??$/i,
|
|
919
1073
|
/^(?:give me|what's) the lay of the land\??$/i,
|
|
920
1074
|
// "what have we got here"/"what've we got here" — a casual, self-answering
|
|
921
1075
|
// opener (matches after a leading "so" strips via LEADING_CONNECTIVE_RE,
|
|
@@ -1257,6 +1411,7 @@ const T_GREETING_BY_PHRASE = {
|
|
|
1257
1411
|
};
|
|
1258
1412
|
const T_THANKS = "conversational-thanks";
|
|
1259
1413
|
const T_FAREWELL = "conversational-farewell";
|
|
1414
|
+
const T_DISMISSAL = "conversational-dismissal";
|
|
1260
1415
|
const T_ORIENTATION = "orientation-friendly";
|
|
1261
1416
|
const T_WHY_EMPTY = "miss-no-previous-answer";
|
|
1262
1417
|
/** Empty / degenerate-graph variants (#3/#5): shown when the loaded graph has 0
|
|
@@ -1358,6 +1513,21 @@ const OK_ACK = new Set([
|
|
|
1358
1513
|
"ok", "okay", "cool", "aight", "fair enough", "got it", "gotcha", "noted",
|
|
1359
1514
|
"sounds good", "sure", "cool cool", "right",
|
|
1360
1515
|
]);
|
|
1516
|
+
/** Dismissals — "drop it, no question here" beats. Routed to a warm dismissal
|
|
1517
|
+
* template, never the identity/orientation blurb (which reads like the tool
|
|
1518
|
+
* didn't understand the user was bowing out). Single-word entries also match as
|
|
1519
|
+
* tokens inside a short mixed line ("ok nvm"); multi-word entries match whole. */
|
|
1520
|
+
const DISMISSAL = new Set([
|
|
1521
|
+
"nvm", "nevermind", "never mind", "nm", "forget it", "forget that",
|
|
1522
|
+
"no worries", "no worry", "skip it", "leave it", "don't worry", "dont worry",
|
|
1523
|
+
"no biggie", "it's fine", "its fine", "never mind then", "nvm then",
|
|
1524
|
+
]);
|
|
1525
|
+
/** Laughter beats — on their own, or leading/trailing a dismissal/ack ("lol ok",
|
|
1526
|
+
* "haha nvm"), they carry no graph intent. */
|
|
1527
|
+
const LAUGHTER = new Set([
|
|
1528
|
+
"lol", "lolol", "lmao", "lmfao", "rofl", "haha", "hahaha", "hah",
|
|
1529
|
+
"heh", "hehe", "ha", "hehehe",
|
|
1530
|
+
]);
|
|
1361
1531
|
/** New-user / confused openers — "I don't know what this is" reads as an
|
|
1362
1532
|
* orientation request, not small-talk and not a grammar-wall near-miss; routed
|
|
1363
1533
|
* the same as CAPABILITY_PHRASES (→ orientationAnswer). */
|
|
@@ -1449,9 +1619,21 @@ const CLOSING_FILLER_CLAUSES = new Set([
|
|
|
1449
1619
|
"that's everything i needed", "that's all i needed",
|
|
1450
1620
|
"that's everything for today", "that's all for today",
|
|
1451
1621
|
]);
|
|
1622
|
+
/** Strip a hedging lead ("i think that's everything for today" → "that's
|
|
1623
|
+
* everything for today") so a hedged closing clause still matches the closed
|
|
1624
|
+
* set above — the hedge is register, not new content. */
|
|
1625
|
+
const CLOSING_HEDGE_RE = /^i (?:think|reckon|guess|believe|suppose|figure) /;
|
|
1626
|
+
const isClosingFillerClause = (c) => CLOSING_FILLER_CLAUSES.has(c) || CLOSING_FILLER_CLAUSES.has(c.replace(CLOSING_HEDGE_RE, ""));
|
|
1627
|
+
/** A thanks clause's optional "for … help" tail ("thanks so much for the help",
|
|
1628
|
+
* "thanks for all your help") — stripped before the closed THANKS lookup so the
|
|
1629
|
+
* bare "thanks" underneath matches. */
|
|
1630
|
+
const THANKS_HELP_TAIL_RE = /\s+for\s+(?:the\s+|your\s+|all\s+|all\s+the\s+|all\s+your\s+)?help\s*$/i;
|
|
1452
1631
|
function farewellOrThanksSignal(raw, q) {
|
|
1453
1632
|
const words = q.split(/\s+/).filter(Boolean);
|
|
1454
|
-
|
|
1633
|
+
// The upper bound is generous because the real safety is the per-clause gate
|
|
1634
|
+
// below (every non-thanks clause must itself be small-talk-shaped or a curated
|
|
1635
|
+
// closing-filler clause), not the total word count.
|
|
1636
|
+
if (words.length < 2 || words.length > 16 || looksCodeish(raw, q)) return null;
|
|
1455
1637
|
const clauses = conversationalClauses(q);
|
|
1456
1638
|
if (clauses.length < 2) return null; // single-clause lines: the exact whole-line checks own this
|
|
1457
1639
|
// OK_ACK is deliberately NOT a signal here (unlike the exact whole-line check
|
|
@@ -1473,16 +1655,39 @@ function farewellOrThanksSignal(raw, q) {
|
|
|
1473
1655
|
const ackMatch = rawClause.match(ACK_LEAD_RE);
|
|
1474
1656
|
const clause = ackMatch ? ackMatch[1].trim() : rawClause;
|
|
1475
1657
|
if (foldedBye(clause)) { byeHit = true; break; }
|
|
1476
|
-
const deIntensified = clause.replace(TRAILING_INTENSIFIER_RE, "").trim();
|
|
1658
|
+
const deIntensified = clause.replace(THANKS_HELP_TAIL_RE, "").replace(TRAILING_INTENSIFIER_RE, "").trim();
|
|
1477
1659
|
if (thanksClauseIdx < 0 && closedOrCollapsed(deIntensified, THANKS, THANKS_COLLAPSED)) thanksClauseIdx = i;
|
|
1478
1660
|
}
|
|
1479
1661
|
if (byeHit) return "bye";
|
|
1480
1662
|
const thanksHit = thanksClauseIdx >= 0 && clauses.every((c, i) => i === thanksClauseIdx
|
|
1481
|
-
||
|
|
1663
|
+
|| isClosingFillerClause(c)
|
|
1482
1664
|
|| (c.split(/\s+/).filter(Boolean).length <= 3 && !looksCodeish(c, c.toLowerCase())));
|
|
1483
1665
|
return thanksHit ? "thanks" : null;
|
|
1484
1666
|
}
|
|
1485
1667
|
|
|
1668
|
+
/** A dismissal / laughter beat ("nvm", "lol ok", "haha never mind"): the whole
|
|
1669
|
+
* line, an ack lead-in peeled off a dismissal, or a short line whose every word
|
|
1670
|
+
* is laughter / an ack / a single-word dismissal with at least one laughter or
|
|
1671
|
+
* dismissal word (so a bare "ok"/"sure" still falls to the ack lane, not here).
|
|
1672
|
+
* Never fires on a codeish line. */
|
|
1673
|
+
function dismissalSignal(q) {
|
|
1674
|
+
if (looksCodeish(q, q)) return false;
|
|
1675
|
+
if (DISMISSAL.has(q) || LAUGHTER.has(q)) return true;
|
|
1676
|
+
const words = q.split(/\s+/).filter(Boolean);
|
|
1677
|
+
if (words.length < 2 || words.length > 5) return false;
|
|
1678
|
+
const isFluff = (w) => LAUGHTER.has(w) || OK_ACK.has(w);
|
|
1679
|
+
let lo = 0;
|
|
1680
|
+
let hi = words.length;
|
|
1681
|
+
let laughed = false;
|
|
1682
|
+
while (lo < hi && isFluff(words[lo])) { if (LAUGHTER.has(words[lo])) laughed = true; lo += 1; }
|
|
1683
|
+
while (hi > lo && isFluff(words[hi - 1])) { if (LAUGHTER.has(words[hi - 1])) laughed = true; hi -= 1; }
|
|
1684
|
+
const core = words.slice(lo, hi).join(" ");
|
|
1685
|
+
// Pure laughter+ack ("lol ok") is a dismissal only when a laughter beat was
|
|
1686
|
+
// present — a bare stack of acks ("ok cool") still falls to the ack lane.
|
|
1687
|
+
if (core === "") return laughed;
|
|
1688
|
+
return DISMISSAL.has(core);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1486
1691
|
/** The fuzzy-typo fallback's candidate pool: every canonical phrase across the
|
|
1487
1692
|
* closed conversational sets, flattened once. Consulted only after every exact/
|
|
1488
1693
|
* collapsed lookup misses (see fuzzyConversationalMatch). */
|
|
@@ -1654,6 +1859,11 @@ function conversationalTurn(line, ctx) {
|
|
|
1654
1859
|
return mk(t(T_THANKS), { lane: "thanks" });
|
|
1655
1860
|
}
|
|
1656
1861
|
}
|
|
1862
|
+
if (dismissalSignal(q)) {
|
|
1863
|
+
note(ctx.trace, "goal: casual/social — dismissal/laughter, no graph intent");
|
|
1864
|
+
note(ctx.trace, "lane: conversational — dismissal (DISMISSAL/LAUGHTER closed set)");
|
|
1865
|
+
return mk(t(T_DISMISSAL), { lane: "thanks" });
|
|
1866
|
+
}
|
|
1657
1867
|
if (aiIdentityMatch(raw)) {
|
|
1658
1868
|
note(ctx.trace, "goal: identity — is tmct an AI/LLM (a very likely first question)");
|
|
1659
1869
|
note(ctx.trace, "lane: conversational — identity/AI (AI_IDENTITY_PHRASES closed set)");
|
|
@@ -2850,15 +3060,36 @@ async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon
|
|
|
2850
3060
|
if (!memoryDir) return null;
|
|
2851
3061
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
2852
3062
|
if (!m) return null;
|
|
2853
|
-
const [, , subjectRaw, , objectRaw] = m;
|
|
3063
|
+
const [, det, subjectRaw, verb, objectRaw] = m;
|
|
2854
3064
|
if (PLACE_ADVERB_OBJECT_RE.test(objectRaw)) return null; // a place adverb is never a property
|
|
2855
|
-
const { loadLexicon, lookupNoun, classify } = await import("../domain/grammar/lexicon.mjs");
|
|
3065
|
+
const { loadLexicon, lookupNoun, lookupAdjective, classify } = await import("../domain/grammar/lexicon.mjs");
|
|
2856
3066
|
const lex = lexicon || loadLexicon();
|
|
2857
3067
|
// Y already a known NOUN or a fact-grounded CLASS term — a genuine class-
|
|
2858
3068
|
// membership sentence, unknownSubjectFallback/unknownObjectFallback's own
|
|
2859
3069
|
// territory (already had first refusal on it) — never misread as a property.
|
|
2860
3070
|
if (lookupNoun(lex, objectRaw) || GENERIC_ANCHOR_NOUNS.has(String(objectRaw).toLowerCase())
|
|
2861
3071
|
|| (await isGroundedByFact(objectRaw, memoryDir, cache))) return null;
|
|
3072
|
+
// CLASS-LEVEL adjective predication — "every snake is venomous": a universal
|
|
3073
|
+
// quantifier over a grounded noun class, with an adjective complement. The
|
|
3074
|
+
// quantifier is the same deliberate-generalization signal the article/
|
|
3075
|
+
// capitalization stand-ins give for the specific-entity form below, so a
|
|
3076
|
+
// bare-lexicon-grounded subject qualifies here (it would not for the
|
|
3077
|
+
// unquantified property claim), and the fact is stored WITH its "every"
|
|
3078
|
+
// quantifier so the read-back ("is a snake venomous", "are snakes venomous")
|
|
3079
|
+
// holds for the whole class. The adjective is confirmed by the static lexicon
|
|
3080
|
+
// or wink's POS tag (the same tag unknownObjectFallback used to defer here);
|
|
3081
|
+
// a noun-shaped Y was already minted as a class upstream and never reaches
|
|
3082
|
+
// this point.
|
|
3083
|
+
const universalQuantifier = /^(?:every|each|all|any)$/i.test((det || "").trim());
|
|
3084
|
+
if (universalQuantifier && (await isGroundedTerm(subjectRaw, lex, memoryDir, cache))
|
|
3085
|
+
&& (lookupAdjective(lex, objectRaw) || (await objectReadsAsNonNoun(objectRaw)))) {
|
|
3086
|
+
const classSubject = /^are$/i.test(verb)
|
|
3087
|
+
? (lookupNoun(lex, subjectRaw)?.lemma || singularizeSurface(subjectRaw))
|
|
3088
|
+
: subjectRaw;
|
|
3089
|
+
return teachFact(memoryDir, sessionId, {
|
|
3090
|
+
subject: classSubject, predicate: HAS_PROPERTY_PREDICATE, object: objectRaw, quantifier: "every",
|
|
3091
|
+
});
|
|
3092
|
+
}
|
|
2862
3093
|
// Subject-side groundedness — strip a leading "the"/"a"/"an" first
|
|
2863
3094
|
// (normFactTerm's own article-strip, mirrored here) so "the cache" checks
|
|
2864
3095
|
// groundedness under its real head noun "cache", the same spelling
|
|
@@ -2975,6 +3206,13 @@ const quantifiedHasSubject = (m) => (/^all$/i.test(m[1]) ? singularizeSurface(m[
|
|
|
2975
3206
|
const quantifiedHasObject = (m) => (/^all$/i.test(m[1])
|
|
2976
3207
|
? m[3].replace(/[\w'-]+$/, (w) => singularizeSurface(w))
|
|
2977
3208
|
: m[3]);
|
|
3209
|
+
/** The determiner-led possession teach ("the tower has 3 disks", "the robot has
|
|
3210
|
+
* 2 arms", "my car has 4 wheels") — the closed has/have verb pins the split the
|
|
3211
|
+
* same way the universal quantifier pins QUANTIFIED_HAS_TEACH_RE's, so a leading
|
|
3212
|
+
* definite/possessive determiner needs no verb-position guessing. The subject is
|
|
3213
|
+
* the single noun between the determiner and the verb; a two-token subject stays
|
|
3214
|
+
* declined, like the preposition-pinned frame, because nothing names its head. */
|
|
3215
|
+
const DETERMINER_HAS_TEACH_RE = /^(?:the|an?|my|your|our|their|his|her|its)\s+([\w'-]+)\s+(?:has|have|had)\s+(.+?)[.!?]*$/i;
|
|
2978
3216
|
/** Verbs owned by an earlier, more specific recognizer in this lane — is/are
|
|
2979
3217
|
* (class-membership/property, above) and owns/maintains (ownership, above).
|
|
2980
3218
|
* generalVerbTeach declines outright on these so it can never race a more
|
|
@@ -3142,10 +3380,15 @@ async function generalVerbTeach(payload) {
|
|
|
3142
3380
|
// declines here exactly as it always has.
|
|
3143
3381
|
if (GENERAL_VERB_DETERMINER_RE.test(subjectRaw)) {
|
|
3144
3382
|
const quantHas = p.match(QUANTIFIED_HAS_TEACH_RE);
|
|
3383
|
+
const detHas = !quantHas ? p.match(DETERMINER_HAS_TEACH_RE) : null;
|
|
3145
3384
|
if (quantHas) {
|
|
3146
3385
|
subjectRaw = quantifiedHasSubject(quantHas);
|
|
3147
3386
|
verbRaw = "has";
|
|
3148
3387
|
objectRaw = quantifiedHasObject(quantHas);
|
|
3388
|
+
} else if (detHas) {
|
|
3389
|
+
subjectRaw = detHas[1];
|
|
3390
|
+
verbRaw = "has";
|
|
3391
|
+
objectRaw = detHas[2];
|
|
3149
3392
|
} else {
|
|
3150
3393
|
const det = p.match(GENERAL_VERB_DETERMINER_TEACH_RE);
|
|
3151
3394
|
if (!det) return null; // not a bare-name subject, and no preposition to pin the verb
|
|
@@ -4579,17 +4822,20 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4579
4822
|
// of THAT subject — the same word generalVerbTeach will store — and leave
|
|
4580
4823
|
// every other sentence reading its first word exactly as before.
|
|
4581
4824
|
const detLed = raw.match(GENERAL_VERB_DETERMINER_TEACH_RE);
|
|
4582
|
-
const
|
|
4825
|
+
const detHasLed = detLed ? null : raw.match(DETERMINER_HAS_TEACH_RE);
|
|
4826
|
+
const quantHasLed = (detLed || detHasLed) ? null : raw.match(QUANTIFIED_HAS_TEACH_RE);
|
|
4583
4827
|
const subjectWord = detLed ? detLed[1].split(/\s+/).pop()
|
|
4584
|
-
: (
|
|
4828
|
+
: (detHasLed ? detHasLed[1]
|
|
4829
|
+
: (quantHasLed ? quantifiedHasSubject(quantHasLed) : raw.match(/^([\w'-]+)/)?.[1]));
|
|
4585
4830
|
// The quantifier lead ("every … has …") is itself a strong declarative
|
|
4586
4831
|
// signal, so it overrides the single-token POS gate: a noun that doubles
|
|
4587
4832
|
// as a verb ("every overbid has a gouger" — wink tags "overbid" VERB)
|
|
4588
|
-
// used to be a SILENT no-op and a later miss.
|
|
4589
|
-
//
|
|
4590
|
-
//
|
|
4833
|
+
// used to be a SILENT no-op and a later miss. A determiner-led possession
|
|
4834
|
+
// ("the tower has 3 disks") pins the same way, so it gets the same override.
|
|
4835
|
+
// NON_DECLARATIVE_OPENER_RE runs even for these leads — "every umm has a
|
|
4836
|
+
// thing" isn't a real quantified sentence, just filler that fits the shape.
|
|
4591
4837
|
if (subjectWord && !NON_DECLARATIVE_OPENER_RE.test(subjectWord)
|
|
4592
|
-
&& (quantHasLed || (await subjectIsNounOrPropn(subjectWord)))) {
|
|
4838
|
+
&& (quantHasLed || detHasLed || (await subjectIsNounOrPropn(subjectWord)))) {
|
|
4593
4839
|
// A PLURAL explicit-capability surface ("wrens can hum") whose
|
|
4594
4840
|
// SINGULAR is a grounded term stores under the singular first — the
|
|
4595
4841
|
// spelling the grounding fact and every query-side variant fold use —
|
|
@@ -4786,7 +5032,7 @@ const WHAT_KNOW_RE = /^(?:what\s+(?:do\s+you|d'?you)\s+know(?:\s+so\s+far)?|what
|
|
|
4786
5032
|
// do" needs the noun OPTIONAL after "this" (kept REQUIRED after "the") or it
|
|
4787
5033
|
// falls through to MODULE_ORIENT_RE, which fails to resolve "this" as an
|
|
4788
5034
|
// entity and hits the raw grammar wall.
|
|
4789
|
-
const META_ORIENT_RE = /^(?:what(?:'s| is| are)?\s+this(?:\s+(?:app|codebase|repo|repository|project|code|thing))?|what\s+(?:codebase|repo|repository|project)\s+is\s+this|what\s+does\s+this(?:\s+(?:app|code|codebase|project|repo))?\s+do|what\s+does\s+the\s+(?:app|code|codebase|project|repo)\s+do|what\s+is\s+(?:this|the)\s+app(?:\s+for)?|what\s+am\s+i\s+looking\s+at|what\s+is\s+tmct|how\s+do\s+i\s+(?:start|begin|get\s+started|get\s+going|load\s+(?:my\s+)?code|index\s+(?:my\s+)?(?:code|repo|repository)|use\s+(?:this|you|tmct))|where\s+do\s+i\s+(?:start|begin)|what\s+should\s+i\s+(?:read|look\s+at)\s+first(?:\s+to\s+understand\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?|where\s+should\s+i\s+start\s+reading(?:\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?|where\s+do\s+i\s+begin\s+reading(?:\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?)$/;
|
|
5035
|
+
const META_ORIENT_RE = /^(?:what(?:'s| is| are)?\s+this(?:\s+(?:app|codebase|repo|repository|project|code|thing))?|what\s+(?:codebase|repo|repository|project)\s+is\s+this|what\s+does\s+this(?:\s+(?:app|code|codebase|project|repo))?\s+do|what\s+does\s+the\s+(?:app|code|codebase|project|repo)\s+do|what\s+is\s+(?:this|the)\s+app(?:\s+for)?|what\s+am\s+i\s+looking\s+at|what\s+is\s+tmct|how\s+do\s+i\s+(?:start|begin|get\s+started|get\s+going|load\s+(?:my\s+)?code|index\s+(?:my\s+)?(?:code|repo|repository)|use\s+(?:this|you|tmct))|where\s+do\s+i\s+(?:start|begin)(?:\s+reading(?:\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?)?|what\s+should\s+i\s+(?:read|look\s+at)\s+first(?:\s+to\s+understand\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?|where\s+should\s+i\s+start\s+reading(?:\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?|where\s+do\s+i\s+begin\s+reading(?:\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?)$/;
|
|
4790
5036
|
/** A bare "what is in here"/"what's in here"/"whats in here" — the SAME
|
|
4791
5037
|
* orientation intent as META_ORIENT_RE's own
|
|
4792
5038
|
* "what's in this repo"-shaped members, just phrased with the CONTEXT_WORDS
|
|
@@ -6771,6 +7017,20 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
6771
7017
|
replace: miss,
|
|
6772
7018
|
};
|
|
6773
7019
|
}
|
|
7020
|
+
// The term names nothing as a fact SUBJECT, but may exist only as the
|
|
7021
|
+
// OBJECT of taught relations ("ahab is the father of ishmael" → "what is
|
|
7022
|
+
// ishmael"): surface those reverse relations rather than missing, the same
|
|
7023
|
+
// facts "what do you know about X" would list.
|
|
7024
|
+
if (!predicate) {
|
|
7025
|
+
const objectHits = rankByBiasThenTrust((await factRows(memoryDir, cache)).filter((f) => variants.has(f.object)), biasByBundle);
|
|
7026
|
+
if (objectHits.length) {
|
|
7027
|
+
const objLines = objectHits.map(renderFactLine);
|
|
7028
|
+
const objShown = objLines.slice(0, FACT_ANSWER_CAP);
|
|
7029
|
+
const objRest = objLines.slice(FACT_ANSWER_CAP);
|
|
7030
|
+
const objExtra = objRest.length ? `\n…and ${objRest.length} more — say 'more' to see them.` : "";
|
|
7031
|
+
return { text: objShown.join("\n") + objExtra, replace: miss, ...(objRest.length ? { pending: { items: objRest, noun: "facts" } } : {}) };
|
|
7032
|
+
}
|
|
7033
|
+
}
|
|
6774
7034
|
return null;
|
|
6775
7035
|
}
|
|
6776
7036
|
// Bias only REORDERS — every hit still renders and is cited (Part 6's
|
|
@@ -7990,6 +8250,26 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7990
8250
|
}
|
|
7991
8251
|
}
|
|
7992
8252
|
|
|
8253
|
+
// Bare "who is/was <name>" with no relational "of Y" tail or genitive (those
|
|
8254
|
+
// are the whoAsk reader's above) — surface every taught fact naming the
|
|
8255
|
+
// person, whether as the subject or only as a relation OBJECT ("ahab is the
|
|
8256
|
+
// father of ishmael" → "who is ishmael"). A name with no stored fact falls
|
|
8257
|
+
// through unchanged.
|
|
8258
|
+
{
|
|
8259
|
+
const whoBare = qHedge.match(WHO_IS_BARE_RE);
|
|
8260
|
+
if (whoBare) {
|
|
8261
|
+
const nameVariants = factTermVariants(normFactTerm, whoBare[1]);
|
|
8262
|
+
const hits = rankByBiasThenTrust(rows.filter((f) => nameVariants.has(f.subject) || nameVariants.has(f.object)), biasByBundle);
|
|
8263
|
+
if (hits.length) {
|
|
8264
|
+
const lines = hits.map(renderFactLine);
|
|
8265
|
+
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
8266
|
+
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
8267
|
+
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
8268
|
+
return { text: shown.join("\n") + extra, replace: true, ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}) };
|
|
8269
|
+
}
|
|
8270
|
+
}
|
|
8271
|
+
}
|
|
8272
|
+
|
|
7993
8273
|
// (a0.5) RECURSIVE-RULE REACHABILITY LIST — "list the <plural> of <X>": a
|
|
7994
8274
|
// genuine KIND-CHANGE from the yes/no dispatcher just above — REACHABILITY-SET
|
|
7995
8275
|
// enumeration (every node ever reached), not single-target search.
|
|
@@ -9443,6 +9723,13 @@ function relationDefinitions() {
|
|
|
9443
9723
|
* it — the fact-lookup path is a low-collision subject lookup, not a structural
|
|
9444
9724
|
* parse, so loosening it here is safe. */
|
|
9445
9725
|
const BARE_WHATIS_RE = /^what\s+(?:is|are)\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
9726
|
+
/** A bare "who is/was <name>" with no relational tail ("of Y") or genitive
|
|
9727
|
+
* ("Y's role") — those keep their own specific who-readers. This single-token
|
|
9728
|
+
* form is armed into the meta-term fact lane only on a would-miss, and only
|
|
9729
|
+
* surfaces an answer when memory actually holds facts about the name (as a
|
|
9730
|
+
* subject or a relation object); with no such facts it returns null and the
|
|
9731
|
+
* turn falls through to the author/relation who-readers unchanged. */
|
|
9732
|
+
const WHO_IS_BARE_RE = /^who\s+(?:is|are|was|were)\s+(?:an?\s+|the\s+)?([\w'-]+)[?.!\s]*$/i;
|
|
9446
9733
|
|
|
9447
9734
|
/** The meta term a "what is a X" / "what is X" / "what does X mean" / "define X"
|
|
9448
9735
|
* question asks about — from the parse when present, else recognized directly
|
|
@@ -11339,8 +11626,14 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11339
11626
|
// above.
|
|
11340
11627
|
const capabilityAskShape = CAN_ASK_RE.test(gateQuery) || WHAT_CAN_DO_RE.test(gateQuery)
|
|
11341
11628
|
|| DO_VERB_ASK_RE.test(gateQuery) || WHICH_KIND_CAN_RE.test(gateQuery) || WHAT_CAN_VERB_RE.test(gateQuery);
|
|
11629
|
+
// A bare "who is/was <name>" (no relational tail) is as short as the
|
|
11630
|
+
// vocabulary openers above and trips isConversational's word-count catch-all
|
|
11631
|
+
// the same way — factReadBack's bare-who reader surfaces the person's stored
|
|
11632
|
+
// relations only on a real hit, so a name with no facts still falls to the
|
|
11633
|
+
// ordinary card.
|
|
11634
|
+
const whoIsShape = WHO_IS_BARE_RE.test(gateQuery);
|
|
11342
11635
|
let bareMetaHit = null;
|
|
11343
|
-
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape || capabilityAskShape || bareNounShape)) {
|
|
11636
|
+
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape || capabilityAskShape || bareNounShape || whoIsShape)) {
|
|
11344
11637
|
if (memoryDir) {
|
|
11345
11638
|
// The bare noun asks its own "what is a X" — the readers never see the
|
|
11346
11639
|
// single word, so the vocabulary route is the constructed question's.
|
|
@@ -12709,6 +13002,10 @@ const GAME_OBS_LOWER_RE = /^(?:no[,\s]+)?(?:lower|too\s+high|too\s+big|smaller|l
|
|
|
12709
13002
|
const GAME_OBS_CORRECT_RE = /^(?:yes|yep|yeah|correct|you\s+got\s+it|you\s+guessed\s+it|that(?:'s|\s+is)\s+it|that(?:'s|\s+is)\s+right|got\s+it|spot\s+on)[.!?\s]*$/i;
|
|
12710
13003
|
const GAME_GUESS_RE = /^(?:is\s+it\s+)?(-?\d{1,12})\s*\??[.!?\s]*$/;
|
|
12711
13004
|
const GAME_FALSE_CORRECT_RE = /^(?:but\s+)?you\s+(?:already\s+)?said\s+(?:it\s+was\s+)?(?:correct|right)\b/i;
|
|
13005
|
+
// Thinking-aloud / hesitation fillers — a closed set (never a real question or a
|
|
13006
|
+
// graph query, which stay free to fall through to the normal lanes) that mid-game
|
|
13007
|
+
// coaches back toward a valid move instead of hitting a bare parse wall.
|
|
13008
|
+
const GAME_HESITATION_RE = /^(?:um+|uh+|erm+|hmm*|(?:hmm*,?\s+)?let me (?:think|see)(?:\s+about\s+(?:it|this))?|thinking|(?:just\s+)?(?:give me|gimme)\s+(?:a\s+)?(?:sec|second|minute|moment)|one\s+sec|hold\s+on|hang\s+on|not\s+sure|no\s+idea|i\s+dunno|dunno|idk|i\s+don'?t\s+know|i'?m\s+not\s+sure|good\s+question)[.!?\s]*$/i;
|
|
12712
13009
|
|
|
12713
13010
|
/** A natural-language plan frame — the shapes planLaneAnswer owns. Mid-game
|
|
12714
13011
|
* these get the one-at-a-time decline instead of clobbering the slot. */
|
|
@@ -12744,7 +13041,12 @@ function gameContinuationAnswer(line, game, planHolder) {
|
|
|
12744
13041
|
}
|
|
12745
13042
|
const higher = GAME_OBS_HIGHER_RE.test(line);
|
|
12746
13043
|
const lower = !higher && GAME_OBS_LOWER_RE.test(line);
|
|
12747
|
-
if (!higher && !lower)
|
|
13044
|
+
if (!higher && !lower) {
|
|
13045
|
+
if (GAME_HESITATION_RE.test(String(line).trim())) {
|
|
13046
|
+
return { text: `take your time — my guess is still ${game.guess} (between ${game.lo} and ${game.hi}). Say higher, lower, or correct.`, goal: gameGoal(game), lane: "game-inform", note: "GAME — a hesitation filler mid-game; re-stated the standing guess without folding an observation" };
|
|
13047
|
+
}
|
|
13048
|
+
return null;
|
|
13049
|
+
}
|
|
12748
13050
|
const prior = game.guess;
|
|
12749
13051
|
const next = { ...game };
|
|
12750
13052
|
if (higher) { next.lo = prior + 1; next.loSetBy = { guess: prior }; }
|
|
@@ -12787,6 +13089,9 @@ function gameContinuationAnswer(line, game, planHolder) {
|
|
|
12787
13089
|
: "you haven't guessed yet";
|
|
12788
13090
|
return { text: `I haven't said "correct" yet — ${record}. Keep guessing.`, goal: gameGoal(game), lane: "game-answer", note: "GAME — rebutted a false \"you said correct\" from the game's own hint record" };
|
|
12789
13091
|
}
|
|
13092
|
+
if (GAME_HESITATION_RE.test(String(line).trim())) {
|
|
13093
|
+
return { text: `no rush — give me a number between ${game.lo0} and ${game.hi0}, or "I give up" to stop.`, goal: gameGoal(game), lane: "game-inform", note: "GAME — a hesitation filler mid-game; coached back to a valid guess without touching the secret" };
|
|
13094
|
+
}
|
|
12790
13095
|
const m = String(line).trim().match(GAME_GUESS_RE);
|
|
12791
13096
|
if (!m) return null;
|
|
12792
13097
|
const guess = Number.parseInt(m[1], 10);
|
|
@@ -12938,6 +13243,52 @@ function rewriteNegativePolarityOpener(line) {
|
|
|
12938
13243
|
return null;
|
|
12939
13244
|
}
|
|
12940
13245
|
|
|
13246
|
+
/** A CONTRACTED NEGATIVE INTERROGATIVE — "isn't a dog an animal?", "doesn't
|
|
13247
|
+
* store.mjs import config?": a confirmation-seeking question whose expected
|
|
13248
|
+
* answer is the positive yes/no. Folded to the plain positive interrogative the
|
|
13249
|
+
* isa/relation readers already answer, so it is ANSWERED rather than walling at
|
|
13250
|
+
* the grammar boundary or reading as a first-person declarative. A trailing "?"
|
|
13251
|
+
* is required — the whole negative-question signal — so a leading-"don't"
|
|
13252
|
+
* imperative ("don't show me tests") is never rewritten into a positive. */
|
|
13253
|
+
const NEG_CONTRACTION_LEAD = {
|
|
13254
|
+
"isn't": "is", "isnt": "is", "aren't": "are", "arent": "are",
|
|
13255
|
+
"wasn't": "was", "wasnt": "was", "weren't": "were", "werent": "were",
|
|
13256
|
+
"doesn't": "does", "doesnt": "does", "don't": "do", "dont": "do",
|
|
13257
|
+
"didn't": "did", "didnt": "did", "can't": "can", "cant": "can",
|
|
13258
|
+
"couldn't": "could", "couldnt": "could", "won't": "will", "wont": "will",
|
|
13259
|
+
"wouldn't": "would", "wouldnt": "would", "hasn't": "has", "hasnt": "has",
|
|
13260
|
+
"haven't": "have", "havent": "have", "hadn't": "had", "hadnt": "had",
|
|
13261
|
+
"shouldn't": "should", "shouldnt": "should",
|
|
13262
|
+
};
|
|
13263
|
+
function rewriteNegativeInterrogative(line) {
|
|
13264
|
+
const s = String(line || "").trim();
|
|
13265
|
+
if (!/\?\s*$/.test(s)) return null;
|
|
13266
|
+
const m = s.replace(/[?.!\s]+$/, "").match(/^(\S+)\s+(.+)$/);
|
|
13267
|
+
if (!m) return null;
|
|
13268
|
+
const positive = NEG_CONTRACTION_LEAD[m[1].toLowerCase()];
|
|
13269
|
+
if (!positive) return null;
|
|
13270
|
+
return `${positive} ${m[2].trim()}`;
|
|
13271
|
+
}
|
|
13272
|
+
|
|
13273
|
+
/** "what is the entry point" / "what's the main entry point of this codebase" /
|
|
13274
|
+
* "which file is the entry point" — the definition/which-file phrasings of the
|
|
13275
|
+
* entry-point question, folded onto the "where is the entry point" surface the
|
|
13276
|
+
* ask engine's own entry-point ranker (ask.mjs ENTRY_POINT_QUERY_RE) already
|
|
13277
|
+
* answers. Without this fold they parse as a vocabulary "what is X" miss. */
|
|
13278
|
+
const ENTRY_POINT_WHATIS_RE = /^(?:what(?:'s|s|\s+is)|which\s+(?:module|file|one)(?:\s+is)?)\s+(?:the\s+)?(?:main\s+|primary\s+)?entry[\s-]?points?(?:\s+(?:of|to|for)\s+(?:this|the)\s+(?:codebase|code|repo|repository|project|app))?[?.!\s]*$/i;
|
|
13279
|
+
const rewriteEntryPointQuestion = (line) => (ENTRY_POINT_WHATIS_RE.test(String(line || "").trim()) ? "where is the entry point" : null);
|
|
13280
|
+
|
|
13281
|
+
/** "prove that X is a Y" / "prove X is Y" — a request for the isa yes/no with
|
|
13282
|
+
* its proof chain, folded onto the "is X a Y" surface the isa reader already
|
|
13283
|
+
* answers with a cited chain. Only the copula form folds; other "prove …"
|
|
13284
|
+
* phrasings fall through to their ordinary handling / honest miss. */
|
|
13285
|
+
const PROVE_THAT_RE = /^prove\s+(?:to\s+me\s+)?(?:that\s+)?(.+?)\s+(is|are)\s+(.+?)[?.!\s]*$/i;
|
|
13286
|
+
function rewriteProveThat(line) {
|
|
13287
|
+
const m = String(line || "").trim().match(PROVE_THAT_RE);
|
|
13288
|
+
if (!m) return null;
|
|
13289
|
+
return `${m[2]} ${m[1].trim()} ${m[3].trim()}`;
|
|
13290
|
+
}
|
|
13291
|
+
|
|
12941
13292
|
/** A DISCONTIGUOUS verb frame, "SUBJECT uses OBJECT as its/a base(class)" —
|
|
12942
13293
|
* "uses" is split from its own qualifier ("as its base") around the object,
|
|
12943
13294
|
* so no contiguous phrase-table entry could ever register it, and "uses"
|
|
@@ -13069,7 +13420,8 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13069
13420
|
// — restored centrally inside withLast (below), once, for every dispatch path.
|
|
13070
13421
|
const indirectMatch = line.match(INDIRECT_REQUEST_RE);
|
|
13071
13422
|
const indirectLine = indirectMatch ? indirectMatch[1].trim() : line;
|
|
13072
|
-
const preRewriteLine =
|
|
13423
|
+
const preRewriteLine = rewriteEntryPointQuestion(indirectLine) || rewriteProveThat(indirectLine)
|
|
13424
|
+
|| rewriteVocabOpener(indirectLine) || indirectLine;
|
|
13073
13425
|
// rewriteUsesAsBaseFrame's discontiguous-frame rewrite: applied here, once,
|
|
13074
13426
|
// before ANY dispatch lane sees the text. Null (no-op) for every turn that
|
|
13075
13427
|
// doesn't match one of the four discontiguous shapes.
|
|
@@ -13087,7 +13439,8 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13087
13439
|
// question is answered by the possession readers instead of walling (the
|
|
13088
13440
|
// write boundary's own "?" gates already refuse to store it).
|
|
13089
13441
|
const eslRewrite = rewriteEslMissingDoes(cleftRewrite || frameLine)
|
|
13090
|
-
|| rewriteNegativePolarityOpener(cleftRewrite || frameLine)
|
|
13442
|
+
|| rewriteNegativePolarityOpener(cleftRewrite || frameLine)
|
|
13443
|
+
|| rewriteNegativeInterrogative(cleftRewrite || frameLine);
|
|
13091
13444
|
const cleftLine = eslRewrite || cleftRewrite || frameLine;
|
|
13092
13445
|
// VOCABULARY pronoun antecedent — "what is a dog" then "can it bark". The
|
|
13093
13446
|
// code-graph focus mechanism only ever binds {id,label} GRAPH entities, so
|
|
@@ -13122,13 +13475,16 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13122
13475
|
// captured from the PRE-narration finished result.
|
|
13123
13476
|
const withLast = (result, fallbackGoal = "unclear — no goal signal for this turn type") => {
|
|
13124
13477
|
const finished = attachDialogueAct(finish(result, { graph }), trace);
|
|
13125
|
-
//
|
|
13126
|
-
// indirect-request wrapper
|
|
13127
|
-
//
|
|
13128
|
-
//
|
|
13478
|
+
// The logged transcript echo is ALWAYS the verbatim user line — no dispatch
|
|
13479
|
+
// path's internal rewrite (the indirect-request wrapper, the vocab-opener /
|
|
13480
|
+
// cleft / ESL rewrites, a discourse substitution) may leak into what the
|
|
13481
|
+
// .log shows the user typed.
|
|
13482
|
+
if (Array.isArray(finished.logLines) && finished.logLines.length > 1) finished.logLines[1] = `> ${line}`;
|
|
13483
|
+
// record.query keeps its narrower restoration for the wrapper/rewrite frames
|
|
13484
|
+
// the ask engine records off `workingLine`; the .jsonl sidecar also carries
|
|
13485
|
+
// the verbatim line as `input`, below.
|
|
13129
13486
|
if (indirectMatch || baseFrameRewrite || vocabAntecedent || eslRewrite) {
|
|
13130
13487
|
if (finished.record) finished.record.query = line;
|
|
13131
|
-
if (Array.isArray(finished.logLines) && finished.logLines.length > 1) finished.logLines[1] = `> ${line}`;
|
|
13132
13488
|
}
|
|
13133
13489
|
// The VERBATIM user line rides every turn record as `input`, beside
|
|
13134
13490
|
// whatever `query` the dispatch path recorded — the session history must
|
|
@@ -13335,7 +13691,15 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13335
13691
|
const endsInPlanTrigger = PLAN_SOLVE_RE.test(lastSentence) || GOAL_TEACH_RE.test(lastSentence)
|
|
13336
13692
|
|| GOAL_TEACH_INFINITIVE_RE.test(lastSentence) || GOAL_TEACH_VERBLESS_RE.test(lastSentence)
|
|
13337
13693
|
|| LEGAL_MOVES_RE.test(lastSentence);
|
|
13338
|
-
|
|
13694
|
+
// The syllogism one-liner — "Every man is mortal. Socrates is a man. Is
|
|
13695
|
+
// Socrates mortal?": every sentence but the last teaches on its own, and
|
|
13696
|
+
// the last is a question. Each teach stores (in order, so the question
|
|
13697
|
+
// sees them), then the final sentence is answered as the payload behind
|
|
13698
|
+
// the teach receipts, the same rendering the plan-trigger case uses.
|
|
13699
|
+
const teachesThenAsks = !endsInPlanTrigger && /\?\s*$/.test(lastSentence.trim())
|
|
13700
|
+
&& await everySentenceTeaches(sentences.slice(0, -1), lexicon);
|
|
13701
|
+
const finalIsPayload = endsInPlanTrigger || teachesThenAsks;
|
|
13702
|
+
if (finalIsPayload || await everySentenceTeaches(sentences, lexicon)) {
|
|
13339
13703
|
let f = focus; let l = last; let ps = planHolder.state;
|
|
13340
13704
|
const receipts = [];
|
|
13341
13705
|
let finalRec = null;
|
|
@@ -13357,7 +13721,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13357
13721
|
// a stray "Goal (inferred)" line the bulleted ones already dropped. Its
|
|
13358
13722
|
// goal-line tail (everything after the receipt's first line) is kept once.
|
|
13359
13723
|
let answer;
|
|
13360
|
-
if (
|
|
13724
|
+
if (finalIsPayload) {
|
|
13361
13725
|
const receiptLines = receipts.slice(0, -1).map((t) => `• ${t}`).join("\n");
|
|
13362
13726
|
answer = receiptLines ? `${receiptLines}\n\n${finalRec.answer}` : finalRec.answer;
|
|
13363
13727
|
} else {
|
|
@@ -13369,6 +13733,12 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13369
13733
|
combined.planState = ps;
|
|
13370
13734
|
combined.focus = f;
|
|
13371
13735
|
combined.last = l;
|
|
13736
|
+
// Each per-sentence turn recorded only its OWN sentence; the transcript
|
|
13737
|
+
// echo and the turn record must quote the whole multi-sentence line the
|
|
13738
|
+
// user actually typed, not just its last sentence.
|
|
13739
|
+
const ts0 = Array.isArray(finalRec.logLines) && finalRec.logLines.length ? finalRec.logLines[0] : new Date().toISOString();
|
|
13740
|
+
combined.logLines = [ts0, `> ${line}`, answer, ""];
|
|
13741
|
+
if (finalRec.record) combined.record = { ...finalRec.record, query: line, input: line };
|
|
13372
13742
|
return combined;
|
|
13373
13743
|
}
|
|
13374
13744
|
}
|
|
@@ -13429,6 +13799,46 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13429
13799
|
return withLast(plainTurn(workingLine, memCount, { via: "count", focus }), "get a count of a memory-store kind");
|
|
13430
13800
|
}
|
|
13431
13801
|
}
|
|
13802
|
+
// "list facts"/"list utterances"/"how many sessions are there" — enumerate the
|
|
13803
|
+
// stored individuals answerMemoryCount only tallies, and reach the meta-classes
|
|
13804
|
+
// (Session/Source/Rule) it skips. Placed here so ask()'s CODE-graph lanes never
|
|
13805
|
+
// steal the phrasing; declines cleanly for a code-graph noun or a real restrictor.
|
|
13806
|
+
if (memoryDir) {
|
|
13807
|
+
const memClass = await answerMemoryClassQuery(memoryDir, workingLine);
|
|
13808
|
+
if (memClass != null) {
|
|
13809
|
+
const goal = "list or count a memory-store kind (facts/utterances/sessions/sources/rules)";
|
|
13810
|
+
note(trace, `goal: ${goal}`);
|
|
13811
|
+
note(trace, "lane: answerMemoryClassQuery — matched a memory-store class noun, answered off the .tmct/memory store's own individuals");
|
|
13812
|
+
const turn = plainTurn(workingLine, memClass.text, { via: memClass.miss ? "miss" : "fact", miss: !!memClass.miss, focus });
|
|
13813
|
+
if (memClass.pending) turn.detail = { traversal: null, matches: [], pending: memClass.pending };
|
|
13814
|
+
return withLast(turn, goal);
|
|
13815
|
+
}
|
|
13816
|
+
}
|
|
13817
|
+
// "how many animals are there" — count a taught class's members, ahead of the
|
|
13818
|
+
// quantifier lane (which reads "there" as a second noun and answers "I was never
|
|
13819
|
+
// told a quantifier" for the exact same phrasing).
|
|
13820
|
+
if (memoryDir) {
|
|
13821
|
+
const taughtCount = await answerTaughtClassCount(memoryDir, workingLine, biasByBundle, factRowsCache);
|
|
13822
|
+
if (taughtCount != null) {
|
|
13823
|
+
note(trace, 'goal: count the taught members of a class ("how many animals are there")');
|
|
13824
|
+
note(trace, "lane: answerTaughtClassCount — matched a plain-noun count over taught isa-facts whose OBJECT is that class");
|
|
13825
|
+
return withLast(plainTurn(workingLine, taughtCount, { via: "count", focus }), "count a taught class's members");
|
|
13826
|
+
}
|
|
13827
|
+
}
|
|
13828
|
+
// "list all animals"/"list the animals" — enumerate a taught class's members
|
|
13829
|
+
// from its own trigger, ahead of the conversational orientation lane that would
|
|
13830
|
+
// otherwise claim the bare "list …" phrasing.
|
|
13831
|
+
if (memoryDir) {
|
|
13832
|
+
const memberList = await answerMembershipList(memoryDir, workingLine, biasByBundle, factRowsCache);
|
|
13833
|
+
if (memberList != null) {
|
|
13834
|
+
const goal = "list the taught members of a class";
|
|
13835
|
+
note(trace, `goal: ${goal}`);
|
|
13836
|
+
note(trace, "lane: answerMembershipList — matched a bare 'list <noun>' over taught isa-facts whose OBJECT is that class");
|
|
13837
|
+
const turn = plainTurn(workingLine, memberList.text, { via: memberList.miss ? "miss" : "fact", miss: !!memberList.miss, focus });
|
|
13838
|
+
if (memberList.pending) turn.detail = { traversal: null, matches: [], pending: memberList.pending };
|
|
13839
|
+
return withLast(turn, goal);
|
|
13840
|
+
}
|
|
13841
|
+
}
|
|
13432
13842
|
// "how many Xs are Ys" — a taught-quantifier RECALL, checked explicitly
|
|
13433
13843
|
// ahead of answerCount. Its own authority gate declines for anything
|
|
13434
13844
|
// answerCount should own, so ordinary structural counts are unaffected.
|