@polycode-projects/the-mechanical-code-talker 2.10.1 → 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.
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
{"id":"conversational-greeting-good-evening","class":"conversational","register":"friendly","template":"Good evening. Ask me about this codebase, or /help."}
|
|
61
61
|
{"id":"conversational-thanks","class":"conversational","register":"friendly","template":"Any time. Ask another, or /help for what I can do."}
|
|
62
62
|
{"id":"conversational-farewell","class":"conversational","register":"friendly","template":"Bye — flushing the session log. Come back with a question any time."}
|
|
63
|
+
{"id":"conversational-dismissal","class":"conversational","register":"friendly","template":"No worries. Ask another, or /help for what I can do."}
|
|
63
64
|
{"id":"orientation-friendly","class":"orientation","register":"friendly","template":"I'm tmct — a deterministic, offline code-graph assistant (no LLM). I answer questions about THIS codebase's structure — imports, calls, definitions,\nhistory and counts. For example:\n which modules import {example1}\n what calls {example2}\n how many classes are there\n/help for commands, /stats for an overview of the graph."}
|
|
64
65
|
{"id":"miss-no-previous-answer","class":"miss","register":"friendly","template":"No previous answer to expand yet — ask me a question first, then say \"why\" or \"say more\"."}
|
|
65
66
|
{"id":"conversational-greeting-empty","class":"conversational","register":"friendly","template":"Hi. I'm tmct. {vocabHint} Point me at a repo with `--repo <path>` for code-structure questions too (imports, calls, definitions). /help for commands."}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
|
package/src/domain/ask.mjs
CHANGED
|
@@ -896,7 +896,12 @@ function parseSuperlative(w, lc, nlp) {
|
|
|
896
896
|
}
|
|
897
897
|
const connectivity = lc.includes("connected") || lc.slice(extIdx, extIdx + 2).join(" ") === "most connected"
|
|
898
898
|
|| ["largest", "biggest", "smallest"].includes(lc[extIdx]);
|
|
899
|
-
|
|
899
|
+
// A bare importance superlative ("the most important file") names no explicit
|
|
900
|
+
// edge metric, so it ranks by total connectivity — the sum of an entity's
|
|
901
|
+
// in/out edges, the most defensible deterministic proxy for "important".
|
|
902
|
+
const IMPORTANCE_WORDS = ["important", "significant", "central", "key", "core", "essential", "critical", "principal"];
|
|
903
|
+
const importanceRanked = lc.some((x) => IMPORTANCE_WORDS.includes(x));
|
|
904
|
+
if (!metric && (connectivity || importanceRanked)) { metric = EDGE_NOUN_TO_METRIC.connections; metricNoun = "connections"; }
|
|
900
905
|
// entity noun anywhere (first match, deterministic); else default from a
|
|
901
906
|
// metric that implies exactly one entity class ("test(s)" always ranks
|
|
902
907
|
// Modules, the one declared exception — see METRIC_IMPLIES_ENTITY — so "what
|
|
@@ -4205,6 +4210,26 @@ export function ask(graph, query, { contextId = null, nlp = undefined, prev = nu
|
|
|
4205
4210
|
content = `${content}\n${lines.join("\n")}`;
|
|
4206
4211
|
}
|
|
4207
4212
|
}
|
|
4213
|
+
// NARROWING DISCLOSURE: the resolver picked ONE entity among several distinct
|
|
4214
|
+
// name-matches (a scored win, not a tie — a tie renders as branches above and
|
|
4215
|
+
// is excluded here). Whether the pick then produced an answer or an empty
|
|
4216
|
+
// result, disclose it and the count of the other matches in one line, so a
|
|
4217
|
+
// silent narrowing (a merged graph's src/store.mjs over src/core/store.mjs, a
|
|
4218
|
+
// directory term landing on one module, a wrong-case pick reporting no members)
|
|
4219
|
+
// is never mistaken for the only reading.
|
|
4220
|
+
if (!rendered.ambiguous && result.objMatch && Array.isArray(result.candidates) && result.candidates.length) {
|
|
4221
|
+
// A ranked shape (entry-point, superlative) carries its runners-up in
|
|
4222
|
+
// `matches` and discloses them its own way; a NAME-narrowing carries the
|
|
4223
|
+
// narrowed-away candidates OUTSIDE `matches`. Only the latter is disclosed.
|
|
4224
|
+
const matchIds = new Set((result.matches || []).map((m) => m && m.id));
|
|
4225
|
+
const others = result.candidates.filter((c) => c && c.id && c.id !== result.objMatch.id && !matchIds.has(c.id));
|
|
4226
|
+
if (others.length) {
|
|
4227
|
+
const shown = others.slice(0, 3).map((c) => c.label).filter(Boolean);
|
|
4228
|
+
const more = others.length - shown.length;
|
|
4229
|
+
const list = shown.join(", ") + (more > 0 ? `, +${more} more` : "");
|
|
4230
|
+
content = `${content}\n(answering for ${result.objMatch.label} — ${others.length} other match${others.length === 1 ? "" : "es"}: ${list})`;
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4208
4233
|
return {
|
|
4209
4234
|
content,
|
|
4210
4235
|
tmct_ask: {
|
package/src/services/chat.mjs
CHANGED
|
@@ -1069,7 +1069,7 @@ const CAPABILITY_PHRASES = [
|
|
|
1069
1069
|
// as new natural phrasings surface, never a general "any long question is
|
|
1070
1070
|
// an orientation request" rule.
|
|
1071
1071
|
/^(?:can you\s+)?walk me through (?:this|the)\s+(?:app|codebase|repo|repository|project|code)\??$/i,
|
|
1072
|
-
/^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,
|
|
1073
1073
|
/^(?:give me|what's) the lay of the land\??$/i,
|
|
1074
1074
|
// "what have we got here"/"what've we got here" — a casual, self-answering
|
|
1075
1075
|
// opener (matches after a leading "so" strips via LEADING_CONNECTIVE_RE,
|
|
@@ -1411,6 +1411,7 @@ const T_GREETING_BY_PHRASE = {
|
|
|
1411
1411
|
};
|
|
1412
1412
|
const T_THANKS = "conversational-thanks";
|
|
1413
1413
|
const T_FAREWELL = "conversational-farewell";
|
|
1414
|
+
const T_DISMISSAL = "conversational-dismissal";
|
|
1414
1415
|
const T_ORIENTATION = "orientation-friendly";
|
|
1415
1416
|
const T_WHY_EMPTY = "miss-no-previous-answer";
|
|
1416
1417
|
/** Empty / degenerate-graph variants (#3/#5): shown when the loaded graph has 0
|
|
@@ -1512,6 +1513,21 @@ const OK_ACK = new Set([
|
|
|
1512
1513
|
"ok", "okay", "cool", "aight", "fair enough", "got it", "gotcha", "noted",
|
|
1513
1514
|
"sounds good", "sure", "cool cool", "right",
|
|
1514
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
|
+
]);
|
|
1515
1531
|
/** New-user / confused openers — "I don't know what this is" reads as an
|
|
1516
1532
|
* orientation request, not small-talk and not a grammar-wall near-miss; routed
|
|
1517
1533
|
* the same as CAPABILITY_PHRASES (→ orientationAnswer). */
|
|
@@ -1603,9 +1619,21 @@ const CLOSING_FILLER_CLAUSES = new Set([
|
|
|
1603
1619
|
"that's everything i needed", "that's all i needed",
|
|
1604
1620
|
"that's everything for today", "that's all for today",
|
|
1605
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;
|
|
1606
1631
|
function farewellOrThanksSignal(raw, q) {
|
|
1607
1632
|
const words = q.split(/\s+/).filter(Boolean);
|
|
1608
|
-
|
|
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;
|
|
1609
1637
|
const clauses = conversationalClauses(q);
|
|
1610
1638
|
if (clauses.length < 2) return null; // single-clause lines: the exact whole-line checks own this
|
|
1611
1639
|
// OK_ACK is deliberately NOT a signal here (unlike the exact whole-line check
|
|
@@ -1627,16 +1655,39 @@ function farewellOrThanksSignal(raw, q) {
|
|
|
1627
1655
|
const ackMatch = rawClause.match(ACK_LEAD_RE);
|
|
1628
1656
|
const clause = ackMatch ? ackMatch[1].trim() : rawClause;
|
|
1629
1657
|
if (foldedBye(clause)) { byeHit = true; break; }
|
|
1630
|
-
const deIntensified = clause.replace(TRAILING_INTENSIFIER_RE, "").trim();
|
|
1658
|
+
const deIntensified = clause.replace(THANKS_HELP_TAIL_RE, "").replace(TRAILING_INTENSIFIER_RE, "").trim();
|
|
1631
1659
|
if (thanksClauseIdx < 0 && closedOrCollapsed(deIntensified, THANKS, THANKS_COLLAPSED)) thanksClauseIdx = i;
|
|
1632
1660
|
}
|
|
1633
1661
|
if (byeHit) return "bye";
|
|
1634
1662
|
const thanksHit = thanksClauseIdx >= 0 && clauses.every((c, i) => i === thanksClauseIdx
|
|
1635
|
-
||
|
|
1663
|
+
|| isClosingFillerClause(c)
|
|
1636
1664
|
|| (c.split(/\s+/).filter(Boolean).length <= 3 && !looksCodeish(c, c.toLowerCase())));
|
|
1637
1665
|
return thanksHit ? "thanks" : null;
|
|
1638
1666
|
}
|
|
1639
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
|
+
|
|
1640
1691
|
/** The fuzzy-typo fallback's candidate pool: every canonical phrase across the
|
|
1641
1692
|
* closed conversational sets, flattened once. Consulted only after every exact/
|
|
1642
1693
|
* collapsed lookup misses (see fuzzyConversationalMatch). */
|
|
@@ -1808,6 +1859,11 @@ function conversationalTurn(line, ctx) {
|
|
|
1808
1859
|
return mk(t(T_THANKS), { lane: "thanks" });
|
|
1809
1860
|
}
|
|
1810
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
|
+
}
|
|
1811
1867
|
if (aiIdentityMatch(raw)) {
|
|
1812
1868
|
note(ctx.trace, "goal: identity — is tmct an AI/LLM (a very likely first question)");
|
|
1813
1869
|
note(ctx.trace, "lane: conversational — identity/AI (AI_IDENTITY_PHRASES closed set)");
|
|
@@ -3004,15 +3060,36 @@ async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon
|
|
|
3004
3060
|
if (!memoryDir) return null;
|
|
3005
3061
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
3006
3062
|
if (!m) return null;
|
|
3007
|
-
const [, , subjectRaw, , objectRaw] = m;
|
|
3063
|
+
const [, det, subjectRaw, verb, objectRaw] = m;
|
|
3008
3064
|
if (PLACE_ADVERB_OBJECT_RE.test(objectRaw)) return null; // a place adverb is never a property
|
|
3009
|
-
const { loadLexicon, lookupNoun, classify } = await import("../domain/grammar/lexicon.mjs");
|
|
3065
|
+
const { loadLexicon, lookupNoun, lookupAdjective, classify } = await import("../domain/grammar/lexicon.mjs");
|
|
3010
3066
|
const lex = lexicon || loadLexicon();
|
|
3011
3067
|
// Y already a known NOUN or a fact-grounded CLASS term — a genuine class-
|
|
3012
3068
|
// membership sentence, unknownSubjectFallback/unknownObjectFallback's own
|
|
3013
3069
|
// territory (already had first refusal on it) — never misread as a property.
|
|
3014
3070
|
if (lookupNoun(lex, objectRaw) || GENERIC_ANCHOR_NOUNS.has(String(objectRaw).toLowerCase())
|
|
3015
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
|
+
}
|
|
3016
3093
|
// Subject-side groundedness — strip a leading "the"/"a"/"an" first
|
|
3017
3094
|
// (normFactTerm's own article-strip, mirrored here) so "the cache" checks
|
|
3018
3095
|
// groundedness under its real head noun "cache", the same spelling
|
|
@@ -4955,7 +5032,7 @@ const WHAT_KNOW_RE = /^(?:what\s+(?:do\s+you|d'?you)\s+know(?:\s+so\s+far)?|what
|
|
|
4955
5032
|
// do" needs the noun OPTIONAL after "this" (kept REQUIRED after "the") or it
|
|
4956
5033
|
// falls through to MODULE_ORIENT_RE, which fails to resolve "this" as an
|
|
4957
5034
|
// entity and hits the raw grammar wall.
|
|
4958
|
-
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))?)$/;
|
|
4959
5036
|
/** A bare "what is in here"/"what's in here"/"whats in here" — the SAME
|
|
4960
5037
|
* orientation intent as META_ORIENT_RE's own
|
|
4961
5038
|
* "what's in this repo"-shaped members, just phrased with the CONTEXT_WORDS
|
|
@@ -6940,6 +7017,20 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
6940
7017
|
replace: miss,
|
|
6941
7018
|
};
|
|
6942
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
|
+
}
|
|
6943
7034
|
return null;
|
|
6944
7035
|
}
|
|
6945
7036
|
// Bias only REORDERS — every hit still renders and is cited (Part 6's
|
|
@@ -8159,6 +8250,26 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
8159
8250
|
}
|
|
8160
8251
|
}
|
|
8161
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
|
+
|
|
8162
8273
|
// (a0.5) RECURSIVE-RULE REACHABILITY LIST — "list the <plural> of <X>": a
|
|
8163
8274
|
// genuine KIND-CHANGE from the yes/no dispatcher just above — REACHABILITY-SET
|
|
8164
8275
|
// enumeration (every node ever reached), not single-target search.
|
|
@@ -9612,6 +9723,13 @@ function relationDefinitions() {
|
|
|
9612
9723
|
* it — the fact-lookup path is a low-collision subject lookup, not a structural
|
|
9613
9724
|
* parse, so loosening it here is safe. */
|
|
9614
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;
|
|
9615
9733
|
|
|
9616
9734
|
/** The meta term a "what is a X" / "what is X" / "what does X mean" / "define X"
|
|
9617
9735
|
* question asks about — from the parse when present, else recognized directly
|
|
@@ -11508,8 +11626,14 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11508
11626
|
// above.
|
|
11509
11627
|
const capabilityAskShape = CAN_ASK_RE.test(gateQuery) || WHAT_CAN_DO_RE.test(gateQuery)
|
|
11510
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);
|
|
11511
11635
|
let bareMetaHit = null;
|
|
11512
|
-
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape || capabilityAskShape || bareNounShape)) {
|
|
11636
|
+
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape || capabilityAskShape || bareNounShape || whoIsShape)) {
|
|
11513
11637
|
if (memoryDir) {
|
|
11514
11638
|
// The bare noun asks its own "what is a X" — the readers never see the
|
|
11515
11639
|
// single word, so the vocabulary route is the constructed question's.
|
|
@@ -12878,6 +13002,10 @@ const GAME_OBS_LOWER_RE = /^(?:no[,\s]+)?(?:lower|too\s+high|too\s+big|smaller|l
|
|
|
12878
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;
|
|
12879
13003
|
const GAME_GUESS_RE = /^(?:is\s+it\s+)?(-?\d{1,12})\s*\??[.!?\s]*$/;
|
|
12880
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;
|
|
12881
13009
|
|
|
12882
13010
|
/** A natural-language plan frame — the shapes planLaneAnswer owns. Mid-game
|
|
12883
13011
|
* these get the one-at-a-time decline instead of clobbering the slot. */
|
|
@@ -12913,7 +13041,12 @@ function gameContinuationAnswer(line, game, planHolder) {
|
|
|
12913
13041
|
}
|
|
12914
13042
|
const higher = GAME_OBS_HIGHER_RE.test(line);
|
|
12915
13043
|
const lower = !higher && GAME_OBS_LOWER_RE.test(line);
|
|
12916
|
-
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
|
+
}
|
|
12917
13050
|
const prior = game.guess;
|
|
12918
13051
|
const next = { ...game };
|
|
12919
13052
|
if (higher) { next.lo = prior + 1; next.loSetBy = { guess: prior }; }
|
|
@@ -12956,6 +13089,9 @@ function gameContinuationAnswer(line, game, planHolder) {
|
|
|
12956
13089
|
: "you haven't guessed yet";
|
|
12957
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" };
|
|
12958
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
|
+
}
|
|
12959
13095
|
const m = String(line).trim().match(GAME_GUESS_RE);
|
|
12960
13096
|
if (!m) return null;
|
|
12961
13097
|
const guess = Number.parseInt(m[1], 10);
|
|
@@ -13107,6 +13243,52 @@ function rewriteNegativePolarityOpener(line) {
|
|
|
13107
13243
|
return null;
|
|
13108
13244
|
}
|
|
13109
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
|
+
|
|
13110
13292
|
/** A DISCONTIGUOUS verb frame, "SUBJECT uses OBJECT as its/a base(class)" —
|
|
13111
13293
|
* "uses" is split from its own qualifier ("as its base") around the object,
|
|
13112
13294
|
* so no contiguous phrase-table entry could ever register it, and "uses"
|
|
@@ -13238,7 +13420,8 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13238
13420
|
// — restored centrally inside withLast (below), once, for every dispatch path.
|
|
13239
13421
|
const indirectMatch = line.match(INDIRECT_REQUEST_RE);
|
|
13240
13422
|
const indirectLine = indirectMatch ? indirectMatch[1].trim() : line;
|
|
13241
|
-
const preRewriteLine =
|
|
13423
|
+
const preRewriteLine = rewriteEntryPointQuestion(indirectLine) || rewriteProveThat(indirectLine)
|
|
13424
|
+
|| rewriteVocabOpener(indirectLine) || indirectLine;
|
|
13242
13425
|
// rewriteUsesAsBaseFrame's discontiguous-frame rewrite: applied here, once,
|
|
13243
13426
|
// before ANY dispatch lane sees the text. Null (no-op) for every turn that
|
|
13244
13427
|
// doesn't match one of the four discontiguous shapes.
|
|
@@ -13256,7 +13439,8 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13256
13439
|
// question is answered by the possession readers instead of walling (the
|
|
13257
13440
|
// write boundary's own "?" gates already refuse to store it).
|
|
13258
13441
|
const eslRewrite = rewriteEslMissingDoes(cleftRewrite || frameLine)
|
|
13259
|
-
|| rewriteNegativePolarityOpener(cleftRewrite || frameLine)
|
|
13442
|
+
|| rewriteNegativePolarityOpener(cleftRewrite || frameLine)
|
|
13443
|
+
|| rewriteNegativeInterrogative(cleftRewrite || frameLine);
|
|
13260
13444
|
const cleftLine = eslRewrite || cleftRewrite || frameLine;
|
|
13261
13445
|
// VOCABULARY pronoun antecedent — "what is a dog" then "can it bark". The
|
|
13262
13446
|
// code-graph focus mechanism only ever binds {id,label} GRAPH entities, so
|
|
@@ -13291,13 +13475,16 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13291
13475
|
// captured from the PRE-narration finished result.
|
|
13292
13476
|
const withLast = (result, fallbackGoal = "unclear — no goal signal for this turn type") => {
|
|
13293
13477
|
const finished = attachDialogueAct(finish(result, { graph }), trace);
|
|
13294
|
-
//
|
|
13295
|
-
// indirect-request wrapper
|
|
13296
|
-
//
|
|
13297
|
-
//
|
|
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.
|
|
13298
13486
|
if (indirectMatch || baseFrameRewrite || vocabAntecedent || eslRewrite) {
|
|
13299
13487
|
if (finished.record) finished.record.query = line;
|
|
13300
|
-
if (Array.isArray(finished.logLines) && finished.logLines.length > 1) finished.logLines[1] = `> ${line}`;
|
|
13301
13488
|
}
|
|
13302
13489
|
// The VERBATIM user line rides every turn record as `input`, beside
|
|
13303
13490
|
// whatever `query` the dispatch path recorded — the session history must
|
|
@@ -13504,7 +13691,15 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13504
13691
|
const endsInPlanTrigger = PLAN_SOLVE_RE.test(lastSentence) || GOAL_TEACH_RE.test(lastSentence)
|
|
13505
13692
|
|| GOAL_TEACH_INFINITIVE_RE.test(lastSentence) || GOAL_TEACH_VERBLESS_RE.test(lastSentence)
|
|
13506
13693
|
|| LEGAL_MOVES_RE.test(lastSentence);
|
|
13507
|
-
|
|
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)) {
|
|
13508
13703
|
let f = focus; let l = last; let ps = planHolder.state;
|
|
13509
13704
|
const receipts = [];
|
|
13510
13705
|
let finalRec = null;
|
|
@@ -13526,7 +13721,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13526
13721
|
// a stray "Goal (inferred)" line the bulleted ones already dropped. Its
|
|
13527
13722
|
// goal-line tail (everything after the receipt's first line) is kept once.
|
|
13528
13723
|
let answer;
|
|
13529
|
-
if (
|
|
13724
|
+
if (finalIsPayload) {
|
|
13530
13725
|
const receiptLines = receipts.slice(0, -1).map((t) => `• ${t}`).join("\n");
|
|
13531
13726
|
answer = receiptLines ? `${receiptLines}\n\n${finalRec.answer}` : finalRec.answer;
|
|
13532
13727
|
} else {
|
|
@@ -13538,6 +13733,12 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13538
13733
|
combined.planState = ps;
|
|
13539
13734
|
combined.focus = f;
|
|
13540
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 };
|
|
13541
13742
|
return combined;
|
|
13542
13743
|
}
|
|
13543
13744
|
}
|