@polycode-projects/the-mechanical-code-talker 1.0.8 → 1.0.9
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 +211 -92
- package/package.json +1 -1
- package/src/ask-vocab.mjs +125 -1
- package/src/ask.mjs +510 -17
- package/src/chat.mjs +1010 -64
- 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,18 @@ 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;
|
|
1358
1476
|
|
|
1359
1477
|
/** The unknown-SUBJECT direct-write fallback (point 1 + point 2's bare-property
|
|
1360
1478
|
* extension): tried ONLY after the real ACE grammar (assertTurn) has already
|
|
@@ -1442,7 +1560,21 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }
|
|
|
1442
1560
|
* multi-word subject simply doesn't match here and honestly declines
|
|
1443
1561
|
* (point 6) rather than risk a wrong split — the is/are-specific frames
|
|
1444
1562
|
* elsewhere in this lane already own that broader territory. */
|
|
1445
|
-
|
|
1563
|
+
// Tier-5 playtest fix (this session): a frequency/degree ADVERB commonly sits
|
|
1564
|
+
// between a bare-name subject and the real verb in a natural teaching
|
|
1565
|
+
// sentence — the operator's own example, "remember that TaskController
|
|
1566
|
+
// usually needs review", mis-split subject="TaskController", VERB="usually"
|
|
1567
|
+
// (GENERAL_VERB_TEACH_RE had no way to see "usually" wasn't the verb), minting
|
|
1568
|
+
// a nonsense mgx:usually predicate and, worse, garbling the confirmation
|
|
1569
|
+
// itself: thirdPersonSingularSurface's naive fallback appended "-ies" to the
|
|
1570
|
+
// unrecognized lemma, surfacing "taskcontroller usuallies needs review". A
|
|
1571
|
+
// closed, non-capturing adverb-skip (never itself eligible to BE the verb)
|
|
1572
|
+
// fixes this at the source for both the teach shape and its query-side twins
|
|
1573
|
+
// below, without widening what counts as a recognized shape at all — the same
|
|
1574
|
+
// "recognition closed, mapping generalized" split this lane already uses.
|
|
1575
|
+
const TEACH_ADVERB_SKIP_SRC = "(?:(?:usually|often|sometimes|rarely|never|always|typically|generally|"
|
|
1576
|
+
+ "occasionally|frequently|normally|regularly|commonly|mostly|currently|still|also|really|actually)\\s+)?";
|
|
1577
|
+
const GENERAL_VERB_TEACH_RE = new RegExp(`^([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+)\\s+(.+?)[.!?]*$`, "i");
|
|
1446
1578
|
/** Determiners/quantifiers that make the FIRST token an article, not a real
|
|
1447
1579
|
* bare-name subject ("every controller…", "the cache…") — GENERAL_VERB_TEACH_RE
|
|
1448
1580
|
* would otherwise happily bind them as a 1-token subject and misread the
|
|
@@ -1477,13 +1609,23 @@ const GENERAL_VERB_ANYWHERE_EXCLUDE_RE = /\b(?:is|are|am|owns|maintains)\b/i;
|
|
|
1477
1609
|
* documented contract) and this falls back to the verb AS TYPED — still a
|
|
1478
1610
|
* perfectly storable/retrievable predicate, just not cross-inflection
|
|
1479
1611
|
* canonicalized. Never a hand-curated per-verb table entry required. */
|
|
1612
|
+
// Bug A (operator manual-chat find, this session): only the exact raw strings
|
|
1613
|
+
// "has"/"have" were special-cased onto HAS_A_PREDICATE above — past tense "had"
|
|
1614
|
+
// (or "having") fell through to the generic mgx:<lemma> path, where the lemma of
|
|
1615
|
+
// "had" IS "have", and predicatePhrase's thirdPersonSingularSurface fallback
|
|
1616
|
+
// naively appends "s" to any unrecognized lemma ending ("have"+"s" = "haves" —
|
|
1617
|
+
// wrong; the correct irregular is "has"). Fixed by checking the LEMMA (not just
|
|
1618
|
+
// the raw verb) for "have" — this catches had/having/has/have uniformly, so
|
|
1619
|
+
// "remember X had soup" reads back "...has soup", never "...haves soup".
|
|
1480
1620
|
async function generalVerbPredicate(verb) {
|
|
1481
1621
|
const v = String(verb || "").toLowerCase();
|
|
1482
1622
|
if (v === "has" || v === "have") return HAS_A_PREDICATE;
|
|
1483
1623
|
try {
|
|
1484
1624
|
const { proseLemma } = await import("./prose-nlp.mjs");
|
|
1485
1625
|
const lemma = proseLemma();
|
|
1486
|
-
|
|
1626
|
+
const l = lemma ? lemma(v) : v;
|
|
1627
|
+
if (l === "have") return HAS_A_PREDICATE;
|
|
1628
|
+
return `mgx:${l}`;
|
|
1487
1629
|
} catch {
|
|
1488
1630
|
return `mgx:${v}`;
|
|
1489
1631
|
}
|
|
@@ -1510,6 +1652,35 @@ async function generalVerbTeach(payload) {
|
|
|
1510
1652
|
return { subject, predicate, object };
|
|
1511
1653
|
}
|
|
1512
1654
|
|
|
1655
|
+
// ---- General verb-to-predicate DIRECT-QUESTION retrieval (item 5, this
|
|
1656
|
+
// session's follow-up to the teach mechanism above): "does margo eat ribs" /
|
|
1657
|
+
// "did margo eat ribs" / "what does margo eat" against a fact taught via
|
|
1658
|
+
// generalVerbTeach. "did" joins "does" so past-tense forms work too (also
|
|
1659
|
+
// GROUP 3 Bug B — "what did X have"/"did margo eat ribs"). Wired into
|
|
1660
|
+
// factReadBack (below), which only runs on an already-true `miss`, so these
|
|
1661
|
+
// never race ask.mjs's closed structural grammar for a real graph query
|
|
1662
|
+
// ("does TaskController call widget" resolves there first, this lane is never
|
|
1663
|
+
// reached). Both run the SAME GENERAL_VERB_EXCLUDE_RE/GENERAL_VERB_ANYWHERE_
|
|
1664
|
+
// EXCLUDE_RE decline guards generalVerbTeach uses, and route the verb through
|
|
1665
|
+
// the SAME generalVerbPredicate (not a re-implementation), so the has/have
|
|
1666
|
+
// bridge (and Bug A's had/having lemma fix) is automatic on the query side too. ----
|
|
1667
|
+
// Same adverb-skip as GENERAL_VERB_TEACH_RE above (TEACH_ADVERB_SKIP_SRC),
|
|
1668
|
+
// reused so "does TaskController usually need review"/"what does
|
|
1669
|
+
// TaskController usually need" read back a fact taught with the adverb
|
|
1670
|
+
// skipped the same way, never mis-splitting "usually" as the verb here either.
|
|
1671
|
+
const GENERAL_VERB_YESNO_RE = new RegExp(`^(?:does|did)\\s+([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+)\\s+(.+?)[?.!\\s]*$`, "i");
|
|
1672
|
+
const GENERAL_VERB_OPEN_RE = new RegExp(`^what\\s+(?:does|did)\\s+([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+)[?.!\\s]*$`, "i");
|
|
1673
|
+
/** GENERAL_VERB_EXCLUDE_RE was written for generalVerbTeach's fully-conjugated
|
|
1674
|
+
* declarative verb ("X OWNS Y", "X MAINTAINS Y") — but "does/did X <verb> Y"
|
|
1675
|
+
* captures the BARE INFINITIVE after do-support ("does X OWN Y", never "does X
|
|
1676
|
+
* owns Y"), so "owns"/"maintains" literally never appear in genYN/genOpen's
|
|
1677
|
+
* captured verb even when the sentence names exactly that relation. Found live
|
|
1678
|
+
* (a real false "no" against a genuinely-true taught ownership fact, since
|
|
1679
|
+
* generalVerbPredicate("own") mints a DIFFERENT predicate — mgx:own — than the
|
|
1680
|
+
* ownership frame's own OWNED_BY_PREDICATE): the query-side guard needs the
|
|
1681
|
+
* bare-infinitive counterpart too. */
|
|
1682
|
+
const GENERAL_VERB_QUERY_EXCLUDE_RE = /^(?:be|own|maintain)$/i;
|
|
1683
|
+
|
|
1513
1684
|
/** Sentence forms to try asserting for a teach payload: the payload as-is, and
|
|
1514
1685
|
* (if it carries no determiner) its "every …" universal — the ACE-OWL shape the
|
|
1515
1686
|
* grammar actually lands. */
|
|
@@ -1574,7 +1745,14 @@ function teachSuggestion(payload) {
|
|
|
1574
1745
|
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
1746
|
|
|
1576
1747
|
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
1577
|
-
|
|
1748
|
+
// Tier 6 playtest: this lane read the raw, un-normalized query, so a closed
|
|
1749
|
+
// discourse-marker preamble ahead of a teach sentence ("howdy pardner,
|
|
1750
|
+
// remember that TaskController is fragile") corrupted TEACH_RE's own match —
|
|
1751
|
+
// applyPreambleFrames is idempotent no-op on an already-clean teach sentence
|
|
1752
|
+
// (none of its frames' anchors — greeting/thanks/ack/modal/explain/show-give-me/
|
|
1753
|
+
// topic-switch/hedge — match ordinary teach phrasing, verified against this
|
|
1754
|
+
// lane's own test corpus), so this is purely additive.
|
|
1755
|
+
const rawInput = applyPreambleFrames(String(query).trim());
|
|
1578
1756
|
const m = rawInput.match(TEACH_RE);
|
|
1579
1757
|
const wrappedInput = m ? m[1].trim() : null;
|
|
1580
1758
|
// "your X is a/an Y" (Feature A) — a plain casual synonym for "a/an X is a
|
|
@@ -1617,6 +1795,18 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
1617
1795
|
});
|
|
1618
1796
|
if (stored) return stored;
|
|
1619
1797
|
}
|
|
1798
|
+
// PASSIVE ownership — "<X> is owned by <Name>" (Tier-5 playtest, cycle 2).
|
|
1799
|
+
// Same bare-form gate as the active shape just above: a Capitalized owner
|
|
1800
|
+
// name AND no interrogative lead, so "is TaskController owned by anyone"
|
|
1801
|
+
// (a genuine yes/no QUESTION, handled by factReadBack instead) never lands
|
|
1802
|
+
// a bogus fact here.
|
|
1803
|
+
const ownPassive = ownSrc.match(OWNS_PASSIVE_TEACH_RE);
|
|
1804
|
+
if (ownPassive && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && (wrapped || /^[A-Z]/.test(ownPassive[2]))) {
|
|
1805
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
1806
|
+
subject: ownPassive[1], predicate: OWNED_BY_PREDICATE, object: ownPassive[2],
|
|
1807
|
+
});
|
|
1808
|
+
if (stored) return stored;
|
|
1809
|
+
}
|
|
1620
1810
|
|
|
1621
1811
|
// "some Xs are Ys" / "a few Xs are Ys" (Feature A) — the plural class-
|
|
1622
1812
|
// membership quantifier shape. ACE has no quantifier-phrase pattern at all
|
|
@@ -1641,6 +1831,33 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
1641
1831
|
subject, predicate: SUBCLASS_PREDICATE, object, quantifier,
|
|
1642
1832
|
});
|
|
1643
1833
|
if (stored) return stored;
|
|
1834
|
+
} else {
|
|
1835
|
+
// Tier-5 playtest fix (cycle 2), found live: "remember that some
|
|
1836
|
+
// functions are risky" — Y ("risky") is not a lexicon NOUN, so the
|
|
1837
|
+
// subclass path just above correctly declines it (SOME_A_FEW_RE is
|
|
1838
|
+
// subclass-only, by design — "risky" isn't even in the closed
|
|
1839
|
+
// lexicon at all, as either noun or adjective, so gating this decline
|
|
1840
|
+
// on "is Y a known adjective" missed the actual case entirely on the
|
|
1841
|
+
// first attempt at this fix). Without this guard, the sentence fell
|
|
1842
|
+
// through to unknownSubjectFallback/TEACH_PROPERTY_RE below, which DO
|
|
1843
|
+
// tolerate a multi-word subject with NO vocabulary check on the
|
|
1844
|
+
// complement at all — silently mis-teaching the LITERAL 2-word string
|
|
1845
|
+
// "some functions" as if it were one proper-noun subject ("noted —
|
|
1846
|
+
// remembered: some functions is risky", the quantifier word baked
|
|
1847
|
+
// wrongly into the subject and a subject/verb agreement error to
|
|
1848
|
+
// boot), a fact "how many functions are risky" could never sensibly
|
|
1849
|
+
// read back either (HOW_MANY_ARE_RE's own reader only ever looks for
|
|
1850
|
+
// the SUBCLASS_PREDICATE shape this path would have stored, not this
|
|
1851
|
+
// one). A quantified PROPERTY claim isn't a supported shape yet (only
|
|
1852
|
+
// a quantified SUBCLASS claim is) — decline honestly here instead of
|
|
1853
|
+
// silently mis-teaching, rather than let a later, less-specific frame
|
|
1854
|
+
// guess a wrong split.
|
|
1855
|
+
return {
|
|
1856
|
+
text: `I can only remember a quantified fact as "${quantifier} ${someMatch[2]} are <a kind of thing>" (like "${quantifier} bugs are issues") — `
|
|
1857
|
+
+ `a quantified claim about a PROPERTY ("${quantifier} ${someMatch[2]} are ${object}") isn't a shape I can store yet. `
|
|
1858
|
+
+ `I can remember "${someMatch[2]} are ${object}" for one specific ${subject}, though — try naming it directly.`,
|
|
1859
|
+
via: "teach-miss", miss: true,
|
|
1860
|
+
};
|
|
1644
1861
|
}
|
|
1645
1862
|
}
|
|
1646
1863
|
|
|
@@ -1665,7 +1882,28 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
1665
1882
|
let payload = null;
|
|
1666
1883
|
if (wrapped && /\b(?:is|are)\b/i.test(wrapped)) payload = wrapped;
|
|
1667
1884
|
else if (BARE_DECLARATIVE_RE.test(raw) && !QUESTION_LEAD_RE.test(raw)) payload = raw;
|
|
1668
|
-
if (!payload)
|
|
1885
|
+
if (!payload) {
|
|
1886
|
+
// Tier-5 playtest fix (cycle 3), found live: "remember that every
|
|
1887
|
+
// controller needs review" — a QUANTIFIED subject ("every X", declined
|
|
1888
|
+
// by generalVerbTeach's own GENERAL_VERB_DETERMINER_RE, by design — see
|
|
1889
|
+
// its docblock on the ambiguity risk of a free-form multi-word subject)
|
|
1890
|
+
// combined with a non-copula verb ("needs", not is/are/owns/maintains)
|
|
1891
|
+
// fits NONE of the recognizers above, so `payload` stays null and this
|
|
1892
|
+
// used to return null SILENTLY — the exact "wrong-context wall" bug
|
|
1893
|
+
// class Bug 3's generalVerbTeach mechanism was built to close for
|
|
1894
|
+
// "remember margo eats ribs", re-escaping here through a combination
|
|
1895
|
+
// that mechanism's own deliberate subject-shape restriction doesn't
|
|
1896
|
+
// cover. An explicit "remember/note/…"-wrapped sentence is an
|
|
1897
|
+
// UNAMBIGUOUS teach-intent signal — falling through to the ordinary
|
|
1898
|
+
// structural-query wall is a wrong-context reply even when nothing here
|
|
1899
|
+
// can actually STORE the fact; if `wrapped` stood, keep going with it as
|
|
1900
|
+
// the payload so the residue-detection/final-decline logic below still
|
|
1901
|
+
// runs (never a guess at storing it, just never silence either). A bare,
|
|
1902
|
+
// unwrapped sentence that also fits no shape has no such signal — return
|
|
1903
|
+
// null and let the ordinary cascade decide, unchanged.
|
|
1904
|
+
if (!wrapped) return null;
|
|
1905
|
+
payload = wrapped;
|
|
1906
|
+
}
|
|
1669
1907
|
// Try to store it (a live session provides the write target). assertTurn returns
|
|
1670
1908
|
// the "noted — remembered …" confirmation or null (grammar miss / unknown words).
|
|
1671
1909
|
if (memoryDir) {
|
|
@@ -1754,11 +1992,18 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
1754
1992
|
// ONLY (the caller gates on a miss) and every pattern is a WHOLE-LINE self/session
|
|
1755
1993
|
// reference with no graph entity or predicate, so real graph queries ("what does X
|
|
1756
1994
|
// import", the meta "what does imports mean", "what did i ask before") never match.
|
|
1757
|
-
|
|
1995
|
+
// Bug D (operator manual-chat find, this session): "what is in your memory"/
|
|
1996
|
+
// "what's in your memory" is a plain synonym of the bare "what do you know" —
|
|
1997
|
+
// widened here rather than folded in as "what do you remember" (that phrase is
|
|
1998
|
+
// ALREADY WHOLE_RECALL_RE's own, more specific, territory — it lists every
|
|
1999
|
+
// remembered fact, a strictly better answer than this lane's short summary; see
|
|
2000
|
+
// WHOLE_RECALL_RE's docblock below and the pinned "'what do you remember' ...
|
|
2001
|
+
// STILL list facts" test — folding it in here would silently regress that).
|
|
2002
|
+
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
2003
|
// 0.8.2 WS4 wall kindness (c): the most likely stranger openers — "what does this
|
|
1759
2004
|
// app/codebase do", "what is this app (for)" — join the orientation lane, so a
|
|
1760
2005
|
// 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))$/;
|
|
2006
|
+
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
2007
|
|
|
1763
2008
|
/** A SHORT memory summary (never a fact dump) for the bare "what do you know".
|
|
1764
2009
|
* This branch only fires when rows.length === 0 — i.e. precisely the case where
|
|
@@ -1795,15 +2040,57 @@ async function memorySummary(memoryDir, graph) {
|
|
|
1795
2040
|
// case-sensitive, so this reads the ORIGINAL query text, never metaLane's
|
|
1796
2041
|
// lowercased `q` (authorLane's same discipline, just above/below).
|
|
1797
2042
|
const MODULE_ORIENT_RE = /^what\s+does\s+(.+?)\s+do\??$/i;
|
|
2043
|
+
/** The SUBJECT-FIRST word order of the SAME question ("what saveStore does" vs
|
|
2044
|
+
* "what does saveStore do") — Tier 6 playtest, §3b surface-variation axis: a
|
|
2045
|
+
* perfectly natural alternate phrasing of an ALREADY-recognized intent that
|
|
2046
|
+
* used to hit the raw grammar wall outright (MODULE_ORIENT_RE's own anchor
|
|
2047
|
+
* requires "does" BEFORE the term). Tried only when MODULE_ORIENT_RE/
|
|
2048
|
+
* MODULE_PURPOSE_RE both miss; the entity-resolution gate just below (a real,
|
|
2049
|
+
* UNIQUE graph entity or this lane declines) is what keeps this loose an
|
|
2050
|
+
* ending safe — a syntactic match against a term that isn't a real entity
|
|
2051
|
+
* simply falls through unchanged, same as every other lane in this file. */
|
|
2052
|
+
const MODULE_ORIENT_SVO_RE = /^what\s+(.+?)\s+does\??$/i;
|
|
2053
|
+
// Seonix Batch 3 (3a) — purpose/identity phrasing: "whats X for"/"what's X
|
|
2054
|
+
// about"/"what is X for", the sibling of "what does X do" that asks for the
|
|
2055
|
+
// SAME module-grain overview. Deliberately does NOT claim the literal noun
|
|
2056
|
+
// "app" ("what is this app for") — META_ORIENT_RE (above) already hardcodes
|
|
2057
|
+
// that exact phrasing and is checked BEFORE moduleOrientLane runs (metaLane's
|
|
2058
|
+
// own ordering), so this regex only ever gets a chance at OTHER resolvable
|
|
2059
|
+
// terms. "what(?:'s|s|\s+is)" mirrors PERSONAL_ASSISTANT_NUDGE_RE's own
|
|
2060
|
+
// tolerance for the bare "whats" contraction spelling, just below.
|
|
2061
|
+
const MODULE_PURPOSE_RE = /^what(?:'s|s|\s+is)\s+(.+?)\s+(?:for|about)\??$/i;
|
|
2062
|
+
|
|
2063
|
+
/** A leading politeness/formal-ESL wrapper this lane's own anchored regexes
|
|
2064
|
+
* otherwise miss entirely (Tier 6 playtest): "please explain what does X do"
|
|
2065
|
+
* starts with neither "what"/"whats" (MODULE_ORIENT_RE/MODULE_PURPOSE_RE's own
|
|
2066
|
+
* anchor) nor bare "explain" (normalize.mjs's own EXPLAIN_WRAPPER_RE, which
|
|
2067
|
+
* requires NOTHING before "explain" — a leading "please" defeats it too), so
|
|
2068
|
+
* it fell straight to the raw grammar wall. A repeated (please|kindly) plus an
|
|
2069
|
+
* optional "explain [to me]" — both closed, both optional, so a bare "what
|
|
2070
|
+
* does X do" is untouched (the whole prefix matches empty). */
|
|
2071
|
+
const MODULE_ORIENT_POLITENESS_RE = /^(?:(?:please|kindly)\s+)*(?:explain\s+(?:to\s+me\s+)?)?/i;
|
|
1798
2072
|
|
|
1799
2073
|
/** authorLane's discipline, mirrored: a closed regex + an EXACT, UNIQUE
|
|
1800
2074
|
* 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
|
|
2075
|
+
* subjects ("what does it/this do", "what's it for") are META_ORIENT_RE's/
|
|
2076
|
+
* isConversational's territory, not this lane's — declined here so they
|
|
2077
|
+
* fall through unchanged. */
|
|
1803
2078
|
async function moduleOrientLane(query, { graph }) {
|
|
1804
2079
|
if (!graph) return null;
|
|
1805
|
-
|
|
1806
|
-
|
|
2080
|
+
let q = String(query).trim().replace(/[?.!]+$/, "").replace(/\s+/g, " ");
|
|
2081
|
+
// Tier 6 playtest: this lane reads the ORIGINAL (case-preserving) query text
|
|
2082
|
+
// and never ran any of the general-purpose normalization passes the rest of
|
|
2083
|
+
// the file uses for the SAME class of surface noise — correctMisspellings
|
|
2084
|
+
// for a typo'd anchor word ("waht dose the logger modul do"), applyPreambleFrames
|
|
2085
|
+
// for a topic-switch/self-interruption preamble ("scratch that, what does X
|
|
2086
|
+
// do") — plus a lane-local politeness strip for "please explain X" (applyPreambleFrames's
|
|
2087
|
+
// own EXPLAIN_WRAPPER_RE requires the string to literally START with "explain",
|
|
2088
|
+
// so a LEADING "please"/"kindly" ahead of it defeats that frame; see
|
|
2089
|
+
// MODULE_ORIENT_POLITENESS_RE's own docblock). All three are additive,
|
|
2090
|
+
// closed-set, and idempotent on an already-clean query, so applying them here
|
|
2091
|
+
// only ever WIDENS what resolves, never narrows it.
|
|
2092
|
+
q = applyPreambleFrames(correctMisspellings(q)).replace(MODULE_ORIENT_POLITENESS_RE, "");
|
|
2093
|
+
const m = q.match(MODULE_ORIENT_RE) || q.match(MODULE_PURPOSE_RE) || q.match(MODULE_ORIENT_SVO_RE);
|
|
1807
2094
|
if (!m) return null;
|
|
1808
2095
|
const term = m[1].trim();
|
|
1809
2096
|
if (/^(?:it|this|that|they|them)$/i.test(term)) return null;
|
|
@@ -2179,7 +2466,11 @@ function configFor(repoPath) {
|
|
|
2179
2466
|
/** Resolve a free-text term to a single graph entity via the ask engine's own
|
|
2180
2467
|
* tiered resolver — {id,label} on a UNIQUE hit, null on a miss/ambiguity/no graph.
|
|
2181
2468
|
* 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.
|
|
2469
|
+
* records fewer ids / does not update the focus, never a crash or a wrong id.
|
|
2470
|
+
* The leading-article-strip + trailing-grain-word disambiguation ("the logger
|
|
2471
|
+
* module" -> Module-only "logger") lives centrally in resolveObject itself
|
|
2472
|
+
* (ask.mjs) so every direct caller of resolveObject (ask()'s own WHERE/describe
|
|
2473
|
+
* grammar, traverse(), etc.) gets it too, not just this wrapper. */
|
|
2183
2474
|
async function resolveEntity(graph, term) {
|
|
2184
2475
|
if (!graph || !term) return null;
|
|
2185
2476
|
try {
|
|
@@ -2398,6 +2689,13 @@ const FACT_PREDICATE_PHRASES = {
|
|
|
2398
2689
|
* renders verbatim, unchanged from before this fix. */
|
|
2399
2690
|
function thirdPersonSingularSurface(lemma) {
|
|
2400
2691
|
const w = String(lemma || "");
|
|
2692
|
+
// Bug A safety net: "have" should never reach this naive fallback at all
|
|
2693
|
+
// (generalVerbPredicate special-cases it onto mgx:hasA before a predicate is
|
|
2694
|
+
// ever minted), but if some OTHER path ever reaches here with it as typed —
|
|
2695
|
+
// e.g. wink-nlp unavailable so lemma degrades to the raw verb — the naive
|
|
2696
|
+
// "+s" rule would produce the wrong-MEANING "haves" instead of the correct
|
|
2697
|
+
// irregular "has".
|
|
2698
|
+
if (/^have$/i.test(w)) return "has";
|
|
2401
2699
|
if (/[a-z]y$/i.test(w) && !/[aeiou]y$/i.test(w)) return `${w.slice(0, -1)}ies`;
|
|
2402
2700
|
if (/(?:s|x|z|ch|sh|o)$/i.test(w)) return `${w}es`;
|
|
2403
2701
|
return `${w}s`;
|
|
@@ -2523,6 +2821,25 @@ function factTermVariants(normFactTerm, term) {
|
|
|
2523
2821
|
return v;
|
|
2524
2822
|
}
|
|
2525
2823
|
|
|
2824
|
+
/** GENERIC "kind" nouns a taught subject's head word is often built from
|
|
2825
|
+
* ("logger MODULE", "task CONTROLLER") — excluded from the head-word
|
|
2826
|
+
* overlap fallback both KNOW_ABOUT_RE's "what do you know about X" listing
|
|
2827
|
+
* and IS_ADJECTIVE_YESNO_RE's property yes/no reader use (below): a bare
|
|
2828
|
+
* length >= 4 floor alone isn't enough, since "module" (6 chars) is shared
|
|
2829
|
+
* by "logger module" AND "validate module" and any OTHER "X module" taught
|
|
2830
|
+
* subject — without this exclusion, "is the validate module deprecated"
|
|
2831
|
+
* confidently answered YES off a fact taught for "logger module" (found
|
|
2832
|
+
* live, Tier-5 playtest cycle 5 — a real false-positive fabrication, not a
|
|
2833
|
+
* routing gap, caught before shipping). Mirrors RECALL_STOPWORDS' own
|
|
2834
|
+
* path-noise exclusion (src/lib/mjs never counting as a real overlap
|
|
2835
|
+
* either) — same principle, a different word class. */
|
|
2836
|
+
const GENERIC_ENTITY_WORDS = new Set([
|
|
2837
|
+
"module", "modules", "class", "classes", "function", "functions",
|
|
2838
|
+
"method", "methods", "handler", "handlers", "controller", "controllers",
|
|
2839
|
+
"service", "services", "component", "components", "flow", "flows",
|
|
2840
|
+
"thing", "things", "item", "items", "object", "objects", "commit", "commits",
|
|
2841
|
+
]);
|
|
2842
|
+
|
|
2526
2843
|
// ---- PLAN_ontology-hierarchies.md §3 tracks (a)+(b): synonymsOf(term) —
|
|
2527
2844
|
// QUERY-TIME term expansion wiring the two already-parsed-but-inert synonym
|
|
2528
2845
|
// resources. §1's "two vocabulary gates" distinction: this widens what a
|
|
@@ -2605,8 +2922,14 @@ async function synonymsOf(term) {
|
|
|
2605
2922
|
* doesn't parse; checked against the isa-family fact predicates only. */
|
|
2606
2923
|
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
2924
|
const ISA_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
|
|
2608
|
-
/** "what do you know about caches" — the open recall-everything form.
|
|
2609
|
-
|
|
2925
|
+
/** "what do you know about caches" — the open recall-everything form. Bug E
|
|
2926
|
+
* (operator manual-chat find, this session) widened this to also accept
|
|
2927
|
+
* "what is in your memory about X" / "what's in your memory about X" / "what
|
|
2928
|
+
* do you remember about X" as plain synonyms — none of these collide with an
|
|
2929
|
+
* existing more-specific lane (TOLD_ABOUT_RE only owns "what did i tell you
|
|
2930
|
+
* about X"; WHOLE_RECALL_RE's own "what do you remember" has no "about X"
|
|
2931
|
+
* tail, so it's a disjoint shape). */
|
|
2932
|
+
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
2933
|
/** How many facts a single answer lists before the remainder is paged with "more". */
|
|
2611
2934
|
const FACT_ANSWER_CAP = 32;
|
|
2612
2935
|
|
|
@@ -2631,7 +2954,11 @@ async function factAnswer(memoryDir, query, envelope, miss) {
|
|
|
2631
2954
|
if (!metaTerm && miss && !envelope?.parsed) {
|
|
2632
2955
|
const m = q.match(BARE_WHATIS_RE)
|
|
2633
2956
|
|| q.match(/^what\s+(?:does|do)\s+(.+?)\s+means?[?.!\s]*$/i);
|
|
2634
|
-
|
|
2957
|
+
// Seonix Batch 2 Fix 3: strip a curated trailing scope clause ("… in this
|
|
2958
|
+
// graph"/"… in this codebase"/…) the same way grammar.mjs's T5 and
|
|
2959
|
+
// metaTermOf do — BARE_WHATIS_RE's capture is otherwise the literal glued
|
|
2960
|
+
// tail, verbatim.
|
|
2961
|
+
if (m) metaTerm = stripTrailingScopeFiller(m[1]);
|
|
2635
2962
|
}
|
|
2636
2963
|
if (metaTerm) {
|
|
2637
2964
|
// BUG 1 fix: "what is a tree used for" parses (grammar.mjs T5) to the
|
|
@@ -2682,15 +3009,93 @@ async function factAnswer(memoryDir, query, envelope, miss) {
|
|
|
2682
3009
|
const know = q.match(KNOW_ABOUT_RE);
|
|
2683
3010
|
if (know) {
|
|
2684
3011
|
const variants = factTermVariants(normFactTerm, know[1]);
|
|
2685
|
-
const
|
|
3012
|
+
const rows = await memoryFacts(memoryDir);
|
|
3013
|
+
// Bug E subtype walk (operator follow-up request, this session): a
|
|
3014
|
+
// cycle-safe BFS DOWNWARD over isa-family facts from the term's own
|
|
3015
|
+
// variants — every fact whose OBJECT is in the current frontier
|
|
3016
|
+
// contributes its SUBJECT as a known SUBTYPE (and the next hop's
|
|
3017
|
+
// frontier), so "every widget is a component" + "button is a widget" +
|
|
3018
|
+
// "button has a blue-color" lets "what do you know about component"
|
|
3019
|
+
// surface the button fact too, even though "button" never literally
|
|
3020
|
+
// mentions "component". Capped at 8 hops — this is a listing operation,
|
|
3021
|
+
// not findIsaChain's strict maxHops:2 proof-chase, but still bounded as a
|
|
3022
|
+
// safety net against pathological data.
|
|
3023
|
+
//
|
|
3024
|
+
// The chain itself is walked over TAUGHT isa facts only (same "isTaught"
|
|
3025
|
+
// discipline the live cax-sco/scm-sco proof chase already uses, below) —
|
|
3026
|
+
// the bulk background corpus (thousands of ConceptNet/seon "is a kind of"
|
|
3027
|
+
// rows) would otherwise chain almost ANY term into hundreds of coincidental
|
|
3028
|
+
// "subtypes" that have nothing to do with what the OPERATOR actually
|
|
3029
|
+
// taught, drowning the real answer and defeating the negative-case
|
|
3030
|
+
// discipline this feature exists to preserve. The literal-mention hits
|
|
3031
|
+
// (the ORIGINAL, non-subtype half of the filter below) still include
|
|
3032
|
+
// corpus facts exactly as before — only the SUBTYPE DISCOVERY chain is
|
|
3033
|
+
// taught-only.
|
|
3034
|
+
// `rows` here is memoryFacts()'s plain {subject,predicate,object,provenance}
|
|
3035
|
+
// shape (no `sourceTypes` — that's factRows()'s own trust-enriched shape),
|
|
3036
|
+
// so the taught/corpus distinction reads the SAME provenance-string
|
|
3037
|
+
// convention renderFactLine already keys its own corpus-vs-taught framing
|
|
3038
|
+
// on, just above.
|
|
3039
|
+
const isTaughtFact = (f) => !String(f.provenance || "").includes("corpus:") && !String(f.provenance || "").includes("web:");
|
|
3040
|
+
const isaRows = rows.filter((f) => ISA_PREDICATES.has(f.predicate) && isTaughtFact(f));
|
|
3041
|
+
const subtypeSubjects = new Set();
|
|
3042
|
+
let frontier = variants;
|
|
3043
|
+
for (let hop = 0; hop < 8 && frontier.size; hop += 1) {
|
|
3044
|
+
const nextSubjects = new Set();
|
|
3045
|
+
for (const f of isaRows) {
|
|
3046
|
+
if (frontier.has(f.object) && !subtypeSubjects.has(f.subject)) nextSubjects.add(f.subject);
|
|
3047
|
+
}
|
|
3048
|
+
if (!nextSubjects.size) break;
|
|
3049
|
+
for (const s of nextSubjects) subtypeSubjects.add(s);
|
|
3050
|
+
const nextFrontier = new Set();
|
|
3051
|
+
for (const s of nextSubjects) for (const v of factTermVariants(normFactTerm, s)) nextFrontier.add(v);
|
|
3052
|
+
frontier = nextFrontier;
|
|
3053
|
+
}
|
|
3054
|
+
let hits = rows.filter((f) => variants.has(f.subject) || variants.has(f.object) || subtypeSubjects.has(f.subject));
|
|
3055
|
+
// Tier-5 playtest fallback: a taught fact's subject is often a real NOUN
|
|
3056
|
+
// PHRASE ("logger module", "tasks handler"), but a natural follow-up
|
|
3057
|
+
// shortens it to one head word ("what do you know about the logger") —
|
|
3058
|
+
// an exact-variant miss above, since "logger" !== "logger module". Only
|
|
3059
|
+
// tried when the exact/subtype pass found NOTHING (never overrides a real
|
|
3060
|
+
// hit), and only on a whole WORD (length >= 4, the same floor
|
|
3061
|
+
// resolveObject's own tier-3/5 containment checks use to keep a short
|
|
3062
|
+
// staccato word from hijacking an unrelated fact) shared between the
|
|
3063
|
+
// query term and a fact's subject/object — a listing/discovery feature
|
|
3064
|
+
// (like the subtype walk above), not a yes/no claim, so a slightly wider
|
|
3065
|
+
// recall net is consistent with this lane's existing inclusiveness.
|
|
3066
|
+
if (!hits.length) {
|
|
3067
|
+
const queryWords = normFactTerm(know[1]).split(/\s+/).filter((w) => w.length >= 4 && !GENERIC_ENTITY_WORDS.has(w));
|
|
3068
|
+
if (queryWords.length) {
|
|
3069
|
+
const wordsOf = (s) => new Set(String(s || "").split(/\s+/));
|
|
3070
|
+
const overlaps = (term) => { const w = wordsOf(term); return queryWords.some((qw) => w.has(qw)); };
|
|
3071
|
+
hits = rows.filter((f) => overlaps(f.subject) || overlaps(f.object));
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
// A genuinely empty result here is a real miss (Tier-5 playtest cycle 3:
|
|
3075
|
+
// "what do you know about the last commit" needs a TEACH-OFFER, not a
|
|
3076
|
+
// bare wall — added as a LATE runTurn-level addition, below, alongside
|
|
3077
|
+
// the sibling "what is X" offer, rather than returned from here: an
|
|
3078
|
+
// early return through this function's normal contract would pre-empt
|
|
3079
|
+
// runTurn's own wall-shortening pass (shortMissHint/lane 5), leaving the
|
|
3080
|
+
// FULL unshortened grammar cheat-sheet standing under the offer instead
|
|
3081
|
+
// of the nicer tailored one-liner — found live while adding this fix).
|
|
2686
3082
|
if (!hits.length) return null;
|
|
2687
3083
|
// echo the STORED spelling ("caches" asked → "cache" known), never a guess
|
|
2688
|
-
const
|
|
3084
|
+
const literalHit = hits.find((f) => variants.has(f.subject) || variants.has(f.object));
|
|
3085
|
+
const term = literalHit
|
|
3086
|
+
? (variants.has(literalHit.subject) ? literalHit.subject : literalHit.object)
|
|
3087
|
+
: know[1].trim();
|
|
3088
|
+
// when a subtype-derived hit contributed something a plain literal-mention
|
|
3089
|
+
// match wouldn't have found, say so — lets the reader tell subtype-derived
|
|
3090
|
+
// facts apart from literal mentions.
|
|
3091
|
+
const viaSubtype = hits.some((f) => subtypeSubjects.has(f.subject) && !variants.has(f.subject) && !variants.has(f.object));
|
|
2689
3092
|
const lines = hits.map((f) => ` ${renderFactLine(f)}`);
|
|
2690
3093
|
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
2691
3094
|
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
2692
3095
|
const extra = rest.length ? `\n …and ${rest.length} more — say 'more' to see them.` : "";
|
|
2693
|
-
|
|
3096
|
+
const header = `${hits.length} remembered fact${hits.length === 1 ? "" : "s"} about ${term}`
|
|
3097
|
+
+ `${viaSubtype ? " (including its known subtypes)" : ""}:`;
|
|
3098
|
+
return { text: `${header}\n${shown.join("\n")}${extra}`, replace: true, ...(rest.length ? { pending: { items: rest.map((l) => l.trim()), noun: "facts" } } : {}) };
|
|
2694
3099
|
}
|
|
2695
3100
|
return null;
|
|
2696
3101
|
}
|
|
@@ -2816,6 +3221,45 @@ const KIND_OF_RE = /^what\s+kind\s+of\s+(?:thing|class|type|category|entity)?\s*
|
|
|
2816
3221
|
/** "who owns <X>" / "who maintains <X>" — the closed ownership read-back over
|
|
2817
3222
|
* the teach lane's mgx:ownedBy facts. */
|
|
2818
3223
|
const WHO_OWNS_RE = /^who\s+(?:owns|maintains)\s+(.+?)[?.!\s]*$/i;
|
|
3224
|
+
/** "does/did <Name> own/maintain <X>" — the yes/no ownership claim over the
|
|
3225
|
+
* SAME mgx:ownedBy facts WHO_OWNS_RE reads (Tier-5 playtest fix). The bare
|
|
3226
|
+
* infinitive after do-support ("does X own Y", never "does X owns Y") mirrors
|
|
3227
|
+
* GENERAL_VERB_YESNO_RE's own do-support convention. */
|
|
3228
|
+
const OWNS_YESNO_RE = /^(?:does|did)\s+([\w'-]+)\s+(?:owns?|maintains?)\s+(.+?)[?.!\s]*$/i;
|
|
3229
|
+
/** "is/are/was/were <X> owned by <Name>" — the PASSIVE yes/no ownership
|
|
3230
|
+
* claim, sibling of OWNS_YESNO_RE just above and OWNS_PASSIVE_TEACH_RE
|
|
3231
|
+
* (chat.mjs's teach lane) — same mgx:ownedBy facts, matched BEFORE
|
|
3232
|
+
* IS_ADJECTIVE_YESNO_RE below (which would otherwise also match this shape,
|
|
3233
|
+
* backtracking "owned by" into its own subject capture and "<Name>" into its
|
|
3234
|
+
* adjective slot, silently declining rather than answering). */
|
|
3235
|
+
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;
|
|
3236
|
+
/** "is/are/was/were <X> <adjective>" — a yes/no claim over a taught
|
|
3237
|
+
* mgx:hasProperty fact (Tier-5 playtest fix). Deliberately has NO marker
|
|
3238
|
+
* between subject and complement — "a"/"an"/"a kind of"/"a type of" is
|
|
3239
|
+
* ISA_ASK_RE's own mandatory territory just below (matched and handled, or
|
|
3240
|
+
* matched-and-declined, BEFORE this code ever runs), so a genuine "is a
|
|
3241
|
+
* module a component" is never reachable here. Anaphoric "it"/"this"/"that"
|
|
3242
|
+
* resolves against the session's current FOCUS (threaded in as `focusLabel`)
|
|
3243
|
+
* — never a guess when there's no standing focus. Only ever answers "yes"
|
|
3244
|
+
* (a real fact found) or DECLINES (null) — never a fabricated closed-world
|
|
3245
|
+
* "no", unlike its ownership/general-verb siblings above: a bare copula
|
|
3246
|
+
* ("is this good", "is it done") is the single most common CASUAL English
|
|
3247
|
+
* shape, so a wrong-feeling "no — no remembered fact says X is Y" for
|
|
3248
|
+
* ordinary small talk would be worse than deferring to the ordinary
|
|
3249
|
+
* cascade/orientation nudge that already handles it. */
|
|
3250
|
+
const IS_ADJECTIVE_YESNO_RE = /^(?:is|are|was|were)\s+(.+?)\s+([A-Za-z][\w-]*)[?.!\s]*$/i;
|
|
3251
|
+
const IS_ADJECTIVE_PRONOUN_RE = /^(?:it|this|that)$/i;
|
|
3252
|
+
/** The TEACH-OFFER for a subject IS_ADJECTIVE_YESNO_RE resolved but has no
|
|
3253
|
+
* fact about at all (Tier-5 playtest, cycle 2) — the offered "remember that
|
|
3254
|
+
* X is Y" phrasing is verified in-state: TEACH_PROPERTY_RE's own subject
|
|
3255
|
+
* capture is unbounded multi-word with no lexicon gate on the complement, so
|
|
3256
|
+
* this always actually stores, unlike the bare unwrapped form (which only
|
|
3257
|
+
* reaches TEACH_PROPERTY_RE via BARE_DECLARATIVE_RE's single-token-subject
|
|
3258
|
+
* restriction and would fail here). */
|
|
3259
|
+
const unknownAdjectiveOffer = (subject, adjective) => ({
|
|
3260
|
+
text: `I don't know anything about "${subject}" yet — teach me directly, e.g. "remember that ${subject.toLowerCase()} is ${adjective}".`,
|
|
3261
|
+
replace: true,
|
|
3262
|
+
});
|
|
2819
3263
|
/** WHOLE-STORE recall (CHATBENCH_006 lever 3): "what did i tell you [last time]",
|
|
2820
3264
|
* "what facts do you know", "what do you remember" — list EVERY remembered fact
|
|
2821
3265
|
* (no subject/object term to filter on), cited, higher-trust first. The multi-turn
|
|
@@ -2881,13 +3325,80 @@ function inheritsChain(graph, startId) {
|
|
|
2881
3325
|
* "what kind of thing is an X" reports X's own type (subject-side first).
|
|
2882
3326
|
* Miss-only and run AFTER factAnswer returns null, so it never shadows the
|
|
2883
3327
|
* subject-side answer or a schema hit. Returns { text, replace:true } or null. */
|
|
2884
|
-
async function factReadBack(memoryDir, query, envelope, miss, graph = null) {
|
|
3328
|
+
async function factReadBack(memoryDir, query, envelope, miss, graph = null, focusLabel = null) {
|
|
2885
3329
|
if (!miss) return null;
|
|
2886
3330
|
let normFactTerm;
|
|
2887
3331
|
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
2888
3332
|
const q = String(query).trim();
|
|
3333
|
+
// Tier-5 playtest fix (cycle 4), found live: "actually is the store module
|
|
3334
|
+
// fragile" WALLED — a leading hedge adverb ("actually"/"really"/"honestly",
|
|
3335
|
+
// optionally comma'd) put the sentence one word out of alignment with
|
|
3336
|
+
// IS_ADJECTIVE_YESNO_RE/OWNS_YESNO_RE/OWNS_PASSIVE_YESNO_RE's own anchored
|
|
3337
|
+
// "is|are|was|were|does|did" openers — this session's own three new yes/no
|
|
3338
|
+
// readers, so scoped narrowly to just them (qHedge), not the older
|
|
3339
|
+
// ISA_ASK_RE/WHO_OWNS_RE paths above, which already work without it and
|
|
3340
|
+
// don't need the extra risk of a behavior change. Full leading-connective
|
|
3341
|
+
// tolerance (and/also/so/…) is STACCATO_LEAKED_CONNECTIVES' own separate,
|
|
3342
|
+
// broader territory elsewhere in this file — this is a narrower, adjacent
|
|
3343
|
+
// closed set (hedge adverbs, not coordinators).
|
|
3344
|
+
// "yeah nah" (Tier 6 playtest, §3b dialect axis): the same AU/NZ discourse
|
|
3345
|
+
// opener chat.mjs's own GREET closed set and GREETING_PREAMBLE_RE already
|
|
3346
|
+
// recognize elsewhere, added here too — "yeah nah, is TaskController
|
|
3347
|
+
// fragile" is the SAME one-word-out-of-alignment problem the hedge adverbs
|
|
3348
|
+
// above were fixed for, just a dialect opener instead of a hedge adverb.
|
|
3349
|
+
const qHedge = q.replace(/^(?:actually|really|honestly|yeah\s+nah)\s*,?\s+/i, "");
|
|
2889
3350
|
const rows = await factRows(memoryDir);
|
|
2890
|
-
if (!rows.length)
|
|
3351
|
+
if (!rows.length) {
|
|
3352
|
+
// Tier-5 playtest fix (cycle 2), found live: with TRULY zero facts
|
|
3353
|
+
// remembered yet (a fresh session, nothing taught at all), the early
|
|
3354
|
+
// bail-out below skipped even IS_ADJECTIVE_YESNO_RE's own "subject
|
|
3355
|
+
// completely unknown" TEACH-OFFER further down in this function — "is
|
|
3356
|
+
// the checkout flow deprecated" as someone's genuinely FIRST question
|
|
3357
|
+
// fell to the raw structural wall, unguided. Special-cased here (ahead
|
|
3358
|
+
// of the general empty-memory bail-out every other lane in this function
|
|
3359
|
+
// still relies on) rather than removing the bail-out outright.
|
|
3360
|
+
//
|
|
3361
|
+
// IS_ADJECTIVE_YESNO_RE's own backtracking (no vocabulary restriction on
|
|
3362
|
+
// either capture) means it ALSO syntactically matches shapes that are
|
|
3363
|
+
// NOT a property claim at all — "is a zebra a mammal" (ISA_ASK_RE's own
|
|
3364
|
+
// territory, tried first in the non-empty-rows path below, so never
|
|
3365
|
+
// reached here) and "is there anything bigger" (an existence/staccato-
|
|
3366
|
+
// comparative shape a LATER lane elsewhere in runTurn owns and answers
|
|
3367
|
+
// better than a teach-offer ever could) both regressed real, pinned
|
|
3368
|
+
// tests on first attempt at this fix — caught by running the full suite,
|
|
3369
|
+
// not just the live playtest transcript. Excluded explicitly: ISA_ASK_RE
|
|
3370
|
+
// matches take the SAME priority here they get in the non-empty-rows
|
|
3371
|
+
// path below, and a leading "there" is existential, never a real named
|
|
3372
|
+
// subject a property claim would name.
|
|
3373
|
+
if (!ISA_ASK_RE.test(qHedge)) {
|
|
3374
|
+
const emptyIsAdj = qHedge.match(IS_ADJECTIVE_YESNO_RE);
|
|
3375
|
+
if (emptyIsAdj) {
|
|
3376
|
+
const rawSubject = emptyIsAdj[1].trim();
|
|
3377
|
+
const subject = IS_ADJECTIVE_PRONOUN_RE.test(rawSubject) ? (focusLabel || null) : rawSubject;
|
|
3378
|
+
// Tier 6 playtest: "is logger tested"/"is the store module tested" —
|
|
3379
|
+
// IS_ADJECTIVE_YESNO_RE's own unrestricted backtracking (already flagged
|
|
3380
|
+
// as a recurring risk, see this branch's own docblock above for the
|
|
3381
|
+
// ISA_ASK_RE/"is there" exclusions found the SAME way) ALSO matches
|
|
3382
|
+
// "tested" as if it were a free-form property adjective — but "tested"/
|
|
3383
|
+
// "covered"/"untested"/"uncovered" are REAL structural relation words
|
|
3384
|
+
// (PASSIVE_PARTICIPLE_TO_KIND/QUALIFIERS, ask-vocab.mjs) with an actual
|
|
3385
|
+
// graph-computable meaning ask()'s own grammar already resolved
|
|
3386
|
+
// (envelope.parsed stands — a genuine "tests" reverse-relation
|
|
3387
|
+
// traversal, hit or honest empty). Offering "I don't know that yet —
|
|
3388
|
+
// teach me" here would silently DISCARD a real, honest structural
|
|
3389
|
+
// answer in favor of an irrelevant memory teach-offer — the opposite
|
|
3390
|
+
// failure from every other exclusion in this function (a wrong
|
|
3391
|
+
// OVER-eager offer, not a missed one). Declines only when a real parse
|
|
3392
|
+
// already stood; "is the checkout flow deprecated" (this branch's own
|
|
3393
|
+
// ORIGINAL T8 target — "deprecated" has no structural meaning at all)
|
|
3394
|
+
// has no envelope.parsed to defer to, so it is untouched.
|
|
3395
|
+
if (subject && !/^there\b/i.test(subject) && !envelope?.parsed) {
|
|
3396
|
+
return unknownAdjectiveOffer(subject, emptyIsAdj[2].trim().toLowerCase());
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
return null;
|
|
3401
|
+
}
|
|
2891
3402
|
const isa = rows.filter((f) => ISA_PREDICATES.has(f.predicate));
|
|
2892
3403
|
const byTrust = (a, b) => b.trust - a.trust;
|
|
2893
3404
|
const renderMany = (hits) => {
|
|
@@ -2993,6 +3504,181 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null) {
|
|
|
2993
3504
|
return renderMany(hits);
|
|
2994
3505
|
}
|
|
2995
3506
|
|
|
3507
|
+
// (a2b) OWNERSHIP yes/no — "does/did <Name> own/maintain <X>": Tier-5
|
|
3508
|
+
// playtest fix, found live — WHO_OWNS_RE only ever answered the OPEN "who
|
|
3509
|
+
// owns X" form; a direct yes/no claim about a specific (owner, thing) pair
|
|
3510
|
+
// ("does margo maintain the tasks handler") fell all the way through to the
|
|
3511
|
+
// structural code-graph wall, even right after teaching exactly that fact,
|
|
3512
|
+
// because GENERAL_VERB_QUERY_EXCLUDE_RE deliberately stands the general-verb
|
|
3513
|
+
// reader down for "own"/"maintain" (they mint a DIFFERENT predicate,
|
|
3514
|
+
// mgx:own/mgx:maintain, than this frame's own OWNED_BY_PREDICATE) — this is
|
|
3515
|
+
// that missing specific reader. Same closed-world convention as (a3)'s
|
|
3516
|
+
// general-verb yes/no just below: a hit answers "yes", no stored fact
|
|
3517
|
+
// answers a definite "no" (never a guessed owner name, unlike the OPEN "who
|
|
3518
|
+
// owns" form above, which stays an honest miss rather than guess WHO).
|
|
3519
|
+
const ownsYN = qHedge.match(OWNS_YESNO_RE);
|
|
3520
|
+
if (ownsYN) {
|
|
3521
|
+
const [, ownerRaw, thingRaw] = ownsYN;
|
|
3522
|
+
const ownerVariants = factTermVariants(normFactTerm, ownerRaw.trim());
|
|
3523
|
+
const thingVariants = factTermVariants(normFactTerm, thingRaw.replace(/^an?\s+/i, "").trim());
|
|
3524
|
+
const hit = rows
|
|
3525
|
+
.filter((f) => f.predicate === OWNED_BY_PREDICATE && thingVariants.has(f.subject) && ownerVariants.has(f.object))
|
|
3526
|
+
.sort(byTrust)[0];
|
|
3527
|
+
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
3528
|
+
return {
|
|
3529
|
+
text: `no — no remembered fact says ${ownerRaw.trim().toLowerCase()} owns/maintains ${thingRaw.trim()}.`,
|
|
3530
|
+
replace: true,
|
|
3531
|
+
};
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
// (a2b-ii) PASSIVE ownership yes/no — "is/are/was/were <X> owned by <Name>"
|
|
3535
|
+
// (Tier-5 playtest, cycle 2): same OWNED_BY_PREDICATE facts as (a2b) above,
|
|
3536
|
+
// just the passive phrasing — "is TaskController owned by sam" found live
|
|
3537
|
+
// to WALL entirely (no recognizer at all, teach OR read, for the passive
|
|
3538
|
+
// shape) even right after teaching that exact fact via OWNS_PASSIVE_TEACH_RE.
|
|
3539
|
+
// Checked BEFORE (a2c)'s adjective reader, which would otherwise also match
|
|
3540
|
+
// this shape (backtracking "owned by" into its subject and the owner name
|
|
3541
|
+
// into its adjective slot) and silently decline instead of answering.
|
|
3542
|
+
const ownsPassiveYN = qHedge.match(OWNS_PASSIVE_YESNO_RE);
|
|
3543
|
+
if (ownsPassiveYN) {
|
|
3544
|
+
const [, thingRaw, ownerRaw] = ownsPassiveYN;
|
|
3545
|
+
const thingVariants = factTermVariants(normFactTerm, thingRaw.replace(/^an?\s+/i, "").trim());
|
|
3546
|
+
const ownerVariants = factTermVariants(normFactTerm, ownerRaw.trim());
|
|
3547
|
+
const hit = rows
|
|
3548
|
+
.filter((f) => f.predicate === OWNED_BY_PREDICATE && thingVariants.has(f.subject) && ownerVariants.has(f.object))
|
|
3549
|
+
.sort(byTrust)[0];
|
|
3550
|
+
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
3551
|
+
return {
|
|
3552
|
+
text: `no — no remembered fact says ${thingRaw.trim().toLowerCase()} is owned by ${ownerRaw.trim()}.`,
|
|
3553
|
+
replace: true,
|
|
3554
|
+
};
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
// (a2c) PROPERTY yes/no — "is/are/was/were <X> <adjective>": Tier-5 playtest
|
|
3558
|
+
// fix, found live — "remember that the logger module is deprecated" taught a
|
|
3559
|
+
// real mgx:hasProperty fact, but there was no direct-question reader for it
|
|
3560
|
+
// AT ALL (only presuppositionNudge's own narrow "why does X still Y" embeds
|
|
3561
|
+
// this same check) — "is the logger deprecated" fell straight to the
|
|
3562
|
+
// structural wall. Checks BOTH shapes a taught "<X> is <adjective>" can land
|
|
3563
|
+
// as (mirrors presuppositionNudge's own dual check, above): the teach lane's
|
|
3564
|
+
// mgx:hasProperty fact, or — when the adjective is a known ACE-OWL lexicon
|
|
3565
|
+
// data-property word — the ACE grammar's own tmct:<adjective> "true" triple.
|
|
3566
|
+
// "it"/"this"/"that" resolve against `focusLabel` — a bare pronoun with no
|
|
3567
|
+
// standing focus declines (null), same discipline STACCATO_PRONOUN_RE uses.
|
|
3568
|
+
const isAdj = qHedge.match(IS_ADJECTIVE_YESNO_RE);
|
|
3569
|
+
if (isAdj) {
|
|
3570
|
+
const rawSubject = isAdj[1].trim();
|
|
3571
|
+
const subject = IS_ADJECTIVE_PRONOUN_RE.test(rawSubject) ? (focusLabel || null) : rawSubject;
|
|
3572
|
+
const adjective = isAdj[2].trim().toLowerCase();
|
|
3573
|
+
if (subject) {
|
|
3574
|
+
const subjVariants = factTermVariants(normFactTerm, subject);
|
|
3575
|
+
const propertyMatch = (f) => (f.predicate === HAS_PROPERTY_PREDICATE && normFactTerm(f.object) === adjective)
|
|
3576
|
+
|| (f.predicate === `tmct:${adjective}` && f.object === "true");
|
|
3577
|
+
// Same head-word fallback as factAnswer's "(c) what do you know about"
|
|
3578
|
+
// lane: a taught subject is often a real noun PHRASE ("logger module"),
|
|
3579
|
+
// shortened in the natural follow-up ("is the logger deprecated") — an
|
|
3580
|
+
// exact-variant miss on its own, on a whole word (length >= 4, the same
|
|
3581
|
+
// floor used elsewhere) shared between the query subject and the fact's
|
|
3582
|
+
// own subject.
|
|
3583
|
+
const subjWords = normFactTerm(subject).split(/\s+/).filter((w) => w.length >= 4 && !GENERIC_ENTITY_WORDS.has(w));
|
|
3584
|
+
const wordOverlap = (f) => subjWords.some((w) => new Set(String(f.subject || "").split(/\s+/)).has(w));
|
|
3585
|
+
const subjectMatch = (f) => subjVariants.has(f.subject) || (subjWords.length && wordOverlap(f));
|
|
3586
|
+
const hit = rows.filter((f) => subjectMatch(f) && propertyMatch(f)).sort(byTrust)[0];
|
|
3587
|
+
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
3588
|
+
// no hit on THIS property — never a guessed "no" (see
|
|
3589
|
+
// IS_ADJECTIVE_YESNO_RE's own docblock for why this stays silent on a
|
|
3590
|
+
// truth claim, unlike its ownership/general-verb siblings above). But a
|
|
3591
|
+
// subject we DO know something else about ("the logger" has a
|
|
3592
|
+
// deprecated-fact, just not a fast-fact) still deserves an honest, named
|
|
3593
|
+
// receipt — never a bare wall — mirroring factAnswer's own established
|
|
3594
|
+
// convention for a known-subject/wrong-predicate miss ("I don't have
|
|
3595
|
+
// any 'X' facts about Y"). The SAME subjectMatch (exact-variant OR
|
|
3596
|
+
// head-word overlap) decides "known", so a shortened/article-led
|
|
3597
|
+
// subject that found its fact via the overlap fallback is recognized
|
|
3598
|
+
// as known too. A subject with NO known facts at all falls through
|
|
3599
|
+
// undecided — there's nothing honest to say beyond the ordinary
|
|
3600
|
+
// cascade's own miss/orientation nudge.
|
|
3601
|
+
//
|
|
3602
|
+
// Tier 6 playtest: gated on `!envelope?.parsed`, the SAME guard this
|
|
3603
|
+
// function's empty-memory branch above just added, for the identical
|
|
3604
|
+
// reason — "is the logger tested" (after teaching an UNRELATED
|
|
3605
|
+
// "logger... is deprecated" fact) used to return "I don't have a fact
|
|
3606
|
+
// saying the logger is tested" here, discarding a REAL structural
|
|
3607
|
+
// answer ("No tests cover logger") for a word ("tested") that already
|
|
3608
|
+
// has genuine graph-computable meaning. A subject with a KNOWN taught
|
|
3609
|
+
// fact under some OTHER, non-structural property (the common,
|
|
3610
|
+
// originally-intended case here) still gets this receipt exactly as
|
|
3611
|
+
// before, since envelope.parsed is null for those adjectives.
|
|
3612
|
+
if (rows.some(subjectMatch) && !envelope?.parsed) {
|
|
3613
|
+
return { text: `I don't have a fact saying ${subject.toLowerCase()} is ${adjective}.`, replace: true };
|
|
3614
|
+
}
|
|
3615
|
+
// Tier-5 playtest fix (cycle 2), found live: "is the checkout flow
|
|
3616
|
+
// deprecated" as a genuinely FIRST-EVER question about a subject tmct
|
|
3617
|
+
// has never heard of (no fact at all, not even under a different
|
|
3618
|
+
// property) fell through to the raw structural wall, unguided — the
|
|
3619
|
+
// exact "honest 'I don't know that yet' offers to learn" case
|
|
3620
|
+
// SKILL_CHAT_PLAYTEST.md's Tier 5 (§3) itself names. The offered
|
|
3621
|
+
// phrasing is the SAME verified "remember that X is Y" wrapped form
|
|
3622
|
+
// TEACH_PROPERTY_RE actually accepts (arbitrary-length subject, no
|
|
3623
|
+
// lexicon gate on the complement) — never the bare unwrapped form,
|
|
3624
|
+
// which TEACH_PROPERTY_RE only reaches via BARE_DECLARATIVE_RE's own
|
|
3625
|
+
// single-token-subject restriction and would fail for a multi-word
|
|
3626
|
+
// subject like this one. Same helper (unknownAdjectiveOffer) the
|
|
3627
|
+
// empty-memory special-case above this function's own rows.length
|
|
3628
|
+
// bail-out reuses, so the two paths can never disagree on wording.
|
|
3629
|
+
//
|
|
3630
|
+
// Tier 6 playtest: same `!envelope?.parsed` guard as just above — a
|
|
3631
|
+
// subject known only under an UNRELATED property (e.g. "deprecated")
|
|
3632
|
+
// must not offer to teach "tested" when ask()'s own grammar already
|
|
3633
|
+
// resolved it structurally.
|
|
3634
|
+
if (!envelope?.parsed) return unknownAdjectiveOffer(subject, adjective);
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
// (a3) GENERAL VERB-TO-PREDICATE direct-question retrieval (item 5, this
|
|
3639
|
+
// session): a taught general-verb fact ("margo eats ribs") answered back
|
|
3640
|
+
// directly. Yes/no form matches the taught triple EXACTLY (subject +
|
|
3641
|
+
// predicate + object, via the SAME factTermVariants/normFactTerm matching
|
|
3642
|
+
// WHO_OWNS_RE just used above) — no match is an honest, closed-world "no",
|
|
3643
|
+
// never a guess. Open form lists every stored fact row for {subject,
|
|
3644
|
+
// predicate} regardless of object.
|
|
3645
|
+
const genYN = q.match(GENERAL_VERB_YESNO_RE);
|
|
3646
|
+
if (genYN && !GENERAL_VERB_ANYWHERE_EXCLUDE_RE.test(q)) {
|
|
3647
|
+
const [, subjectRaw, verbRaw, objectRaw] = genYN;
|
|
3648
|
+
const verb = verbRaw.toLowerCase();
|
|
3649
|
+
if (!GENERAL_VERB_EXCLUDE_RE.test(verb) && !GENERAL_VERB_QUERY_EXCLUDE_RE.test(verb)) {
|
|
3650
|
+
const subject = subjectRaw.trim();
|
|
3651
|
+
const object = objectRaw.replace(/^an?\s+/i, "").trim();
|
|
3652
|
+
if (subject && object) {
|
|
3653
|
+
const predicate = await generalVerbPredicate(verb);
|
|
3654
|
+
const subjVariants = factTermVariants(normFactTerm, subject);
|
|
3655
|
+
const objVariants = factTermVariants(normFactTerm, object);
|
|
3656
|
+
const hit = rows
|
|
3657
|
+
.filter((f) => f.predicate === predicate && subjVariants.has(f.subject) && objVariants.has(f.object))
|
|
3658
|
+
.sort(byTrust)[0];
|
|
3659
|
+
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true, generalVerbQuery: true };
|
|
3660
|
+
return {
|
|
3661
|
+
text: `no — no remembered fact says ${subject.toLowerCase()} ${predicatePhrase(predicate)} ${object}.`,
|
|
3662
|
+
replace: true, generalVerbQuery: true,
|
|
3663
|
+
};
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
const genOpen = q.match(GENERAL_VERB_OPEN_RE);
|
|
3668
|
+
if (genOpen && !GENERAL_VERB_ANYWHERE_EXCLUDE_RE.test(q)) {
|
|
3669
|
+
const [, subjectRaw, verbRaw] = genOpen;
|
|
3670
|
+
const verb = verbRaw.toLowerCase();
|
|
3671
|
+
if (!GENERAL_VERB_EXCLUDE_RE.test(verb) && !GENERAL_VERB_QUERY_EXCLUDE_RE.test(verb)) {
|
|
3672
|
+
const subject = subjectRaw.trim();
|
|
3673
|
+
if (subject) {
|
|
3674
|
+
const predicate = await generalVerbPredicate(verb);
|
|
3675
|
+
const subjVariants = factTermVariants(normFactTerm, subject);
|
|
3676
|
+
const hits = rows.filter((f) => f.predicate === predicate && subjVariants.has(f.subject)).sort(byTrust);
|
|
3677
|
+
if (hits.length) return { ...renderMany(hits), generalVerbQuery: true };
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
|
|
2996
3682
|
// (b) RECALL — "what did i tell you about X": every remembered fact mentioning X.
|
|
2997
3683
|
const told = q.match(TOLD_ABOUT_RE);
|
|
2998
3684
|
if (told) {
|
|
@@ -3326,14 +4012,42 @@ const BARE_WHATIS_RE = /^what\s+(?:is|are)\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
|
3326
4012
|
* question asks about — from the parse when present, else recognized directly
|
|
3327
4013
|
* via BARE_WHATIS_RE (article optional — see its own docblock for why that's
|
|
3328
4014
|
* safe here even though the grammar's own T5 keeps the article mandatory).
|
|
3329
|
-
* Null when the line isn't such a form.
|
|
4015
|
+
* Null when the line isn't such a form. Seonix Batch 2 Fix 3: a curated trailing
|
|
4016
|
+
* scope clause ("what is a Module in this graph") is stripped off the captured
|
|
4017
|
+
* term the same way grammar.mjs's T5 does (stripTrailingScopeFiller,
|
|
4018
|
+
* ask-vocab.mjs) — the envelope.parsed.object branch above already carries a
|
|
4019
|
+
* trimmed term when it came from that template, so the strip here only needs to
|
|
4020
|
+
* cover this function's own regex fallback. */
|
|
3330
4021
|
function metaTermOf(query, envelope) {
|
|
3331
4022
|
if (envelope?.parsed?.shape === "meta" && envelope.parsed.object) return envelope.parsed.object;
|
|
3332
4023
|
const q = String(query).trim();
|
|
3333
4024
|
const m = q.match(BARE_WHATIS_RE)
|
|
3334
4025
|
|| q.match(/^what\s+(?:does|do)\s+(?:an?\s+)?(.+?)\s+means?[?.!\s]*$/i)
|
|
3335
4026
|
|| q.match(/^define\s+(?:an?\s+)?(.+?)[?.!\s]*$/i);
|
|
3336
|
-
return m ? m[1].trim() : null;
|
|
4027
|
+
return m ? stripTrailingScopeFiller(m[1].trim()) : null;
|
|
4028
|
+
}
|
|
4029
|
+
|
|
4030
|
+
/** The TEACH-OFFER line for a term that's genuinely unknown everywhere (Tier-5
|
|
4031
|
+
* playtest fix): "I don't know 'X' yet — teach me directly, e.g. …". The
|
|
4032
|
+
* concrete example is worded by WORD COUNT, verified in-state
|
|
4033
|
+
* (SKILL_CHAT_PLAYTEST.md §4's own rule) — an unwrapped bare declarative
|
|
4034
|
+
* only stores for a single-token subject (BARE_DECLARATIVE_RE's own scope);
|
|
4035
|
+
* the wrapped "remember X is a Y" form tolerates up to a two-token subject
|
|
4036
|
+
* (unknownSubjectFallback's UNKNOWN_SUBJECT_RE). A 3+-word term fits
|
|
4037
|
+
* neither shape — never offer a concrete example that would itself fail,
|
|
4038
|
+
* the plain nudge to teach it still guides, honestly. Shared by runTurn's
|
|
4039
|
+
* own "what is X" miss nudge and factAnswer's "what do you know about X"
|
|
4040
|
+
* miss nudge, below, so the two can never disagree on wording. */
|
|
4041
|
+
function unknownVocabTermOffer(term) {
|
|
4042
|
+
const article = /^[aeiou]/i.test(term) ? "an" : "a";
|
|
4043
|
+
const words = term.trim().split(/\s+/);
|
|
4044
|
+
const remember = `remember ${term} is ${article} <thing>`;
|
|
4045
|
+
const example = words.length === 1
|
|
4046
|
+
? `"${term} is ${article} <thing>" or "${remember}"`
|
|
4047
|
+
: words.length === 2
|
|
4048
|
+
? `"${remember}"`
|
|
4049
|
+
: null;
|
|
4050
|
+
return `I don't know "${term}" yet — teach me directly${example ? `, e.g. ${example}` : ` (e.g. "remember <name> is ${article} <thing>")`}.`;
|
|
3337
4051
|
}
|
|
3338
4052
|
|
|
3339
4053
|
/** The curated SEON definition to PREFER for a "what is a <lexicon term>", or null.
|
|
@@ -3387,6 +4101,18 @@ async function curatedDefinitionAnswer(query, envelope, { memoryDir, lexicon })
|
|
|
3387
4101
|
// SHORTHAND_CONTRACTIONS above: scoped locally to the lane that owns the word.
|
|
3388
4102
|
// Word-boundary matched so "hotel"/"intel" are untouched.
|
|
3389
4103
|
const VAGUE_TOUCH_TEL_RE = /\btel\b/i;
|
|
4104
|
+
// "abut" -> "about" (Tier 6 playtest, §3b typo axis): a one-letter-dropped
|
|
4105
|
+
// typo of THIS lane's own anchor word ("what abut imports" used to miss the
|
|
4106
|
+
// "what about …" regex entirely and search for a module literally named
|
|
4107
|
+
// "abut" instead). "about" is real English on its own (a genuine word) but is
|
|
4108
|
+
// not itself part of ask.mjs's code-graph grammar (VERB_TO_KIND/ENTITY_TO_TYPE/
|
|
4109
|
+
// anchor words) — same reasoning as VAGUE_TOUCH_TEL_RE just above, so this
|
|
4110
|
+
// stays a local, lane-scoped replace rather than a shared MISSPELLINGS entry
|
|
4111
|
+
// (test/ask-vocab.test.mjs enforces every correction TABLE value is grammar-
|
|
4112
|
+
// owned; a bare discourse word like "about" fails that gate on purpose).
|
|
4113
|
+
// Word-boundary matched so a real identifier merely containing "abut" (rare,
|
|
4114
|
+
// but e.g. "rebuttal") is untouched.
|
|
4115
|
+
const VAGUE_TOUCH_ABUT_RE = /\babut\b/i;
|
|
3390
4116
|
/** "explain X" / "please explain X" / "kindly explain X" / "explain X to me" /
|
|
3391
4117
|
* "explain X please" — a bare vague-touch shape, sibling of WHAT_ABOUT_RE
|
|
3392
4118
|
* above. Named (not inlined) so both vagueTouchTermOf (term extraction) and
|
|
@@ -3407,6 +4133,7 @@ function vagueTouchTermOf(query) {
|
|
|
3407
4133
|
// bridge frame), breaking this very regex.
|
|
3408
4134
|
let q = correctMisspellings(String(query).trim());
|
|
3409
4135
|
q = q.replace(VAGUE_TOUCH_TEL_RE, "tell");
|
|
4136
|
+
q = q.replace(VAGUE_TOUCH_ABUT_RE, "about");
|
|
3410
4137
|
q = applyPreambleFrames(q);
|
|
3411
4138
|
const m = q.match(/^(?:kindly\s+)?tell me about\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i)
|
|
3412
4139
|
|| 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 +4245,22 @@ function relationTermOf(query, envelope) {
|
|
|
3518
4245
|
* can never parse and always misses) — a drill-down chain that opens with
|
|
3519
4246
|
* "describe X" (the README's own example) used to dead-end on the very next
|
|
3520
4247
|
* "what about it"/"what about Y" turn. */
|
|
4248
|
+
// Bug F point 4 (operator follow-up request): "please tell me X" (no "about")
|
|
4249
|
+
// answers like "describe X"/"what is X" — the "tell me" branch's own "about"
|
|
4250
|
+
// is now OPTIONAL, so "please tell me Widget" reaches the same rescue "please
|
|
4251
|
+
// tell me about Widget" already did. "describe"/"what(?:'s|\s+is)? about" stay
|
|
4252
|
+
// unchanged (describe never took "about" at all; the "what about" branch
|
|
4253
|
+
// still requires it — a bare "what X" is BARE_WHATIS_RE's own territory, not
|
|
4254
|
+
// this lane's, and folding it in here would risk double-claiming that shape).
|
|
4255
|
+
// Note the trailing \s+ moved INSIDE each alternation branch (rather than one
|
|
4256
|
+
// shared \s+ after the whole group): making "about" optional inside the "tell
|
|
4257
|
+
// me" branch means that branch's own separator is sometimes owned by "me\s+"
|
|
4258
|
+
// and sometimes by "about\s+" — a single external \s+ double-counted the
|
|
4259
|
+
// separator when "about" fired (swallowing the one real space and then
|
|
4260
|
+
// requiring a second one that was never there, an always-null regex found
|
|
4261
|
+
// live while testing this fix).
|
|
3521
4262
|
const DESCRIBE_WRAPPER_RE =
|
|
3522
|
-
/^(?:(?:can|could|would)\s+you\s+(?:please\s+)?|please\s+)?(?:tell\s+me\s+(?:more\s+)?about
|
|
4263
|
+
/^(?:(?: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
4264
|
|
|
3524
4265
|
/** Bare focus pronouns this lane resolves against the STANDING focus (0.9.13
|
|
3525
4266
|
* Tier-1 playtest) — "describe that" / "tell me about it" after a prior turn
|
|
@@ -3542,14 +4283,77 @@ const DESCRIBE_PRONOUN_RE = /^(?:it|that|this|those|them)$/i;
|
|
|
3542
4283
|
* unaffected either way. */
|
|
3543
4284
|
const STACCATO_PRONOUN_RE = /^(?:and|also|so|then|now)\s+(it|that|this|those|them)(?:\s+ones?)?\s*\??$/i;
|
|
3544
4285
|
|
|
3545
|
-
|
|
3546
|
-
|
|
4286
|
+
/** Tier 6 playtest: "describe the logger module"/"describe the Task class" —
|
|
4287
|
+
* dispatchTool("tmct_describe") resolves its `symbol` arg via codegraph.mjs's
|
|
4288
|
+
* resolveSymbol, a separate, simpler path/basename matcher with NO article- or
|
|
4289
|
+
* grain-word tolerance. A first attempt routed this whole free-text `term`
|
|
4290
|
+
* through resolveEntity/resolveObject instead (which DOES have that tolerance,
|
|
4291
|
+
* just added above) — reverted live, found via this same playtest cycle's own
|
|
4292
|
+
* regression run: resolveObject's tier-3 ANY-overlap fallback is tuned for
|
|
4293
|
+
* near-path/near-symbol terms, not arbitrary English sentences, and a genuine
|
|
4294
|
+
* English article ("a", "the") can itself be a real one-character path
|
|
4295
|
+
* component of some fixture module ("a.mjs") — "tell me A JOKE" tier-3-matched
|
|
4296
|
+
* that module by the shared bare "a" alone (test/sessions.test.mjs's own guard
|
|
4297
|
+
* test caught it: a turn meant to fall through as an honest grammar miss
|
|
4298
|
+
* instead silently "described" an unrelated module). Scoped down to ONLY ever
|
|
4299
|
+
* attempt a resolution when the term carries an EXPLICIT trailing grain word
|
|
4300
|
+
* (module/class/function/method/…, ENTITY_TO_TYPE's own closed table) — the
|
|
4301
|
+
* class-narrowed pool that then searches is both far smaller and still
|
|
4302
|
+
* requires the head noun to actually match a stem, so it stays safe; a bare
|
|
4303
|
+
* "the X"/"an X" or ordinary sentence (no grain word) gets NO rescue attempt
|
|
4304
|
+
* at all and falls through to the untouched, always-safe resolveSymbol path,
|
|
4305
|
+
* exactly as before this fix. */
|
|
4306
|
+
const DESCRIBE_GRAIN_WORD_RE = new RegExp(
|
|
4307
|
+
`^(?:(?:the|a|an)\\s+)?(.+?)\\s+(${Object.keys(ENTITY_TO_TYPE).join("|")})$`, "i",
|
|
4308
|
+
);
|
|
4309
|
+
async function describeGrainRescue(graph, term) {
|
|
4310
|
+
if (!graph) return null;
|
|
4311
|
+
const m = String(term || "").trim().match(DESCRIBE_GRAIN_WORD_RE);
|
|
4312
|
+
if (!m) return null;
|
|
4313
|
+
const [, head, grainWord] = m;
|
|
4314
|
+
const expectedClass = ENTITY_TO_TYPE[grainWord.toLowerCase()];
|
|
4315
|
+
if (!head?.trim() || !expectedClass) return null;
|
|
4316
|
+
try {
|
|
4317
|
+
const { resolveObject } = await import("./ask.mjs");
|
|
4318
|
+
const r = resolveObject(graph, head.trim(), { expectedClass });
|
|
4319
|
+
if (r?.match?.id && !r.ambiguous) return { id: r.match.id, label: r.match.label };
|
|
4320
|
+
} catch { /* tolerated */ }
|
|
4321
|
+
return null;
|
|
4322
|
+
}
|
|
4323
|
+
|
|
4324
|
+
async function describeWrapperAnswer(query, { config, source, focus, graph }) {
|
|
4325
|
+
// Tier 6 playtest: this lane is the LAST-RESORT rescue (4d, tried after every
|
|
4326
|
+
// earlier lane declines on the ORIGINAL query) — but it tested its own
|
|
4327
|
+
// DESCRIBE_WRAPPER_RE against the RAW, un-normalized text, so a preamble an
|
|
4328
|
+
// earlier lane (relationForceAnswer/vagueTouchTermOf) already knows how to
|
|
4329
|
+
// strip ("ok cool, what about the TaskController" — relationForceAnswer
|
|
4330
|
+
// correctly declines since "TaskController" isn't an enumerable RELATION_TERM,
|
|
4331
|
+
// but never hands its own stripped text forward) reappeared here, unstripped,
|
|
4332
|
+
// and broke DESCRIBE_WRAPPER_RE's own anchor. applyPreambleFrames is the same
|
|
4333
|
+
// general-purpose, closed, idempotent pass every other lane in this file
|
|
4334
|
+
// already runs first.
|
|
4335
|
+
const q = applyPreambleFrames(String(query || "").trim());
|
|
3547
4336
|
const m = DESCRIBE_WRAPPER_RE.exec(q) || STACCATO_PRONOUN_RE.exec(q);
|
|
3548
4337
|
let term = m?.[1]?.trim();
|
|
3549
4338
|
if (!term) return null;
|
|
3550
4339
|
if (DESCRIBE_PRONOUN_RE.test(term)) {
|
|
3551
4340
|
if (!focus?.label) return null; // no standing focus to resolve against — honest decline
|
|
3552
4341
|
term = focus.label;
|
|
4342
|
+
} else {
|
|
4343
|
+
const rescued = await describeGrainRescue(graph, term);
|
|
4344
|
+
if (rescued?.label) {
|
|
4345
|
+
term = rescued.label;
|
|
4346
|
+
} else {
|
|
4347
|
+
// Tier 6 playtest: "what about the TaskController" (no grain word, just a
|
|
4348
|
+
// bare article) — resolveSymbol (codegraph.mjs) has no component/overlap
|
|
4349
|
+
// tier at all, only exact/endsWith/basename/includes checks, so a leading
|
|
4350
|
+
// "the"/"a"/"an" is pure NOISE here (unlike resolveObject's looser tiers,
|
|
4351
|
+
// there is no accidental-match risk this could introduce — stripping it
|
|
4352
|
+
// only ever REMOVES characters no real label ever contains as a match
|
|
4353
|
+
// signal). "TaskController" resolves exactly where "the TaskController"
|
|
4354
|
+
// didn't.
|
|
4355
|
+
term = term.replace(/^(?:the|a|an)\s+/i, "");
|
|
4356
|
+
}
|
|
3553
4357
|
}
|
|
3554
4358
|
try {
|
|
3555
4359
|
const text = await dispatchTool("tmct_describe", { symbol: term }, { config, source });
|
|
@@ -4008,16 +4812,28 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
4008
4812
|
// guarantees a real answer before it defers, never stranding the turn on a
|
|
4009
4813
|
// worse outcome — see isStaccatoPronounNoFocus's own docblock for the same
|
|
4010
4814
|
// discipline).
|
|
4815
|
+
// Tier-5 playtest fix (this session): "is it deprecated" is EXACTLY the same
|
|
4816
|
+
// race BUG 2 (above) fixed for "what is john" — 3 words, no code-ish token,
|
|
4817
|
+
// so isConversationalCandidate would otherwise win unconditionally and a
|
|
4818
|
+
// just-taught property fact ("logger module is deprecated") becomes
|
|
4819
|
+
// unreachable the moment it's asked back about with a short pronoun/bare
|
|
4820
|
+
// form ("is it deprecated" / "is the logger deprecated"). Widened the SAME
|
|
4821
|
+
// divert-only-on-a-real-hit gate to also try IS_ADJECTIVE_YESNO_RE shapes —
|
|
4822
|
+
// factAnswer itself declines for this shape (no metaTerm), so the only
|
|
4823
|
+
// change in practice is that factReadBack's (a2c) property lane gets a
|
|
4824
|
+
// chance to run before the orientation card claims the turn.
|
|
4825
|
+
const bareWhatisShape = BARE_WHATIS_RE.test(String(query).trim());
|
|
4826
|
+
const isAdjectiveShape = IS_ADJECTIVE_YESNO_RE.test(String(query).trim());
|
|
4011
4827
|
let bareMetaHit = null;
|
|
4012
|
-
if (isConversationalCandidate && memoryDir &&
|
|
4828
|
+
if (isConversationalCandidate && memoryDir && (bareWhatisShape || isAdjectiveShape)) {
|
|
4013
4829
|
bareMetaHit = (await factAnswer(memoryDir, query, envelope, miss))
|
|
4014
|
-
?? (await factReadBack(memoryDir, query, envelope, miss, graph));
|
|
4830
|
+
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label));
|
|
4015
4831
|
}
|
|
4016
4832
|
if (bareMetaHit) {
|
|
4017
4833
|
answer = bareMetaHit.replace ? bareMetaHit.text : `${answer}\n${bareMetaHit.text}`;
|
|
4018
4834
|
via = "fact"; recordMiss = false; handled = true;
|
|
4019
4835
|
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");
|
|
4836
|
+
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
4837
|
note(trace, "source: .tmct/memory Facts (see /memory for provenance per line)");
|
|
4022
4838
|
} else if (isConversationalCandidate) {
|
|
4023
4839
|
// A conversational miss (a greeting, "what can you do", a very short non-code
|
|
@@ -4053,7 +4869,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
4053
4869
|
// first (factAnswer), then the reverse-membership read-back (factReadBack) so an
|
|
4054
4870
|
// asserted "every X is a Y" answers "what is a Y" too.
|
|
4055
4871
|
const fact = (await factAnswer(memoryDir, query, envelope, miss))
|
|
4056
|
-
?? (await factReadBack(memoryDir, query, envelope, miss, graph));
|
|
4872
|
+
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label));
|
|
4057
4873
|
if (fact) {
|
|
4058
4874
|
answer = fact.replace ? fact.text : `${answer}\n${fact.text}`;
|
|
4059
4875
|
via = "fact";
|
|
@@ -4061,6 +4877,15 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
4061
4877
|
if (fact.pending) factPending = fact.pending; // a truncated fact list → paginable remainder
|
|
4062
4878
|
note(trace, `lane: (3) memory facts — factAnswer/factReadBack matched (memoryDir=${memoryDir})`);
|
|
4063
4879
|
note(trace, "source: .tmct/memory Facts (see /memory for provenance per line)");
|
|
4880
|
+
// Goal-line fix (item 5 follow-up, this session): mirrors the TEACH lane's
|
|
4881
|
+
// own goal revision just below (Bug 3 point 4) — `deduced` was computed
|
|
4882
|
+
// WAY above off envelope.parsed alone, but a general-verb direct question
|
|
4883
|
+
// ("does margo eat ribs") never parses as a structural graph query at all,
|
|
4884
|
+
// so it either landed on an unrelated GOAL_BY_KIND guess or nothing.
|
|
4885
|
+
if (fact.generalVerbQuery) {
|
|
4886
|
+
deduced = "look up a taught fact about a subject/verb/object";
|
|
4887
|
+
note(trace, `goal: ${deduced} (revised — a general-verb direct-question fact lookup answered this turn)`);
|
|
4888
|
+
}
|
|
4064
4889
|
} else if (miss) {
|
|
4065
4890
|
// W2: after the honest miss is composed, consult the folded-session memory. A
|
|
4066
4891
|
// relevant enough block ANSWERS — recalled Q/A framed + cited first, with the
|
|
@@ -4246,7 +5071,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
4246
5071
|
// for what would otherwise become the generic wall, never a competing route:
|
|
4247
5072
|
// it only claims the turn if /describe actually resolves the captured term.
|
|
4248
5073
|
if (miss && recordMiss && via === "composed") {
|
|
4249
|
-
const described = await describeWrapperAnswer(query, { config, source, focus: newFocus });
|
|
5074
|
+
const described = await describeWrapperAnswer(query, { config, source, focus: newFocus, graph });
|
|
4250
5075
|
if (described) {
|
|
4251
5076
|
answer = described.text; via = "describe"; recordMiss = false;
|
|
4252
5077
|
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 +5098,47 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
4273
5098
|
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
5099
|
note(trace, "intermediate: HONEST-EMPTY POLISH — the loaded graph has 0 modules, so the dead-end got a --repo/tmct init pointer appended");
|
|
4275
5100
|
}
|
|
5101
|
+
// TEACH-OFFER (Tier-5 playtest, this session) — SKILL_CHAT_PLAYTEST.md §0
|
|
5102
|
+
// names "'X' isn't a term in this graph's own vocabulary" as its own
|
|
5103
|
+
// dead-end example, and Tier 5 (§3) explicitly wants "the honest 'I don't
|
|
5104
|
+
// know that yet' that offers to learn" rather than a bare wall. A "what is
|
|
5105
|
+
// X" miss where X is genuinely unknown EVERYWHERE — not a real graph entity
|
|
5106
|
+
// (resolveEntity), not a schema/vocab term (that's what "still standing
|
|
5107
|
+
// miss" already means here), and not already in memory (checked directly,
|
|
5108
|
+
// not via factAnswer's OWN metaTerm branch, so this never duplicates its
|
|
5109
|
+
// more specific "no X facts about Y" miss) — gets a short offer appended
|
|
5110
|
+
// UNDER the existing miss text, never replacing it (every pinned assertion
|
|
5111
|
+
// on the miss wording elsewhere stays intact; this is purely additive, the
|
|
5112
|
+
// same discipline the corpus aside (W5) and empty-graph polish above use).
|
|
5113
|
+
if (recordMiss && (via === "composed" || via === "miss") && memoryDir) {
|
|
5114
|
+
// Tier-5 playtest fix (cycle 3): "what do you know about X" is its OWN
|
|
5115
|
+
// sibling shape — checked FIRST (and, unlike metaTermOf below, without a
|
|
5116
|
+
// resolveEntity(graph) gate): it's inherently a MEMORY question, not a
|
|
5117
|
+
// graph-structure one, so "nothing yet, teach me" is appropriate even
|
|
5118
|
+
// when X also happens to be a real graph entity — there's no genuinely
|
|
5119
|
+
// BETTER answer path to defer to the way "what is X" has (the concept
|
|
5120
|
+
// force, schema docs, …). Found live: "what do you know about the last
|
|
5121
|
+
// commit" (nothing in memory, and the whole sentence never fits ask.mjs's
|
|
5122
|
+
// grammar either) fell to the raw wall, unguided.
|
|
5123
|
+
const knowAboutTerm = String(query).trim().match(KNOW_ABOUT_RE)?.[1]?.trim();
|
|
5124
|
+
const offerTerm = knowAboutTerm || metaTermOf(query, envelope);
|
|
5125
|
+
if (offerTerm) {
|
|
5126
|
+
let normFactTerm;
|
|
5127
|
+
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { normFactTerm = null; }
|
|
5128
|
+
if (normFactTerm) {
|
|
5129
|
+
const cleanTerm = normFactTerm(offerTerm);
|
|
5130
|
+
const ent = knowAboutTerm ? null : await resolveEntity(graph, offerTerm);
|
|
5131
|
+
if (!ent) {
|
|
5132
|
+
const variants = factTermVariants(normFactTerm, offerTerm);
|
|
5133
|
+
const known = (await memoryFacts(memoryDir)).some((f) => variants.has(f.subject) || variants.has(f.object));
|
|
5134
|
+
if (!known) {
|
|
5135
|
+
answer = `${answer}\n${unknownVocabTermOffer(cleanTerm)}`;
|
|
5136
|
+
note(trace, `intermediate: TEACH-OFFER — "${cleanTerm}" is unknown to both the graph and memory, so the miss got an offer to learn appended`);
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
5139
|
+
}
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
4276
5142
|
// W5 (flag-gated, default OFF): an unknown-term miss may consult the LOCAL
|
|
4277
5143
|
// committed corpus slice — a hit APPENDS a grounded, licence-cited aside under
|
|
4278
5144
|
// the honest miss (the miss itself stands; the aside is context, not an answer).
|
|
@@ -4363,6 +5229,38 @@ function plainTurn(query, answer, { command, via = "composed", miss = false, foc
|
|
|
4363
5229
|
};
|
|
4364
5230
|
}
|
|
4365
5231
|
|
|
5232
|
+
// Bug F point 5 (operator's explicit, most important ask this round): a
|
|
5233
|
+
// command name -> a short, honest one-line goal string, mirroring GOAL_BY_KIND's
|
|
5234
|
+
// own spirit (above) — but for COMMAND dispatches (find/search/describe/…)
|
|
5235
|
+
// instead of ask()-parsed relation queries, so "I want you to search for
|
|
5236
|
+
// Widget" (now reachable via a slash command per Bug F point 3) ALSO gets a
|
|
5237
|
+
// real "Goal (inferred): …" line instead of none at all, generalizing the
|
|
5238
|
+
// existing mechanism exactly as asked. Reuses GOAL_BY_KIND's EXISTING wording
|
|
5239
|
+
// verbatim wherever a command's intent overlaps one of those kinds (members/
|
|
5240
|
+
// subclasses reuse contains/inherits's own phrasing; callers/callees reuse
|
|
5241
|
+
// calls's; tests/untested reuse tests's; history reuses touches's; exports
|
|
5242
|
+
// reuses reexports's) — never invents new phrasing for the same concept. A
|
|
5243
|
+
// command not worth a bespoke entry (help/stats/memory/focus/narrate/unknown)
|
|
5244
|
+
// falls back to a short generic line in mk() itself, below.
|
|
5245
|
+
const GOAL_BY_COMMAND = {
|
|
5246
|
+
find: "locate a specific named entity",
|
|
5247
|
+
search: "locate a specific named entity",
|
|
5248
|
+
context: "gather the sized edit bundle for a symbol before changing code",
|
|
5249
|
+
snippet: "view a symbol's exact source",
|
|
5250
|
+
describe: "look up a symbol's definition and relations",
|
|
5251
|
+
signature: "view a symbol's signature",
|
|
5252
|
+
members: GOAL_BY_KIND.contains,
|
|
5253
|
+
subclasses: GOAL_BY_KIND.inherits,
|
|
5254
|
+
impact: "understand what a change to this module would reach (impact closure)",
|
|
5255
|
+
callers: GOAL_BY_KIND.calls,
|
|
5256
|
+
callees: GOAL_BY_KIND.calls,
|
|
5257
|
+
tests: GOAL_BY_KIND.tests,
|
|
5258
|
+
untested: GOAL_BY_KIND.tests,
|
|
5259
|
+
history: GOAL_BY_KIND.touches,
|
|
5260
|
+
exports: GOAL_BY_KIND.reexports,
|
|
5261
|
+
arch: "understand the overall architecture (package/module boundaries)",
|
|
5262
|
+
};
|
|
5263
|
+
|
|
4366
5264
|
/** A slash-command → the mapped tool (or the /help, /focus, /narrate, unknown
|
|
4367
5265
|
* cases). Returns the same { answer, logLines, record, focus } shape as
|
|
4368
5266
|
* runAsk; the record carries the command name and the resolved entity id
|
|
@@ -4370,7 +5268,10 @@ function plainTurn(query, answer, { command, via = "composed", miss = false, foc
|
|
|
4370
5268
|
* wherever it resolves an entity. `ctx.trace` (narrate mode, or undefined
|
|
4371
5269
|
* when off) gets one "goal:"/"lane:" note per branch — a slash-command's
|
|
4372
5270
|
* "decision" is simply which command+tool ran, so this is intentionally
|
|
4373
|
-
* lighter than runAsk's miss-cascade instrumentation.
|
|
5271
|
+
* lighter than runAsk's miss-cascade instrumentation. Also carries a `goal`
|
|
5272
|
+
* field now (Bug F point 5) — mirrors runAsk's own `goal` field so
|
|
5273
|
+
* withGoalLine's short "Goal (inferred): …" line fires for command
|
|
5274
|
+
* dispatches too, not just ask()-parsed queries. */
|
|
4374
5275
|
async function runCommand(line, { config, source, graph, focus, memoryDir, trace, narrate = false }) {
|
|
4375
5276
|
const ts = new Date().toISOString();
|
|
4376
5277
|
const sp = line.indexOf(" ");
|
|
@@ -4381,6 +5282,7 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
4381
5282
|
logLines: [ts, `> ${line}`, answer, ""],
|
|
4382
5283
|
record: { type: "turn", ts, query: line, command: name, via: "command", resolvedIds, answeredIds: [], miss },
|
|
4383
5284
|
focus: newFocus,
|
|
5285
|
+
goal: GOAL_BY_COMMAND[name] || "use a specific tool/command directly",
|
|
4384
5286
|
...(narrateNext !== undefined ? { narrate: narrateNext } : {}),
|
|
4385
5287
|
});
|
|
4386
5288
|
|
|
@@ -4441,6 +5343,20 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
4441
5343
|
value = focus?.label || "";
|
|
4442
5344
|
if (value) note(trace, `intermediate: no/pronoun argument -> fell back to the standing focus "${value}"`);
|
|
4443
5345
|
}
|
|
5346
|
+
// Tier 6 playtest: a bare English "describe the logger module"/"describe the
|
|
5347
|
+
// Task class" is short enough (≤3 tokens, no query connective) that
|
|
5348
|
+
// asBareCommand (above) already rewrote it into a literal slash command
|
|
5349
|
+
// BEFORE this function ever sees it — so describeGrainRescue's OWN lane
|
|
5350
|
+
// (describeWrapperAnswer) never runs for this exact shape; the rescue has to
|
|
5351
|
+
// happen HERE too, on the raw command argument, before it reaches
|
|
5352
|
+
// dispatchTool's resolveSymbol (which has no article/grain-word tolerance).
|
|
5353
|
+
if (entityArg && value) {
|
|
5354
|
+
const rescued = await describeGrainRescue(graph, value);
|
|
5355
|
+
if (rescued?.label) {
|
|
5356
|
+
note(trace, `intermediate: "${value}" carries a grain word -> resolved to ${rescued.label} before dispatch`);
|
|
5357
|
+
value = rescued.label;
|
|
5358
|
+
}
|
|
5359
|
+
}
|
|
4444
5360
|
if (spec.arg && !spec.optional && !value) {
|
|
4445
5361
|
const need = entityArg ? `${spec.arg} (none given and no focus set — /focus <x> or pass one)` : spec.arg;
|
|
4446
5362
|
return mk(`/${name} needs a ${need}.`, { miss: true });
|
|
@@ -4570,8 +5486,30 @@ function morePage(query, { last, focus }) {
|
|
|
4570
5486
|
return turn;
|
|
4571
5487
|
}
|
|
4572
5488
|
|
|
5489
|
+
// Bug F point 3 (operator follow-up request): "I want you to search for
|
|
5490
|
+
// Widget" / "I'd like you to search for Widget" — a closed-set indirect-
|
|
5491
|
+
// request wrapper, checked VERY early (before asBareCommand/conversationalTurn/
|
|
5492
|
+
// the ask engine ever see the raw prefix). Found live: without this, "I want
|
|
5493
|
+
// you to search for Widget" was mis-swallowed by GENERAL_VERB_TEACH_RE as a
|
|
5494
|
+
// bare <subject> <verb> <object> teach triple (subject "I", verb "want") —
|
|
5495
|
+
// declined by the pronoun-subject guard with a confusing "pronouns aren't
|
|
5496
|
+
// things I can classify" message, instead of ever reaching /search at all.
|
|
5497
|
+
// Deliberately does NOT strip bare "please X" alone — that's already handled
|
|
5498
|
+
// ad hoc by many individual regexes throughout this file (TEACH_RE,
|
|
5499
|
+
// EXPLAIN_TOUCH_RE, describeWrapperAnswer's own regex, IMPERATIVE_NUDGE_RE) and
|
|
5500
|
+
// re-stripping it centrally here risks double-processing interactions across
|
|
5501
|
+
// the whole file — out of scope for this fix, higher risk than the concrete
|
|
5502
|
+
// gain.
|
|
5503
|
+
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;
|
|
5504
|
+
|
|
4573
5505
|
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
5506
|
const line = String(input ?? "").trim();
|
|
5507
|
+
// The captured residue is used for RECOGNITION at every dispatch site below
|
|
5508
|
+
// (asBareCommand, conversationalTurn, assertTurn, the count lanes, runAsk);
|
|
5509
|
+
// the ORIGINAL `line` survives untouched for record.query/logLines fidelity
|
|
5510
|
+
// — restored centrally inside withLast (below), once, for every dispatch path.
|
|
5511
|
+
const indirectMatch = line.match(INDIRECT_REQUEST_RE);
|
|
5512
|
+
const workingLine = indirectMatch ? indirectMatch[1].trim() : line;
|
|
4575
5513
|
const templates = await chatTemplates(); // failure-tolerated: null degrades, never throws
|
|
4576
5514
|
// narrate mode: allocate the mutable trace array ONLY when on (`null` when off,
|
|
4577
5515
|
// matching every OTHER optional collaborator here — templates/memoryDir/lexicon
|
|
@@ -4598,6 +5536,14 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
4598
5536
|
// the PRE-narration finished result — see withNarration's docblock for why.
|
|
4599
5537
|
const withLast = (result, fallbackGoal = "unclear — no goal signal for this turn type") => {
|
|
4600
5538
|
const finished = finish(result, { graph });
|
|
5539
|
+
// Bug F point 3 fidelity: every dispatch path below built its own record off
|
|
5540
|
+
// `workingLine` (the indirect-request wrapper stripped, above) — restore the
|
|
5541
|
+
// ORIGINAL raw `line` into record.query and the logged "> …" transcript echo
|
|
5542
|
+
// here, once, centrally, for every path (they all funnel through withLast).
|
|
5543
|
+
if (indirectMatch) {
|
|
5544
|
+
if (finished.record) finished.record.query = line;
|
|
5545
|
+
if (Array.isArray(finished.logLines) && finished.logLines.length > 1) finished.logLines[1] = `> ${line}`;
|
|
5546
|
+
}
|
|
4601
5547
|
// runAsk's own effectiveQuery (set only when discourseRewrite substituted a
|
|
4602
5548
|
// new subject AND the rewrite produced a genuine non-miss answer) takes
|
|
4603
5549
|
// over as the continuation base for the NEXT turn's own discourseRewrite —
|
|
@@ -4617,32 +5563,32 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
4617
5563
|
// "memory", "describe X") is routed to its slash form BEFORE the conversational
|
|
4618
5564
|
// layer, so a forgiving shell answers "stats" the way it answers "/stats" instead
|
|
4619
5565
|
// of falling through to the generic orientation.
|
|
4620
|
-
const bareCmd = asBareCommand(
|
|
5566
|
+
const bareCmd = asBareCommand(workingLine);
|
|
4621
5567
|
if (bareCmd) return withLast(await runCommand(bareCmd, ctx), "use a specific tool/command directly");
|
|
4622
5568
|
|
|
4623
5569
|
// Conversational layer next (greetings, thanks, help, bye, why/say-more) — these
|
|
4624
5570
|
// resolve no entity and carry their own preserved `last`. Bypasses withLast (a
|
|
4625
5571
|
// conversational turn is never finish()'d / never becomes a new `last`), so the
|
|
4626
5572
|
// narrate block is applied directly here instead.
|
|
4627
|
-
const convo = conversationalTurn(
|
|
5573
|
+
const convo = conversationalTurn(workingLine, ctx);
|
|
4628
5574
|
if (convo) return withNarration(convo, trace, "casual/social — no graph intent");
|
|
4629
5575
|
|
|
4630
5576
|
// "more" — page the remainder of a previous long listing, if one is held. Gated on
|
|
4631
5577
|
// an actual pending remainder so a bare "more" with nothing to continue falls through
|
|
4632
5578
|
// to the ordinary path (an honest miss), never a pretend page.
|
|
4633
|
-
if (MORE_RE.test(
|
|
5579
|
+
if (MORE_RE.test(workingLine) && Array.isArray(last?.detail?.pending?.items) && last.detail.pending.items.length) {
|
|
4634
5580
|
note(trace, "goal: continue viewing a previous long listing (pagination)");
|
|
4635
5581
|
note(trace, "lane: MORE_RE matched a held pending remainder from the previous turn's detail.pending");
|
|
4636
|
-
return withLast(morePage(
|
|
5582
|
+
return withLast(morePage(workingLine, ctx), "continue viewing a previous long listing");
|
|
4637
5583
|
}
|
|
4638
5584
|
|
|
4639
|
-
if (
|
|
5585
|
+
if (workingLine.startsWith("/")) return withLast(await runCommand(workingLine, ctx), "use a specific tool/command directly");
|
|
4640
5586
|
// Declarative ACE sentences ("every module is a artifact") ASSERT into tmct's
|
|
4641
5587
|
// own memory and confirm — they are statements to remember, not graph queries.
|
|
4642
5588
|
// Gated on memoryDir: only a session shell provides a write target, so a bare
|
|
4643
5589
|
// runTurn (tests, library callers) stays pure and falls through to the engine.
|
|
4644
5590
|
if (memoryDir) {
|
|
4645
|
-
const asserted = await assertTurn(
|
|
5591
|
+
const asserted = await assertTurn(workingLine, ctx);
|
|
4646
5592
|
if (asserted) {
|
|
4647
5593
|
note(trace, "goal: teach/remember a new fact (declarative ACE sentence)");
|
|
4648
5594
|
note(trace, "lane: assertTurn — grammar/ace.mjs parseAce matched a full triple with no residue");
|
|
@@ -4655,11 +5601,11 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
4655
5601
|
// otherwise say "I can't count facts"); it only speaks for a memory-class noun, so
|
|
4656
5602
|
// structural counts (classes/functions/…) and sessions fall through unaffected.
|
|
4657
5603
|
if (memoryDir) {
|
|
4658
|
-
const memCount = await answerMemoryCount(memoryDir,
|
|
5604
|
+
const memCount = await answerMemoryCount(memoryDir, workingLine);
|
|
4659
5605
|
if (memCount != null) {
|
|
4660
5606
|
note(trace, "goal: get a count of a memory-store kind (facts/utterances)");
|
|
4661
5607
|
note(trace, "lane: answerMemoryCount — matched a MEMORY_COUNT_NOUNS entry, answered off the .tmct/memory graph header");
|
|
4662
|
-
return withLast(plainTurn(
|
|
5608
|
+
return withLast(plainTurn(workingLine, memCount, { via: "count", focus }), "get a count of a memory-store kind");
|
|
4663
5609
|
}
|
|
4664
5610
|
}
|
|
4665
5611
|
// Feature A point 4: "how many Xs are Ys" — a taught-quantifier RECALL, checked
|
|
@@ -4668,32 +5614,32 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
4668
5614
|
// authority gate declines (returns null) for anything answerCount should own,
|
|
4669
5615
|
// so ordinary structural counts fall through completely unaffected.
|
|
4670
5616
|
if (memoryDir) {
|
|
4671
|
-
const quantifierRecall = await answerQuantifierRecall(memoryDir,
|
|
5617
|
+
const quantifierRecall = await answerQuantifierRecall(memoryDir, workingLine);
|
|
4672
5618
|
if (quantifierRecall != null) {
|
|
4673
5619
|
note(trace, 'goal: recall a taught quantifier for a class-membership pair ("how many Xs are Ys")');
|
|
4674
5620
|
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(
|
|
5621
|
+
return withLast(plainTurn(workingLine, quantifierRecall, { via: "fact", focus }), "recall a taught quantifier");
|
|
4676
5622
|
}
|
|
4677
5623
|
}
|
|
4678
5624
|
// Aggregate/count questions are answered mechanically off the loaded graph header,
|
|
4679
5625
|
// BEFORE falling through to the ask engine (focus unchanged — a count names no entity).
|
|
4680
|
-
const count = answerCount(graph,
|
|
5626
|
+
const count = answerCount(graph, workingLine);
|
|
4681
5627
|
if (count != null) {
|
|
4682
5628
|
// An "I can't count <noun>" from a bare kind may still be answerable from an
|
|
4683
5629
|
// ASSERTED vocabulary fact ("every class is a type" → "how many types" = the
|
|
4684
5630
|
// class count). countFromFacts declines on a real graph kind, so ordinary
|
|
4685
5631
|
// counts are unaffected; it only speaks for a remembered object noun.
|
|
4686
|
-
const viaFact = memoryDir ? await countFromFacts(graph, memoryDir,
|
|
5632
|
+
const viaFact = memoryDir ? await countFromFacts(graph, memoryDir, workingLine) : null;
|
|
4687
5633
|
if (viaFact != null) {
|
|
4688
5634
|
note(trace, 'goal: get a count of an asserted-vocabulary kind ("every X is a Y" inherited cardinality)');
|
|
4689
5635
|
note(trace, "lane: countFromFacts — the counted noun matched a remembered isa-fact's SUBJECT, whose class IS countable");
|
|
4690
|
-
return withLast(plainTurn(
|
|
5636
|
+
return withLast(plainTurn(workingLine, viaFact, { via: "fact", focus }), "get a count");
|
|
4691
5637
|
}
|
|
4692
5638
|
note(trace, "goal: get a count of a graph kind (classes/functions/modules/…)");
|
|
4693
5639
|
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(
|
|
5640
|
+
return withLast(plainTurn(workingLine, count, { via: "count", focus }), "get a count of a graph kind");
|
|
4695
5641
|
}
|
|
4696
|
-
return withLast(await runAsk(
|
|
5642
|
+
return withLast(await runAsk(workingLine, ctx), "unclear — no goal signal computed by the ask engine");
|
|
4697
5643
|
}
|
|
4698
5644
|
|
|
4699
5645
|
// ---- W3: seedMemory → bootstrap (first run in a graph-less repo) ----
|