@polycode-projects/the-mechanical-code-talker 1.4.1 → 1.5.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 +1 -1
- package/ROADMAP.md +76 -11
- package/corpus/README.md +23 -22
- package/corpus/seon/README.md +7 -6
- package/corpus/seon/concepts.jsonl +119 -0
- package/corpus/tier2/general.jsonl +49 -0
- package/corpus/tier2/generate.mjs +68 -0
- package/corpus/tier2/manifest.json +14 -0
- package/data/templates/constructions/agent-noun-relations.toml +98 -0
- package/data/templates/responses.jsonl +1 -0
- package/package.json +5 -1
- package/src/ask-vocab.mjs +39 -1
- package/src/ask.mjs +278 -32
- package/src/chat.mjs +681 -212
- package/src/completions/complete.mjs +138 -0
- package/src/completions/group.mjs +171 -0
- package/src/completions/infer.mjs +395 -0
- package/src/completions/prune.mjs +156 -0
- package/src/completions/rank.mjs +154 -0
- package/src/completions/search.mjs +85 -0
- package/src/corpus/conceptnet.mjs +36 -3
- package/src/corpus/unknown-ingest.mjs +209 -0
- package/src/extensions.mjs +14 -4
- package/src/finish.mjs +61 -18
- package/src/grammar/ace.mjs +24 -338
- package/src/grammar/lexicon.mjs +37 -194
- package/src/interpret/pipeline.mjs +23 -2
- package/src/interpret/strategies/constructions.mjs +207 -0
- package/src/interpret/strategies/grammar.mjs +24 -3
- package/src/interpret/strategies/keywords.mjs +34 -0
- package/src/memory/blocks.mjs +7 -2
- package/src/memory/core.mjs +283 -20
- package/src/memory/shacl.mjs +114 -0
- package/src/prose.mjs +5 -1
- package/src/syllogise.mjs +0 -0
- package/src/grammar/lexicon-core.json +0 -302
package/src/chat.mjs
CHANGED
|
@@ -56,9 +56,9 @@ import { rankByBiasThenTrust } from "./memory/bias.mjs";
|
|
|
56
56
|
import { finish, beginsWithVowelSound, grammarRules } from "./finish.mjs";
|
|
57
57
|
import {
|
|
58
58
|
VERB_TO_KIND, WHERE_MARKERS, MENTION_MARKERS, ENTITY_TO_TYPE, PASSIVE_PARTICIPLE_TO_KIND,
|
|
59
|
-
stripTrailingScopeFiller,
|
|
59
|
+
stripTrailingScopeFiller, stripTrailingDiscourseTag,
|
|
60
60
|
} from "./ask-vocab.mjs";
|
|
61
|
-
import { COUNTERFACTUAL_RE, correctMisspellings, applyPreambleFrames } from "./interpret/normalize.mjs";
|
|
61
|
+
import { COUNTERFACTUAL_RE, correctMisspellings, applyPreambleFrames, escapeRegex } from "./interpret/normalize.mjs";
|
|
62
62
|
import { fuzzyMatchInSet, fuzzyBound } from "./interpret/fuzzy.mjs";
|
|
63
63
|
|
|
64
64
|
// uuidv7 lives in ./uuid.mjs (shared with telemetry + the bench stamp); re-exported
|
|
@@ -156,6 +156,7 @@ function deduceGoalFromParsed(parsed) {
|
|
|
156
156
|
if (shape === "meta") return `understand a vocabulary/definition term ("${parsed.object}")`;
|
|
157
157
|
if (shape === "where") return `locate where something is defined ("${parsed.object}")`;
|
|
158
158
|
if (shape === "when") return "understand when something last changed (history)";
|
|
159
|
+
if (shape === "whoLast") return "find who most recently touched something (history)";
|
|
159
160
|
if (shape === "mentions") return `find where something is mentioned in prose ("${parsed.object}")`;
|
|
160
161
|
if (shape === "ask") return (kind && GOAL_BY_KIND[kind]) || "check a specific subject/object relationship";
|
|
161
162
|
if ((shape === "reverse" || shape === "forward") && kind) return GOAL_BY_KIND[kind] || `understand a "${kind}" relationship`;
|
|
@@ -698,7 +699,12 @@ export function renderStats(graph) {
|
|
|
698
699
|
* which meant "who are you" always got the "here's what I can query" blurb and
|
|
699
700
|
* never a self-description — split so each gets the answer it actually asked for. */
|
|
700
701
|
const CAPABILITY_PHRASES = [
|
|
701
|
-
|
|
702
|
+
// HANDOVER.md 2026-07-10 item 10: "what can you actually do" (an intensifier
|
|
703
|
+
// adverb inserted before the verb) used to miss this exact-match regex entirely
|
|
704
|
+
// and fall to the raw grammar wall instead of orientationAnswer — the same
|
|
705
|
+
// question in every way that matters, just phrased with emphasis.
|
|
706
|
+
/^(?:so,?\s+)?what can (?:you|u)(?:\s+(?:actually|really))? do\??$/i, /^(?:so,?\s+)?what do you(?:\s+(?:actually|really))? do\??$/i,
|
|
707
|
+
/^help( me)?\??$/i, /^\?+$/,
|
|
702
708
|
/^how do (i|you) work\??$/i, /^how does (this|it) work\??$/i,
|
|
703
709
|
// unix-habit openers typed inside the REPL out of muscle memory — argv-only
|
|
704
710
|
// today (bin/tmct.mjs), dead once inside the chat loop; route to the same
|
|
@@ -770,11 +776,28 @@ const IDENTITY_PHRASES = [
|
|
|
770
776
|
* a genuinely different, more specific answer than the generic self-description,
|
|
771
777
|
* and this is a very likely first question given how most chat tools work today. */
|
|
772
778
|
const AI_IDENTITY_PHRASES = [
|
|
773
|
-
|
|
779
|
+
// HANDOVER.md 2026-07-10 item 10: "are you secretly GPT" — an adverb ("secretly"/
|
|
780
|
+
// "really"/"actually") wedged between "are you" and the noun (a deliberate-breaker
|
|
781
|
+
// persona's own phrasing) used to mis-segment the subject as "you secretly" and
|
|
782
|
+
// fall through to the ordinary graph-query grammar instead of this lane.
|
|
783
|
+
/^(are you|r u)\s+(?:secretly|really|actually)?\s*(an? )?(ai|a bot|chatgpt|gpt|an? llm|a language model|a robot)\??$/i,
|
|
774
784
|
/^is this (chatgpt|gpt|claude|an? ai|an? llm)\??$/i,
|
|
775
785
|
/^do you use ai\??$/i, /^what language model are you( using)?\??$/i,
|
|
776
786
|
/^am i (talking|speaking|chatting) (to|with) a (real )?(person|human|bot|ai)\??$/i,
|
|
777
787
|
];
|
|
788
|
+
/** "Do you have feelings/emotions" — HANDOVER.md 2026-07-10 item 10 (small-talk
|
|
789
|
+
* persona finding): with no closed-set match, this used to misfire into a
|
|
790
|
+
* literal module-name lookup for the bare noun ("no module matching 'feelings'
|
|
791
|
+
* found in the index") — a wrong-flavor wall, not an honest personality decline.
|
|
792
|
+
* Same family/placement as AI_IDENTITY_PHRASES just above (a self-awareness
|
|
793
|
+
* question about tmct, not a code-graph query), checked in conversationalTurn
|
|
794
|
+
* BEFORE any graph query is attempted. */
|
|
795
|
+
const FEELINGS_PHRASES = [
|
|
796
|
+
/^do you have (?:feelings|emotions|opinions|thoughts)\??$/i,
|
|
797
|
+
/^are you (?:sentient|conscious|self[- ]aware)\??$/i,
|
|
798
|
+
/^can you feel(?:\s+(?:things|emotions|anything))?\??$/i,
|
|
799
|
+
/^do you (?:feel|think|dream)\??$/i,
|
|
800
|
+
];
|
|
778
801
|
/** The structural verbs/nouns that mark a near-miss code question (→ keep the
|
|
779
802
|
* precise grammar hint, not the friendly nudge). */
|
|
780
803
|
const STRUCT_WORDS = new Set([
|
|
@@ -843,6 +866,7 @@ const T_ORIENTATION_EMPTY = "orientation-empty";
|
|
|
843
866
|
* depend on whether a repo is loaded. */
|
|
844
867
|
const T_IDENTITY_SELF = "identity-self";
|
|
845
868
|
const T_IDENTITY_NOT_LLM = "identity-not-an-llm";
|
|
869
|
+
const T_IDENTITY_NO_FEELINGS = "identity-no-feelings";
|
|
846
870
|
/** THE CONCEPT FORCE (concept.mjs): the three-band answer to a vague "what is a X"
|
|
847
871
|
* that names a known concept WITH instances — {definition}/{examples}/{followups}. */
|
|
848
872
|
const T_CONCEPT = "concept-force";
|
|
@@ -895,11 +919,27 @@ const THANKS = new Set([
|
|
|
895
919
|
"thanks", "thank you", "thankyou", "thx", "ty", "ta", "cheers", "nice one",
|
|
896
920
|
"much appreciated", "cool thanks", "many thanks", "much obliged", "ta very much",
|
|
897
921
|
"cheers mate", "cheers for that", "tks", "sweet thanks", "nice",
|
|
922
|
+
// "brilliant" (playtest sprint round 3, 2026-07-10): a UK-English enthusiasm
|
|
923
|
+
// interjection functioning as a bare acknowledgement, the same shape as
|
|
924
|
+
// "nice"/"cheers" just above — "brilliant, that's all I needed" hit the raw
|
|
925
|
+
// grammar wall via item 2's own multi-clause scan (which deliberately checks
|
|
926
|
+
// THANKS only, not OK_ACK — see farewellOrThanksSignal's own docblock for why
|
|
927
|
+
// "ok"/"cool"/"right" stay excluded there) because "brilliant" wasn't in
|
|
928
|
+
// EITHER closed set yet.
|
|
929
|
+
"brilliant",
|
|
898
930
|
// "ta for that" (Tier 6 playtest): "cheers for that" was already here, but
|
|
899
931
|
// its "ta" sibling (both dropped-word forms of the SAME "thanks for that"
|
|
900
932
|
// shape) was missing — fell to the generic orientation card via
|
|
901
933
|
// isConversational's ≤3-word catch-all instead of a thanks reply.
|
|
902
934
|
"ta for that",
|
|
935
|
+
// Playtest sprint round 3 (2026-07-10): a natural session-closing remark
|
|
936
|
+
// hit the raw grammar wall instead of a warm sign-off — the LAST turn of a
|
|
937
|
+
// session is a bad place to end on a wall. Same discipline as "ta for
|
|
938
|
+
// that": add the SPECIFIC found phrasing, not a general "closing remark"
|
|
939
|
+
// grammar.
|
|
940
|
+
"cheers, that's everything for now, thanks",
|
|
941
|
+
"that's everything for now, thanks",
|
|
942
|
+
"that's all for now, thanks",
|
|
903
943
|
]);
|
|
904
944
|
/** Farewells → a goodbye AND a clean end of session (same path as /exit). */
|
|
905
945
|
const BYE = new Set([
|
|
@@ -957,6 +997,82 @@ function closedOrCollapsed(q, set, idx) {
|
|
|
957
997
|
return idx.get(collapseRuns(q)) ?? null;
|
|
958
998
|
}
|
|
959
999
|
|
|
1000
|
+
/** HANDOVER.md 2026-07-10 item 2: THANKS/BYE were exact-match-the-WHOLE-line
|
|
1001
|
+
* closed sets, grown one literal phrase at a time across sessions — and kept
|
|
1002
|
+
* failing on the very next unlisted phrasing tried (3 independently-run
|
|
1003
|
+
* personas each hit this in one persona-sweep: "thanks, that was fun",
|
|
1004
|
+
* "ok thank you very much, bye bye", "thanks, bye"). The generalization is
|
|
1005
|
+
* over PHRASE SHAPE (a thanks/bye clause tacked onto a larger sentence),
|
|
1006
|
+
* not another one-off literal string:
|
|
1007
|
+
* - split on comma/semicolon/a standalone "and" into clauses
|
|
1008
|
+
* - strip a leading bare OK_ACK lead-in off each clause ("ok thank you…")
|
|
1009
|
+
* - strip a trailing intensifier ("very much"/"so much"/"a lot"/"a bunch" —
|
|
1010
|
+
* the SAME curated set THANKS_PREAMBLE_RE, interpret/normalize.mjs,
|
|
1011
|
+
* already recognizes) before matching THANKS
|
|
1012
|
+
* - fold an exact word-repeated clause ("bye bye") to one instance before
|
|
1013
|
+
* matching BYE — informal reduplication for emphasis, not a new phrase
|
|
1014
|
+
* Still the SAME closed THANKS/BYE sets underneath (same closedOrCollapsed
|
|
1015
|
+
* matcher) — only the SEGMENTATION generalizes. Bounded to short, non-codeish
|
|
1016
|
+
* lines (same discipline as isConversational/fuzzyConversationalMatch) so a
|
|
1017
|
+
* genuine structural question is never grabbed. A single-clause line (no
|
|
1018
|
+
* comma/semicolon/"and") is left to the exact whole-line checks above/below —
|
|
1019
|
+
* this only handles the MULTI-clause case those can't. Returns "bye"/"thanks"/
|
|
1020
|
+
* null; bye wins when a line carries both (a farewell should end the session
|
|
1021
|
+
* even alongside a thanks — the small-talk persona's "thanks, bye" finding:
|
|
1022
|
+
* the README implies "bye" phrasing should end the session, full stop). */
|
|
1023
|
+
const ACK_LEAD_RE = new RegExp(`^(?:${[...OK_ACK].map(escapeRegex).join("|")})\\s+(.+)$`, "i");
|
|
1024
|
+
const TRAILING_INTENSIFIER_RE = /\s+(?:very\s+much|so\s+much|a\s+lot|a\s+bunch)\s*$/i;
|
|
1025
|
+
const REPEATED_WORD_RE = /^(\S+)\s+\1$/i;
|
|
1026
|
+
// Comma/semicolon (optionally swallowing a following "and") OR a standalone
|
|
1027
|
+
// "and" — a single combined pattern so "X, and Y" splits into ["X", "Y"], not
|
|
1028
|
+
// ["X", "and Y"] (a naive comma-only split leaves "and" glued to the second
|
|
1029
|
+
// clause, which then fails every closed-set match downstream).
|
|
1030
|
+
const CLAUSE_SPLIT_RE = /\s*[,;]\s*(?:and\s+)?|\s+and\s+/;
|
|
1031
|
+
function conversationalClauses(q) {
|
|
1032
|
+
return q.split(CLAUSE_SPLIT_RE).map((c) => c.trim()).filter(Boolean);
|
|
1033
|
+
}
|
|
1034
|
+
/** BYE match tolerant of informal reduplication ("bye bye", "no no" — general,
|
|
1035
|
+
* not specific to any one word): a clause consisting of the SAME word twice
|
|
1036
|
+
* folds to one instance before the ordinary closed/collapsed BYE lookup.
|
|
1037
|
+
* Shared by the single-clause whole-line check and the multi-clause scan
|
|
1038
|
+
* below, so "bye bye" resolves the same way whether or not a comma follows it. */
|
|
1039
|
+
function foldedBye(clause) {
|
|
1040
|
+
if (closedOrCollapsed(clause, BYE, BYE_COLLAPSED)) return true;
|
|
1041
|
+
const folded = clause.match(REPEATED_WORD_RE);
|
|
1042
|
+
return !!(folded && closedOrCollapsed(folded[1], BYE, BYE_COLLAPSED));
|
|
1043
|
+
}
|
|
1044
|
+
function farewellOrThanksSignal(raw, q) {
|
|
1045
|
+
const words = q.split(/\s+/).filter(Boolean);
|
|
1046
|
+
if (words.length < 2 || words.length > 8 || looksCodeish(raw, q)) return null;
|
|
1047
|
+
const clauses = conversationalClauses(q);
|
|
1048
|
+
if (clauses.length < 2) return null; // single-clause lines: the exact whole-line checks own this
|
|
1049
|
+
// OK_ACK is deliberately NOT a signal here (unlike the exact whole-line check
|
|
1050
|
+
// above/below): "ok"/"cool"/"right"/"sure" are constitutionally ACK-PREAMBLE
|
|
1051
|
+
// words in this codebase (ACK_PREAMBLE_RE, interpret/normalize.mjs) — "right,
|
|
1052
|
+
// can you walk me through this codebase" is an ack-preamble before a REAL
|
|
1053
|
+
// question, not a closing acknowledgement, and treating a bare OK_ACK clause
|
|
1054
|
+
// as a thanks-signal regressed exactly that live case. THANKS itself is more
|
|
1055
|
+
// specific (genuine gratitude words rarely lead into an unrelated question),
|
|
1056
|
+
// but still gated below: a THANKS-hit only counts when every OTHER clause is
|
|
1057
|
+
// itself small-talk-shaped (≤3 words, non-codeish) — the SAME bound
|
|
1058
|
+
// isConversational's own catch-all uses — so "cheers, what does X do" is left
|
|
1059
|
+
// to the existing THANKS_PREAMBLE_RE lane, never grabbed here.
|
|
1060
|
+
let thanksClauseIdx = -1;
|
|
1061
|
+
let byeHit = false;
|
|
1062
|
+
for (let i = 0; i < clauses.length; i += 1) {
|
|
1063
|
+
const rawClause = clauses[i];
|
|
1064
|
+
const ackMatch = rawClause.match(ACK_LEAD_RE);
|
|
1065
|
+
const clause = ackMatch ? ackMatch[1].trim() : rawClause;
|
|
1066
|
+
if (foldedBye(clause)) { byeHit = true; break; }
|
|
1067
|
+
const deIntensified = clause.replace(TRAILING_INTENSIFIER_RE, "").trim();
|
|
1068
|
+
if (thanksClauseIdx < 0 && closedOrCollapsed(deIntensified, THANKS, THANKS_COLLAPSED)) thanksClauseIdx = i;
|
|
1069
|
+
}
|
|
1070
|
+
if (byeHit) return "bye";
|
|
1071
|
+
const thanksHit = thanksClauseIdx >= 0 && clauses.every((c, i) => i === thanksClauseIdx
|
|
1072
|
+
|| (c.split(/\s+/).filter(Boolean).length <= 3 && !looksCodeish(c, c.toLowerCase())));
|
|
1073
|
+
return thanksHit ? "thanks" : null;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
960
1076
|
/** The fuzzy-typo fallback's candidate pool: every canonical phrase across the
|
|
961
1077
|
* closed conversational sets, flattened once. Consulted only after every exact/
|
|
962
1078
|
* collapsed lookup misses (see fuzzyConversationalMatch). */
|
|
@@ -1050,11 +1166,29 @@ function conversationalTurn(line, ctx) {
|
|
|
1050
1166
|
...(end ? { end: true } : {}),
|
|
1051
1167
|
};
|
|
1052
1168
|
};
|
|
1053
|
-
if (
|
|
1169
|
+
if (foldedBye(q)) {
|
|
1054
1170
|
note(ctx.trace, "goal: casual/social — ending the session (no graph intent)");
|
|
1055
|
-
note(ctx.trace, "lane: conversational — farewell (BYE closed set)");
|
|
1171
|
+
note(ctx.trace, "lane: conversational — farewell (BYE closed set, incl. bare reduplication e.g. \"bye bye\")");
|
|
1056
1172
|
return mk(t(T_FAREWELL), { end: true });
|
|
1057
1173
|
}
|
|
1174
|
+
{
|
|
1175
|
+
// HANDOVER.md 2026-07-10 item 2: a multi-clause line carrying a bye/thanks
|
|
1176
|
+
// clause tacked onto a larger sentence ("thanks, that was fun", "ok thank
|
|
1177
|
+
// you very much, bye bye", "thanks, bye") — see farewellOrThanksSignal's
|
|
1178
|
+
// own docblock. Never fires on a single-clause line (those are the exact
|
|
1179
|
+
// checks just above/below), so this only ADDS coverage, never shadows it.
|
|
1180
|
+
const signal = farewellOrThanksSignal(raw, q);
|
|
1181
|
+
if (signal === "bye") {
|
|
1182
|
+
note(ctx.trace, "goal: casual/social — ending the session (no graph intent)");
|
|
1183
|
+
note(ctx.trace, "lane: conversational — farewell (multi-clause phrase-shape match)");
|
|
1184
|
+
return mk(t(T_FAREWELL), { end: true });
|
|
1185
|
+
}
|
|
1186
|
+
if (signal === "thanks") {
|
|
1187
|
+
note(ctx.trace, "goal: casual/social — acknowledgement, no graph intent");
|
|
1188
|
+
note(ctx.trace, "lane: conversational — thanks (multi-clause phrase-shape match)");
|
|
1189
|
+
return mk(t(T_THANKS));
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1058
1192
|
if (WHY.has(q)) {
|
|
1059
1193
|
note(ctx.trace, "goal: elaborate on the previous answer (why/say-more)");
|
|
1060
1194
|
note(ctx.trace, "lane: conversational — why/say-more (WHY closed set)");
|
|
@@ -1098,6 +1232,11 @@ function conversationalTurn(line, ctx) {
|
|
|
1098
1232
|
note(ctx.trace, "lane: conversational — identity/AI (AI_IDENTITY_PHRASES closed set)");
|
|
1099
1233
|
return mk(t(T_IDENTITY_NOT_LLM));
|
|
1100
1234
|
}
|
|
1235
|
+
if (FEELINGS_PHRASES.some((re) => re.test(raw))) {
|
|
1236
|
+
note(ctx.trace, "goal: identity — does tmct have feelings/consciousness (small-talk persona finding)");
|
|
1237
|
+
note(ctx.trace, "lane: conversational — identity/feelings (FEELINGS_PHRASES closed set)");
|
|
1238
|
+
return mk(t(T_IDENTITY_NO_FEELINGS));
|
|
1239
|
+
}
|
|
1101
1240
|
if (IDENTITY_PHRASES.some((re) => re.test(raw))) {
|
|
1102
1241
|
note(ctx.trace, "goal: identity — who/what tmct is, not a capability listing");
|
|
1103
1242
|
note(ctx.trace, "lane: conversational — identity (IDENTITY_PHRASES closed set)");
|
|
@@ -1402,6 +1541,53 @@ const OWNS_PASSIVE_TEACH_RE = /^(.+?)\s+(?:is|are|was|were)\s+owned\s+by\s+([A-Z
|
|
|
1402
1541
|
const RELATION_FACT_TEACH_RE =
|
|
1403
1542
|
/^([\w'-]+(?:\s+[A-Z][\w'-]*)?)\s+(?:is|are|was|were)\s+the\s+([a-z][\w-]*)\s+of\s+([\w'-]+(?:\s+[A-Z][\w'-]*)?)[.!?]*$/i;
|
|
1404
1543
|
|
|
1544
|
+
/** "every/a/an/the <N1> has a/an <N2> method" — the HAS-A-METHOD teach
|
|
1545
|
+
* declarative (HANDOVER.md 2026-07-10 item 9, a new pattern the operator
|
|
1546
|
+
* explicitly authorized this session — NOT one of PLAN_TAUGHT_RELATIONS.md's
|
|
1547
|
+
* own six items): a possession-of-capability claim about a class/entity's
|
|
1548
|
+
* method ("every Component has a render method", "a Widget has a render
|
|
1549
|
+
* method"). Before this pattern existed, this exact phrasing reached NO teach
|
|
1550
|
+
* recognizer at all: RELATION_FACT_TEACH_RE (above) requires a literal "is/
|
|
1551
|
+
* are the ROLE of", never "has a ROLE method"; GENERAL_VERB_TEACH_RE (below)
|
|
1552
|
+
* maps "has"/"have" onto the same HAS_A_PREDICATE this pattern uses, but only
|
|
1553
|
+
* for a BARE, wrapper-required, single-token subject with NO leading
|
|
1554
|
+
* determiner (GENERAL_VERB_DETERMINER_RE explicitly declines "every"/"a"/
|
|
1555
|
+
* "the" as a subject, by design — see its own docblock) — so a determiner-led
|
|
1556
|
+
* subject (the operator's own canonical example, "every Component…") fell
|
|
1557
|
+
* all the way through teachLane, landing on ask.mjs's own structural
|
|
1558
|
+
* "defines" grammar instead (VERB_TO_KIND maps "has"/"have" to the code-graph
|
|
1559
|
+
* "defines" relation), which can't resolve "Component"/"render" as real
|
|
1560
|
+
* code-graph entities and reports the vague, non-actionable
|
|
1561
|
+
* `"couldn't resolve one of the terms in this question."` wall — exactly the
|
|
1562
|
+
* symptom HANDOVER.md item 9 names.
|
|
1563
|
+
*
|
|
1564
|
+
* Deliberately a NARROW, EXPLICIT new pattern, not a widening of
|
|
1565
|
+
* GENERAL_VERB_TEACH_RE's own bare-subject shape (this project's own
|
|
1566
|
+
* discipline: small curated closed-set patterns, each independently tested,
|
|
1567
|
+
* never one generalized catch-all) — the literal trailing word "method" is
|
|
1568
|
+
* the anchor that keeps this pattern structurally DISJOINT from
|
|
1569
|
+
* generalVerbTeach's broader "X has a Y" territory (an ordinary "TaskController
|
|
1570
|
+
* has a hat" still never matches here, and falls through to generalVerbTeach
|
|
1571
|
+
* unaffected). Tried on the SAME ownSrc the other relational/possessive teach
|
|
1572
|
+
* shapes above already use, ahead of generalVerbTeach's own call site, so a
|
|
1573
|
+
* wrapped sentence with NO determiner ("remember that Component has a render
|
|
1574
|
+
* method" — already handled by generalVerbTeach today) is claimed here first
|
|
1575
|
+
* instead, producing the byte-identical stored fact and confirmation text —
|
|
1576
|
+
* a widening of COVERAGE (the determiner-led/bare-unwrapped case), never a
|
|
1577
|
+
* behavior change to the case that already worked.
|
|
1578
|
+
*
|
|
1579
|
+
* Predicate minting reuses the EXISTING HAS_A_PREDICATE (mgx:hasA) —
|
|
1580
|
+
* generalVerbTeach's own has/have special case already mints this, so a fact
|
|
1581
|
+
* taught via either recognizer reads back interoperably. m[1] = the subject
|
|
1582
|
+
* (N1, "Component"); m[2] = the capability word (N2, "render") — stored as
|
|
1583
|
+
* the object `"<N2> method"` ("render method"), so the query-side readers
|
|
1584
|
+
* below (HAS_METHOD_YESNO_RE/HAS_METHOD_OPEN_RE) can match on the whole
|
|
1585
|
+
* "<capability> method" phrase, never just the bare capability word (which
|
|
1586
|
+
* would risk colliding with an unrelated mgx:hasA fact about the same
|
|
1587
|
+
* capability noun taught some other way). */
|
|
1588
|
+
const TEACH_HAS_METHOD_RE =
|
|
1589
|
+
/^(?:every\s+|each\s+|all\s+|a\s+|an\s+|the\s+)?([A-Za-z][\w'-]*)\s+has\s+an?\s+([a-z][\w-]*)\s+method[.!?]*$/i;
|
|
1590
|
+
|
|
1405
1591
|
/** "a <name> is a <base1> of a <base2>" — the fixed-hop COMPOSITION-RULE teach
|
|
1406
1592
|
* declarative (PLAN_TAUGHT_RELATIONS.md Item 3, Phase 4): "a grandparent is a
|
|
1407
1593
|
* parent of a parent" teaches a RULE (mgx:ruleKind "compose2"), never a Fact —
|
|
@@ -1653,8 +1839,17 @@ async function ungroundedPairHint(payload, lexicon, memoryDir) {
|
|
|
1653
1839
|
const lex = lexicon || loadLexicon();
|
|
1654
1840
|
if (await isGroundedTerm(subjectRaw, lex, memoryDir)) return "";
|
|
1655
1841
|
if (await isGroundedTerm(objectRaw, lex, memoryDir)) return "";
|
|
1656
|
-
|
|
1657
|
-
|
|
1842
|
+
// 2026-07-10 (found live via SKILL_BENCHMARK_CONVERSATION.md playtest, a
|
|
1843
|
+
// classic first-thing-a-user-tries example: "john is a man"): the original
|
|
1844
|
+
// suggestion chained the second term UNDER the first's now-grounded proper
|
|
1845
|
+
// name ("every man is a john") — technically accepted by the grammar (once
|
|
1846
|
+
// "john" is grounded, ANY term can be taught as a kind of it), but reads as
|
|
1847
|
+
// nonsense to a human, since a proper name is never a category. Ground both
|
|
1848
|
+
// sides independently instead — two clear, parallel, semantically sane
|
|
1849
|
+
// suggestions, not a confusing chain through an arbitrary first term.
|
|
1850
|
+
return ` I don't know "${subjectRaw}" or "${objectRaw}" yet. Try grounding each one first, e.g. `
|
|
1851
|
+
+ `"every ${subjectRaw} is a thing" and "every ${objectRaw} is a thing", then re-teach the`
|
|
1852
|
+
+ ` original fact.`;
|
|
1658
1853
|
}
|
|
1659
1854
|
|
|
1660
1855
|
/** The unknown-SUBJECT direct-write fallback (point 1 + point 2's bare-property
|
|
@@ -2187,6 +2382,26 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
2187
2382
|
if (stored) return stored;
|
|
2188
2383
|
}
|
|
2189
2384
|
|
|
2385
|
+
// HAS-A-METHOD TEACH — "every/a/an/the <N1> has a/an <N2> method"
|
|
2386
|
+
// (HANDOVER.md 2026-07-10 item 9): a possession-of-capability claim, stored
|
|
2387
|
+
// as an ordinary Fact via the SAME HAS_A_PREDICATE generalVerbTeach's own
|
|
2388
|
+
// has/have special case already uses (see TEACH_HAS_METHOD_RE's own
|
|
2389
|
+
// docblock above for the full design). Grouped with the other relational/
|
|
2390
|
+
// possessive teach shapes above, tried on the SAME ownSrc, unconditionally
|
|
2391
|
+
// ahead of generalVerbTeach's own call site below — disjoint from
|
|
2392
|
+
// RELATION_FACT_TEACH_RE just above (that shape requires a literal "the
|
|
2393
|
+
// ROLE of", never "has a … method") and from generalVerbTeach's own bare-
|
|
2394
|
+
// subject shape (this one is the ONLY recognizer in this lane that accepts
|
|
2395
|
+
// a leading determiner — "every"/"a"/"an"/"the" — before a "has a … method"
|
|
2396
|
+
// claim).
|
|
2397
|
+
const hasMethod = ownSrc.match(TEACH_HAS_METHOD_RE);
|
|
2398
|
+
if (hasMethod && memoryDir && !QUESTION_LEAD_RE.test(ownSrc)) {
|
|
2399
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
2400
|
+
subject: hasMethod[1], predicate: HAS_A_PREDICATE, object: `${hasMethod[2]} method`,
|
|
2401
|
+
});
|
|
2402
|
+
if (stored) return stored;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2190
2405
|
// COMPOSE2 RULE TEACH — "a <name> is a <base1> of a <base2>"
|
|
2191
2406
|
// (PLAN_TAUGHT_RELATIONS.md Item 3, Phase 4): stores a RULE (appendRule,
|
|
2192
2407
|
// kind "compose2"), never a Fact — tried right after item 1's relational
|
|
@@ -2451,10 +2666,17 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
2451
2666
|
}
|
|
2452
2667
|
} catch { /* lexicon unavailable — fall through to the generic message */ }
|
|
2453
2668
|
}
|
|
2669
|
+
// HANDOVER.md 2026-07-10 item 3: this used to claim "I can only teach facts
|
|
2670
|
+
// using tmct's own code-vocabulary nouns" — false (general vocabulary teaching
|
|
2671
|
+
// is fully supported elsewhere, e.g. "Paris is the capital of France" stores
|
|
2672
|
+
// directly, and unknownSubjectFallback/ungroundedPairHint above both accept
|
|
2673
|
+
// ANY new vocabulary once one side is grounded). The real constraint named
|
|
2674
|
+
// here now: at least one side of a fact must already be grounded (or the
|
|
2675
|
+
// sentence must fit a specific relation shape) — not a vocabulary restriction.
|
|
2454
2676
|
const why = unknown.length
|
|
2455
2677
|
? ` I don't recognize ${joinList(unknown.map((w) => `"${w}"`))} as ${unknown.length === 1 ? "a word" : "words"} I know — `
|
|
2456
|
-
+ "
|
|
2457
|
-
+ "not
|
|
2678
|
+
+ "any vocabulary works, but at least one side of a fact needs to already be grounded to something I "
|
|
2679
|
+
+ "know (or fit one of my specific relation shapes), not two brand-new terms at once."
|
|
2458
2680
|
: "";
|
|
2459
2681
|
// Grounding NUDGE (operator refinement, 2026-07-09): APPENDED, never a
|
|
2460
2682
|
// replacement, exactly like "did" above — see ungroundedPairHint's own
|
|
@@ -2484,7 +2706,16 @@ const WHAT_KNOW_RE = /^(?:what\s+(?:do\s+you|d'?you)\s+know(?:\s+so\s+far)?|what
|
|
|
2484
2706
|
// 0.8.2 WS4 wall kindness (c): the most likely stranger openers — "what does this
|
|
2485
2707
|
// app/codebase do", "what is this app (for)" — join the orientation lane, so a
|
|
2486
2708
|
// first-touch question gets the live overview instead of the grammar wall.
|
|
2487
|
-
|
|
2709
|
+
// Playtest sprint round 1 (2026-07-10): "what does this do" — the bare pronoun,
|
|
2710
|
+
// no explicit noun — used to fall through this lane entirely (the "do" branch
|
|
2711
|
+
// required an explicit app/code/codebase/project/repo noun after this/the) into
|
|
2712
|
+
// MODULE_ORIENT_RE, which tried to resolve "this" as a graph entity, failed, and
|
|
2713
|
+
// hit the raw grammar wall — even though the identical-intent "what can you tell
|
|
2714
|
+
// me about this project" already answers cleanly via this same lane. The noun is
|
|
2715
|
+
// now OPTIONAL after "this" specifically (kept REQUIRED after "the", since bare
|
|
2716
|
+
// "what does the do" is not real input) — a natural stranger-opener that was one
|
|
2717
|
+
// token away from already working.
|
|
2718
|
+
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))?)$/;
|
|
2488
2719
|
|
|
2489
2720
|
/** A SHORT memory summary (never a fact dump) for the bare "what do you know".
|
|
2490
2721
|
* This branch only fires when rows.length === 0 — i.e. precisely the case where
|
|
@@ -3364,8 +3595,30 @@ const GENERIC_ENTITY_WORDS = new Set([
|
|
|
3364
3595
|
* index admits only SINGLE-WORD, purely-alphabetic ConceptNet endpoints on
|
|
3365
3596
|
* BOTH sides of a row — a first-cut heuristic filter, not a full manual
|
|
3366
3597
|
* review of all 1,228 rows (a natural follow-up, not claimed as done here).
|
|
3367
|
-
*
|
|
3368
|
-
*
|
|
3598
|
+
* A FOLLOW-UP spot check (later dispatch, same plan) sampled the 903 rows
|
|
3599
|
+
* this heuristic admits and confirmed the risk note is real even after the
|
|
3600
|
+
* single-word filter: generic-English collisions ("battalion"~"heap",
|
|
3601
|
+
* "bash"~"sock") and, more dangerously, IN-DOMAIN false synonyms — pairs
|
|
3602
|
+
* where both endpoints are real software terms but are NOT interchangeable
|
|
3603
|
+
* ("interpreter"~"compiler", "string"~"thread") — the exact "confidently
|
|
3604
|
+
* wrong within the domain" failure this codebase's ground rules treat as
|
|
3605
|
+
* worse than an honest miss. SYNONYM_DENYLIST below removes the specific
|
|
3606
|
+
* false pairs found by that spot check (a manually-reviewed blocklist, the
|
|
3607
|
+
* same shape as `conceptnet-map.toml`'s own reviewed relation-gate — not a
|
|
3608
|
+
* general noise heuristic); a full manual review of the remaining ~900
|
|
3609
|
+
* rows is still the honest follow-up, not claimed as done here either. */
|
|
3610
|
+
const SYNONYM_DENYLIST = new Set([
|
|
3611
|
+
["interpreter", "compiler"], // different execution strategies, not synonyms
|
|
3612
|
+
["string", "thread"], // unrelated CS concepts (text data vs. execution thread)
|
|
3613
|
+
["heart", "kernel"], // generic-English collision on "kernel"
|
|
3614
|
+
["battalion", "heap"], // generic-English collision on "heap" (data structure)
|
|
3615
|
+
["bash", "sock"], // generic-English collision ("bash"/"sock" = to hit)
|
|
3616
|
+
["command", "skill"], // too loose to be a safe query-time substitution
|
|
3617
|
+
["docker", "longshoreman"], // proper-noun/tool name vs. unrelated profession
|
|
3618
|
+
["name", "list"], // generic-English collision, not a domain synonym
|
|
3619
|
+
["list", "number"], // generic-English collision, not a domain synonym
|
|
3620
|
+
].map(([a, b]) => [a, b].sort().join("|")));
|
|
3621
|
+
|
|
3369
3622
|
let synonymIndexCache = null;
|
|
3370
3623
|
async function synonymIndex() {
|
|
3371
3624
|
if (synonymIndexCache) return synonymIndexCache;
|
|
@@ -3374,6 +3627,7 @@ async function synonymIndex() {
|
|
|
3374
3627
|
const ta = String(a || "").trim().toLowerCase();
|
|
3375
3628
|
const tb = String(b || "").trim().toLowerCase();
|
|
3376
3629
|
if (!ta || !tb || ta === tb) return;
|
|
3630
|
+
if (SYNONYM_DENYLIST.has([ta, tb].sort().join("|"))) return;
|
|
3377
3631
|
if (!index.has(ta)) index.set(ta, []);
|
|
3378
3632
|
if (!index.get(ta).some((e) => e.variant === tb)) index.get(ta).push({ variant: tb, source });
|
|
3379
3633
|
if (!index.has(tb)) index.set(tb, []);
|
|
@@ -3460,9 +3714,17 @@ const RELATION_FACT_YESNO_RE =
|
|
|
3460
3714
|
* below — a `resolveRelationChaseReverse` closure re-deriving the SAME
|
|
3461
3715
|
* resolution logic as (a0)'s `resolveRelationChase` (direct fact, alias via
|
|
3462
3716
|
* findIsaChain, compose2 via a reverse hop-counted chase, filter via a
|
|
3463
|
-
* recursive base-then-property chase), walked backward from the object.
|
|
3717
|
+
* recursive base-then-property chase), walked backward from the object.
|
|
3718
|
+
* HANDOVER.md 2026-07-10 item 3 (teach-then-recall gap): "who" also accepts
|
|
3719
|
+
* "what" — a taught relation whose role isn't a person ("paris is the capital
|
|
3720
|
+
* of france") reads naturally as "what is the capital of france", and the
|
|
3721
|
+
* resolution below is identical either way (it just returns the satisfying
|
|
3722
|
+
* subject(s)); the two words never compete for a query built from a DIFFERENT
|
|
3723
|
+
* shape, since T5's bare meta-whatis grammar shape ("what is X") only wins the
|
|
3724
|
+
* turn when factAnswer/factReadBack's own more specific readers upstream (this
|
|
3725
|
+
* one included) have already declined. */
|
|
3464
3726
|
const RELATION_WHO_ASK_RE =
|
|
3465
|
-
/^who\s+(?:is|are)\s+(?:the|an?)\s+([a-z][\w-]*)\s+of\s+([\w'-]+(?:\s+[A-Z][\w'-]*)?)[?.!\s]*$/i;
|
|
3727
|
+
/^(?:who|what)\s+(?:is|are)\s+(?:the|an?)\s+([a-z][\w-]*)\s+of\s+([\w'-]+(?:\s+[A-Z][\w'-]*)?)[?.!\s]*$/i;
|
|
3466
3728
|
|
|
3467
3729
|
/** "list the descendants of ahab" — the REACHABILITY-SET list query
|
|
3468
3730
|
* (PLAN_TAUGHT_RELATIONS.md Item 6, Phase 6's wiring half): a genuine
|
|
@@ -3480,6 +3742,21 @@ const RECURSIVE_LIST_ASK_RE = /^list\s+(?:the\s+|all\s+)?([a-z][\w-]*)\s+of\s+([
|
|
|
3480
3742
|
* doesn't parse; checked against the isa-family fact predicates only. */
|
|
3481
3743
|
const ISA_ASK_RE = /^(?:is|are)\s+(?:an?\s+)?(.+?)\s+(?:a\s+kind\s+of|a\s+type\s+of|an?)\s+(.+?)[?.!\s]*$/i;
|
|
3482
3744
|
const ISA_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
|
|
3745
|
+
/** "so john is a man now right?" / "john is a man, right?" — a DECLARATIVE
|
|
3746
|
+
* statement wrapped in a confirmation-check tag ("now right?"/"right?"/
|
|
3747
|
+
* "correct?"), found live (playtest sprint round 1, 2026-07-10) after a
|
|
3748
|
+
* just-declined teach attempt: the user reasonably assumes it worked and
|
|
3749
|
+
* asks to confirm — but this shape doesn't match ISA_ASK_RE at all (no
|
|
3750
|
+
* leading "is/are"), so it fell to the fully GENERIC grammar wall instead of
|
|
3751
|
+
* the same (already ISA-tailored) honest miss/hint the plain "is X a Y" form
|
|
3752
|
+
* gets. Deliberately narrow (requires "right?"/"correct?"/"yeah?" as the
|
|
3753
|
+
* VERY LAST word, optionally preceded by "now" and/or a comma) so it can
|
|
3754
|
+
* only ever REDIRECT a would-be-wall to the isaAsk block's own answer —
|
|
3755
|
+
* never a fabricated confirmation, and never touches phrasings that already
|
|
3756
|
+
* have their own home (e.g. OPINION_NUDGE_RE's own "is the code good"
|
|
3757
|
+
* ordering is unaffected — that starts with "is", leaving no room for this
|
|
3758
|
+
* regex's required leading subject clause). */
|
|
3759
|
+
const CONFIRM_TAG_RE = /^(?:so\s+)?(.+?)\s+(?:is|are)\s+(?:an?\s+)?(.+?)\s*,?\s*(?:now\s+)?(?:right|correct|yeah)\??$/i;
|
|
3483
3760
|
/** "what do you know about caches" — the open recall-everything form. Bug E
|
|
3484
3761
|
* (operator manual-chat find, this session) widened this to also accept
|
|
3485
3762
|
* "what is in your memory about X" / "what's in your memory about X" / "what
|
|
@@ -3645,6 +3922,32 @@ async function factAnswer(memoryDir, query, envelope, miss, biasByBundle = {}) {
|
|
|
3645
3922
|
// FULL unshortened grammar cheat-sheet standing under the offer instead
|
|
3646
3923
|
// of the nicer tailored one-liner — found live while adding this fix).
|
|
3647
3924
|
if (!hits.length) return null;
|
|
3925
|
+
// LIVE CONSISTENCY CHECK (PLAN_INFERENCE_TESTING.md INF-C2, §4 stage 5):
|
|
3926
|
+
// before answering from this subject's memory, check whether its OWN
|
|
3927
|
+
// taught/entailed types contradict each other (x rdf:type C1, x rdf:type
|
|
3928
|
+
// C2, C1 owl:disjointWith C2, lifted through both types' ⊑-ancestor
|
|
3929
|
+
// closures) via syllogise.mjs's findConsistencyViolations, LIVE and
|
|
3930
|
+
// READ-ONLY — same discipline as the cax-dw chase in the isaAsk block
|
|
3931
|
+
// above. A hit REFUSES the whole answer (every belief about a
|
|
3932
|
+
// contradictory subject is suspect, not just the clashing pair) rather
|
|
3933
|
+
// than silently answering from a memory that's already inconsistent.
|
|
3934
|
+
const { findConsistencyViolations, TYPE_PREDICATE: CONS_TYPE_PREDICATE, SUBCLASS_PREDICATE: CONS_SC_PREDICATE, DISJOINT_PREDICATE: CONS_DISJOINT_PREDICATE } = await import("./syllogise.mjs");
|
|
3935
|
+
const consIsTaught = (f) => !f.provenance?.includes("corpus:") && !f.provenance?.includes("web:");
|
|
3936
|
+
const consTypeEdges = rows.filter((f) => f.predicate === CONS_TYPE_PREDICATE && consIsTaught(f)).map((f) => [f.subject, f.object]);
|
|
3937
|
+
const consSubClassEdges = rows.filter((f) => f.predicate === CONS_SC_PREDICATE && consIsTaught(f)).map((f) => [f.subject, f.object]);
|
|
3938
|
+
const consDisjointEdges = rows.filter((f) => f.predicate === CONS_DISJOINT_PREDICATE && consIsTaught(f)).map((f) => [f.subject, f.object]);
|
|
3939
|
+
if (consDisjointEdges.length) {
|
|
3940
|
+
const clashes = findConsistencyViolations(consTypeEdges, consSubClassEdges, consDisjointEdges, { focus: variants, budget: 5 });
|
|
3941
|
+
const clash = clashes.find((c) => variants.has(c.subject));
|
|
3942
|
+
if (clash) {
|
|
3943
|
+
return {
|
|
3944
|
+
text: `I can't answer that — what I've been told about ${clash.subject} is inconsistent: it's taught to be both `
|
|
3945
|
+
+ `${clash.classA} and ${clash.classB}, but ${clash.viaA} and ${clash.viaB} are disjoint (${clash.viaA} owl:disjointWith `
|
|
3946
|
+
+ `${clash.viaB}). I'd need one of those retracted before I can answer honestly.`,
|
|
3947
|
+
replace: true,
|
|
3948
|
+
};
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3648
3951
|
// echo the STORED spelling ("caches" asked → "cache" known), never a guess
|
|
3649
3952
|
const literalHit = hits.find((f) => variants.has(f.subject) || variants.has(f.object));
|
|
3650
3953
|
const term = literalHit
|
|
@@ -3802,6 +4105,46 @@ const OWNS_YESNO_RE = /^(?:does|did)\s+([\w'-]+)\s+(?:owns?|maintains?)\s+(.+?)[
|
|
|
3802
4105
|
* backtracking "owned by" into its own subject capture and "<Name>" into its
|
|
3803
4106
|
* adjective slot, silently declining rather than answering). */
|
|
3804
4107
|
const OWNS_PASSIVE_YESNO_RE = /^(?:is|are|was|were)\s+(.+?)\s+owned\s+by\s+([A-Za-z][\w'-]*(?:\s+[A-Z][\w'-]*)?)[?.!\s]*$/i;
|
|
4108
|
+
/** "does/did <N1> have a/an <N2> method" — the HAS-A-METHOD yes/no reader
|
|
4109
|
+
* (HANDOVER.md 2026-07-10 item 9), sibling of OWNS_YESNO_RE above: mirrors
|
|
4110
|
+
* TEACH_HAS_METHOD_RE's own subject/capability shape, answering a direct
|
|
4111
|
+
* yes/no claim against a fact taught via that pattern (mgx:hasA, object
|
|
4112
|
+
* `"<capability> method"`).
|
|
4113
|
+
*
|
|
4114
|
+
* NOTE — a real, pre-existing structural collision, confirmed live before
|
|
4115
|
+
* wiring this: ask.mjs's OWN structural grammar already maps "has"/"have"
|
|
4116
|
+
* onto the code-graph "defines" relation (ask-vocab.mjs's VERB_TO_KIND), so
|
|
4117
|
+
* when a real code graph is loaded this EXACT phrasing is parsed there
|
|
4118
|
+
* FIRST — and because "a <word> method" is separately ambiguous with a
|
|
4119
|
+
* QUALIFIER reading there ("a public method"), ask.mjs resolves with its own
|
|
4120
|
+
* (possibly confusing) disambiguation choice, `miss: false`, before this
|
|
4121
|
+
* reader (factReadBack, gated on `miss` already being true) ever gets a
|
|
4122
|
+
* turn. Verified live: with a populated code graph, "does Component have a
|
|
4123
|
+
* render method" always lands on ask.mjs's disambiguation prompt, regardless
|
|
4124
|
+
* of subject/object identity or any taught fact; with NO code graph loaded
|
|
4125
|
+
* (this project's other supported mode — a purely conceptual teach-and-
|
|
4126
|
+
* recall session, see PLAN_TAUGHT_RELATIONS.md's own CONFIG={} test
|
|
4127
|
+
* convention) ask.mjs's structural attempt declines outright and this reader
|
|
4128
|
+
* answers correctly. Changing ask.mjs's own qualifier-disambiguation
|
|
4129
|
+
* behavior is a pre-existing, unrelated structural-grammar concern — out of
|
|
4130
|
+
* scope for this item.
|
|
4131
|
+
*
|
|
4132
|
+
* Same "never a guessed no" discipline as IS_ADJECTIVE_YESNO_RE/
|
|
4133
|
+
* GENERAL_VERB_YESNO_RE below (not OWNS_YESNO_RE's closed-world "no" text):
|
|
4134
|
+
* a hit answers "yes"; no matching fact DECLINES (null), since "nothing
|
|
4135
|
+
* taught yet" is not proof the class genuinely lacks the method. */
|
|
4136
|
+
const HAS_METHOD_YESNO_RE = /^(?:does|did)\s+([\w'-]+)\s+(?:has|have)\s+an?\s+([a-z][\w-]*)\s+method[?.!\s]*$/i;
|
|
4137
|
+
/** "what methods does <N1> have" — the HAS-A-METHOD open-list reader
|
|
4138
|
+
* (HANDOVER.md 2026-07-10 item 9): the read-back companion to
|
|
4139
|
+
* HAS_METHOD_YESNO_RE just above — lists every taught mgx:hasA fact for
|
|
4140
|
+
* <N1> whose object is a "<word> method" phrase. A distinct query shape
|
|
4141
|
+
* (object noun right after "what", not after the subject), so it does NOT
|
|
4142
|
+
* share HAS_METHOD_YESNO_RE's own ask.mjs collision: "what methods does X
|
|
4143
|
+
* have" already reaches an honest `miss: true` from ask.mjs even against a
|
|
4144
|
+
* populated code graph (confirmed live — "no module matching X found in the
|
|
4145
|
+
* index" when X isn't a real graph entity), so this reader is reachable in
|
|
4146
|
+
* both configurations. */
|
|
4147
|
+
const HAS_METHOD_OPEN_RE = /^what\s+methods\s+does\s+([\w'-]+)\s+have[?.!\s]*$/i;
|
|
3805
4148
|
/** "is/are/was/were <X> <adjective>" — a yes/no claim over a taught
|
|
3806
4149
|
* mgx:hasProperty fact (Tier-5 playtest fix). Deliberately has NO marker
|
|
3807
4150
|
* between subject and complement — "a"/"an"/"a kind of"/"a type of" is
|
|
@@ -4080,91 +4423,17 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
4080
4423
|
// guard is needed at THIS dispatch level (the search kernels
|
|
4081
4424
|
// underneath — findActionPath — carry their own `seen`-set safety
|
|
4082
4425
|
// regardless).
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
)] };
|
|
4095
|
-
}
|
|
4096
|
-
// The queried name may itself be a taught RULE. findRuleByName is
|
|
4097
|
-
// the SAME lookup §2/§3's own genericity design uses ("what kind of
|
|
4098
|
-
// thing is X") — no per-rule-name branch, just a class/kind check.
|
|
4099
|
-
const {
|
|
4100
|
-
loadMemory, findRuleByName, RULE_KIND_PROP: ruleKindProp,
|
|
4101
|
-
RULE_KIND_COMPOSE2: composeKind, RULE_KIND_FILTER: filterKind,
|
|
4102
|
-
} = await import("./memory/core.mjs");
|
|
4103
|
-
const memory = await loadMemory(memoryDir);
|
|
4104
|
-
const rule = findRuleByName(memory, target);
|
|
4105
|
-
const ruleKind = rule?.attributes?.find((a) => a.prop === ruleKindProp)?.value;
|
|
4106
|
-
// (iii) COMPOSE2 RULE CHASE (Phase 4 item 3) — a hop-counted
|
|
4107
|
-
// findActionPath search over { entity, hopsTaken } states,
|
|
4108
|
-
// dispatching base1's edges at hop 0 and base2's edges at hop 1,
|
|
4109
|
-
// requiring EXACTLY hopsTaken === 2 at the goal — never just
|
|
4110
|
-
// entity === target at any depth.
|
|
4111
|
-
if (rule && ruleKind === composeKind) {
|
|
4112
|
-
const base1 = rule.attributes.find((a) => a.prop === "mgx:ruleBase1")?.value;
|
|
4113
|
-
const base2 = rule.attributes.find((a) => a.prop === "mgx:ruleBase2")?.value;
|
|
4114
|
-
const startEntity = normFactTerm(subjectTerm);
|
|
4115
|
-
const targetEntity = normFactTerm(objectTerm);
|
|
4116
|
-
if (!base1 || !base2 || !startEntity || !targetEntity) return null;
|
|
4117
|
-
const { findActionPath } = await import("./planning.mjs");
|
|
4118
|
-
const applyActions = (state) => {
|
|
4119
|
-
if (state.hopsTaken >= 2) return [];
|
|
4120
|
-
const relName = state.hopsTaken === 0 ? base1 : base2;
|
|
4121
|
-
return relationFactsFor(relName)
|
|
4122
|
-
.filter((e) => e.fact.subject === state.entity)
|
|
4123
|
-
.map((e) => ({ action: e, nextState: { entity: e.fact.object, hopsTaken: state.hopsTaken + 1 } }));
|
|
4124
|
-
};
|
|
4125
|
-
const isGoal = (state) => state.hopsTaken === 2 && state.entity === targetEntity;
|
|
4126
|
-
const stateKey = (state) => `${state.entity}#${state.hopsTaken}`;
|
|
4127
|
-
const found = findActionPath({ entity: startEntity, hopsTaken: 0 }, isGoal, applyActions, { maxDepth: 2, stateKey });
|
|
4128
|
-
if (!found) return null;
|
|
4129
|
-
const seenAlias = new Set();
|
|
4130
|
-
const parts = [];
|
|
4131
|
-
for (const e of found.actions) {
|
|
4132
|
-
parts.push(renderFactLine(e.fact));
|
|
4133
|
-
for (const af of e.aliasFacts) {
|
|
4134
|
-
const key = af.id || `${af.subject}|${af.predicate}|${af.object}`;
|
|
4135
|
-
if (seenAlias.has(key)) continue;
|
|
4136
|
-
seenAlias.add(key);
|
|
4137
|
-
parts.push(`${factPhrase(af)}${af.provenance ? ` (source: ${af.provenance})` : ""}`);
|
|
4138
|
-
}
|
|
4139
|
-
}
|
|
4140
|
-
return { citation: parts };
|
|
4141
|
-
}
|
|
4142
|
-
// (iv) FILTER RULE CHASE (Phase 5 item 4) — recursively resolve the
|
|
4143
|
-
// base (a plain relation OR another rule — this SAME function,
|
|
4144
|
-
// generic over which one it turns out to be), then filter by
|
|
4145
|
-
// whether the SUBJECT carries the property literal
|
|
4146
|
-
// (mgx:hasProperty, a plain Fact lookup over the already-loaded
|
|
4147
|
-
// `rows`). A base chase that fails declines here too (never a
|
|
4148
|
-
// guess); a base chase that succeeds but whose subject lacks the
|
|
4149
|
-
// taught property declines as well — the filter correctly EXCLUDES
|
|
4150
|
-
// that candidate rather than silently ignoring the property clause.
|
|
4151
|
-
if (rule && ruleKind === filterKind) {
|
|
4152
|
-
const base = rule.attributes.find((a) => a.prop === "mgx:ruleBase1")?.value;
|
|
4153
|
-
const property = rule.attributes.find((a) => a.prop === "mgx:ruleFilterProperty")?.value;
|
|
4154
|
-
if (!base || !property) return null;
|
|
4155
|
-
const baseHit = await resolveRelationChase(base, subjectTerm, objectTerm);
|
|
4156
|
-
if (!baseHit) return null;
|
|
4157
|
-
const subjectEntity = normFactTerm(subjectTerm);
|
|
4158
|
-
const propertyNorm = normFactTerm(property);
|
|
4159
|
-
const propHit = rows.find(
|
|
4160
|
-
(f) => f.predicate === HAS_PROPERTY_PREDICATE && f.subject === subjectEntity && normFactTerm(f.object) === propertyNorm,
|
|
4161
|
-
);
|
|
4162
|
-
if (!propHit) return null; // base relation holds, but the property filter excludes this candidate
|
|
4163
|
-
return { citation: [...baseHit.citation, renderFactLine(propHit)] };
|
|
4164
|
-
}
|
|
4165
|
-
return null; // no remembered fact, alias, or rule (of any kind) reaches this
|
|
4166
|
-
};
|
|
4167
|
-
const hit = await resolveRelationChase(relationName, subject, object);
|
|
4426
|
+
// Extracted to memory/core.mjs (PLAN_COMPLETIONS.md Stage 1
|
|
4427
|
+
// prerequisite: cross-group inference reuses this SAME resolution
|
|
4428
|
+
// logic outside chat.mjs's dispatch context) — findRuleByName's own
|
|
4429
|
+
// natural sibling there. `relationFactsFor`/`renderFactLine`/
|
|
4430
|
+
// `factPhrase`/`factTermVariants`/`byTrust`/`rows`/
|
|
4431
|
+
// `HAS_PROPERTY_PREDICATE` are this block's own local closures/
|
|
4432
|
+
// constants, threaded through explicitly rather than re-derived.
|
|
4433
|
+
const { loadMemory, findRuleByName, resolveRelationChase } = await import("./memory/core.mjs");
|
|
4434
|
+
const memory = await loadMemory(memoryDir);
|
|
4435
|
+
const relationChaseHelpers = { relationFactsFor, renderFactLine, factPhrase, factTermVariants, byTrust, rows, HAS_PROPERTY_PREDICATE };
|
|
4436
|
+
const hit = await resolveRelationChase(memory, relationName, subject, object, relationChaseHelpers);
|
|
4168
4437
|
if (hit) return { text: `yes — ${hit.citation.join("; ")}`, replace: true };
|
|
4169
4438
|
// Gap 1 fix (live-tested 2026-07-09, PLAN_TAUGHT_RELATIONS.md follow-up):
|
|
4170
4439
|
// this used to `return null` unconditionally on any miss here — the
|
|
@@ -4180,10 +4449,8 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
4180
4449
|
// object) pair's chase came up short (e.g. a 2-hop rule with only 1
|
|
4181
4450
|
// hop of facts taught, or an unrelated pair) — an honest, specific
|
|
4182
4451
|
// decline that NAMES the relation, never a guessed "no".
|
|
4183
|
-
const { loadMemory: loadMemForMiss, findRuleByName: findRuleByNameForMiss } = await import("./memory/core.mjs");
|
|
4184
|
-
const memoryForMiss = await loadMemForMiss(memoryDir);
|
|
4185
4452
|
const nameKnown = relationFactsFor(relationName).length > 0
|
|
4186
|
-
|| !!
|
|
4453
|
+
|| !!findRuleByName(memory, relationName);
|
|
4187
4454
|
if (!nameKnown) {
|
|
4188
4455
|
return { text: `I don't know a relation or rule called '${relationName}' yet.`, replace: true };
|
|
4189
4456
|
}
|
|
@@ -4237,10 +4504,7 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
4237
4504
|
}
|
|
4238
4505
|
return out;
|
|
4239
4506
|
};
|
|
4240
|
-
const {
|
|
4241
|
-
loadMemory: loadMemWho, findRuleByName: findRuleByNameWho, RULE_KIND_PROP: ruleKindPropWho,
|
|
4242
|
-
RULE_KIND_COMPOSE2: composeKindWho, RULE_KIND_FILTER: filterKindWho,
|
|
4243
|
-
} = await import("./memory/core.mjs");
|
|
4507
|
+
const { loadMemory: loadMemWho, findRuleByName: findRuleByNameWho, resolveRelationChaseReverse } = await import("./memory/core.mjs");
|
|
4244
4508
|
const memoryWho = await loadMemWho(memoryDir);
|
|
4245
4509
|
// Generic REVERSE relation-NAME resolver — the mirror image of (a0)'s
|
|
4246
4510
|
// resolveRelationChase: given a relation/rule name and a FIXED OBJECT,
|
|
@@ -4249,102 +4513,13 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
4249
4513
|
// bounded the SAME way (a0)'s own chase is (§3.3): a filter rule's base
|
|
4250
4514
|
// is always either a plain relation (terminal) or another rule (one
|
|
4251
4515
|
// level deeper), never itself.
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
if (directHits.length) {
|
|
4260
|
-
const bySubject = new Map();
|
|
4261
|
-
for (const e of directHits) {
|
|
4262
|
-
if (!bySubject.has(e.fact.subject)) bySubject.set(e.fact.subject, []);
|
|
4263
|
-
bySubject.get(e.fact.subject).push(e);
|
|
4264
|
-
}
|
|
4265
|
-
return [...bySubject.entries()].map(([subj, hits]) => {
|
|
4266
|
-
const hit = hits.slice().sort((a, b) => byTrust(a.fact, b.fact))[0];
|
|
4267
|
-
return {
|
|
4268
|
-
subject: subj,
|
|
4269
|
-
citation: [renderFactLine(hit.fact), ...hit.aliasFacts.map(
|
|
4270
|
-
(af) => `${factPhrase(af)}${af.provenance ? ` (source: ${af.provenance})` : ""}`,
|
|
4271
|
-
)],
|
|
4272
|
-
};
|
|
4273
|
-
});
|
|
4274
|
-
}
|
|
4275
|
-
const rule = findRuleByNameWho(memoryWho, target);
|
|
4276
|
-
const ruleKind = rule?.attributes?.find((a) => a.prop === ruleKindPropWho)?.value;
|
|
4277
|
-
// (iii) COMPOSE2 REVERSE CHASE — the same hop-counted search (a0)'s
|
|
4278
|
-
// forward chase uses, walked BACKWARD: seed from the TARGET object,
|
|
4279
|
-
// reverse-hop via base2's edges first (the SECOND forward hop,
|
|
4280
|
-
// closest to the object), then base1's edges (the FIRST forward
|
|
4281
|
-
// hop) — swapping which side of each fact is queried (object instead
|
|
4282
|
-
// of subject) rather than building a new search kernel. Enumerates
|
|
4283
|
-
// every subject reachable at EXACTLY 2 reverse hops (never just
|
|
4284
|
-
// "reachable within budget" — the same exact-hop-count discipline
|
|
4285
|
-
// (a0)'s own isGoal uses), via findReachableSet (already proven
|
|
4286
|
-
// cycle-safe by item 6's own reachability-list wiring, (a0.5) below).
|
|
4287
|
-
if (rule && ruleKind === composeKindWho) {
|
|
4288
|
-
const base1 = rule.attributes.find((a) => a.prop === "mgx:ruleBase1")?.value;
|
|
4289
|
-
const base2 = rule.attributes.find((a) => a.prop === "mgx:ruleBase2")?.value;
|
|
4290
|
-
const targetEntity = normFactTerm(objectTerm);
|
|
4291
|
-
if (!base1 || !base2 || !targetEntity) return [];
|
|
4292
|
-
const { findReachableSet: findReachableSetWho } = await import("./planning.mjs");
|
|
4293
|
-
const applyActionsRev = (state) => {
|
|
4294
|
-
if (state.hopsTaken >= 2) return [];
|
|
4295
|
-
const relName = state.hopsTaken === 0 ? base2 : base1;
|
|
4296
|
-
return relationFactsForWho(relName)
|
|
4297
|
-
.filter((e) => e.fact.object === state.entity)
|
|
4298
|
-
.map((e) => ({ action: e, nextState: { entity: e.fact.subject, hopsTaken: state.hopsTaken + 1 } }));
|
|
4299
|
-
};
|
|
4300
|
-
const stateKeyRev = (state) => `${state.entity}#${state.hopsTaken}`;
|
|
4301
|
-
const reached = findReachableSetWho(
|
|
4302
|
-
{ entity: targetEntity, hopsTaken: 0 }, applyActionsRev, { maxDepth: 2, stateKey: stateKeyRev },
|
|
4303
|
-
);
|
|
4304
|
-
return reached.filter((r) => r.node.hopsTaken === 2).map(({ node, path }) => {
|
|
4305
|
-
const seenAlias = new Set();
|
|
4306
|
-
const parts = [];
|
|
4307
|
-
// path.actions was accumulated walking BACKWARD from the object
|
|
4308
|
-
// (base2's edge first, base1's edge second) — reversed here so
|
|
4309
|
-
// the citation reads in the natural subject-to-object order
|
|
4310
|
-
// ("ahab fathers john; …; john fathers ishmael"), matching (a0)'s
|
|
4311
|
-
// own forward-chase citation order rather than exposing the
|
|
4312
|
-
// reverse-walk's internal accumulation order to the user.
|
|
4313
|
-
for (const e of path.actions.slice().reverse()) {
|
|
4314
|
-
parts.push(renderFactLine(e.fact));
|
|
4315
|
-
for (const af of e.aliasFacts) {
|
|
4316
|
-
const key = af.id || `${af.subject}|${af.predicate}|${af.object}`;
|
|
4317
|
-
if (seenAlias.has(key)) continue;
|
|
4318
|
-
seenAlias.add(key);
|
|
4319
|
-
parts.push(`${factPhrase(af)}${af.provenance ? ` (source: ${af.provenance})` : ""}`);
|
|
4320
|
-
}
|
|
4321
|
-
}
|
|
4322
|
-
return { subject: node.entity, citation: parts };
|
|
4323
|
-
});
|
|
4324
|
-
}
|
|
4325
|
-
// (iv) FILTER REVERSE CHASE — reverse-chase the base (recursively,
|
|
4326
|
-
// same as the forward filter chase — this SAME function calls
|
|
4327
|
-
// itself), then filter the resulting subjects by whether EACH
|
|
4328
|
-
// carries the taught property.
|
|
4329
|
-
if (rule && ruleKind === filterKindWho) {
|
|
4330
|
-
const base = rule.attributes.find((a) => a.prop === "mgx:ruleBase1")?.value;
|
|
4331
|
-
const property = rule.attributes.find((a) => a.prop === "mgx:ruleFilterProperty")?.value;
|
|
4332
|
-
if (!base || !property) return [];
|
|
4333
|
-
const baseHits = await resolveRelationChaseReverse(base, objectTerm);
|
|
4334
|
-
const propertyNorm = normFactTerm(property);
|
|
4335
|
-
const out = [];
|
|
4336
|
-
for (const bh of baseHits) {
|
|
4337
|
-
const subjectEntity = normFactTerm(bh.subject);
|
|
4338
|
-
const propHit = rows.find(
|
|
4339
|
-
(f) => f.predicate === HAS_PROPERTY_PREDICATE && f.subject === subjectEntity && normFactTerm(f.object) === propertyNorm,
|
|
4340
|
-
);
|
|
4341
|
-
if (propHit) out.push({ subject: bh.subject, citation: [...bh.citation, renderFactLine(propHit)] });
|
|
4342
|
-
}
|
|
4343
|
-
return out;
|
|
4344
|
-
}
|
|
4345
|
-
return []; // no remembered fact, alias, or rule (of any kind) reaches this
|
|
4346
|
-
};
|
|
4347
|
-
const hits = await resolveRelationChaseReverse(relationName, object);
|
|
4516
|
+
// Extracted to memory/core.mjs alongside (a0)'s own resolveRelationChase
|
|
4517
|
+
// (PLAN_COMPLETIONS.md Stage 1 prerequisite — see (a0)'s own comment for
|
|
4518
|
+
// why); `relationFactsForWho`/`renderFactLine`/`factPhrase`/
|
|
4519
|
+
// `factTermVariants`/`byTrust`/`rows`/`HAS_PROPERTY_PREDICATE` are this
|
|
4520
|
+
// block's own local closures/constants, threaded through explicitly.
|
|
4521
|
+
const relationChaseHelpersWho = { relationFactsFor: relationFactsForWho, renderFactLine, factPhrase, factTermVariants, byTrust, rows, HAS_PROPERTY_PREDICATE };
|
|
4522
|
+
const hits = await resolveRelationChaseReverse(memoryWho, relationName, object, relationChaseHelpersWho);
|
|
4348
4523
|
if (hits.length) {
|
|
4349
4524
|
const lines = hits.map((h) => `${h.subject} — ${h.citation.join("; ")}`);
|
|
4350
4525
|
return { text: lines.join("\n"), replace: true };
|
|
@@ -4357,8 +4532,15 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
4357
4532
|
if (!nameKnownWho) {
|
|
4358
4533
|
return { text: `I don't know a relation or rule called '${relationName}' yet.`, replace: true };
|
|
4359
4534
|
}
|
|
4535
|
+
// HANDOVER.md 2026-07-10 item 3: "what is the capital of france" reads
|
|
4536
|
+
// oddly as "I don't know ANYONE who is the capital…" — the neutral
|
|
4537
|
+
// "nothing/anyone" split below matches whichever interrogative word the
|
|
4538
|
+
// query actually used.
|
|
4539
|
+
const isWhatAsk = /^what\b/i.test(qHedge);
|
|
4360
4540
|
return {
|
|
4361
|
-
text:
|
|
4541
|
+
text: isWhatAsk
|
|
4542
|
+
? `I don't know what the ${relationName} of ${object} is from what you've told me.`
|
|
4543
|
+
: `I don't know anyone who is the ${relationName} of ${object} from what you've told me.`,
|
|
4362
4544
|
replace: true,
|
|
4363
4545
|
};
|
|
4364
4546
|
}
|
|
@@ -4462,9 +4644,20 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
4462
4644
|
// (a) FORWARD membership — "is an X a Y". X's fact-subject candidates are the
|
|
4463
4645
|
// term itself (a class word) AND, when it resolves in the graph, its class-noun
|
|
4464
4646
|
// (an instance) — so "is app/lib/a.mjs a component" answers off "module …".
|
|
4465
|
-
|
|
4647
|
+
// A confirmation-check wrapper ("so X is a Y now right?") is rewritten to the
|
|
4648
|
+
// plain "is X a Y" form and re-tried when the raw query itself doesn't match —
|
|
4649
|
+
// see CONFIRM_TAG_RE's own docblock.
|
|
4650
|
+
const confirmTag = q.match(CONFIRM_TAG_RE);
|
|
4651
|
+
const isaAsk = q.match(ISA_ASK_RE) || (confirmTag && `is ${confirmTag[1].trim()} a ${confirmTag[2].trim()}`.match(ISA_ASK_RE));
|
|
4466
4652
|
if (isaAsk) {
|
|
4467
|
-
|
|
4653
|
+
// Playtest sprint round 1 (2026-07-10): "is TaskController a validator then"
|
|
4654
|
+
// — the same trailing bare discourse tag item 8 fixed for metaTermOf's bare
|
|
4655
|
+
// "what is X" shape also glues onto ISA_ASK_RE's captured kind term (its own
|
|
4656
|
+
// trailing anchor only allows punctuation/whitespace, not a stray word), so
|
|
4657
|
+
// "validator then" never matched any taught fact even though the CLASS↔
|
|
4658
|
+
// INSTANCE BRIDGE below would otherwise answer yes. Same stripTrailingDiscourseTag
|
|
4659
|
+
// fix, applied here too.
|
|
4660
|
+
const objVariants = factTermVariants(normFactTerm, stripTrailingDiscourseTag(isaAsk[2]));
|
|
4468
4661
|
const subjCandidates = new Set(factTermVariants(normFactTerm, isaAsk[1]));
|
|
4469
4662
|
const noun = await entityClassNoun(graph, isaAsk[1]);
|
|
4470
4663
|
if (noun) for (const v of factTermVariants(normFactTerm, noun)) subjCandidates.add(v);
|
|
@@ -4529,6 +4722,73 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
4529
4722
|
const premises = chain.map(factForStep);
|
|
4530
4723
|
if (premises.every(Boolean)) return { text: `yes — ${renderIsaChain(premises)}`, replace: true };
|
|
4531
4724
|
}
|
|
4725
|
+
// LIVE cax-dw PROOF CHASE (PLAN_INFERENCE_TESTING.md INF-B1, §4 stage 3):
|
|
4726
|
+
// every "yes" strategy above missed — check whether X's taught type
|
|
4727
|
+
// (lifted through its FULL ⊑-ancestor closure) is disjointWith the
|
|
4728
|
+
// queried class, via syllogise.mjs's deriveDisjointViolations, LIVE and
|
|
4729
|
+
// READ-ONLY (same discipline as the findIsaChain chase just above:
|
|
4730
|
+
// nothing is written; syllogise()'s materializing batch pass is the
|
|
4731
|
+
// persisting counterpart of this same rule, never on the chat hot path).
|
|
4732
|
+
// A hit here is a PROVABLE "no" — the one shape on this ladder allowed to
|
|
4733
|
+
// answer "no" from absence-of-membership rather than decline; anything
|
|
4734
|
+
// this chase can't connect through a stated disjointness falls through
|
|
4735
|
+
// to the honest miss below, never a guessed "no".
|
|
4736
|
+
const { deriveDisjointViolations, DISJOINT_PREDICATE } = await import("./syllogise.mjs");
|
|
4737
|
+
const disjointRows = rows.filter((f) => f.predicate === DISJOINT_PREDICATE && isTaught(f));
|
|
4738
|
+
if (disjointRows.length) {
|
|
4739
|
+
const disjointEdges = disjointRows.map((f) => [f.subject, f.object]);
|
|
4740
|
+
const violations = deriveDisjointViolations(chainTypeEdges, chainSubClassEdges, disjointEdges, { budget: 10 });
|
|
4741
|
+
for (const subj of subjCandidates) {
|
|
4742
|
+
const v = violations.find((vv) => vv.subject === subj && objVariants.has(vv.object));
|
|
4743
|
+
if (!v) continue;
|
|
4744
|
+
const typeFact = chainTypeRows.find((f) => f.subject === v.subject && f.object === v.viaType);
|
|
4745
|
+
const disjointFact = disjointRows.find((f) => (f.subject === v.viaClass && f.object === v.object)
|
|
4746
|
+
|| (f.subject === v.object && f.object === v.viaClass));
|
|
4747
|
+
const parts = [typeFact, disjointFact].filter(Boolean).map(renderFactLine);
|
|
4748
|
+
return { text: `no — ${parts.length ? parts.join("; ") : `${v.viaClass} and ${v.object} are disjoint.`}`, replace: true };
|
|
4749
|
+
}
|
|
4750
|
+
}
|
|
4751
|
+
// LIVE cls-svf1 PROOF CHASE (HANDOVER.md 2026-07-10 item 4,
|
|
4752
|
+
// PLAN_INFERENCE_TESTING.md INF-B2, §4 stage 4): every strategy above
|
|
4753
|
+
// missed — check whether X, having taught-P'd something of a taught type
|
|
4754
|
+
// (lifted through that type's FULL ⊑-ancestor closure), satisfies a
|
|
4755
|
+
// TAUGHT someValuesFrom restriction declared over that SAME (property,
|
|
4756
|
+
// type) pair — the restriction CLASS itself entailed (OWL 2 RL Table 8's
|
|
4757
|
+
// cls-svf1), via syllogise.mjs's deriveSomeValuesFromApplication, LIVE and
|
|
4758
|
+
// READ-ONLY (same discipline as the cax-dw chase just above: nothing is
|
|
4759
|
+
// written; syllogise()'s materializing batch pass is the persisting
|
|
4760
|
+
// counterpart of this same rule). The restriction's own scaffolding
|
|
4761
|
+
// (owl:onProperty/owl:someValuesFrom) and every property/type premise must
|
|
4762
|
+
// all be TAUGHT (never corpus-sourced), same as every other live chase in
|
|
4763
|
+
// this block.
|
|
4764
|
+
const { deriveSomeValuesFromApplication, ON_PROPERTY_PREDICATE, SOME_VALUES_FROM_PREDICATE } = await import("./syllogise.mjs");
|
|
4765
|
+
const onPropertyRows = rows.filter((f) => f.predicate === ON_PROPERTY_PREDICATE && isTaught(f));
|
|
4766
|
+
const someValuesFromRows = rows.filter((f) => f.predicate === SOME_VALUES_FROM_PREDICATE && isTaught(f));
|
|
4767
|
+
if (onPropertyRows.length && someValuesFromRows.length) {
|
|
4768
|
+
const someValuesFromOf = new Map(someValuesFromRows.map((f) => [f.subject, f.object]));
|
|
4769
|
+
const restrictionEdges = onPropertyRows
|
|
4770
|
+
.map((f) => ({ restriction: f.subject, property: f.object, target: someValuesFromOf.get(f.subject) }))
|
|
4771
|
+
.filter((r) => r.target);
|
|
4772
|
+
// Every OTHER taught object-property assertion is a candidate premise —
|
|
4773
|
+
// never hard-coded to one verb, mirroring syllogise()'s own generic
|
|
4774
|
+
// propertyEdges scan (RESERVED_PREDICATES, syllogise.mjs) minus the
|
|
4775
|
+
// predicates the other rules on this ladder already own.
|
|
4776
|
+
const svf1Reserved = new Set([SC_PREDICATE, RDF_TYPE_PREDICATE, DISJOINT_PREDICATE, ON_PROPERTY_PREDICATE, SOME_VALUES_FROM_PREDICATE, "owl:intersectionOf"]);
|
|
4777
|
+
const propertyRows = rows.filter((f) => isTaught(f) && !svf1Reserved.has(f.predicate));
|
|
4778
|
+
const propertyEdges = propertyRows.map((f) => [f.subject, f.predicate, f.object]);
|
|
4779
|
+
const svf1Derived = deriveSomeValuesFromApplication(propertyEdges, chainTypeEdges, chainSubClassEdges, restrictionEdges, { budget: 10 });
|
|
4780
|
+
for (const subj of subjCandidates) {
|
|
4781
|
+
const hit = svf1Derived.find((d) => d.subject === subj && objVariants.has(d.object));
|
|
4782
|
+
if (!hit) continue;
|
|
4783
|
+
const propFact = propertyRows.find((f) => f.subject === hit.subject && f.predicate === hit.viaProperty && f.object === hit.viaValue);
|
|
4784
|
+
const typeFact = chainTypeRows.find((f) => f.subject === hit.viaValue && f.object === hit.viaType);
|
|
4785
|
+
const parts = [propFact, typeFact].filter(Boolean).map(renderFactLine);
|
|
4786
|
+
return {
|
|
4787
|
+
text: `yes — ${parts.length ? parts.join("; ") : `${hit.subject} ${hit.viaProperty} ${hit.viaValue}, and ${hit.viaValue} is a ${hit.viaType}.`}`,
|
|
4788
|
+
replace: true,
|
|
4789
|
+
};
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4532
4792
|
return null; // no remembered fact — the honest miss stands (never a guessed "no")
|
|
4533
4793
|
}
|
|
4534
4794
|
|
|
@@ -4595,6 +4855,39 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
4595
4855
|
};
|
|
4596
4856
|
}
|
|
4597
4857
|
|
|
4858
|
+
// (a2b-iii) HAS-A-METHOD yes/no — "does/did <N1> have a/an <N2> method"
|
|
4859
|
+
// (HANDOVER.md 2026-07-10 item 9): see HAS_METHOD_YESNO_RE's own docblock
|
|
4860
|
+
// for the full design, including the confirmed pre-existing ask.mjs
|
|
4861
|
+
// structural collision when a real code graph is loaded. A hit answers
|
|
4862
|
+
// "yes"; no matching fact DECLINES (null) — never a guessed "no" (this
|
|
4863
|
+
// reader follows IS_ADJECTIVE_YESNO_RE/GENERAL_VERB_YESNO_RE's OWA
|
|
4864
|
+
// discipline below, not OWNS_YESNO_RE's closed-world "no" just above).
|
|
4865
|
+
const hasMethodYN = qHedge.match(HAS_METHOD_YESNO_RE);
|
|
4866
|
+
if (hasMethodYN) {
|
|
4867
|
+
const [, subjRaw, capRaw] = hasMethodYN;
|
|
4868
|
+
const subjVariants = factTermVariants(normFactTerm, subjRaw.trim());
|
|
4869
|
+
const objVariants = factTermVariants(normFactTerm, `${capRaw.trim()} method`);
|
|
4870
|
+
const hit = rows
|
|
4871
|
+
.filter((f) => f.predicate === HAS_A_PREDICATE && subjVariants.has(f.subject) && objVariants.has(f.object))
|
|
4872
|
+
.sort(byTrust)[0];
|
|
4873
|
+
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
4874
|
+
return null; // no remembered fact — honest decline, never a guessed "no"
|
|
4875
|
+
}
|
|
4876
|
+
|
|
4877
|
+
// (a2b-iv) HAS-A-METHOD open list — "what methods does <N1> have"
|
|
4878
|
+
// (HANDOVER.md 2026-07-10 item 9): every taught mgx:hasA fact for <N1>
|
|
4879
|
+
// whose object is a "<word> method" phrase. An honest empty (null, never a
|
|
4880
|
+
// guessed method name) when nothing was taught for this subject.
|
|
4881
|
+
const hasMethodOpen = qHedge.match(HAS_METHOD_OPEN_RE);
|
|
4882
|
+
if (hasMethodOpen) {
|
|
4883
|
+
const subjVariants = factTermVariants(normFactTerm, hasMethodOpen[1].trim());
|
|
4884
|
+
const hits = rows
|
|
4885
|
+
.filter((f) => f.predicate === HAS_A_PREDICATE && subjVariants.has(f.subject) && / method$/.test(f.object))
|
|
4886
|
+
.sort(byTrust);
|
|
4887
|
+
if (!hits.length) return null;
|
|
4888
|
+
return renderMany(hits);
|
|
4889
|
+
}
|
|
4890
|
+
|
|
4598
4891
|
// (a2c) PROPERTY yes/no — "is/are/was/were <X> <adjective>": Tier-5 playtest
|
|
4599
4892
|
// fix, found live — "remember that the logger module is deprecated" taught a
|
|
4600
4893
|
// real mgx:hasProperty fact, but there was no direct-question reader for it
|
|
@@ -4613,6 +4906,30 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
4613
4906
|
const adjective = isAdj[2].trim().toLowerCase();
|
|
4614
4907
|
if (subject) {
|
|
4615
4908
|
const subjVariants = factTermVariants(normFactTerm, subject);
|
|
4909
|
+
// CLASS↔INSTANCE BRIDGE (playtest sprint round 2, 2026-07-10): "is Task
|
|
4910
|
+
// auditable" used to say "I don't know anything about Task yet" even
|
|
4911
|
+
// with "every Record is auditable" taught and Task inheriting Record in
|
|
4912
|
+
// the code graph — this property-yes/no reader had no inheritance
|
|
4913
|
+
// bridging at all, unlike isaAsk's own CLASS↔INSTANCE BRIDGE just above
|
|
4914
|
+
// (chat.mjs's `inheritsChain`). Same bridge, same discipline: when the
|
|
4915
|
+
// subject resolves to a real graph entity, its superclass LABELS are
|
|
4916
|
+
// ADDITIONAL subject candidates, so a taught property on an ancestor
|
|
4917
|
+
// class is found too — never a guess, still just a direct fact lookup,
|
|
4918
|
+
// now over a wider (but still fact-backed) candidate set.
|
|
4919
|
+
const bridgeSubjects = new Map(); // fact-term variant → superclass label, as spelled in the graph
|
|
4920
|
+
let bridgeEnt = null;
|
|
4921
|
+
if (graph) {
|
|
4922
|
+
const ent = await resolveEntity(graph, subject);
|
|
4923
|
+
bridgeEnt = ent;
|
|
4924
|
+
if (ent) {
|
|
4925
|
+
for (const sup of inheritsChain(graph, ent.id)) {
|
|
4926
|
+
for (const v of factTermVariants(normFactTerm, sup.label)) {
|
|
4927
|
+
if (!subjVariants.has(v) && !bridgeSubjects.has(v)) bridgeSubjects.set(v, sup.label);
|
|
4928
|
+
subjVariants.add(v);
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4616
4933
|
const propertyMatch = (f) => (f.predicate === HAS_PROPERTY_PREDICATE && normFactTerm(f.object) === adjective)
|
|
4617
4934
|
|| (f.predicate === `tmct:${adjective}` && f.object === "true");
|
|
4618
4935
|
// Same head-word fallback as factAnswer's "(c) what do you know about"
|
|
@@ -4625,7 +4942,17 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
4625
4942
|
const wordOverlap = (f) => subjWords.some((w) => new Set(String(f.subject || "").split(/\s+/)).has(w));
|
|
4626
4943
|
const subjectMatch = (f) => subjVariants.has(f.subject) || (subjWords.length && wordOverlap(f));
|
|
4627
4944
|
const hit = rows.filter((f) => subjectMatch(f) && propertyMatch(f)).sort(byTrust)[0];
|
|
4628
|
-
if (hit)
|
|
4945
|
+
if (hit) {
|
|
4946
|
+
const viaSuper = bridgeSubjects.get(hit.subject);
|
|
4947
|
+
// Named explicitly (never a silent subject swap) — same honesty
|
|
4948
|
+
// discipline isaAsk's own class↔instance bridge follows just above.
|
|
4949
|
+
return {
|
|
4950
|
+
text: viaSuper
|
|
4951
|
+
? `yes — the code graph says ${bridgeEnt.label} inherits ${viaSuper}, and ${renderFactLine(hit)}`
|
|
4952
|
+
: `yes — ${renderFactLine(hit)}`,
|
|
4953
|
+
replace: true,
|
|
4954
|
+
};
|
|
4955
|
+
}
|
|
4629
4956
|
// no hit on THIS property — never a guessed "no" (see
|
|
4630
4957
|
// IS_ADJECTIVE_YESNO_RE's own docblock for why this stays silent on a
|
|
4631
4958
|
// truth claim, unlike its ownership/general-verb siblings above). But a
|
|
@@ -5061,14 +5388,16 @@ const BARE_WHATIS_RE = /^what\s+(?:is|are)\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
|
5061
5388
|
* term the same way grammar.mjs's T5 does (stripTrailingScopeFiller,
|
|
5062
5389
|
* ask-vocab.mjs) — the envelope.parsed.object branch above already carries a
|
|
5063
5390
|
* trimmed term when it came from that template, so the strip here only needs to
|
|
5064
|
-
* cover this function's own regex fallback.
|
|
5391
|
+
* cover this function's own regex fallback. HANDOVER.md 2026-07-10 item 8: a
|
|
5392
|
+
* trailing bare discourse tag ("what is a component THEN") is stripped the same
|
|
5393
|
+
* way (stripTrailingDiscourseTag) before the scope-filler strip. */
|
|
5065
5394
|
function metaTermOf(query, envelope) {
|
|
5066
5395
|
if (envelope?.parsed?.shape === "meta" && envelope.parsed.object) return envelope.parsed.object;
|
|
5067
5396
|
const q = String(query).trim();
|
|
5068
5397
|
const m = q.match(BARE_WHATIS_RE)
|
|
5069
5398
|
|| q.match(/^what\s+(?:does|do)\s+(?:an?\s+)?(.+?)\s+means?[?.!\s]*$/i)
|
|
5070
5399
|
|| q.match(/^define\s+(?:an?\s+)?(.+?)[?.!\s]*$/i);
|
|
5071
|
-
return m ? stripTrailingScopeFiller(m[1].trim()) : null;
|
|
5400
|
+
return m ? stripTrailingScopeFiller(stripTrailingDiscourseTag(m[1].trim())) : null;
|
|
5072
5401
|
}
|
|
5073
5402
|
|
|
5074
5403
|
/** The TEACH-OFFER line for a term that's genuinely unknown everywhere (Tier-5
|
|
@@ -5380,6 +5709,14 @@ async function describeWrapperAnswer(query, { config, source, focus, graph, tel
|
|
|
5380
5709
|
const m = DESCRIBE_WRAPPER_RE.exec(q) || STACCATO_PRONOUN_RE.exec(q);
|
|
5381
5710
|
let term = m?.[1]?.trim();
|
|
5382
5711
|
if (!term) return null;
|
|
5712
|
+
// HANDOVER.md 2026-07-10 item 10: "describe about X" (a doubled verb — the
|
|
5713
|
+
// "describe" branch of DESCRIBE_WRAPPER_RE never expects a following "about",
|
|
5714
|
+
// unlike its own "tell me about"/"what about" branches, which already consume
|
|
5715
|
+
// theirs inside the regex) leaves a redundant leading "about " glued to the
|
|
5716
|
+
// captured term. Stripped once, here, before any resolution — the other two
|
|
5717
|
+
// branches never leave this residue, so this can only ever help the doubled-
|
|
5718
|
+
// verb case, never change a correctly-captured term.
|
|
5719
|
+
term = term.replace(/^about\s+/i, "");
|
|
5383
5720
|
if (DESCRIBE_PRONOUN_RE.test(term)) {
|
|
5384
5721
|
if (!focus?.label) return null; // no standing focus to resolve against — honest decline
|
|
5385
5722
|
term = focus.label;
|
|
@@ -5407,6 +5744,84 @@ async function describeWrapperAnswer(query, { config, source, focus, graph, tel
|
|
|
5407
5744
|
}
|
|
5408
5745
|
}
|
|
5409
5746
|
|
|
5747
|
+
/** DETAILED-SUMMARY / EXPLAIN-IN-DETAIL closed phrasings (HANDOVER.md 2026-07-10 item
|
|
5748
|
+
* 7) — "give me a detailed summary of how the task system works" / "explain in detail
|
|
5749
|
+
* how X works" / "give me a detailed overview of X". PLAYTESTBENCH_1.4.1.md round 3
|
|
5750
|
+
* caught this EXACT phrasing hitting the plain grammar wall with NO inferred goal at
|
|
5751
|
+
* all, even though src/completions/'s extractive multi-sentence pipeline (Stages 0-3,
|
|
5752
|
+
* built and unit-tested the same session) already existed and could answer it when
|
|
5753
|
+
* called directly — it was simply unreachable from any real chat turn.
|
|
5754
|
+
*
|
|
5755
|
+
* Two closed shapes, deliberately narrow (this project's own discipline: curated
|
|
5756
|
+
* closed patterns, never a general "any long question" catch-all):
|
|
5757
|
+
* 1. DETAILED_HOW_WORKS_RE — "...detailed (summary|overview|explanation) of how X
|
|
5758
|
+
* works" / "explain ... in detail how X works" — the "how X works" shape
|
|
5759
|
+
* PLAYTESTBENCH's own probe used. Tried FIRST (its "works" anchor is strictly
|
|
5760
|
+
* more specific, so it must win over #2 whenever both could parse).
|
|
5761
|
+
* 2. DETAILED_OVERVIEW_RE — "...detailed (overview|summary|explanation) of X" — the
|
|
5762
|
+
* bare-subject sibling, no "how...works" wrapper.
|
|
5763
|
+
* Distinct from DESCRIBE_WRAPPER_RE (a single-answer "one definition" lane, anchored
|
|
5764
|
+
* on "tell me about"/"describe"/"what about") — neither of these two anchors on
|
|
5765
|
+
* "give me"/"explain ... in detail", so there is no overlap to shadow. */
|
|
5766
|
+
const DETAILED_HOW_WORKS_RE =
|
|
5767
|
+
/^(?:(?:can|could|would)\s+you\s+(?:please\s+)?|please\s+)?(?:give\s+me\s+a\s+detailed\s+(?:summary|overview|explanation)\s+of\s+how|explain\s+(?:to\s+me\s+)?in\s+detail\s+how)\s+(.+?)\s+works\s*\??$/i;
|
|
5768
|
+
|
|
5769
|
+
const DETAILED_OVERVIEW_RE =
|
|
5770
|
+
/^(?:(?:can|could|would)\s+you\s+(?:please\s+)?|please\s+)?give\s+me\s+a\s+detailed\s+(?:overview|summary|explanation)\s+of\s+(.+?)\s*\??$/i;
|
|
5771
|
+
|
|
5772
|
+
/** THE COMPLETIONS RESCUE (HANDOVER.md 2026-07-10 item 7) — wires src/completions/'s
|
|
5773
|
+
* extractive, cited, groundedness-checked multi-sentence pipeline (generateCompletion(),
|
|
5774
|
+
* src/completions/complete.mjs) into live chat dispatch. Tried in runAsk ONLY after
|
|
5775
|
+
* (4d) DESCRIBE-WRAPPER RESCUE (and everything above it) has already declined — the
|
|
5776
|
+
* same "last-resort lane" discipline synonymFactAnswer's and describeWrapperAnswer's
|
|
5777
|
+
* own docblocks each spell out: "describe X" / "tell me about X" phrasings must keep
|
|
5778
|
+
* reaching describeWrapperAnswer's (or the relation force's) single-answer rescue
|
|
5779
|
+
* unmolested; this lane only ever claims a turn shaped as an EXPLICIT request for a
|
|
5780
|
+
* detailed/multi-sentence account (DETAILED_HOW_WORKS_RE / DETAILED_OVERVIEW_RE,
|
|
5781
|
+
* above), a shape neither DESCRIBE_WRAPPER_RE nor vagueTouchTermOf recognizes.
|
|
5782
|
+
*
|
|
5783
|
+
* Honest by construction: generateCompletion() itself declines (returns
|
|
5784
|
+
* `declined:true`, empty text) whenever nothing in the corpus/graph clears its own
|
|
5785
|
+
* pruning bar for the term (PLAN_COMPLETIONS.md §3's honest ceiling) — this lane
|
|
5786
|
+
* passes that decline straight through as null, falling through to the ordinary miss
|
|
5787
|
+
* below. NEVER fabricates. Lazy + failure-tolerated (dynamic import, try/catch) like
|
|
5788
|
+
* every other lane in this file. src/completions/ itself is untouched by this change —
|
|
5789
|
+
* this is the call site only. */
|
|
5790
|
+
async function completionsRescueAnswer(query, { memoryDir, graph }) {
|
|
5791
|
+
if (!memoryDir) return null; // no repo/memory to search — honest decline
|
|
5792
|
+
// Deliberately NOT applyPreambleFrames here (unlike describeWrapperAnswer just
|
|
5793
|
+
// above) — found live while wiring this lane: its own SHOW_GIVE_ME_RE frame turns
|
|
5794
|
+
// ANY "give me (the)? X" into "describe X" before this lane would ever see it,
|
|
5795
|
+
// which is exactly right for describeWrapperAnswer (DESCRIBE_WRAPPER_RE has its own
|
|
5796
|
+
// "describe " branch to catch that) but SILENTLY DESTROYS this lane's own
|
|
5797
|
+
// "give me a detailed summary/overview of ..." anchor — "give me a detailed summary
|
|
5798
|
+
// of how the Widget works" became "describe a detailed summary of how the Widget
|
|
5799
|
+
// works" and neither DETAILED_HOW_WORKS_RE nor DETAILED_OVERVIEW_RE could match it
|
|
5800
|
+
// anymore, so the turn silently fell through to the plain grammar wall exactly like
|
|
5801
|
+
// before this lane existed. Matching the RAW trimmed query instead is safe: this
|
|
5802
|
+
// lane's own two regexes already carry their own optional "can/could/would you
|
|
5803
|
+
// (please)?"/"please" politeness prefix (mirroring DESCRIBE_WRAPPER_RE's own), so no
|
|
5804
|
+
// separate normalization pass is needed for the phrasings this lane targets.
|
|
5805
|
+
const q = String(query || "").trim();
|
|
5806
|
+
const m = DETAILED_HOW_WORKS_RE.exec(q) || DETAILED_OVERVIEW_RE.exec(q);
|
|
5807
|
+
let term = m?.[1]?.trim();
|
|
5808
|
+
if (!term) return null;
|
|
5809
|
+
// same bare-article strip describeWrapperAnswer's resolveSymbol-facing branch
|
|
5810
|
+
// already uses just above — pure retrieval/ranking noise, never a real content
|
|
5811
|
+
// signal, and stripping it only ever REMOVES characters, never changes a
|
|
5812
|
+
// correctly-captured term.
|
|
5813
|
+
term = term.replace(/^(?:the|a|an)\s+/i, "").trim();
|
|
5814
|
+
if (!term) return null;
|
|
5815
|
+
try {
|
|
5816
|
+
const { generateCompletion } = await import("./completions/complete.mjs");
|
|
5817
|
+
const result = await generateCompletion(memoryDir, term, { query: term, graph });
|
|
5818
|
+
if (!result || result.declined || !result.text) return null; // honest decline — never fabricate
|
|
5819
|
+
return { text: result.text };
|
|
5820
|
+
} catch {
|
|
5821
|
+
return null; // unresolvable/errored — decline, the ordinary wall stands unchanged
|
|
5822
|
+
}
|
|
5823
|
+
}
|
|
5824
|
+
|
|
5410
5825
|
/** THE RELATION CONCEPT FORCE — compose the three-band answer (curated relation
|
|
5411
5826
|
* definition + real example EDGES + pre-validated follow-ups) for a vague touch on a
|
|
5412
5827
|
* relation/edge kind ("what about imports", "what are the calls", "tell me about
|
|
@@ -5869,9 +6284,43 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
5869
6284
|
const bareWhatisShape = BARE_WHATIS_RE.test(String(query).trim());
|
|
5870
6285
|
const isAdjectiveShape = IS_ADJECTIVE_YESNO_RE.test(String(query).trim());
|
|
5871
6286
|
let bareMetaHit = null;
|
|
5872
|
-
if (isConversationalCandidate &&
|
|
5873
|
-
|
|
5874
|
-
|
|
6287
|
+
if (isConversationalCandidate && (bareWhatisShape || isAdjectiveShape)) {
|
|
6288
|
+
if (memoryDir) {
|
|
6289
|
+
bareMetaHit = (await factAnswer(memoryDir, query, envelope, miss, biasByBundle))
|
|
6290
|
+
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label, biasByBundle));
|
|
6291
|
+
// HANDOVER.md 2026-07-10 item 10 (dropped-article gap): a bare "what is X"
|
|
6292
|
+
// with NO taught fact but a KNOWN curated corpus term ("what is cache", no
|
|
6293
|
+
// article) used to lose this exact same isConversationalCandidate race —
|
|
6294
|
+
// curatedDefinitionAnswer was only ever reached once the article made T5's
|
|
6295
|
+
// structural parse succeed (envelope.parsed non-null), never on the bare
|
|
6296
|
+
// form. Same "only diverts on a REAL hit" discipline as the rest of this
|
|
6297
|
+
// lane — an unknown bare term still falls through to the ordinary
|
|
6298
|
+
// orientation card, exactly as before.
|
|
6299
|
+
if (!bareMetaHit) {
|
|
6300
|
+
const def = await curatedDefinitionAnswer(query, envelope, { memoryDir, lexicon });
|
|
6301
|
+
if (def) bareMetaHit = { text: def.text, replace: true };
|
|
6302
|
+
}
|
|
6303
|
+
}
|
|
6304
|
+
// HANDOVER.md 2026-07-10 item 6 (CHATBENCH g-a2-naming-2: "what is Widget",
|
|
6305
|
+
// no article): a bare "what is X" naming a REAL code-graph entity (Class/
|
|
6306
|
+
// Function/Method/GlobalVariable/Attribute — not a taught fact, not a
|
|
6307
|
+
// curated corpus term) lost this SAME isConversationalCandidate race too.
|
|
6308
|
+
// metaFallbackEntityAnswer (ask.mjs) is the exact fallback the ARTICLED
|
|
6309
|
+
// form's structural parse already reaches once T5 succeeds; tried here so
|
|
6310
|
+
// the bare form gets the byte-identical answer, never a worse one just
|
|
6311
|
+
// because it dropped the article. Deliberately OUTSIDE the `memoryDir`
|
|
6312
|
+
// check above — this is a pure graph lookup, no memory/Facts access
|
|
6313
|
+
// needed, and CHATBENCH's own "turns" replay mode drives runTurn with a
|
|
6314
|
+
// graph but no memoryDir at all, so gating this on memoryDir too would
|
|
6315
|
+
// silently never fire in the one harness this fix specifically targets.
|
|
6316
|
+
if (!bareMetaHit && graph) {
|
|
6317
|
+
const term = metaTermOf(query, envelope);
|
|
6318
|
+
if (term) {
|
|
6319
|
+
const { metaFallbackEntityAnswer } = await import("./ask.mjs");
|
|
6320
|
+
const fallback = metaFallbackEntityAnswer(graph, term);
|
|
6321
|
+
if (fallback) bareMetaHit = { text: fallback.text, replace: true };
|
|
6322
|
+
}
|
|
6323
|
+
}
|
|
5875
6324
|
}
|
|
5876
6325
|
if (bareMetaHit) {
|
|
5877
6326
|
answer = bareMetaHit.replace ? bareMetaHit.text : `${answer}\n${bareMetaHit.text}`;
|
|
@@ -6122,6 +6571,26 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
6122
6571
|
note(trace, "goal: get a symbol's definition/kind/relations (phrased conversationally)");
|
|
6123
6572
|
}
|
|
6124
6573
|
}
|
|
6574
|
+
// (4e) COMPLETIONS RESCUE (HANDOVER.md 2026-07-10 item 7) — wires src/completions/'s
|
|
6575
|
+
// extractive multi-sentence pipeline in as a genuine last-resort lane, tried ONLY
|
|
6576
|
+
// here, after EVERY lane above (including (4d) DESCRIBE-WRAPPER RESCUE) has already
|
|
6577
|
+
// declined — "describe X"/"tell me about X" must keep reaching describeWrapperAnswer's
|
|
6578
|
+
// (or the relation force's) single-answer rescue unmolested; this lane only fires for
|
|
6579
|
+
// an EXPLICIT "detailed summary/overview of how X works" phrasing
|
|
6580
|
+
// (DETAILED_HOW_WORKS_RE/DETAILED_OVERVIEW_RE), a shape neither DESCRIBE_WRAPPER_RE
|
|
6581
|
+
// nor vagueTouchTermOf recognizes. PLAYTESTBENCH_1.4.1.md round 3's own
|
|
6582
|
+
// architecturally-confirmed gap: this exact phrasing hit the plain grammar wall with
|
|
6583
|
+
// no inferred goal at all, even though the pipeline that could answer it already
|
|
6584
|
+
// existed — it was simply unreachable from any real chat turn.
|
|
6585
|
+
if (miss && recordMiss && via === "composed") {
|
|
6586
|
+
const completed = await completionsRescueAnswer(query, { memoryDir, graph });
|
|
6587
|
+
if (completed) {
|
|
6588
|
+
answer = completed.text; via = "completion"; recordMiss = false;
|
|
6589
|
+
note(trace, "lane: (4e) COMPLETIONS RESCUE — a \"detailed summary/overview of how X works\" phrasing matched, answered via src/completions/'s extractive multi-sentence pipeline (generateCompletion())");
|
|
6590
|
+
note(trace, "source: src/completions/complete.mjs generateCompletion() (broadSearch + groupHits + rankSentences + inferRelations + pruneCompletion + finish())");
|
|
6591
|
+
note(trace, "goal: produce a grounded, cited, multi-sentence account of the subject (not a single fact/definition)");
|
|
6592
|
+
}
|
|
6593
|
+
}
|
|
6125
6594
|
// (5) #1 SHORT TAILORED MISS — replace ONLY the engine's full grammar cheat-sheet
|
|
6126
6595
|
// wall (WALL_MISS_RE). Receipt-bearing misses keep their specific wording.
|
|
6127
6596
|
// WALL KINDNESS (0.8.2 WS4 (a)): when the PREVIOUS turn's answer was already a
|