@polycode-projects/the-mechanical-code-talker 1.0.8 → 1.2.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/README.md +51 -32
- package/ROADMAP.md +212 -92
- package/package.json +1 -1
- package/src/ask-vocab.mjs +125 -1
- package/src/ask.mjs +510 -17
- package/src/chat.mjs +1216 -78
- package/src/codegraph.mjs +1 -1
- package/src/interpret/normalize.mjs +137 -3
- package/src/interpret/strategies/grammar.mjs +44 -11
- package/src/interpret/strategies/keywords.mjs +15 -1
- package/src/memory/core.mjs +15 -1
- package/src/syllogise.mjs +0 -0
package/src/chat.mjs
CHANGED
|
@@ -52,7 +52,10 @@ import { createTelemetry } from "./telemetry.mjs";
|
|
|
52
52
|
import * as defaultSource from "./source.mjs";
|
|
53
53
|
import { loadTemplates, render as renderTemplate } from "./corpus/templates.mjs";
|
|
54
54
|
import { finish, beginsWithVowelSound, grammarRules } from "./finish.mjs";
|
|
55
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
VERB_TO_KIND, WHERE_MARKERS, MENTION_MARKERS, ENTITY_TO_TYPE, PASSIVE_PARTICIPLE_TO_KIND,
|
|
57
|
+
stripTrailingScopeFiller,
|
|
58
|
+
} from "./ask-vocab.mjs";
|
|
56
59
|
import { COUNTERFACTUAL_RE, correctMisspellings, applyPreambleFrames } from "./interpret/normalize.mjs";
|
|
57
60
|
import { fuzzyMatchInSet, fuzzyBound } from "./interpret/fuzzy.mjs";
|
|
58
61
|
|
|
@@ -228,8 +231,10 @@ function withNarration(result, trace, fallbackGoal) {
|
|
|
228
231
|
* own test suite pins composed answers with (see withGoalLine's own docblock,
|
|
229
232
|
* just below, for why appended rather than led-with).
|
|
230
233
|
*
|
|
231
|
-
* `result.goal` is set
|
|
232
|
-
* statement)
|
|
234
|
+
* `result.goal` is set by runAsk (see its own docblock at its return
|
|
235
|
+
* statement) AND by runCommand's own mk() (Bug F point 5 — GOAL_BY_COMMAND,
|
|
236
|
+
* below — generalizes the SAME mechanism to slash-command dispatches like
|
|
237
|
+
* /search and /describe); a plain count or a teach confirmation never
|
|
233
238
|
* carries the field, so this is a no-op for those turn types BY
|
|
234
239
|
* CONSTRUCTION, not a special-cased suppression list here. Also a no-op when
|
|
235
240
|
* `result.goal` is null/empty — deduceGoalFromParsed's own "nothing to
|
|
@@ -370,12 +375,23 @@ function looksLikePredicateFind(restTok) {
|
|
|
370
375
|
export function asBareCommand(line) {
|
|
371
376
|
const trimmed = String(line || "").trim();
|
|
372
377
|
if (!trimmed || trimmed.startsWith("/")) return null;
|
|
373
|
-
const [first, ...
|
|
378
|
+
const [first, ...restTokRaw] = trimmed.split(/\s+/);
|
|
374
379
|
if (!COMMAND_WORDS.has(first.toLowerCase())) return null;
|
|
375
380
|
const fl = first.toLowerCase();
|
|
381
|
+
// Bug F point 2 (operator follow-up request): "search for X" (bare, via this
|
|
382
|
+
// function) used to route with "for X" as the literal command argument — "for"
|
|
383
|
+
// is filler, not part of the search term, and burning one of the 3 allowed
|
|
384
|
+
// tokens on it could wrongly reject an otherwise-short query as "too long"
|
|
385
|
+
// ("search for the payment controller" is 4 tokens WITH "for", 3 without).
|
|
386
|
+
// Strip a leading "for " before it becomes the argument, and rebuild the
|
|
387
|
+
// returned command line from the STRIPPED remainder — the token-count check
|
|
388
|
+
// below already reads the stripped `restTok`/`rest`.
|
|
389
|
+
const stripped = (fl === "search" || fl === "find") && restTokRaw[0]?.toLowerCase() === "for";
|
|
390
|
+
const restTok = stripped ? restTokRaw.slice(1) : restTokRaw;
|
|
376
391
|
const rest = restTok.join(" ");
|
|
392
|
+
const effectiveLine = stripped ? `${fl}${rest ? ` ${rest}` : ""}` : trimmed;
|
|
377
393
|
// Zero-arg system commands are always the command; a bare command word is too.
|
|
378
|
-
if (!rest || fl === "stats" || fl === "memory") return `/${
|
|
394
|
+
if (!rest || fl === "stats" || fl === "memory") return `/${effectiveLine}`;
|
|
379
395
|
// "find" (only — "search", its /find-tool alias, keeps its original behavior
|
|
380
396
|
// unconditionally): the predicate-find grammar's own shape wins regardless of
|
|
381
397
|
// word count, see the precedence note above.
|
|
@@ -398,7 +414,7 @@ export function asBareCommand(line) {
|
|
|
398
414
|
if (COMMANDS[fl]?.arg == null && rest) return null;
|
|
399
415
|
// Arg commands: route only a short, name-like argument (no query connectives),
|
|
400
416
|
// so "describe Widget" / "members my class" route but a compositional query does not.
|
|
401
|
-
if (restTok.length <= 3 && !QUERY_CONNECTIVES.test(rest)) return `/${
|
|
417
|
+
if (restTok.length <= 3 && !QUERY_CONNECTIVES.test(rest)) return `/${effectiveLine}`;
|
|
402
418
|
return null;
|
|
403
419
|
}
|
|
404
420
|
|
|
@@ -525,7 +541,18 @@ export function answerCount(graph, query) {
|
|
|
525
541
|
const cls = COUNT_NOUNS[noun];
|
|
526
542
|
if (cls && RESTRICTOR_VERB_RE.test(String(query).slice(m.index + m[0].length))) return null;
|
|
527
543
|
if (!cls) {
|
|
528
|
-
|
|
544
|
+
const kinds = countableKinds(graph);
|
|
545
|
+
// Bug C (operator manual-chat find, this session): when NO code graph is
|
|
546
|
+
// loaded, countableKinds(graph) is genuinely EMPTY (no class is present at
|
|
547
|
+
// all) — the old message rendered "I count: ." (a dangling empty list
|
|
548
|
+
// before the period) and then pointlessly suggested "how many classes are
|
|
549
|
+
// there", which would ALSO fail for the same reason. An honest, non-dangling
|
|
550
|
+
// message instead, pointing at how to actually get a graph loaded.
|
|
551
|
+
if (!kinds.length) {
|
|
552
|
+
return `I can't count "${noun}" — no code graph is loaded yet, so there's nothing to count ` +
|
|
553
|
+
`(point me at one with --repo, or run "npm run example:mini").`;
|
|
554
|
+
}
|
|
555
|
+
return `I can't count "${noun}". I count: ${kinds.join(", ")}. ` +
|
|
529
556
|
`Try "how many classes are there".`;
|
|
530
557
|
}
|
|
531
558
|
const n = countClass(graph, cls);
|
|
@@ -673,6 +700,39 @@ const CAPABILITY_PHRASES = [
|
|
|
673
700
|
// today (bin/tmct.mjs), dead once inside the chat loop; route to the same
|
|
674
701
|
// capability answer a plain "help" gets.
|
|
675
702
|
/^--help$/i, /^-h$/i, /^man( tmct)?\??$/i,
|
|
703
|
+
// Tier 6 playtest ("the messy real user", §3): the vague-opener family a
|
|
704
|
+
// stranger genuinely asks before knowing any query shapes — "what can you
|
|
705
|
+
// tell me about this repo", "tell me something interesting (about this
|
|
706
|
+
// codebase)", "so, what is going on in this codebase" (an optional leading
|
|
707
|
+
// "so," discourse connective, same species as LEADING_CONNECTIVE_RE
|
|
708
|
+
// elsewhere). All three used to fall straight to the raw grammar wall
|
|
709
|
+
// (isConversational's own ≤3-word/no-codeish catch-all never claims an
|
|
710
|
+
// 8-word sentence like these) even though orientationAnswer is EXACTLY the
|
|
711
|
+
// right answer — the same overview CAPABILITY_PHRASES' other entries already
|
|
712
|
+
// reach. The noun set mirrors META_ORIENT_RE's own closed list.
|
|
713
|
+
/^what can (?:you|u) tell me(?:\s+(?:more|anything))?\s+about (?:this|the)\s+(?:app|codebase|repo|repository|project|code|thing)\??$/i,
|
|
714
|
+
/^tell me something interesting(?:\s+about (?:this|the)\s+(?:app|codebase|repo|repository|project|code))?\??$/i,
|
|
715
|
+
/^(?:so,?\s+)?what(?:'s|s|\s+is)\s+(?:going on|happening)\s+(?:in|with)\s+(?:this|the)\s+(?:app|codebase|repo|repository|project|code)\??$/i,
|
|
716
|
+
// Tier 6 playtest cycle 2: three more vague-opener idioms found live, same
|
|
717
|
+
// family as the three just above — a stranger's orientation request has no
|
|
718
|
+
// fixed wording, so this closed set keeps growing additively as new natural
|
|
719
|
+
// phrasings surface, never a general "any long question is an orientation
|
|
720
|
+
// request" rule.
|
|
721
|
+
/^(?:can you\s+)?walk me through (?:this|the)\s+(?:app|codebase|repo|repository|project|code)\??$/i,
|
|
722
|
+
/^what(?:'s|s|\s+is) the big picture(?:\s+here)?\??$/i,
|
|
723
|
+
/^(?:give me|what's) the lay of the land\??$/i,
|
|
724
|
+
// Tier 6 playtest cycle 3: "what have we got here"/"what've we got here" —
|
|
725
|
+
// a casual, self-answering opener (found after a leading "so" strips via
|
|
726
|
+
// LEADING_CONNECTIVE_RE, leaving this as the bare remainder).
|
|
727
|
+
/^what(?:'ve| have) (?:we|i) got here\??$/i,
|
|
728
|
+
// "what's in this repo/codebase" — arguably the MOST natural vague opener of
|
|
729
|
+
// this whole family, and genuinely ambiguous with the real "what's in <X>"
|
|
730
|
+
// members/containment grammar (ask.mjs) for any OTHER term — closed to
|
|
731
|
+
// exactly the same self-referential noun set META_ORIENT_RE already uses,
|
|
732
|
+
// so a real module/class named literally "repo"/"codebase" is never at risk
|
|
733
|
+
// (this repo's own fixture has none, and the noun list itself excludes
|
|
734
|
+
// ordinary code-ish names).
|
|
735
|
+
/^what(?:'s|s|\s+is) in (?:this|the)\s+(?:app|codebase|repo|repository|project|code)\??$/i,
|
|
676
736
|
];
|
|
677
737
|
/** IDENTITY questions — "who/what are you", by name, in plain or ESL-ish phrasing.
|
|
678
738
|
* Routed to a self-description (identity-self) that works regardless of graph
|
|
@@ -831,6 +891,11 @@ const THANKS = new Set([
|
|
|
831
891
|
"thanks", "thank you", "thankyou", "thx", "ty", "ta", "cheers", "nice one",
|
|
832
892
|
"much appreciated", "cool thanks", "many thanks", "much obliged", "ta very much",
|
|
833
893
|
"cheers mate", "cheers for that", "tks", "sweet thanks", "nice",
|
|
894
|
+
// "ta for that" (Tier 6 playtest): "cheers for that" was already here, but
|
|
895
|
+
// its "ta" sibling (both dropped-word forms of the SAME "thanks for that"
|
|
896
|
+
// shape) was missing — fell to the generic orientation card via
|
|
897
|
+
// isConversational's ≤3-word catch-all instead of a thanks reply.
|
|
898
|
+
"ta for that",
|
|
834
899
|
]);
|
|
835
900
|
/** Farewells → a goodbye AND a clean end of session (same path as /exit). */
|
|
836
901
|
const BYE = new Set([
|
|
@@ -1034,7 +1099,16 @@ function conversationalTurn(line, ctx) {
|
|
|
1034
1099
|
note(ctx.trace, "lane: conversational — identity (IDENTITY_PHRASES closed set)");
|
|
1035
1100
|
return mk(t(T_IDENTITY_SELF));
|
|
1036
1101
|
}
|
|
1037
|
-
|
|
1102
|
+
// Tier 6 playtest cycle 2: CAPABILITY_PHRASES' vague-opener entries are
|
|
1103
|
+
// self-contained closed regexes, but a preamble ahead of one ("right, can
|
|
1104
|
+
// you walk me through this codebase" — an ACK_PREAMBLE_RE + MODAL_WRAPPER_RE
|
|
1105
|
+
// stack) is tested nowhere upstream of this check, unlike vagueTouchTermOf/
|
|
1106
|
+
// describeWrapperAnswer (both run applyPreambleFrames first). Trying the
|
|
1107
|
+
// SAME closed set again against the preamble-stripped text is purely
|
|
1108
|
+
// additive — it can only ever ADD a match CAPABILITY_PHRASES.test(raw)
|
|
1109
|
+
// alone would have missed, never take one away.
|
|
1110
|
+
if (q === "help" || q === "?" || CAPABILITY_PHRASES.some((re) => re.test(raw))
|
|
1111
|
+
|| CAPABILITY_PHRASES.some((re) => re.test(applyPreambleFrames(raw))) || ORIENT_OPENERS.has(q)) {
|
|
1038
1112
|
note(ctx.trace, "goal: get oriented — what can tmct answer, how do I start");
|
|
1039
1113
|
note(ctx.trace, "lane: conversational — help/orientation (CAPABILITY_PHRASES/ORIENT_OPENERS / bare help / ?)");
|
|
1040
1114
|
return mk(orientationAnswer(ctx.templates, ctx.graph, ctx.vocabHint));
|
|
@@ -1251,7 +1325,13 @@ const WALL_MISS_ANYWHERE_RE = /couldn't parse this as a graph question\. Try:/;
|
|
|
1251
1325
|
// silently swallowed);
|
|
1252
1326
|
// - "<Name> owns/maintains <X>" (bare declarative or wrapped) → an
|
|
1253
1327
|
// mgx:ownedBy fact, read back by "who owns <X>" (factReadBack).
|
|
1254
|
-
|
|
1328
|
+
// Bug F point 1 (operator follow-up request, this session): "I want you to
|
|
1329
|
+
// remember X"/"I'd like you to remember X" teaches exactly like bare "remember
|
|
1330
|
+
// X" — a closed-set optional lead-in before the existing verb list, so it
|
|
1331
|
+
// automatically inherits the correct "teach/remember a new fact" goal line for
|
|
1332
|
+
// free (it flows through the SAME teach-lane goal revision, chat.mjs's runTurn
|
|
1333
|
+
// cascade — no extra wiring needed for this phrasing).
|
|
1334
|
+
const TEACH_RE = /^(?:please\s+)?(?:i\s+(?:want|wanted)\s+you\s+to\s+|i(?:'d|\s+would)\s+like\s+you\s+to\s+)?(?:remember|note|keep in mind|jot down|for the record|fyi)\b[:,]?\s*(?:that\s+)?(.+?)[.?!]*$/i;
|
|
1255
1335
|
const BARE_DECLARATIVE_RE = /^(?:every |each |all |a |an )?[\w-]+ (?:is|are) (?:a |an )?[\w-]+$/i;
|
|
1256
1336
|
/** Interrogative / auxiliary leads that make an "X is a Y"-shaped line a QUESTION
|
|
1257
1337
|
* ("what is a cache", "is a module a component"), never a teach declarative. */
|
|
@@ -1274,16 +1354,46 @@ const SUBCLASS_PREDICATE = "rdfs:subClassOf";
|
|
|
1274
1354
|
const HAS_A_PREDICATE = "mgx:hasA";
|
|
1275
1355
|
|
|
1276
1356
|
/** "<Name> owns/maintains <X>" — the ownership teach declarative. <Name> is one
|
|
1277
|
-
* or two name tokens
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1280
|
-
|
|
1357
|
+
* or two name tokens; <X> is a code-ish token (a path, a file, a symbol) OR a
|
|
1358
|
+
* short natural noun phrase ("the tasks handler") — widened from a
|
|
1359
|
+
* single-token-only object (Tier-5 playtest fix, found live: "remember that
|
|
1360
|
+
* margo maintains the tasks handler" WALLED entirely, because the object
|
|
1361
|
+
* didn't fit ONE bare token and generalVerbTeach explicitly stands down for
|
|
1362
|
+
* "owns"/"maintains" anywhere in the sentence, deferring to this frame — so
|
|
1363
|
+
* neither recognizer ever stored the fact). The article-stripping needed so
|
|
1364
|
+
* "the tasks handler" reads back the same as a bare "tasks handler" is
|
|
1365
|
+
* handled once, centrally, by normFactTerm (memory/core.mjs) — teachFact
|
|
1366
|
+
* already normalizes both subject and object through it. The BARE form
|
|
1367
|
+
* additionally requires a Capitalized name (see teachLane), so ordinary
|
|
1368
|
+
* lowercase prose never lands a fact without the explicit wrapper. */
|
|
1369
|
+
const OWNS_TEACH_RE = /^([A-Za-z][\w'-]*(?:\s+[A-Z][\w'-]*)?)\s+(?:owns|maintains)\s+(.+?)[.!?]*$/;
|
|
1370
|
+
/** "<X> is owned by <Name>" — the PASSIVE ownership teach declarative
|
|
1371
|
+
* (Tier-5 playtest fix, cycle 2, found live): at least as natural a way to
|
|
1372
|
+
* state ownership as the active "<Name> owns <X>" above ("TaskController is
|
|
1373
|
+
* owned by sam" WALLED entirely — "is" put it in generalVerbTeach's own
|
|
1374
|
+
* GENERAL_VERB_ANYWHERE_EXCLUDE_RE stand-down territory, but no frame in
|
|
1375
|
+
* this lane actually recognized the passive shape, so nothing ever claimed
|
|
1376
|
+
* it). <X> (the owned thing) is a lazy multi-word capture, same discipline
|
|
1377
|
+
* OWNS_TEACH_RE's own object got widened to; <Name> (the owner) mirrors
|
|
1378
|
+
* OWNS_TEACH_RE's own 1-2-token name capture. Stores the SAME
|
|
1379
|
+
* OWNED_BY_PREDICATE shape (subject=thing, object=owner), so "who owns X" /
|
|
1380
|
+
* the yes/no readers below answer either phrasing identically. */
|
|
1381
|
+
const OWNS_PASSIVE_TEACH_RE = /^(.+?)\s+(?:is|are|was|were)\s+owned\s+by\s+([A-Za-z][\w'-]*(?:\s+[A-Z][\w'-]*)?)[.!?]*$/i;
|
|
1281
1382
|
|
|
1282
1383
|
/** "<X> is <adjective>" — the property teach payload (wrapper-REQUIRED): a lazy
|
|
1283
1384
|
* subject and a single bare complement word. Never matches the "is a <noun>"
|
|
1284
1385
|
* membership shape (that stays the ACE grammar's), so "remember that cache is
|
|
1285
|
-
* a store" still lands as rdfs:subClassOf, not a property.
|
|
1286
|
-
|
|
1386
|
+
* a store" still lands as rdfs:subClassOf, not a property. "was"/"were" join
|
|
1387
|
+
* "is"/"are" (Tier-5 playtest fix, cycle 3, sibling of Bug A's had->have
|
|
1388
|
+
* bridge for general-verb facts): "remember that the last commit was risky"
|
|
1389
|
+
* reads back as a present-tense property fact ("...is risky") the same way a
|
|
1390
|
+
* general-verb "had" fact already reads back as "has" — properties are
|
|
1391
|
+
* timeless facts in this store, not tensed events. Safe to widen here
|
|
1392
|
+
* (unlike the entry gates further up that decide whether `payload` even
|
|
1393
|
+
* reaches this match at all): this path only runs on an explicit
|
|
1394
|
+
* "remember/note/…"-WRAPPED sentence, never a bare one, so there's no real
|
|
1395
|
+
* question-shape ("was X Y?") this could ever misfire on. */
|
|
1396
|
+
const TEACH_PROPERTY_RE = /^(?:every\s+|each\s+|all\s+|the\s+)?(.+?)\s+(?:is|are|was|were)\s+(?!an?\b|the\b)([A-Za-z][\w-]*)$/i;
|
|
1287
1397
|
|
|
1288
1398
|
/** The teach lane's provenance tag — mirrors grammar/assert.mjs's provenanceTag
|
|
1289
1399
|
* shape under a distinct "teach:" family, so a taught fact is auditable apart
|
|
@@ -1351,10 +1461,124 @@ const SOME_A_FEW_RE = /^(some|a few)\s+([\w-]+)\s+are\s+([\w-]+)$/i;
|
|
|
1351
1461
|
* apart from a singular/specific-entity "a"/bare reading (only "every" gets a
|
|
1352
1462
|
* recorded quantifier here — this function's OWN caller passes it through to
|
|
1353
1463
|
* teachFact; assertTurn, below, records the same "every" quantifier
|
|
1354
|
-
* independently for the pre-existing ACE-success path).
|
|
1355
|
-
*
|
|
1356
|
-
*
|
|
1357
|
-
|
|
1464
|
+
* independently for the pre-existing ACE-success path). Y (the object) is a
|
|
1465
|
+
* single token, same as parseAce's own copula fragments; X (the subject) is
|
|
1466
|
+
* ONE OR TWO tokens (Tier-5 playtest fix: "vulcan gizmo is a tool"/"remember
|
|
1467
|
+
* vulcan gizmo is a tool" fell straight to a "teach me" nudge that offered
|
|
1468
|
+
* THIS EXACT phrasing as the fix, then itself failed when tried — a
|
|
1469
|
+
* single-token-only subject was too narrow for a natural 2-word noun phrase,
|
|
1470
|
+
* the same class of gap OWNS_TEACH_RE's own object had before its own
|
|
1471
|
+
* Tier-5 widening, above). The greedy quantifier tries the longer 2-word
|
|
1472
|
+
* subject first, backtracking to 1 word only if the tail doesn't then start
|
|
1473
|
+
* with is/are — the "is/are" anchor immediately after the subject removes
|
|
1474
|
+
* the ambiguity a fully free-form multi-word subject would otherwise have. */
|
|
1475
|
+
const UNKNOWN_SUBJECT_RE = /^(every\s+|each\s+|all\s+|a\s+|an\s+)?([\w-]+(?:\s+[\w-]+)?)\s+(?:is|are)\s+(?:an?\s+)?([\w-]+)$/i;
|
|
1476
|
+
|
|
1477
|
+
/** ISA-family predicates (mirrors the private ISA_PREDICATES set defined near
|
|
1478
|
+
* memoryFacts, below, at module scope — both are simple top-level consts
|
|
1479
|
+
* evaluated once at load time, so referencing either from a function defined
|
|
1480
|
+
* earlier in this file is safe: no function here actually RUNS until well
|
|
1481
|
+
* after the whole module has finished loading). Named again here, right by
|
|
1482
|
+
* its one caller, so isGroundedByFact reads standalone. */
|
|
1483
|
+
const MINT_ISA_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
|
|
1484
|
+
|
|
1485
|
+
/** Small CLOSED set of generic English root nouns that count as always-
|
|
1486
|
+
* grounded anchor terms for the mint-fallbacks below (operator refinement,
|
|
1487
|
+
* 2026-07-09) — deliberately NOT added to lexicon-core.json itself (that
|
|
1488
|
+
* file stays the curated ~180-word CODE vocabulary; these are ordinary-
|
|
1489
|
+
* English root nouns with no code meaning at all, confirmed absent from it
|
|
1490
|
+
* today). Their only job is to give a user who hits the "both sides
|
|
1491
|
+
* ungrounded" decline (groundingSuggestionMiss, below) an honest, guessable
|
|
1492
|
+
* way in: ground one brand-new term via one of THESE words first ("every
|
|
1493
|
+
* zorp is a thing"), then chain the other new term off the now-grounded one. */
|
|
1494
|
+
const GENERIC_ANCHOR_NOUNS = new Set(["thing", "concept", "object", "entity"]);
|
|
1495
|
+
|
|
1496
|
+
/** Shared fact-groundedness primitive (Feature A mint-extension, point 2):
|
|
1497
|
+
* true when `term` already appears as the SUBJECT or OBJECT of a previously
|
|
1498
|
+
* taught isa-family fact (rdfs:subClassOf/rdf:type) in memory. A term minted
|
|
1499
|
+
* by EITHER mint-fallback below (this session, or an earlier one — this
|
|
1500
|
+
* reads persisted memory, not session-scoped state) is exactly as legitimate
|
|
1501
|
+
* an anchor for a NEW fact as a static lexicon-core.json word — the whole
|
|
1502
|
+
* point of this extension is letting new vocabulary compound turn over turn
|
|
1503
|
+
* ("every cache is a store" mints "store"; "every store is a container" then
|
|
1504
|
+
* needs "store" to read as known even though it's not in the static lexicon
|
|
1505
|
+
* at all). Read-only, reuses memoryFacts' plain read path (existence only —
|
|
1506
|
+
* no trust-ranking needed here) and normFactTerm's own normalization, so a
|
|
1507
|
+
* fact-grounded term matches under the EXACT spelling teachFact itself stored
|
|
1508
|
+
* it under. Failure-tolerated: no memory dir / no match → false, never a
|
|
1509
|
+
* guessed "yes". */
|
|
1510
|
+
async function isGroundedByFact(term, memoryDir) {
|
|
1511
|
+
if (!memoryDir) return false;
|
|
1512
|
+
const raw = String(term ?? "").trim();
|
|
1513
|
+
if (!raw) return false;
|
|
1514
|
+
const { normFactTerm } = await import("./memory/core.mjs");
|
|
1515
|
+
const t = normFactTerm(raw);
|
|
1516
|
+
if (!t) return false;
|
|
1517
|
+
// TAUGHT-only (same discipline factReadBack's own cax-sco/scm-sco proof
|
|
1518
|
+
// chase already uses, above, for the identical reason: the bulk background
|
|
1519
|
+
// corpus band (ConceptNet, trust 0.7, seeded by the thousands on a fresh
|
|
1520
|
+
// repo) mentions ordinary English words like "store"/"container" constantly
|
|
1521
|
+
// — treating THOSE as "grounded" would silently reopen the general lexicon
|
|
1522
|
+
// bypass this whole feature is deliberately narrow to avoid. Only what the
|
|
1523
|
+
// OPERATOR actually taught (or a prior `tmct syllogise` entailment) anchors
|
|
1524
|
+
// a term here. factRows (not memoryFacts) is used specifically because it's
|
|
1525
|
+
// the one read path that carries sourceTypes for this filter.
|
|
1526
|
+
const rows = await factRows(memoryDir);
|
|
1527
|
+
const isTaught = (f) => !f.sourceTypes?.includes("corpus") && !f.sourceTypes?.includes("web");
|
|
1528
|
+
return rows.some((f) => MINT_ISA_PREDICATES.has(f.predicate) && isTaught(f) && (f.subject === t || f.object === t));
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/** Shared "is this term grounded in ANY sense" aggregate (Feature A mint-
|
|
1532
|
+
* extension, point 2's named shared helper) — a static lexicon word (any
|
|
1533
|
+
* part of speech, via `classify`), a GENERIC_ANCHOR_NOUNS root, OR a term
|
|
1534
|
+
* already anchored by a previously taught isa-family fact (isGroundedByFact,
|
|
1535
|
+
* above). Used by unknownObjectFallback's subject/object groundedness checks
|
|
1536
|
+
* below, where no part-of-speech branching follows — just "known or not".
|
|
1537
|
+
* (unknownSubjectFallback's own object-known check, above/below, stays
|
|
1538
|
+
* narrower and NOUN-specific — see its own comment — so an object that's
|
|
1539
|
+
* merely a known ADJECTIVE doesn't get misrouted into the class/subClassOf
|
|
1540
|
+
* branch instead of the property branch.) */
|
|
1541
|
+
async function isGroundedTerm(term, lex, memoryDir) {
|
|
1542
|
+
const raw = String(term ?? "").trim();
|
|
1543
|
+
if (!raw) return false;
|
|
1544
|
+
if (GENERIC_ANCHOR_NOUNS.has(raw.toLowerCase())) return true;
|
|
1545
|
+
const { classify } = await import("./grammar/lexicon.mjs");
|
|
1546
|
+
if (classify(raw, lex)) return true;
|
|
1547
|
+
return isGroundedByFact(raw, memoryDir);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/** The "both sides ungrounded" grounding NUDGE (operator refinement,
|
|
1551
|
+
* 2026-07-09): reuses teachSuggestion's own "compute a hint, APPEND it to
|
|
1552
|
+
* the existing honest-miss message, never replace/silently guess" pattern
|
|
1553
|
+
* (see its docblock, above, and the "did"/"why" append-style construction in
|
|
1554
|
+
* teachLane's own final decline, below) for a DIFFERENT decline case —
|
|
1555
|
+
* rather than mint a relationship between two brand-new terms (a real
|
|
1556
|
+
* fabrication risk, unknownObjectFallback's own explicit safety guard,
|
|
1557
|
+
* below), teachLane's final honest-miss text gets an EXTRA appended nudge
|
|
1558
|
+
* whenever the declined payload fit the "X is/are Y" shape
|
|
1559
|
+
* (UNKNOWN_SUBJECT_RE) but NEITHER side is grounded — an honest, actionable
|
|
1560
|
+
* way in: ground one side via a GENERIC_ANCHOR_NOUNS root first ("every zorp
|
|
1561
|
+
* is a thing"), then chain the other off the now-grounded term. Deliberately
|
|
1562
|
+
* APPENDED rather than a replacement/short-circuit: a "both sides
|
|
1563
|
+
* ungrounded" is/are sentence with a KNOWN subject on one side (e.g. "module
|
|
1564
|
+
* is banana") never reaches this at all (isGroundedTerm(subject) is true, so
|
|
1565
|
+
* the very first return below fires) — that stays unknownObjectFallback's
|
|
1566
|
+
* own mint territory, entirely unaffected here. Returns "" (message
|
|
1567
|
+
* unchanged) whenever the payload doesn't fit the shape, or at least one
|
|
1568
|
+
* side IS already grounded — a DIFFERENT, more specific reason it declined,
|
|
1569
|
+
* where this nudge would be actively unhelpful noise. */
|
|
1570
|
+
async function ungroundedPairHint(payload, lexicon, memoryDir) {
|
|
1571
|
+
if (!memoryDir) return "";
|
|
1572
|
+
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
1573
|
+
if (!m) return "";
|
|
1574
|
+
const [, , subjectRaw, objectRaw] = m;
|
|
1575
|
+
const { loadLexicon } = await import("./grammar/lexicon.mjs");
|
|
1576
|
+
const lex = lexicon || loadLexicon();
|
|
1577
|
+
if (await isGroundedTerm(subjectRaw, lex, memoryDir)) return "";
|
|
1578
|
+
if (await isGroundedTerm(objectRaw, lex, memoryDir)) return "";
|
|
1579
|
+
return ` I don't know "${subjectRaw}" or "${objectRaw}" yet. Try grounding one first, e.g. `
|
|
1580
|
+
+ `"every ${subjectRaw} is a thing", then "every ${objectRaw} is a ${subjectRaw}".`;
|
|
1581
|
+
}
|
|
1358
1582
|
|
|
1359
1583
|
/** The unknown-SUBJECT direct-write fallback (point 1 + point 2's bare-property
|
|
1360
1584
|
* extension): tried ONLY after the real ACE grammar (assertTurn) has already
|
|
@@ -1367,18 +1591,25 @@ const UNKNOWN_SUBJECT_RE = /^(every\s+|each\s+|all\s+|a\s+|an\s+)?([\w-]+)\s+(?:
|
|
|
1367
1591
|
* - X is actually a KNOWN lexicon word — then the ACE grammar's own miss was
|
|
1368
1592
|
* a real structural/vocabulary problem elsewhere (e.g. Y itself unknown as
|
|
1369
1593
|
* the WRONG part of speech), never silently reinterpreted through this
|
|
1370
|
-
* narrow exception
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
1380
|
-
*
|
|
1381
|
-
*
|
|
1594
|
+
* narrow exception. (NOTE: this stays a STATIC-lexicon-only check,
|
|
1595
|
+
* deliberately not widened to isGroundedTerm — a subject that's grounded
|
|
1596
|
+
* only via a PRIOR taught fact, not the static lexicon, is precisely the
|
|
1597
|
+
* case unknownObjectFallback, below, owns instead.)
|
|
1598
|
+
* - Y resolves as NEITHER a known noun NOR a known adjective NOR a term
|
|
1599
|
+
* already grounded by a prior taught fact / a GENERIC_ANCHOR_NOUNS root
|
|
1600
|
+
* (Feature A mint-extension, point 2 — a term minted by
|
|
1601
|
+
* unknownObjectFallback, below, reads exactly as known here as any
|
|
1602
|
+
* lexicon word) — the OBJECT must still be a term tmct actually knows;
|
|
1603
|
+
* an unknown Y stays an honest miss (never a guess), exactly like the
|
|
1604
|
+
* pre-existing "monkey is an animal" case.
|
|
1605
|
+
* Y resolving as a NOUN (or fact-/anchor-grounded) writes rdfs:subClassOf
|
|
1606
|
+
* (mirrors the ACE grammar's own subClassOf/typeAssertion pattern); Y
|
|
1607
|
+
* resolving as an ADJECTIVE (and not also a noun) writes mgx:hasProperty
|
|
1608
|
+
* (mirrors the wrapped "remember that X is deprecated" property frame —
|
|
1609
|
+
* reused here for the bare/unwrapped form too, since the free pass is about
|
|
1610
|
+
* the SUBJECT, not about the "remember that" wrapper). Only the "every"
|
|
1611
|
+
* determiner records a quantifier (point 3: "a"/bare/"your" read as one
|
|
1612
|
+
* specific entity, not a class-level generalization). */
|
|
1382
1613
|
async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }) {
|
|
1383
1614
|
if (!memoryDir) return null;
|
|
1384
1615
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
@@ -1389,7 +1620,12 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }
|
|
|
1389
1620
|
// A known X's own ACE miss is a real miss — never silently reinterpreted here.
|
|
1390
1621
|
if (classify(subjectRaw, lex)) return null;
|
|
1391
1622
|
const quantifier = /^every$/i.test((det || "").trim()) ? "every" : "";
|
|
1392
|
-
|
|
1623
|
+
// Point 2 (mint-extension): a PRIOR turn's minted term, or a
|
|
1624
|
+
// GENERIC_ANCHOR_NOUNS root, grounds Y just as legitimately as a static
|
|
1625
|
+
// lexicon noun — both are always treated as class-level (never property),
|
|
1626
|
+
// consistent with unknownObjectFallback (below) always minting a CLASS.
|
|
1627
|
+
if (lookupNoun(lex, objectRaw) || GENERIC_ANCHOR_NOUNS.has(String(objectRaw).toLowerCase())
|
|
1628
|
+
|| (await isGroundedByFact(objectRaw, memoryDir))) {
|
|
1393
1629
|
return teachFact(memoryDir, sessionId, {
|
|
1394
1630
|
subject: subjectRaw, predicate: SUBCLASS_PREDICATE, object: objectRaw, quantifier,
|
|
1395
1631
|
});
|
|
@@ -1404,6 +1640,67 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }
|
|
|
1404
1640
|
return null; // Y unknown too — decline honestly, never guess
|
|
1405
1641
|
}
|
|
1406
1642
|
|
|
1643
|
+
/** The unknown-OBJECT mint fallback (Feature A, 2026-07-09 operator-authorized
|
|
1644
|
+
* vocabulary-growth extension): the MIRROR of unknownSubjectFallback, above —
|
|
1645
|
+
* same "X is/are Y" payload shape (UNKNOWN_SUBJECT_RE, reused verbatim,
|
|
1646
|
+
* never a second regex for the identical shape), tried as a SIBLING call
|
|
1647
|
+
* right after unknownSubjectFallback in teachLane (below), but firing on the
|
|
1648
|
+
* OPPOSITE asymmetry: SUBJECT already grounded (a real lexicon-core.json
|
|
1649
|
+
* word of ANY part of speech, a GENERIC_ANCHOR_NOUNS root, OR a term a PRIOR
|
|
1650
|
+
* turn already minted via either fallback — isGroundedTerm, shared with this
|
|
1651
|
+
* check) and OBJECT completely ungrounded. Mints the object as a new
|
|
1652
|
+
* class-level concept (rdfs:subClassOf, same predicate/quantifier machinery
|
|
1653
|
+
* teachFact/unknownSubjectFallback already use) so ordinary conversation can
|
|
1654
|
+
* build up new vocabulary turn over turn: "every cache is a store" (subject
|
|
1655
|
+
* "cache" grounded via the static lexicon) mints "store"; a LATER "every
|
|
1656
|
+
* store is a container" then finds "store" grounded via the fact just
|
|
1657
|
+
* minted (not the static lexicon at all) and mints "container" the same way.
|
|
1658
|
+
*
|
|
1659
|
+
* GATED ON A GENUINE UNIVERSAL QUANTIFIER ("every"/"each"/"all" — never bare/
|
|
1660
|
+
* "a"/"an"/"your"): minting a NEW CLASS-LEVEL CONCEPT is inherently a general
|
|
1661
|
+
* claim about a class, the same "every"/bare distinction unknownSubjectFallback's
|
|
1662
|
+
* own docblock already draws (point 3) between a class generalization and a
|
|
1663
|
+
* claim about ONE specific entity. This is load-bearing, not cosmetic: a bare
|
|
1664
|
+
* "module is banana" (a KNOWN lexicon subject, an unrecognized bare object,
|
|
1665
|
+
* NO determiner at all) is a pinned regression — it must stay a plain honest
|
|
1666
|
+
* miss, never silently minted — and a WRAPPED "remember that X is <adjective>"
|
|
1667
|
+
* (also determiner-less at the subject) must keep falling through to
|
|
1668
|
+
* TEACH_PROPERTY_RE's own, more permissive arbitrary-adjective path
|
|
1669
|
+
* unimpeded. Requiring the determiner keeps this fallback's mint exactly as
|
|
1670
|
+
* narrow as the vocabulary-growth feature actually needs (every required
|
|
1671
|
+
* test case in this feature's own spec phrases the mint sentence with
|
|
1672
|
+
* "every"), without swallowing either of those pre-existing shapes.
|
|
1673
|
+
*
|
|
1674
|
+
* The critical safety guard (operator's own stated worry, mirrored from
|
|
1675
|
+
* unknownSubjectFallback's docblock): this must NEVER silently mint when
|
|
1676
|
+
* BOTH sides are ungrounded ("every zorp is a florp" — two brand-new,
|
|
1677
|
+
* never-seen terms with no relation to each other tmct actually knows) —
|
|
1678
|
+
* declines here (null), and teachLane's own final honest-miss text picks up
|
|
1679
|
+
* an appended grounding NUDGE for exactly this case (ungroundedPairHint,
|
|
1680
|
+
* above) — never a silent guess, never a silent hard-swallowed decline
|
|
1681
|
+
* either. Any OTHER decline (subject ungrounded + object grounded — not this
|
|
1682
|
+
* fallback's asymmetry; or both grounded — already known, nothing to mint;
|
|
1683
|
+
* or no genuine universal quantifier) falls through as a plain null,
|
|
1684
|
+
* letting the ordinary teachLane cascade (property teach, then the generic
|
|
1685
|
+
* honest-miss text) continue unaffected. */
|
|
1686
|
+
async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon }) {
|
|
1687
|
+
if (!memoryDir) return null;
|
|
1688
|
+
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
1689
|
+
if (!m) return null;
|
|
1690
|
+
const [, det, subjectRaw, objectRaw] = m;
|
|
1691
|
+
if (!/^(?:every|each|all)$/i.test((det || "").trim())) return null; // class-level mint needs a real universal quantifier
|
|
1692
|
+
const { loadLexicon } = await import("./grammar/lexicon.mjs");
|
|
1693
|
+
const lex = lexicon || loadLexicon();
|
|
1694
|
+
const subjectGrounded = await isGroundedTerm(subjectRaw, lex, memoryDir);
|
|
1695
|
+
if (!subjectGrounded) return null; // ungrounded subject isn't this fallback's asymmetry — never a guessed mint
|
|
1696
|
+
const objectGrounded = await isGroundedTerm(objectRaw, lex, memoryDir);
|
|
1697
|
+
if (objectGrounded) return null; // object already known — nothing to mint
|
|
1698
|
+
const quantifier = /^every$/i.test((det || "").trim()) ? "every" : "";
|
|
1699
|
+
return teachFact(memoryDir, sessionId, {
|
|
1700
|
+
subject: subjectRaw, predicate: SUBCLASS_PREDICATE, object: objectRaw, quantifier,
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1407
1704
|
// ---- BUG 3 (2026-07-09, operator-authorized generalizing — "I don't know
|
|
1408
1705
|
// where that ban came from, overturn it. build it."): general verb-to-
|
|
1409
1706
|
// predicate teaching. "remember tony has a hat" / "remember margo eats ribs"
|
|
@@ -1442,7 +1739,21 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }
|
|
|
1442
1739
|
* multi-word subject simply doesn't match here and honestly declines
|
|
1443
1740
|
* (point 6) rather than risk a wrong split — the is/are-specific frames
|
|
1444
1741
|
* elsewhere in this lane already own that broader territory. */
|
|
1445
|
-
|
|
1742
|
+
// Tier-5 playtest fix (this session): a frequency/degree ADVERB commonly sits
|
|
1743
|
+
// between a bare-name subject and the real verb in a natural teaching
|
|
1744
|
+
// sentence — the operator's own example, "remember that TaskController
|
|
1745
|
+
// usually needs review", mis-split subject="TaskController", VERB="usually"
|
|
1746
|
+
// (GENERAL_VERB_TEACH_RE had no way to see "usually" wasn't the verb), minting
|
|
1747
|
+
// a nonsense mgx:usually predicate and, worse, garbling the confirmation
|
|
1748
|
+
// itself: thirdPersonSingularSurface's naive fallback appended "-ies" to the
|
|
1749
|
+
// unrecognized lemma, surfacing "taskcontroller usuallies needs review". A
|
|
1750
|
+
// closed, non-capturing adverb-skip (never itself eligible to BE the verb)
|
|
1751
|
+
// fixes this at the source for both the teach shape and its query-side twins
|
|
1752
|
+
// below, without widening what counts as a recognized shape at all — the same
|
|
1753
|
+
// "recognition closed, mapping generalized" split this lane already uses.
|
|
1754
|
+
const TEACH_ADVERB_SKIP_SRC = "(?:(?:usually|often|sometimes|rarely|never|always|typically|generally|"
|
|
1755
|
+
+ "occasionally|frequently|normally|regularly|commonly|mostly|currently|still|also|really|actually)\\s+)?";
|
|
1756
|
+
const GENERAL_VERB_TEACH_RE = new RegExp(`^([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+)\\s+(.+?)[.!?]*$`, "i");
|
|
1446
1757
|
/** Determiners/quantifiers that make the FIRST token an article, not a real
|
|
1447
1758
|
* bare-name subject ("every controller…", "the cache…") — GENERAL_VERB_TEACH_RE
|
|
1448
1759
|
* would otherwise happily bind them as a 1-token subject and misread the
|
|
@@ -1477,13 +1788,23 @@ const GENERAL_VERB_ANYWHERE_EXCLUDE_RE = /\b(?:is|are|am|owns|maintains)\b/i;
|
|
|
1477
1788
|
* documented contract) and this falls back to the verb AS TYPED — still a
|
|
1478
1789
|
* perfectly storable/retrievable predicate, just not cross-inflection
|
|
1479
1790
|
* canonicalized. Never a hand-curated per-verb table entry required. */
|
|
1791
|
+
// Bug A (operator manual-chat find, this session): only the exact raw strings
|
|
1792
|
+
// "has"/"have" were special-cased onto HAS_A_PREDICATE above — past tense "had"
|
|
1793
|
+
// (or "having") fell through to the generic mgx:<lemma> path, where the lemma of
|
|
1794
|
+
// "had" IS "have", and predicatePhrase's thirdPersonSingularSurface fallback
|
|
1795
|
+
// naively appends "s" to any unrecognized lemma ending ("have"+"s" = "haves" —
|
|
1796
|
+
// wrong; the correct irregular is "has"). Fixed by checking the LEMMA (not just
|
|
1797
|
+
// the raw verb) for "have" — this catches had/having/has/have uniformly, so
|
|
1798
|
+
// "remember X had soup" reads back "...has soup", never "...haves soup".
|
|
1480
1799
|
async function generalVerbPredicate(verb) {
|
|
1481
1800
|
const v = String(verb || "").toLowerCase();
|
|
1482
1801
|
if (v === "has" || v === "have") return HAS_A_PREDICATE;
|
|
1483
1802
|
try {
|
|
1484
1803
|
const { proseLemma } = await import("./prose-nlp.mjs");
|
|
1485
1804
|
const lemma = proseLemma();
|
|
1486
|
-
|
|
1805
|
+
const l = lemma ? lemma(v) : v;
|
|
1806
|
+
if (l === "have") return HAS_A_PREDICATE;
|
|
1807
|
+
return `mgx:${l}`;
|
|
1487
1808
|
} catch {
|
|
1488
1809
|
return `mgx:${v}`;
|
|
1489
1810
|
}
|
|
@@ -1510,6 +1831,35 @@ async function generalVerbTeach(payload) {
|
|
|
1510
1831
|
return { subject, predicate, object };
|
|
1511
1832
|
}
|
|
1512
1833
|
|
|
1834
|
+
// ---- General verb-to-predicate DIRECT-QUESTION retrieval (item 5, this
|
|
1835
|
+
// session's follow-up to the teach mechanism above): "does margo eat ribs" /
|
|
1836
|
+
// "did margo eat ribs" / "what does margo eat" against a fact taught via
|
|
1837
|
+
// generalVerbTeach. "did" joins "does" so past-tense forms work too (also
|
|
1838
|
+
// GROUP 3 Bug B — "what did X have"/"did margo eat ribs"). Wired into
|
|
1839
|
+
// factReadBack (below), which only runs on an already-true `miss`, so these
|
|
1840
|
+
// never race ask.mjs's closed structural grammar for a real graph query
|
|
1841
|
+
// ("does TaskController call widget" resolves there first, this lane is never
|
|
1842
|
+
// reached). Both run the SAME GENERAL_VERB_EXCLUDE_RE/GENERAL_VERB_ANYWHERE_
|
|
1843
|
+
// EXCLUDE_RE decline guards generalVerbTeach uses, and route the verb through
|
|
1844
|
+
// the SAME generalVerbPredicate (not a re-implementation), so the has/have
|
|
1845
|
+
// bridge (and Bug A's had/having lemma fix) is automatic on the query side too. ----
|
|
1846
|
+
// Same adverb-skip as GENERAL_VERB_TEACH_RE above (TEACH_ADVERB_SKIP_SRC),
|
|
1847
|
+
// reused so "does TaskController usually need review"/"what does
|
|
1848
|
+
// TaskController usually need" read back a fact taught with the adverb
|
|
1849
|
+
// skipped the same way, never mis-splitting "usually" as the verb here either.
|
|
1850
|
+
const GENERAL_VERB_YESNO_RE = new RegExp(`^(?:does|did)\\s+([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+)\\s+(.+?)[?.!\\s]*$`, "i");
|
|
1851
|
+
const GENERAL_VERB_OPEN_RE = new RegExp(`^what\\s+(?:does|did)\\s+([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+)[?.!\\s]*$`, "i");
|
|
1852
|
+
/** GENERAL_VERB_EXCLUDE_RE was written for generalVerbTeach's fully-conjugated
|
|
1853
|
+
* declarative verb ("X OWNS Y", "X MAINTAINS Y") — but "does/did X <verb> Y"
|
|
1854
|
+
* captures the BARE INFINITIVE after do-support ("does X OWN Y", never "does X
|
|
1855
|
+
* owns Y"), so "owns"/"maintains" literally never appear in genYN/genOpen's
|
|
1856
|
+
* captured verb even when the sentence names exactly that relation. Found live
|
|
1857
|
+
* (a real false "no" against a genuinely-true taught ownership fact, since
|
|
1858
|
+
* generalVerbPredicate("own") mints a DIFFERENT predicate — mgx:own — than the
|
|
1859
|
+
* ownership frame's own OWNED_BY_PREDICATE): the query-side guard needs the
|
|
1860
|
+
* bare-infinitive counterpart too. */
|
|
1861
|
+
const GENERAL_VERB_QUERY_EXCLUDE_RE = /^(?:be|own|maintain)$/i;
|
|
1862
|
+
|
|
1513
1863
|
/** Sentence forms to try asserting for a teach payload: the payload as-is, and
|
|
1514
1864
|
* (if it carries no determiner) its "every …" universal — the ACE-OWL shape the
|
|
1515
1865
|
* grammar actually lands. */
|
|
@@ -1574,7 +1924,14 @@ function teachSuggestion(payload) {
|
|
|
1574
1924
|
const TEACH_PRONOUN_RE = /^(?:every\s+|each\s+|all\s+|some\s+|a few\s+|a\s+|an\s+)?(you|i|it|they|he|she|we)\s+\S+/i;
|
|
1575
1925
|
|
|
1576
1926
|
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
1577
|
-
|
|
1927
|
+
// Tier 6 playtest: this lane read the raw, un-normalized query, so a closed
|
|
1928
|
+
// discourse-marker preamble ahead of a teach sentence ("howdy pardner,
|
|
1929
|
+
// remember that TaskController is fragile") corrupted TEACH_RE's own match —
|
|
1930
|
+
// applyPreambleFrames is idempotent no-op on an already-clean teach sentence
|
|
1931
|
+
// (none of its frames' anchors — greeting/thanks/ack/modal/explain/show-give-me/
|
|
1932
|
+
// topic-switch/hedge — match ordinary teach phrasing, verified against this
|
|
1933
|
+
// lane's own test corpus), so this is purely additive.
|
|
1934
|
+
const rawInput = applyPreambleFrames(String(query).trim());
|
|
1578
1935
|
const m = rawInput.match(TEACH_RE);
|
|
1579
1936
|
const wrappedInput = m ? m[1].trim() : null;
|
|
1580
1937
|
// "your X is a/an Y" (Feature A) — a plain casual synonym for "a/an X is a
|
|
@@ -1617,6 +1974,18 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
1617
1974
|
});
|
|
1618
1975
|
if (stored) return stored;
|
|
1619
1976
|
}
|
|
1977
|
+
// PASSIVE ownership — "<X> is owned by <Name>" (Tier-5 playtest, cycle 2).
|
|
1978
|
+
// Same bare-form gate as the active shape just above: a Capitalized owner
|
|
1979
|
+
// name AND no interrogative lead, so "is TaskController owned by anyone"
|
|
1980
|
+
// (a genuine yes/no QUESTION, handled by factReadBack instead) never lands
|
|
1981
|
+
// a bogus fact here.
|
|
1982
|
+
const ownPassive = ownSrc.match(OWNS_PASSIVE_TEACH_RE);
|
|
1983
|
+
if (ownPassive && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && (wrapped || /^[A-Z]/.test(ownPassive[2]))) {
|
|
1984
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
1985
|
+
subject: ownPassive[1], predicate: OWNED_BY_PREDICATE, object: ownPassive[2],
|
|
1986
|
+
});
|
|
1987
|
+
if (stored) return stored;
|
|
1988
|
+
}
|
|
1620
1989
|
|
|
1621
1990
|
// "some Xs are Ys" / "a few Xs are Ys" (Feature A) — the plural class-
|
|
1622
1991
|
// membership quantifier shape. ACE has no quantifier-phrase pattern at all
|
|
@@ -1641,6 +2010,33 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
1641
2010
|
subject, predicate: SUBCLASS_PREDICATE, object, quantifier,
|
|
1642
2011
|
});
|
|
1643
2012
|
if (stored) return stored;
|
|
2013
|
+
} else {
|
|
2014
|
+
// Tier-5 playtest fix (cycle 2), found live: "remember that some
|
|
2015
|
+
// functions are risky" — Y ("risky") is not a lexicon NOUN, so the
|
|
2016
|
+
// subclass path just above correctly declines it (SOME_A_FEW_RE is
|
|
2017
|
+
// subclass-only, by design — "risky" isn't even in the closed
|
|
2018
|
+
// lexicon at all, as either noun or adjective, so gating this decline
|
|
2019
|
+
// on "is Y a known adjective" missed the actual case entirely on the
|
|
2020
|
+
// first attempt at this fix). Without this guard, the sentence fell
|
|
2021
|
+
// through to unknownSubjectFallback/TEACH_PROPERTY_RE below, which DO
|
|
2022
|
+
// tolerate a multi-word subject with NO vocabulary check on the
|
|
2023
|
+
// complement at all — silently mis-teaching the LITERAL 2-word string
|
|
2024
|
+
// "some functions" as if it were one proper-noun subject ("noted —
|
|
2025
|
+
// remembered: some functions is risky", the quantifier word baked
|
|
2026
|
+
// wrongly into the subject and a subject/verb agreement error to
|
|
2027
|
+
// boot), a fact "how many functions are risky" could never sensibly
|
|
2028
|
+
// read back either (HOW_MANY_ARE_RE's own reader only ever looks for
|
|
2029
|
+
// the SUBCLASS_PREDICATE shape this path would have stored, not this
|
|
2030
|
+
// one). A quantified PROPERTY claim isn't a supported shape yet (only
|
|
2031
|
+
// a quantified SUBCLASS claim is) — decline honestly here instead of
|
|
2032
|
+
// silently mis-teaching, rather than let a later, less-specific frame
|
|
2033
|
+
// guess a wrong split.
|
|
2034
|
+
return {
|
|
2035
|
+
text: `I can only remember a quantified fact as "${quantifier} ${someMatch[2]} are <a kind of thing>" (like "${quantifier} bugs are issues") — `
|
|
2036
|
+
+ `a quantified claim about a PROPERTY ("${quantifier} ${someMatch[2]} are ${object}") isn't a shape I can store yet. `
|
|
2037
|
+
+ `I can remember "${someMatch[2]} are ${object}" for one specific ${subject}, though — try naming it directly.`,
|
|
2038
|
+
via: "teach-miss", miss: true,
|
|
2039
|
+
};
|
|
1644
2040
|
}
|
|
1645
2041
|
}
|
|
1646
2042
|
|
|
@@ -1665,7 +2061,28 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
1665
2061
|
let payload = null;
|
|
1666
2062
|
if (wrapped && /\b(?:is|are)\b/i.test(wrapped)) payload = wrapped;
|
|
1667
2063
|
else if (BARE_DECLARATIVE_RE.test(raw) && !QUESTION_LEAD_RE.test(raw)) payload = raw;
|
|
1668
|
-
if (!payload)
|
|
2064
|
+
if (!payload) {
|
|
2065
|
+
// Tier-5 playtest fix (cycle 3), found live: "remember that every
|
|
2066
|
+
// controller needs review" — a QUANTIFIED subject ("every X", declined
|
|
2067
|
+
// by generalVerbTeach's own GENERAL_VERB_DETERMINER_RE, by design — see
|
|
2068
|
+
// its docblock on the ambiguity risk of a free-form multi-word subject)
|
|
2069
|
+
// combined with a non-copula verb ("needs", not is/are/owns/maintains)
|
|
2070
|
+
// fits NONE of the recognizers above, so `payload` stays null and this
|
|
2071
|
+
// used to return null SILENTLY — the exact "wrong-context wall" bug
|
|
2072
|
+
// class Bug 3's generalVerbTeach mechanism was built to close for
|
|
2073
|
+
// "remember margo eats ribs", re-escaping here through a combination
|
|
2074
|
+
// that mechanism's own deliberate subject-shape restriction doesn't
|
|
2075
|
+
// cover. An explicit "remember/note/…"-wrapped sentence is an
|
|
2076
|
+
// UNAMBIGUOUS teach-intent signal — falling through to the ordinary
|
|
2077
|
+
// structural-query wall is a wrong-context reply even when nothing here
|
|
2078
|
+
// can actually STORE the fact; if `wrapped` stood, keep going with it as
|
|
2079
|
+
// the payload so the residue-detection/final-decline logic below still
|
|
2080
|
+
// runs (never a guess at storing it, just never silence either). A bare,
|
|
2081
|
+
// unwrapped sentence that also fits no shape has no such signal — return
|
|
2082
|
+
// null and let the ordinary cascade decide, unchanged.
|
|
2083
|
+
if (!wrapped) return null;
|
|
2084
|
+
payload = wrapped;
|
|
2085
|
+
}
|
|
1669
2086
|
// Try to store it (a live session provides the write target). assertTurn returns
|
|
1670
2087
|
// the "noted — remembered …" confirmation or null (grammar miss / unknown words).
|
|
1671
2088
|
if (memoryDir) {
|
|
@@ -1684,6 +2101,14 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
1684
2101
|
// the exact narrowing rules (object must still be known, etc.).
|
|
1685
2102
|
const fallback = await unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon });
|
|
1686
2103
|
if (fallback) return fallback;
|
|
2104
|
+
// MIRROR mint fallback (Feature A, 2026-07-09 operator-authorized vocabulary-
|
|
2105
|
+
// growth extension): the known-subject/unknown-object asymmetry — tried
|
|
2106
|
+
// right after the unknown-subject case declines, so a subject the STATIC
|
|
2107
|
+
// lexicon (or a prior taught fact) already grounds can mint a brand-new
|
|
2108
|
+
// object term. See unknownObjectFallback's own docblock for the exact
|
|
2109
|
+
// narrowing rules (the "both sides ungrounded" safety guard, etc.).
|
|
2110
|
+
const objectFallback = await unknownObjectFallback(payload, { memoryDir, sessionId, lexicon });
|
|
2111
|
+
if (objectFallback) return objectFallback;
|
|
1687
2112
|
// PROPERTY teach — "remember/note that <X> is <adjective>": wrapper-REQUIRED
|
|
1688
2113
|
// (a bare "X is deprecated" is never silently reified), and only after the
|
|
1689
2114
|
// ACE grammar declined (unknown words / not the membership shape), so a
|
|
@@ -1742,9 +2167,14 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
1742
2167
|
+ "I can only teach facts using tmct's own code-vocabulary nouns (like module, class, function…), "
|
|
1743
2168
|
+ "not arbitrary new terms."
|
|
1744
2169
|
: "";
|
|
2170
|
+
// Grounding NUDGE (operator refinement, 2026-07-09): APPENDED, never a
|
|
2171
|
+
// replacement, exactly like "did" above — see ungroundedPairHint's own
|
|
2172
|
+
// docblock for why this is scoped to the "both sides ungrounded, fits the
|
|
2173
|
+
// X is/are Y shape" case only.
|
|
2174
|
+
const groundingHint = await ungroundedPairHint(payload, lexicon, memoryDir);
|
|
1745
2175
|
return {
|
|
1746
2176
|
text: `I couldn't store that —${why} I remember facts in the shape "every X is a Y", where X and Y are `
|
|
1747
|
-
+ `words I know.${did} Type /memory to see what I already remember.`,
|
|
2177
|
+
+ `words I know.${did}${groundingHint} Type /memory to see what I already remember.`,
|
|
1748
2178
|
via: "teach-miss", miss: true,
|
|
1749
2179
|
};
|
|
1750
2180
|
}
|
|
@@ -1754,11 +2184,18 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
1754
2184
|
// ONLY (the caller gates on a miss) and every pattern is a WHOLE-LINE self/session
|
|
1755
2185
|
// reference with no graph entity or predicate, so real graph queries ("what does X
|
|
1756
2186
|
// import", the meta "what does imports mean", "what did i ask before") never match.
|
|
1757
|
-
|
|
2187
|
+
// Bug D (operator manual-chat find, this session): "what is in your memory"/
|
|
2188
|
+
// "what's in your memory" is a plain synonym of the bare "what do you know" —
|
|
2189
|
+
// widened here rather than folded in as "what do you remember" (that phrase is
|
|
2190
|
+
// ALREADY WHOLE_RECALL_RE's own, more specific, territory — it lists every
|
|
2191
|
+
// remembered fact, a strictly better answer than this lane's short summary; see
|
|
2192
|
+
// WHOLE_RECALL_RE's docblock below and the pinned "'what do you remember' ...
|
|
2193
|
+
// STILL list facts" test — folding it in here would silently regress that).
|
|
2194
|
+
const WHAT_KNOW_RE = /^(?:what\s+(?:do\s+you|d'?you)\s+know(?:\s+so\s+far)?|what(?:'s|s|\s+is)\s+in\s+your\s+memory)$/;
|
|
1758
2195
|
// 0.8.2 WS4 wall kindness (c): the most likely stranger openers — "what does this
|
|
1759
2196
|
// app/codebase do", "what is this app (for)" — join the orientation lane, so a
|
|
1760
2197
|
// first-touch question gets the live overview instead of the grammar wall.
|
|
1761
|
-
const META_ORIENT_RE = /^(?:what(?:'s| is| are)?\s+this(?:\s+(?:app|codebase|repo|repository|project|code|thing))?|what\s+(?:codebase|repo|repository|project)\s+is\s+this|what\s+does\s+(?:this|the)\s+(?:app|code|codebase|project|repo)\s+do|what\s+is\s+(?:this|the)\s+app(?:\s+for)?|what\s+am\s+i\s+looking\s+at|what\s+is\s+tmct|how\s+do\s+i\s+(?:start|begin|get\s+started|get\s+going|load\s+(?:my\s+)?code|index\s+(?:my\s+)?(?:code|repo|repository)|use\s+(?:this|you|tmct))|where\s+do\s+i\s+(?:start|begin))$/;
|
|
2198
|
+
const META_ORIENT_RE = /^(?:what(?:'s| is| are)?\s+this(?:\s+(?:app|codebase|repo|repository|project|code|thing))?|what\s+(?:codebase|repo|repository|project)\s+is\s+this|what\s+does\s+(?:this|the)\s+(?:app|code|codebase|project|repo)\s+do|what\s+is\s+(?:this|the)\s+app(?:\s+for)?|what\s+am\s+i\s+looking\s+at|what\s+is\s+tmct|how\s+do\s+i\s+(?:start|begin|get\s+started|get\s+going|load\s+(?:my\s+)?code|index\s+(?:my\s+)?(?:code|repo|repository)|use\s+(?:this|you|tmct))|where\s+do\s+i\s+(?:start|begin)|what\s+should\s+i\s+(?:read|look\s+at)\s+first(?:\s+to\s+understand\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?|where\s+should\s+i\s+start\s+reading(?:\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?|where\s+do\s+i\s+begin\s+reading(?:\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?)$/;
|
|
1762
2199
|
|
|
1763
2200
|
/** A SHORT memory summary (never a fact dump) for the bare "what do you know".
|
|
1764
2201
|
* This branch only fires when rows.length === 0 — i.e. precisely the case where
|
|
@@ -1795,15 +2232,57 @@ async function memorySummary(memoryDir, graph) {
|
|
|
1795
2232
|
// case-sensitive, so this reads the ORIGINAL query text, never metaLane's
|
|
1796
2233
|
// lowercased `q` (authorLane's same discipline, just above/below).
|
|
1797
2234
|
const MODULE_ORIENT_RE = /^what\s+does\s+(.+?)\s+do\??$/i;
|
|
2235
|
+
/** The SUBJECT-FIRST word order of the SAME question ("what saveStore does" vs
|
|
2236
|
+
* "what does saveStore do") — Tier 6 playtest, §3b surface-variation axis: a
|
|
2237
|
+
* perfectly natural alternate phrasing of an ALREADY-recognized intent that
|
|
2238
|
+
* used to hit the raw grammar wall outright (MODULE_ORIENT_RE's own anchor
|
|
2239
|
+
* requires "does" BEFORE the term). Tried only when MODULE_ORIENT_RE/
|
|
2240
|
+
* MODULE_PURPOSE_RE both miss; the entity-resolution gate just below (a real,
|
|
2241
|
+
* UNIQUE graph entity or this lane declines) is what keeps this loose an
|
|
2242
|
+
* ending safe — a syntactic match against a term that isn't a real entity
|
|
2243
|
+
* simply falls through unchanged, same as every other lane in this file. */
|
|
2244
|
+
const MODULE_ORIENT_SVO_RE = /^what\s+(.+?)\s+does\??$/i;
|
|
2245
|
+
// Seonix Batch 3 (3a) — purpose/identity phrasing: "whats X for"/"what's X
|
|
2246
|
+
// about"/"what is X for", the sibling of "what does X do" that asks for the
|
|
2247
|
+
// SAME module-grain overview. Deliberately does NOT claim the literal noun
|
|
2248
|
+
// "app" ("what is this app for") — META_ORIENT_RE (above) already hardcodes
|
|
2249
|
+
// that exact phrasing and is checked BEFORE moduleOrientLane runs (metaLane's
|
|
2250
|
+
// own ordering), so this regex only ever gets a chance at OTHER resolvable
|
|
2251
|
+
// terms. "what(?:'s|s|\s+is)" mirrors PERSONAL_ASSISTANT_NUDGE_RE's own
|
|
2252
|
+
// tolerance for the bare "whats" contraction spelling, just below.
|
|
2253
|
+
const MODULE_PURPOSE_RE = /^what(?:'s|s|\s+is)\s+(.+?)\s+(?:for|about)\??$/i;
|
|
2254
|
+
|
|
2255
|
+
/** A leading politeness/formal-ESL wrapper this lane's own anchored regexes
|
|
2256
|
+
* otherwise miss entirely (Tier 6 playtest): "please explain what does X do"
|
|
2257
|
+
* starts with neither "what"/"whats" (MODULE_ORIENT_RE/MODULE_PURPOSE_RE's own
|
|
2258
|
+
* anchor) nor bare "explain" (normalize.mjs's own EXPLAIN_WRAPPER_RE, which
|
|
2259
|
+
* requires NOTHING before "explain" — a leading "please" defeats it too), so
|
|
2260
|
+
* it fell straight to the raw grammar wall. A repeated (please|kindly) plus an
|
|
2261
|
+
* optional "explain [to me]" — both closed, both optional, so a bare "what
|
|
2262
|
+
* does X do" is untouched (the whole prefix matches empty). */
|
|
2263
|
+
const MODULE_ORIENT_POLITENESS_RE = /^(?:(?:please|kindly)\s+)*(?:explain\s+(?:to\s+me\s+)?)?/i;
|
|
1798
2264
|
|
|
1799
2265
|
/** authorLane's discipline, mirrored: a closed regex + an EXACT, UNIQUE
|
|
1800
2266
|
* resolution via resolveEntity, else null — never a guess. Pronoun/self
|
|
1801
|
-
* subjects ("what does it/this do") are META_ORIENT_RE's/
|
|
1802
|
-
* territory, not this lane's — declined here so they
|
|
2267
|
+
* subjects ("what does it/this do", "what's it for") are META_ORIENT_RE's/
|
|
2268
|
+
* isConversational's territory, not this lane's — declined here so they
|
|
2269
|
+
* fall through unchanged. */
|
|
1803
2270
|
async function moduleOrientLane(query, { graph }) {
|
|
1804
2271
|
if (!graph) return null;
|
|
1805
|
-
|
|
1806
|
-
|
|
2272
|
+
let q = String(query).trim().replace(/[?.!]+$/, "").replace(/\s+/g, " ");
|
|
2273
|
+
// Tier 6 playtest: this lane reads the ORIGINAL (case-preserving) query text
|
|
2274
|
+
// and never ran any of the general-purpose normalization passes the rest of
|
|
2275
|
+
// the file uses for the SAME class of surface noise — correctMisspellings
|
|
2276
|
+
// for a typo'd anchor word ("waht dose the logger modul do"), applyPreambleFrames
|
|
2277
|
+
// for a topic-switch/self-interruption preamble ("scratch that, what does X
|
|
2278
|
+
// do") — plus a lane-local politeness strip for "please explain X" (applyPreambleFrames's
|
|
2279
|
+
// own EXPLAIN_WRAPPER_RE requires the string to literally START with "explain",
|
|
2280
|
+
// so a LEADING "please"/"kindly" ahead of it defeats that frame; see
|
|
2281
|
+
// MODULE_ORIENT_POLITENESS_RE's own docblock). All three are additive,
|
|
2282
|
+
// closed-set, and idempotent on an already-clean query, so applying them here
|
|
2283
|
+
// only ever WIDENS what resolves, never narrows it.
|
|
2284
|
+
q = applyPreambleFrames(correctMisspellings(q)).replace(MODULE_ORIENT_POLITENESS_RE, "");
|
|
2285
|
+
const m = q.match(MODULE_ORIENT_RE) || q.match(MODULE_PURPOSE_RE) || q.match(MODULE_ORIENT_SVO_RE);
|
|
1807
2286
|
if (!m) return null;
|
|
1808
2287
|
const term = m[1].trim();
|
|
1809
2288
|
if (/^(?:it|this|that|they|them)$/i.test(term)) return null;
|
|
@@ -2179,7 +2658,11 @@ function configFor(repoPath) {
|
|
|
2179
2658
|
/** Resolve a free-text term to a single graph entity via the ask engine's own
|
|
2180
2659
|
* tiered resolver — {id,label} on a UNIQUE hit, null on a miss/ambiguity/no graph.
|
|
2181
2660
|
* Lazy + failure-tolerated (see the file docblock): the worst case is a turn that
|
|
2182
|
-
* records fewer ids / does not update the focus, never a crash or a wrong id.
|
|
2661
|
+
* records fewer ids / does not update the focus, never a crash or a wrong id.
|
|
2662
|
+
* The leading-article-strip + trailing-grain-word disambiguation ("the logger
|
|
2663
|
+
* module" -> Module-only "logger") lives centrally in resolveObject itself
|
|
2664
|
+
* (ask.mjs) so every direct caller of resolveObject (ask()'s own WHERE/describe
|
|
2665
|
+
* grammar, traverse(), etc.) gets it too, not just this wrapper. */
|
|
2183
2666
|
async function resolveEntity(graph, term) {
|
|
2184
2667
|
if (!graph || !term) return null;
|
|
2185
2668
|
try {
|
|
@@ -2398,6 +2881,13 @@ const FACT_PREDICATE_PHRASES = {
|
|
|
2398
2881
|
* renders verbatim, unchanged from before this fix. */
|
|
2399
2882
|
function thirdPersonSingularSurface(lemma) {
|
|
2400
2883
|
const w = String(lemma || "");
|
|
2884
|
+
// Bug A safety net: "have" should never reach this naive fallback at all
|
|
2885
|
+
// (generalVerbPredicate special-cases it onto mgx:hasA before a predicate is
|
|
2886
|
+
// ever minted), but if some OTHER path ever reaches here with it as typed —
|
|
2887
|
+
// e.g. wink-nlp unavailable so lemma degrades to the raw verb — the naive
|
|
2888
|
+
// "+s" rule would produce the wrong-MEANING "haves" instead of the correct
|
|
2889
|
+
// irregular "has".
|
|
2890
|
+
if (/^have$/i.test(w)) return "has";
|
|
2401
2891
|
if (/[a-z]y$/i.test(w) && !/[aeiou]y$/i.test(w)) return `${w.slice(0, -1)}ies`;
|
|
2402
2892
|
if (/(?:s|x|z|ch|sh|o)$/i.test(w)) return `${w}es`;
|
|
2403
2893
|
return `${w}s`;
|
|
@@ -2523,6 +3013,25 @@ function factTermVariants(normFactTerm, term) {
|
|
|
2523
3013
|
return v;
|
|
2524
3014
|
}
|
|
2525
3015
|
|
|
3016
|
+
/** GENERIC "kind" nouns a taught subject's head word is often built from
|
|
3017
|
+
* ("logger MODULE", "task CONTROLLER") — excluded from the head-word
|
|
3018
|
+
* overlap fallback both KNOW_ABOUT_RE's "what do you know about X" listing
|
|
3019
|
+
* and IS_ADJECTIVE_YESNO_RE's property yes/no reader use (below): a bare
|
|
3020
|
+
* length >= 4 floor alone isn't enough, since "module" (6 chars) is shared
|
|
3021
|
+
* by "logger module" AND "validate module" and any OTHER "X module" taught
|
|
3022
|
+
* subject — without this exclusion, "is the validate module deprecated"
|
|
3023
|
+
* confidently answered YES off a fact taught for "logger module" (found
|
|
3024
|
+
* live, Tier-5 playtest cycle 5 — a real false-positive fabrication, not a
|
|
3025
|
+
* routing gap, caught before shipping). Mirrors RECALL_STOPWORDS' own
|
|
3026
|
+
* path-noise exclusion (src/lib/mjs never counting as a real overlap
|
|
3027
|
+
* either) — same principle, a different word class. */
|
|
3028
|
+
const GENERIC_ENTITY_WORDS = new Set([
|
|
3029
|
+
"module", "modules", "class", "classes", "function", "functions",
|
|
3030
|
+
"method", "methods", "handler", "handlers", "controller", "controllers",
|
|
3031
|
+
"service", "services", "component", "components", "flow", "flows",
|
|
3032
|
+
"thing", "things", "item", "items", "object", "objects", "commit", "commits",
|
|
3033
|
+
]);
|
|
3034
|
+
|
|
2526
3035
|
// ---- PLAN_ontology-hierarchies.md §3 tracks (a)+(b): synonymsOf(term) —
|
|
2527
3036
|
// QUERY-TIME term expansion wiring the two already-parsed-but-inert synonym
|
|
2528
3037
|
// resources. §1's "two vocabulary gates" distinction: this widens what a
|
|
@@ -2605,8 +3114,14 @@ async function synonymsOf(term) {
|
|
|
2605
3114
|
* doesn't parse; checked against the isa-family fact predicates only. */
|
|
2606
3115
|
const ISA_ASK_RE = /^(?:is|are)\s+(?:an?\s+)?(.+?)\s+(?:a\s+kind\s+of|a\s+type\s+of|an?)\s+(.+?)[?.!\s]*$/i;
|
|
2607
3116
|
const ISA_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
|
|
2608
|
-
/** "what do you know about caches" — the open recall-everything form.
|
|
2609
|
-
|
|
3117
|
+
/** "what do you know about caches" — the open recall-everything form. Bug E
|
|
3118
|
+
* (operator manual-chat find, this session) widened this to also accept
|
|
3119
|
+
* "what is in your memory about X" / "what's in your memory about X" / "what
|
|
3120
|
+
* do you remember about X" as plain synonyms — none of these collide with an
|
|
3121
|
+
* existing more-specific lane (TOLD_ABOUT_RE only owns "what did i tell you
|
|
3122
|
+
* about X"; WHOLE_RECALL_RE's own "what do you remember" has no "about X"
|
|
3123
|
+
* tail, so it's a disjoint shape). */
|
|
3124
|
+
const KNOW_ABOUT_RE = /^(?:what\s+do\s+you\s+know\s+about|what(?:'s|s|\s+is)\s+in\s+your\s+memory\s+about|what\s+do\s+you\s+remember\s+about)\s+(.+?)[?.!\s]*$/i;
|
|
2610
3125
|
/** How many facts a single answer lists before the remainder is paged with "more". */
|
|
2611
3126
|
const FACT_ANSWER_CAP = 32;
|
|
2612
3127
|
|
|
@@ -2631,7 +3146,11 @@ async function factAnswer(memoryDir, query, envelope, miss) {
|
|
|
2631
3146
|
if (!metaTerm && miss && !envelope?.parsed) {
|
|
2632
3147
|
const m = q.match(BARE_WHATIS_RE)
|
|
2633
3148
|
|| q.match(/^what\s+(?:does|do)\s+(.+?)\s+means?[?.!\s]*$/i);
|
|
2634
|
-
|
|
3149
|
+
// Seonix Batch 2 Fix 3: strip a curated trailing scope clause ("… in this
|
|
3150
|
+
// graph"/"… in this codebase"/…) the same way grammar.mjs's T5 and
|
|
3151
|
+
// metaTermOf do — BARE_WHATIS_RE's capture is otherwise the literal glued
|
|
3152
|
+
// tail, verbatim.
|
|
3153
|
+
if (m) metaTerm = stripTrailingScopeFiller(m[1]);
|
|
2635
3154
|
}
|
|
2636
3155
|
if (metaTerm) {
|
|
2637
3156
|
// BUG 1 fix: "what is a tree used for" parses (grammar.mjs T5) to the
|
|
@@ -2682,15 +3201,93 @@ async function factAnswer(memoryDir, query, envelope, miss) {
|
|
|
2682
3201
|
const know = q.match(KNOW_ABOUT_RE);
|
|
2683
3202
|
if (know) {
|
|
2684
3203
|
const variants = factTermVariants(normFactTerm, know[1]);
|
|
2685
|
-
const
|
|
3204
|
+
const rows = await memoryFacts(memoryDir);
|
|
3205
|
+
// Bug E subtype walk (operator follow-up request, this session): a
|
|
3206
|
+
// cycle-safe BFS DOWNWARD over isa-family facts from the term's own
|
|
3207
|
+
// variants — every fact whose OBJECT is in the current frontier
|
|
3208
|
+
// contributes its SUBJECT as a known SUBTYPE (and the next hop's
|
|
3209
|
+
// frontier), so "every widget is a component" + "button is a widget" +
|
|
3210
|
+
// "button has a blue-color" lets "what do you know about component"
|
|
3211
|
+
// surface the button fact too, even though "button" never literally
|
|
3212
|
+
// mentions "component". Capped at 8 hops — this is a listing operation,
|
|
3213
|
+
// not findIsaChain's strict maxHops:2 proof-chase, but still bounded as a
|
|
3214
|
+
// safety net against pathological data.
|
|
3215
|
+
//
|
|
3216
|
+
// The chain itself is walked over TAUGHT isa facts only (same "isTaught"
|
|
3217
|
+
// discipline the live cax-sco/scm-sco proof chase already uses, below) —
|
|
3218
|
+
// the bulk background corpus (thousands of ConceptNet/seon "is a kind of"
|
|
3219
|
+
// rows) would otherwise chain almost ANY term into hundreds of coincidental
|
|
3220
|
+
// "subtypes" that have nothing to do with what the OPERATOR actually
|
|
3221
|
+
// taught, drowning the real answer and defeating the negative-case
|
|
3222
|
+
// discipline this feature exists to preserve. The literal-mention hits
|
|
3223
|
+
// (the ORIGINAL, non-subtype half of the filter below) still include
|
|
3224
|
+
// corpus facts exactly as before — only the SUBTYPE DISCOVERY chain is
|
|
3225
|
+
// taught-only.
|
|
3226
|
+
// `rows` here is memoryFacts()'s plain {subject,predicate,object,provenance}
|
|
3227
|
+
// shape (no `sourceTypes` — that's factRows()'s own trust-enriched shape),
|
|
3228
|
+
// so the taught/corpus distinction reads the SAME provenance-string
|
|
3229
|
+
// convention renderFactLine already keys its own corpus-vs-taught framing
|
|
3230
|
+
// on, just above.
|
|
3231
|
+
const isTaughtFact = (f) => !String(f.provenance || "").includes("corpus:") && !String(f.provenance || "").includes("web:");
|
|
3232
|
+
const isaRows = rows.filter((f) => ISA_PREDICATES.has(f.predicate) && isTaughtFact(f));
|
|
3233
|
+
const subtypeSubjects = new Set();
|
|
3234
|
+
let frontier = variants;
|
|
3235
|
+
for (let hop = 0; hop < 8 && frontier.size; hop += 1) {
|
|
3236
|
+
const nextSubjects = new Set();
|
|
3237
|
+
for (const f of isaRows) {
|
|
3238
|
+
if (frontier.has(f.object) && !subtypeSubjects.has(f.subject)) nextSubjects.add(f.subject);
|
|
3239
|
+
}
|
|
3240
|
+
if (!nextSubjects.size) break;
|
|
3241
|
+
for (const s of nextSubjects) subtypeSubjects.add(s);
|
|
3242
|
+
const nextFrontier = new Set();
|
|
3243
|
+
for (const s of nextSubjects) for (const v of factTermVariants(normFactTerm, s)) nextFrontier.add(v);
|
|
3244
|
+
frontier = nextFrontier;
|
|
3245
|
+
}
|
|
3246
|
+
let hits = rows.filter((f) => variants.has(f.subject) || variants.has(f.object) || subtypeSubjects.has(f.subject));
|
|
3247
|
+
// Tier-5 playtest fallback: a taught fact's subject is often a real NOUN
|
|
3248
|
+
// PHRASE ("logger module", "tasks handler"), but a natural follow-up
|
|
3249
|
+
// shortens it to one head word ("what do you know about the logger") —
|
|
3250
|
+
// an exact-variant miss above, since "logger" !== "logger module". Only
|
|
3251
|
+
// tried when the exact/subtype pass found NOTHING (never overrides a real
|
|
3252
|
+
// hit), and only on a whole WORD (length >= 4, the same floor
|
|
3253
|
+
// resolveObject's own tier-3/5 containment checks use to keep a short
|
|
3254
|
+
// staccato word from hijacking an unrelated fact) shared between the
|
|
3255
|
+
// query term and a fact's subject/object — a listing/discovery feature
|
|
3256
|
+
// (like the subtype walk above), not a yes/no claim, so a slightly wider
|
|
3257
|
+
// recall net is consistent with this lane's existing inclusiveness.
|
|
3258
|
+
if (!hits.length) {
|
|
3259
|
+
const queryWords = normFactTerm(know[1]).split(/\s+/).filter((w) => w.length >= 4 && !GENERIC_ENTITY_WORDS.has(w));
|
|
3260
|
+
if (queryWords.length) {
|
|
3261
|
+
const wordsOf = (s) => new Set(String(s || "").split(/\s+/));
|
|
3262
|
+
const overlaps = (term) => { const w = wordsOf(term); return queryWords.some((qw) => w.has(qw)); };
|
|
3263
|
+
hits = rows.filter((f) => overlaps(f.subject) || overlaps(f.object));
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
// A genuinely empty result here is a real miss (Tier-5 playtest cycle 3:
|
|
3267
|
+
// "what do you know about the last commit" needs a TEACH-OFFER, not a
|
|
3268
|
+
// bare wall — added as a LATE runTurn-level addition, below, alongside
|
|
3269
|
+
// the sibling "what is X" offer, rather than returned from here: an
|
|
3270
|
+
// early return through this function's normal contract would pre-empt
|
|
3271
|
+
// runTurn's own wall-shortening pass (shortMissHint/lane 5), leaving the
|
|
3272
|
+
// FULL unshortened grammar cheat-sheet standing under the offer instead
|
|
3273
|
+
// of the nicer tailored one-liner — found live while adding this fix).
|
|
2686
3274
|
if (!hits.length) return null;
|
|
2687
3275
|
// echo the STORED spelling ("caches" asked → "cache" known), never a guess
|
|
2688
|
-
const
|
|
3276
|
+
const literalHit = hits.find((f) => variants.has(f.subject) || variants.has(f.object));
|
|
3277
|
+
const term = literalHit
|
|
3278
|
+
? (variants.has(literalHit.subject) ? literalHit.subject : literalHit.object)
|
|
3279
|
+
: know[1].trim();
|
|
3280
|
+
// when a subtype-derived hit contributed something a plain literal-mention
|
|
3281
|
+
// match wouldn't have found, say so — lets the reader tell subtype-derived
|
|
3282
|
+
// facts apart from literal mentions.
|
|
3283
|
+
const viaSubtype = hits.some((f) => subtypeSubjects.has(f.subject) && !variants.has(f.subject) && !variants.has(f.object));
|
|
2689
3284
|
const lines = hits.map((f) => ` ${renderFactLine(f)}`);
|
|
2690
3285
|
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
2691
3286
|
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
2692
3287
|
const extra = rest.length ? `\n …and ${rest.length} more — say 'more' to see them.` : "";
|
|
2693
|
-
|
|
3288
|
+
const header = `${hits.length} remembered fact${hits.length === 1 ? "" : "s"} about ${term}`
|
|
3289
|
+
+ `${viaSubtype ? " (including its known subtypes)" : ""}:`;
|
|
3290
|
+
return { text: `${header}\n${shown.join("\n")}${extra}`, replace: true, ...(rest.length ? { pending: { items: rest.map((l) => l.trim()), noun: "facts" } } : {}) };
|
|
2694
3291
|
}
|
|
2695
3292
|
return null;
|
|
2696
3293
|
}
|
|
@@ -2816,6 +3413,45 @@ const KIND_OF_RE = /^what\s+kind\s+of\s+(?:thing|class|type|category|entity)?\s*
|
|
|
2816
3413
|
/** "who owns <X>" / "who maintains <X>" — the closed ownership read-back over
|
|
2817
3414
|
* the teach lane's mgx:ownedBy facts. */
|
|
2818
3415
|
const WHO_OWNS_RE = /^who\s+(?:owns|maintains)\s+(.+?)[?.!\s]*$/i;
|
|
3416
|
+
/** "does/did <Name> own/maintain <X>" — the yes/no ownership claim over the
|
|
3417
|
+
* SAME mgx:ownedBy facts WHO_OWNS_RE reads (Tier-5 playtest fix). The bare
|
|
3418
|
+
* infinitive after do-support ("does X own Y", never "does X owns Y") mirrors
|
|
3419
|
+
* GENERAL_VERB_YESNO_RE's own do-support convention. */
|
|
3420
|
+
const OWNS_YESNO_RE = /^(?:does|did)\s+([\w'-]+)\s+(?:owns?|maintains?)\s+(.+?)[?.!\s]*$/i;
|
|
3421
|
+
/** "is/are/was/were <X> owned by <Name>" — the PASSIVE yes/no ownership
|
|
3422
|
+
* claim, sibling of OWNS_YESNO_RE just above and OWNS_PASSIVE_TEACH_RE
|
|
3423
|
+
* (chat.mjs's teach lane) — same mgx:ownedBy facts, matched BEFORE
|
|
3424
|
+
* IS_ADJECTIVE_YESNO_RE below (which would otherwise also match this shape,
|
|
3425
|
+
* backtracking "owned by" into its own subject capture and "<Name>" into its
|
|
3426
|
+
* adjective slot, silently declining rather than answering). */
|
|
3427
|
+
const OWNS_PASSIVE_YESNO_RE = /^(?:is|are|was|were)\s+(.+?)\s+owned\s+by\s+([A-Za-z][\w'-]*(?:\s+[A-Z][\w'-]*)?)[?.!\s]*$/i;
|
|
3428
|
+
/** "is/are/was/were <X> <adjective>" — a yes/no claim over a taught
|
|
3429
|
+
* mgx:hasProperty fact (Tier-5 playtest fix). Deliberately has NO marker
|
|
3430
|
+
* between subject and complement — "a"/"an"/"a kind of"/"a type of" is
|
|
3431
|
+
* ISA_ASK_RE's own mandatory territory just below (matched and handled, or
|
|
3432
|
+
* matched-and-declined, BEFORE this code ever runs), so a genuine "is a
|
|
3433
|
+
* module a component" is never reachable here. Anaphoric "it"/"this"/"that"
|
|
3434
|
+
* resolves against the session's current FOCUS (threaded in as `focusLabel`)
|
|
3435
|
+
* — never a guess when there's no standing focus. Only ever answers "yes"
|
|
3436
|
+
* (a real fact found) or DECLINES (null) — never a fabricated closed-world
|
|
3437
|
+
* "no", unlike its ownership/general-verb siblings above: a bare copula
|
|
3438
|
+
* ("is this good", "is it done") is the single most common CASUAL English
|
|
3439
|
+
* shape, so a wrong-feeling "no — no remembered fact says X is Y" for
|
|
3440
|
+
* ordinary small talk would be worse than deferring to the ordinary
|
|
3441
|
+
* cascade/orientation nudge that already handles it. */
|
|
3442
|
+
const IS_ADJECTIVE_YESNO_RE = /^(?:is|are|was|were)\s+(.+?)\s+([A-Za-z][\w-]*)[?.!\s]*$/i;
|
|
3443
|
+
const IS_ADJECTIVE_PRONOUN_RE = /^(?:it|this|that)$/i;
|
|
3444
|
+
/** The TEACH-OFFER for a subject IS_ADJECTIVE_YESNO_RE resolved but has no
|
|
3445
|
+
* fact about at all (Tier-5 playtest, cycle 2) — the offered "remember that
|
|
3446
|
+
* X is Y" phrasing is verified in-state: TEACH_PROPERTY_RE's own subject
|
|
3447
|
+
* capture is unbounded multi-word with no lexicon gate on the complement, so
|
|
3448
|
+
* this always actually stores, unlike the bare unwrapped form (which only
|
|
3449
|
+
* reaches TEACH_PROPERTY_RE via BARE_DECLARATIVE_RE's single-token-subject
|
|
3450
|
+
* restriction and would fail here). */
|
|
3451
|
+
const unknownAdjectiveOffer = (subject, adjective) => ({
|
|
3452
|
+
text: `I don't know anything about "${subject}" yet — teach me directly, e.g. "remember that ${subject.toLowerCase()} is ${adjective}".`,
|
|
3453
|
+
replace: true,
|
|
3454
|
+
});
|
|
2819
3455
|
/** WHOLE-STORE recall (CHATBENCH_006 lever 3): "what did i tell you [last time]",
|
|
2820
3456
|
* "what facts do you know", "what do you remember" — list EVERY remembered fact
|
|
2821
3457
|
* (no subject/object term to filter on), cited, higher-trust first. The multi-turn
|
|
@@ -2881,13 +3517,80 @@ function inheritsChain(graph, startId) {
|
|
|
2881
3517
|
* "what kind of thing is an X" reports X's own type (subject-side first).
|
|
2882
3518
|
* Miss-only and run AFTER factAnswer returns null, so it never shadows the
|
|
2883
3519
|
* subject-side answer or a schema hit. Returns { text, replace:true } or null. */
|
|
2884
|
-
async function factReadBack(memoryDir, query, envelope, miss, graph = null) {
|
|
3520
|
+
async function factReadBack(memoryDir, query, envelope, miss, graph = null, focusLabel = null) {
|
|
2885
3521
|
if (!miss) return null;
|
|
2886
3522
|
let normFactTerm;
|
|
2887
3523
|
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
2888
3524
|
const q = String(query).trim();
|
|
3525
|
+
// Tier-5 playtest fix (cycle 4), found live: "actually is the store module
|
|
3526
|
+
// fragile" WALLED — a leading hedge adverb ("actually"/"really"/"honestly",
|
|
3527
|
+
// optionally comma'd) put the sentence one word out of alignment with
|
|
3528
|
+
// IS_ADJECTIVE_YESNO_RE/OWNS_YESNO_RE/OWNS_PASSIVE_YESNO_RE's own anchored
|
|
3529
|
+
// "is|are|was|were|does|did" openers — this session's own three new yes/no
|
|
3530
|
+
// readers, so scoped narrowly to just them (qHedge), not the older
|
|
3531
|
+
// ISA_ASK_RE/WHO_OWNS_RE paths above, which already work without it and
|
|
3532
|
+
// don't need the extra risk of a behavior change. Full leading-connective
|
|
3533
|
+
// tolerance (and/also/so/…) is STACCATO_LEAKED_CONNECTIVES' own separate,
|
|
3534
|
+
// broader territory elsewhere in this file — this is a narrower, adjacent
|
|
3535
|
+
// closed set (hedge adverbs, not coordinators).
|
|
3536
|
+
// "yeah nah" (Tier 6 playtest, §3b dialect axis): the same AU/NZ discourse
|
|
3537
|
+
// opener chat.mjs's own GREET closed set and GREETING_PREAMBLE_RE already
|
|
3538
|
+
// recognize elsewhere, added here too — "yeah nah, is TaskController
|
|
3539
|
+
// fragile" is the SAME one-word-out-of-alignment problem the hedge adverbs
|
|
3540
|
+
// above were fixed for, just a dialect opener instead of a hedge adverb.
|
|
3541
|
+
const qHedge = q.replace(/^(?:actually|really|honestly|yeah\s+nah)\s*,?\s+/i, "");
|
|
2889
3542
|
const rows = await factRows(memoryDir);
|
|
2890
|
-
if (!rows.length)
|
|
3543
|
+
if (!rows.length) {
|
|
3544
|
+
// Tier-5 playtest fix (cycle 2), found live: with TRULY zero facts
|
|
3545
|
+
// remembered yet (a fresh session, nothing taught at all), the early
|
|
3546
|
+
// bail-out below skipped even IS_ADJECTIVE_YESNO_RE's own "subject
|
|
3547
|
+
// completely unknown" TEACH-OFFER further down in this function — "is
|
|
3548
|
+
// the checkout flow deprecated" as someone's genuinely FIRST question
|
|
3549
|
+
// fell to the raw structural wall, unguided. Special-cased here (ahead
|
|
3550
|
+
// of the general empty-memory bail-out every other lane in this function
|
|
3551
|
+
// still relies on) rather than removing the bail-out outright.
|
|
3552
|
+
//
|
|
3553
|
+
// IS_ADJECTIVE_YESNO_RE's own backtracking (no vocabulary restriction on
|
|
3554
|
+
// either capture) means it ALSO syntactically matches shapes that are
|
|
3555
|
+
// NOT a property claim at all — "is a zebra a mammal" (ISA_ASK_RE's own
|
|
3556
|
+
// territory, tried first in the non-empty-rows path below, so never
|
|
3557
|
+
// reached here) and "is there anything bigger" (an existence/staccato-
|
|
3558
|
+
// comparative shape a LATER lane elsewhere in runTurn owns and answers
|
|
3559
|
+
// better than a teach-offer ever could) both regressed real, pinned
|
|
3560
|
+
// tests on first attempt at this fix — caught by running the full suite,
|
|
3561
|
+
// not just the live playtest transcript. Excluded explicitly: ISA_ASK_RE
|
|
3562
|
+
// matches take the SAME priority here they get in the non-empty-rows
|
|
3563
|
+
// path below, and a leading "there" is existential, never a real named
|
|
3564
|
+
// subject a property claim would name.
|
|
3565
|
+
if (!ISA_ASK_RE.test(qHedge)) {
|
|
3566
|
+
const emptyIsAdj = qHedge.match(IS_ADJECTIVE_YESNO_RE);
|
|
3567
|
+
if (emptyIsAdj) {
|
|
3568
|
+
const rawSubject = emptyIsAdj[1].trim();
|
|
3569
|
+
const subject = IS_ADJECTIVE_PRONOUN_RE.test(rawSubject) ? (focusLabel || null) : rawSubject;
|
|
3570
|
+
// Tier 6 playtest: "is logger tested"/"is the store module tested" —
|
|
3571
|
+
// IS_ADJECTIVE_YESNO_RE's own unrestricted backtracking (already flagged
|
|
3572
|
+
// as a recurring risk, see this branch's own docblock above for the
|
|
3573
|
+
// ISA_ASK_RE/"is there" exclusions found the SAME way) ALSO matches
|
|
3574
|
+
// "tested" as if it were a free-form property adjective — but "tested"/
|
|
3575
|
+
// "covered"/"untested"/"uncovered" are REAL structural relation words
|
|
3576
|
+
// (PASSIVE_PARTICIPLE_TO_KIND/QUALIFIERS, ask-vocab.mjs) with an actual
|
|
3577
|
+
// graph-computable meaning ask()'s own grammar already resolved
|
|
3578
|
+
// (envelope.parsed stands — a genuine "tests" reverse-relation
|
|
3579
|
+
// traversal, hit or honest empty). Offering "I don't know that yet —
|
|
3580
|
+
// teach me" here would silently DISCARD a real, honest structural
|
|
3581
|
+
// answer in favor of an irrelevant memory teach-offer — the opposite
|
|
3582
|
+
// failure from every other exclusion in this function (a wrong
|
|
3583
|
+
// OVER-eager offer, not a missed one). Declines only when a real parse
|
|
3584
|
+
// already stood; "is the checkout flow deprecated" (this branch's own
|
|
3585
|
+
// ORIGINAL T8 target — "deprecated" has no structural meaning at all)
|
|
3586
|
+
// has no envelope.parsed to defer to, so it is untouched.
|
|
3587
|
+
if (subject && !/^there\b/i.test(subject) && !envelope?.parsed) {
|
|
3588
|
+
return unknownAdjectiveOffer(subject, emptyIsAdj[2].trim().toLowerCase());
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
return null;
|
|
3593
|
+
}
|
|
2891
3594
|
const isa = rows.filter((f) => ISA_PREDICATES.has(f.predicate));
|
|
2892
3595
|
const byTrust = (a, b) => b.trust - a.trust;
|
|
2893
3596
|
const renderMany = (hits) => {
|
|
@@ -2993,6 +3696,181 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null) {
|
|
|
2993
3696
|
return renderMany(hits);
|
|
2994
3697
|
}
|
|
2995
3698
|
|
|
3699
|
+
// (a2b) OWNERSHIP yes/no — "does/did <Name> own/maintain <X>": Tier-5
|
|
3700
|
+
// playtest fix, found live — WHO_OWNS_RE only ever answered the OPEN "who
|
|
3701
|
+
// owns X" form; a direct yes/no claim about a specific (owner, thing) pair
|
|
3702
|
+
// ("does margo maintain the tasks handler") fell all the way through to the
|
|
3703
|
+
// structural code-graph wall, even right after teaching exactly that fact,
|
|
3704
|
+
// because GENERAL_VERB_QUERY_EXCLUDE_RE deliberately stands the general-verb
|
|
3705
|
+
// reader down for "own"/"maintain" (they mint a DIFFERENT predicate,
|
|
3706
|
+
// mgx:own/mgx:maintain, than this frame's own OWNED_BY_PREDICATE) — this is
|
|
3707
|
+
// that missing specific reader. Same closed-world convention as (a3)'s
|
|
3708
|
+
// general-verb yes/no just below: a hit answers "yes", no stored fact
|
|
3709
|
+
// answers a definite "no" (never a guessed owner name, unlike the OPEN "who
|
|
3710
|
+
// owns" form above, which stays an honest miss rather than guess WHO).
|
|
3711
|
+
const ownsYN = qHedge.match(OWNS_YESNO_RE);
|
|
3712
|
+
if (ownsYN) {
|
|
3713
|
+
const [, ownerRaw, thingRaw] = ownsYN;
|
|
3714
|
+
const ownerVariants = factTermVariants(normFactTerm, ownerRaw.trim());
|
|
3715
|
+
const thingVariants = factTermVariants(normFactTerm, thingRaw.replace(/^an?\s+/i, "").trim());
|
|
3716
|
+
const hit = rows
|
|
3717
|
+
.filter((f) => f.predicate === OWNED_BY_PREDICATE && thingVariants.has(f.subject) && ownerVariants.has(f.object))
|
|
3718
|
+
.sort(byTrust)[0];
|
|
3719
|
+
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
3720
|
+
return {
|
|
3721
|
+
text: `no — no remembered fact says ${ownerRaw.trim().toLowerCase()} owns/maintains ${thingRaw.trim()}.`,
|
|
3722
|
+
replace: true,
|
|
3723
|
+
};
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
// (a2b-ii) PASSIVE ownership yes/no — "is/are/was/were <X> owned by <Name>"
|
|
3727
|
+
// (Tier-5 playtest, cycle 2): same OWNED_BY_PREDICATE facts as (a2b) above,
|
|
3728
|
+
// just the passive phrasing — "is TaskController owned by sam" found live
|
|
3729
|
+
// to WALL entirely (no recognizer at all, teach OR read, for the passive
|
|
3730
|
+
// shape) even right after teaching that exact fact via OWNS_PASSIVE_TEACH_RE.
|
|
3731
|
+
// Checked BEFORE (a2c)'s adjective reader, which would otherwise also match
|
|
3732
|
+
// this shape (backtracking "owned by" into its subject and the owner name
|
|
3733
|
+
// into its adjective slot) and silently decline instead of answering.
|
|
3734
|
+
const ownsPassiveYN = qHedge.match(OWNS_PASSIVE_YESNO_RE);
|
|
3735
|
+
if (ownsPassiveYN) {
|
|
3736
|
+
const [, thingRaw, ownerRaw] = ownsPassiveYN;
|
|
3737
|
+
const thingVariants = factTermVariants(normFactTerm, thingRaw.replace(/^an?\s+/i, "").trim());
|
|
3738
|
+
const ownerVariants = factTermVariants(normFactTerm, ownerRaw.trim());
|
|
3739
|
+
const hit = rows
|
|
3740
|
+
.filter((f) => f.predicate === OWNED_BY_PREDICATE && thingVariants.has(f.subject) && ownerVariants.has(f.object))
|
|
3741
|
+
.sort(byTrust)[0];
|
|
3742
|
+
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
3743
|
+
return {
|
|
3744
|
+
text: `no — no remembered fact says ${thingRaw.trim().toLowerCase()} is owned by ${ownerRaw.trim()}.`,
|
|
3745
|
+
replace: true,
|
|
3746
|
+
};
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3749
|
+
// (a2c) PROPERTY yes/no — "is/are/was/were <X> <adjective>": Tier-5 playtest
|
|
3750
|
+
// fix, found live — "remember that the logger module is deprecated" taught a
|
|
3751
|
+
// real mgx:hasProperty fact, but there was no direct-question reader for it
|
|
3752
|
+
// AT ALL (only presuppositionNudge's own narrow "why does X still Y" embeds
|
|
3753
|
+
// this same check) — "is the logger deprecated" fell straight to the
|
|
3754
|
+
// structural wall. Checks BOTH shapes a taught "<X> is <adjective>" can land
|
|
3755
|
+
// as (mirrors presuppositionNudge's own dual check, above): the teach lane's
|
|
3756
|
+
// mgx:hasProperty fact, or — when the adjective is a known ACE-OWL lexicon
|
|
3757
|
+
// data-property word — the ACE grammar's own tmct:<adjective> "true" triple.
|
|
3758
|
+
// "it"/"this"/"that" resolve against `focusLabel` — a bare pronoun with no
|
|
3759
|
+
// standing focus declines (null), same discipline STACCATO_PRONOUN_RE uses.
|
|
3760
|
+
const isAdj = qHedge.match(IS_ADJECTIVE_YESNO_RE);
|
|
3761
|
+
if (isAdj) {
|
|
3762
|
+
const rawSubject = isAdj[1].trim();
|
|
3763
|
+
const subject = IS_ADJECTIVE_PRONOUN_RE.test(rawSubject) ? (focusLabel || null) : rawSubject;
|
|
3764
|
+
const adjective = isAdj[2].trim().toLowerCase();
|
|
3765
|
+
if (subject) {
|
|
3766
|
+
const subjVariants = factTermVariants(normFactTerm, subject);
|
|
3767
|
+
const propertyMatch = (f) => (f.predicate === HAS_PROPERTY_PREDICATE && normFactTerm(f.object) === adjective)
|
|
3768
|
+
|| (f.predicate === `tmct:${adjective}` && f.object === "true");
|
|
3769
|
+
// Same head-word fallback as factAnswer's "(c) what do you know about"
|
|
3770
|
+
// lane: a taught subject is often a real noun PHRASE ("logger module"),
|
|
3771
|
+
// shortened in the natural follow-up ("is the logger deprecated") — an
|
|
3772
|
+
// exact-variant miss on its own, on a whole word (length >= 4, the same
|
|
3773
|
+
// floor used elsewhere) shared between the query subject and the fact's
|
|
3774
|
+
// own subject.
|
|
3775
|
+
const subjWords = normFactTerm(subject).split(/\s+/).filter((w) => w.length >= 4 && !GENERIC_ENTITY_WORDS.has(w));
|
|
3776
|
+
const wordOverlap = (f) => subjWords.some((w) => new Set(String(f.subject || "").split(/\s+/)).has(w));
|
|
3777
|
+
const subjectMatch = (f) => subjVariants.has(f.subject) || (subjWords.length && wordOverlap(f));
|
|
3778
|
+
const hit = rows.filter((f) => subjectMatch(f) && propertyMatch(f)).sort(byTrust)[0];
|
|
3779
|
+
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
3780
|
+
// no hit on THIS property — never a guessed "no" (see
|
|
3781
|
+
// IS_ADJECTIVE_YESNO_RE's own docblock for why this stays silent on a
|
|
3782
|
+
// truth claim, unlike its ownership/general-verb siblings above). But a
|
|
3783
|
+
// subject we DO know something else about ("the logger" has a
|
|
3784
|
+
// deprecated-fact, just not a fast-fact) still deserves an honest, named
|
|
3785
|
+
// receipt — never a bare wall — mirroring factAnswer's own established
|
|
3786
|
+
// convention for a known-subject/wrong-predicate miss ("I don't have
|
|
3787
|
+
// any 'X' facts about Y"). The SAME subjectMatch (exact-variant OR
|
|
3788
|
+
// head-word overlap) decides "known", so a shortened/article-led
|
|
3789
|
+
// subject that found its fact via the overlap fallback is recognized
|
|
3790
|
+
// as known too. A subject with NO known facts at all falls through
|
|
3791
|
+
// undecided — there's nothing honest to say beyond the ordinary
|
|
3792
|
+
// cascade's own miss/orientation nudge.
|
|
3793
|
+
//
|
|
3794
|
+
// Tier 6 playtest: gated on `!envelope?.parsed`, the SAME guard this
|
|
3795
|
+
// function's empty-memory branch above just added, for the identical
|
|
3796
|
+
// reason — "is the logger tested" (after teaching an UNRELATED
|
|
3797
|
+
// "logger... is deprecated" fact) used to return "I don't have a fact
|
|
3798
|
+
// saying the logger is tested" here, discarding a REAL structural
|
|
3799
|
+
// answer ("No tests cover logger") for a word ("tested") that already
|
|
3800
|
+
// has genuine graph-computable meaning. A subject with a KNOWN taught
|
|
3801
|
+
// fact under some OTHER, non-structural property (the common,
|
|
3802
|
+
// originally-intended case here) still gets this receipt exactly as
|
|
3803
|
+
// before, since envelope.parsed is null for those adjectives.
|
|
3804
|
+
if (rows.some(subjectMatch) && !envelope?.parsed) {
|
|
3805
|
+
return { text: `I don't have a fact saying ${subject.toLowerCase()} is ${adjective}.`, replace: true };
|
|
3806
|
+
}
|
|
3807
|
+
// Tier-5 playtest fix (cycle 2), found live: "is the checkout flow
|
|
3808
|
+
// deprecated" as a genuinely FIRST-EVER question about a subject tmct
|
|
3809
|
+
// has never heard of (no fact at all, not even under a different
|
|
3810
|
+
// property) fell through to the raw structural wall, unguided — the
|
|
3811
|
+
// exact "honest 'I don't know that yet' offers to learn" case
|
|
3812
|
+
// SKILL_CHAT_PLAYTEST.md's Tier 5 (§3) itself names. The offered
|
|
3813
|
+
// phrasing is the SAME verified "remember that X is Y" wrapped form
|
|
3814
|
+
// TEACH_PROPERTY_RE actually accepts (arbitrary-length subject, no
|
|
3815
|
+
// lexicon gate on the complement) — never the bare unwrapped form,
|
|
3816
|
+
// which TEACH_PROPERTY_RE only reaches via BARE_DECLARATIVE_RE's own
|
|
3817
|
+
// single-token-subject restriction and would fail for a multi-word
|
|
3818
|
+
// subject like this one. Same helper (unknownAdjectiveOffer) the
|
|
3819
|
+
// empty-memory special-case above this function's own rows.length
|
|
3820
|
+
// bail-out reuses, so the two paths can never disagree on wording.
|
|
3821
|
+
//
|
|
3822
|
+
// Tier 6 playtest: same `!envelope?.parsed` guard as just above — a
|
|
3823
|
+
// subject known only under an UNRELATED property (e.g. "deprecated")
|
|
3824
|
+
// must not offer to teach "tested" when ask()'s own grammar already
|
|
3825
|
+
// resolved it structurally.
|
|
3826
|
+
if (!envelope?.parsed) return unknownAdjectiveOffer(subject, adjective);
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
// (a3) GENERAL VERB-TO-PREDICATE direct-question retrieval (item 5, this
|
|
3831
|
+
// session): a taught general-verb fact ("margo eats ribs") answered back
|
|
3832
|
+
// directly. Yes/no form matches the taught triple EXACTLY (subject +
|
|
3833
|
+
// predicate + object, via the SAME factTermVariants/normFactTerm matching
|
|
3834
|
+
// WHO_OWNS_RE just used above) — no match is an honest, closed-world "no",
|
|
3835
|
+
// never a guess. Open form lists every stored fact row for {subject,
|
|
3836
|
+
// predicate} regardless of object.
|
|
3837
|
+
const genYN = q.match(GENERAL_VERB_YESNO_RE);
|
|
3838
|
+
if (genYN && !GENERAL_VERB_ANYWHERE_EXCLUDE_RE.test(q)) {
|
|
3839
|
+
const [, subjectRaw, verbRaw, objectRaw] = genYN;
|
|
3840
|
+
const verb = verbRaw.toLowerCase();
|
|
3841
|
+
if (!GENERAL_VERB_EXCLUDE_RE.test(verb) && !GENERAL_VERB_QUERY_EXCLUDE_RE.test(verb)) {
|
|
3842
|
+
const subject = subjectRaw.trim();
|
|
3843
|
+
const object = objectRaw.replace(/^an?\s+/i, "").trim();
|
|
3844
|
+
if (subject && object) {
|
|
3845
|
+
const predicate = await generalVerbPredicate(verb);
|
|
3846
|
+
const subjVariants = factTermVariants(normFactTerm, subject);
|
|
3847
|
+
const objVariants = factTermVariants(normFactTerm, object);
|
|
3848
|
+
const hit = rows
|
|
3849
|
+
.filter((f) => f.predicate === predicate && subjVariants.has(f.subject) && objVariants.has(f.object))
|
|
3850
|
+
.sort(byTrust)[0];
|
|
3851
|
+
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true, generalVerbQuery: true };
|
|
3852
|
+
return {
|
|
3853
|
+
text: `no — no remembered fact says ${subject.toLowerCase()} ${predicatePhrase(predicate)} ${object}.`,
|
|
3854
|
+
replace: true, generalVerbQuery: true,
|
|
3855
|
+
};
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
const genOpen = q.match(GENERAL_VERB_OPEN_RE);
|
|
3860
|
+
if (genOpen && !GENERAL_VERB_ANYWHERE_EXCLUDE_RE.test(q)) {
|
|
3861
|
+
const [, subjectRaw, verbRaw] = genOpen;
|
|
3862
|
+
const verb = verbRaw.toLowerCase();
|
|
3863
|
+
if (!GENERAL_VERB_EXCLUDE_RE.test(verb) && !GENERAL_VERB_QUERY_EXCLUDE_RE.test(verb)) {
|
|
3864
|
+
const subject = subjectRaw.trim();
|
|
3865
|
+
if (subject) {
|
|
3866
|
+
const predicate = await generalVerbPredicate(verb);
|
|
3867
|
+
const subjVariants = factTermVariants(normFactTerm, subject);
|
|
3868
|
+
const hits = rows.filter((f) => f.predicate === predicate && subjVariants.has(f.subject)).sort(byTrust);
|
|
3869
|
+
if (hits.length) return { ...renderMany(hits), generalVerbQuery: true };
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
|
|
2996
3874
|
// (b) RECALL — "what did i tell you about X": every remembered fact mentioning X.
|
|
2997
3875
|
const told = q.match(TOLD_ABOUT_RE);
|
|
2998
3876
|
if (told) {
|
|
@@ -3326,14 +4204,42 @@ const BARE_WHATIS_RE = /^what\s+(?:is|are)\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
|
3326
4204
|
* question asks about — from the parse when present, else recognized directly
|
|
3327
4205
|
* via BARE_WHATIS_RE (article optional — see its own docblock for why that's
|
|
3328
4206
|
* safe here even though the grammar's own T5 keeps the article mandatory).
|
|
3329
|
-
* Null when the line isn't such a form.
|
|
4207
|
+
* Null when the line isn't such a form. Seonix Batch 2 Fix 3: a curated trailing
|
|
4208
|
+
* scope clause ("what is a Module in this graph") is stripped off the captured
|
|
4209
|
+
* term the same way grammar.mjs's T5 does (stripTrailingScopeFiller,
|
|
4210
|
+
* ask-vocab.mjs) — the envelope.parsed.object branch above already carries a
|
|
4211
|
+
* trimmed term when it came from that template, so the strip here only needs to
|
|
4212
|
+
* cover this function's own regex fallback. */
|
|
3330
4213
|
function metaTermOf(query, envelope) {
|
|
3331
4214
|
if (envelope?.parsed?.shape === "meta" && envelope.parsed.object) return envelope.parsed.object;
|
|
3332
4215
|
const q = String(query).trim();
|
|
3333
4216
|
const m = q.match(BARE_WHATIS_RE)
|
|
3334
4217
|
|| q.match(/^what\s+(?:does|do)\s+(?:an?\s+)?(.+?)\s+means?[?.!\s]*$/i)
|
|
3335
4218
|
|| q.match(/^define\s+(?:an?\s+)?(.+?)[?.!\s]*$/i);
|
|
3336
|
-
return m ? m[1].trim() : null;
|
|
4219
|
+
return m ? stripTrailingScopeFiller(m[1].trim()) : null;
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
/** The TEACH-OFFER line for a term that's genuinely unknown everywhere (Tier-5
|
|
4223
|
+
* playtest fix): "I don't know 'X' yet — teach me directly, e.g. …". The
|
|
4224
|
+
* concrete example is worded by WORD COUNT, verified in-state
|
|
4225
|
+
* (SKILL_CHAT_PLAYTEST.md §4's own rule) — an unwrapped bare declarative
|
|
4226
|
+
* only stores for a single-token subject (BARE_DECLARATIVE_RE's own scope);
|
|
4227
|
+
* the wrapped "remember X is a Y" form tolerates up to a two-token subject
|
|
4228
|
+
* (unknownSubjectFallback's UNKNOWN_SUBJECT_RE). A 3+-word term fits
|
|
4229
|
+
* neither shape — never offer a concrete example that would itself fail,
|
|
4230
|
+
* the plain nudge to teach it still guides, honestly. Shared by runTurn's
|
|
4231
|
+
* own "what is X" miss nudge and factAnswer's "what do you know about X"
|
|
4232
|
+
* miss nudge, below, so the two can never disagree on wording. */
|
|
4233
|
+
function unknownVocabTermOffer(term) {
|
|
4234
|
+
const article = /^[aeiou]/i.test(term) ? "an" : "a";
|
|
4235
|
+
const words = term.trim().split(/\s+/);
|
|
4236
|
+
const remember = `remember ${term} is ${article} <thing>`;
|
|
4237
|
+
const example = words.length === 1
|
|
4238
|
+
? `"${term} is ${article} <thing>" or "${remember}"`
|
|
4239
|
+
: words.length === 2
|
|
4240
|
+
? `"${remember}"`
|
|
4241
|
+
: null;
|
|
4242
|
+
return `I don't know "${term}" yet — teach me directly${example ? `, e.g. ${example}` : ` (e.g. "remember <name> is ${article} <thing>")`}.`;
|
|
3337
4243
|
}
|
|
3338
4244
|
|
|
3339
4245
|
/** The curated SEON definition to PREFER for a "what is a <lexicon term>", or null.
|
|
@@ -3387,6 +4293,18 @@ async function curatedDefinitionAnswer(query, envelope, { memoryDir, lexicon })
|
|
|
3387
4293
|
// SHORTHAND_CONTRACTIONS above: scoped locally to the lane that owns the word.
|
|
3388
4294
|
// Word-boundary matched so "hotel"/"intel" are untouched.
|
|
3389
4295
|
const VAGUE_TOUCH_TEL_RE = /\btel\b/i;
|
|
4296
|
+
// "abut" -> "about" (Tier 6 playtest, §3b typo axis): a one-letter-dropped
|
|
4297
|
+
// typo of THIS lane's own anchor word ("what abut imports" used to miss the
|
|
4298
|
+
// "what about …" regex entirely and search for a module literally named
|
|
4299
|
+
// "abut" instead). "about" is real English on its own (a genuine word) but is
|
|
4300
|
+
// not itself part of ask.mjs's code-graph grammar (VERB_TO_KIND/ENTITY_TO_TYPE/
|
|
4301
|
+
// anchor words) — same reasoning as VAGUE_TOUCH_TEL_RE just above, so this
|
|
4302
|
+
// stays a local, lane-scoped replace rather than a shared MISSPELLINGS entry
|
|
4303
|
+
// (test/ask-vocab.test.mjs enforces every correction TABLE value is grammar-
|
|
4304
|
+
// owned; a bare discourse word like "about" fails that gate on purpose).
|
|
4305
|
+
// Word-boundary matched so a real identifier merely containing "abut" (rare,
|
|
4306
|
+
// but e.g. "rebuttal") is untouched.
|
|
4307
|
+
const VAGUE_TOUCH_ABUT_RE = /\babut\b/i;
|
|
3390
4308
|
/** "explain X" / "please explain X" / "kindly explain X" / "explain X to me" /
|
|
3391
4309
|
* "explain X please" — a bare vague-touch shape, sibling of WHAT_ABOUT_RE
|
|
3392
4310
|
* above. Named (not inlined) so both vagueTouchTermOf (term extraction) and
|
|
@@ -3407,6 +4325,7 @@ function vagueTouchTermOf(query) {
|
|
|
3407
4325
|
// bridge frame), breaking this very regex.
|
|
3408
4326
|
let q = correctMisspellings(String(query).trim());
|
|
3409
4327
|
q = q.replace(VAGUE_TOUCH_TEL_RE, "tell");
|
|
4328
|
+
q = q.replace(VAGUE_TOUCH_ABUT_RE, "about");
|
|
3410
4329
|
q = applyPreambleFrames(q);
|
|
3411
4330
|
const m = q.match(/^(?:kindly\s+)?tell me about\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i)
|
|
3412
4331
|
|| q.match(/^(?:(?:and|so|but|ok|okay|now|then|kindly)\s+)*what about\s+(?:an?\s+|the\s+)?(.+?)(?:\s+then|\s+though)?[?.!\s]*$/i)
|
|
@@ -3518,8 +4437,22 @@ function relationTermOf(query, envelope) {
|
|
|
3518
4437
|
* can never parse and always misses) — a drill-down chain that opens with
|
|
3519
4438
|
* "describe X" (the README's own example) used to dead-end on the very next
|
|
3520
4439
|
* "what about it"/"what about Y" turn. */
|
|
4440
|
+
// Bug F point 4 (operator follow-up request): "please tell me X" (no "about")
|
|
4441
|
+
// answers like "describe X"/"what is X" — the "tell me" branch's own "about"
|
|
4442
|
+
// is now OPTIONAL, so "please tell me Widget" reaches the same rescue "please
|
|
4443
|
+
// tell me about Widget" already did. "describe"/"what(?:'s|\s+is)? about" stay
|
|
4444
|
+
// unchanged (describe never took "about" at all; the "what about" branch
|
|
4445
|
+
// still requires it — a bare "what X" is BARE_WHATIS_RE's own territory, not
|
|
4446
|
+
// this lane's, and folding it in here would risk double-claiming that shape).
|
|
4447
|
+
// Note the trailing \s+ moved INSIDE each alternation branch (rather than one
|
|
4448
|
+
// shared \s+ after the whole group): making "about" optional inside the "tell
|
|
4449
|
+
// me" branch means that branch's own separator is sometimes owned by "me\s+"
|
|
4450
|
+
// and sometimes by "about\s+" — a single external \s+ double-counted the
|
|
4451
|
+
// separator when "about" fired (swallowing the one real space and then
|
|
4452
|
+
// requiring a second one that was never there, an always-null regex found
|
|
4453
|
+
// live while testing this fix).
|
|
3521
4454
|
const DESCRIBE_WRAPPER_RE =
|
|
3522
|
-
/^(?:(?:can|could|would)\s+you\s+(?:please\s+)?|please\s+)?(?:tell\s+me\s+(?:more\s+)?about
|
|
4455
|
+
/^(?:(?:can|could|would)\s+you\s+(?:please\s+)?|please\s+)?(?:tell\s+me\s+(?:more\s+)?(?:about\s+)?|describe\s+|what(?:'s|\s+is)?\s+about\s+)(.+?)(?:\s+for\s+me)?(?:\s+please)?\s*\??$/i;
|
|
3523
4456
|
|
|
3524
4457
|
/** Bare focus pronouns this lane resolves against the STANDING focus (0.9.13
|
|
3525
4458
|
* Tier-1 playtest) — "describe that" / "tell me about it" after a prior turn
|
|
@@ -3542,14 +4475,77 @@ const DESCRIBE_PRONOUN_RE = /^(?:it|that|this|those|them)$/i;
|
|
|
3542
4475
|
* unaffected either way. */
|
|
3543
4476
|
const STACCATO_PRONOUN_RE = /^(?:and|also|so|then|now)\s+(it|that|this|those|them)(?:\s+ones?)?\s*\??$/i;
|
|
3544
4477
|
|
|
3545
|
-
|
|
3546
|
-
|
|
4478
|
+
/** Tier 6 playtest: "describe the logger module"/"describe the Task class" —
|
|
4479
|
+
* dispatchTool("tmct_describe") resolves its `symbol` arg via codegraph.mjs's
|
|
4480
|
+
* resolveSymbol, a separate, simpler path/basename matcher with NO article- or
|
|
4481
|
+
* grain-word tolerance. A first attempt routed this whole free-text `term`
|
|
4482
|
+
* through resolveEntity/resolveObject instead (which DOES have that tolerance,
|
|
4483
|
+
* just added above) — reverted live, found via this same playtest cycle's own
|
|
4484
|
+
* regression run: resolveObject's tier-3 ANY-overlap fallback is tuned for
|
|
4485
|
+
* near-path/near-symbol terms, not arbitrary English sentences, and a genuine
|
|
4486
|
+
* English article ("a", "the") can itself be a real one-character path
|
|
4487
|
+
* component of some fixture module ("a.mjs") — "tell me A JOKE" tier-3-matched
|
|
4488
|
+
* that module by the shared bare "a" alone (test/sessions.test.mjs's own guard
|
|
4489
|
+
* test caught it: a turn meant to fall through as an honest grammar miss
|
|
4490
|
+
* instead silently "described" an unrelated module). Scoped down to ONLY ever
|
|
4491
|
+
* attempt a resolution when the term carries an EXPLICIT trailing grain word
|
|
4492
|
+
* (module/class/function/method/…, ENTITY_TO_TYPE's own closed table) — the
|
|
4493
|
+
* class-narrowed pool that then searches is both far smaller and still
|
|
4494
|
+
* requires the head noun to actually match a stem, so it stays safe; a bare
|
|
4495
|
+
* "the X"/"an X" or ordinary sentence (no grain word) gets NO rescue attempt
|
|
4496
|
+
* at all and falls through to the untouched, always-safe resolveSymbol path,
|
|
4497
|
+
* exactly as before this fix. */
|
|
4498
|
+
const DESCRIBE_GRAIN_WORD_RE = new RegExp(
|
|
4499
|
+
`^(?:(?:the|a|an)\\s+)?(.+?)\\s+(${Object.keys(ENTITY_TO_TYPE).join("|")})$`, "i",
|
|
4500
|
+
);
|
|
4501
|
+
async function describeGrainRescue(graph, term) {
|
|
4502
|
+
if (!graph) return null;
|
|
4503
|
+
const m = String(term || "").trim().match(DESCRIBE_GRAIN_WORD_RE);
|
|
4504
|
+
if (!m) return null;
|
|
4505
|
+
const [, head, grainWord] = m;
|
|
4506
|
+
const expectedClass = ENTITY_TO_TYPE[grainWord.toLowerCase()];
|
|
4507
|
+
if (!head?.trim() || !expectedClass) return null;
|
|
4508
|
+
try {
|
|
4509
|
+
const { resolveObject } = await import("./ask.mjs");
|
|
4510
|
+
const r = resolveObject(graph, head.trim(), { expectedClass });
|
|
4511
|
+
if (r?.match?.id && !r.ambiguous) return { id: r.match.id, label: r.match.label };
|
|
4512
|
+
} catch { /* tolerated */ }
|
|
4513
|
+
return null;
|
|
4514
|
+
}
|
|
4515
|
+
|
|
4516
|
+
async function describeWrapperAnswer(query, { config, source, focus, graph }) {
|
|
4517
|
+
// Tier 6 playtest: this lane is the LAST-RESORT rescue (4d, tried after every
|
|
4518
|
+
// earlier lane declines on the ORIGINAL query) — but it tested its own
|
|
4519
|
+
// DESCRIBE_WRAPPER_RE against the RAW, un-normalized text, so a preamble an
|
|
4520
|
+
// earlier lane (relationForceAnswer/vagueTouchTermOf) already knows how to
|
|
4521
|
+
// strip ("ok cool, what about the TaskController" — relationForceAnswer
|
|
4522
|
+
// correctly declines since "TaskController" isn't an enumerable RELATION_TERM,
|
|
4523
|
+
// but never hands its own stripped text forward) reappeared here, unstripped,
|
|
4524
|
+
// and broke DESCRIBE_WRAPPER_RE's own anchor. applyPreambleFrames is the same
|
|
4525
|
+
// general-purpose, closed, idempotent pass every other lane in this file
|
|
4526
|
+
// already runs first.
|
|
4527
|
+
const q = applyPreambleFrames(String(query || "").trim());
|
|
3547
4528
|
const m = DESCRIBE_WRAPPER_RE.exec(q) || STACCATO_PRONOUN_RE.exec(q);
|
|
3548
4529
|
let term = m?.[1]?.trim();
|
|
3549
4530
|
if (!term) return null;
|
|
3550
4531
|
if (DESCRIBE_PRONOUN_RE.test(term)) {
|
|
3551
4532
|
if (!focus?.label) return null; // no standing focus to resolve against — honest decline
|
|
3552
4533
|
term = focus.label;
|
|
4534
|
+
} else {
|
|
4535
|
+
const rescued = await describeGrainRescue(graph, term);
|
|
4536
|
+
if (rescued?.label) {
|
|
4537
|
+
term = rescued.label;
|
|
4538
|
+
} else {
|
|
4539
|
+
// Tier 6 playtest: "what about the TaskController" (no grain word, just a
|
|
4540
|
+
// bare article) — resolveSymbol (codegraph.mjs) has no component/overlap
|
|
4541
|
+
// tier at all, only exact/endsWith/basename/includes checks, so a leading
|
|
4542
|
+
// "the"/"a"/"an" is pure NOISE here (unlike resolveObject's looser tiers,
|
|
4543
|
+
// there is no accidental-match risk this could introduce — stripping it
|
|
4544
|
+
// only ever REMOVES characters no real label ever contains as a match
|
|
4545
|
+
// signal). "TaskController" resolves exactly where "the TaskController"
|
|
4546
|
+
// didn't.
|
|
4547
|
+
term = term.replace(/^(?:the|a|an)\s+/i, "");
|
|
4548
|
+
}
|
|
3553
4549
|
}
|
|
3554
4550
|
try {
|
|
3555
4551
|
const text = await dispatchTool("tmct_describe", { symbol: term }, { config, source });
|
|
@@ -4008,16 +5004,28 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
4008
5004
|
// guarantees a real answer before it defers, never stranding the turn on a
|
|
4009
5005
|
// worse outcome — see isStaccatoPronounNoFocus's own docblock for the same
|
|
4010
5006
|
// discipline).
|
|
5007
|
+
// Tier-5 playtest fix (this session): "is it deprecated" is EXACTLY the same
|
|
5008
|
+
// race BUG 2 (above) fixed for "what is john" — 3 words, no code-ish token,
|
|
5009
|
+
// so isConversationalCandidate would otherwise win unconditionally and a
|
|
5010
|
+
// just-taught property fact ("logger module is deprecated") becomes
|
|
5011
|
+
// unreachable the moment it's asked back about with a short pronoun/bare
|
|
5012
|
+
// form ("is it deprecated" / "is the logger deprecated"). Widened the SAME
|
|
5013
|
+
// divert-only-on-a-real-hit gate to also try IS_ADJECTIVE_YESNO_RE shapes —
|
|
5014
|
+
// factAnswer itself declines for this shape (no metaTerm), so the only
|
|
5015
|
+
// change in practice is that factReadBack's (a2c) property lane gets a
|
|
5016
|
+
// chance to run before the orientation card claims the turn.
|
|
5017
|
+
const bareWhatisShape = BARE_WHATIS_RE.test(String(query).trim());
|
|
5018
|
+
const isAdjectiveShape = IS_ADJECTIVE_YESNO_RE.test(String(query).trim());
|
|
4011
5019
|
let bareMetaHit = null;
|
|
4012
|
-
if (isConversationalCandidate && memoryDir &&
|
|
5020
|
+
if (isConversationalCandidate && memoryDir && (bareWhatisShape || isAdjectiveShape)) {
|
|
4013
5021
|
bareMetaHit = (await factAnswer(memoryDir, query, envelope, miss))
|
|
4014
|
-
?? (await factReadBack(memoryDir, query, envelope, miss, graph));
|
|
5022
|
+
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label));
|
|
4015
5023
|
}
|
|
4016
5024
|
if (bareMetaHit) {
|
|
4017
5025
|
answer = bareMetaHit.replace ? bareMetaHit.text : `${answer}\n${bareMetaHit.text}`;
|
|
4018
5026
|
via = "fact"; recordMiss = false; handled = true;
|
|
4019
5027
|
if (bareMetaHit.pending) factPending = bareMetaHit.pending;
|
|
4020
|
-
note(trace, "lane: (2b) BARE META FACT — \"what is X\" (no article) resolved to a remembered fact before the conversational catch-all could claim it");
|
|
5028
|
+
note(trace, "lane: (2b) BARE META FACT — \"what is X\" (no article) / \"is X <adjective>\" resolved to a remembered fact before the conversational catch-all could claim it");
|
|
4021
5029
|
note(trace, "source: .tmct/memory Facts (see /memory for provenance per line)");
|
|
4022
5030
|
} else if (isConversationalCandidate) {
|
|
4023
5031
|
// A conversational miss (a greeting, "what can you do", a very short non-code
|
|
@@ -4053,7 +5061,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
4053
5061
|
// first (factAnswer), then the reverse-membership read-back (factReadBack) so an
|
|
4054
5062
|
// asserted "every X is a Y" answers "what is a Y" too.
|
|
4055
5063
|
const fact = (await factAnswer(memoryDir, query, envelope, miss))
|
|
4056
|
-
?? (await factReadBack(memoryDir, query, envelope, miss, graph));
|
|
5064
|
+
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label));
|
|
4057
5065
|
if (fact) {
|
|
4058
5066
|
answer = fact.replace ? fact.text : `${answer}\n${fact.text}`;
|
|
4059
5067
|
via = "fact";
|
|
@@ -4061,6 +5069,15 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
4061
5069
|
if (fact.pending) factPending = fact.pending; // a truncated fact list → paginable remainder
|
|
4062
5070
|
note(trace, `lane: (3) memory facts — factAnswer/factReadBack matched (memoryDir=${memoryDir})`);
|
|
4063
5071
|
note(trace, "source: .tmct/memory Facts (see /memory for provenance per line)");
|
|
5072
|
+
// Goal-line fix (item 5 follow-up, this session): mirrors the TEACH lane's
|
|
5073
|
+
// own goal revision just below (Bug 3 point 4) — `deduced` was computed
|
|
5074
|
+
// WAY above off envelope.parsed alone, but a general-verb direct question
|
|
5075
|
+
// ("does margo eat ribs") never parses as a structural graph query at all,
|
|
5076
|
+
// so it either landed on an unrelated GOAL_BY_KIND guess or nothing.
|
|
5077
|
+
if (fact.generalVerbQuery) {
|
|
5078
|
+
deduced = "look up a taught fact about a subject/verb/object";
|
|
5079
|
+
note(trace, `goal: ${deduced} (revised — a general-verb direct-question fact lookup answered this turn)`);
|
|
5080
|
+
}
|
|
4064
5081
|
} else if (miss) {
|
|
4065
5082
|
// W2: after the honest miss is composed, consult the folded-session memory. A
|
|
4066
5083
|
// relevant enough block ANSWERS — recalled Q/A framed + cited first, with the
|
|
@@ -4246,7 +5263,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
4246
5263
|
// for what would otherwise become the generic wall, never a competing route:
|
|
4247
5264
|
// it only claims the turn if /describe actually resolves the captured term.
|
|
4248
5265
|
if (miss && recordMiss && via === "composed") {
|
|
4249
|
-
const described = await describeWrapperAnswer(query, { config, source, focus: newFocus });
|
|
5266
|
+
const described = await describeWrapperAnswer(query, { config, source, focus: newFocus, graph });
|
|
4250
5267
|
if (described) {
|
|
4251
5268
|
answer = described.text; via = "describe"; recordMiss = false;
|
|
4252
5269
|
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");
|
|
@@ -4273,6 +5290,47 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
4273
5290
|
answer = `${answer}\n(this repo has no code graph — for structure, point me at a \`.tmct/graph.json\` with \`--repo <path>\` or run \`npm run example:mini\`; tmct doesn't index code itself.)`;
|
|
4274
5291
|
note(trace, "intermediate: HONEST-EMPTY POLISH — the loaded graph has 0 modules, so the dead-end got a --repo/tmct init pointer appended");
|
|
4275
5292
|
}
|
|
5293
|
+
// TEACH-OFFER (Tier-5 playtest, this session) — SKILL_CHAT_PLAYTEST.md §0
|
|
5294
|
+
// names "'X' isn't a term in this graph's own vocabulary" as its own
|
|
5295
|
+
// dead-end example, and Tier 5 (§3) explicitly wants "the honest 'I don't
|
|
5296
|
+
// know that yet' that offers to learn" rather than a bare wall. A "what is
|
|
5297
|
+
// X" miss where X is genuinely unknown EVERYWHERE — not a real graph entity
|
|
5298
|
+
// (resolveEntity), not a schema/vocab term (that's what "still standing
|
|
5299
|
+
// miss" already means here), and not already in memory (checked directly,
|
|
5300
|
+
// not via factAnswer's OWN metaTerm branch, so this never duplicates its
|
|
5301
|
+
// more specific "no X facts about Y" miss) — gets a short offer appended
|
|
5302
|
+
// UNDER the existing miss text, never replacing it (every pinned assertion
|
|
5303
|
+
// on the miss wording elsewhere stays intact; this is purely additive, the
|
|
5304
|
+
// same discipline the corpus aside (W5) and empty-graph polish above use).
|
|
5305
|
+
if (recordMiss && (via === "composed" || via === "miss") && memoryDir) {
|
|
5306
|
+
// Tier-5 playtest fix (cycle 3): "what do you know about X" is its OWN
|
|
5307
|
+
// sibling shape — checked FIRST (and, unlike metaTermOf below, without a
|
|
5308
|
+
// resolveEntity(graph) gate): it's inherently a MEMORY question, not a
|
|
5309
|
+
// graph-structure one, so "nothing yet, teach me" is appropriate even
|
|
5310
|
+
// when X also happens to be a real graph entity — there's no genuinely
|
|
5311
|
+
// BETTER answer path to defer to the way "what is X" has (the concept
|
|
5312
|
+
// force, schema docs, …). Found live: "what do you know about the last
|
|
5313
|
+
// commit" (nothing in memory, and the whole sentence never fits ask.mjs's
|
|
5314
|
+
// grammar either) fell to the raw wall, unguided.
|
|
5315
|
+
const knowAboutTerm = String(query).trim().match(KNOW_ABOUT_RE)?.[1]?.trim();
|
|
5316
|
+
const offerTerm = knowAboutTerm || metaTermOf(query, envelope);
|
|
5317
|
+
if (offerTerm) {
|
|
5318
|
+
let normFactTerm;
|
|
5319
|
+
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { normFactTerm = null; }
|
|
5320
|
+
if (normFactTerm) {
|
|
5321
|
+
const cleanTerm = normFactTerm(offerTerm);
|
|
5322
|
+
const ent = knowAboutTerm ? null : await resolveEntity(graph, offerTerm);
|
|
5323
|
+
if (!ent) {
|
|
5324
|
+
const variants = factTermVariants(normFactTerm, offerTerm);
|
|
5325
|
+
const known = (await memoryFacts(memoryDir)).some((f) => variants.has(f.subject) || variants.has(f.object));
|
|
5326
|
+
if (!known) {
|
|
5327
|
+
answer = `${answer}\n${unknownVocabTermOffer(cleanTerm)}`;
|
|
5328
|
+
note(trace, `intermediate: TEACH-OFFER — "${cleanTerm}" is unknown to both the graph and memory, so the miss got an offer to learn appended`);
|
|
5329
|
+
}
|
|
5330
|
+
}
|
|
5331
|
+
}
|
|
5332
|
+
}
|
|
5333
|
+
}
|
|
4276
5334
|
// W5 (flag-gated, default OFF): an unknown-term miss may consult the LOCAL
|
|
4277
5335
|
// committed corpus slice — a hit APPENDS a grounded, licence-cited aside under
|
|
4278
5336
|
// the honest miss (the miss itself stands; the aside is context, not an answer).
|
|
@@ -4363,6 +5421,38 @@ function plainTurn(query, answer, { command, via = "composed", miss = false, foc
|
|
|
4363
5421
|
};
|
|
4364
5422
|
}
|
|
4365
5423
|
|
|
5424
|
+
// Bug F point 5 (operator's explicit, most important ask this round): a
|
|
5425
|
+
// command name -> a short, honest one-line goal string, mirroring GOAL_BY_KIND's
|
|
5426
|
+
// own spirit (above) — but for COMMAND dispatches (find/search/describe/…)
|
|
5427
|
+
// instead of ask()-parsed relation queries, so "I want you to search for
|
|
5428
|
+
// Widget" (now reachable via a slash command per Bug F point 3) ALSO gets a
|
|
5429
|
+
// real "Goal (inferred): …" line instead of none at all, generalizing the
|
|
5430
|
+
// existing mechanism exactly as asked. Reuses GOAL_BY_KIND's EXISTING wording
|
|
5431
|
+
// verbatim wherever a command's intent overlaps one of those kinds (members/
|
|
5432
|
+
// subclasses reuse contains/inherits's own phrasing; callers/callees reuse
|
|
5433
|
+
// calls's; tests/untested reuse tests's; history reuses touches's; exports
|
|
5434
|
+
// reuses reexports's) — never invents new phrasing for the same concept. A
|
|
5435
|
+
// command not worth a bespoke entry (help/stats/memory/focus/narrate/unknown)
|
|
5436
|
+
// falls back to a short generic line in mk() itself, below.
|
|
5437
|
+
const GOAL_BY_COMMAND = {
|
|
5438
|
+
find: "locate a specific named entity",
|
|
5439
|
+
search: "locate a specific named entity",
|
|
5440
|
+
context: "gather the sized edit bundle for a symbol before changing code",
|
|
5441
|
+
snippet: "view a symbol's exact source",
|
|
5442
|
+
describe: "look up a symbol's definition and relations",
|
|
5443
|
+
signature: "view a symbol's signature",
|
|
5444
|
+
members: GOAL_BY_KIND.contains,
|
|
5445
|
+
subclasses: GOAL_BY_KIND.inherits,
|
|
5446
|
+
impact: "understand what a change to this module would reach (impact closure)",
|
|
5447
|
+
callers: GOAL_BY_KIND.calls,
|
|
5448
|
+
callees: GOAL_BY_KIND.calls,
|
|
5449
|
+
tests: GOAL_BY_KIND.tests,
|
|
5450
|
+
untested: GOAL_BY_KIND.tests,
|
|
5451
|
+
history: GOAL_BY_KIND.touches,
|
|
5452
|
+
exports: GOAL_BY_KIND.reexports,
|
|
5453
|
+
arch: "understand the overall architecture (package/module boundaries)",
|
|
5454
|
+
};
|
|
5455
|
+
|
|
4366
5456
|
/** A slash-command → the mapped tool (or the /help, /focus, /narrate, unknown
|
|
4367
5457
|
* cases). Returns the same { answer, logLines, record, focus } shape as
|
|
4368
5458
|
* runAsk; the record carries the command name and the resolved entity id
|
|
@@ -4370,7 +5460,10 @@ function plainTurn(query, answer, { command, via = "composed", miss = false, foc
|
|
|
4370
5460
|
* wherever it resolves an entity. `ctx.trace` (narrate mode, or undefined
|
|
4371
5461
|
* when off) gets one "goal:"/"lane:" note per branch — a slash-command's
|
|
4372
5462
|
* "decision" is simply which command+tool ran, so this is intentionally
|
|
4373
|
-
* lighter than runAsk's miss-cascade instrumentation.
|
|
5463
|
+
* lighter than runAsk's miss-cascade instrumentation. Also carries a `goal`
|
|
5464
|
+
* field now (Bug F point 5) — mirrors runAsk's own `goal` field so
|
|
5465
|
+
* withGoalLine's short "Goal (inferred): …" line fires for command
|
|
5466
|
+
* dispatches too, not just ask()-parsed queries. */
|
|
4374
5467
|
async function runCommand(line, { config, source, graph, focus, memoryDir, trace, narrate = false }) {
|
|
4375
5468
|
const ts = new Date().toISOString();
|
|
4376
5469
|
const sp = line.indexOf(" ");
|
|
@@ -4381,6 +5474,7 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
4381
5474
|
logLines: [ts, `> ${line}`, answer, ""],
|
|
4382
5475
|
record: { type: "turn", ts, query: line, command: name, via: "command", resolvedIds, answeredIds: [], miss },
|
|
4383
5476
|
focus: newFocus,
|
|
5477
|
+
goal: GOAL_BY_COMMAND[name] || "use a specific tool/command directly",
|
|
4384
5478
|
...(narrateNext !== undefined ? { narrate: narrateNext } : {}),
|
|
4385
5479
|
});
|
|
4386
5480
|
|
|
@@ -4441,6 +5535,20 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
4441
5535
|
value = focus?.label || "";
|
|
4442
5536
|
if (value) note(trace, `intermediate: no/pronoun argument -> fell back to the standing focus "${value}"`);
|
|
4443
5537
|
}
|
|
5538
|
+
// Tier 6 playtest: a bare English "describe the logger module"/"describe the
|
|
5539
|
+
// Task class" is short enough (≤3 tokens, no query connective) that
|
|
5540
|
+
// asBareCommand (above) already rewrote it into a literal slash command
|
|
5541
|
+
// BEFORE this function ever sees it — so describeGrainRescue's OWN lane
|
|
5542
|
+
// (describeWrapperAnswer) never runs for this exact shape; the rescue has to
|
|
5543
|
+
// happen HERE too, on the raw command argument, before it reaches
|
|
5544
|
+
// dispatchTool's resolveSymbol (which has no article/grain-word tolerance).
|
|
5545
|
+
if (entityArg && value) {
|
|
5546
|
+
const rescued = await describeGrainRescue(graph, value);
|
|
5547
|
+
if (rescued?.label) {
|
|
5548
|
+
note(trace, `intermediate: "${value}" carries a grain word -> resolved to ${rescued.label} before dispatch`);
|
|
5549
|
+
value = rescued.label;
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
4444
5552
|
if (spec.arg && !spec.optional && !value) {
|
|
4445
5553
|
const need = entityArg ? `${spec.arg} (none given and no focus set — /focus <x> or pass one)` : spec.arg;
|
|
4446
5554
|
return mk(`/${name} needs a ${need}.`, { miss: true });
|
|
@@ -4570,8 +5678,30 @@ function morePage(query, { last, focus }) {
|
|
|
4570
5678
|
return turn;
|
|
4571
5679
|
}
|
|
4572
5680
|
|
|
5681
|
+
// Bug F point 3 (operator follow-up request): "I want you to search for
|
|
5682
|
+
// Widget" / "I'd like you to search for Widget" — a closed-set indirect-
|
|
5683
|
+
// request wrapper, checked VERY early (before asBareCommand/conversationalTurn/
|
|
5684
|
+
// the ask engine ever see the raw prefix). Found live: without this, "I want
|
|
5685
|
+
// you to search for Widget" was mis-swallowed by GENERAL_VERB_TEACH_RE as a
|
|
5686
|
+
// bare <subject> <verb> <object> teach triple (subject "I", verb "want") —
|
|
5687
|
+
// declined by the pronoun-subject guard with a confusing "pronouns aren't
|
|
5688
|
+
// things I can classify" message, instead of ever reaching /search at all.
|
|
5689
|
+
// Deliberately does NOT strip bare "please X" alone — that's already handled
|
|
5690
|
+
// ad hoc by many individual regexes throughout this file (TEACH_RE,
|
|
5691
|
+
// EXPLAIN_TOUCH_RE, describeWrapperAnswer's own regex, IMPERATIVE_NUDGE_RE) and
|
|
5692
|
+
// re-stripping it centrally here risks double-processing interactions across
|
|
5693
|
+
// the whole file — out of scope for this fix, higher risk than the concrete
|
|
5694
|
+
// gain.
|
|
5695
|
+
const INDIRECT_REQUEST_RE = /^(?:i\s+(?:want|wanted)\s+you\s+to\s+|i(?:'d|\s+would)\s+like\s+you\s+to\s+)\s*(.+)$/i;
|
|
5696
|
+
|
|
4573
5697
|
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, vocabHint = null } = {}) {
|
|
4574
5698
|
const line = String(input ?? "").trim();
|
|
5699
|
+
// The captured residue is used for RECOGNITION at every dispatch site below
|
|
5700
|
+
// (asBareCommand, conversationalTurn, assertTurn, the count lanes, runAsk);
|
|
5701
|
+
// the ORIGINAL `line` survives untouched for record.query/logLines fidelity
|
|
5702
|
+
// — restored centrally inside withLast (below), once, for every dispatch path.
|
|
5703
|
+
const indirectMatch = line.match(INDIRECT_REQUEST_RE);
|
|
5704
|
+
const workingLine = indirectMatch ? indirectMatch[1].trim() : line;
|
|
4575
5705
|
const templates = await chatTemplates(); // failure-tolerated: null degrades, never throws
|
|
4576
5706
|
// narrate mode: allocate the mutable trace array ONLY when on (`null` when off,
|
|
4577
5707
|
// matching every OTHER optional collaborator here — templates/memoryDir/lexicon
|
|
@@ -4598,6 +5728,14 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
4598
5728
|
// the PRE-narration finished result — see withNarration's docblock for why.
|
|
4599
5729
|
const withLast = (result, fallbackGoal = "unclear — no goal signal for this turn type") => {
|
|
4600
5730
|
const finished = finish(result, { graph });
|
|
5731
|
+
// Bug F point 3 fidelity: every dispatch path below built its own record off
|
|
5732
|
+
// `workingLine` (the indirect-request wrapper stripped, above) — restore the
|
|
5733
|
+
// ORIGINAL raw `line` into record.query and the logged "> …" transcript echo
|
|
5734
|
+
// here, once, centrally, for every path (they all funnel through withLast).
|
|
5735
|
+
if (indirectMatch) {
|
|
5736
|
+
if (finished.record) finished.record.query = line;
|
|
5737
|
+
if (Array.isArray(finished.logLines) && finished.logLines.length > 1) finished.logLines[1] = `> ${line}`;
|
|
5738
|
+
}
|
|
4601
5739
|
// runAsk's own effectiveQuery (set only when discourseRewrite substituted a
|
|
4602
5740
|
// new subject AND the rewrite produced a genuine non-miss answer) takes
|
|
4603
5741
|
// over as the continuation base for the NEXT turn's own discourseRewrite —
|
|
@@ -4617,32 +5755,32 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
4617
5755
|
// "memory", "describe X") is routed to its slash form BEFORE the conversational
|
|
4618
5756
|
// layer, so a forgiving shell answers "stats" the way it answers "/stats" instead
|
|
4619
5757
|
// of falling through to the generic orientation.
|
|
4620
|
-
const bareCmd = asBareCommand(
|
|
5758
|
+
const bareCmd = asBareCommand(workingLine);
|
|
4621
5759
|
if (bareCmd) return withLast(await runCommand(bareCmd, ctx), "use a specific tool/command directly");
|
|
4622
5760
|
|
|
4623
5761
|
// Conversational layer next (greetings, thanks, help, bye, why/say-more) — these
|
|
4624
5762
|
// resolve no entity and carry their own preserved `last`. Bypasses withLast (a
|
|
4625
5763
|
// conversational turn is never finish()'d / never becomes a new `last`), so the
|
|
4626
5764
|
// narrate block is applied directly here instead.
|
|
4627
|
-
const convo = conversationalTurn(
|
|
5765
|
+
const convo = conversationalTurn(workingLine, ctx);
|
|
4628
5766
|
if (convo) return withNarration(convo, trace, "casual/social — no graph intent");
|
|
4629
5767
|
|
|
4630
5768
|
// "more" — page the remainder of a previous long listing, if one is held. Gated on
|
|
4631
5769
|
// an actual pending remainder so a bare "more" with nothing to continue falls through
|
|
4632
5770
|
// to the ordinary path (an honest miss), never a pretend page.
|
|
4633
|
-
if (MORE_RE.test(
|
|
5771
|
+
if (MORE_RE.test(workingLine) && Array.isArray(last?.detail?.pending?.items) && last.detail.pending.items.length) {
|
|
4634
5772
|
note(trace, "goal: continue viewing a previous long listing (pagination)");
|
|
4635
5773
|
note(trace, "lane: MORE_RE matched a held pending remainder from the previous turn's detail.pending");
|
|
4636
|
-
return withLast(morePage(
|
|
5774
|
+
return withLast(morePage(workingLine, ctx), "continue viewing a previous long listing");
|
|
4637
5775
|
}
|
|
4638
5776
|
|
|
4639
|
-
if (
|
|
5777
|
+
if (workingLine.startsWith("/")) return withLast(await runCommand(workingLine, ctx), "use a specific tool/command directly");
|
|
4640
5778
|
// Declarative ACE sentences ("every module is a artifact") ASSERT into tmct's
|
|
4641
5779
|
// own memory and confirm — they are statements to remember, not graph queries.
|
|
4642
5780
|
// Gated on memoryDir: only a session shell provides a write target, so a bare
|
|
4643
5781
|
// runTurn (tests, library callers) stays pure and falls through to the engine.
|
|
4644
5782
|
if (memoryDir) {
|
|
4645
|
-
const asserted = await assertTurn(
|
|
5783
|
+
const asserted = await assertTurn(workingLine, ctx);
|
|
4646
5784
|
if (asserted) {
|
|
4647
5785
|
note(trace, "goal: teach/remember a new fact (declarative ACE sentence)");
|
|
4648
5786
|
note(trace, "lane: assertTurn — grammar/ace.mjs parseAce matched a full triple with no residue");
|
|
@@ -4655,11 +5793,11 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
4655
5793
|
// otherwise say "I can't count facts"); it only speaks for a memory-class noun, so
|
|
4656
5794
|
// structural counts (classes/functions/…) and sessions fall through unaffected.
|
|
4657
5795
|
if (memoryDir) {
|
|
4658
|
-
const memCount = await answerMemoryCount(memoryDir,
|
|
5796
|
+
const memCount = await answerMemoryCount(memoryDir, workingLine);
|
|
4659
5797
|
if (memCount != null) {
|
|
4660
5798
|
note(trace, "goal: get a count of a memory-store kind (facts/utterances)");
|
|
4661
5799
|
note(trace, "lane: answerMemoryCount — matched a MEMORY_COUNT_NOUNS entry, answered off the .tmct/memory graph header");
|
|
4662
|
-
return withLast(plainTurn(
|
|
5800
|
+
return withLast(plainTurn(workingLine, memCount, { via: "count", focus }), "get a count of a memory-store kind");
|
|
4663
5801
|
}
|
|
4664
5802
|
}
|
|
4665
5803
|
// Feature A point 4: "how many Xs are Ys" — a taught-quantifier RECALL, checked
|
|
@@ -4668,32 +5806,32 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
4668
5806
|
// authority gate declines (returns null) for anything answerCount should own,
|
|
4669
5807
|
// so ordinary structural counts fall through completely unaffected.
|
|
4670
5808
|
if (memoryDir) {
|
|
4671
|
-
const quantifierRecall = await answerQuantifierRecall(memoryDir,
|
|
5809
|
+
const quantifierRecall = await answerQuantifierRecall(memoryDir, workingLine);
|
|
4672
5810
|
if (quantifierRecall != null) {
|
|
4673
5811
|
note(trace, 'goal: recall a taught quantifier for a class-membership pair ("how many Xs are Ys")');
|
|
4674
5812
|
note(trace, "lane: answerQuantifierRecall — matched HOW_MANY_ARE_RE with a subject tmct has facts about; literal recall, never real counting");
|
|
4675
|
-
return withLast(plainTurn(
|
|
5813
|
+
return withLast(plainTurn(workingLine, quantifierRecall, { via: "fact", focus }), "recall a taught quantifier");
|
|
4676
5814
|
}
|
|
4677
5815
|
}
|
|
4678
5816
|
// Aggregate/count questions are answered mechanically off the loaded graph header,
|
|
4679
5817
|
// BEFORE falling through to the ask engine (focus unchanged — a count names no entity).
|
|
4680
|
-
const count = answerCount(graph,
|
|
5818
|
+
const count = answerCount(graph, workingLine);
|
|
4681
5819
|
if (count != null) {
|
|
4682
5820
|
// An "I can't count <noun>" from a bare kind may still be answerable from an
|
|
4683
5821
|
// ASSERTED vocabulary fact ("every class is a type" → "how many types" = the
|
|
4684
5822
|
// class count). countFromFacts declines on a real graph kind, so ordinary
|
|
4685
5823
|
// counts are unaffected; it only speaks for a remembered object noun.
|
|
4686
|
-
const viaFact = memoryDir ? await countFromFacts(graph, memoryDir,
|
|
5824
|
+
const viaFact = memoryDir ? await countFromFacts(graph, memoryDir, workingLine) : null;
|
|
4687
5825
|
if (viaFact != null) {
|
|
4688
5826
|
note(trace, 'goal: get a count of an asserted-vocabulary kind ("every X is a Y" inherited cardinality)');
|
|
4689
5827
|
note(trace, "lane: countFromFacts — the counted noun matched a remembered isa-fact's SUBJECT, whose class IS countable");
|
|
4690
|
-
return withLast(plainTurn(
|
|
5828
|
+
return withLast(plainTurn(workingLine, viaFact, { via: "fact", focus }), "get a count");
|
|
4691
5829
|
}
|
|
4692
5830
|
note(trace, "goal: get a count of a graph kind (classes/functions/modules/…)");
|
|
4693
5831
|
note(trace, "lane: answerCount — a header-count aggregate question, answered mechanically off the graph header, never dispatched to the ask engine");
|
|
4694
|
-
return withLast(plainTurn(
|
|
5832
|
+
return withLast(plainTurn(workingLine, count, { via: "count", focus }), "get a count of a graph kind");
|
|
4695
5833
|
}
|
|
4696
|
-
return withLast(await runAsk(
|
|
5834
|
+
return withLast(await runAsk(workingLine, ctx), "unclear — no goal signal computed by the ask engine");
|
|
4697
5835
|
}
|
|
4698
5836
|
|
|
4699
5837
|
// ---- W3: seedMemory → bootstrap (first run in a graph-less repo) ----
|