@polycode-projects/the-mechanical-code-talker 2.5.4 → 2.6.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 +2 -2
- package/data/templates/responses.jsonl +1 -1
- package/package.json +1 -1
- package/src/domain/ask.mjs +66 -10
- package/src/domain/codegraph.mjs +6 -2
- package/src/domain/interpret/normalize.mjs +1 -1
- package/src/domain/router/registry.mjs +17 -4
- package/src/domain/router/resolver.mjs +4 -2
- package/src/services/chat.mjs +626 -43
- package/src/services/sessions.mjs +10 -2
- package/src/surfaces/web/memory-ask-browser.bundle.js +88 -19
package/src/services/chat.mjs
CHANGED
|
@@ -312,7 +312,7 @@ export const COMMANDS = {
|
|
|
312
312
|
signature: { tool: "tmct_signature", arg: "symbol", help: "a symbol's signature only" },
|
|
313
313
|
members: { tool: "tmct_members", arg: "class", help: "the methods/attributes of a class" },
|
|
314
314
|
subclasses: { tool: "tmct_subclasses", arg: "class", help: "the subclasses of a class" },
|
|
315
|
-
impact: { tool: "tmct_impact", arg: "module", help: "what a change to this module reaches (impact closure)" },
|
|
315
|
+
impact: { tool: "tmct_impact", arg: "module", help: "what a change to this module or symbol reaches (impact closure)" },
|
|
316
316
|
callers: { tool: "tmct_callers", arg: "symbol", help: "functions that call this symbol" },
|
|
317
317
|
callees: { tool: "tmct_callees", arg: "symbol", help: "functions this symbol calls" },
|
|
318
318
|
tests: { tool: "tmct_tests_for", arg: "symbol", help: "the tests covering this symbol" },
|
|
@@ -1554,7 +1554,13 @@ function conversationalTurn(line, ctx) {
|
|
|
1554
1554
|
}
|
|
1555
1555
|
}
|
|
1556
1556
|
{
|
|
1557
|
-
|
|
1557
|
+
// "ok cool thanks" — an ack RUN in front of a bare thanks word: the ack
|
|
1558
|
+
// preamble peel (the same closed frames every other surface uses) leaves
|
|
1559
|
+
// the thanks word standing, so the stacked form lands where its parts do
|
|
1560
|
+
// instead of on the orientation blurb.
|
|
1561
|
+
const ackPeeled = applyPreambleFrames(q);
|
|
1562
|
+
const thanksHit = closedOrCollapsed(q, THANKS, THANKS_COLLAPSED) || (OK_ACK.has(q) ? q : null)
|
|
1563
|
+
|| (ackPeeled !== q && (THANKS.has(ackPeeled) || OK_ACK.has(ackPeeled)) ? ackPeeled : null);
|
|
1558
1564
|
if (thanksHit) {
|
|
1559
1565
|
note(ctx.trace, "goal: casual/social — acknowledgement, no graph intent");
|
|
1560
1566
|
note(ctx.trace, `lane: conversational — thanks/acknowledgement (${OK_ACK.has(q) ? "OK_ACK" : "THANKS"} closed set${thanksHit === q ? "" : ", elongation-collapsed"})`);
|
|
@@ -1812,7 +1818,10 @@ const QUESTION_LEAD_RE = /^(?:what|who|which|where|when|why|how|is|are|do|does|d
|
|
|
1812
1818
|
* not a storage decision). Used by runAsk's relaxedTeachCollision guard (below)
|
|
1813
1819
|
* to recognize when a query the ask engine "answered" via relaxation was
|
|
1814
1820
|
* actually a teach-shaped sentence, not a real question. */
|
|
1815
|
-
|
|
1821
|
+
// The object takes 1–2 tokens: the shipped hanoi recipe's own "a disk is a
|
|
1822
|
+
// kind of game piece" is exactly this shape, and a single-token object left
|
|
1823
|
+
// its sentence-pair line unsplittable (the split gate reads this regex).
|
|
1824
|
+
const DECLARATIVE_KIND_OF_RE = /^(?:every\s+|each\s+|all\s+|a\s+|an\s+)?[\w-]+(?:\s+[\w-]+)?\s+(?:is|are)\s+(?:an?\s+)?(?:(?:kind|type)\s+of\s+)?[\w-]+(?:\s+[\w-]+)?[.!]*$/i;
|
|
1816
1825
|
/** A bare wh-word token, tested one word at a time against `hasMidSentenceInterrogative`'s
|
|
1817
1826
|
* own tokenization below — never re-anchored, so it matches at ANY word position. */
|
|
1818
1827
|
const MID_SENTENCE_WH_RE = /^(?:which|who|what|where|when|why|how)$/i;
|
|
@@ -1839,6 +1848,11 @@ async function hasMidSentenceInterrogative(text) {
|
|
|
1839
1848
|
if (MID_SENTENCE_WH_RE.test(words[i].replace(/^[.,!?;:'"]+|[.,!?;:'"]+$/g, ""))) whIdx.push(i);
|
|
1840
1849
|
}
|
|
1841
1850
|
if (!whIdx.length) return false;
|
|
1851
|
+
// A wh-word opening a NEW CLAUSE ("I'm new here, what should I read
|
|
1852
|
+
// first") is an interrogative clause, full stop — no POS evidence needed:
|
|
1853
|
+
// the clause boundary (the preceding word's trailing comma/semicolon) is
|
|
1854
|
+
// itself the signal, and it holds with or without the wink adapter.
|
|
1855
|
+
if (whIdx.some((i) => /[,;:]["')]*$/.test(words[i - 1]))) return true;
|
|
1842
1856
|
try {
|
|
1843
1857
|
const { nlpAdapter } = await import("../adapters/ask-nlp.mjs");
|
|
1844
1858
|
const adapter = nlpAdapter();
|
|
@@ -2145,6 +2159,7 @@ async function repairSharesLemma(from, to) {
|
|
|
2145
2159
|
* the two-word object; single-word objects stay with the ACE path. */
|
|
2146
2160
|
async function bareTaxonomyTeach(line, { memoryDir, sessionId }) {
|
|
2147
2161
|
if (!memoryDir || QUESTION_LEAD_RE.test(line)) return null;
|
|
2162
|
+
if (/\?\s*$/.test(String(line).trim())) return null; // a question never writes
|
|
2148
2163
|
const inst = line.match(INSTANCE_TYPE_TEACH_RE);
|
|
2149
2164
|
if (inst) {
|
|
2150
2165
|
return teachFact(memoryDir, sessionId, {
|
|
@@ -2191,6 +2206,11 @@ const ACTION_SIGNATURE_ASK_RE = new RegExp(
|
|
|
2191
2206
|
const PLAN_SOLVE_RE = /^(?:solve\s+it|solve\s+(?:the\s+)?(?:towers?\s+of\s+hanoi|hanoi|puzzle|game|river\s+crossing|this)|plan\s+the\s+moves|how\s+do\s+i\s+get(?:\s+from\s+here)?\s+to\s+the\s+goal)[?.!\s]*$/i;
|
|
2192
2207
|
const LEGAL_MOVES_RE = /^what\s+moves\s+are\s+legal(?:\s+now)?[?.!\s]*$/i;
|
|
2193
2208
|
const PLAN_NEXT_RE = /^(?:next|next\s+move|go\s+on|continue)[.!?\s]*$/i;
|
|
2209
|
+
// Plan-navigation gestures beyond "next": the unwind ask (not supported —
|
|
2210
|
+
// answered honestly, never the blurb) and the goal drop (a real action on the
|
|
2211
|
+
// session's plan slot; taught board facts are never touched by it).
|
|
2212
|
+
const PLAN_UNDO_RE = /^(?:undo(?:\s+(?:that|it|the\s+last\s+move))?|go\s+back(?:\s+(?:one|a)\s+move)?|take\s+(?:that|it)\s+back|revert(?:\s+(?:that|it|the\s+last\s+move))?)[.!?\s]*$/i;
|
|
2213
|
+
const PLAN_FORGET_GOAL_RE = /^(?:forget|drop|clear|abandon|cancel|scrap)\s+(?:the\s+|that\s+|my\s+)?(?:goal|plan)[.!?\s]*$/i;
|
|
2194
2214
|
// The imperative voicing of a universal goal ("get all the disks onto peg-c"):
|
|
2195
2215
|
// like the verbless frame it names no board verb, so planLaneAnswer reads that
|
|
2196
2216
|
// off the taught locative facts. Captures a quantifier, a (possibly plural)
|
|
@@ -2767,8 +2787,12 @@ const GENERAL_VERB_DETERMINER_TEACH_RE = new RegExp(
|
|
|
2767
2787
|
/** The quantified possession teach ("every dog has fur", "all dogs have
|
|
2768
2788
|
* tails") — the closed has/have verb pins the split the way the preposition
|
|
2769
2789
|
* pins GENERAL_VERB_DETERMINER_TEACH_RE's, so a universal quantifier can
|
|
2770
|
-
* lead without any verb-position guessing.
|
|
2771
|
-
|
|
2790
|
+
* lead without any verb-position guessing. The quantifier is captured:
|
|
2791
|
+
* "every"/"each" take a grammatically SINGULAR noun, so only "all" folds
|
|
2792
|
+
* the plural — the naive fold clipped an s-final singular ("every lens" was
|
|
2793
|
+
* stored, and cited, as "len"). */
|
|
2794
|
+
const QUANTIFIED_HAS_TEACH_RE = /^(every|each|all)\s+([\w'-]+)\s+(?:has|have)\s+(.+?)[.!?]*$/i;
|
|
2795
|
+
const quantifiedHasSubject = (m) => (/^all$/i.test(m[1]) ? singularizeSurface(m[2]) : m[2]);
|
|
2772
2796
|
/** Verbs owned by an earlier, more specific recognizer in this lane — is/are
|
|
2773
2797
|
* (class-membership/property, above) and owns/maintains (ownership, above).
|
|
2774
2798
|
* generalVerbTeach declines outright on these so it can never race a more
|
|
@@ -2915,9 +2939,9 @@ async function generalVerbTeach(payload) {
|
|
|
2915
2939
|
if (GENERAL_VERB_DETERMINER_RE.test(subjectRaw)) {
|
|
2916
2940
|
const quantHas = p.match(QUANTIFIED_HAS_TEACH_RE);
|
|
2917
2941
|
if (quantHas) {
|
|
2918
|
-
subjectRaw =
|
|
2942
|
+
subjectRaw = quantifiedHasSubject(quantHas);
|
|
2919
2943
|
verbRaw = "has";
|
|
2920
|
-
objectRaw = quantHas[
|
|
2944
|
+
objectRaw = quantHas[3];
|
|
2921
2945
|
} else {
|
|
2922
2946
|
const det = p.match(GENERAL_VERB_DETERMINER_TEACH_RE);
|
|
2923
2947
|
if (!det) return null; // not a bare-name subject, and no preposition to pin the verb
|
|
@@ -3288,7 +3312,13 @@ function habitualGroundingHintText(line, habitual) {
|
|
|
3288
3312
|
* grammatical category error regardless of the verb — keeping the guard
|
|
3289
3313
|
* ahead of every teach recognizer (copula AND general-verb alike) the same
|
|
3290
3314
|
* way it already stood ahead of teachSuggestion/unknownSubjectFallback. */
|
|
3291
|
-
|
|
3315
|
+
// The contracted forms ("i'm new here …") join the bare pronouns: a leading
|
|
3316
|
+
// pronoun+copula contraction is exactly as invalid a fact subject, and it
|
|
3317
|
+
// slipped past this guard into the general-verb mint as a one-token "i'm".
|
|
3318
|
+
const TEACH_PRONOUNS = Object.freeze([
|
|
3319
|
+
"you're", "i'm", "it's", "they're", "he's", "she's", "we're",
|
|
3320
|
+
"you", "i", "it", "they", "he", "she", "we",
|
|
3321
|
+
]);
|
|
3292
3322
|
const TEACH_PRONOUN_RE = new RegExp(`^(?:every\\s+|each\\s+|all\\s+|some\\s+|a few\\s+|a\\s+|an\\s+)?(${TEACH_PRONOUNS.join("|")})\\s+\\S+`, "i");
|
|
3293
3323
|
/** The same closed set, read as a whole-word membership test: a pronoun is no
|
|
3294
3324
|
* more a legal fact subject when a reader LIFTS one out of a prior answer than
|
|
@@ -3311,8 +3341,61 @@ const RETRACT_NOT_A_RE = /^(?:a\s+|an\s+)?([\w-]+(?:\s+[\w-]+)?)\s+(?:(?:is|are)
|
|
|
3311
3341
|
* (unwrapped) sentence, unlike RETRACT_NOT_A_RE above which is tried against
|
|
3312
3342
|
* the remember-wrapped surface too. */
|
|
3313
3343
|
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;
|
|
3344
|
+
/** The locative teach shape ("disk-1 rests on peg-b") — the board-fact
|
|
3345
|
+
* surface, shared by the mid-plan write guard and the locative forget. */
|
|
3346
|
+
const BOARD_TEACH_LOCATIVE_RE = new RegExp(`^([\\w-]+)\\s+([a-z]+)s\\s+(${PREP_SRC})\\s+([\\w-]+)$`, "i");
|
|
3347
|
+
/** "forget that disk-1 rests on peg-b" — the locative twin of
|
|
3348
|
+
* RETRACT_FORGET_RE: a plain minted mgx:<verb>-<prep> fact has no entailment
|
|
3349
|
+
* cascade, so removing the one row IS the retraction. */
|
|
3350
|
+
const RETRACT_FORGET_LOCATIVE_RE = new RegExp(`^forget\\s+(?:that\\s+)?([\\w-]+)\\s+([a-z]+)s\\s+(${PREP_SRC})\\s+([\\w-]+)$`, "i");
|
|
3351
|
+
|
|
3352
|
+
/** The closed related-to pair — "X relates to Y" / "X is related to Y" —
|
|
3353
|
+
* minted onto mgx:relatedTo (the SKOS view's skos:related source), so the
|
|
3354
|
+
* synonym/related lane has a teach phrasing. Single-token subject, 1–2
|
|
3355
|
+
* token object, articles tolerated on both. */
|
|
3356
|
+
const RELATED_TO_TEACH_RE = /^(?:a\s+|an\s+|the\s+)?([\w-]+)\s+(?:relates\s+to|is\s+related\s+to)\s+(?:a\s+|an\s+|the\s+)?([\w-]+(?:\s+[\w-]+)?)$/i;
|
|
3357
|
+
|
|
3358
|
+
/** NEGATIVE UNIVERSAL — "no X is a Y" / "no Xs are Ys": a class-level
|
|
3359
|
+
* exclusion, stored as `X owl:disjointWith Y` on the RESOLVED class pair.
|
|
3360
|
+
* The ACE grammar already mints exactly this triple when both words sit in
|
|
3361
|
+
* its closed lexicon; this frame is the SAME mint for the words outside it,
|
|
3362
|
+
* so the sentence never falls through to the unknown-subject fallback, which
|
|
3363
|
+
* would warehouse it under the subject-literal "no X" — a spelling no
|
|
3364
|
+
* reader (the cax-dw veto included) ever consults, leaving a later chain
|
|
3365
|
+
* proof free to certify the very thing the user excluded. Single-token
|
|
3366
|
+
* sides only (the disjointness readers resolve class TERMS, not phrases);
|
|
3367
|
+
* a plural surface folds to the singular the ⊑ facts use. */
|
|
3368
|
+
const NEGATIVE_UNIVERSAL_TEACH_RE = /^no\s+([\w-]+)\s+(is|are)\s+(?:an?\s+)?(?:(?:kind|type)\s+of\s+)?([\w-]+)[.!]*$/i;
|
|
3369
|
+
|
|
3370
|
+
/** The mint (or the reflexive refusal) for a NEGATIVE_UNIVERSAL_TEACH_RE
|
|
3371
|
+
* match, shared by teachLane and the ACE-path reflexive gate: null when the
|
|
3372
|
+
* sentence isn't this shape. */
|
|
3373
|
+
async function negativeUniversalTeach(sentence, { memoryDir, sessionId }) {
|
|
3374
|
+
const m = String(sentence || "").trim().match(NEGATIVE_UNIVERSAL_TEACH_RE);
|
|
3375
|
+
if (!m || !memoryDir) return null;
|
|
3376
|
+
const plural = m[2].toLowerCase() === "are";
|
|
3377
|
+
const subject = plural ? singularizeSurface(m[1]) : m[1];
|
|
3378
|
+
const object = plural ? singularizeSurface(m[3]) : m[3];
|
|
3379
|
+
if (subject.toLowerCase() === object.toLowerCase()) {
|
|
3380
|
+
return {
|
|
3381
|
+
text: `I can't store "no ${subject} is a ${object}" — every ${subject} is a ${subject} by definition, so that exclusion contradicts itself. Nothing was stored.`,
|
|
3382
|
+
via: "teach-miss", miss: true,
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
const { DISJOINT_PREDICATE } = await import("../domain/syllogise.mjs");
|
|
3386
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
3387
|
+
subject, predicate: DISJOINT_PREDICATE, object,
|
|
3388
|
+
});
|
|
3389
|
+
if (!stored) {
|
|
3390
|
+
return {
|
|
3391
|
+
text: `I couldn't store the exclusion "no ${subject} is a ${object}" — say it with single-word class names ("no dog is a mammal") and I'll remember it as a disjointness.`,
|
|
3392
|
+
via: "teach-miss", miss: true,
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
return stored;
|
|
3396
|
+
}
|
|
3314
3397
|
|
|
3315
|
-
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null }) {
|
|
3398
|
+
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null, planHolder = null }) {
|
|
3316
3399
|
// A closed discourse-marker preamble ahead of a teach sentence ("howdy
|
|
3317
3400
|
// pardner, remember that TaskController is fragile") would otherwise
|
|
3318
3401
|
// corrupt TEACH_RE's own match, so strip it first. applyPreambleFrames is
|
|
@@ -3321,6 +3404,17 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3321
3404
|
// topic-switch/hedge — match ordinary teach phrasing), so this is purely
|
|
3322
3405
|
// additive.
|
|
3323
3406
|
const rawInput = applyPreambleFrames(String(query).trim());
|
|
3407
|
+
// A trailing "?" is an unambiguous "this is a question" marker, and a
|
|
3408
|
+
// question must never reach the write boundary — the ESL missing-"does"
|
|
3409
|
+
// yes/no ("dog have tail?") is a bare declarative to every shape gate in
|
|
3410
|
+
// this lane, and it STORED, at teach trust, until this gate existed. The
|
|
3411
|
+
// whole lane stands down; the ask cascade owns question marks.
|
|
3412
|
+
if (/\?\s*$/.test(rawInput)) return null;
|
|
3413
|
+
// A typo'd interrogative ("wat is a hrose") reads as a declarative to every
|
|
3414
|
+
// anchored QUESTION_LEAD_RE gate below — run the SAME closed misspelling
|
|
3415
|
+
// repair ask.mjs's typo tolerance applies BEFORE classifying, so the
|
|
3416
|
+
// question goes back to the question side instead of a teach suggestion.
|
|
3417
|
+
if (QUESTION_LEAD_RE.test(correctMisspellings(rawInput))) return null;
|
|
3324
3418
|
const m = rawInput.match(TEACH_RE);
|
|
3325
3419
|
const wrappedInput = m ? m[1].trim() : null;
|
|
3326
3420
|
// Refuse an existential BEFORE any frame below can read it as a universal:
|
|
@@ -3406,7 +3500,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3406
3500
|
if (memoryDir && !QUESTION_LEAD_RE.test(conjSrc) && /\s+and\s+/i.test(conjSrc)
|
|
3407
3501
|
&& !(await hasMidSentenceInterrogative(conjSrc))) {
|
|
3408
3502
|
const rewrap = (half) => (wrapped != null ? `remember that ${half}` : half);
|
|
3409
|
-
const recurse = (half) => teachLane(rewrap(half), { memoryDir, sessionId, lexicon, cache });
|
|
3503
|
+
const recurse = (half) => teachLane(rewrap(half), { memoryDir, sessionId, lexicon, cache, planHolder });
|
|
3410
3504
|
const stripNoted = (t) => String(t).replace(/^noted — remembered(?:\s+\d+\s+facts?)?:\s*/i, "").trim();
|
|
3411
3505
|
const shared = conjSrc.match(/^(.+?)\s+and\s+((?:is|are|has|have|can)\b.+)$/i);
|
|
3412
3506
|
const sharedSubject = shared ? shared[1].match(/^(.+?)\s+(?:is|are|has|have|can)\b/i)?.[1]?.trim() : null;
|
|
@@ -3503,6 +3597,32 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3503
3597
|
const retractNotMatch = memoryDir && !QUESTION_LEAD_RE.test(retractSrc) && !retractSrcMidQuestion
|
|
3504
3598
|
? retractSrc.match(RETRACT_NOT_A_RE) : null;
|
|
3505
3599
|
const forgetSrc = raw.replace(/[.!?]+\s*$/, "");
|
|
3600
|
+
// The locative forget rides beside the subclass one, on the same raw
|
|
3601
|
+
// surface: find the one stored row and remove it — no cascade exists for a
|
|
3602
|
+
// plain minted mgx:<verb>-<prep> fact. A no-match falls through unchanged.
|
|
3603
|
+
const forgetLocative = memoryDir && !QUESTION_LEAD_RE.test(forgetSrc)
|
|
3604
|
+
? forgetSrc.match(RETRACT_FORGET_LOCATIVE_RE) : null;
|
|
3605
|
+
if (forgetLocative) {
|
|
3606
|
+
try {
|
|
3607
|
+
const { loadMemory: loadMemForLoc, readFactRows: readRowsForLoc, removeFacts: removeFactsForLoc, normFactTerm: normTermForLoc } = await import("../adapters/memory/core.mjs");
|
|
3608
|
+
const locPredicate = foldPrepositionIntoPredicate(
|
|
3609
|
+
await generalVerbPredicate(forgetLocative[2].toLowerCase()),
|
|
3610
|
+
`${forgetLocative[3].toLowerCase()} ${forgetLocative[4]}`,
|
|
3611
|
+
).predicate;
|
|
3612
|
+
const locSubject = normTermForLoc(forgetLocative[1]);
|
|
3613
|
+
const locObject = normTermForLoc(forgetLocative[4]);
|
|
3614
|
+
const row = readRowsForLoc(await loadMemForLoc(memoryDir))
|
|
3615
|
+
.find((r) => r.subject === locSubject && r.predicate === locPredicate && r.object === locObject);
|
|
3616
|
+
if (row?.id) {
|
|
3617
|
+
await removeFactsForLoc(memoryDir, [row.id]);
|
|
3618
|
+
return {
|
|
3619
|
+
text: `noted — forgotten: "${locSubject} ${predicatePhrase(locPredicate)} ${locObject}" is no longer stored.`,
|
|
3620
|
+
via: "retract", miss: false,
|
|
3621
|
+
};
|
|
3622
|
+
}
|
|
3623
|
+
// nothing stored under that triple — fall through to the ordinary cascade
|
|
3624
|
+
} catch { /* store unavailable — fall through */ }
|
|
3625
|
+
}
|
|
3506
3626
|
const retractForgetMatch = !retractNotMatch && memoryDir && !QUESTION_LEAD_RE.test(forgetSrc)
|
|
3507
3627
|
? forgetSrc.match(RETRACT_FORGET_RE) : null;
|
|
3508
3628
|
const retractMatch = retractNotMatch || retractForgetMatch;
|
|
@@ -3596,6 +3716,45 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3596
3716
|
}
|
|
3597
3717
|
}
|
|
3598
3718
|
|
|
3719
|
+
// MID-PLAN BOARD TEACH — a locative fact about a piece the LIVE plan's
|
|
3720
|
+
// moves touch is declined naming the plan, never accepted-then-ignored:
|
|
3721
|
+
// the plan's board rides @step snapshots, so a base-fact write here would
|
|
3722
|
+
// be confirmed ("noted — remembered") and then contradicted by the very
|
|
3723
|
+
// next "next". Scoped to the locative teach shape over the plan's own
|
|
3724
|
+
// pieces; every other teach (new vocabulary, new pieces, rules) is
|
|
3725
|
+
// untouched, and with no live plan nothing changes at all.
|
|
3726
|
+
{
|
|
3727
|
+
const livePlan = planHolder?.state && !planHolder.state.done
|
|
3728
|
+
&& Array.isArray(planHolder.state.actions) && planHolder.state.actions.length
|
|
3729
|
+
? planHolder.state : null;
|
|
3730
|
+
const boardSrc = (wrapped ?? raw).replace(/[.!?]+\s*$/, "");
|
|
3731
|
+
const board = livePlan ? boardSrc.match(BOARD_TEACH_LOCATIVE_RE) : null;
|
|
3732
|
+
if (board && memoryDir && !QUESTION_LEAD_RE.test(boardSrc)) {
|
|
3733
|
+
const { normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
3734
|
+
const planPieces = new Set(livePlan.actions.flatMap((a) => [normFactTerm(a.subject), normFactTerm(a.target)]));
|
|
3735
|
+
if (planPieces.has(normFactTerm(board[1])) || planPieces.has(normFactTerm(board[4]))) {
|
|
3736
|
+
const at = livePlan.cursor > 0 ? `step ${livePlan.cursor} of ${livePlan.actions.length}` : `0 of ${livePlan.actions.length} moves made`;
|
|
3737
|
+
return {
|
|
3738
|
+
text: `a plan is live (${at}, toward: ${livePlan.goalText ?? livePlan.goalTexts?.join("; ") ?? "the held goal"}) — I won't change the board mid-plan: the plan's moves write board@step snapshots, and "${board[0]}" would sit under them, silently contradicted by the next move. Say "forget the goal" first, re-teach the board, then "solve it" to replan.`,
|
|
3739
|
+
via: "teach-miss", miss: true,
|
|
3740
|
+
};
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
// NEGATIVE UNIVERSAL — "no X is a Y": the class-pair disjointness mint (or
|
|
3746
|
+
// the reflexive refusal). Tried on both surfaces, ahead of every frame that
|
|
3747
|
+
// could otherwise read "no X" as a subject literal — see
|
|
3748
|
+
// NEGATIVE_UNIVERSAL_TEACH_RE's own docblock.
|
|
3749
|
+
{
|
|
3750
|
+
const negUniversalSrc = (wrapped ?? raw).replace(/[.!?]+\s*$/, "");
|
|
3751
|
+
if (memoryDir && !QUESTION_LEAD_RE.test(negUniversalSrc)
|
|
3752
|
+
&& !(await hasMidSentenceInterrogative(negUniversalSrc))) {
|
|
3753
|
+
const negUniversal = await negativeUniversalTeach(negUniversalSrc, { memoryDir, sessionId });
|
|
3754
|
+
if (negUniversal) return negUniversal;
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3599
3758
|
// OWNERSHIP — "<Name> owns/maintains <X>", bare or remember-wrapped. The bare
|
|
3600
3759
|
// form is double-gated: no interrogative lead, PLUS either side spelling a
|
|
3601
3760
|
// Capitalized token — so the "who owns <X>" READ question and ordinary
|
|
@@ -3632,6 +3791,19 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3632
3791
|
if (stored) return stored;
|
|
3633
3792
|
}
|
|
3634
3793
|
|
|
3794
|
+
// RELATED-TO — the closed pair "X relates to Y" / "X is related to Y"
|
|
3795
|
+
// maps onto mgx:relatedTo, the SAME predicate the SKOS view reads as
|
|
3796
|
+
// skos:related — giving the synonym/related lane its natural teach
|
|
3797
|
+
// phrasing. Without this the general-verb mint stored a preposition-glued
|
|
3798
|
+
// object ("cat mgx:relate 'to milk'") no reader could ever match.
|
|
3799
|
+
const relatedTo = ownSrc.match(RELATED_TO_TEACH_RE);
|
|
3800
|
+
if (relatedTo && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
3801
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
3802
|
+
subject: relatedTo[1], predicate: "mgx:relatedTo", object: relatedTo[2],
|
|
3803
|
+
});
|
|
3804
|
+
if (stored) return stored;
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3635
3807
|
// RELATIONAL FACT — "<Name> is the <role> of <Name>". Grouped with the
|
|
3636
3808
|
// other relational/possessive teach shapes just above (both ownership
|
|
3637
3809
|
// forms), tried on the SAME ownSrc, unconditionally
|
|
@@ -4047,8 +4219,12 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4047
4219
|
const detLed = raw.match(GENERAL_VERB_DETERMINER_TEACH_RE);
|
|
4048
4220
|
const quantHasLed = detLed ? null : raw.match(QUANTIFIED_HAS_TEACH_RE);
|
|
4049
4221
|
const subjectWord = detLed ? detLed[1].split(/\s+/).pop()
|
|
4050
|
-
: (quantHasLed ?
|
|
4051
|
-
|
|
4222
|
+
: (quantHasLed ? quantifiedHasSubject(quantHasLed) : raw.match(/^([\w'-]+)/)?.[1]);
|
|
4223
|
+
// The quantifier lead ("every … has …") is itself a strong declarative
|
|
4224
|
+
// signal, so it overrides the single-token POS gate: a noun that doubles
|
|
4225
|
+
// as a verb ("every overbid has a gouger" — wink tags "overbid" VERB)
|
|
4226
|
+
// used to be a SILENT no-op and a later miss.
|
|
4227
|
+
if (subjectWord && (quantHasLed || (await subjectIsNounOrPropn(subjectWord)))) {
|
|
4052
4228
|
// A PLURAL explicit-capability surface ("wrens can hum") whose
|
|
4053
4229
|
// SINGULAR is a grounded term stores under the singular first — the
|
|
4054
4230
|
// spelling the grounding fact and every query-side variant fold use —
|
|
@@ -4430,10 +4606,19 @@ async function moduleOrientLane(query, { graph }) {
|
|
|
4430
4606
|
}
|
|
4431
4607
|
|
|
4432
4608
|
async function metaLane(query, { graph, memoryDir, last = null, templates = null, vocabHint = null, focus = null }) {
|
|
4609
|
+
// Preamble-peeled twin of `q`: a self-intro/greeting lead ("I'm new here,
|
|
4610
|
+
// what should I read first") wraps exactly the orientation questions this
|
|
4611
|
+
// lane owns, and the anchored META_ORIENT_RE can't see past it. Peeling
|
|
4612
|
+
// with the SAME closed frames every other surface uses is purely additive.
|
|
4613
|
+
const peeled = applyPreambleFrames(String(query).trim()).toLowerCase().replace(/[?.!]+$/, "").replace(/\s+/g, " ").trim();
|
|
4433
4614
|
const q = String(query).trim().toLowerCase().replace(/[?.!]+$/, "").replace(/\s+/g, " ");
|
|
4434
4615
|
if (WHAT_KNOW_RE.test(q) || q === "what have you learned" || q === "what have you learnt") {
|
|
4435
4616
|
return { text: await memorySummary(memoryDir, graph), via: "meta" };
|
|
4436
4617
|
}
|
|
4618
|
+
if (peeled !== q && META_ORIENT_RE.test(peeled)) {
|
|
4619
|
+
const text = orientationText(graph, templates, vocabHint);
|
|
4620
|
+
return { text: last?.answer === text ? META_ORIENT_REPEAT_ONELINER : text, via: "meta" };
|
|
4621
|
+
}
|
|
4437
4622
|
if (META_ORIENT_RE.test(q)) {
|
|
4438
4623
|
// This META_ORIENT_RE branch is a SEPARATE route to the same class of
|
|
4439
4624
|
// full-blurb text as the isConversational-triggered orientation branch
|
|
@@ -5138,6 +5323,9 @@ function renderFactLine(f) {
|
|
|
5138
5323
|
// SOLID corpus facts are background DATA — present the relation plainly, cited
|
|
5139
5324
|
// to its source, never "i learned: …" (a first-person claim over corpus data).
|
|
5140
5325
|
if (f.provenance.includes("corpus:")) return `${factPhrase(f)}${cite}`;
|
|
5326
|
+
// Reference-pack facts are the same class of cited data — the "i learned:"
|
|
5327
|
+
// frame read as a definition-less non-answer on the re-ask.
|
|
5328
|
+
if (f.provenance.includes("reference:")) return `${factPhrase(f)}${cite}`;
|
|
5141
5329
|
return `i learned: ${factPhrase(f)}${cite}`;
|
|
5142
5330
|
}
|
|
5143
5331
|
|
|
@@ -6382,12 +6570,24 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
6382
6570
|
);
|
|
6383
6571
|
const hit = hasHit(subj);
|
|
6384
6572
|
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6573
|
+
// The ⊑-lift walks a BOUNDED chain (not one hop): "every canine has fur"
|
|
6574
|
+
// + "every dog is a canine" + "rex is a dog" answers "does rex have fur"
|
|
6575
|
+
// citing all three premises. One chain, first parent per level (the
|
|
6576
|
+
// 1-hop behavior generalized), cycle-safe, and the bound keeps a deep
|
|
6577
|
+
// taught taxonomy from turning a yes/no into a graph scan.
|
|
6578
|
+
let liftFrontier = subj;
|
|
6579
|
+
const liftChain = [];
|
|
6580
|
+
const liftSeen = new Set();
|
|
6581
|
+
for (let hop = 0; hop < 4; hop += 1) {
|
|
6582
|
+
const step = facts.find((f) => ISA_PREDICATES.has(f.predicate) && liftFrontier.has(f.subject) && !liftSeen.has(f.object));
|
|
6583
|
+
if (!step) break;
|
|
6584
|
+
liftSeen.add(step.object);
|
|
6585
|
+
liftChain.push(step);
|
|
6586
|
+
const lifted = hasHit(factTermVariants(normFactTerm, step.object));
|
|
6388
6587
|
if (lifted) {
|
|
6389
|
-
return { text: `yes — ${renderFactLine
|
|
6588
|
+
return { text: `yes — ${[...liftChain.map(renderFactLine), renderFactLine(lifted)].join("; ")}`, replace: true };
|
|
6390
6589
|
}
|
|
6590
|
+
liftFrontier = factTermVariants(normFactTerm, step.object);
|
|
6391
6591
|
}
|
|
6392
6592
|
return null;
|
|
6393
6593
|
}
|
|
@@ -7084,6 +7284,17 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7084
7284
|
const directSup = inheritsChain(graph, ent.id)
|
|
7085
7285
|
.find((sup) => [...factTermVariants(normFactTerm, sup.label)].some((v) => directObjVariants.has(v)));
|
|
7086
7286
|
if (directSup) return { text: `yes — the code graph says ${ent.label} inherits ${directSup.label}.`, replace: true };
|
|
7287
|
+
// CONVERSE NUDGE, code-graph half: the taught-fact lane already names
|
|
7288
|
+
// a stored converse instead of the bare wall; the graph's inherits
|
|
7289
|
+
// relation deserves the same. Still a miss — the converse holding
|
|
7290
|
+
// says nothing about the asked direction, and a "no" would guess.
|
|
7291
|
+
const objEnt = await resolveEntity(graph, stripTrailingDiscourseTag(directIsaAsk[2]));
|
|
7292
|
+
if (objEnt && inheritsChain(graph, objEnt.id).some((sup) => sup.id === ent.id)) {
|
|
7293
|
+
return {
|
|
7294
|
+
text: `I can't confirm that — the code graph's stored direction runs the other way: ${objEnt.label} inherits ${ent.label}. An inheritance doesn't reverse.`,
|
|
7295
|
+
replace: true, miss: true,
|
|
7296
|
+
};
|
|
7297
|
+
}
|
|
7087
7298
|
}
|
|
7088
7299
|
}
|
|
7089
7300
|
}
|
|
@@ -8552,8 +8763,12 @@ function discourseRewrite(query, last) {
|
|
|
8552
8763
|
// question — a code drill-down chain keeps the code-ish NAME_TOKEN rule
|
|
8553
8764
|
// below unchanged.
|
|
8554
8765
|
const articled = cand.match(/^(?:an?|the)\s+([a-z][\w-]*)$/i);
|
|
8766
|
+
// A what-else EXPANSION turn is still a vocabulary turn — the swap has to
|
|
8767
|
+
// survive it, or "tell me about a dog" / "what else can dogs do" / "and a
|
|
8768
|
+
// cat" strands the third turn on the blurb.
|
|
8555
8769
|
const prevWasVocab = last?.query
|
|
8556
|
-
&& (BARE_WHATIS_RE.test(String(last.query)) || vagueTouchTermOf(String(last.query))
|
|
8770
|
+
&& (BARE_WHATIS_RE.test(String(last.query)) || vagueTouchTermOf(String(last.query))
|
|
8771
|
+
|| /^(?:what|anything)\s+else\b/i.test(String(last.query).trim()));
|
|
8557
8772
|
if (articled && prevWasVocab) return `what is a ${singularizeSurface(articled[1])}`;
|
|
8558
8773
|
if (!NAME_TOKEN_RE.test(cand)) return null;
|
|
8559
8774
|
newSubj = cand;
|
|
@@ -9040,6 +9255,11 @@ async function describeGrainRescue(graph, term) {
|
|
|
9040
9255
|
}
|
|
9041
9256
|
|
|
9042
9257
|
async function describeWrapperAnswer(query, { config, source, focus, graph, tel = null }) {
|
|
9258
|
+
// The detailed-summary/overview phrasings belong to the completions rescue
|
|
9259
|
+
// (4e, tried right after this lane) — applyPreambleFrames' show/give-me
|
|
9260
|
+
// bridge would otherwise rewrite them into a describe this lane claims
|
|
9261
|
+
// with a worse answer.
|
|
9262
|
+
if (DETAILED_HOW_WORKS_RE.test(String(query || "").trim()) || DETAILED_OVERVIEW_RE.test(String(query || "").trim())) return null;
|
|
9043
9263
|
// This lane is the LAST-RESORT rescue (4d), tried after every earlier lane
|
|
9044
9264
|
// declines. applyPreambleFrames + correctMisspellings run first, the same
|
|
9045
9265
|
// general-purpose normalization every other lane in this file applies,
|
|
@@ -9067,6 +9287,24 @@ async function describeWrapperAnswer(query, { config, source, focus, graph, tel
|
|
|
9067
9287
|
// — resolveSymbol (codegraph.mjs) has no component/overlap tier at all,
|
|
9068
9288
|
// so a leading "the"/"a"/"an" is pure noise here, safe to strip.
|
|
9069
9289
|
term = term.replace(/^(?:the|a|an)\s+/i, "");
|
|
9290
|
+
// The stale-modifier residue guard, carried into this lane — the last
|
|
9291
|
+
// of the 1.4 family without it: "describe the old Task class" must not
|
|
9292
|
+
// return the Task card with "old" silently swallowed. The resolver's
|
|
9293
|
+
// own unplaced-words verdict decides; a term it reads fully proceeds.
|
|
9294
|
+
if (graph && /\s/.test(term)) {
|
|
9295
|
+
try {
|
|
9296
|
+
const { resolveObject } = await import("../domain/ask.mjs");
|
|
9297
|
+
const guarded = resolveObject(graph, term);
|
|
9298
|
+
if (guarded?.unplacedWords?.length) {
|
|
9299
|
+
const words = guarded.unplacedWords;
|
|
9300
|
+
const quoted = words.map((w) => `"${w}"`).join(" and ");
|
|
9301
|
+
return {
|
|
9302
|
+
text: `nothing matching "${term}" is in the index. ${quoted} name${words.length === 1 ? "s" : ""} nothing here, and reading past ${words.length === 1 ? "it" : "them"} would answer a different question.${guarded.nearestLabel ? ` Did you mean ${guarded.nearestLabel}?` : ""}`,
|
|
9303
|
+
miss: true,
|
|
9304
|
+
};
|
|
9305
|
+
}
|
|
9306
|
+
} catch { /* resolver unavailable — the ordinary dispatch decides */ }
|
|
9307
|
+
}
|
|
9070
9308
|
}
|
|
9071
9309
|
}
|
|
9072
9310
|
try {
|
|
@@ -9217,6 +9455,20 @@ async function completionsRescueAnswer(query, { memoryDir, graph }) {
|
|
|
9217
9455
|
if (!term) return null;
|
|
9218
9456
|
term = term.replace(/^(?:the|a|an)\s+/i, "").trim();
|
|
9219
9457
|
if (!term) return null;
|
|
9458
|
+
// The APP-DEICTIC subject ("how this app works") names the whole program,
|
|
9459
|
+
// not a searchable symbol — the pipeline's best-match collapsed it to a
|
|
9460
|
+
// bare module name. Ground the overview on the ranked ENTRY-POINT module
|
|
9461
|
+
// instead: named as the way in, with its full module-grain overview.
|
|
9462
|
+
if (graph && /^(?:this|the)?\s*(?:app|application|codebase|project|repo|repository|system|program)$/i.test(term)) {
|
|
9463
|
+
try {
|
|
9464
|
+
const { ask } = await import("../domain/ask.mjs");
|
|
9465
|
+
const entry = ask(graph, "where is the entry point")?.tmct_ask?.matches?.[0];
|
|
9466
|
+
const ind = entry?.id ? graph.byId?.get?.(entry.id) : null;
|
|
9467
|
+
if (ind) {
|
|
9468
|
+
return { text: `the app enters at ${ind.label} — here is that module in detail:\n\n${moduleOverviewText(graph, ind)}` };
|
|
9469
|
+
}
|
|
9470
|
+
} catch { /* no entry point rankable — the ordinary pipeline below decides */ }
|
|
9471
|
+
}
|
|
9220
9472
|
try {
|
|
9221
9473
|
const { generateCompletion } = await import("./completions.mjs");
|
|
9222
9474
|
// createCompletionsGraphAdapter wraps the SAME graph object this turn
|
|
@@ -9408,7 +9660,18 @@ const sameGoalSpec = (a, b) =>
|
|
|
9408
9660
|
* { text, via, deduced, note, plan? } or null when the query is none of the
|
|
9409
9661
|
* three shapes. Mutates planHolder.state (the session's plan slot). */
|
|
9410
9662
|
async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", }) {
|
|
9411
|
-
|
|
9663
|
+
let q = String(query).trim();
|
|
9664
|
+
// GOAL REVISION — "actually the goal is …", "instead, the goal is …", "the
|
|
9665
|
+
// goal is now …": a revision marker ahead of (or inside) a goal frame means
|
|
9666
|
+
// REPLACE the held goal, not accumulate beside it — restating used to pile
|
|
9667
|
+
// up an unsatisfiable conjunction that burned the full search.
|
|
9668
|
+
let goalRevision = false;
|
|
9669
|
+
{
|
|
9670
|
+
const lead = q.match(/^(?:actually|instead|no|wait|scratch\s+that|on\s+second\s+thought)[,\s]+(.+)$/i);
|
|
9671
|
+
if (lead && /\bgoal\b/i.test(lead[1])) { q = lead[1].trim(); goalRevision = true; }
|
|
9672
|
+
const now = q.match(/^the\s+(?:new\s+goal\s+is|goal\s+is\s+now)\s+(.+)$/i);
|
|
9673
|
+
if (now) { q = `the goal is ${now[1].trim()}`; goalRevision = true; }
|
|
9674
|
+
}
|
|
9412
9675
|
|
|
9413
9676
|
// "can you move a disk onto a peg?" — read the taught action signatures back.
|
|
9414
9677
|
// Answered HERE rather than beside the other capability readers because
|
|
@@ -9532,7 +9795,8 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
9532
9795
|
? q.replace(/^the\s+goal\s+is\s+that\s+/i, "").replace(/[.!?]+$/, "")
|
|
9533
9796
|
: `${m[1] ? `${m[1].toLowerCase()} ` : ""}${m[2].toLowerCase()} ${verb}s ${m[4].toLowerCase()} ${m[5].toLowerCase()}`));
|
|
9534
9797
|
}
|
|
9535
|
-
const prev = planHolder.state && Array.isArray(planHolder.state.goals) && !planHolder.state.done ? planHolder.state : null;
|
|
9798
|
+
const prev = !goalRevision && planHolder.state && Array.isArray(planHolder.state.goals) && !planHolder.state.done ? planHolder.state : null;
|
|
9799
|
+
const replaced = goalRevision && planHolder.state?.goalTexts?.length ? planHolder.state.goalTexts.join("; ") : null;
|
|
9536
9800
|
// Restating a goal you already set is one goal, not two. The spec is four
|
|
9537
9801
|
// normalized scalars, so the same goal in either voicing ("the goal is
|
|
9538
9802
|
// that …" / "the goal is to …") compiles to the identical object and a
|
|
@@ -9557,11 +9821,12 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
9557
9821
|
actions: null, states: null, stepGoals: null, cursor: 0, done: false,
|
|
9558
9822
|
};
|
|
9559
9823
|
const n = heldGoals.length;
|
|
9824
|
+
const replacedClause = replaced ? ` (replacing the earlier goal: ${replaced})` : "";
|
|
9560
9825
|
return {
|
|
9561
|
-
text: `${added ? "noted" : "already noted"} — the goal is that ${tails.join(" and ")}.${n > 1 ? ` (${n} goals held)` : ""} Say "solve it" when the state is taught.`,
|
|
9826
|
+
text: `${added ? "noted" : "already noted"} — the goal is that ${tails.join(" and ")}${replacedClause}.${n > 1 ? ` (${n} goals held)` : ""} Say "solve it" when the state is taught.`,
|
|
9562
9827
|
via: "plan", lane: "goal", deduced: "record the goal state for a later plan",
|
|
9563
9828
|
note: added
|
|
9564
|
-
? `GOAL frame — ${added === 1 ? "goal spec" : `${added} goal specs`} accumulated on the session plan slot`
|
|
9829
|
+
? `GOAL frame — ${added === 1 ? "goal spec" : `${added} goal specs`} ${replaced ? "REPLACED the held goal (revision marker)" : "accumulated on the session plan slot"}`
|
|
9565
9830
|
: "GOAL frame — the same goal spec was already held, so it folded onto the existing one",
|
|
9566
9831
|
};
|
|
9567
9832
|
}
|
|
@@ -9639,6 +9904,49 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
9639
9904
|
note: "plan lane — honest decline: the goal names an untaught term, search never started",
|
|
9640
9905
|
};
|
|
9641
9906
|
}
|
|
9907
|
+
// An UNSATISFIABLE conjunction — two held goals put the same subject (or
|
|
9908
|
+
// the same universal class) in two different places under one predicate —
|
|
9909
|
+
// is named BEFORE the search, so it never burns the full move budget just
|
|
9910
|
+
// to report "no plan found".
|
|
9911
|
+
{
|
|
9912
|
+
const texts = planHolder.state.goalTexts || [];
|
|
9913
|
+
for (let i = 0; i < goals.length; i += 1) {
|
|
9914
|
+
for (let j = i + 1; j < goals.length; j += 1) {
|
|
9915
|
+
const a = goals[i];
|
|
9916
|
+
const b = goals[j];
|
|
9917
|
+
if (a.universal === b.universal && a.term === b.term && a.predicate === b.predicate && a.object !== b.object) {
|
|
9918
|
+
return {
|
|
9919
|
+
text: `those goals can't both hold — "${texts[i] ?? `${a.term} … ${a.object}`}" and "${texts[j] ?? `${b.term} … ${b.object}`}" put the same thing in two places, so no plan exists and I won't search for one. Say "forget the goal", then state the goal you mean.`,
|
|
9920
|
+
via: "plan", deduced: "plan a move sequence (unsatisfiable goal conjunction)",
|
|
9921
|
+
note: "plan lane — honest decline: conflicting goal atoms named before the search",
|
|
9922
|
+
};
|
|
9923
|
+
}
|
|
9924
|
+
}
|
|
9925
|
+
}
|
|
9926
|
+
}
|
|
9927
|
+
// A CONTRADICTORY taught board — one piece placed in two places by the base
|
|
9928
|
+
// facts — makes every "shortest" claim depend on which placement you
|
|
9929
|
+
// resolve, so it is flagged before planning rather than silently read.
|
|
9930
|
+
{
|
|
9931
|
+
const placements = new Map();
|
|
9932
|
+
for (const r of state) {
|
|
9933
|
+
const key = `${r.subject} ${r.predicate}`;
|
|
9934
|
+
if (!placements.has(key)) placements.set(key, new Set());
|
|
9935
|
+
placements.get(key).add(r.object);
|
|
9936
|
+
}
|
|
9937
|
+
const clashes = [...placements.entries()].filter(([, objs]) => objs.size > 1);
|
|
9938
|
+
if (clashes.length) {
|
|
9939
|
+
const shown = clashes.map(([key, objs]) => {
|
|
9940
|
+
const [subj, pred] = key.split(" ");
|
|
9941
|
+
return [...objs].map((o) => `${subj} ${predicatePhrase(pred)} ${o}`).join(" AND ");
|
|
9942
|
+
}).join("; ");
|
|
9943
|
+
return {
|
|
9944
|
+
text: `the taught board contradicts itself — ${shown}. A shortest plan depends on which placement is real, so I won't pick one. Say "forget that <the wrong placement>" (e.g. "forget that ${clashes[0][0].split(" ")[0]} ${predicatePhrase(clashes[0][0].split(" ")[1])} ${[...clashes[0][1]][1]}"), then "solve it" again.`,
|
|
9945
|
+
via: "plan", deduced: "plan a move sequence (contradictory board)",
|
|
9946
|
+
note: "plan lane — honest decline: contradictory placements flagged before planning",
|
|
9947
|
+
};
|
|
9948
|
+
}
|
|
9949
|
+
}
|
|
9642
9950
|
let isGoal;
|
|
9643
9951
|
try {
|
|
9644
9952
|
isGoal = compileGoal(goals, domain);
|
|
@@ -9684,11 +9992,20 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
9684
9992
|
};
|
|
9685
9993
|
const ruleNames = [...new Set(domain.actions.map((a) => a.name))].join('", "');
|
|
9686
9994
|
const moveLines = actions.map((a, i) => ` ${i + 1}. ${a.label}`);
|
|
9687
|
-
// A piece
|
|
9688
|
-
//
|
|
9689
|
-
//
|
|
9995
|
+
// A piece with no taught position is an ASSUMPTION the plan silently makes
|
|
9996
|
+
// (it reads the board as taught, without that piece) — said out loud with
|
|
9997
|
+
// the plan rather than left implicit. Covers every piece a plan STEP
|
|
9998
|
+
// touches, not just the goal-named ones ("move disk-1 onto disk-3" with
|
|
9999
|
+
// disk-3 never placed is the same silent gap-fill). Scoped to pieces whose
|
|
10000
|
+
// CLASS has at least one positioned member, so a peg — whose class never
|
|
10001
|
+
// takes a position — is not "unplaced".
|
|
9690
10002
|
const goalPieces = [...new Set(goals.flatMap((g) => (g.universal ? (domain.classMembers?.[g.term] || []) : [g.term])))];
|
|
9691
|
-
const
|
|
10003
|
+
const touchedPieces = [...new Set(actions.flatMap((a) => [a.subject, a.target]))];
|
|
10004
|
+
const classOfPiece = (p) => Object.keys(domain.classMembers || {}).find((cls) => (domain.classMembers[cls] || []).includes(p));
|
|
10005
|
+
const positionedClasses = new Set(state.map((r) => classOfPiece(r.subject)).filter(Boolean));
|
|
10006
|
+
const unplacedPieces = [...new Set([...goalPieces, ...touchedPieces])]
|
|
10007
|
+
.filter((p) => !state.some((r) => r.subject === p))
|
|
10008
|
+
.filter((p) => positionedClasses.has(classOfPiece(p)));
|
|
9692
10009
|
const assumptionNote = unplacedPieces.length
|
|
9693
10010
|
? `\n\nnote — ${unplacedPieces.join(" and ")} ha${unplacedPieces.length === 1 ? "s" : "ve"} no taught position, so this plan reads the board without ${unplacedPieces.length === 1 ? "it" : "them"}. Teach the missing position(s) and solve again if that's wrong.`
|
|
9694
10011
|
: "";
|
|
@@ -9756,11 +10073,43 @@ async function executePlanStep(planHolder, { memoryDir, sessionId = "" }) {
|
|
|
9756
10073
|
* "next" moves a piece, "what rests on X" reflects the snapshot, not the stale
|
|
9757
10074
|
* pre-plan facts. Clearness is derived, never stored — a piece is clear iff
|
|
9758
10075
|
* nothing rests on it on the current board. */
|
|
9759
|
-
async function planFollowUpAnswer(query, { memoryDir, planHolder }) {
|
|
10076
|
+
async function planFollowUpAnswer(query, { memoryDir, planHolder, pendingPager = false }) {
|
|
9760
10077
|
const q = String(query).trim();
|
|
9761
10078
|
const ps = planHolder?.state;
|
|
9762
10079
|
const activePlan = ps && Array.isArray(ps.actions) && ps.actions.length;
|
|
9763
10080
|
|
|
10081
|
+
// PLAN-NAVIGATION GESTURES — routed, honest replies while a plan (or a
|
|
10082
|
+
// held goal) stands, so the orientation blurb never fronts a mid-plan
|
|
10083
|
+
// turn. With nothing standing these return null and a cold "undo" keeps
|
|
10084
|
+
// its ordinary path.
|
|
10085
|
+
if (PLAN_UNDO_RE.test(q)) {
|
|
10086
|
+
if (!activePlan) return null;
|
|
10087
|
+
const k = ps.cursor;
|
|
10088
|
+
const board = k > 0 ? `the board stands at board@step${k}` : "no move has been made yet";
|
|
10089
|
+
return {
|
|
10090
|
+
text: `there's no undo — each move wrote a board@step snapshot and I don't unwind them. ${board}. Say "solve it" to replan from the current board, or "forget the goal" to drop the plan.`,
|
|
10091
|
+
deduced: "unwind a plan move (not supported — honest decline)",
|
|
10092
|
+
note: "PLAN FOLLOW-UP — undo/go-back gesture named the snapshot model instead of the blurb",
|
|
10093
|
+
};
|
|
10094
|
+
}
|
|
10095
|
+
if (PLAN_FORGET_GOAL_RE.test(q)) {
|
|
10096
|
+
if (!ps || !(ps.goals?.length || activePlan)) return null;
|
|
10097
|
+
const held = ps.goalTexts?.length ? ` (${ps.goalTexts.join("; ")})` : "";
|
|
10098
|
+
planHolder.state = null;
|
|
10099
|
+
return {
|
|
10100
|
+
text: `forgotten — the goal${held} and its plan are dropped. The taught board facts stay; set a new goal with "the goal is that …".`,
|
|
10101
|
+
deduced: "drop the held goal and plan",
|
|
10102
|
+
note: "PLAN FOLLOW-UP — forget-the-goal cleared the session plan slot",
|
|
10103
|
+
};
|
|
10104
|
+
}
|
|
10105
|
+
if (PLAN_NEXT_RE.test(q) && ps?.done && Array.isArray(ps.actions) && ps.actions.length && !pendingPager) {
|
|
10106
|
+
return {
|
|
10107
|
+
text: `the plan is complete — all ${ps.actions.length} moves are made and the goal was checked against the written board. Teach a new goal ("the goal is that …") to plan again.`,
|
|
10108
|
+
deduced: "continue a plan that is already complete (honest decline)",
|
|
10109
|
+
note: "PLAN FOLLOW-UP — next-after-done answered from the finished plan instead of the blurb",
|
|
10110
|
+
};
|
|
10111
|
+
}
|
|
10112
|
+
|
|
9764
10113
|
if (PLAN_WHAT_NEXT_RE.test(q)) {
|
|
9765
10114
|
if (!activePlan) return null;
|
|
9766
10115
|
if (ps.done || ps.cursor >= ps.actions.length) {
|
|
@@ -9846,6 +10195,30 @@ async function planFollowUpAnswer(query, { memoryDir, planHolder }) {
|
|
|
9846
10195
|
: { text: `nothing ${emptyPhrase} ${x} on the current board.`, deduced: "read the current board (what rests on a piece)", note: "BOARD — reverse locative, nothing on the current board" };
|
|
9847
10196
|
}
|
|
9848
10197
|
|
|
10198
|
+
/** "what was X called/named before" and its siblings — a name-HISTORY ask.
|
|
10199
|
+
* The index records current names only, so the whole family declines by
|
|
10200
|
+
* name (mirrors the guarded "renamed X" adjective — the verb slipped). */
|
|
10201
|
+
const RENAME_HISTORY_RE = /^what\s+(?:was|were)\s+(.+?)\s+(?:called|named|known\s+as)\s+(?:before|previously|originally|earlier|at\s+first)[?.!\s]*$|^what\s+did\s+(.+?)\s+use(?:d)?\s+to\s+be\s+(?:called|named)[?.!\s]*$/i;
|
|
10202
|
+
|
|
10203
|
+
/** "what do the handlers import" — a COLLECTIVE plural subject naming a
|
|
10204
|
+
* module GROUP (a directory/path component), not a single module. The
|
|
10205
|
+
* resolver's best-match tiers read "handlers" as one module and answer for
|
|
10206
|
+
* it alone, silently — a wrong set with no disclosure. The closed verb set
|
|
10207
|
+
* mirrors the forward relations; a plural that names a graph KIND
|
|
10208
|
+
* (modules/classes/…) stays with the engine's own kind-level reading. */
|
|
10209
|
+
const COLLECTIVE_FORWARD_RE = /^what\s+(?:do|does)\s+the\s+([a-z][\w-]*s)\s+(import|call|use|export|touch|test|define|contain)s?[?.!\s]*$/i;
|
|
10210
|
+
|
|
10211
|
+
/** Decision-recall — "remind me what we decided about X", "what did we agree
|
|
10212
|
+
* on about X": a question about the CONVERSATION's record, so it belongs to
|
|
10213
|
+
* the session-recall surface, never the definition locator ("decided" used
|
|
10214
|
+
* to be read-as-rewritten into "defined"). */
|
|
10215
|
+
const DECISION_RECALL_RE = /^(?:remind\s+me\s+)?what\s+(?:did\s+)?(?:we|i|you)\s+(?:decided?|agreed?(?:\s+on)?|settled?(?:\s+on)?|concluded?)\s+(?:about|on|regarding|for)\s+(?:the\s+)?(.+?)[?.!\s]*$/i;
|
|
10216
|
+
|
|
10217
|
+
/** "where did X move to" — a move-HISTORY ask, sibling of RENAME_HISTORY_RE:
|
|
10218
|
+
* the index records current locations only, so the premise is named rather
|
|
10219
|
+
* than silently accepted alongside the current location. */
|
|
10220
|
+
const MOVE_HISTORY_RE = /^where\s+did\s+(.+?)\s+(?:move|get\s+moved|go)(?:\s+to)?[?.!\s]*$/i;
|
|
10221
|
+
|
|
9849
10222
|
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null, tel = null, biasByBundle = {}, cache = null, vocabAntecedent = null, planHolder = null }) {
|
|
9850
10223
|
const ts = new Date().toISOString();
|
|
9851
10224
|
// DISCOURSE ANAPHORA: a follow-up like "which of those are tested" / "count
|
|
@@ -9871,6 +10244,96 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
9871
10244
|
.replace(/^(?:and|so|then|also)\s+/i, "")
|
|
9872
10245
|
.replace(/how many\s+/i, "how many of those ");
|
|
9873
10246
|
}
|
|
10247
|
+
// RENAME HISTORY — "what was X called before" and its siblings. The index
|
|
10248
|
+
// records current names only, and without this gate "called" fuzzes onto
|
|
10249
|
+
// the calls relation ("before" simply drops), so the reply read as fluent
|
|
10250
|
+
// confirmation of a rename that never happened. Checked BEFORE the ask
|
|
10251
|
+
// engine because the misread ANSWERS — a miss-gated lane never gets a turn.
|
|
10252
|
+
{
|
|
10253
|
+
const rename = String(query).trim().match(RENAME_HISTORY_RE);
|
|
10254
|
+
if (rename) {
|
|
10255
|
+
const term = (rename[1] ?? rename[2]).trim().replace(/^the\s+/i, "");
|
|
10256
|
+
const ent = graph ? await resolveEntity(graph, term) : null;
|
|
10257
|
+
const named = ent ? `${ent.label} is its only recorded name` : `"${term}" has no recorded prior name`;
|
|
10258
|
+
note(trace, "goal: recover a name history the index does not record (honest decline)");
|
|
10259
|
+
note(trace, "lane: RENAME_HISTORY_RE — the index carries no rename data, so the calls-relation misread is refused by name");
|
|
10260
|
+
return plainTurn(query, `I can't say what ${ent ? ent.label : `"${term}"`} was called before — this index records current names only, no rename history. ${named}${ent ? ` here; "who touched ${ent.label}" lists its recorded commits` : ""}.`, {
|
|
10261
|
+
via: "miss", miss: true, focus,
|
|
10262
|
+
});
|
|
10263
|
+
}
|
|
10264
|
+
}
|
|
10265
|
+
// COLLECTIVE PLURAL SUBJECT — see COLLECTIVE_FORWARD_RE. Members are the
|
|
10266
|
+
// modules whose path carries the plural as a component; two or more make it
|
|
10267
|
+
// a group question, answered as the disclosed union over every member. One
|
|
10268
|
+
// or zero members leaves the ordinary resolver reading untouched.
|
|
10269
|
+
{
|
|
10270
|
+
const collective = graph ? String(query).trim().match(COLLECTIVE_FORWARD_RE) : null;
|
|
10271
|
+
const stem = collective ? collective[1].toLowerCase() : null;
|
|
10272
|
+
if (collective && !ENTITY_TO_TYPE[stem] && !ENTITY_TO_TYPE[singularizeSurface(stem)]) {
|
|
10273
|
+
const memberRe = new RegExp(`(^|/)${escapeRegex(stem)}(/|\\.|$)`, "i");
|
|
10274
|
+
const members = graph.individuals.filter((i) => i.class === "Module" && memberRe.test(String(i.label)));
|
|
10275
|
+
if (members.length > 1) {
|
|
10276
|
+
const verb = collective[2].toLowerCase();
|
|
10277
|
+
const { ask } = await import("../domain/ask.mjs");
|
|
10278
|
+
const union = new Map();
|
|
10279
|
+
for (const member of members) {
|
|
10280
|
+
const r = ask(graph, `what does ${member.label} ${verb}`);
|
|
10281
|
+
for (const hit of r?.tmct_ask?.matches ?? []) if (hit?.id) union.set(hit.id, hit.label ?? hit.id);
|
|
10282
|
+
}
|
|
10283
|
+
const memberList = joinList(members.map((mm) => mm.label).sort());
|
|
10284
|
+
const labels = [...union.values()].sort();
|
|
10285
|
+
const text = labels.length
|
|
10286
|
+
? `the ${stem} here are ${memberList} — together they ${verb}: ${joinList(labels)}.`
|
|
10287
|
+
: `the ${stem} here are ${memberList} — none of them has ${verb} edges in the index.`;
|
|
10288
|
+
note(trace, `goal: read a forward relation over a module GROUP (${members.length} members), unioned with the set disclosed`);
|
|
10289
|
+
note(trace, `lane: COLLECTIVE_FORWARD_RE — "${stem}" resolved to ${members.length} modules; answered the union, never a silent single best-match`);
|
|
10290
|
+
const turn = plainTurn(query, text, { via: "composed", miss: !labels.length, focus });
|
|
10291
|
+
turn.detail = { traversal: `${verb} edges unioned over ${memberList}`, matches: [...union.keys()].map((id) => graph.byId?.get?.(id)).filter(Boolean) };
|
|
10292
|
+
return turn;
|
|
10293
|
+
}
|
|
10294
|
+
}
|
|
10295
|
+
}
|
|
10296
|
+
// MOVE HISTORY — "where did X move to": the index records current
|
|
10297
|
+
// locations, not moves, so the premise is denied by name and the current
|
|
10298
|
+
// location answers beside it (stating the location alone read as silently
|
|
10299
|
+
// confirming a move nobody recorded).
|
|
10300
|
+
{
|
|
10301
|
+
const moved = graph ? String(query).trim().match(MOVE_HISTORY_RE) : null;
|
|
10302
|
+
if (moved) {
|
|
10303
|
+
const term = moved[1].trim().replace(/^the\s+/i, "");
|
|
10304
|
+
const ent = await resolveEntity(graph, term);
|
|
10305
|
+
if (ent) {
|
|
10306
|
+
let located = "";
|
|
10307
|
+
try {
|
|
10308
|
+
const { ask } = await import("../domain/ask.mjs");
|
|
10309
|
+
const r = ask(graph, `where is ${ent.label} defined`);
|
|
10310
|
+
if (r?.content && !r?.tmct_ask?.miss) located = ` Right now, ${r.content}`;
|
|
10311
|
+
} catch { /* the premise note stands alone */ }
|
|
10312
|
+
note(trace, "goal: recover a move history the index does not record (premise denied, current location cited)");
|
|
10313
|
+
note(trace, "lane: MOVE_HISTORY_RE — no move data exists; the current location answers with the premise named");
|
|
10314
|
+
return plainTurn(query, `this index records current locations only, so I can't confirm ${ent.label} moved anywhere.${located}`, {
|
|
10315
|
+
via: "composed", miss: false, focus,
|
|
10316
|
+
});
|
|
10317
|
+
}
|
|
10318
|
+
}
|
|
10319
|
+
}
|
|
10320
|
+
// DECISION RECALL — "remind me what we decided about X" reaches the
|
|
10321
|
+
// session-recall surface (the folded transcript blocks); with nothing
|
|
10322
|
+
// relevant folded it misses honestly by name. Never the definition locator
|
|
10323
|
+
// ("decided" is not "defined").
|
|
10324
|
+
{
|
|
10325
|
+
const decision = memoryDir ? String(query).trim().match(DECISION_RECALL_RE) : null;
|
|
10326
|
+
if (decision) {
|
|
10327
|
+
const term = decision[1].trim();
|
|
10328
|
+
const recalled = await recallFromBlocks(memoryDir, `what did we decide about ${term}`, graph);
|
|
10329
|
+
note(trace, "goal: recall a decision from the conversation record (session-recall surface)");
|
|
10330
|
+
note(trace, `lane: DECISION_RECALL_RE — routed to the folded-session recall surface, ${recalled ? "a relevant block answered" : "nothing relevant folded (honest miss)"}`);
|
|
10331
|
+
return plainTurn(query, recalled
|
|
10332
|
+
?? `I don't have a recorded decision about "${term}" — I keep facts and session transcripts, and nothing folded mentions deciding on it. "what did i ask before" lists the last session's questions.`, {
|
|
10333
|
+
via: recalled ? "recall" : "miss", miss: !recalled, focus,
|
|
10334
|
+
});
|
|
10335
|
+
}
|
|
10336
|
+
}
|
|
9874
10337
|
// W2: the explicit recall forms are answered from memory's folded blocks, never
|
|
9875
10338
|
// the graph. Gated on memoryDir — a bare runTurn (no session shell) stays pure.
|
|
9876
10339
|
if (memoryDir && RECALL_ASK_RE.test(String(query).trim())) {
|
|
@@ -9936,10 +10399,13 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
9936
10399
|
// on that first turn, so the test is noCodeGraph, not `!graph`: an empty
|
|
9937
10400
|
// graph is as unusable as an absent one, and reporting its emptiness to
|
|
9938
10401
|
// someone asking about a dog answers a question they never asked.
|
|
10402
|
+
// The exit named is the one this session can actually take: a SEEDED
|
|
10403
|
+
// vocabulary session points at a vocabulary shape (code-question
|
|
10404
|
+
// examples are the wrong audience here), an unseeded one at the seed/
|
|
10405
|
+
// teach pair — vocabHint already carries exactly that split.
|
|
9939
10406
|
answer = (!graph || noCodeGraph(graph)) && (!config || e?.emptyGraph || /^cannot read graph artifact\b/.test(thrown))
|
|
9940
|
-
?
|
|
9941
|
-
|
|
9942
|
-
+ "or run `tmct init` in a repo to index one."
|
|
10407
|
+
? `I can't answer that as a code question — no code graph is loaded in this session. ${vocabHint
|
|
10408
|
+
|| "I can still remember and answer taught facts (try \"every bug is an issue\"), or run `tmct init` in a repo to index one."}`
|
|
9943
10409
|
: thrown;
|
|
9944
10410
|
note(trace, `intermediate: the ask engine threw — ${thrown}`);
|
|
9945
10411
|
}
|
|
@@ -10266,6 +10732,16 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
10266
10732
|
const gateQuery = normalizeQuery(String(query));
|
|
10267
10733
|
const bareWhatisShape = BARE_WHATIS_RE.test(gateQuery);
|
|
10268
10734
|
const isAdjectiveShape = IS_ADJECTIVE_YESNO_RE.test(gateQuery);
|
|
10735
|
+
// A BARE NOUN on its own ("dog", "teh dog" after the typo repair) is the
|
|
10736
|
+
// shortest vocabulary opener there is — it answers exactly as "what is a
|
|
10737
|
+
// dog" does, gated on a REAL fact hit like every divert in this family, so
|
|
10738
|
+
// chatter with no facts behind it still falls to the ordinary card. Read
|
|
10739
|
+
// off the raw line (typos repaired), NOT the filler-stripped gateQuery: a
|
|
10740
|
+
// request with filler around a noun ("jokes please") is a different speech
|
|
10741
|
+
// act than a bare noun, and stripping must not manufacture one.
|
|
10742
|
+
const bareNounMatch = correctMisspellings(String(query).trim()).replace(/[?.!]+\s*$/, "").trim()
|
|
10743
|
+
.match(/^(?:the\s+|a\s+|an\s+)?([a-z][a-z-]*)$/i);
|
|
10744
|
+
const bareNounShape = !!bareNounMatch;
|
|
10269
10745
|
// `isBareCamelCaseMetaQuestion` ORs in alongside isConversationalCandidate
|
|
10270
10746
|
// for THIS lane only — a bare "what is TaskController" (CamelCase compound,
|
|
10271
10747
|
// no article) is otherwise excluded solely because isConversational()'s
|
|
@@ -10288,10 +10764,17 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
10288
10764
|
const capabilityAskShape = CAN_ASK_RE.test(gateQuery) || WHAT_CAN_DO_RE.test(gateQuery)
|
|
10289
10765
|
|| DO_VERB_ASK_RE.test(gateQuery) || WHICH_KIND_CAN_RE.test(gateQuery) || WHAT_CAN_VERB_RE.test(gateQuery);
|
|
10290
10766
|
let bareMetaHit = null;
|
|
10291
|
-
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape || capabilityAskShape)) {
|
|
10767
|
+
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape || capabilityAskShape || bareNounShape)) {
|
|
10292
10768
|
if (memoryDir) {
|
|
10293
|
-
|
|
10294
|
-
|
|
10769
|
+
// The bare noun asks its own "what is a X" — the readers never see the
|
|
10770
|
+
// single word, so the vocabulary route is the constructed question's.
|
|
10771
|
+
const bareNoun = bareNounShape ? singularizeSurface(bareNounMatch[1].toLowerCase()) : null;
|
|
10772
|
+
const factQuery = bareNounShape && !bareWhatisShape
|
|
10773
|
+
? `what is ${indefiniteArticleFor(bareNoun)} ${bareNoun}`
|
|
10774
|
+
: gateQuery;
|
|
10775
|
+
bareMetaHit = (await factAnswer(memoryDir, factQuery, envelope, miss, biasByBundle, cache, newFocus?.label))
|
|
10776
|
+
?? (await factReadBack(memoryDir, factQuery, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
|
|
10777
|
+
if (bareNounShape && !bareWhatisShape && bareMetaHit?.miss) bareMetaHit = null;
|
|
10295
10778
|
// An honest-miss return never diverts the gate — EXCEPT the capability
|
|
10296
10779
|
// family's can't-confirm, which names the subject's real capabilities
|
|
10297
10780
|
// and a round-trip teach hint: strictly more useful than the
|
|
@@ -10302,14 +10785,14 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
10302
10785
|
// hit" treatment — curatedDefinitionAnswer otherwise only ever runs once
|
|
10303
10786
|
// the article makes T5's structural parse succeed.
|
|
10304
10787
|
if (!bareMetaHit) {
|
|
10305
|
-
const def = await curatedDefinitionAnswer(
|
|
10788
|
+
const def = await curatedDefinitionAnswer(factQuery, envelope, { memoryDir, lexicon });
|
|
10306
10789
|
if (def) bareMetaHit = { text: def.text, replace: true };
|
|
10307
10790
|
}
|
|
10308
10791
|
// The reference pack's bare-form fallback, beside the curated one and
|
|
10309
10792
|
// under the IDENTICAL clean-miss gate the articled hook (4h) applies —
|
|
10310
10793
|
// "what is otter" reaches the pack exactly as "what is an otter" does.
|
|
10311
10794
|
if (!bareMetaHit) {
|
|
10312
|
-
const refTerm = metaTermOf(
|
|
10795
|
+
const refTerm = metaTermOf(factQuery, envelope);
|
|
10313
10796
|
const ref = refTerm ? await referencePackMissAnswer(refTerm, { graph, memoryDir, lexicon, env, cache }) : null;
|
|
10314
10797
|
if (ref) bareMetaHit = { text: ref.text, replace: true, reference: ref };
|
|
10315
10798
|
}
|
|
@@ -10395,6 +10878,20 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
10395
10878
|
via = "template"; handled = true;
|
|
10396
10879
|
note(trace, "lane: (2) COLD PRONOUN — a subject pronoun with no antecedent bound and no focus standing; named the pronoun instead of the orientation card");
|
|
10397
10880
|
note(trace, "goal: resolve a pronoun to a subject (nothing named yet)");
|
|
10881
|
+
} else if (isConversationalCandidate && planHolder?.state?.game) {
|
|
10882
|
+
// MID-GAME: a short line that parsed as nothing ("you said lower", "is it
|
|
10883
|
+
// warm in here") stays INSIDE the game frame with a nudge naming the
|
|
10884
|
+
// state — the identity card answers a question nobody asked, and it used
|
|
10885
|
+
// to front exactly these turns. Real asides ("what is a dog") still
|
|
10886
|
+
// route out above; only the would-be blurb is replaced.
|
|
10887
|
+
const game = planHolder.state.game;
|
|
10888
|
+
answer = game.mode === "guesser"
|
|
10889
|
+
? `we're mid-game — I'm guessing your number (currently between ${game.lo} and ${game.hi}; my guess: ${game.guess}). Say higher, lower, or correct — or "I give up" to stop.`
|
|
10890
|
+
: `we're mid-game — you're guessing my number between ${game.lo0} and ${game.hi0}${game.lastHint ? ` (my last hint: ${game.lastHint} than your ${game.lastGuess})` : ""}. Guess a number — or "I give up" to stop.`;
|
|
10891
|
+
via = "game"; handled = true;
|
|
10892
|
+
dialogueLaneOverride = "game-inform";
|
|
10893
|
+
note(trace, "lane: (2) MID-GAME NUDGE — an unparsed short turn stayed inside the live game frame instead of the identity card");
|
|
10894
|
+
note(trace, "goal: keep the running guess-the-number game on track");
|
|
10398
10895
|
} else if (isConversationalCandidate) {
|
|
10399
10896
|
// A conversational miss (a greeting, "what can you do", a very short non-code
|
|
10400
10897
|
// line) gets the friendly orientation (module-aware: empty → --repo/tmct init).
|
|
@@ -10566,7 +11063,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
10566
11063
|
// (4) #2 TEACH lane — a teach-shaped would-miss nothing above answered: route to
|
|
10567
11064
|
// memory, or say what CAN be remembered (LOUD), never the wall / a silent drop.
|
|
10568
11065
|
if (miss && recordMiss && via === "composed") {
|
|
10569
|
-
const taught = await teachLane(query, { memoryDir, sessionId, lexicon, cache });
|
|
11066
|
+
const taught = await teachLane(query, { memoryDir, sessionId, lexicon, cache, planHolder });
|
|
10570
11067
|
if (taught) {
|
|
10571
11068
|
answer = taught.text; via = taught.via; recordMiss = taught.miss;
|
|
10572
11069
|
if (!taught.miss) dialogueLaneOverride = "teach";
|
|
@@ -10636,7 +11133,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
10636
11133
|
if (miss && recordMiss && via === "composed") {
|
|
10637
11134
|
const described = await describeWrapperAnswer(query, { config, source, focus: newFocus, graph, tel });
|
|
10638
11135
|
if (described) {
|
|
10639
|
-
answer = described.text; via = "describe"; recordMiss =
|
|
11136
|
+
answer = described.text; via = described.miss ? "miss" : "describe"; recordMiss = !!described.miss;
|
|
10640
11137
|
note(trace, "lane: (4d) DESCRIBE-WRAPPER RESCUE — a polite wrapper around \"describe/tell me about <symbol>\" resolved via /describe, tried last after every other lane declined");
|
|
10641
11138
|
note(trace, "goal: get a symbol's definition/kind/relations (phrased conversationally)");
|
|
10642
11139
|
// Carry the resolved entity forward as the new focus, same class-gated
|
|
@@ -10739,7 +11236,13 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
10739
11236
|
// miss re-offers the tailored hint instead of droning.
|
|
10740
11237
|
if (miss && recordMiss && via === "composed" && WALL_MISS_RE.test(answer)) {
|
|
10741
11238
|
const repeat = last?.answer && WALL_MISS_RE.test(String(last.answer));
|
|
10742
|
-
|
|
11239
|
+
// A GRAPH-LESS session's wall must not hand a vocabulary question a list
|
|
11240
|
+
// of import/calls shapes — that guidance is aimed at an audience that
|
|
11241
|
+
// isn't in the room. Offer what THIS session can answer instead.
|
|
11242
|
+
answer = repeat ? WALL_REPEAT_ONELINER
|
|
11243
|
+
: (noCodeGraph(graph) && vocabHint
|
|
11244
|
+
? `I couldn't read that as a question I can answer. ${vocabHint} Type /help for all query shapes.`
|
|
11245
|
+
: shortMissHint(query));
|
|
10743
11246
|
via = "miss";
|
|
10744
11247
|
note(trace, `lane: (5) SHORT TAILORED MISS — every lane above declined; ${repeat ? "REPEAT collapsed to one-liner (wall kindness)" : "the full grammar wall was shortened + tailored to the query's keywords"}`);
|
|
10745
11248
|
}
|
|
@@ -11212,7 +11715,8 @@ function sentenceTeachesAlone(sentence, parseAce, lex) {
|
|
|
11212
11715
|
if (!s || s.includes("?")) return false;
|
|
11213
11716
|
const parse = parseAce(s, lex);
|
|
11214
11717
|
if (parse && parse.triples?.length && !parse.residue?.length) return true;
|
|
11215
|
-
return DECLARATIVE_KIND_OF_RE.test(s) || COMPARATIVE_TEACH_RE.test(s)
|
|
11718
|
+
return DECLARATIVE_KIND_OF_RE.test(s) || COMPARATIVE_TEACH_RE.test(s)
|
|
11719
|
+
|| RENDERS_AS_TEACH_RE.test(s) || matchesGeneralVerbTeachFrame(s);
|
|
11216
11720
|
}
|
|
11217
11721
|
|
|
11218
11722
|
/** Does every sentence of a multi-sentence line teach on its own? Then the line
|
|
@@ -11233,6 +11737,10 @@ async function everySentenceTeaches(sentences, lexicon) {
|
|
|
11233
11737
|
}
|
|
11234
11738
|
|
|
11235
11739
|
async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, cache = null }) {
|
|
11740
|
+
// A trailing "?" marks a question, and a question never writes — the ACE
|
|
11741
|
+
// fragment happily parses "dog have tail?" as the declarative it is not,
|
|
11742
|
+
// which stored a Fact at teach trust over a FLOW-0 vocabulary question.
|
|
11743
|
+
if (/\?\s*$/.test(String(line).trim())) return null;
|
|
11236
11744
|
try {
|
|
11237
11745
|
const { parseAce, parseAceAmbiguous } = await import("../domain/grammar/ace.mjs");
|
|
11238
11746
|
// A session handle carries its own loaded lexicon (createSession loads it once);
|
|
@@ -11259,6 +11767,18 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, c
|
|
|
11259
11767
|
if (!parse || !parse.triples?.length || parse.residue?.length) return null;
|
|
11260
11768
|
const { assertSentence } = await import("../domain/grammar/assert.mjs");
|
|
11261
11769
|
const { normFactTerm, appendFact } = await import("../adapters/memory/core.mjs");
|
|
11770
|
+
// A REFLEXIVE disjointness ("no dog is a dog") is a self-contradiction,
|
|
11771
|
+
// not a fact — the same refusal the teach lane's negative-universal frame
|
|
11772
|
+
// gives the out-of-lexicon spelling, so the two surfaces can't disagree.
|
|
11773
|
+
const reflexiveDisjoint = parse.triples.find(
|
|
11774
|
+
(t) => t.predicate === "owl:disjointWith" && normFactTerm(t.subject) === normFactTerm(t.object),
|
|
11775
|
+
);
|
|
11776
|
+
if (reflexiveDisjoint) {
|
|
11777
|
+
const term = normFactTerm(reflexiveDisjoint.subject);
|
|
11778
|
+
return plainTurn(line,
|
|
11779
|
+
`I can't store "no ${term} is a ${term}" — every ${term} is a ${term} by definition, so that exclusion contradicts itself. Nothing was stored.`,
|
|
11780
|
+
{ command: "assert", via: "teach-miss", miss: true, focus });
|
|
11781
|
+
}
|
|
11262
11782
|
const ts = new Date().toISOString();
|
|
11263
11783
|
const res = await assertSentence(memoryDir, line, {
|
|
11264
11784
|
lexicon: lex,
|
|
@@ -11426,7 +11946,7 @@ function morePage(query, { last, focus }) {
|
|
|
11426
11946
|
// the generic parse, which reads these phrasings as something else entirely.
|
|
11427
11947
|
// A term that mints no concept — unknown, or with no synonym/related facts —
|
|
11428
11948
|
// misses honestly, naming the term, never a guessed neighbour.
|
|
11429
|
-
const SKOS_SYNONYM_RE = /^(?:another\s+word\s+for|other\s+words\s+for|synonyms?\s+(?:of|for)|what\s+is\s+a\s+synonym\s+(?:of|for))\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i;
|
|
11949
|
+
const SKOS_SYNONYM_RE = /^(?:(?:what\s+is|whats)\s+another\s+word\s+for|(?:got\s+|are\s+there\s+)?any\s+(?:other\s+)?words?\s+like|(?:other\s+)?words\s+like|another\s+word\s+for|other\s+words\s+for|synonyms?\s+(?:of|for)|what\s+is\s+a\s+synonym\s+(?:of|for))\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i;
|
|
11430
11950
|
const SKOS_RELATED_RE = /^(?:what\s+is\s+related\s+to|what\s+relates\s+to|what\s+words\s+are\s+related\s+to)\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i;
|
|
11431
11951
|
|
|
11432
11952
|
/** The SKOS-view answer for a synonym/related question, or null when the
|
|
@@ -11493,6 +12013,12 @@ const GUESSER_OPEN_LEAD_RE = /^(?:i\s*(?:'m|am)\s+thinking\s+of\s+a\s+number|gue
|
|
|
11493
12013
|
const THINKER_OPEN_LEAD_RE = /^(?:think\s+of\s+a\s+number)\b(.*)$/i;
|
|
11494
12014
|
const GUESSER_OPEN_TAIL_RE = /^[\s,.!?—-]*(?:and\s+)?(?:you\s+)?(?:can\s+|have\s+to\s+|try\s+to\s+)?(?:guess(?:\s+it|\s+what\s+it\s+is)?)?[\s,.!?—-]*$/i;
|
|
11495
12015
|
const THINKER_OPEN_TAIL_RE = /^[\s,.!?—-]*(?:and\s+)?(?:i\s*(?:'ll|\s+will)\s+(?:try\s+to\s+)?guess(?:\s+it)?|i\s+guess)?[\s,.!?—-]*$/i;
|
|
12016
|
+
// The INVITATION family — "let's play guess the number", "wanna play a
|
|
12017
|
+
// guessing game?": an invitation names the game without saying who holds the
|
|
12018
|
+
// secret, and the canonical guess-the-number reading is that the inviter
|
|
12019
|
+
// GUESSES — so it opens thinker mode (tmct commits the secret).
|
|
12020
|
+
const INVITATION_OPEN_LEAD_RE = /^(?:let'?s\s+play|wanna\s+play|want\s+to\s+play|can\s+we\s+play|shall\s+we\s+play|do\s+you\s+want\s+to\s+play|will\s+you\s+play|play)\s+(?:a\s+)?(?:game\s+of\s+)?(?:guess[- ]the[- ]number|number[- ]guessing(?:\s+game)?|guessing\s+game)\b(.*)$/i;
|
|
12021
|
+
const INVITATION_OPEN_TAIL_RE = /^[\s,.!?—-]*(?:with\s+me|together)?[\s,.!?—-]*$/i;
|
|
11496
12022
|
|
|
11497
12023
|
/** An opening move — { mode, bounds } — or null. */
|
|
11498
12024
|
function matchGameOpening(line) {
|
|
@@ -11505,6 +12031,10 @@ function matchGameOpening(line) {
|
|
|
11505
12031
|
if (thinker && THINKER_OPEN_TAIL_RE.test(thinker[1].replace(GAME_BOUNDS_CLAUSE_RE, " "))) {
|
|
11506
12032
|
return { mode: "thinker", bounds: parseGameBounds(l) };
|
|
11507
12033
|
}
|
|
12034
|
+
const invite = l.match(INVITATION_OPEN_LEAD_RE);
|
|
12035
|
+
if (invite && INVITATION_OPEN_TAIL_RE.test(invite[1].replace(GAME_BOUNDS_CLAUSE_RE, " "))) {
|
|
12036
|
+
return { mode: "thinker", bounds: parseGameBounds(l) };
|
|
12037
|
+
}
|
|
11508
12038
|
return null;
|
|
11509
12039
|
}
|
|
11510
12040
|
|
|
@@ -11707,6 +12237,41 @@ function rewriteVocabOpener(line) {
|
|
|
11707
12237
|
return null;
|
|
11708
12238
|
}
|
|
11709
12239
|
|
|
12240
|
+
/** The ESL missing-"does" yes/no — "dog have tail?": subject + bare
|
|
12241
|
+
* have/has + object, question mark REQUIRED (the "?" is the whole signal;
|
|
12242
|
+
* without it the line is a declarative and belongs to the teach path).
|
|
12243
|
+
* Rewritten to the do-support form the possession readers already answer, so
|
|
12244
|
+
* the question is ANSWERED as the yes/no it is rather than merely refused at
|
|
12245
|
+
* the write boundary. Single/two-token sides, mirroring the teach shapes'
|
|
12246
|
+
* own subject width; articles tolerated on the object. */
|
|
12247
|
+
const ESL_MISSING_DOES_RE = /^([\w-]+(?:\s+[\w-]+)?)\s+(?:has|have)\s+(?:an?\s+|the\s+)?([\w-]+(?:\s+[\w-]+)?)\s*\?+$/i;
|
|
12248
|
+
function rewriteEslMissingDoes(line) {
|
|
12249
|
+
const m = String(line || "").trim().match(ESL_MISSING_DOES_RE);
|
|
12250
|
+
if (!m) return null;
|
|
12251
|
+
if (QUESTION_LEAD_RE.test(m[1])) return null; // already do-supported ("does dog have tail?")
|
|
12252
|
+
return `does ${m[1].trim()} have ${m[2].trim()}`;
|
|
12253
|
+
}
|
|
12254
|
+
|
|
12255
|
+
/** The NEGATIVE-POLARITY opener — "I don't suppose X imports anything": a
|
|
12256
|
+
* politeness implicature meaning the question underneath. The one wrapper
|
|
12257
|
+
* the desire/wrapper stripper family didn't peel; unpeeled it reads as a
|
|
12258
|
+
* first-person declarative and lands on the pronoun-subject lecture. The
|
|
12259
|
+
* anything-form folds straight to the open question; an interrogative-led
|
|
12260
|
+
* remainder unwraps to itself; anything else stays untouched (never a
|
|
12261
|
+
* guessed reading). */
|
|
12262
|
+
const NEG_POLARITY_OPENER_RE = /^i\s+(?:do\s+not|don'?t)\s+suppose\s+(?:that\s+)?(.+?)[?.!\s]*$/i;
|
|
12263
|
+
function rewriteNegativePolarityOpener(line) {
|
|
12264
|
+
const m = String(line || "").trim().match(NEG_POLARITY_OPENER_RE);
|
|
12265
|
+
if (!m) return null;
|
|
12266
|
+
const rest = m[1].trim();
|
|
12267
|
+
const anyForm = rest.match(/^(.+?)\s+([a-z]+)s\s+(?:anything|something)(?:\s+else)?$/i);
|
|
12268
|
+
if (anyForm && VERB_TO_KIND[`${anyForm[2].toLowerCase()}s`]) {
|
|
12269
|
+
return `what does ${anyForm[1].trim()} ${anyForm[2].toLowerCase()}`;
|
|
12270
|
+
}
|
|
12271
|
+
if (QUESTION_LEAD_RE.test(rest)) return rest;
|
|
12272
|
+
return null;
|
|
12273
|
+
}
|
|
12274
|
+
|
|
11710
12275
|
/** A DISCONTIGUOUS verb frame, "SUBJECT uses OBJECT as its/a base(class)" —
|
|
11711
12276
|
* "uses" is split from its own qualifier ("as its base") around the object,
|
|
11712
12277
|
* so no contiguous phrase-table entry could ever register it, and "uses"
|
|
@@ -11845,7 +12410,13 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
11845
12410
|
// The pronoun lead's own guard only spares the BARE "what is it", so this
|
|
11846
12411
|
// shape has to stop existing before that match runs at all.
|
|
11847
12412
|
const cleftRewrite = reverseCleftRewrite(frameLine);
|
|
11848
|
-
|
|
12413
|
+
// The ESL missing-"does" yes/no ("dog have tail?") — rewritten to the
|
|
12414
|
+
// do-support form here, once, before any dispatch lane sees it, so the
|
|
12415
|
+
// question is answered by the possession readers instead of walling (the
|
|
12416
|
+
// write boundary's own "?" gates already refuse to store it).
|
|
12417
|
+
const eslRewrite = rewriteEslMissingDoes(cleftRewrite || frameLine)
|
|
12418
|
+
|| rewriteNegativePolarityOpener(cleftRewrite || frameLine);
|
|
12419
|
+
const cleftLine = eslRewrite || cleftRewrite || frameLine;
|
|
11849
12420
|
// VOCABULARY pronoun antecedent — "what is a dog" then "can it bark". The
|
|
11850
12421
|
// code-graph focus mechanism only ever binds {id,label} GRAPH entities, so
|
|
11851
12422
|
// in a vocabulary conversation "it" resolved to nothing and the question
|
|
@@ -11883,10 +12454,16 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
11883
12454
|
// indirect-request wrapper stripped and/or the discontiguous-frame
|
|
11884
12455
|
// rewrite applied) — restore the ORIGINAL raw `line` into record.query
|
|
11885
12456
|
// and the logged transcript echo here, once, centrally.
|
|
11886
|
-
if (indirectMatch || baseFrameRewrite || vocabAntecedent) {
|
|
12457
|
+
if (indirectMatch || baseFrameRewrite || vocabAntecedent || eslRewrite) {
|
|
11887
12458
|
if (finished.record) finished.record.query = line;
|
|
11888
12459
|
if (Array.isArray(finished.logLines) && finished.logLines.length > 1) finished.logLines[1] = `> ${line}`;
|
|
11889
12460
|
}
|
|
12461
|
+
// The VERBATIM user line rides every turn record as `input`, beside
|
|
12462
|
+
// whatever `query` the dispatch path recorded — the session history must
|
|
12463
|
+
// be able to quote the user exactly, and bench session-mode matching
|
|
12464
|
+
// needs the pre-rewrite turn. Additive: `query` keeps its existing
|
|
12465
|
+
// fidelity rules unchanged.
|
|
12466
|
+
if (finished.record && finished.record.type === "turn") finished.record.input = line;
|
|
11890
12467
|
// runAsk's own effectiveQuery (set only when discourseRewrite substituted
|
|
11891
12468
|
// a new subject and produced a genuine non-miss answer) takes over as the
|
|
11892
12469
|
// continuation base for the NEXT turn's own discourseRewrite.
|
|
@@ -11943,7 +12520,10 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
11943
12520
|
// conversational turn is never finish()'d / never becomes a new `last`), so the
|
|
11944
12521
|
// narrate block is applied directly here instead.
|
|
11945
12522
|
const convo = vocabAntecedent ? null : conversationalTurn(workingLine, ctx);
|
|
11946
|
-
if (convo)
|
|
12523
|
+
if (convo) {
|
|
12524
|
+
if (convo.record && convo.record.type === "turn") convo.record.input = line; // verbatim, same as withLast
|
|
12525
|
+
return withNarration(convo, trace, "casual/social — no graph intent");
|
|
12526
|
+
}
|
|
11947
12527
|
|
|
11948
12528
|
// PLAN NEXT — "next"/"continue" with an ACTIVE plan executes the plan's
|
|
11949
12529
|
// next move as a snapshot write. Checked BEFORE the MORE_RE pager because
|
|
@@ -11969,7 +12549,10 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
11969
12549
|
// code-graph miss. Returns null with no plan/board standing, so nothing
|
|
11970
12550
|
// changes for a cold session — an honest miss still stands.
|
|
11971
12551
|
if (memoryDir) {
|
|
11972
|
-
const follow = await planFollowUpAnswer(workingLine, {
|
|
12552
|
+
const follow = await planFollowUpAnswer(workingLine, {
|
|
12553
|
+
memoryDir, planHolder,
|
|
12554
|
+
pendingPager: !!(Array.isArray(last?.detail?.pending?.items) && last.detail.pending.items.length),
|
|
12555
|
+
});
|
|
11973
12556
|
if (follow) {
|
|
11974
12557
|
note(trace, `goal: ${follow.deduced}`);
|
|
11975
12558
|
note(trace, `lane: ${follow.note}`);
|