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