@polycode-projects/the-mechanical-code-talker 1.0.6 → 1.0.7
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 +3 -1
- package/package.json +1 -1
- package/src/ask.mjs +60 -5
- package/src/chat.mjs +343 -12
package/README.md
CHANGED
|
@@ -17,7 +17,9 @@ tmct> /callers checkout
|
|
|
17
17
|
tmct> /exit
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
**[Try it live in your browser →](https://polycode-projects.gitlab.io/the-mechanical-code-talker/)**
|
|
21
|
+
— a real, interactive chat demo running client-side: your browser runs the
|
|
22
|
+
actual query engine against a small example codebase, no server, no install.
|
|
21
23
|
|
|
22
24
|
## How it interprets you
|
|
23
25
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
|
package/src/ask.mjs
CHANGED
|
@@ -863,6 +863,18 @@ function parseSuperlative(w, lc, nlp) {
|
|
|
863
863
|
for (let i = extIdx; i < lc.length; i += 1) {
|
|
864
864
|
if (EDGE_NOUN_TO_METRIC[lc[i]]) { metric = EDGE_NOUN_TO_METRIC[lc[i]]; metricNoun = lc[i]; break; }
|
|
865
865
|
}
|
|
866
|
+
// Fallback: a passive-verb-led phrasing puts the participle metric word BEFORE
|
|
867
|
+
// the extreme, not after ("which function IS CALLED the most" — cf. "which
|
|
868
|
+
// module is MOST imported", the already-supported order where the participle
|
|
869
|
+
// trails "most" and the forward scan above already catches it). Scan backward
|
|
870
|
+
// from just before the extreme, taking the CLOSEST metric word — the natural
|
|
871
|
+
// reading when one exists at all. Only a fallback (never overrides a forward
|
|
872
|
+
// hit), so it can't touch any phrasing that already resolves correctly today.
|
|
873
|
+
if (!metric) {
|
|
874
|
+
for (let i = extIdx - 1; i >= 0; i -= 1) {
|
|
875
|
+
if (EDGE_NOUN_TO_METRIC[lc[i]]) { metric = EDGE_NOUN_TO_METRIC[lc[i]]; metricNoun = lc[i]; break; }
|
|
876
|
+
}
|
|
877
|
+
}
|
|
866
878
|
const connectivity = lc.includes("connected") || lc.slice(extIdx, extIdx + 2).join(" ") === "most connected"
|
|
867
879
|
|| ["largest", "biggest", "smallest"].includes(lc[extIdx]);
|
|
868
880
|
if (!metric) {
|
|
@@ -1063,15 +1075,48 @@ function parseRelationalOrQualified(w, lc, nlp, depth) {
|
|
|
1063
1075
|
const { blc } = dropLeadCopula(bw, bw.map((x) => x.toLowerCase()));
|
|
1064
1076
|
return blc.length && blc.every((x) => QUALIFIERS[x]);
|
|
1065
1077
|
});
|
|
1078
|
+
// A bare "call X and call Y" / "call X but not Y" chain — verb+verb (or
|
|
1079
|
+
// verb+bare-object), no qualifier, no "that" — is ALSO the compositional shape
|
|
1080
|
+
// when branch0 leads with an explicit verb and every OTHER branch is either (a)
|
|
1081
|
+
// an explicit repeat of that SAME mapped verb kind ("call loadStore and call
|
|
1082
|
+
// saveStore" == calls(loadStore) ∩ calls(saveStore)) or (b) a bare object with NO
|
|
1083
|
+
// verb of its own at all ("call saveStore but not loadStore" — "loadStore" alone
|
|
1084
|
+
// inherits "call" via the SAME ellipsis-borrowing buildPredicateAtoms/the atoms
|
|
1085
|
+
// loop below already do for OR-chains like "importing X or Y"; this only widens
|
|
1086
|
+
// the GATE that lets that borrowing fire for and/but-not too). Deliberately
|
|
1087
|
+
// narrower than "any verb+verb and": the compat-guarded bare case ("which classes
|
|
1088
|
+
// extends Base and couples to logging") gives its SECOND branch its own DIFFERENT
|
|
1089
|
+
// explicit verb (coupled-to, not inherits) and so fails case (a) and isn't bare
|
|
1090
|
+
// for case (b) either — it still has no marker and correctly stays on the legacy
|
|
1091
|
+
// ambiguous-parse path, untouched.
|
|
1092
|
+
const sameVerbBranches = predWords.length > 0 ? splitBoolean(predLc, predWords).branches : [];
|
|
1093
|
+
let sameVerbLed = false;
|
|
1094
|
+
if (sameVerbBranches.length > 1) {
|
|
1095
|
+
const firstBlc = sameVerbBranches[0].map((x) => x.toLowerCase());
|
|
1096
|
+
const firstVh = findPhrase(firstBlc, VERB_TO_KIND);
|
|
1097
|
+
if (firstVh && firstVh.start === 0) {
|
|
1098
|
+
sameVerbLed = sameVerbBranches.slice(1).every((bw) => {
|
|
1099
|
+
const blc = bw.map((x) => x.toLowerCase());
|
|
1100
|
+
const vh = findPhrase(blc, VERB_TO_KIND);
|
|
1101
|
+
return vh && vh.start === 0 ? vh.kind === firstVh.kind : !vh;
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1066
1105
|
// marker gate — the crux of backward-compat: without one of these, this is not a
|
|
1067
1106
|
// compositional query and we must NOT hijack it from the existing parser.
|
|
1068
|
-
if (!(quals.length || relFlag || membershipLed || gerundLed || boolQualLed)) return null;
|
|
1107
|
+
if (!(quals.length || relFlag || membershipLed || gerundLed || boolQualLed || sameVerbLed)) return null;
|
|
1069
1108
|
|
|
1070
1109
|
// empty predicate → a bare qualified class ("public methods")
|
|
1071
1110
|
if (!predWords.length) {
|
|
1072
1111
|
let base = { node: "allOfClass", entityType };
|
|
1073
1112
|
if (!quals.length) return { node: "miss", reason: "nothing to filter or traverse" };
|
|
1074
|
-
|
|
1113
|
+
// entityType carried on the qualifier node itself too (not just `inner`) — a
|
|
1114
|
+
// top-level "qualifier" AST has no dedicated evalComposite case, so it falls to
|
|
1115
|
+
// the generic {compositeKind:"set", entityType: ast.entityType||null} catch-all;
|
|
1116
|
+
// without this, a zero-match qualifier query ("public methods of X" with no
|
|
1117
|
+
// public methods) rendered a bare "nothing in the index matches that." with no
|
|
1118
|
+
// entity-kind receipt, same wall-shaped miss as a genuinely unrecognized query.
|
|
1119
|
+
return { node: "qualifier", filters: quals, inner: base, entityType };
|
|
1075
1120
|
}
|
|
1076
1121
|
|
|
1077
1122
|
const subjPrefix = noun.placeholder ? "what" : `which ${entWord}`;
|
|
@@ -1111,7 +1156,9 @@ function parseRelationalOrQualified(w, lc, nlp, depth) {
|
|
|
1111
1156
|
} else {
|
|
1112
1157
|
result = { node: "boolean", entityType, atoms };
|
|
1113
1158
|
}
|
|
1114
|
-
|
|
1159
|
+
// entityType carried on the qualifier wrapper too — see the identical comment on
|
|
1160
|
+
// the empty-predicate qualifier node above; same catch-all-miss-receipt fix.
|
|
1161
|
+
if (quals.length) result = { node: "qualifier", filters: quals, inner: result, entityType };
|
|
1115
1162
|
return result;
|
|
1116
1163
|
}
|
|
1117
1164
|
|
|
@@ -1537,7 +1584,8 @@ function evalBoolean(graph, ast, opts) {
|
|
|
1537
1584
|
function evalAnaphora(graph, ast, opts) {
|
|
1538
1585
|
const prev = opts && opts.prev;
|
|
1539
1586
|
if (!Array.isArray(prev) || !prev.length) return { compositeMiss: true, reason: "no-prev", matches: [] };
|
|
1540
|
-
|
|
1587
|
+
const baseItems = prev.map((id) => graph.byId.get(id)).filter(Boolean);
|
|
1588
|
+
let items = baseItems;
|
|
1541
1589
|
const f = ast.filter;
|
|
1542
1590
|
if (f && f.type === "qual") {
|
|
1543
1591
|
items = items.filter((ind) => f.filters.every((q) => qualHolds(graph, ind, QUALIFIERS[q])));
|
|
@@ -1554,7 +1602,14 @@ function evalAnaphora(graph, ast, opts) {
|
|
|
1554
1602
|
}
|
|
1555
1603
|
}
|
|
1556
1604
|
// a count over a prior set names the entity kind when the survivors share a class.
|
|
1557
|
-
|
|
1605
|
+
// When the filter narrows a real prior set down to ZERO, fall back to the PRIOR
|
|
1606
|
+
// set's own class (still shared, pre-filter) so the honest-empty render still
|
|
1607
|
+
// names what was checked ("nothing in the index matches that (methods)."
|
|
1608
|
+
// instead of a bare, kind-less "nothing in the index matches that.") — the
|
|
1609
|
+
// filter genuinely found no survivors, but the entity kind it filtered is not
|
|
1610
|
+
// itself unknown, so the miss shouldn't read as if it were.
|
|
1611
|
+
const sameClass = (list) => (list.length && list.every((x) => x.class === list[0].class) ? list[0].class : null);
|
|
1612
|
+
const common = items.length ? sameClass(items) : sameClass(baseItems);
|
|
1558
1613
|
if (ast.mode === "count") return { compositeKind: "count", count: items.length, entityType: common, matches: [] };
|
|
1559
1614
|
return { compositeKind: "set", matches: items, entityType: common };
|
|
1560
1615
|
}
|
package/src/chat.mjs
CHANGED
|
@@ -1266,6 +1266,12 @@ const HAS_PROPERTY_PREDICATE = "mgx:hasProperty";
|
|
|
1266
1266
|
// "some/a few Xs are Ys" shape) stay obviously in that same family rather than
|
|
1267
1267
|
// re-typing the CURIE string at each call site.
|
|
1268
1268
|
const SUBCLASS_PREDICATE = "rdfs:subClassOf";
|
|
1269
|
+
// Bug 3 (2026-07-09): the SAME "has a" predicate ConceptNet's own /r/HasA
|
|
1270
|
+
// facts already use (FACT_PREDICATE_PHRASES, conceptnet-map.toml) — named
|
|
1271
|
+
// here too so generalVerbTeach's "has"/"have" special case (below) stays
|
|
1272
|
+
// obviously in that same family, interoperable with corpus HasA data on the
|
|
1273
|
+
// read side, rather than minting a redundant mgx:has.
|
|
1274
|
+
const HAS_A_PREDICATE = "mgx:hasA";
|
|
1269
1275
|
|
|
1270
1276
|
/** "<Name> owns/maintains <X>" — the ownership teach declarative. <Name> is one
|
|
1271
1277
|
* or two name tokens, <X> one code-ish token (a path, a file, a symbol). The
|
|
@@ -1299,7 +1305,7 @@ async function teachFact(memoryDir, sessionId, { subject, predicate, object, qua
|
|
|
1299
1305
|
provenance: teachProvenanceTag(sessionId, new Date().toISOString()),
|
|
1300
1306
|
...(quantifier ? { quantifier } : {}),
|
|
1301
1307
|
});
|
|
1302
|
-
const phrase =
|
|
1308
|
+
const phrase = predicatePhrase(predicate);
|
|
1303
1309
|
return { text: `noted — remembered: ${s} ${phrase} ${o}`, via: "assert", miss: false };
|
|
1304
1310
|
} catch {
|
|
1305
1311
|
return null;
|
|
@@ -1398,6 +1404,111 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }
|
|
|
1398
1404
|
return null; // Y unknown too — decline honestly, never guess
|
|
1399
1405
|
}
|
|
1400
1406
|
|
|
1407
|
+
// ---- BUG 3 (2026-07-09, operator-authorized generalizing — "I don't know
|
|
1408
|
+
// where that ban came from, overturn it. build it."): general verb-to-
|
|
1409
|
+
// predicate teaching. "remember tony has a hat" / "remember margo eats ribs"
|
|
1410
|
+
// used to fall straight through teachLane returning null (not even this
|
|
1411
|
+
// lane's own honest miss text) because the ONLY verbs the lane recognized at
|
|
1412
|
+
// all were is/are (class-membership/property) and owns/maintains
|
|
1413
|
+
// (ownership) — a sentence with any OTHER verb never matched a single
|
|
1414
|
+
// recognizer, and fell to the STRUCTURAL code-graph grammar, which of course
|
|
1415
|
+
// can't resolve an arbitrary proper noun as a code entity (confusing,
|
|
1416
|
+
// wrong-context miss text, and sometimes a confidently WRONG "Goal
|
|
1417
|
+
// (inferred)" line — runAsk's own teach-lane goal deduction fixes that half).
|
|
1418
|
+
//
|
|
1419
|
+
// RECOGNITION stays exactly as CLOSED as every other frame in this lane:
|
|
1420
|
+
// wrapper-REQUIRED (teachLane only ever calls generalVerbTeach on `wrapped`,
|
|
1421
|
+
// i.e. only inside an already "remember/note/…"-triggered payload — a bare
|
|
1422
|
+
// "tony has a hat" is never silently reified, same discipline
|
|
1423
|
+
// TEACH_PROPERTY_RE already uses), and only a well-formed <subject> <verb>
|
|
1424
|
+
// <object> triple matches AT ALL (point 6 — a missing/unparseable object
|
|
1425
|
+
// still declines honestly, never a guess). What's generalized is ONLY the
|
|
1426
|
+
// PREDICATE a recognized shape maps to, never what counts as a recognized
|
|
1427
|
+
// shape — the same "recognition closed, mapping generalized" split the
|
|
1428
|
+
// operator explicitly authorized over this dispatch's default "prefer
|
|
1429
|
+
// templates" guidance. ----
|
|
1430
|
+
|
|
1431
|
+
/** <subject> (ONE bare word — a name, "tony"/"margo") <verb> (one lowercase
|
|
1432
|
+
* word) <object> (the rest). Deliberately bounded to a SINGLE-TOKEN subject
|
|
1433
|
+
* with no leading determiner — not the lazy/greedy multi-word subject the
|
|
1434
|
+
* is/are frames elsewhere in this lane tolerate. Reasoning (found live while
|
|
1435
|
+
* building this): without real verb-position knowledge, a positional regex
|
|
1436
|
+
* over an ARBITRARY-length subject is genuinely ambiguous — "every
|
|
1437
|
+
* controller is a handler" would just as happily (mis)parse as
|
|
1438
|
+
* subject="every", verb="controller", object="is a handler" as it would the
|
|
1439
|
+
* intended reading. Bounding the subject to one bare word removes that
|
|
1440
|
+
* ambiguity for exactly the shape this mechanism targets (a name-like
|
|
1441
|
+
* subject, per the operator's own examples); a determiner/quantifier-led or
|
|
1442
|
+
* multi-word subject simply doesn't match here and honestly declines
|
|
1443
|
+
* (point 6) rather than risk a wrong split — the is/are-specific frames
|
|
1444
|
+
* elsewhere in this lane already own that broader territory. */
|
|
1445
|
+
const GENERAL_VERB_TEACH_RE = /^([\w'-]+)\s+([a-z]+)\s+(.+?)[.!?]*$/i;
|
|
1446
|
+
/** Determiners/quantifiers that make the FIRST token an article, not a real
|
|
1447
|
+
* bare-name subject ("every controller…", "the cache…") — GENERAL_VERB_TEACH_RE
|
|
1448
|
+
* would otherwise happily bind them as a 1-token subject and misread the
|
|
1449
|
+
* REAL subject's second word as the verb. Declining here hands the sentence
|
|
1450
|
+
* back to the is/are-specific frames above/below (their own territory) or an
|
|
1451
|
+
* honest miss — never a guessed split. */
|
|
1452
|
+
const GENERAL_VERB_DETERMINER_RE = /^(?:every|each|all|some|a|an|the|your|my|our|their|his|her|its)$/i;
|
|
1453
|
+
/** Verbs owned by an earlier, more specific recognizer in this lane — is/are
|
|
1454
|
+
* (class-membership/property, above) and owns/maintains (ownership, above).
|
|
1455
|
+
* generalVerbTeach declines outright on these so it can never race a more
|
|
1456
|
+
* specific frame for the same sentence; a genuine miss on one of THESE verbs
|
|
1457
|
+
* stays that frame's own honest miss, never silently reinterpreted here. */
|
|
1458
|
+
const GENERAL_VERB_EXCLUDE_RE = /^(?:is|are|am|owns|maintains)$/i;
|
|
1459
|
+
/** Whole-payload safety net (defense in depth alongside the single-token
|
|
1460
|
+
* subject bound above): if "is"/"are"/"am"/"owns"/"maintains" appears
|
|
1461
|
+
* ANYWHERE in the sentence — not just at the guessed verb position — this
|
|
1462
|
+
* is territory another frame in this lane already owns (or will, in the
|
|
1463
|
+
* is/are payload block right after this one runs), so generalVerbTeach
|
|
1464
|
+
* stands down entirely rather than risk a positional misread of a longer
|
|
1465
|
+
* copula/ownership sentence it was never meant to parse. */
|
|
1466
|
+
const GENERAL_VERB_ANYWHERE_EXCLUDE_RE = /\b(?:is|are|am|owns|maintains)\b/i;
|
|
1467
|
+
|
|
1468
|
+
/** The predicate a general-verb teach payload's VERB maps to. "has"/"have"
|
|
1469
|
+
* special-cases onto the EXISTING mgx:hasA predicate (point 2) — the same
|
|
1470
|
+
* one ConceptNet's own /r/HasA facts already use (FACT_PREDICATE_PHRASES),
|
|
1471
|
+
* so a taught "X has a Y" fact reads back interoperably with corpus HasA
|
|
1472
|
+
* data, rather than minting a redundant mgx:has. Any OTHER verb mints
|
|
1473
|
+
* mgx:<lemma> (point 3a) — proseLemma, the wink-nlp lemmatiser this
|
|
1474
|
+
* codebase already loads elsewhere (prose-nlp.mjs), canonicalizes "eats"/
|
|
1475
|
+
* "ate"/"eating" alike onto the same mgx:eat predicate; when the optional
|
|
1476
|
+
* wink model isn't installed, proseLemma degrades to null (its own
|
|
1477
|
+
* documented contract) and this falls back to the verb AS TYPED — still a
|
|
1478
|
+
* perfectly storable/retrievable predicate, just not cross-inflection
|
|
1479
|
+
* canonicalized. Never a hand-curated per-verb table entry required. */
|
|
1480
|
+
async function generalVerbPredicate(verb) {
|
|
1481
|
+
const v = String(verb || "").toLowerCase();
|
|
1482
|
+
if (v === "has" || v === "have") return HAS_A_PREDICATE;
|
|
1483
|
+
try {
|
|
1484
|
+
const { proseLemma } = await import("./prose-nlp.mjs");
|
|
1485
|
+
const lemma = proseLemma();
|
|
1486
|
+
return `mgx:${lemma ? lemma(v) : v}`;
|
|
1487
|
+
} catch {
|
|
1488
|
+
return `mgx:${v}`;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/** Recognize + resolve a general-verb teach payload into {subject, predicate,
|
|
1493
|
+
* object}, or null when it doesn't fit the shape / names an excluded verb /
|
|
1494
|
+
* is missing a real subject or object (point 6 — an honest decline, never a
|
|
1495
|
+
* guess). Pure recognition + predicate mapping; the caller (teachLane) does
|
|
1496
|
+
* the actual write via the shared teachFact. */
|
|
1497
|
+
async function generalVerbTeach(payload) {
|
|
1498
|
+
const p = String(payload || "").trim();
|
|
1499
|
+
if (GENERAL_VERB_ANYWHERE_EXCLUDE_RE.test(p)) return null; // another frame's territory — stand down
|
|
1500
|
+
const m = p.match(GENERAL_VERB_TEACH_RE);
|
|
1501
|
+
if (!m) return null;
|
|
1502
|
+
const [, subjectRaw, verbRaw, objectRaw] = m;
|
|
1503
|
+
const verb = verbRaw.toLowerCase();
|
|
1504
|
+
if (GENERAL_VERB_EXCLUDE_RE.test(verb)) return null; // owned by a more specific frame above
|
|
1505
|
+
if (GENERAL_VERB_DETERMINER_RE.test(subjectRaw)) return null; // not a bare-name subject
|
|
1506
|
+
const subject = subjectRaw.trim();
|
|
1507
|
+
const object = objectRaw.replace(/^an?\s+/i, "").trim();
|
|
1508
|
+
if (!subject || !object) return null; // no well-formed triple — honest decline (point 6)
|
|
1509
|
+
const predicate = await generalVerbPredicate(verb);
|
|
1510
|
+
return { subject, predicate, object };
|
|
1511
|
+
}
|
|
1401
1512
|
|
|
1402
1513
|
/** Sentence forms to try asserting for a teach payload: the payload as-is, and
|
|
1403
1514
|
* (if it carries no determiner) its "every …" universal — the ACE-OWL shape the
|
|
@@ -1448,8 +1559,19 @@ function teachSuggestion(payload) {
|
|
|
1448
1559
|
* double as legitimate demonstrative entity references elsewhere in this
|
|
1449
1560
|
* file (DESCRIBE_PRONOUN_RE, NEGATION_PRONOUN_RE et al.), and a claim about
|
|
1450
1561
|
* a demonstrated entity ("that is a bug", pointing at something real) is a
|
|
1451
|
-
* much closer call than "every you is a womble" — not this bug's territory.
|
|
1452
|
-
|
|
1562
|
+
* much closer call than "every you is a womble" — not this bug's territory.
|
|
1563
|
+
*
|
|
1564
|
+
* WIDENED (Bug 3, 2026-07-09): the verb group used to be the closed
|
|
1565
|
+
* is/are/am copula set — correct while pronoun subjects could only ever
|
|
1566
|
+
* reach a class-membership/property claim, but Bug 3's generalVerbTeach
|
|
1567
|
+
* (below) opens a SECOND way a pronoun subject can reach the store, via ANY
|
|
1568
|
+
* verb ("remember you has a hat", "remember he eats ribs"). A pronoun is
|
|
1569
|
+
* just as invalid a fact subject under a general verb as it is under "is" —
|
|
1570
|
+
* this is a grammatical category error regardless of the verb — so the verb
|
|
1571
|
+
* slot now matches ANY word, not just the copula three, keeping the guard
|
|
1572
|
+
* ahead of every teach recognizer (copula AND general-verb alike) the same
|
|
1573
|
+
* way it already stood ahead of teachSuggestion/unknownSubjectFallback. */
|
|
1574
|
+
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;
|
|
1453
1575
|
|
|
1454
1576
|
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
1455
1577
|
const rawInput = String(query).trim();
|
|
@@ -1522,6 +1644,24 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
1522
1644
|
}
|
|
1523
1645
|
}
|
|
1524
1646
|
|
|
1647
|
+
// GENERAL VERB-TO-PREDICATE TEACH (Bug 3) — "remember <Subject> <verb>
|
|
1648
|
+
// <Object>" where <verb> is neither is/are (handled below via the ACE/
|
|
1649
|
+
// unknown-subject/property paths) nor owns/maintains (handled above).
|
|
1650
|
+
// Wrapper-REQUIRED (`wrapped`, not `raw`) — see generalVerbTeach's own
|
|
1651
|
+
// docblock for why this keeps recognition exactly as closed as every other
|
|
1652
|
+
// frame in this lane. Tried before the is/are `payload` block below so a
|
|
1653
|
+
// non-copula verb never falls through this function returning null with no
|
|
1654
|
+
// miss text at all (the ORIGINAL bug: "remember tony has a hat" never even
|
|
1655
|
+
// reached this lane's own honest-miss cascade, landing on the structural
|
|
1656
|
+
// grammar's wrong-context wall instead).
|
|
1657
|
+
if (wrapped && memoryDir && !QUESTION_LEAD_RE.test(wrapped)) {
|
|
1658
|
+
const gv = await generalVerbTeach(wrapped);
|
|
1659
|
+
if (gv) {
|
|
1660
|
+
const stored = await teachFact(memoryDir, sessionId, gv);
|
|
1661
|
+
if (stored) return stored;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1525
1665
|
let payload = null;
|
|
1526
1666
|
if (wrapped && /\b(?:is|are)\b/i.test(wrapped)) payload = wrapped;
|
|
1527
1667
|
else if (BARE_DECLARATIVE_RE.test(raw) && !QUESTION_LEAD_RE.test(raw)) payload = raw;
|
|
@@ -2241,7 +2381,33 @@ const FACT_PREDICATE_PHRASES = {
|
|
|
2241
2381
|
"mgx:hasPrerequisite": "requires",
|
|
2242
2382
|
"mgx:ownedBy": "is owned by", // the teach lane's ownership frame ("Priya owns tasks.mjs")
|
|
2243
2383
|
};
|
|
2244
|
-
|
|
2384
|
+
|
|
2385
|
+
/** Bug 3 (2026-07-09) point 3b: the MECHANICAL fallback for a predicate this
|
|
2386
|
+
* table has no curated entry for — specifically generalVerbTeach's minted
|
|
2387
|
+
* "mgx:<lemma>" predicates ("mgx:eat", "mgx:drive", …), which by design have
|
|
2388
|
+
* no per-verb table row (that would be the anti-pattern the operator's
|
|
2389
|
+
* dispatch explicitly called out to avoid). Reconstructs the naive third-
|
|
2390
|
+
* person-singular surface form so "margo mgx:eat ribs" still renders as the
|
|
2391
|
+
* natural "margo eats ribs" — the mechanical INVERSE of singularizeSurface's
|
|
2392
|
+
* own naive -s/-es/-ies fold used elsewhere in this file, same accepted-
|
|
2393
|
+
* limitation trade (no real morphology; a handful of doubly-irregular verbs
|
|
2394
|
+
* render slightly off but never wrong-MEANING). "has"/"have" never reach
|
|
2395
|
+
* this fallback — generalVerbPredicate special-cases them onto the CURATED
|
|
2396
|
+
* mgx:hasA entry above before a predicate is ever minted. Any OTHER unknown
|
|
2397
|
+
* predicate (not the "mgx:<lemma>" shape — e.g. a stray/foreign CURIE) still
|
|
2398
|
+
* renders verbatim, unchanged from before this fix. */
|
|
2399
|
+
function thirdPersonSingularSurface(lemma) {
|
|
2400
|
+
const w = String(lemma || "");
|
|
2401
|
+
if (/[a-z]y$/i.test(w) && !/[aeiou]y$/i.test(w)) return `${w.slice(0, -1)}ies`;
|
|
2402
|
+
if (/(?:s|x|z|ch|sh|o)$/i.test(w)) return `${w}es`;
|
|
2403
|
+
return `${w}s`;
|
|
2404
|
+
}
|
|
2405
|
+
function predicatePhrase(predicate) {
|
|
2406
|
+
if (FACT_PREDICATE_PHRASES[predicate]) return FACT_PREDICATE_PHRASES[predicate];
|
|
2407
|
+
const m = /^mgx:([a-z]+)$/i.exec(String(predicate || ""));
|
|
2408
|
+
return m ? thirdPersonSingularSurface(m[1]) : predicate;
|
|
2409
|
+
}
|
|
2410
|
+
const factPhrase = (f) => `${f.subject} ${predicatePhrase(f.predicate)} ${f.object}`;
|
|
2245
2411
|
|
|
2246
2412
|
// ---- BUG 1 fix (2026-07-08): "what is a tree used for" filters to JUST the
|
|
2247
2413
|
// UsedFor facts, instead of grammar.mjs's meta-whatis template's lazy tail
|
|
@@ -2458,10 +2624,12 @@ async function factAnswer(memoryDir, query, envelope, miss) {
|
|
|
2458
2624
|
// alongside the schema-docs answer) and misses (facts answer alone) alike.
|
|
2459
2625
|
// When the engine produced NO parse at all (the empty-bootstrap graph
|
|
2460
2626
|
// short-circuits before parsing), the meta FORM is recognized directly on a
|
|
2461
|
-
// miss —
|
|
2627
|
+
// miss — via BARE_WHATIS_RE (chat.mjs's own fact-lookup discipline, article
|
|
2628
|
+
// OPTIONAL — see that regex's docblock for why this is safe to loosen here
|
|
2629
|
+
// even though the structural grammar's T5 keeps the article mandatory).
|
|
2462
2630
|
let metaTerm = envelope?.parsed?.shape === "meta" ? envelope.parsed.object : null;
|
|
2463
2631
|
if (!metaTerm && miss && !envelope?.parsed) {
|
|
2464
|
-
const m = q.match(
|
|
2632
|
+
const m = q.match(BARE_WHATIS_RE)
|
|
2465
2633
|
|| q.match(/^what\s+(?:does|do)\s+(.+?)\s+means?[?.!\s]*$/i);
|
|
2466
2634
|
if (m) metaTerm = m[1];
|
|
2467
2635
|
}
|
|
@@ -2527,6 +2695,83 @@ async function factAnswer(memoryDir, query, envelope, miss) {
|
|
|
2527
2695
|
return null;
|
|
2528
2696
|
}
|
|
2529
2697
|
|
|
2698
|
+
// ---- BUG 1 fix (2026-07-09): "what else is X" repeated the SAME primary
|
|
2699
|
+
// definition sentence verbatim, byte-identical to a plain "what is X" turn
|
|
2700
|
+
// right before it. Root cause: "what else is a function" is NOT itself a
|
|
2701
|
+
// recognized shape anywhere in this file — ask()'s own relaxation cascade
|
|
2702
|
+
// (relaxParse, ask.mjs: NOISE-STRIP then DROP-UNMATCHED) quietly treats
|
|
2703
|
+
// "else" as an unmatched leftover token once the anchored grammar misses the
|
|
2704
|
+
// sentence as typed, drops it, and re-parses the survivor as the ORDINARY
|
|
2705
|
+
// "what is a function" meta shape — a real, non-miss answer, so relaxParse
|
|
2706
|
+
// happily accepts it. By the time curatedDefinitionAnswer/factAnswer see the
|
|
2707
|
+
// query, "else" is already gone and there is nothing left to distinguish a
|
|
2708
|
+
// follow-up asking for MORE from the original question. whatElseAnswer is
|
|
2709
|
+
// recognized FIRST, off the RAW query text (never the relaxed envelope), so
|
|
2710
|
+
// it always gets first look regardless of what the ask engine's own parse
|
|
2711
|
+
// collapsed the sentence to. ----
|
|
2712
|
+
|
|
2713
|
+
/** "what else is/are X" / "what else about X" / "what else do you know about
|
|
2714
|
+
* X" — the follow-up shape asking for information BEYOND whatever the
|
|
2715
|
+
* primary answer already said. Two separate anchors (not one alternation)
|
|
2716
|
+
* because the "is/are" copula form and the "about" form take the article
|
|
2717
|
+
* differently ("what else is a function" vs "what else about the cache").
|
|
2718
|
+
* The negative lookahead on the "is/are" form excludes "what else is
|
|
2719
|
+
* in/inside X" — that's a DIFFERENT, already-working feature (normalize.mjs
|
|
2720
|
+
* PHRASING_FRAMES rewrites it to "what does X contain", a members-of-class
|
|
2721
|
+
* query, tested by chatflow-tier1-single-touch.test.mjs); without this
|
|
2722
|
+
* exclusion this lane's own raw-text-first priority (it runs BEFORE ask()'s
|
|
2723
|
+
* pipeline even gets a look) would wrongly swallow that idiom as a
|
|
2724
|
+
* vocabulary-term lookup for the literal term "in X". */
|
|
2725
|
+
const WHAT_ELSE_IS_RE = /^what\s+else\s+(?:is|are)\s+(?!in\b|inside\b)(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
2726
|
+
const WHAT_ELSE_ABOUT_RE = /^what\s+else\s+(?:do\s+you\s+know\s+)?about\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i;
|
|
2727
|
+
|
|
2728
|
+
/** "what else is X" — surface remembered facts about X BEYOND the primary
|
|
2729
|
+
* curated (corpus/seon) prose definition, which is itself never a Facts row
|
|
2730
|
+
* (it comes from a separate prose file, seonDefinitions() — see
|
|
2731
|
+
* curatedDefinitionAnswer) — so every subject-side fact this returns is
|
|
2732
|
+
* genuinely additional information, never a repeat of the definition
|
|
2733
|
+
* sentence. Reuses factAnswer's own subject-scan machinery (memoryFacts +
|
|
2734
|
+
* factTermVariants + renderFactLine + the SAME FACT_ANSWER_CAP/'more'-paging
|
|
2735
|
+
* convention as factAnswer/factReadBack), just filtered/framed differently.
|
|
2736
|
+
*
|
|
2737
|
+
* Honest "nothing more" fallback (never a spurious repeat) in TWO cases: (a)
|
|
2738
|
+
* the term carries no facts at all — there is nothing to add beyond the
|
|
2739
|
+
* definition; (b) every fact line this would show ALREADY appears verbatim
|
|
2740
|
+
* in the immediately-preceding turn's answer (`last.answer`) — meaning the
|
|
2741
|
+
* primary answer was itself an exhaustive fact listing (via:"fact", not a
|
|
2742
|
+
* curated prose definition), so "what else" truly has nothing new to say.
|
|
2743
|
+
* That second check reuses this codebase's own established repeat-detection
|
|
2744
|
+
* discipline (comparing rendered lines against `last.answer` bytes — see
|
|
2745
|
+
* ORIENTATION_REPEAT_ONELINER/WALL_REPEAT_ONELINER for the same pattern). */
|
|
2746
|
+
async function whatElseAnswer(memoryDir, query, last) {
|
|
2747
|
+
if (!memoryDir) return null;
|
|
2748
|
+
const q = String(query).trim();
|
|
2749
|
+
const m = q.match(WHAT_ELSE_IS_RE) || q.match(WHAT_ELSE_ABOUT_RE);
|
|
2750
|
+
if (!m) return null;
|
|
2751
|
+
const term = m[1].trim();
|
|
2752
|
+
if (!term) return null;
|
|
2753
|
+
let normFactTerm;
|
|
2754
|
+
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
2755
|
+
const variants = factTermVariants(normFactTerm, term);
|
|
2756
|
+
const hits = (await memoryFacts(memoryDir)).filter((f) => variants.has(f.subject));
|
|
2757
|
+
const nothingMore = {
|
|
2758
|
+
text: `That's everything I know about "${term}" — /memory to see the full picture.`,
|
|
2759
|
+
replace: true,
|
|
2760
|
+
};
|
|
2761
|
+
if (!hits.length) return nothingMore;
|
|
2762
|
+
const lines = hits.map(renderFactLine);
|
|
2763
|
+
const prevAnswer = String(last?.answer || "");
|
|
2764
|
+
if (lines.every((l) => prevAnswer.includes(l))) return nothingMore;
|
|
2765
|
+
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
2766
|
+
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
2767
|
+
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
2768
|
+
return {
|
|
2769
|
+
text: `Beyond that, here's what else I know about "${term}":\n${shown.join("\n")}${extra}`,
|
|
2770
|
+
replace: true,
|
|
2771
|
+
...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}),
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2530
2775
|
/** Ontology plan tracks (a)+(b) (PLAN_ontology-hierarchies.md §3): a LAST-
|
|
2531
2776
|
* RESORT query-time synonym expansion for a "what is a X"-shaped term with NO
|
|
2532
2777
|
* direct facts. Deliberately run where the caller runs it (runAsk, after
|
|
@@ -3058,13 +3303,34 @@ function relationDefinitions() {
|
|
|
3058
3303
|
return seonRelsPromise;
|
|
3059
3304
|
}
|
|
3060
3305
|
|
|
3061
|
-
/**
|
|
3062
|
-
*
|
|
3063
|
-
*
|
|
3306
|
+
/** BUG 2 fix (2026-07-09): "what is a/an <term>" with the article made OPTIONAL,
|
|
3307
|
+
* for the FACT-LOOKUP path only (metaTermOf/factAnswer's own bare-form fallback)
|
|
3308
|
+
* — NOT grammar.mjs's structural T5 template, which keeps its article MANDATORY
|
|
3309
|
+
* on purpose (a bare "what is <anything>" would also swallow "what is the
|
|
3310
|
+
* meaning of this codebase", an existing, deliberately honest grammar-miss
|
|
3311
|
+
* regression — test/ask.test.mjs pins it null; see T5's own docblock). That
|
|
3312
|
+
* collision risk is a STRUCTURAL-PARSE concern (T5's tail becomes the literal
|
|
3313
|
+
* graph-query object); it doesn't apply here: this regex only extracts a
|
|
3314
|
+
* SUBJECT STRING to look up against the memory Facts store / curated lexicon —
|
|
3315
|
+
* a miss (no fact, no lexicon entry) is silently absorbed by the caller and
|
|
3316
|
+
* falls through to the ordinary honest-miss cascade, exactly like today's
|
|
3317
|
+
* mandatory-article miss does. Root cause this fixes: "what is john" (no
|
|
3318
|
+
* article) never matched the old mandatory-article regex at all, so a freshly
|
|
3319
|
+
* taught "john rdfs:subClassOf function" fact was invisible to "what is john"
|
|
3320
|
+
* even though "what is a john" (or "what is john used for") would have found
|
|
3321
|
+
* it — the fact-lookup path is a low-collision subject lookup, not a structural
|
|
3322
|
+
* parse, so loosening it here is safe. */
|
|
3323
|
+
const BARE_WHATIS_RE = /^what\s+(?:is|are)\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
3324
|
+
|
|
3325
|
+
/** The meta term a "what is a X" / "what is X" / "what does X mean" / "define X"
|
|
3326
|
+
* question asks about — from the parse when present, else recognized directly
|
|
3327
|
+
* via BARE_WHATIS_RE (article optional — see its own docblock for why that's
|
|
3328
|
+
* safe here even though the grammar's own T5 keeps the article mandatory).
|
|
3329
|
+
* Null when the line isn't such a form. */
|
|
3064
3330
|
function metaTermOf(query, envelope) {
|
|
3065
3331
|
if (envelope?.parsed?.shape === "meta" && envelope.parsed.object) return envelope.parsed.object;
|
|
3066
3332
|
const q = String(query).trim();
|
|
3067
|
-
const m = q.match(
|
|
3333
|
+
const m = q.match(BARE_WHATIS_RE)
|
|
3068
3334
|
|| q.match(/^what\s+(?:does|do)\s+(?:an?\s+)?(.+?)\s+means?[?.!\s]*$/i)
|
|
3069
3335
|
|| q.match(/^define\s+(?:an?\s+)?(.+?)[?.!\s]*$/i);
|
|
3070
3336
|
return m ? m[1].trim() : null;
|
|
@@ -3603,6 +3869,27 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
3603
3869
|
// facts/recall (a fact EXTENDS a non-miss schema hit too — NOT miss-gated),
|
|
3604
3870
|
// (4) TEACH lane (would-miss), (5) the short tailored miss (would-miss).
|
|
3605
3871
|
let handled = false;
|
|
3872
|
+
// (0) BUG 1 fix (2026-07-09): "what else is X" — recognized off the RAW
|
|
3873
|
+
// query text, before every other lane below (all of which read `envelope`,
|
|
3874
|
+
// already relaxed/reparsed by ask()'s own noise-strip cascade — "else" is
|
|
3875
|
+
// exactly the kind of unmatched token that cascade silently drops, which is
|
|
3876
|
+
// why a plain factAnswer/curatedDefinitionAnswer lookup used to answer
|
|
3877
|
+
// "what else is X" with the byte-identical primary definition, as if
|
|
3878
|
+
// repeating it were new information). via is set to a value NONE of the
|
|
3879
|
+
// downstream `via === "composed"/"fact"/"corpus/seon"` gates match, so a
|
|
3880
|
+
// hit here is final — curatedDefinitionAnswer/conceptForceAnswer never get
|
|
3881
|
+
// a chance to re-answer with the same primary definition afterward.
|
|
3882
|
+
if (memoryDir) {
|
|
3883
|
+
const whatElse = await whatElseAnswer(memoryDir, query, last);
|
|
3884
|
+
if (whatElse) {
|
|
3885
|
+
answer = whatElse.text; via = "fact:what-else"; recordMiss = false; handled = true;
|
|
3886
|
+
if (whatElse.pending) factPending = whatElse.pending;
|
|
3887
|
+
deduced = "surface additional remembered facts beyond the primary definition";
|
|
3888
|
+
note(trace, "lane: (0) WHAT ELSE — \"what else is/about X\" recognized off the raw query, before the relaxation cascade could quietly drop \"else\" and reduce it to a plain \"what is X\"");
|
|
3889
|
+
note(trace, "source: .tmct/memory Facts (see /memory for provenance per line)");
|
|
3890
|
+
note(trace, `goal: ${deduced} (revised — the raw \"what else\" phrasing was recognized directly, not the relaxed/reparsed envelope)`);
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3606
3893
|
// (1) #2 META/SELF: bare self/session questions ("what do you know", "what is this
|
|
3607
3894
|
// codebase", "how do i start") → a summary / orientation, answered before the
|
|
3608
3895
|
// fact-dump readers so "what do you know" gets a summary, not raw facts.
|
|
@@ -3702,7 +3989,37 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
3702
3989
|
} catch { /* leave false — the ordinary path decides */ }
|
|
3703
3990
|
}
|
|
3704
3991
|
}
|
|
3705
|
-
|
|
3992
|
+
const isConversationalCandidate = !handled && miss && !envelope?.parsed && isConversational(query) && !isWhatAboutContinuation && !isDescribePronounContinuation && !isExplainTouch && !isStaccatoNegation && !isVagueRelationTouch && !isStaccatoComparative && !isStaccatoPronounNoFocus;
|
|
3993
|
+
// BUG 2 fix (2026-07-09): "what is X" with NO article ("what is john") is BOTH
|
|
3994
|
+
// conversational-shaped (≤3 words, no code-ish token — isConversational() would
|
|
3995
|
+
// claim it) AND a legitimate bare meta/fact-lookup form (BARE_WHATIS_RE —
|
|
3996
|
+
// metaTermOf's own docblock explains why the article is safe to make optional on
|
|
3997
|
+
// this fact-lookup path specifically). Root cause: grammar.mjs's T5 template
|
|
3998
|
+
// requires the article, so envelope.parsed stays null for the bare form — which
|
|
3999
|
+
// is exactly isConversationalCandidate's own `!envelope?.parsed` gate — so
|
|
4000
|
+
// isConversational used to win the race unconditionally, and a freshly taught
|
|
4001
|
+
// "john is a function" fact became invisible the moment its own subject was
|
|
4002
|
+
// asked back about bare ("what is john" fell to the generic capability-
|
|
4003
|
+
// orientation card, byte-identical to asking about a term tmct had never heard
|
|
4004
|
+
// of). Diverts ONLY when a REAL fact actually resolves for the bare term —
|
|
4005
|
+
// never a speculative reroute: a bare "what is up"/"what is wrong" with nothing
|
|
4006
|
+
// behind it falls straight through to the SAME orientation card as before,
|
|
4007
|
+
// exactly like every other isConversationalCandidate exemption above (each one
|
|
4008
|
+
// guarantees a real answer before it defers, never stranding the turn on a
|
|
4009
|
+
// worse outcome — see isStaccatoPronounNoFocus's own docblock for the same
|
|
4010
|
+
// discipline).
|
|
4011
|
+
let bareMetaHit = null;
|
|
4012
|
+
if (isConversationalCandidate && memoryDir && BARE_WHATIS_RE.test(String(query).trim())) {
|
|
4013
|
+
bareMetaHit = (await factAnswer(memoryDir, query, envelope, miss))
|
|
4014
|
+
?? (await factReadBack(memoryDir, query, envelope, miss, graph));
|
|
4015
|
+
}
|
|
4016
|
+
if (bareMetaHit) {
|
|
4017
|
+
answer = bareMetaHit.replace ? bareMetaHit.text : `${answer}\n${bareMetaHit.text}`;
|
|
4018
|
+
via = "fact"; recordMiss = false; handled = true;
|
|
4019
|
+
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");
|
|
4021
|
+
note(trace, "source: .tmct/memory Facts (see /memory for provenance per line)");
|
|
4022
|
+
} else if (isConversationalCandidate) {
|
|
3706
4023
|
// A conversational miss (a greeting, "what can you do", a very short non-code
|
|
3707
4024
|
// line) gets the friendly orientation (module-aware: empty → --repo/tmct init).
|
|
3708
4025
|
// Bug B1 (0.8.2 follow-up): this branch carries via:"template" and never
|
|
@@ -3861,7 +4178,21 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
3861
4178
|
if (taught) {
|
|
3862
4179
|
answer = taught.text; via = taught.via; recordMiss = taught.miss;
|
|
3863
4180
|
note(trace, `lane: (4) TEACH — TEACH_RE/OWNS_TEACH_RE/BARE_DECLARATIVE_RE matched, ${taught.miss ? "but the payload could not be stored" : "reified into .tmct/memory"}`);
|
|
3864
|
-
|
|
4181
|
+
// Goal-line fix (Bug 3 point 4, 2026-07-09): `deduced` was computed WAY
|
|
4182
|
+
// above, straight off envelope.parsed alone (deduceGoalFromParsed) —
|
|
4183
|
+
// the structural grammar has no business parsing a teach-shaped
|
|
4184
|
+
// sentence at all ("remember tony has a hat" isn't a code-graph
|
|
4185
|
+
// question), so whatever it landed on there was either confidently
|
|
4186
|
+
// WRONG (a stray structural template matched part of the sentence and
|
|
4187
|
+
// deduced an unrelated GOAL_BY_KIND entry, e.g. "locate what a
|
|
4188
|
+
// module/class defines") or silently absent (no parse stood). Every
|
|
4189
|
+
// successfully-RECOGNIZED teach attempt (`taught` stood — whether it
|
|
4190
|
+
// went on to STORE or to its own honest teach-miss text) gets the SAME
|
|
4191
|
+
// honest, consistent goal line here instead — the same "revise off the
|
|
4192
|
+
// LANE that actually answered, not the raw structural parse"
|
|
4193
|
+
// discipline the relation-force fix above already uses.
|
|
4194
|
+
deduced = "teach/remember a new fact";
|
|
4195
|
+
note(trace, `goal: ${deduced} (revised — the teach lane recognized this shape where the raw structural parse never should have)`);
|
|
3865
4196
|
}
|
|
3866
4197
|
}
|
|
3867
4198
|
// (4b) #4 AUTHOR lane (0.8.2 WS4) — "who is <Name>", "what did <Name> touch",
|