@polycode-projects/the-mechanical-code-talker 1.8.20 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -6
- package/ROADMAP.md +1 -1
- package/bin/tmct.mjs +167 -20
- package/corpus/generated/README.md +2 -2
- package/corpus/namenet/LICENSE-NOTICE +68 -0
- package/corpus/namenet/generate.mjs +309 -0
- package/corpus/namenet/manifest.json +20 -0
- package/corpus/namenet/namenet.jsonl +7260 -0
- package/corpus/wordnet/LICENSE-NOTICE +49 -0
- package/corpus/wordnet/generate.mjs +333 -0
- package/corpus/wordnet/manifest.json +34 -0
- package/corpus/wordnet/wordnet-full.jsonl +192498 -0
- package/corpus/wordnet/wordnet-xl.jsonl +23805 -0
- package/package.json +6 -2
- package/src/ask-browser-entry.mjs +13 -2
- package/src/ask-browser.bundle.js +272 -19
- package/src/chat.mjs +229 -82
- package/src/cli-args.mjs +20 -1
- package/src/codegraph.mjs +306 -1
- package/src/corpus/conceptnet-map.toml +17 -12
- package/src/corpus/conceptnet.mjs +23 -1
- package/src/extensions.mjs +44 -1
- package/src/init.mjs +99 -46
- package/src/interpret/normalize.mjs +1 -3
- package/src/memory/core.mjs +191 -14
- package/src/memory/trust.mjs +27 -6
- package/src/memory-ask-browser-entry.mjs +36 -0
- package/src/memory-ask-browser.bundle.js +5544 -0
- package/src/toml-config.mjs +11 -1
- package/src/viz.mjs +548 -73
package/src/chat.mjs
CHANGED
|
@@ -677,7 +677,7 @@ async function answerEdgeCount(graph, query) {
|
|
|
677
677
|
* Consulted only when answerCount can't map the noun to a graph class (an unknown
|
|
678
678
|
* kind) AND a session's memory is in hand. Returns the count string or null (no
|
|
679
679
|
* such fact → the honest "I can't count …" from answerCount stands). */
|
|
680
|
-
async function countFromFacts(graph, memoryDir, query, biasByBundle = {}) {
|
|
680
|
+
async function countFromFacts(graph, memoryDir, query, biasByBundle = {}, cache = null) {
|
|
681
681
|
if (!graph || !memoryDir) return null;
|
|
682
682
|
const m = String(query).match(/\b(?:how many|number of|count(?:\s+the)?)\s+([a-z]+)\b/i);
|
|
683
683
|
if (!m) return null;
|
|
@@ -686,7 +686,7 @@ async function countFromFacts(graph, memoryDir, query, biasByBundle = {}) {
|
|
|
686
686
|
let normFactTerm;
|
|
687
687
|
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
688
688
|
const objVariants = factTermVariants(normFactTerm, asked);
|
|
689
|
-
const isa = (await factRows(memoryDir))
|
|
689
|
+
const isa = (await factRows(memoryDir, cache))
|
|
690
690
|
.filter((f) => ISA_PREDICATES.has(f.predicate) && objVariants.has(f.object));
|
|
691
691
|
// pick the highest-bias, then highest-trust asserted subject that maps to a
|
|
692
692
|
// countable graph class (rankByBiasThenTrust: bias-tied/unconfigured degrades
|
|
@@ -721,7 +721,7 @@ async function countFromFacts(graph, memoryDir, query, biasByBundle = {}) {
|
|
|
721
721
|
// graph-cardinality count untouched — same honest-decline discipline as
|
|
722
722
|
// every other lane here).
|
|
723
723
|
const HOW_MANY_ARE_RE = /^how\s+many\s+([\w-]+)\s+(?:are|is)\s+(.+?)[?.!\s]*$/i;
|
|
724
|
-
async function answerQuantifierRecall(memoryDir, query, biasByBundle = {}) {
|
|
724
|
+
async function answerQuantifierRecall(memoryDir, query, biasByBundle = {}, cache = null) {
|
|
725
725
|
if (!memoryDir) return null;
|
|
726
726
|
const m = String(query).trim().match(HOW_MANY_ARE_RE);
|
|
727
727
|
if (!m) return null;
|
|
@@ -730,7 +730,7 @@ async function answerQuantifierRecall(memoryDir, query, biasByBundle = {}) {
|
|
|
730
730
|
let normFactTerm;
|
|
731
731
|
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
732
732
|
const subjVariants = factTermVariants(normFactTerm, asked);
|
|
733
|
-
const rows = (await factRows(memoryDir)).filter((f) => ISA_PREDICATES.has(f.predicate) && subjVariants.has(f.subject));
|
|
733
|
+
const rows = (await factRows(memoryDir, cache)).filter((f) => ISA_PREDICATES.has(f.predicate) && subjVariants.has(f.subject));
|
|
734
734
|
if (!rows.length) return null; // never heard of this subject at all — let answerCount own the shape
|
|
735
735
|
const objVariants = factTermVariants(normFactTerm, m[2]);
|
|
736
736
|
const hit = rankByBiasThenTrust(rows.filter((f) => objVariants.has(f.object)), biasByBundle)[0];
|
|
@@ -2067,7 +2067,7 @@ const GENERIC_ANCHOR_NOUNS = new Set(["thing", "concept", "object", "entity"]);
|
|
|
2067
2067
|
* fact-grounded term matches under the EXACT spelling teachFact itself stored
|
|
2068
2068
|
* it under. Failure-tolerated: no memory dir / no match → false, never a
|
|
2069
2069
|
* guessed "yes". */
|
|
2070
|
-
async function isGroundedByFact(term, memoryDir) {
|
|
2070
|
+
async function isGroundedByFact(term, memoryDir, cache = null) {
|
|
2071
2071
|
if (!memoryDir) return false;
|
|
2072
2072
|
const raw = String(term ?? "").trim();
|
|
2073
2073
|
if (!raw) return false;
|
|
@@ -2083,7 +2083,7 @@ async function isGroundedByFact(term, memoryDir) {
|
|
|
2083
2083
|
// OPERATOR actually taught (or a prior `tmct syllogise` entailment) anchors
|
|
2084
2084
|
// a term here. factRows (not memoryFacts) is used specifically because it's
|
|
2085
2085
|
// the one read path that carries sourceTypes for this filter.
|
|
2086
|
-
const rows = await factRows(memoryDir);
|
|
2086
|
+
const rows = await factRows(memoryDir, cache);
|
|
2087
2087
|
const isTaught = (f) => !f.sourceTypes?.includes("corpus") && !f.sourceTypes?.includes("web");
|
|
2088
2088
|
return rows.some((f) => MINT_ISA_PREDICATES.has(f.predicate) && isTaught(f) && (f.subject === t || f.object === t));
|
|
2089
2089
|
}
|
|
@@ -2098,13 +2098,13 @@ async function isGroundedByFact(term, memoryDir) {
|
|
|
2098
2098
|
* narrower and NOUN-specific — see its own comment — so an object that's
|
|
2099
2099
|
* merely a known ADJECTIVE doesn't get misrouted into the class/subClassOf
|
|
2100
2100
|
* branch instead of the property branch.) */
|
|
2101
|
-
async function isGroundedTerm(term, lex, memoryDir) {
|
|
2101
|
+
async function isGroundedTerm(term, lex, memoryDir, cache = null) {
|
|
2102
2102
|
const raw = String(term ?? "").trim();
|
|
2103
2103
|
if (!raw) return false;
|
|
2104
2104
|
if (GENERIC_ANCHOR_NOUNS.has(raw.toLowerCase())) return true;
|
|
2105
2105
|
const { classify } = await import("./grammar/lexicon.mjs");
|
|
2106
2106
|
if (classify(raw, lex)) return true;
|
|
2107
|
-
return isGroundedByFact(raw, memoryDir);
|
|
2107
|
+
return isGroundedByFact(raw, memoryDir, cache);
|
|
2108
2108
|
}
|
|
2109
2109
|
|
|
2110
2110
|
/** The "both sides ungrounded" grounding NUDGE (operator refinement,
|
|
@@ -2127,15 +2127,15 @@ async function isGroundedTerm(term, lex, memoryDir) {
|
|
|
2127
2127
|
* unchanged) whenever the payload doesn't fit the shape, or at least one
|
|
2128
2128
|
* side IS already grounded — a DIFFERENT, more specific reason it declined,
|
|
2129
2129
|
* where this nudge would be actively unhelpful noise. */
|
|
2130
|
-
async function ungroundedPairHint(payload, lexicon, memoryDir) {
|
|
2130
|
+
async function ungroundedPairHint(payload, lexicon, memoryDir, cache = null) {
|
|
2131
2131
|
if (!memoryDir) return "";
|
|
2132
2132
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
2133
2133
|
if (!m) return "";
|
|
2134
2134
|
const [, , subjectRaw, , objectRaw] = m;
|
|
2135
2135
|
const { loadLexicon } = await import("./grammar/lexicon.mjs");
|
|
2136
2136
|
const lex = lexicon || loadLexicon();
|
|
2137
|
-
if (await isGroundedTerm(subjectRaw, lex, memoryDir)) return "";
|
|
2138
|
-
if (await isGroundedTerm(objectRaw, lex, memoryDir)) return "";
|
|
2137
|
+
if (await isGroundedTerm(subjectRaw, lex, memoryDir, cache)) return "";
|
|
2138
|
+
if (await isGroundedTerm(objectRaw, lex, memoryDir, cache)) return "";
|
|
2139
2139
|
// 2026-07-10 (found live via SKILL_BENCHMARK_CONVERSATION.md playtest, a
|
|
2140
2140
|
// classic first-thing-a-user-tries example: "john is a man"): the original
|
|
2141
2141
|
// suggestion chained the second term UNDER the first's now-grounded proper
|
|
@@ -2179,7 +2179,7 @@ async function ungroundedPairHint(payload, lexicon, memoryDir) {
|
|
|
2179
2179
|
* the SUBJECT, not about the "remember that" wrapper). Only the "every"
|
|
2180
2180
|
* determiner records a quantifier (point 3: "a"/bare/"your" read as one
|
|
2181
2181
|
* specific entity, not a class-level generalization). */
|
|
2182
|
-
async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }) {
|
|
2182
|
+
async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }, cache = null) {
|
|
2183
2183
|
if (!memoryDir) return null;
|
|
2184
2184
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
2185
2185
|
if (!m) return null;
|
|
@@ -2211,7 +2211,7 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }
|
|
|
2211
2211
|
// lexicon noun — both are always treated as class-level (never property),
|
|
2212
2212
|
// consistent with unknownObjectFallback (below) always minting a CLASS.
|
|
2213
2213
|
if (lookupNoun(lex, objectRaw) || GENERIC_ANCHOR_NOUNS.has(String(objectRaw).toLowerCase())
|
|
2214
|
-
|| (await isGroundedByFact(objectRaw, memoryDir))) {
|
|
2214
|
+
|| (await isGroundedByFact(objectRaw, memoryDir, cache))) {
|
|
2215
2215
|
return teachFact(memoryDir, sessionId, {
|
|
2216
2216
|
subject, predicate: SUBCLASS_PREDICATE, object: objectRaw, quantifier,
|
|
2217
2217
|
});
|
|
@@ -2297,7 +2297,7 @@ async function objectReadsAsNonNoun(word) {
|
|
|
2297
2297
|
return false;
|
|
2298
2298
|
}
|
|
2299
2299
|
}
|
|
2300
|
-
async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon }) {
|
|
2300
|
+
async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon }, cache = null) {
|
|
2301
2301
|
if (!memoryDir) return null;
|
|
2302
2302
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
2303
2303
|
if (!m) return null;
|
|
@@ -2305,9 +2305,9 @@ async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon })
|
|
|
2305
2305
|
if (!/^(?:every|each|all)$/i.test((det || "").trim())) return null; // class-level mint needs a real universal quantifier
|
|
2306
2306
|
const { loadLexicon, lookupNoun } = await import("./grammar/lexicon.mjs");
|
|
2307
2307
|
const lex = lexicon || loadLexicon();
|
|
2308
|
-
const subjectGrounded = await isGroundedTerm(subjectRaw, lex, memoryDir);
|
|
2308
|
+
const subjectGrounded = await isGroundedTerm(subjectRaw, lex, memoryDir, cache);
|
|
2309
2309
|
if (!subjectGrounded) return null; // ungrounded subject isn't this fallback's asymmetry — never a guessed mint
|
|
2310
|
-
const objectGrounded = await isGroundedTerm(objectRaw, lex, memoryDir);
|
|
2310
|
+
const objectGrounded = await isGroundedTerm(objectRaw, lex, memoryDir, cache);
|
|
2311
2311
|
if (objectGrounded) return null; // object already known — nothing to mint
|
|
2312
2312
|
if (await objectReadsAsNonNoun(objectRaw)) return null; // reads like an adjective/verb, not a class noun — defer to unknownAdjectiveFallback
|
|
2313
2313
|
const quantifier = /^every$/i.test((det || "").trim()) ? "every" : "";
|
|
@@ -2399,7 +2399,7 @@ async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon })
|
|
|
2399
2399
|
* otherwise provide. "the cache is bespoke" and "Mary is female" both carry
|
|
2400
2400
|
* one of those signals (the leading "the", and capitalization,
|
|
2401
2401
|
* respectively); "module is banana" carries none. */
|
|
2402
|
-
async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon }) {
|
|
2402
|
+
async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon }, cache = null) {
|
|
2403
2403
|
if (!memoryDir) return null;
|
|
2404
2404
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
2405
2405
|
if (!m) return null;
|
|
@@ -2410,7 +2410,7 @@ async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon
|
|
|
2410
2410
|
// membership sentence, unknownSubjectFallback/unknownObjectFallback's own
|
|
2411
2411
|
// territory (already had first refusal on it) — never misread as a property.
|
|
2412
2412
|
if (lookupNoun(lex, objectRaw) || GENERIC_ANCHOR_NOUNS.has(String(objectRaw).toLowerCase())
|
|
2413
|
-
|| (await isGroundedByFact(objectRaw, memoryDir))) return null;
|
|
2413
|
+
|| (await isGroundedByFact(objectRaw, memoryDir, cache))) return null;
|
|
2414
2414
|
// Subject-side groundedness — strip a leading "the"/"a"/"an" first
|
|
2415
2415
|
// (normFactTerm's own article-strip, mirrored here) so "the cache" checks
|
|
2416
2416
|
// groundedness under its real head noun "cache", the same spelling
|
|
@@ -2418,7 +2418,7 @@ async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon
|
|
|
2418
2418
|
const bareSubject = subjectRaw.replace(/^(?:the|an?)\s+/i, "").trim() || subjectRaw;
|
|
2419
2419
|
const hadArticle = bareSubject !== subjectRaw;
|
|
2420
2420
|
const capitalized = /^[A-Z]/.test(bareSubject);
|
|
2421
|
-
const factGrounded = await isGroundedByFact(bareSubject, memoryDir);
|
|
2421
|
+
const factGrounded = await isGroundedByFact(bareSubject, memoryDir, cache);
|
|
2422
2422
|
const genericAnchor = GENERIC_ANCHOR_NOUNS.has(bareSubject.toLowerCase());
|
|
2423
2423
|
// A bare (no article, no capitalization) subject grounded ONLY via the
|
|
2424
2424
|
// static lexicon is exactly the pinned "module is banana" shape — see this
|
|
@@ -2745,7 +2745,7 @@ const RETRACT_NOT_A_RE = /^(?:a\s+|an\s+)?([\w-]+(?:\s+[\w-]+)?)\s+(?:(?:is|are)
|
|
|
2745
2745
|
* is tried against the remember-wrapped surface too. */
|
|
2746
2746
|
const RETRACT_FORGET_RE = /^forget\s+(?:that\s+)?(?:a\s+|an\s+)?([\w-]+(?:\s+[\w-]+)?)\s+(?:is|are)\s+(?:an?\s+)?(?:(?:kind|type)\s+of\s+)?([\w-]+)$/i;
|
|
2747
2747
|
|
|
2748
|
-
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
2748
|
+
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null }) {
|
|
2749
2749
|
// Tier 6 playtest: this lane read the raw, un-normalized query, so a closed
|
|
2750
2750
|
// discourse-marker preamble ahead of a teach sentence ("howdy pardner,
|
|
2751
2751
|
// remember that TaskController is fragile") corrupted TEACH_RE's own match —
|
|
@@ -3174,7 +3174,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
3174
3174
|
// assertTurn ITSELF records the "every" quantifier (point 3) on a plain
|
|
3175
3175
|
// universal success, so every caller (this loop AND the top-level
|
|
3176
3176
|
// declarative-sentence dispatch in runTurn) gets it uniformly.
|
|
3177
|
-
const stored = await assertTurn(cand, { memoryDir, sessionId, focus: null, lexicon });
|
|
3177
|
+
const stored = await assertTurn(cand, { memoryDir, sessionId, focus: null, lexicon, cache });
|
|
3178
3178
|
if (stored) return { text: stored.answer, via: "assert", miss: false };
|
|
3179
3179
|
}
|
|
3180
3180
|
// BUG "redis" fix (Feature A point 1): the real ACE grammar just declined
|
|
@@ -3183,7 +3183,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
3183
3183
|
// Covers BOTH the bare and the wrapped surface (payload is already
|
|
3184
3184
|
// unwrapped either way) — see unknownSubjectFallback's own docblock for
|
|
3185
3185
|
// the exact narrowing rules (object must still be known, etc.).
|
|
3186
|
-
const fallback = await unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon });
|
|
3186
|
+
const fallback = await unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }, cache);
|
|
3187
3187
|
if (fallback) return fallback;
|
|
3188
3188
|
// MIRROR mint fallback (Feature A, 2026-07-09 operator-authorized vocabulary-
|
|
3189
3189
|
// growth extension): the known-subject/unknown-object asymmetry — tried
|
|
@@ -3191,7 +3191,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
3191
3191
|
// lexicon (or a prior taught fact) already grounds can mint a brand-new
|
|
3192
3192
|
// object term. See unknownObjectFallback's own docblock for the exact
|
|
3193
3193
|
// narrowing rules (the "both sides ungrounded" safety guard, etc.).
|
|
3194
|
-
const objectFallback = await unknownObjectFallback(payload, { memoryDir, sessionId, lexicon });
|
|
3194
|
+
const objectFallback = await unknownObjectFallback(payload, { memoryDir, sessionId, lexicon }, cache);
|
|
3195
3195
|
if (objectFallback) return objectFallback;
|
|
3196
3196
|
// ADJECTIVE-MINT fallback (PLAN_TAUGHT_RELATIONS.md Item 5, Phase 1): tried
|
|
3197
3197
|
// right after unknownObjectFallback declines, so a grounded subject (static
|
|
@@ -3200,7 +3200,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
3200
3200
|
// docblock for the exact narrowing rules (the "both sides ungrounded"
|
|
3201
3201
|
// safety guard, and why this must be a standalone function rather than
|
|
3202
3202
|
// nested inside unknownSubjectFallback).
|
|
3203
|
-
const adjectiveFallback = await unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon });
|
|
3203
|
+
const adjectiveFallback = await unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon }, cache);
|
|
3204
3204
|
if (adjectiveFallback) return adjectiveFallback;
|
|
3205
3205
|
// PROPERTY teach — "remember/note that <X> is <adjective>": wrapper-REQUIRED
|
|
3206
3206
|
// (a bare "X is deprecated" is never silently reified), and only after the
|
|
@@ -3271,7 +3271,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
3271
3271
|
// replacement, exactly like "did" above — see ungroundedPairHint's own
|
|
3272
3272
|
// docblock for why this is scoped to the "both sides ungrounded, fits the
|
|
3273
3273
|
// X is/are Y shape" case only.
|
|
3274
|
-
const groundingHint = await ungroundedPairHint(payload, lexicon, memoryDir);
|
|
3274
|
+
const groundingHint = await ungroundedPairHint(payload, lexicon, memoryDir, cache);
|
|
3275
3275
|
return {
|
|
3276
3276
|
text: `I couldn't store that —${why} I remember facts in the shape "every X is a Y", where X and Y are `
|
|
3277
3277
|
+ `words I know.${did}${groundingHint} Type /memory to see what I already remember.`,
|
|
@@ -4044,6 +4044,11 @@ const FACT_PREDICATE_PHRASES = {
|
|
|
4044
4044
|
"mgx:hasLastSubevent": "ends with",
|
|
4045
4045
|
"mgx:hasPrerequisite": "requires",
|
|
4046
4046
|
"mgx:ownedBy": "is owned by", // the teach lane's ownership frame ("Priya owns tasks.mjs")
|
|
4047
|
+
"mgx:synonym": "means the same as",
|
|
4048
|
+
"mgx:antonym": "is the opposite of",
|
|
4049
|
+
"mgx:similarTo": "is similar to",
|
|
4050
|
+
"mgx:relatedTo": "is related to",
|
|
4051
|
+
"mgx:symbolOf": "is a symbol of",
|
|
4047
4052
|
};
|
|
4048
4053
|
|
|
4049
4054
|
/** Bug 3 (2026-07-09) point 3b: the MECHANICAL fallback for a predicate this
|
|
@@ -4138,16 +4143,26 @@ function splitMetaPredicate(term) {
|
|
|
4138
4143
|
|
|
4139
4144
|
/** One rendered fact line. An OPERATOR-asserted fact keeps the true first-person
|
|
4140
4145
|
* provenance ("you told me: …"). A CORPUS fact is presented as clean DATA with its
|
|
4141
|
-
* source cited
|
|
4142
|
-
*
|
|
4143
|
-
* for themselves.
|
|
4146
|
+
* source cited, not "i learned: …" — that phrase over-claims and anthropomorphises
|
|
4147
|
+
* a first-person experience the bot never had; the relation and its provenance
|
|
4148
|
+
* speak for themselves. A WEAK-corpus fact (memory/trust.mjs SOURCE_PRIOR.corpusWeak
|
|
4149
|
+
* — real data, low-precision relation, e.g. ConceptNet's undirected /r/RelatedTo)
|
|
4150
|
+
* still isn't "i learned" (same anthropomorphism problem), but reads identically to
|
|
4151
|
+
* a solid corpus fact loses the only reader-visible signal that it's lower-
|
|
4152
|
+
* confidence — re-examined 2026-07-12 (TOO_HARD_AUDIT.md): the prior blanket "corpus
|
|
4153
|
+
* never hedges" rule predates corpus data having any confidence spread at all, so a
|
|
4154
|
+
* distinct, honest hedge ("possibly: …") applies here instead of either extreme.
|
|
4155
|
+
* Provenance stays VERBATIM in every case. */
|
|
4144
4156
|
function renderFactLine(f) {
|
|
4145
4157
|
const cite = f.provenance ? ` (source: ${f.provenance})` : "";
|
|
4146
4158
|
// ace:chat = the ACE-parsed operator assert; teach:chat = the teach lane's
|
|
4147
4159
|
// natural frames — both are things the operator SAID, so both read first-person.
|
|
4148
4160
|
if (f.provenance.includes("ace:chat") || f.provenance.includes("teach:chat")) return `you told me: ${factPhrase(f)}${cite}`;
|
|
4149
|
-
//
|
|
4150
|
-
//
|
|
4161
|
+
// WEAK corpus facts (lower trust, e.g. RelatedTo) — real, cited, but hedged as
|
|
4162
|
+
// uncertain rather than either flatly stated or falsely claimed as "learned".
|
|
4163
|
+
if (f.provenance.includes("corpus-weak:")) return `possibly: ${factPhrase(f)}${cite}`;
|
|
4164
|
+
// SOLID corpus facts are background DATA — present the relation plainly, cited
|
|
4165
|
+
// to its source, never "i learned: …" (a first-person claim over corpus data).
|
|
4151
4166
|
if (f.provenance.includes("corpus:")) return `${factPhrase(f)}${cite}`;
|
|
4152
4167
|
return `i learned: ${factPhrase(f)}${cite}`;
|
|
4153
4168
|
}
|
|
@@ -4190,11 +4205,27 @@ async function memoryFacts(memoryDir) {
|
|
|
4190
4205
|
/** Load memory once and resolve every reified Fact into a TRUST-BEARING row
|
|
4191
4206
|
* ({subject,predicate,object,provenance,trust,sourceTypes,…}) via core's
|
|
4192
4207
|
* readFactRows — the seam the answer layer ranks + cites without re-walking the
|
|
4193
|
-
* graph shape (Wave-A memory/core.mjs). Lazy + failure-tolerated: no memory → [].
|
|
4194
|
-
|
|
4208
|
+
* graph shape (Wave-A memory/core.mjs). Lazy + failure-tolerated: no memory → [].
|
|
4209
|
+
*
|
|
4210
|
+
* `cache` (PLAN_GRAPH_SCAN.md "Query side: memoize the per-turn reload"): an
|
|
4211
|
+
* optional, caller-owned plain object (`{ rows: null }`, e.g. one runTurn call's
|
|
4212
|
+
* own `factRowsCache`) — when `cache.rows` is already populated, it's returned
|
|
4213
|
+
* directly, skipping loadMemory/readFactRows entirely; otherwise the result is
|
|
4214
|
+
* computed as before and stashed onto `cache.rows` for the next caller sharing
|
|
4215
|
+
* the same cache this turn. Absent/null (the default) reproduces today's
|
|
4216
|
+
* behavior exactly — a fresh, uncached reload every call — so every caller that
|
|
4217
|
+
* doesn't pass one is byte-for-byte unaffected. Never shared across turns or
|
|
4218
|
+
* with mutateMemory (see the plan doc for why a global cache was rejected).
|
|
4219
|
+
* `cache.reloads` is bumped once per REAL loadMemory/readFactRows call (never on
|
|
4220
|
+
* a cache hit) purely so a test can assert "computed once per turn" by call
|
|
4221
|
+
* count instead of wall-clock — see test/chat-factrows-cache.test.mjs. */
|
|
4222
|
+
async function factRows(memoryDir, cache = null) {
|
|
4223
|
+
if (cache?.rows) return cache.rows;
|
|
4195
4224
|
try {
|
|
4196
4225
|
const { loadMemory, readFactRows } = await import("./memory/core.mjs");
|
|
4197
|
-
|
|
4226
|
+
const rows = readFactRows(await loadMemory(memoryDir));
|
|
4227
|
+
if (cache) { cache.rows = rows; cache.reloads = (cache.reloads || 0) + 1; }
|
|
4228
|
+
return rows;
|
|
4198
4229
|
} catch {
|
|
4199
4230
|
return [];
|
|
4200
4231
|
}
|
|
@@ -4508,6 +4539,42 @@ const WHAT_HAS_RE = /^what\s+has\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
|
4508
4539
|
// (object known, subject unknown), so it fell all the way through to the
|
|
4509
4540
|
// code-graph miss cascade — actively misleading for a pure vocabulary query.
|
|
4510
4541
|
const WHAT_USED_FOR_RE = /^what\s+(?:(?:can\s+be|is)\s+used\s+for|is\s+for)\s+(.+?)[?.!\s]*$/i;
|
|
4542
|
+
|
|
4543
|
+
// Live-caught 2026-07-12 follow-up: the SAME gap as mgx:usedFor above turned
|
|
4544
|
+
// out to be systemic, not one-off — "what causes fire", "what is made of
|
|
4545
|
+
// wood", "what is found in a kitchen", "what wants food" all fell through to
|
|
4546
|
+
// the same misleading code-graph miss, for the same reason (no reverse-by-
|
|
4547
|
+
// object reader existed for these predicates either). Rather than hand-roll
|
|
4548
|
+
// one more one-off regex per predicate, this DERIVES a reverse-by-object
|
|
4549
|
+
// regex for every FACT_PREDICATE_PHRASES entry that's safe to reverse — the
|
|
4550
|
+
// same "derivation, not a curated subset" philosophy TRAILING_PREDICATE_MARKERS
|
|
4551
|
+
// already uses for the forward direction (see that const's own docblock).
|
|
4552
|
+
// Excluded, each for a specific reason:
|
|
4553
|
+
// - rdfs:subClassOf, mgx:hasA, mgx:capableOf, mgx:usedFor — already have
|
|
4554
|
+
// their own dedicated, richer reverse readers (WHAT_INHERITS_RE/
|
|
4555
|
+
// WHAT_HAS_RE/WHAT_CAN_DO_RE/WHAT_USED_FOR_RE above).
|
|
4556
|
+
// - mgx:ownedBy — already has its own dedicated "who owns X" reader
|
|
4557
|
+
// (WHO_OWNS_RE) — a WHO question, not a WHAT question, so it would never
|
|
4558
|
+
// collide, but is excluded anyway to keep exactly one reader per relation.
|
|
4559
|
+
// - rdf:type ("is a") and mgx:hasProperty ("is") — too short/generic to
|
|
4560
|
+
// safely anchor a reverse question: "what is X" already belongs to the
|
|
4561
|
+
// meta lane's own vocabulary lookup, and reversing it here would mean
|
|
4562
|
+
// guessing whether the user meant "define X" or "what has property X"
|
|
4563
|
+
// from word order alone.
|
|
4564
|
+
// - owl:disjointWith ("is not a") and mgx:receivesAction ("can be") — both
|
|
4565
|
+
// broad enough that "what is not a X" / "what can be X" read as much more
|
|
4566
|
+
// likely to be a different question shape than a genuine reverse lookup.
|
|
4567
|
+
const REVERSE_PREDICATE_EXCLUDE = new Set([
|
|
4568
|
+
"rdfs:subClassOf", "rdf:type", "mgx:hasA", "mgx:capableOf", "mgx:usedFor",
|
|
4569
|
+
"mgx:ownedBy", "owl:disjointWith", "mgx:hasProperty", "mgx:receivesAction",
|
|
4570
|
+
]);
|
|
4571
|
+
const REVERSE_PREDICATE_MARKERS = Object.entries(FACT_PREDICATE_PHRASES)
|
|
4572
|
+
.filter(([predicate]) => !REVERSE_PREDICATE_EXCLUDE.has(predicate))
|
|
4573
|
+
.map(([predicate, phrase]) => ({
|
|
4574
|
+
predicate,
|
|
4575
|
+
re: new RegExp(`^what\\s+${escapeRegex(phrase)}\\s+(.+?)[?.!\\s]*$`, "i"),
|
|
4576
|
+
}))
|
|
4577
|
+
.sort((a, b) => b.re.source.length - a.re.source.length); // longest phrase first
|
|
4511
4578
|
// Widened 2026-07-11 (live-caught follow-up to the ambiguousParse fix, commit
|
|
4512
4579
|
// 5c858bf): on the FIRST turn of a graph-less session, dispatchTool's
|
|
4513
4580
|
// loadGraph() throws its own documented "the graph is empty... this repo
|
|
@@ -4549,8 +4616,19 @@ function uniqueFacts(rows) {
|
|
|
4549
4616
|
/** W4 seam: answer (or extend) a vocabulary/definition question from the MEMORY
|
|
4550
4617
|
* graph's Facts. Returns { text, replace } — `replace:false` means the engine's
|
|
4551
4618
|
* own (schema-docs) answer stands and the fact lines are appended under it —
|
|
4552
|
-
* or null when memory holds nothing relevant (misses stay unchanged).
|
|
4553
|
-
|
|
4619
|
+
* or null when memory holds nothing relevant (misses stay unchanged).
|
|
4620
|
+
* Exported (PLAN_VIZ_MEMORY.md Bug 1 fix) so src/memory-ask-browser-entry.mjs
|
|
4621
|
+
* can re-export it for `tmct viz`'s embedded "Ask the graph" panel — the ONLY
|
|
4622
|
+
* reason this is `export` rather than module-private; the function's own
|
|
4623
|
+
* behavior is unchanged (same signature, same logic, answers identically in
|
|
4624
|
+
* the CLI and the browser bundle). `memoryDir` may be memory/core.mjs's
|
|
4625
|
+
* Backend-B in-memory handle (`createInMemoryStore()`) as well as a real repo
|
|
4626
|
+
* path — every I/O this function does routes through `loadMemory(memoryDir)`
|
|
4627
|
+
* (via factRows/memoryFacts below), and loadMemory's own Backend-B branch
|
|
4628
|
+
* returns the handle's `payload` directly with ZERO fs calls — so a caller
|
|
4629
|
+
* that hands this a handle already carrying the embedded page's full graph
|
|
4630
|
+
* gets a pure, disk-free traversal, no bundle-time module shimming needed. */
|
|
4631
|
+
export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle = {}, cache = null) {
|
|
4554
4632
|
let normFactTerm;
|
|
4555
4633
|
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
4556
4634
|
const q = String(query).trim();
|
|
@@ -4570,7 +4648,7 @@ async function factAnswer(memoryDir, query, envelope, miss, biasByBundle = {}) {
|
|
|
4570
4648
|
const usedForQ = q.match(WHAT_USED_FOR_RE);
|
|
4571
4649
|
if (usedForQ) {
|
|
4572
4650
|
const variants = factTermVariants(normFactTerm, usedForQ[1]);
|
|
4573
|
-
const hits = (await factRows(memoryDir)).filter((f) => f.predicate === "mgx:usedFor" && variants.has(f.object));
|
|
4651
|
+
const hits = (await factRows(memoryDir, cache)).filter((f) => f.predicate === "mgx:usedFor" && variants.has(f.object));
|
|
4574
4652
|
if (hits.length) {
|
|
4575
4653
|
const ranked = rankByBiasThenTrust(uniqueFacts(hits), biasByBundle);
|
|
4576
4654
|
const lines = ranked.map(renderFactLine);
|
|
@@ -4581,6 +4659,26 @@ async function factAnswer(memoryDir, query, envelope, miss, biasByBundle = {}) {
|
|
|
4581
4659
|
}
|
|
4582
4660
|
}
|
|
4583
4661
|
|
|
4662
|
+
// (a-pre2) The generic derived cascade for every other reversible predicate
|
|
4663
|
+
// (REVERSE_PREDICATE_MARKERS, see its own docblock for the exclusion list
|
|
4664
|
+
// and why). Same checked-before-the-meta-lane placement and same
|
|
4665
|
+
// only-take-over-on-a-real-hit discipline as (a-pre) just above — a phrase
|
|
4666
|
+
// like "is found in"/"is made of" also starts with "what is …", so it must
|
|
4667
|
+
// run before (a) can greedily claim the whole tail as a literal term.
|
|
4668
|
+
for (const { predicate, re } of REVERSE_PREDICATE_MARKERS) {
|
|
4669
|
+
const m = q.match(re);
|
|
4670
|
+
if (!m) continue;
|
|
4671
|
+
const variants = factTermVariants(normFactTerm, m[1]);
|
|
4672
|
+
const hits = (await factRows(memoryDir, cache)).filter((f) => f.predicate === predicate && variants.has(f.object));
|
|
4673
|
+
if (!hits.length) continue; // try the next candidate marker, don't give up yet
|
|
4674
|
+
const ranked = rankByBiasThenTrust(uniqueFacts(hits), biasByBundle);
|
|
4675
|
+
const lines = ranked.map(renderFactLine);
|
|
4676
|
+
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
4677
|
+
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
4678
|
+
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
4679
|
+
return { text: shown.join("\n") + extra, replace: true, ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}) };
|
|
4680
|
+
}
|
|
4681
|
+
|
|
4584
4682
|
// (a) meta-shaped questions ("what is a module", "what does cache mean") — the
|
|
4585
4683
|
// parsed object term, matched against fact SUBJECTS; consulted for hits (append
|
|
4586
4684
|
// alongside the schema-docs answer) and misses (facts answer alone) alike.
|
|
@@ -4621,7 +4719,7 @@ async function factAnswer(memoryDir, query, envelope, miss, biasByBundle = {}) {
|
|
|
4621
4719
|
// factRows (trust+sourceIds-bearing), not the plain memoryFacts shape — the
|
|
4622
4720
|
// bias-weighted ranking below needs each hit's sourceIds to resolve which
|
|
4623
4721
|
// bundle it came from (memory/bias.mjs's biasForRow).
|
|
4624
|
-
const subjectHits = (await factRows(memoryDir)).filter((f) => variants.has(f.subject));
|
|
4722
|
+
const subjectHits = (await factRows(memoryDir, cache)).filter((f) => variants.has(f.subject));
|
|
4625
4723
|
let hits = predicate ? subjectHits.filter((f) => f.predicate === predicate) : subjectHits;
|
|
4626
4724
|
if (!hits.length) {
|
|
4627
4725
|
// The subject itself is known, but not under this specific relation —
|
|
@@ -4681,7 +4779,7 @@ async function factAnswer(memoryDir, query, envelope, miss, biasByBundle = {}) {
|
|
|
4681
4779
|
const canDo = q.match(WHAT_CAN_DO_RE);
|
|
4682
4780
|
if (canDo) {
|
|
4683
4781
|
const variants = factTermVariants(normFactTerm, canDo[1]);
|
|
4684
|
-
const hits = (await factRows(memoryDir)).filter((f) => f.predicate === "mgx:capableOf" && variants.has(f.subject));
|
|
4782
|
+
const hits = (await factRows(memoryDir, cache)).filter((f) => f.predicate === "mgx:capableOf" && variants.has(f.subject));
|
|
4685
4783
|
if (!hits.length) return null;
|
|
4686
4784
|
const ranked = rankByBiasThenTrust(uniqueFacts(hits), biasByBundle);
|
|
4687
4785
|
const lines = ranked.map(renderFactLine);
|
|
@@ -4700,7 +4798,7 @@ async function factAnswer(memoryDir, query, envelope, miss, biasByBundle = {}) {
|
|
|
4700
4798
|
const hasQ = q.match(WHAT_HAS_RE);
|
|
4701
4799
|
if (hasQ && !HAS_TEMPORAL_TAIL.has(hasQ[1].trim().split(/\s+/)[0]?.toLowerCase())) {
|
|
4702
4800
|
const variants = factTermVariants(normFactTerm, hasQ[1]);
|
|
4703
|
-
const hits = (await factRows(memoryDir)).filter((f) => f.predicate === "mgx:hasA" && variants.has(f.object));
|
|
4801
|
+
const hits = (await factRows(memoryDir, cache)).filter((f) => f.predicate === "mgx:hasA" && variants.has(f.object));
|
|
4704
4802
|
if (!hits.length) return null;
|
|
4705
4803
|
const ranked = rankByBiasThenTrust(uniqueFacts(hits), biasByBundle);
|
|
4706
4804
|
const lines = ranked.map(renderFactLine);
|
|
@@ -4737,7 +4835,7 @@ async function factAnswer(memoryDir, query, envelope, miss, biasByBundle = {}) {
|
|
|
4737
4835
|
: inheritsQ?.[1];
|
|
4738
4836
|
if (inheritsObj) {
|
|
4739
4837
|
const variants = factTermVariants(normFactTerm, inheritsObj);
|
|
4740
|
-
const hits = (await factRows(memoryDir)).filter((f) => ISA_PREDICATES.has(f.predicate) && variants.has(f.object));
|
|
4838
|
+
const hits = (await factRows(memoryDir, cache)).filter((f) => ISA_PREDICATES.has(f.predicate) && variants.has(f.object));
|
|
4741
4839
|
// Only diverts on a REAL hit — same discipline every other reader in this
|
|
4742
4840
|
// cascade follows (CAN_ASK_RE/WHAT_CAN_DO_RE/WHAT_HAS_RE above all `return
|
|
4743
4841
|
// null` on zero hits too). A zero-hit case here must NOT invent its own
|
|
@@ -4765,7 +4863,7 @@ async function factAnswer(memoryDir, query, envelope, miss, biasByBundle = {}) {
|
|
|
4765
4863
|
const know = q.match(KNOW_ABOUT_RE);
|
|
4766
4864
|
if (know) {
|
|
4767
4865
|
const variants = factTermVariants(normFactTerm, know[1]);
|
|
4768
|
-
const rows = await factRows(memoryDir);
|
|
4866
|
+
const rows = await factRows(memoryDir, cache);
|
|
4769
4867
|
// Bug E subtype walk (operator follow-up request, this session): a
|
|
4770
4868
|
// cycle-safe BFS DOWNWARD over isa-family facts from the term's own
|
|
4771
4869
|
// variants — every fact whose OBJECT is in the current frontier
|
|
@@ -5201,7 +5299,7 @@ function inheritsChain(graph, startId) {
|
|
|
5201
5299
|
* "what kind of thing is an X" reports X's own type (subject-side first).
|
|
5202
5300
|
* Miss-only and run AFTER factAnswer returns null, so it never shadows the
|
|
5203
5301
|
* subject-side answer or a schema hit. Returns { text, replace:true } or null. */
|
|
5204
|
-
async function factReadBack(memoryDir, query, envelope, miss, graph = null, focusLabel = null, biasByBundle = {}) {
|
|
5302
|
+
async function factReadBack(memoryDir, query, envelope, miss, graph = null, focusLabel = null, biasByBundle = {}, cache = null) {
|
|
5205
5303
|
if (!miss) return null;
|
|
5206
5304
|
let normFactTerm;
|
|
5207
5305
|
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
@@ -5248,7 +5346,7 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
5248
5346
|
// fragile" is the SAME one-word-out-of-alignment problem the hedge adverbs
|
|
5249
5347
|
// above were fixed for, just a dialect opener instead of a hedge adverb.
|
|
5250
5348
|
const qHedge = q.replace(/^(?:actually|really|honestly|yeah\s+nah)\s*,?\s+/i, "");
|
|
5251
|
-
const rows = await factRows(memoryDir);
|
|
5349
|
+
const rows = await factRows(memoryDir, cache);
|
|
5252
5350
|
if (!rows.length) {
|
|
5253
5351
|
// Tier-5 playtest fix (cycle 2), found live: with TRULY zero facts
|
|
5254
5352
|
// remembered yet (a fresh session, nothing taught at all), the early
|
|
@@ -6329,10 +6427,10 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
6329
6427
|
* only (a `/describe` names ONE code entity as the subject of its own facts,
|
|
6330
6428
|
* not every fact that merely mentions it in passing) — null when memory holds
|
|
6331
6429
|
* nothing about this subject. */
|
|
6332
|
-
async function describedFacts(memoryDir, label, biasByBundle = {}) {
|
|
6430
|
+
async function describedFacts(memoryDir, label, biasByBundle = {}, cache = null) {
|
|
6333
6431
|
let normFactTerm;
|
|
6334
6432
|
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
6335
|
-
const rows = await factRows(memoryDir);
|
|
6433
|
+
const rows = await factRows(memoryDir, cache);
|
|
6336
6434
|
if (!rows.length) return null;
|
|
6337
6435
|
const variants = factTermVariants(normFactTerm, label);
|
|
6338
6436
|
const hits = rankByBiasThenTrust(rows.filter((f) => variants.has(f.subject)), biasByBundle);
|
|
@@ -7267,7 +7365,7 @@ async function relationForceAnswer(query, envelope, { graph, config, source, tem
|
|
|
7267
7365
|
* shipped corpus/seon file (seonDefinitions), so it works without per-repo memory
|
|
7268
7366
|
* seeding; the memory fact rows only ADD remembered "A is a X" examples when present.
|
|
7269
7367
|
* Lazy + failure-tolerated throughout (chat.mjs ethos). Returns { text, instances }. */
|
|
7270
|
-
async function conceptForceAnswer(query, envelope, { graph, config, source, memoryDir, templates }) {
|
|
7368
|
+
async function conceptForceAnswer(query, envelope, { graph, config, source, memoryDir, templates, cache = null }) {
|
|
7271
7369
|
const rawTerm = conceptTermOf(query, envelope);
|
|
7272
7370
|
if (!rawTerm) return null;
|
|
7273
7371
|
let normFactTerm; let composeConcept; let CONCEPT_CLASS;
|
|
@@ -7287,7 +7385,7 @@ async function conceptForceAnswer(query, envelope, { graph, config, source, memo
|
|
|
7287
7385
|
try { g = parseEntities(await source.fetchEntities(config)); } catch { g = null; }
|
|
7288
7386
|
}
|
|
7289
7387
|
if (!g) return null;
|
|
7290
|
-
const rows = memoryDir ? await factRows(memoryDir) : [];
|
|
7388
|
+
const rows = memoryDir ? await factRows(memoryDir, cache) : [];
|
|
7291
7389
|
let composed;
|
|
7292
7390
|
try { composed = composeConcept(g, term, { definition, factRows: rows }); }
|
|
7293
7391
|
catch { return null; }
|
|
@@ -7343,7 +7441,7 @@ async function entityOfKindInText(graph, expectedClass, answerText) {
|
|
|
7343
7441
|
* otherwise the unchanged dispatchTool path (which also yields the no-graph error).
|
|
7344
7442
|
* A hit updates the focus to the resolved object. Grammar miss / ToolError → a
|
|
7345
7443
|
* normal answer, never a crash. */
|
|
7346
|
-
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null, tel = null, biasByBundle = {} }) {
|
|
7444
|
+
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null, tel = null, biasByBundle = {}, cache = null }) {
|
|
7347
7445
|
const ts = new Date().toISOString();
|
|
7348
7446
|
// DISCOURSE ANAPHORA (CHATBENCH_006 levers 1+2): a follow-up like "which of those
|
|
7349
7447
|
// are tested" / "how many of those" / "count them" filters or counts the PREVIOUS
|
|
@@ -7850,11 +7948,25 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7850
7948
|
// further down is UNCHANGED, so a CamelCase term with no real hit still falls
|
|
7851
7949
|
// through to its existing miss handling, never the generic orientation card.
|
|
7852
7950
|
const isBareCamelCaseWhatisCandidate = conversationalCandidateBaseGate && isBareCamelCaseMetaQuestion(gateQuery);
|
|
7951
|
+
// 2026-07-12 follow-up to the used-for/reverse-predicate fix (factAnswer's
|
|
7952
|
+
// WHAT_USED_FOR_RE/REVERSE_PREDICATE_MARKERS, above): the SAME race BUG 2/
|
|
7953
|
+
// Tier-5/CamelCase already fixed above hits these too, and for the shortest
|
|
7954
|
+
// members of the family it's actually MORE likely to fire — "what wants
|
|
7955
|
+
// happiness" is exactly 3 words, none of them in STRUCT_WORDS, so
|
|
7956
|
+
// isConversational() claims it before factAnswer ever gets a turn, even
|
|
7957
|
+
// though a real mgx:desires fact answers it correctly once reached (proven:
|
|
7958
|
+
// the longer "what can be used for riding" already worked, since 5 words
|
|
7959
|
+
// clears isConversational's <=3-word gate outright — only the short
|
|
7960
|
+
// members of this family were ever actually broken). Same discipline as
|
|
7961
|
+
// every sibling exemption on this gate: matching the shape alone changes
|
|
7962
|
+
// nothing by itself, factAnswer below still only diverts on a REAL hit.
|
|
7963
|
+
const reversePredicateShape = WHAT_USED_FOR_RE.test(gateQuery)
|
|
7964
|
+
|| REVERSE_PREDICATE_MARKERS.some(({ re }) => re.test(gateQuery));
|
|
7853
7965
|
let bareMetaHit = null;
|
|
7854
|
-
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape)) {
|
|
7966
|
+
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape)) {
|
|
7855
7967
|
if (memoryDir) {
|
|
7856
|
-
bareMetaHit = (await factAnswer(memoryDir, gateQuery, envelope, miss, biasByBundle))
|
|
7857
|
-
?? (await factReadBack(memoryDir, gateQuery, envelope, miss, graph, newFocus?.label, biasByBundle));
|
|
7968
|
+
bareMetaHit = (await factAnswer(memoryDir, gateQuery, envelope, miss, biasByBundle, cache))
|
|
7969
|
+
?? (await factReadBack(memoryDir, gateQuery, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
|
|
7858
7970
|
// HANDOVER.md 2026-07-10 item 10 (dropped-article gap): a bare "what is X"
|
|
7859
7971
|
// with NO taught fact but a KNOWN curated corpus term ("what is cache", no
|
|
7860
7972
|
// article) used to lose this exact same isConversationalCandidate race —
|
|
@@ -7946,8 +8058,8 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7946
8058
|
// reified fact is stronger evidence than a transcript echo. Subject-side facts
|
|
7947
8059
|
// first (factAnswer), then the reverse-membership read-back (factReadBack) so an
|
|
7948
8060
|
// asserted "every X is a Y" answers "what is a Y" too.
|
|
7949
|
-
const fact = (await factAnswer(memoryDir, query, envelope, miss, biasByBundle))
|
|
7950
|
-
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label, biasByBundle));
|
|
8061
|
+
const fact = (await factAnswer(memoryDir, query, envelope, miss, biasByBundle, cache))
|
|
8062
|
+
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
|
|
7951
8063
|
if (fact) {
|
|
7952
8064
|
answer = fact.replace ? fact.text : `${answer}\n${fact.text}`;
|
|
7953
8065
|
via = "fact";
|
|
@@ -8021,7 +8133,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
8021
8133
|
let conceptAllIds = null;
|
|
8022
8134
|
let conceptPending = null;
|
|
8023
8135
|
if (via === "composed" || via === "corpus/seon") {
|
|
8024
|
-
const concept = await conceptForceAnswer(query, envelope, { graph, config, source, memoryDir, templates });
|
|
8136
|
+
const concept = await conceptForceAnswer(query, envelope, { graph, config, source, memoryDir, templates, cache });
|
|
8025
8137
|
if (concept) {
|
|
8026
8138
|
answer = concept.text; via = "corpus/seon"; recordMiss = false;
|
|
8027
8139
|
conceptInstances = concept.instances;
|
|
@@ -8078,7 +8190,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
8078
8190
|
// (4) #2 TEACH lane — a teach-shaped would-miss nothing above answered: route to
|
|
8079
8191
|
// memory, or say what CAN be remembered (LOUD), never the wall / a silent drop.
|
|
8080
8192
|
if (miss && recordMiss && via === "composed") {
|
|
8081
|
-
const taught = await teachLane(query, { memoryDir, sessionId, lexicon });
|
|
8193
|
+
const taught = await teachLane(query, { memoryDir, sessionId, lexicon, cache });
|
|
8082
8194
|
if (taught) {
|
|
8083
8195
|
answer = taught.text; via = taught.via; recordMiss = taught.miss;
|
|
8084
8196
|
note(trace, `lane: (4) TEACH — TEACH_RE/OWNS_TEACH_RE/BARE_DECLARATIVE_RE matched, ${taught.miss ? "but the payload could not be stored" : "reified into .tmct/memory"}`);
|
|
@@ -8444,7 +8556,7 @@ const GOAL_BY_COMMAND = {
|
|
|
8444
8556
|
* field now (Bug F point 5) — mirrors runAsk's own `goal` field so
|
|
8445
8557
|
* withGoalLine's short "Goal (inferred): …" line fires for command
|
|
8446
8558
|
* dispatches too, not just ask()-parsed queries. */
|
|
8447
|
-
async function runCommand(line, { config, source, graph, focus, memoryDir, trace, narrate = false, tel = null, biasByBundle = {} }) {
|
|
8559
|
+
async function runCommand(line, { config, source, graph, focus, memoryDir, trace, narrate = false, tel = null, biasByBundle = {}, cache = null }) {
|
|
8448
8560
|
const ts = new Date().toISOString();
|
|
8449
8561
|
const sp = line.indexOf(" ");
|
|
8450
8562
|
const name = (sp === -1 ? line.slice(1) : line.slice(1, sp)).toLowerCase();
|
|
@@ -8555,7 +8667,7 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
8555
8667
|
// matching taught facts (subject === the resolved entity, trust-ranked)
|
|
8556
8668
|
// under the code-map answer, mirroring the ask-path's fact-append pattern.
|
|
8557
8669
|
if (name === "describe" && memoryDir) {
|
|
8558
|
-
const facts = await describedFacts(memoryDir, ent.label, biasByBundle);
|
|
8670
|
+
const facts = await describedFacts(memoryDir, ent.label, biasByBundle, cache);
|
|
8559
8671
|
if (facts) { answer = `${answer}\n${facts}`; note(trace, "source: memory facts (describedFacts) appended to the code-map answer"); }
|
|
8560
8672
|
}
|
|
8561
8673
|
return mk(answer, { resolvedIds: [ent.id], newFocus: nextFocus(graph, focus, ent) });
|
|
@@ -8603,7 +8715,7 @@ function renderAmbiguousAssert(line, ambiguous, normFactTerm) {
|
|
|
8603
8715
|
* relation-shaped with 0-1 surviving readings), so this adds exactly one
|
|
8604
8716
|
* cheap check ahead of the EXISTING, unchanged parseAce path below — every
|
|
8605
8717
|
* single-reading sentence renders byte-identically to before. */
|
|
8606
|
-
async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null }) {
|
|
8718
|
+
async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, cache = null }) {
|
|
8607
8719
|
try {
|
|
8608
8720
|
const { parseAce, parseAceAmbiguous } = await import("./grammar/ace.mjs");
|
|
8609
8721
|
// A session handle carries its own loaded lexicon (createSession loads it once);
|
|
@@ -8682,7 +8794,7 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null })
|
|
|
8682
8794
|
const newSubj = normFactTerm(res.triples[0].subject);
|
|
8683
8795
|
const newObj = normFactTerm(res.triples[0].object);
|
|
8684
8796
|
const isTaughtRow = (f) => !f.sourceTypes?.includes("corpus") && !f.sourceTypes?.includes("web");
|
|
8685
|
-
const priorEdges = (await factRows(memoryDir))
|
|
8797
|
+
const priorEdges = (await factRows(memoryDir, cache))
|
|
8686
8798
|
.filter((f) => f.predicate === SUBCLASS_PREDICATE && isTaughtRow(f)
|
|
8687
8799
|
&& !(normFactTerm(f.subject) === newSubj && normFactTerm(f.object) === newObj))
|
|
8688
8800
|
.map((f) => [normFactTerm(f.subject), normFactTerm(f.object)]);
|
|
@@ -8834,8 +8946,22 @@ function rewriteUsesAsBaseFrame(text) {
|
|
|
8834
8946
|
return null;
|
|
8835
8947
|
}
|
|
8836
8948
|
|
|
8837
|
-
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, vocabHint = null, tel = null, biasByBundle = {} } = {}) {
|
|
8949
|
+
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null } = {}) {
|
|
8838
8950
|
const line = String(input ?? "").trim();
|
|
8951
|
+
// PLAN_GRAPH_SCAN.md "Query side: memoize the per-turn reload": ONE fresh,
|
|
8952
|
+
// empty cache for this turn only — every factRows() reader reached from this
|
|
8953
|
+
// call (factAnswer, factReadBack, describedFacts, countFromFacts,
|
|
8954
|
+
// answerQuantifierRecall, assertTurn, teachLane's grounding fallbacks,
|
|
8955
|
+
// conceptForceAnswer, …) shares it via `ctx`/an explicit trailing arg, so the
|
|
8956
|
+
// first reader to run computes loadMemory+readFactRows once and every later
|
|
8957
|
+
// reader THIS TURN reuses that same result instead of reloading from disk.
|
|
8958
|
+
// Never persisted, never shared across turns or with mutateMemory (a global
|
|
8959
|
+
// cache was explicitly rejected — see the plan doc's own reasoning: a reader
|
|
8960
|
+
// could observe a mutator's half-written object). `injectedFactRowsCache` is a
|
|
8961
|
+
// TEST-ONLY escape hatch (default null, so every real caller gets a fresh one
|
|
8962
|
+
// exactly as before) — passing one in lets a test observe `.reloads` after the
|
|
8963
|
+
// call to assert the real load path ran exactly once this turn.
|
|
8964
|
+
const factRowsCache = injectedFactRowsCache ?? { rows: null };
|
|
8839
8965
|
// The captured residue is used for RECOGNITION at every dispatch site below
|
|
8840
8966
|
// (asBareCommand, conversationalTurn, assertTurn, the count lanes, runAsk);
|
|
8841
8967
|
// the ORIGINAL `line` survives untouched for record.query/logLines fidelity
|
|
@@ -8863,7 +8989,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8863
8989
|
// pass one gets it computed here instead, so "try this vocabulary example" is
|
|
8864
8990
|
// never wrong regardless of caller.
|
|
8865
8991
|
const resolvedVocabHint = vocabHint ?? vocabExampleHint(await hasSeededVocabulary(memoryDir));
|
|
8866
|
-
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, vocabHint: resolvedVocabHint, tel, biasByBundle };
|
|
8992
|
+
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache };
|
|
8867
8993
|
// A DISPATCHED turn (count / slash-command / ask) becomes the new "last answer"
|
|
8868
8994
|
// that why/say-more re-renders; a conversational turn does not (it preserves it).
|
|
8869
8995
|
// FINISH SEAM (PLAN_RESPONSE_FINISHING §"Where it lives"): every dispatched turn's
|
|
@@ -8955,7 +9081,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8955
9081
|
// authority gate declines (returns null) for anything answerCount should own,
|
|
8956
9082
|
// so ordinary structural counts fall through completely unaffected.
|
|
8957
9083
|
if (memoryDir) {
|
|
8958
|
-
const quantifierRecall = await answerQuantifierRecall(memoryDir, workingLine, biasByBundle);
|
|
9084
|
+
const quantifierRecall = await answerQuantifierRecall(memoryDir, workingLine, biasByBundle, factRowsCache);
|
|
8959
9085
|
if (quantifierRecall != null) {
|
|
8960
9086
|
note(trace, 'goal: recall a taught quantifier for a class-membership pair ("how many Xs are Ys")');
|
|
8961
9087
|
note(trace, "lane: answerQuantifierRecall — matched HOW_MANY_ARE_RE with a subject tmct has facts about; literal recall, never real counting");
|
|
@@ -8983,7 +9109,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8983
9109
|
// ASSERTED vocabulary fact ("every class is a type" → "how many types" = the
|
|
8984
9110
|
// class count). countFromFacts declines on a real graph kind, so ordinary
|
|
8985
9111
|
// counts are unaffected; it only speaks for a remembered object noun.
|
|
8986
|
-
const viaFact = memoryDir ? await countFromFacts(graph, memoryDir, workingLine, biasByBundle) : null;
|
|
9112
|
+
const viaFact = memoryDir ? await countFromFacts(graph, memoryDir, workingLine, biasByBundle, factRowsCache) : null;
|
|
8987
9113
|
if (viaFact != null) {
|
|
8988
9114
|
note(trace, 'goal: get a count of an asserted-vocabulary kind ("every X is a Y" inherited cardinality)');
|
|
8989
9115
|
note(trace, "lane: countFromFacts — the counted noun matched a remembered isa-fact's SUBJECT, whose class IS countable");
|
|
@@ -9175,9 +9301,11 @@ export async function createSession({
|
|
|
9175
9301
|
// module-global state). "sqlite" selects Backend C (createSqliteMemoryStore
|
|
9176
9302
|
// — a live node:sqlite connection kept open for the session's lifetime,
|
|
9177
9303
|
// lazily imported only when this is actually chosen). TMCT_MEMORY_BACKEND
|
|
9178
|
-
// mirrors the TMCT_EPHEMERAL/TMCT_NARRATE on/off env convention.
|
|
9179
|
-
//
|
|
9180
|
-
//
|
|
9304
|
+
// mirrors the TMCT_EPHEMERAL/TMCT_NARRATE on/off env convention. This
|
|
9305
|
+
// parameter IS `bin/tmct.mjs`'s `tmct chat --memory-backend <...>` CLI flag
|
|
9306
|
+
// (a library/test caller can still set it directly) — the full precedence
|
|
9307
|
+
// (this param > TMCT_MEMORY_BACKEND env > tmct.toml's `[memory] backend` >
|
|
9308
|
+
// "default") is resolved below, once `toml` is known.
|
|
9181
9309
|
memoryBackend = null,
|
|
9182
9310
|
} = {}) {
|
|
9183
9311
|
// EPHEMERAL mode (--ephemeral, or TMCT_EPHEMERAL=1): read the target graph but
|
|
@@ -9218,6 +9346,13 @@ export async function createSession({
|
|
|
9218
9346
|
// instead of the whole repo.
|
|
9219
9347
|
let repo;
|
|
9220
9348
|
let config;
|
|
9349
|
+
// tmct.toml's normalized knobs (src/toml-config.mjs), captured alongside
|
|
9350
|
+
// `config` in whichever branch below resolves the graph path — used further
|
|
9351
|
+
// down for the memory-backend precedence (`toml.memory.backend`), so that
|
|
9352
|
+
// knob is honoured the same way regardless of which graph-resolution tier
|
|
9353
|
+
// fired. `null` when no branch could read a tmct.toml (never fatal — the
|
|
9354
|
+
// backend precedence below just skips this tier).
|
|
9355
|
+
let toml = null;
|
|
9221
9356
|
const explicitGraphs = (graphPaths || []).filter(Boolean);
|
|
9222
9357
|
if (explicitGraphs.length) {
|
|
9223
9358
|
repo = repoPath || gitRoot(cwd) || cwd;
|
|
@@ -9225,6 +9360,11 @@ export async function createSession({
|
|
|
9225
9360
|
config = resolvedGraphs.length > 1
|
|
9226
9361
|
? { graphFile: resolvedGraphs[0], graphFiles: resolvedGraphs }
|
|
9227
9362
|
: { graphFile: resolvedGraphs[0] };
|
|
9363
|
+
try {
|
|
9364
|
+
const argv = ["--repo", repo];
|
|
9365
|
+
if (configPath) argv.push("--config", configPath);
|
|
9366
|
+
({ toml } = await resolveRuntimeConfig({ argv, cwd, env: {}, gitRoot }));
|
|
9367
|
+
} catch { toml = null; }
|
|
9228
9368
|
} else if (repoPath) {
|
|
9229
9369
|
repo = repoPath;
|
|
9230
9370
|
// env is deliberately withheld from resolveRuntimeConfig here (passed as
|
|
@@ -9235,17 +9375,22 @@ export async function createSession({
|
|
|
9235
9375
|
// honored too.
|
|
9236
9376
|
const argv = ["--repo", repoPath];
|
|
9237
9377
|
if (configPath) argv.push("--config", configPath);
|
|
9238
|
-
({ config } = await resolveRuntimeConfig({ argv, cwd, env: {}, gitRoot }));
|
|
9378
|
+
({ config, toml } = await resolveRuntimeConfig({ argv, cwd, env: {}, gitRoot }));
|
|
9239
9379
|
} else {
|
|
9240
9380
|
const root = gitRoot(cwd);
|
|
9241
9381
|
repo = root || cwd;
|
|
9242
9382
|
const envGraph = env.TMCT_GRAPH_FILE && String(env.TMCT_GRAPH_FILE).trim();
|
|
9243
9383
|
if (envGraph) {
|
|
9244
9384
|
config = loadConfig(env, cwd);
|
|
9385
|
+
try {
|
|
9386
|
+
const argv = [];
|
|
9387
|
+
if (configPath) argv.push("--config", configPath);
|
|
9388
|
+
({ toml } = await resolveRuntimeConfig({ argv, cwd, env: {}, gitRoot }));
|
|
9389
|
+
} catch { toml = null; }
|
|
9245
9390
|
} else {
|
|
9246
9391
|
const argv = [];
|
|
9247
9392
|
if (configPath) argv.push("--config", configPath);
|
|
9248
|
-
({ config } = await resolveRuntimeConfig({ argv, cwd, env, gitRoot }));
|
|
9393
|
+
({ config, toml } = await resolveRuntimeConfig({ argv, cwd, env, gitRoot }));
|
|
9249
9394
|
}
|
|
9250
9395
|
}
|
|
9251
9396
|
|
|
@@ -9330,20 +9475,22 @@ export async function createSession({
|
|
|
9330
9475
|
// `closeMemoryStore` is a no-op unless Backend C actually opened a
|
|
9331
9476
|
// connection (Backend C's node:sqlite import is lazy — it only happens if
|
|
9332
9477
|
// this branch is actually taken).
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9478
|
+
//
|
|
9479
|
+
// Precedence — CLI flag > env > tmct.toml > default — matches the graph-path
|
|
9480
|
+
// precedence documented above: `memoryBackend` here is `tmct chat
|
|
9481
|
+
// --memory-backend <...>`'s already-resolved value; TMCT_MEMORY_BACKEND is
|
|
9482
|
+
// the env tier; `toml.memory.backend` is tmct.toml's `[memory] backend`
|
|
9483
|
+
// (src/toml-config.mjs). A toml value of "default" (or anything unrecognized)
|
|
9484
|
+
// falls through to Backend A below, same as an absent value always has.
|
|
9485
|
+
const backendChoice = String(memoryBackend || env.TMCT_MEMORY_BACKEND || toml?.memory?.backend || "").trim().toLowerCase();
|
|
9486
|
+
// openMemoryBackend (memory/core.mjs) is the ONE shared resolver for this
|
|
9487
|
+
// seam — src/init.mjs's corpus seed and bin/tmct.mjs's --corpus/--ontology/
|
|
9488
|
+
// --lexicon activation now call the exact same function, so a repo's
|
|
9489
|
+
// seeded facts and its chat-taught facts always land in the same backend
|
|
9490
|
+
// (a split-brain bug found in review: init used to always seed Backend A
|
|
9491
|
+
// regardless of the configured backend).
|
|
9492
|
+
const { openMemoryBackend } = await import("./memory/core.mjs");
|
|
9493
|
+
const { dir: memoryDir, close: closeMemoryStore } = await openMemoryBackend(repo, backendChoice);
|
|
9347
9494
|
|
|
9348
9495
|
const empty = graph.individuals.length === 0;
|
|
9349
9496
|
// W3: FIRST RUN in a graph-less repo seeds a capped ConceptNet slice into
|