@polycode-projects/the-mechanical-code-talker 2.10.1 → 2.10.3
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/data/templates/responses.jsonl +1 -0
- package/package.json +1 -1
- package/src/domain/ask.mjs +26 -1
- package/src/domain/domain.mjs +49 -21
- package/src/services/adventure.mjs +73 -1
- package/src/services/chat.mjs +283 -26
- package/src/services/spider-fly-turn.mjs +65 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +113 -108
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)");
|
|
@@ -2499,6 +2555,10 @@ const GOAL_CONJUNCT_RE = new RegExp(
|
|
|
2499
2555
|
// three only REPORT.
|
|
2500
2556
|
const PLAN_WHAT_NEXT_RE = /^(?:what(?:'s|\s+is)?|whats)\s+the\s+next\s+move[?.!\s]*$/i;
|
|
2501
2557
|
const PLAN_MOVE_COUNT_RE = /^how\s+many\s+moves(?:\s+(?:are\s+(?:there|left)|remain(?:ing)?|left|to\s+go|in\s+the\s+plan|total))?[?.!\s]*$/i;
|
|
2558
|
+
// "what is the goal" while a goal is held — a read-back off planState, so a
|
|
2559
|
+
// mid-plan aside never falls to the child-pack lane and answers from corpus
|
|
2560
|
+
// vocabulary about the word "goal".
|
|
2561
|
+
const PLAN_GOAL_READBACK_RE = /^(?:what(?:'s|\s+is)\s+(?:the\s+|my\s+)?goal|remind\s+me\s+(?:of\s+|what\s+)?the\s+goal(?:\s+is)?|what\s+am\s+i\s+solving\s+for|what\s+goal(?:'s|\s+is)\s+(?:set|held))[?.!\s]*$/i;
|
|
2502
2562
|
// "is that really the minimum number of moves?" / "could there be a shorter
|
|
2503
2563
|
// plan than that?" — a confirmation of the planner's own optimality claim,
|
|
2504
2564
|
// not a request to count anything (without this it fell to the unrelated
|
|
@@ -3004,15 +3064,36 @@ async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon
|
|
|
3004
3064
|
if (!memoryDir) return null;
|
|
3005
3065
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
3006
3066
|
if (!m) return null;
|
|
3007
|
-
const [, , subjectRaw, , objectRaw] = m;
|
|
3067
|
+
const [, det, subjectRaw, verb, objectRaw] = m;
|
|
3008
3068
|
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");
|
|
3069
|
+
const { loadLexicon, lookupNoun, lookupAdjective, classify } = await import("../domain/grammar/lexicon.mjs");
|
|
3010
3070
|
const lex = lexicon || loadLexicon();
|
|
3011
3071
|
// Y already a known NOUN or a fact-grounded CLASS term — a genuine class-
|
|
3012
3072
|
// membership sentence, unknownSubjectFallback/unknownObjectFallback's own
|
|
3013
3073
|
// territory (already had first refusal on it) — never misread as a property.
|
|
3014
3074
|
if (lookupNoun(lex, objectRaw) || GENERIC_ANCHOR_NOUNS.has(String(objectRaw).toLowerCase())
|
|
3015
3075
|
|| (await isGroundedByFact(objectRaw, memoryDir, cache))) return null;
|
|
3076
|
+
// CLASS-LEVEL adjective predication — "every snake is venomous": a universal
|
|
3077
|
+
// quantifier over a grounded noun class, with an adjective complement. The
|
|
3078
|
+
// quantifier is the same deliberate-generalization signal the article/
|
|
3079
|
+
// capitalization stand-ins give for the specific-entity form below, so a
|
|
3080
|
+
// bare-lexicon-grounded subject qualifies here (it would not for the
|
|
3081
|
+
// unquantified property claim), and the fact is stored WITH its "every"
|
|
3082
|
+
// quantifier so the read-back ("is a snake venomous", "are snakes venomous")
|
|
3083
|
+
// holds for the whole class. The adjective is confirmed by the static lexicon
|
|
3084
|
+
// or wink's POS tag (the same tag unknownObjectFallback used to defer here);
|
|
3085
|
+
// a noun-shaped Y was already minted as a class upstream and never reaches
|
|
3086
|
+
// this point.
|
|
3087
|
+
const universalQuantifier = /^(?:every|each|all|any)$/i.test((det || "").trim());
|
|
3088
|
+
if (universalQuantifier && (await isGroundedTerm(subjectRaw, lex, memoryDir, cache))
|
|
3089
|
+
&& (lookupAdjective(lex, objectRaw) || (await objectReadsAsNonNoun(objectRaw)))) {
|
|
3090
|
+
const classSubject = /^are$/i.test(verb)
|
|
3091
|
+
? (lookupNoun(lex, subjectRaw)?.lemma || singularizeSurface(subjectRaw))
|
|
3092
|
+
: subjectRaw;
|
|
3093
|
+
return teachFact(memoryDir, sessionId, {
|
|
3094
|
+
subject: classSubject, predicate: HAS_PROPERTY_PREDICATE, object: objectRaw, quantifier: "every",
|
|
3095
|
+
});
|
|
3096
|
+
}
|
|
3016
3097
|
// Subject-side groundedness — strip a leading "the"/"a"/"an" first
|
|
3017
3098
|
// (normFactTerm's own article-strip, mirrored here) so "the cache" checks
|
|
3018
3099
|
// groundedness under its real head noun "cache", the same spelling
|
|
@@ -4955,7 +5036,7 @@ const WHAT_KNOW_RE = /^(?:what\s+(?:do\s+you|d'?you)\s+know(?:\s+so\s+far)?|what
|
|
|
4955
5036
|
// do" needs the noun OPTIONAL after "this" (kept REQUIRED after "the") or it
|
|
4956
5037
|
// falls through to MODULE_ORIENT_RE, which fails to resolve "this" as an
|
|
4957
5038
|
// 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))?)$/;
|
|
5039
|
+
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
5040
|
/** A bare "what is in here"/"what's in here"/"whats in here" — the SAME
|
|
4960
5041
|
* orientation intent as META_ORIENT_RE's own
|
|
4961
5042
|
* "what's in this repo"-shaped members, just phrased with the CONTEXT_WORDS
|
|
@@ -5721,8 +5802,38 @@ const FACT_PREDICATE_PHRASES = {
|
|
|
5721
5802
|
"mgx:similarTo": "is similar to",
|
|
5722
5803
|
"mgx:relatedTo": "is related to",
|
|
5723
5804
|
"mgx:symbolOf": "is a symbol of",
|
|
5805
|
+
// A loaded adventure world's placement predicates, so a describe read-back of
|
|
5806
|
+
// a visible prop reads as English ("lamp is in the study") instead of the
|
|
5807
|
+
// mechanical -s fold garbling them ("lamp locateds in study"). The world's
|
|
5808
|
+
// SECRET/mechanics predicates (a hidden object's location, the objective
|
|
5809
|
+
// marker, the lock/open/NPC internals) are kept out of the describe lane
|
|
5810
|
+
// entirely by WORLD_INTERNAL_PREDICATES below, so they never render at all.
|
|
5811
|
+
"mgx:currently-in": "is in",
|
|
5812
|
+
"mgx:located-in": "is in",
|
|
5813
|
+
"mgx:fixed-in": "is fixed in",
|
|
5814
|
+
"mgx:stands-locked-in": "stands locked in",
|
|
5815
|
+
"mgx:works-in": "works in",
|
|
5724
5816
|
};
|
|
5725
5817
|
|
|
5818
|
+
/** The world-mechanics predicates the generic describe read-back must never
|
|
5819
|
+
* surface: a hidden object's location and the objective marker spoil the
|
|
5820
|
+
* puzzle, and the lock/container/open/NPC-schedule flags are datatype internals
|
|
5821
|
+
* the adventure's own readers answer in-game. Mirrors adventure.mjs's own
|
|
5822
|
+
* VIEW_EXCLUDED_PREDICATES — the same discipline the room-look digest uses. */
|
|
5823
|
+
const WORLD_INTERNAL_PREDICATES = new Set([
|
|
5824
|
+
"mgx:hidden-in", "mgx:is-objective", "mgx:unlocks-with",
|
|
5825
|
+
"mgx:is-npc", "mgx:acts-on-turn", "mgx:acts-toward",
|
|
5826
|
+
"mgx:is-container", "mgx:is-open",
|
|
5827
|
+
]);
|
|
5828
|
+
|
|
5829
|
+
/** The world PLACEMENT predicates carry curated phrases above so they render as
|
|
5830
|
+
* English, but they must stay OUT of the query-marker families derived from
|
|
5831
|
+
* FACT_PREDICATE_PHRASES — "what is in the study" is a members-of-class query,
|
|
5832
|
+
* not a reverse placement lookup, and "is in" is far too broad an anchor. */
|
|
5833
|
+
const WORLD_PLACEMENT_PREDICATES = new Set([
|
|
5834
|
+
"mgx:currently-in", "mgx:located-in", "mgx:fixed-in", "mgx:stands-locked-in", "mgx:works-in",
|
|
5835
|
+
]);
|
|
5836
|
+
|
|
5726
5837
|
/** The MECHANICAL fallback for a predicate this table has no curated entry
|
|
5727
5838
|
* for — specifically generalVerbTeach's minted "mgx:<lemma>" predicates
|
|
5728
5839
|
* ("mgx:eat", "mgx:drive", …) — the mechanical INVERSE of singularizeSurface's
|
|
@@ -5812,6 +5923,7 @@ function relationRoleWord(predicate) {
|
|
|
5812
5923
|
// (no curated second table) — the single-letter "a" is excluded, too short
|
|
5813
5924
|
// to anchor on without risking eating a genuine multi-word subject.
|
|
5814
5925
|
const TRAILING_PREDICATE_MARKERS = Object.entries(FACT_PREDICATE_PHRASES)
|
|
5926
|
+
.filter(([predicate]) => !WORLD_PLACEMENT_PREDICATES.has(predicate))
|
|
5815
5927
|
.map(([predicate, phrase]) => {
|
|
5816
5928
|
const m = /^(?:is|are)\s+(.+)$/i.exec(phrase);
|
|
5817
5929
|
return m ? { predicate, marker: m[1].trim().toLowerCase() } : null;
|
|
@@ -6577,7 +6689,7 @@ const REVERSE_PREDICATE_EXCLUDE = new Set([
|
|
|
6577
6689
|
"mgx:ownedBy", "owl:disjointWith", "mgx:hasProperty", "mgx:receivesAction",
|
|
6578
6690
|
]);
|
|
6579
6691
|
const REVERSE_PREDICATE_MARKERS = Object.entries(FACT_PREDICATE_PHRASES)
|
|
6580
|
-
.filter(([predicate]) => !REVERSE_PREDICATE_EXCLUDE.has(predicate))
|
|
6692
|
+
.filter(([predicate]) => !REVERSE_PREDICATE_EXCLUDE.has(predicate) && !WORLD_PLACEMENT_PREDICATES.has(predicate))
|
|
6581
6693
|
.map(([predicate, phrase]) => ({
|
|
6582
6694
|
predicate,
|
|
6583
6695
|
re: new RegExp(`^what\\s+${escapeRegex(phrase)}\\s+(.+?)[?.!\\s]*$`, "i"),
|
|
@@ -6603,7 +6715,7 @@ const FORWARD_YESNO_EXCLUDE = new Set([
|
|
|
6603
6715
|
"mgx:ownedBy",
|
|
6604
6716
|
]);
|
|
6605
6717
|
const FORWARD_YESNO_MARKERS = Object.entries(FACT_PREDICATE_PHRASES)
|
|
6606
|
-
.filter(([predicate]) => !FORWARD_YESNO_EXCLUDE.has(predicate))
|
|
6718
|
+
.filter(([predicate]) => !FORWARD_YESNO_EXCLUDE.has(predicate) && !WORLD_PLACEMENT_PREDICATES.has(predicate))
|
|
6607
6719
|
.map(([predicate, phrase]) => {
|
|
6608
6720
|
let re;
|
|
6609
6721
|
if (phrase === "can be") {
|
|
@@ -6926,8 +7038,11 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
6926
7038
|
const variants = factTermVariants(normFactTerm, subject);
|
|
6927
7039
|
// factRows (trust+sourceIds-bearing), not the plain memoryFacts shape — the
|
|
6928
7040
|
// bias-weighted ranking below needs each hit's sourceIds to resolve which
|
|
6929
|
-
// bundle it came from (memory/bias.mjs's biasForRow).
|
|
6930
|
-
|
|
7041
|
+
// bundle it came from (memory/bias.mjs's biasForRow). A live world's secret
|
|
7042
|
+
// and mechanics predicates are dropped so "what is the letter" never reads
|
|
7043
|
+
// back where it's hidden or that it's the objective (WORLD_INTERNAL_PREDICATES).
|
|
7044
|
+
const subjectHits = (await factRows(memoryDir, cache))
|
|
7045
|
+
.filter((f) => variants.has(f.subject) && !WORLD_INTERNAL_PREDICATES.has(f.predicate));
|
|
6931
7046
|
let hits = predicate ? subjectHits.filter((f) => f.predicate === predicate) : subjectHits;
|
|
6932
7047
|
if (!hits.length) {
|
|
6933
7048
|
// The subject itself is known, but not under this specific relation —
|
|
@@ -6940,6 +7055,20 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
6940
7055
|
replace: miss,
|
|
6941
7056
|
};
|
|
6942
7057
|
}
|
|
7058
|
+
// The term names nothing as a fact SUBJECT, but may exist only as the
|
|
7059
|
+
// OBJECT of taught relations ("ahab is the father of ishmael" → "what is
|
|
7060
|
+
// ishmael"): surface those reverse relations rather than missing, the same
|
|
7061
|
+
// facts "what do you know about X" would list.
|
|
7062
|
+
if (!predicate) {
|
|
7063
|
+
const objectHits = rankByBiasThenTrust((await factRows(memoryDir, cache)).filter((f) => variants.has(f.object)), biasByBundle);
|
|
7064
|
+
if (objectHits.length) {
|
|
7065
|
+
const objLines = objectHits.map(renderFactLine);
|
|
7066
|
+
const objShown = objLines.slice(0, FACT_ANSWER_CAP);
|
|
7067
|
+
const objRest = objLines.slice(FACT_ANSWER_CAP);
|
|
7068
|
+
const objExtra = objRest.length ? `\n…and ${objRest.length} more — say 'more' to see them.` : "";
|
|
7069
|
+
return { text: objShown.join("\n") + objExtra, replace: miss, ...(objRest.length ? { pending: { items: objRest, noun: "facts" } } : {}) };
|
|
7070
|
+
}
|
|
7071
|
+
}
|
|
6943
7072
|
return null;
|
|
6944
7073
|
}
|
|
6945
7074
|
// Bias only REORDERS — every hit still renders and is cited (Part 6's
|
|
@@ -7442,6 +7571,12 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7442
7571
|
hits = rows.filter((f) => overlaps(f.subject) || overlaps(f.object));
|
|
7443
7572
|
}
|
|
7444
7573
|
}
|
|
7574
|
+
// A live adventure world's mechanics never leak through the describe lane:
|
|
7575
|
+
// "what is the letter" must not read back where it's hidden or that it's the
|
|
7576
|
+
// objective, and those datatype internals render as garbled non-English
|
|
7577
|
+
// besides. The adventure's own where/openness readers answer the legitimate
|
|
7578
|
+
// in-game questions from the world fold.
|
|
7579
|
+
hits = hits.filter((f) => !WORLD_INTERNAL_PREDICATES.has(f.predicate));
|
|
7445
7580
|
// A genuinely empty result here is a real miss: "what do you know about
|
|
7446
7581
|
// the last commit" needs a TEACH-OFFER, not a bare wall — added as a LATE
|
|
7447
7582
|
// runTurn-level addition, below, alongside the sibling "what is X" offer,
|
|
@@ -8159,6 +8294,26 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
8159
8294
|
}
|
|
8160
8295
|
}
|
|
8161
8296
|
|
|
8297
|
+
// Bare "who is/was <name>" with no relational "of Y" tail or genitive (those
|
|
8298
|
+
// are the whoAsk reader's above) — surface every taught fact naming the
|
|
8299
|
+
// person, whether as the subject or only as a relation OBJECT ("ahab is the
|
|
8300
|
+
// father of ishmael" → "who is ishmael"). A name with no stored fact falls
|
|
8301
|
+
// through unchanged.
|
|
8302
|
+
{
|
|
8303
|
+
const whoBare = qHedge.match(WHO_IS_BARE_RE);
|
|
8304
|
+
if (whoBare) {
|
|
8305
|
+
const nameVariants = factTermVariants(normFactTerm, whoBare[1]);
|
|
8306
|
+
const hits = rankByBiasThenTrust(rows.filter((f) => nameVariants.has(f.subject) || nameVariants.has(f.object)), biasByBundle);
|
|
8307
|
+
if (hits.length) {
|
|
8308
|
+
const lines = hits.map(renderFactLine);
|
|
8309
|
+
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
8310
|
+
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
8311
|
+
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
8312
|
+
return { text: shown.join("\n") + extra, replace: true, ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}) };
|
|
8313
|
+
}
|
|
8314
|
+
}
|
|
8315
|
+
}
|
|
8316
|
+
|
|
8162
8317
|
// (a0.5) RECURSIVE-RULE REACHABILITY LIST — "list the <plural> of <X>": a
|
|
8163
8318
|
// genuine KIND-CHANGE from the yes/no dispatcher just above — REACHABILITY-SET
|
|
8164
8319
|
// enumeration (every node ever reached), not single-target search.
|
|
@@ -9612,6 +9767,13 @@ function relationDefinitions() {
|
|
|
9612
9767
|
* it — the fact-lookup path is a low-collision subject lookup, not a structural
|
|
9613
9768
|
* parse, so loosening it here is safe. */
|
|
9614
9769
|
const BARE_WHATIS_RE = /^what\s+(?:is|are)\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
9770
|
+
/** A bare "who is/was <name>" with no relational tail ("of Y") or genitive
|
|
9771
|
+
* ("Y's role") — those keep their own specific who-readers. This single-token
|
|
9772
|
+
* form is armed into the meta-term fact lane only on a would-miss, and only
|
|
9773
|
+
* surfaces an answer when memory actually holds facts about the name (as a
|
|
9774
|
+
* subject or a relation object); with no such facts it returns null and the
|
|
9775
|
+
* turn falls through to the author/relation who-readers unchanged. */
|
|
9776
|
+
const WHO_IS_BARE_RE = /^who\s+(?:is|are|was|were)\s+(?:an?\s+|the\s+)?([\w'-]+)[?.!\s]*$/i;
|
|
9615
9777
|
|
|
9616
9778
|
/** The meta term a "what is a X" / "what is X" / "what does X mean" / "define X"
|
|
9617
9779
|
* question asks about — from the parse when present, else recognized directly
|
|
@@ -10566,7 +10728,7 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", ga
|
|
|
10566
10728
|
if (wantsLegal) {
|
|
10567
10729
|
let moves;
|
|
10568
10730
|
try {
|
|
10569
|
-
moves = movesFromRules(state, domain);
|
|
10731
|
+
moves = movesFromRules(state, domain, { scope: "taught" });
|
|
10570
10732
|
} catch (err) {
|
|
10571
10733
|
if (err instanceof PlanBudgetError) {
|
|
10572
10734
|
return { text: `too many possible moves to enumerate here (${err.message}) — narrow the classes involved.`, via: "plan", deduced: "list the legal moves (budget exceeded)", note: "plan lane — budget decline" };
|
|
@@ -10656,7 +10818,7 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", ga
|
|
|
10656
10818
|
}
|
|
10657
10819
|
let isGoal;
|
|
10658
10820
|
try {
|
|
10659
|
-
isGoal = compileGoal(goals, domain);
|
|
10821
|
+
isGoal = compileGoal(goals, domain, { scope: "taught" });
|
|
10660
10822
|
} catch (err) {
|
|
10661
10823
|
return { text: `I can't compile that goal: ${err?.message ?? err}`, via: "plan", deduced: "plan a move sequence (uncompilable goal)", note: "plan lane — goal compile decline" };
|
|
10662
10824
|
}
|
|
@@ -10664,7 +10826,7 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", ga
|
|
|
10664
10826
|
const maxDepth = gameConfig?.planning?.maxDepth ?? DEFAULT_GAME_CONFIG.planning.maxDepth;
|
|
10665
10827
|
let found;
|
|
10666
10828
|
try {
|
|
10667
|
-
found = findActionPath(state, isGoal, (s) => movesFromRules(s, domain), { maxDepth, stateKey: stateKeyFor });
|
|
10829
|
+
found = findActionPath(state, isGoal, (s) => movesFromRules(s, domain, { scope: "taught" }), { maxDepth, stateKey: stateKeyFor });
|
|
10668
10830
|
} catch (err) {
|
|
10669
10831
|
if (err instanceof PlanBudgetError) {
|
|
10670
10832
|
return { text: `the search space is too large (${err.message}) — narrow the classes involved.`, via: "plan", deduced: "plan a move sequence (budget exceeded)", note: "plan lane — budget decline" };
|
|
@@ -10765,7 +10927,7 @@ async function executePlanStep(planHolder, { memoryDir, sessionId = "" }) {
|
|
|
10765
10927
|
const factRows = readFactRows(payload);
|
|
10766
10928
|
const domain = compileDomain(factRows, readRuleRows(payload));
|
|
10767
10929
|
const finalState = stateFromFacts(factRows, domain);
|
|
10768
|
-
const holds = compileGoal(ps.goals, domain)(finalState);
|
|
10930
|
+
const holds = compileGoal(ps.goals, domain, { scope: "taught" })(finalState);
|
|
10769
10931
|
planHolder.state = { ...planHolder.state, done: true };
|
|
10770
10932
|
return {
|
|
10771
10933
|
text: holds
|
|
@@ -10822,6 +10984,18 @@ async function planFollowUpAnswer(query, { memoryDir, planHolder, pendingPager =
|
|
|
10822
10984
|
};
|
|
10823
10985
|
}
|
|
10824
10986
|
|
|
10987
|
+
if (PLAN_GOAL_READBACK_RE.test(q)) {
|
|
10988
|
+
if (!ps || !(ps.goalTexts?.length || ps.goals?.length)) return null;
|
|
10989
|
+
const goalText = ps.goalTexts?.length ? ps.goalTexts.join("; ") : "the goal you set";
|
|
10990
|
+
const status = activePlan
|
|
10991
|
+
? ` A plan is ready — ${ps.actions.length} move${ps.actions.length === 1 ? "" : "s"}; say "next" to step through it.`
|
|
10992
|
+
: ' Say "solve it" when the board is taught.';
|
|
10993
|
+
return {
|
|
10994
|
+
text: `the goal is that ${goalText}.${status}`,
|
|
10995
|
+
deduced: "read back the held goal",
|
|
10996
|
+
note: "PLAN FOLLOW-UP — goal read-back from the held planState",
|
|
10997
|
+
};
|
|
10998
|
+
}
|
|
10825
10999
|
if (PLAN_WHAT_NEXT_RE.test(q)) {
|
|
10826
11000
|
if (!activePlan) return null;
|
|
10827
11001
|
if (ps.done || ps.cursor >= ps.actions.length) {
|
|
@@ -11508,8 +11682,14 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11508
11682
|
// above.
|
|
11509
11683
|
const capabilityAskShape = CAN_ASK_RE.test(gateQuery) || WHAT_CAN_DO_RE.test(gateQuery)
|
|
11510
11684
|
|| DO_VERB_ASK_RE.test(gateQuery) || WHICH_KIND_CAN_RE.test(gateQuery) || WHAT_CAN_VERB_RE.test(gateQuery);
|
|
11685
|
+
// A bare "who is/was <name>" (no relational tail) is as short as the
|
|
11686
|
+
// vocabulary openers above and trips isConversational's word-count catch-all
|
|
11687
|
+
// the same way — factReadBack's bare-who reader surfaces the person's stored
|
|
11688
|
+
// relations only on a real hit, so a name with no facts still falls to the
|
|
11689
|
+
// ordinary card.
|
|
11690
|
+
const whoIsShape = WHO_IS_BARE_RE.test(gateQuery);
|
|
11511
11691
|
let bareMetaHit = null;
|
|
11512
|
-
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape || capabilityAskShape || bareNounShape)) {
|
|
11692
|
+
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape || capabilityAskShape || bareNounShape || whoIsShape)) {
|
|
11513
11693
|
if (memoryDir) {
|
|
11514
11694
|
// The bare noun asks its own "what is a X" — the readers never see the
|
|
11515
11695
|
// single word, so the vocabulary route is the constructed question's.
|
|
@@ -12878,6 +13058,10 @@ const GAME_OBS_LOWER_RE = /^(?:no[,\s]+)?(?:lower|too\s+high|too\s+big|smaller|l
|
|
|
12878
13058
|
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
13059
|
const GAME_GUESS_RE = /^(?:is\s+it\s+)?(-?\d{1,12})\s*\??[.!?\s]*$/;
|
|
12880
13060
|
const GAME_FALSE_CORRECT_RE = /^(?:but\s+)?you\s+(?:already\s+)?said\s+(?:it\s+was\s+)?(?:correct|right)\b/i;
|
|
13061
|
+
// Thinking-aloud / hesitation fillers — a closed set (never a real question or a
|
|
13062
|
+
// graph query, which stay free to fall through to the normal lanes) that mid-game
|
|
13063
|
+
// coaches back toward a valid move instead of hitting a bare parse wall.
|
|
13064
|
+
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
13065
|
|
|
12882
13066
|
/** A natural-language plan frame — the shapes planLaneAnswer owns. Mid-game
|
|
12883
13067
|
* these get the one-at-a-time decline instead of clobbering the slot. */
|
|
@@ -12913,7 +13097,12 @@ function gameContinuationAnswer(line, game, planHolder) {
|
|
|
12913
13097
|
}
|
|
12914
13098
|
const higher = GAME_OBS_HIGHER_RE.test(line);
|
|
12915
13099
|
const lower = !higher && GAME_OBS_LOWER_RE.test(line);
|
|
12916
|
-
if (!higher && !lower)
|
|
13100
|
+
if (!higher && !lower) {
|
|
13101
|
+
if (GAME_HESITATION_RE.test(String(line).trim())) {
|
|
13102
|
+
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" };
|
|
13103
|
+
}
|
|
13104
|
+
return null;
|
|
13105
|
+
}
|
|
12917
13106
|
const prior = game.guess;
|
|
12918
13107
|
const next = { ...game };
|
|
12919
13108
|
if (higher) { next.lo = prior + 1; next.loSetBy = { guess: prior }; }
|
|
@@ -12956,6 +13145,9 @@ function gameContinuationAnswer(line, game, planHolder) {
|
|
|
12956
13145
|
: "you haven't guessed yet";
|
|
12957
13146
|
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
13147
|
}
|
|
13148
|
+
if (GAME_HESITATION_RE.test(String(line).trim())) {
|
|
13149
|
+
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" };
|
|
13150
|
+
}
|
|
12959
13151
|
const m = String(line).trim().match(GAME_GUESS_RE);
|
|
12960
13152
|
if (!m) return null;
|
|
12961
13153
|
const guess = Number.parseInt(m[1], 10);
|
|
@@ -13107,6 +13299,52 @@ function rewriteNegativePolarityOpener(line) {
|
|
|
13107
13299
|
return null;
|
|
13108
13300
|
}
|
|
13109
13301
|
|
|
13302
|
+
/** A CONTRACTED NEGATIVE INTERROGATIVE — "isn't a dog an animal?", "doesn't
|
|
13303
|
+
* store.mjs import config?": a confirmation-seeking question whose expected
|
|
13304
|
+
* answer is the positive yes/no. Folded to the plain positive interrogative the
|
|
13305
|
+
* isa/relation readers already answer, so it is ANSWERED rather than walling at
|
|
13306
|
+
* the grammar boundary or reading as a first-person declarative. A trailing "?"
|
|
13307
|
+
* is required — the whole negative-question signal — so a leading-"don't"
|
|
13308
|
+
* imperative ("don't show me tests") is never rewritten into a positive. */
|
|
13309
|
+
const NEG_CONTRACTION_LEAD = {
|
|
13310
|
+
"isn't": "is", "isnt": "is", "aren't": "are", "arent": "are",
|
|
13311
|
+
"wasn't": "was", "wasnt": "was", "weren't": "were", "werent": "were",
|
|
13312
|
+
"doesn't": "does", "doesnt": "does", "don't": "do", "dont": "do",
|
|
13313
|
+
"didn't": "did", "didnt": "did", "can't": "can", "cant": "can",
|
|
13314
|
+
"couldn't": "could", "couldnt": "could", "won't": "will", "wont": "will",
|
|
13315
|
+
"wouldn't": "would", "wouldnt": "would", "hasn't": "has", "hasnt": "has",
|
|
13316
|
+
"haven't": "have", "havent": "have", "hadn't": "had", "hadnt": "had",
|
|
13317
|
+
"shouldn't": "should", "shouldnt": "should",
|
|
13318
|
+
};
|
|
13319
|
+
function rewriteNegativeInterrogative(line) {
|
|
13320
|
+
const s = String(line || "").trim();
|
|
13321
|
+
if (!/\?\s*$/.test(s)) return null;
|
|
13322
|
+
const m = s.replace(/[?.!\s]+$/, "").match(/^(\S+)\s+(.+)$/);
|
|
13323
|
+
if (!m) return null;
|
|
13324
|
+
const positive = NEG_CONTRACTION_LEAD[m[1].toLowerCase()];
|
|
13325
|
+
if (!positive) return null;
|
|
13326
|
+
return `${positive} ${m[2].trim()}`;
|
|
13327
|
+
}
|
|
13328
|
+
|
|
13329
|
+
/** "what is the entry point" / "what's the main entry point of this codebase" /
|
|
13330
|
+
* "which file is the entry point" — the definition/which-file phrasings of the
|
|
13331
|
+
* entry-point question, folded onto the "where is the entry point" surface the
|
|
13332
|
+
* ask engine's own entry-point ranker (ask.mjs ENTRY_POINT_QUERY_RE) already
|
|
13333
|
+
* answers. Without this fold they parse as a vocabulary "what is X" miss. */
|
|
13334
|
+
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;
|
|
13335
|
+
const rewriteEntryPointQuestion = (line) => (ENTRY_POINT_WHATIS_RE.test(String(line || "").trim()) ? "where is the entry point" : null);
|
|
13336
|
+
|
|
13337
|
+
/** "prove that X is a Y" / "prove X is Y" — a request for the isa yes/no with
|
|
13338
|
+
* its proof chain, folded onto the "is X a Y" surface the isa reader already
|
|
13339
|
+
* answers with a cited chain. Only the copula form folds; other "prove …"
|
|
13340
|
+
* phrasings fall through to their ordinary handling / honest miss. */
|
|
13341
|
+
const PROVE_THAT_RE = /^prove\s+(?:to\s+me\s+)?(?:that\s+)?(.+?)\s+(is|are)\s+(.+?)[?.!\s]*$/i;
|
|
13342
|
+
function rewriteProveThat(line) {
|
|
13343
|
+
const m = String(line || "").trim().match(PROVE_THAT_RE);
|
|
13344
|
+
if (!m) return null;
|
|
13345
|
+
return `${m[2]} ${m[1].trim()} ${m[3].trim()}`;
|
|
13346
|
+
}
|
|
13347
|
+
|
|
13110
13348
|
/** A DISCONTIGUOUS verb frame, "SUBJECT uses OBJECT as its/a base(class)" —
|
|
13111
13349
|
* "uses" is split from its own qualifier ("as its base") around the object,
|
|
13112
13350
|
* so no contiguous phrase-table entry could ever register it, and "uses"
|
|
@@ -13238,7 +13476,8 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13238
13476
|
// — restored centrally inside withLast (below), once, for every dispatch path.
|
|
13239
13477
|
const indirectMatch = line.match(INDIRECT_REQUEST_RE);
|
|
13240
13478
|
const indirectLine = indirectMatch ? indirectMatch[1].trim() : line;
|
|
13241
|
-
const preRewriteLine =
|
|
13479
|
+
const preRewriteLine = rewriteEntryPointQuestion(indirectLine) || rewriteProveThat(indirectLine)
|
|
13480
|
+
|| rewriteVocabOpener(indirectLine) || indirectLine;
|
|
13242
13481
|
// rewriteUsesAsBaseFrame's discontiguous-frame rewrite: applied here, once,
|
|
13243
13482
|
// before ANY dispatch lane sees the text. Null (no-op) for every turn that
|
|
13244
13483
|
// doesn't match one of the four discontiguous shapes.
|
|
@@ -13256,7 +13495,8 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13256
13495
|
// question is answered by the possession readers instead of walling (the
|
|
13257
13496
|
// write boundary's own "?" gates already refuse to store it).
|
|
13258
13497
|
const eslRewrite = rewriteEslMissingDoes(cleftRewrite || frameLine)
|
|
13259
|
-
|| rewriteNegativePolarityOpener(cleftRewrite || frameLine)
|
|
13498
|
+
|| rewriteNegativePolarityOpener(cleftRewrite || frameLine)
|
|
13499
|
+
|| rewriteNegativeInterrogative(cleftRewrite || frameLine);
|
|
13260
13500
|
const cleftLine = eslRewrite || cleftRewrite || frameLine;
|
|
13261
13501
|
// VOCABULARY pronoun antecedent — "what is a dog" then "can it bark". The
|
|
13262
13502
|
// code-graph focus mechanism only ever binds {id,label} GRAPH entities, so
|
|
@@ -13291,13 +13531,16 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13291
13531
|
// captured from the PRE-narration finished result.
|
|
13292
13532
|
const withLast = (result, fallbackGoal = "unclear — no goal signal for this turn type") => {
|
|
13293
13533
|
const finished = attachDialogueAct(finish(result, { graph }), trace);
|
|
13294
|
-
//
|
|
13295
|
-
// indirect-request wrapper
|
|
13296
|
-
//
|
|
13297
|
-
//
|
|
13534
|
+
// The logged transcript echo is ALWAYS the verbatim user line — no dispatch
|
|
13535
|
+
// path's internal rewrite (the indirect-request wrapper, the vocab-opener /
|
|
13536
|
+
// cleft / ESL rewrites, a discourse substitution) may leak into what the
|
|
13537
|
+
// .log shows the user typed.
|
|
13538
|
+
if (Array.isArray(finished.logLines) && finished.logLines.length > 1) finished.logLines[1] = `> ${line}`;
|
|
13539
|
+
// record.query keeps its narrower restoration for the wrapper/rewrite frames
|
|
13540
|
+
// the ask engine records off `workingLine`; the .jsonl sidecar also carries
|
|
13541
|
+
// the verbatim line as `input`, below.
|
|
13298
13542
|
if (indirectMatch || baseFrameRewrite || vocabAntecedent || eslRewrite) {
|
|
13299
13543
|
if (finished.record) finished.record.query = line;
|
|
13300
|
-
if (Array.isArray(finished.logLines) && finished.logLines.length > 1) finished.logLines[1] = `> ${line}`;
|
|
13301
13544
|
}
|
|
13302
13545
|
// The VERBATIM user line rides every turn record as `input`, beside
|
|
13303
13546
|
// whatever `query` the dispatch path recorded — the session history must
|
|
@@ -13504,7 +13747,15 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13504
13747
|
const endsInPlanTrigger = PLAN_SOLVE_RE.test(lastSentence) || GOAL_TEACH_RE.test(lastSentence)
|
|
13505
13748
|
|| GOAL_TEACH_INFINITIVE_RE.test(lastSentence) || GOAL_TEACH_VERBLESS_RE.test(lastSentence)
|
|
13506
13749
|
|| LEGAL_MOVES_RE.test(lastSentence);
|
|
13507
|
-
|
|
13750
|
+
// The syllogism one-liner — "Every man is mortal. Socrates is a man. Is
|
|
13751
|
+
// Socrates mortal?": every sentence but the last teaches on its own, and
|
|
13752
|
+
// the last is a question. Each teach stores (in order, so the question
|
|
13753
|
+
// sees them), then the final sentence is answered as the payload behind
|
|
13754
|
+
// the teach receipts, the same rendering the plan-trigger case uses.
|
|
13755
|
+
const teachesThenAsks = !endsInPlanTrigger && /\?\s*$/.test(lastSentence.trim())
|
|
13756
|
+
&& await everySentenceTeaches(sentences.slice(0, -1), lexicon);
|
|
13757
|
+
const finalIsPayload = endsInPlanTrigger || teachesThenAsks;
|
|
13758
|
+
if (finalIsPayload || await everySentenceTeaches(sentences, lexicon)) {
|
|
13508
13759
|
let f = focus; let l = last; let ps = planHolder.state;
|
|
13509
13760
|
const receipts = [];
|
|
13510
13761
|
let finalRec = null;
|
|
@@ -13526,7 +13777,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13526
13777
|
// a stray "Goal (inferred)" line the bulleted ones already dropped. Its
|
|
13527
13778
|
// goal-line tail (everything after the receipt's first line) is kept once.
|
|
13528
13779
|
let answer;
|
|
13529
|
-
if (
|
|
13780
|
+
if (finalIsPayload) {
|
|
13530
13781
|
const receiptLines = receipts.slice(0, -1).map((t) => `• ${t}`).join("\n");
|
|
13531
13782
|
answer = receiptLines ? `${receiptLines}\n\n${finalRec.answer}` : finalRec.answer;
|
|
13532
13783
|
} else {
|
|
@@ -13538,6 +13789,12 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13538
13789
|
combined.planState = ps;
|
|
13539
13790
|
combined.focus = f;
|
|
13540
13791
|
combined.last = l;
|
|
13792
|
+
// Each per-sentence turn recorded only its OWN sentence; the transcript
|
|
13793
|
+
// echo and the turn record must quote the whole multi-sentence line the
|
|
13794
|
+
// user actually typed, not just its last sentence.
|
|
13795
|
+
const ts0 = Array.isArray(finalRec.logLines) && finalRec.logLines.length ? finalRec.logLines[0] : new Date().toISOString();
|
|
13796
|
+
combined.logLines = [ts0, `> ${line}`, answer, ""];
|
|
13797
|
+
if (finalRec.record) combined.record = { ...finalRec.record, query: line, input: line };
|
|
13541
13798
|
return combined;
|
|
13542
13799
|
}
|
|
13543
13800
|
}
|
|
@@ -381,6 +381,68 @@ async function runToldFactTurn(match, { planHolder, memoryDir, cache, gameConfig
|
|
|
381
381
|
});
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
+
// ---- in-game orientation asides ---------------------------------------------
|
|
385
|
+
//
|
|
386
|
+
// "where is the spider", "where am I", "what can I do", "what is the goal" —
|
|
387
|
+
// while the board is live these must answer from the board, not fall through to
|
|
388
|
+
// the code-graph lanes, where "where is the spider" reads "spider" as a module
|
|
389
|
+
// name and "what is the goal" answers from corpus vocabulary. There is no
|
|
390
|
+
// player piece here (both agents move on their own), so "where am I" reports
|
|
391
|
+
// the watcher stance and where the pieces stand.
|
|
392
|
+
|
|
393
|
+
const SF_WHERE_AGENT_RE = /^where(?:'s|\s+is|\s+are)\s+(?:the\s+)?(spider|fly)(?:-\d+)?(?:\s+now)?[?.!\s]*$/i;
|
|
394
|
+
const SF_WHERE_AM_I_RE = /^where\s+am\s+i(?:\s+now)?[?.!\s]*$/i;
|
|
395
|
+
const SF_OPTIONS_RE = /^(?:what\s+can\s+i\s+do(?:\s+(?:here|now))?|what\s+are\s+my\s+options|what\s+(?:should|do)\s+i\s+do(?:\s+(?:here|now))?|what\s+now)[?.!\s]*$/i;
|
|
396
|
+
const SF_GOAL_RE = /^(?:what(?:'s|\s+is)\s+(?:the\s+|my\s+)?(?:goal|objective|point|quest|aim)|what\s+are\s+they\s+(?:doing|trying\s+to\s+do)|what\s+am\s+i\s+(?:trying\s+to\s+do|(?:supposed|meant)\s+to\s+do))[?.!\s]*$/i;
|
|
397
|
+
|
|
398
|
+
const WATCHER_STANCE = 'you have no piece here — both agents move on their own. Watch, say "tick" to advance, or address one, e.g. "@spider the fly is east".';
|
|
399
|
+
|
|
400
|
+
const positionsOfKind = (kind, state) =>
|
|
401
|
+
liveIdsOfKind(kind, state).map((id) => `${id} at ${state.placements.get(id).cell}`);
|
|
402
|
+
|
|
403
|
+
async function spiderFlyContextAnswer(line, { memoryDir }) {
|
|
404
|
+
const l = String(line).trim();
|
|
405
|
+
const whereAgent = l.match(SF_WHERE_AGENT_RE);
|
|
406
|
+
const asksWhereMe = SF_WHERE_AM_I_RE.test(l);
|
|
407
|
+
const asksOptions = SF_OPTIONS_RE.test(l);
|
|
408
|
+
const asksGoal = SF_GOAL_RE.test(l);
|
|
409
|
+
if (!whereAgent && !asksWhereMe && !asksOptions && !asksGoal) return null;
|
|
410
|
+
let state;
|
|
411
|
+
try { state = foldSpiderFlyState(readFactRows(await loadMemory(memoryDir))); } catch { return null; }
|
|
412
|
+
|
|
413
|
+
if (whereAgent) {
|
|
414
|
+
const kind = whereAgent[1].toLowerCase();
|
|
415
|
+
const positions = positionsOfKind(kind, state);
|
|
416
|
+
return {
|
|
417
|
+
text: positions.length ? `${positions.join("; ")}.` : `there's no live ${kind} on the board right now.`,
|
|
418
|
+
lane: "game-answer",
|
|
419
|
+
note: `SPIDER-FLY — where-aside: ${kind} positions from the current board fold`,
|
|
420
|
+
goal: `find the ${kind}`,
|
|
421
|
+
miss: !positions.length,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (asksWhereMe) {
|
|
426
|
+
return { text: WATCHER_STANCE, lane: "game-inform", note: "SPIDER-FLY — where-am-I aside: the watcher stance (no player piece)", goal: "understand your role" };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (asksOptions) {
|
|
430
|
+
return {
|
|
431
|
+
text: 'say "tick" to advance a turn, or address an agent — e.g. "@spider the fly is east" or "@spider the fly is at cell-7-3" to plant a belief. Say "stop watching" to end.',
|
|
432
|
+
lane: "game-inform",
|
|
433
|
+
note: "SPIDER-FLY — options aside: the live game's own commands",
|
|
434
|
+
goal: "see what you can do",
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
text: "the spider hunts the fly; the fly tries to stay clear. You watch it play out — plant a belief to nudge one, or say \"tick\" to advance.",
|
|
440
|
+
lane: "game-inform",
|
|
441
|
+
note: "SPIDER-FLY — goal aside: the game's predator/prey objective",
|
|
442
|
+
goal: "understand the game",
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
384
446
|
// ---- the lane ------------------------------------------------------------
|
|
385
447
|
|
|
386
448
|
/**
|
|
@@ -468,5 +530,8 @@ export async function spiderFlyTurn(line, { planHolder, memoryDir, env, cache =
|
|
|
468
530
|
return runTickAndRender({ planHolder, memoryDir, cache, toldFacts: [], gameConfig });
|
|
469
531
|
}
|
|
470
532
|
|
|
533
|
+
const contextAside = await spiderFlyContextAnswer(line, { memoryDir });
|
|
534
|
+
if (contextAside) return contextAside;
|
|
535
|
+
|
|
471
536
|
return null; // an unaddressed aside — the ordinary lanes answer, board untouched
|
|
472
537
|
}
|