@polycode-projects/the-mechanical-code-talker 1.11.0 → 1.11.6
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 +13 -20
- package/ROADMAP.md +9 -12
- package/bin/tmct.mjs +82 -98
- package/data/games/river.txt +33 -0
- package/package.json +1 -1
- package/src/ask.mjs +119 -6
- package/src/chat.mjs +962 -51
- package/src/codegraph.mjs +24 -11
- package/src/domain.mjs +85 -6
- package/src/interpret/normalize.mjs +46 -0
- package/src/interpret/strategies/keywords.mjs +13 -9
- package/src/ledger-viz.mjs +31 -9
- package/src/memory/core.mjs +20 -1
- package/src/memory/shacl.mjs +4 -1
- package/src/memory-ask-browser-entry.mjs +5 -5
- package/src/memory-ask-browser.bundle.js +526 -47
- package/src/plan-viz.mjs +5 -6
- package/src/router/drive.mjs +122 -13
- package/src/router/resolver.mjs +13 -0
- package/src/router/taught.mjs +19 -8
- package/src/syllogise.mjs +154 -38
- package/src/viz-theme.mjs +19 -3
- package/src/ask-browser-entry.mjs +0 -19
- package/src/ask-browser.bundle.js +0 -5142
- package/src/viz.mjs +0 -959
package/src/chat.mjs
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
// structured sidecar (.tmct/sessions/session-<uuidv7>.jsonl, sessions.mjs) and
|
|
12
12
|
// a `Session` individual upserted into graph.json per turn.
|
|
13
13
|
//
|
|
14
|
-
// runTurn(input, …) is a PURE function so tests exercise it directly;
|
|
15
|
-
//
|
|
14
|
+
// runTurn(input, …) is a PURE function so tests exercise it directly; the ask
|
|
15
|
+
// ENGINE is imported lazily and failure-tolerated, so a turn never crashes
|
|
16
|
+
// (the one static ask.mjs import, classDisplayName, is a pure formatter).
|
|
16
17
|
// createSession(…) is the SESSION SINK every shell shares (runChat's readline
|
|
17
18
|
// loop, src/tui/app.mjs's Ink shell).
|
|
18
19
|
|
|
@@ -26,6 +27,7 @@ import { dispatchTool, loadGraph } from "./server.mjs";
|
|
|
26
27
|
import { loadConfig, DEFAULT_GRAPH_REL } from "./config.mjs";
|
|
27
28
|
import { resolveRuntimeConfig } from "./cli-args.mjs";
|
|
28
29
|
import { parseEntities, edgesOfKind, renderAuthorCard, renderAuthorTouches, renderCommitAuthor, resolveSymbol, renderCompare } from "./codegraph.mjs";
|
|
30
|
+
import { classDisplayName } from "./ask.mjs";
|
|
29
31
|
import { SESSIONS_DIR_REL, appendSessionToGraph } from "./sessions.mjs";
|
|
30
32
|
import { uuidv7 } from "./uuid.mjs";
|
|
31
33
|
import { createTelemetry } from "./telemetry.mjs";
|
|
@@ -33,6 +35,7 @@ import * as defaultSource from "./source.mjs";
|
|
|
33
35
|
import { loadTemplates, render as renderTemplate } from "./corpus/templates.mjs";
|
|
34
36
|
import { resolveExtensions, mergedLexiconExtra } from "./extensions.mjs";
|
|
35
37
|
import { rankByBiasThenTrust } from "./memory/bias.mjs";
|
|
38
|
+
import { HAS_A_PREDICATE } from "./memory/core.mjs";
|
|
36
39
|
import { finish, beginsWithVowelSound, grammarRules } from "./finish.mjs";
|
|
37
40
|
import { splitSentences } from "./sentences.mjs";
|
|
38
41
|
import {
|
|
@@ -102,6 +105,10 @@ const GOAL_BY_KIND = {
|
|
|
102
105
|
};
|
|
103
106
|
const goalNoun = (entityType) => (entityType ? `${String(entityType).toLowerCase()}(s)` : "entities");
|
|
104
107
|
|
|
108
|
+
/** The goal wording for a taught subject/verb/object lookup — shared by
|
|
109
|
+
* runAsk's fact-lane goal revision and withDeducedGoal's fact-reader field. */
|
|
110
|
+
const TAUGHT_FACT_LOOKUP_GOAL = "look up a taught fact about a subject/verb/object";
|
|
111
|
+
|
|
105
112
|
/** Deduce a one-line goal statement from the ask engine's parsed AST — either
|
|
106
113
|
* the plain-clause form ({shape,kind,entityType,object[,subject]}) or the
|
|
107
114
|
* compositional form ({node:...}, ask.mjs's §compositional grammar). Returns
|
|
@@ -1464,7 +1471,7 @@ function moduleOverviewText(graph, ind) {
|
|
|
1464
1471
|
parts.push(testedBy.length
|
|
1465
1472
|
? `covered by ${testedBy.length} test module${testedBy.length === 1 ? "" : "s"}`
|
|
1466
1473
|
: "no recorded tests");
|
|
1467
|
-
const cls = (ind.class || "entity")
|
|
1474
|
+
const cls = classDisplayName(ind.class || "entity");
|
|
1468
1475
|
const pointer = pickPhrase("full-breakdown", ind.id, "for the full breakdown");
|
|
1469
1476
|
return `${ind.label} is a ${cls} — ${parts.join("; ")}. `
|
|
1470
1477
|
+ `/describe ${ind.label} ${pointer}.`;
|
|
@@ -1533,7 +1540,7 @@ const WALL_MISS_ANYWHERE_RE = /couldn't parse this as a graph question\. Try:/;
|
|
|
1533
1540
|
// assert/memory path; when it can't be stored, say what CAN be remembered
|
|
1534
1541
|
// instead of the grammar wall or a silent data loss.
|
|
1535
1542
|
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|learn)\b(?:\s+(?:this|that|also))?[:,]?\s*(?:that\s+)?(.+?)[.?!]*$/i;
|
|
1536
|
-
const BARE_DECLARATIVE_RE = /^(?:every |each |all |a |an )?[\w-]+(?: [\w-]+)? (?:is|are) (?:a |an )?[\w-]
|
|
1543
|
+
const BARE_DECLARATIVE_RE = /^(?:every |each |all |a |an )?[\w-]+(?: [\w-]+)? (?:is|are) (?:a |an )?[\w-]+(?: too)?$/i;
|
|
1537
1544
|
/** "X is <comparative> than Y" — the comparative teach/ask surface. The
|
|
1538
1545
|
* comparative slot is closed by SHAPE (-er word, better/worse, or a
|
|
1539
1546
|
* more/less + adjective pair), never a hand-list of adjectives. */
|
|
@@ -1601,12 +1608,9 @@ const HAS_PROPERTY_PREDICATE = "mgx:hasProperty";
|
|
|
1601
1608
|
// "some/a few Xs are Ys" shape) stay obviously in that same family rather than
|
|
1602
1609
|
// re-typing the CURIE string at each call site.
|
|
1603
1610
|
const SUBCLASS_PREDICATE = "rdfs:subClassOf";
|
|
1604
|
-
//
|
|
1605
|
-
//
|
|
1606
|
-
//
|
|
1607
|
-
// obviously in that same family, interoperable with corpus HasA data on the
|
|
1608
|
-
// read side, rather than minting a redundant mgx:has.
|
|
1609
|
-
const HAS_A_PREDICATE = "mgx:hasA";
|
|
1611
|
+
// HAS_A_PREDICATE (imported from memory/core.mjs, the canonical home) keeps
|
|
1612
|
+
// generalVerbTeach's "has"/"have" special case in the same family ConceptNet's
|
|
1613
|
+
// /r/HasA corpus facts already use, rather than minting a redundant mgx:has.
|
|
1610
1614
|
|
|
1611
1615
|
// mgx:sourceType's own closed kind set (memory/core.mjs) splits "the operator
|
|
1612
1616
|
// said it" across two tags depending which lane wrote it (ace: -> "operator",
|
|
@@ -1679,6 +1683,46 @@ const OWNS_PASSIVE_TEACH_RE = /^(.+?)\s+(?:is|are|was|were)\s+owned\s+by\s+([A-Z
|
|
|
1679
1683
|
const RELATION_FACT_TEACH_RE =
|
|
1680
1684
|
/^([\w'-]+(?:\s+[A-Z][\w'-]*)?)\s+(?:is|are|was|were)\s+the\s+([a-z][\w-]*)\s+of\s+([\w'-]+(?:\s+[A-Z][\w'-]*)?)[.!?]*$/i;
|
|
1681
1685
|
|
|
1686
|
+
/** The GENITIVE surfaces of the same relational fact — "ahab is john's
|
|
1687
|
+
* father" and "john's father is ahab" both state exactly what "ahab is the
|
|
1688
|
+
* father of john" states, so both store through the identical
|
|
1689
|
+
* generalVerbPredicate mint (subject=ahab, relation=father, object=john).
|
|
1690
|
+
* Same 1-2-token name captures as RELATION_FACT_TEACH_RE; the role slot is
|
|
1691
|
+
* the same lowercase bare noun. The possessive's own token deliberately
|
|
1692
|
+
* excludes apostrophes ([\w-]+, not [\w'-]+) so the 's split is unambiguous. */
|
|
1693
|
+
const GENITIVE_RELATION_TEACH_RE =
|
|
1694
|
+
/^([\w-]+(?:\s+[A-Z][\w-]*)?)\s+(?:is|was)\s+([\w-]+(?:\s+[A-Z][\w-]*)?)'s\s+([a-z][\w-]*)[.!?]*$/i;
|
|
1695
|
+
const GENITIVE_RELATION_TEACH_REV_RE =
|
|
1696
|
+
/^([\w-]+(?:\s+[A-Z][\w-]*)?)'s\s+([a-z][\w-]*)\s+(?:is|was)\s+([\w-]+(?:\s+[A-Z][\w-]*)?)[.!?]*$/i;
|
|
1697
|
+
|
|
1698
|
+
/** The VERB-INFLECTED surface of the same relational fact — "ahab fathered
|
|
1699
|
+
* john" states what "ahab is the father of john" states, so it stores
|
|
1700
|
+
* through the identical generalVerbPredicate mint. Same 1-2-token name
|
|
1701
|
+
* captures as RELATION_FACT_TEACH_RE on both sides; the verb slot requires
|
|
1702
|
+
* a literal "-ed" tail, so a present-tense "john likes mary" never matches
|
|
1703
|
+
* (that bare shape stays wrapper-required — see the nudge in runAsk). The
|
|
1704
|
+
* regex is only the SHAPE trigger: matchRelationalVerbTeach (below) adds
|
|
1705
|
+
* the determiner/closed-class/POS guards that keep "the build failed
|
|
1706
|
+
* yesterday" and "john failed spectacularly" out. */
|
|
1707
|
+
const RELATION_VERB_TEACH_RE =
|
|
1708
|
+
/^([\w'-]+(?:\s+[A-Z][\w'-]*)?)\s+([a-z][\w-]*ed)\s+([\w'-]+(?:\s+[A-Z][\w'-]*)?)[.!?]*$/i;
|
|
1709
|
+
|
|
1710
|
+
/** Closed past-tense strip: "<base>ed" (fathered → father), the doubled-
|
|
1711
|
+
* consonant form (hopped → hop), and the -ied fold (carried → carry).
|
|
1712
|
+
* Returns null when the word doesn't carry a strippable "-ed" tail at all.
|
|
1713
|
+
* Deliberately naive (same accepted trade as singularizeSurface) — callers
|
|
1714
|
+
* prefer wink's lemma when it's available and only lean on this strip as
|
|
1715
|
+
* the shape check / fallback. */
|
|
1716
|
+
function pastVerbBase(verb) {
|
|
1717
|
+
const v = String(verb || "").toLowerCase();
|
|
1718
|
+
const m = v.match(/^([a-z][a-z-]*)ed$/);
|
|
1719
|
+
if (!m || m[1].length < 2) return null;
|
|
1720
|
+
const stem = m[1];
|
|
1721
|
+
if (/([b-df-hj-np-tv-z])\1$/.test(stem)) return stem.slice(0, -1);
|
|
1722
|
+
if (/[^aeiou]i$/.test(stem)) return `${stem.slice(0, -1)}y`;
|
|
1723
|
+
return stem;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1682
1726
|
/** "every/a/an/the <N1> has a/an <N2> method" — the HAS-A-METHOD teach
|
|
1683
1727
|
* declarative: a possession-of-capability claim about a class/entity's
|
|
1684
1728
|
* method ("every Component has a render method", "a Widget has a render
|
|
@@ -1762,7 +1806,7 @@ const RECURSIVE_RULE_TEACH_RE =
|
|
|
1762
1806
|
|
|
1763
1807
|
/** ACTION-RULE TEACH FRAMES — a world-mutating action taught one sentence at
|
|
1764
1808
|
* a time, each sentence its own Rule individual (kind action-signature /
|
|
1765
|
-
* action-precond / action-effect) sharing one rule name ("<verb> <prep>",
|
|
1809
|
+
* action-precond / action-effect / action-constraint) sharing one rule name ("<verb> <prep>",
|
|
1766
1810
|
* e.g. "move onto"). src/domain.mjs collects the family by name
|
|
1767
1811
|
* (findRulesByName) and grounds it over class members at plan time; nothing
|
|
1768
1812
|
* in the teach lane executes an action. Predicate slot values are stored
|
|
@@ -1770,13 +1814,28 @@ const RECURSIVE_RULE_TEACH_RE =
|
|
|
1770
1814
|
* values; readers re-attach it. The class/role words are single tokens, the
|
|
1771
1815
|
* preposition set is PREP_SRC, the comparative slot is COMPARATIVE_SRC. */
|
|
1772
1816
|
const ACTION_SIGNATURE_TEACH_RE = new RegExp(
|
|
1773
|
-
`^you\\s+can\\s+([a-z]+)\\s+an?\\s+([a-z][\\w-]*)\\s+(${PREP_SRC})\\s+an?\\s+([a-z][\\w-]*)[.!?]*$`, "i");
|
|
1817
|
+
`^you\\s+(?:can|may)\\s+([a-z]+)\\s+an?\\s+([a-z][\\w-]*)\\s+(${PREP_SRC})\\s+an?\\s+([a-z][\\w-]*)[.!?]*$`, "i");
|
|
1818
|
+
// The passive voicing of the same signature ("a disk can be moved onto a
|
|
1819
|
+
// peg"): class first, participle verb. Minted through the same actionLemma
|
|
1820
|
+
// authority so both voicings land on one rule name.
|
|
1821
|
+
const ACTION_SIGNATURE_PASSIVE_RE = new RegExp(
|
|
1822
|
+
`^an?\\s+([a-z][\\w-]*)\\s+(?:can|may)\\s+be\\s+([a-z]+)\\s+(${PREP_SRC})\\s+an?\\s+([a-z][\\w-]*)[.!?]*$`, "i");
|
|
1774
1823
|
const ACTION_PRECOND_NOTHING_RE = new RegExp(
|
|
1775
1824
|
`^to\\s+([a-z]+)\\s+an?\\s+([a-z][\\w-]*)\\s+(${PREP_SRC})\\s+an?\\s+([a-z][\\w-]*)\\s*,?\\s*nothing\\s+may\\s+([a-z]+)\\s+(${PREP_SRC})\\s+the\\s+([a-z][\\w-]*)[.!?]*$`, "i");
|
|
1776
1825
|
const ACTION_PRECOND_COMPARATIVE_RE = new RegExp(
|
|
1777
1826
|
`^to\\s+([a-z]+)\\s+an?\\s+([a-z][\\w-]*)\\s+(${PREP_SRC})\\s+an?\\s+([a-z][\\w-]*)\\s*,?\\s*the\\s+([a-z][\\w-]*)\\s+must\\s+be\\s+(${COMPARATIVE_SRC})\\s+than\\s+the\\s+([a-z][\\w-]*)[.!?]*$`, "i");
|
|
1778
1827
|
const ACTION_EFFECT_TEACH_RE = new RegExp(
|
|
1779
|
-
`^([a-z]+ing)\\s+an?\\s+([a-z][\\w-]*)\\s+(${PREP_SRC})\\s+an?\\s+([a-z][\\w-]*)\\s+makes\\s+the\\s+([a-z][\\w-]*)\\s+([a-z]+)\\s+(${PREP_SRC})\\s+the\\s+([a-z][\\w-]*)[.!?]*$`, "i");
|
|
1828
|
+
`^([a-z]+ing)\\s+an?\\s+([a-z][\\w-]*)\\s+(${PREP_SRC})\\s+an?\\s+([a-z][\\w-]*)\\s+makes\\s+(?:it|the\\s+([a-z][\\w-]*))\\s+([a-z]+)\\s+(${PREP_SRC})\\s+the\\s+([a-z][\\w-]*)[.!?]*$`, "i");
|
|
1829
|
+
/** "to ferry a passenger onto a bank, the wolf may not be with the goat
|
|
1830
|
+
* without the farmer" — the co-location CONSTRAINT sentence (kind
|
|
1831
|
+
* action-constraint): after a move, <left> and <right> may not share a
|
|
1832
|
+
* position unless <guard> is there too. All three trailing words name a
|
|
1833
|
+
* class whose sole member src/domain.mjs binds at plan time. Disjoint from
|
|
1834
|
+
* the two precondition frames above by anchor phrase alone ("may not be
|
|
1835
|
+
* with … without", never "nothing may" or "must be … than") — PREP_SRC has
|
|
1836
|
+
* no "without", so the preposition captures can't collide either. */
|
|
1837
|
+
const ACTION_CONSTRAINT_TEACH_RE = new RegExp(
|
|
1838
|
+
`^to\\s+([a-z]+)\\s+an?\\s+([a-z][\\w-]*)\\s+(${PREP_SRC})\\s+an?\\s+([a-z][\\w-]*)\\s*,?\\s*the\\s+([a-z][\\w-]*)\\s+may\\s+not\\s+be\\s+with\\s+the\\s+([a-z][\\w-]*)\\s+without\\s+the\\s+([a-z][\\w-]*)[.!?]*$`, "i");
|
|
1780
1839
|
/** "a disk renders as a block" — the render-template binding, an ordinary
|
|
1781
1840
|
* Fact on the curated mgx:rendersAs predicate (camelCase, so the
|
|
1782
1841
|
* general-verb preposition fold can never suffix it). */
|
|
@@ -1836,6 +1895,12 @@ async function bareTaxonomyTeach(line, { memoryDir, sessionId }) {
|
|
|
1836
1895
|
// not a Rule — goals accumulate on the session's planState slot.
|
|
1837
1896
|
const GOAL_TEACH_RE = new RegExp(
|
|
1838
1897
|
`^the\\s+goal\\s+is\\s+that\\s+(?:(every|each|all)\\s+)?([\\w-]+)\\s+([a-z]+s)\\s+(${PREP_SRC})\\s+([\\w-]+)[.!?]*$`, "i");
|
|
1898
|
+
// The infinitive-complement voicings of the same goal ("the goal is for every
|
|
1899
|
+
// disk to rest on peg-b", "i want every disk to rest on peg-b") — same
|
|
1900
|
+
// captures, verb already in base form. The confirmation restates the that-form
|
|
1901
|
+
// so the normalization is disclosed.
|
|
1902
|
+
const GOAL_TEACH_INFINITIVE_RE = new RegExp(
|
|
1903
|
+
`^(?:the\\s+goal\\s+is\\s+for|i\\s+want)\\s+(?:(every|each|all)\\s+)?([\\w-]+)\\s+to\\s+([a-z]+)\\s+(${PREP_SRC})\\s+([\\w-]+)[.!?]*$`, "i");
|
|
1839
1904
|
const PLAN_SOLVE_RE = /^(?:solve\s+it|plan\s+the\s+moves|how\s+do\s+i\s+get(?:\s+from\s+here)?\s+to\s+the\s+goal)[?.!\s]*$/i;
|
|
1840
1905
|
const LEGAL_MOVES_RE = /^what\s+moves\s+are\s+legal(?:\s+now)?[?.!\s]*$/i;
|
|
1841
1906
|
const PLAN_NEXT_RE = /^(?:next|next\s+move|go\s+on|continue)[.!?\s]*$/i;
|
|
@@ -2476,6 +2541,93 @@ async function subjectIsNounOrPropn(word) {
|
|
|
2476
2541
|
}
|
|
2477
2542
|
}
|
|
2478
2543
|
|
|
2544
|
+
/** Recognize "<Name> <verb>ed <Name>" ("ahab fathered john") as a relational
|
|
2545
|
+
* teach, or null. RELATION_VERB_TEACH_RE gives the shape; this adds the
|
|
2546
|
+
* guards that keep non-relational pasts out:
|
|
2547
|
+
* - neither side may lead with a determiner ("the build failed yesterday")
|
|
2548
|
+
* or a closed-class word;
|
|
2549
|
+
* - the verb may not be a closed-class or structural word;
|
|
2550
|
+
* - both name heads must POS-tag NOUN/PROPN (the same wink adapter
|
|
2551
|
+
* subjectIsNounOrPropn uses — "john failed spectacularly" tags its tail
|
|
2552
|
+
* ADV and declines). No wink → no signal, never a store;
|
|
2553
|
+
* - wink's lemma must actually DIFFER from the typed verb — a base-form
|
|
2554
|
+
* "-eed" word ("breed", "exceed") is not an inflected past at all, and
|
|
2555
|
+
* lemma-vs-strip disagreement resolves toward the lemma so the minted
|
|
2556
|
+
* predicate matches what the wrapped "remember that ahab fathered john"
|
|
2557
|
+
* path (generalVerbTeach) would mint.
|
|
2558
|
+
* Returns { subject, verb, base, object }; `base` is what the caller mints
|
|
2559
|
+
* through generalVerbPredicate. */
|
|
2560
|
+
async function matchRelationalVerbTeach(text) {
|
|
2561
|
+
const line = String(text || "").trim();
|
|
2562
|
+
const m = line.match(RELATION_VERB_TEACH_RE);
|
|
2563
|
+
if (!m) return null;
|
|
2564
|
+
const [, subjectRaw, verbRaw, objectRaw] = m;
|
|
2565
|
+
const verb = verbRaw.toLowerCase();
|
|
2566
|
+
const strip = pastVerbBase(verb);
|
|
2567
|
+
if (!strip) return null;
|
|
2568
|
+
if (GENERAL_VERB_NOT_A_VERB_RE.test(verb) || STRUCT_WORDS.has(verb)) return null;
|
|
2569
|
+
const subjWords = subjectRaw.split(/\s+/);
|
|
2570
|
+
const objWords = objectRaw.split(/\s+/);
|
|
2571
|
+
for (const head of [subjWords[0], objWords[0]]) {
|
|
2572
|
+
if (GENERAL_VERB_DETERMINER_RE.test(head) || GENERAL_VERB_NOT_A_VERB_RE.test(head)) return null;
|
|
2573
|
+
}
|
|
2574
|
+
try {
|
|
2575
|
+
const { nlpAdapter } = await import("./ask-nlp.mjs");
|
|
2576
|
+
const adapter = nlpAdapter();
|
|
2577
|
+
if (!adapter) return null;
|
|
2578
|
+
const tags = adapter.posTags([...subjWords, verbRaw, ...objWords]);
|
|
2579
|
+
const nameTag = (t) => t === "NOUN" || t === "PROPN";
|
|
2580
|
+
if (!nameTag(tags[0]) || !nameTag(tags[subjWords.length + 1])) return null;
|
|
2581
|
+
} catch {
|
|
2582
|
+
return null;
|
|
2583
|
+
}
|
|
2584
|
+
let base = strip;
|
|
2585
|
+
try {
|
|
2586
|
+
const { proseLemma } = await import("./prose-nlp.mjs");
|
|
2587
|
+
const lemma = proseLemma();
|
|
2588
|
+
if (lemma) {
|
|
2589
|
+
const l = lemma(verb);
|
|
2590
|
+
if (l === verb) return null; // wink says this is already a base form, not a past
|
|
2591
|
+
if (l) base = l;
|
|
2592
|
+
}
|
|
2593
|
+
} catch { /* no lemmatizer — the closed strip stands */ }
|
|
2594
|
+
return { subject: subjectRaw.trim(), verb, base, object: objectRaw.trim() };
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
/** The bare "<name> <verb>s <name>" nudge text ("john likes mary"), or null.
|
|
2598
|
+
* The bare form stays wrapper-required — the imperative-lookalike problem in
|
|
2599
|
+
* subjectIsNounOrPropn's docblock is only half the story at exactly three
|
|
2600
|
+
* words, where the conversational catch-all otherwise answers with the
|
|
2601
|
+
* orientation card. This recognizes the shape ONLY well enough to point at
|
|
2602
|
+
* the wrapped form that does store; it never stores anything itself. Closed
|
|
2603
|
+
* the same way matchRelationalVerbTeach is: no determiner/closed-class
|
|
2604
|
+
* heads, no structural/discourse verb, subject POS-tags NOUN/PROPN, object
|
|
2605
|
+
* tags NOUN/PROPN/ADJ (wink tags bare lowercase names like "mary" ADJ;
|
|
2606
|
+
* a genuine adverb tail — "dog barks loudly" — still declines). */
|
|
2607
|
+
async function bareTeachWrapperNudgeText(text) {
|
|
2608
|
+
const line = String(text || "").trim().replace(/[.!?]+\s*$/, "");
|
|
2609
|
+
const m = line.match(/^([\w'-]+)\s+([a-z][\w-]*s)\s+([\w'-]+)$/i);
|
|
2610
|
+
if (!m) return null;
|
|
2611
|
+
const [, subj, verbRaw, obj] = m;
|
|
2612
|
+
const verb = verbRaw.toLowerCase();
|
|
2613
|
+
if (/^(?:is|was|does)$/.test(verb)) return null;
|
|
2614
|
+
if (GENERAL_VERB_NOT_A_VERB_RE.test(verb) || STRUCT_WORDS.has(verb) || HABITUAL_VERB_EXCLUDE.has(verb)) return null;
|
|
2615
|
+
for (const head of [subj, obj]) {
|
|
2616
|
+
if (GENERAL_VERB_DETERMINER_RE.test(head) || GENERAL_VERB_NOT_A_VERB_RE.test(head)) return null;
|
|
2617
|
+
}
|
|
2618
|
+
try {
|
|
2619
|
+
const { nlpAdapter } = await import("./ask-nlp.mjs");
|
|
2620
|
+
const adapter = nlpAdapter();
|
|
2621
|
+
if (!adapter) return null;
|
|
2622
|
+
const tags = adapter.posTags([subj, verbRaw, obj]);
|
|
2623
|
+
if (tags[0] !== "NOUN" && tags[0] !== "PROPN") return null;
|
|
2624
|
+
if (tags[2] !== "NOUN" && tags[2] !== "PROPN" && tags[2] !== "ADJ") return null;
|
|
2625
|
+
} catch {
|
|
2626
|
+
return null;
|
|
2627
|
+
}
|
|
2628
|
+
return `I don't store a bare "${line}" on its own — to store that, say: "remember that ${line}".`;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2479
2631
|
// ---- General verb-to-predicate DIRECT-QUESTION retrieval: "does margo eat
|
|
2480
2632
|
// ribs" / "what does margo eat" against a fact taught via generalVerbTeach.
|
|
2481
2633
|
// Wired into factReadBack, gated on an already-true `miss` so a real graph
|
|
@@ -2518,8 +2670,85 @@ function assertCandidates(payload) {
|
|
|
2518
2670
|
const p = String(payload).trim();
|
|
2519
2671
|
const out = [p];
|
|
2520
2672
|
if (!/^(?:every|each|all|a|an)\b/i.test(p)) out.push(`every ${p}`);
|
|
2673
|
+
// "dogs are animals" — the bare-plural surface of the membership shape the
|
|
2674
|
+
// grammar already owns as "every dog is an animal". Purely additive and
|
|
2675
|
+
// inherently safe: the rewritten candidate still has to parse against the
|
|
2676
|
+
// closed lexicon, so a false singular ("redis" → "redi") never stores. A
|
|
2677
|
+
// trailing "too" is tolerated — it adds discourse flavor, not content.
|
|
2678
|
+
const plural = p.match(/^(?:all\s+|every\s+|each\s+)?([\w-]+)\s+are\s+([\w-]+?)(?:\s+too)?[.!?]*$/i);
|
|
2679
|
+
if (plural) {
|
|
2680
|
+
const subject = singularizeSurface(plural[1].toLowerCase());
|
|
2681
|
+
const object = singularizeSurface(plural[2].toLowerCase());
|
|
2682
|
+
if (subject !== plural[1].toLowerCase() || object !== plural[2].toLowerCase()) {
|
|
2683
|
+
const articleRule = grammarRules().find((r) => r.kind === "article");
|
|
2684
|
+
const article = articleRule && beginsWithVowelSound(object, articleRule) ? "an" : "a";
|
|
2685
|
+
out.push(`every ${subject} is ${article} ${object}`);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
// HABITUAL → CAPABILITY: "dogs bark" / "a dog barks" are the habitual
|
|
2689
|
+
// surfaces of the capability teach the lane already owns as "a dog can
|
|
2690
|
+
// bark" (the same reading the seed corpus itself uses: dog /r/CapableOf
|
|
2691
|
+
// bark). Same safety story as the plural rewrite above — the candidate
|
|
2692
|
+
// still has to ground through the teach path (this rewrite, or teachLane's
|
|
2693
|
+
// grounded-subject direct write), so a subject grounded nowhere
|
|
2694
|
+
// ("penguins swim" with no prior grounding) stays an honest decline.
|
|
2695
|
+
const habitual = matchBareHabitualTeach(p);
|
|
2696
|
+
if (habitual) {
|
|
2697
|
+
const articleRule = grammarRules().find((r) => r.kind === "article");
|
|
2698
|
+
const article = articleRule && beginsWithVowelSound(habitual.subject, articleRule) ? "an" : "a";
|
|
2699
|
+
out.push(`${article} ${habitual.subject} can ${habitual.verb}`);
|
|
2700
|
+
}
|
|
2521
2701
|
return [...new Set(out)];
|
|
2522
2702
|
}
|
|
2703
|
+
|
|
2704
|
+
/** The two bare HABITUAL teach surfaces, recognized as one shape:
|
|
2705
|
+
* "dogs bark" (plural subject + base verb) and "a dog barks" (articled
|
|
2706
|
+
* singular + 3sg verb), both meaning the capability fact "a dog can
|
|
2707
|
+
* bark". Returns {subject, verb} folded to the singular/base forms, or
|
|
2708
|
+
* null. Deliberately closed: structural verbs (imports/calls/tests …)
|
|
2709
|
+
* are excluded so a truncated code query never reads as a capability
|
|
2710
|
+
* claim, and the plural surface's verb must be a BASE form (no
|
|
2711
|
+
* plural-looking "s" tail — "dogs animals" is not a habitual sentence;
|
|
2712
|
+
* "pass"/"miss"-style "ss" verbs stay eligible). */
|
|
2713
|
+
/** Words that sit in the habitual shapes' verb slot without being verbs —
|
|
2714
|
+
* politeness/discourse tails ("jokes please", "dogs too") that must stay
|
|
2715
|
+
* with the conversational lane. */
|
|
2716
|
+
const HABITUAL_VERB_EXCLUDE = new Set([
|
|
2717
|
+
"please", "thanks", "kindly", "anyway", "though", "indeed", "maybe",
|
|
2718
|
+
"perhaps", "still", "too", "also", "instead", "now", "then", "here", "there",
|
|
2719
|
+
]);
|
|
2720
|
+
function matchBareHabitualTeach(text) {
|
|
2721
|
+
const t = String(text || "").trim();
|
|
2722
|
+
const plural = t.match(/^(?:all\s+|every\s+)?([\w-]+s)\s+([a-z][\w-]*)[.!?]*$/i);
|
|
2723
|
+
if (plural && !STRUCT_WORDS.has(plural[2].toLowerCase()) && !HABITUAL_VERB_EXCLUDE.has(plural[2].toLowerCase()) && !/[^s]s$/i.test(plural[2])) {
|
|
2724
|
+
const subject = singularizeSurface(plural[1].toLowerCase());
|
|
2725
|
+
if (subject !== plural[1].toLowerCase()) return { subject, verb: plural[2].toLowerCase() };
|
|
2726
|
+
}
|
|
2727
|
+
const singular = t.match(/^an?\s+([\w-]+)\s+([a-z][\w-]*s)[.!?]*$/i);
|
|
2728
|
+
if (singular && !STRUCT_WORDS.has(singular[2].toLowerCase()) && !HABITUAL_VERB_EXCLUDE.has(singular[2].toLowerCase())) {
|
|
2729
|
+
const verb = singularizeSurface(singular[2].toLowerCase());
|
|
2730
|
+
if (verb !== singular[2].toLowerCase()) return { subject: singular[1].toLowerCase(), verb };
|
|
2731
|
+
}
|
|
2732
|
+
return null;
|
|
2733
|
+
}
|
|
2734
|
+
/** The EXPLICIT capability surface — "a wren can sing" / "penguins can swim":
|
|
2735
|
+
* the same {subject, verb} reading matchBareHabitualTeach folds its two
|
|
2736
|
+
* habitual surfaces onto, for the sentence that says "can" outright. The ACE
|
|
2737
|
+
* grammar already owns this shape for closed-lexicon words; recognizing it
|
|
2738
|
+
* here lets the teach lane's grounded-subject direct write catch a subject
|
|
2739
|
+
* grounded only by a prior taught fact. Same closed verb-slot exclusions as
|
|
2740
|
+
* the habitual shapes; a question lead ("can a wren sing") never reaches
|
|
2741
|
+
* this — every call site is already QUESTION_LEAD-gated. */
|
|
2742
|
+
function matchBareCanTeach(text) {
|
|
2743
|
+
const m = String(text || "").trim().match(/^(?:an?\s+|every\s+|all\s+)?([\w-]+)\s+can\s+([a-z][\w-]*)[.!?]*$/i);
|
|
2744
|
+
if (!m) return null;
|
|
2745
|
+
const subject = m[1].toLowerCase();
|
|
2746
|
+
const verb = m[2].toLowerCase();
|
|
2747
|
+
if (STRUCT_WORDS.has(verb) || HABITUAL_VERB_EXCLUDE.has(verb) || GENERAL_VERB_NOT_A_VERB_RE.test(verb)) return null;
|
|
2748
|
+
if (GENERAL_VERB_DETERMINER_RE.test(subject) || GENERAL_VERB_NOT_A_VERB_RE.test(subject)) return null;
|
|
2749
|
+
return { subject, verb };
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2523
2752
|
/** The "every X is a Y" rewrite of a declarative, for the "did you mean …"
|
|
2524
2753
|
* hint. Real a/an agreement (never a hardcoded "a", which is ungrammatical
|
|
2525
2754
|
* for a vowel-initial Y — "every monkey is a animal") reuses finish.mjs's
|
|
@@ -2536,6 +2765,21 @@ function teachSuggestion(payload) {
|
|
|
2536
2765
|
return `every ${subject} is ${article} ${object}`;
|
|
2537
2766
|
}
|
|
2538
2767
|
|
|
2768
|
+
/** The honest decline for a bare habitual teach ("penguins swim") whose
|
|
2769
|
+
* subject is grounded nowhere — neither the static lexicon nor a prior
|
|
2770
|
+
* taught fact. Mirrors ungroundedPairHint's "name the gap, hand over a
|
|
2771
|
+
* phrasing that actually works, never guess" discipline for the capability
|
|
2772
|
+
* shape, which has no is/are payload for that hint to match. The suggested
|
|
2773
|
+
* grounding sentence uses the same GENERIC_ANCHOR_NOUNS root that hint
|
|
2774
|
+
* suggests, so it round-trips through the ordinary teach cascade as-is. */
|
|
2775
|
+
function habitualGroundingHintText(line, habitual) {
|
|
2776
|
+
const articleRule = grammarRules().find((r) => r.kind === "article");
|
|
2777
|
+
const article = articleRule && beginsWithVowelSound(habitual.subject, articleRule) ? "an" : "a";
|
|
2778
|
+
return `I don't know "${habitual.subject}" yet, so I can't store "${line}" as a capability fact. `
|
|
2779
|
+
+ `Ground it first — say "every ${habitual.subject} is a thing" — then say "${line}" again `
|
|
2780
|
+
+ `and I'll remember that ${article} ${habitual.subject} can ${habitual.verb}.`;
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2539
2783
|
/** PRONOUN-SUBJECT GUARD: "remember you are a womble" and the literal "every
|
|
2540
2784
|
* you is a womble" would otherwise reach teachSuggestion/
|
|
2541
2785
|
* unknownSubjectFallback treating "you" like an ordinary unknown common
|
|
@@ -2640,6 +2884,70 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2640
2884
|
const raw = stripKindOf(stripYour(stripPossessiveNamedInstance(rawInput)));
|
|
2641
2885
|
const wrapped = stripKindOf(stripYour(stripPossessiveNamedInstance(wrappedInput)));
|
|
2642
2886
|
|
|
2887
|
+
// CONJUNCTION PRE-PASS — "ahab is male and is the father of john": two
|
|
2888
|
+
// facts about ONE subject stated in one sentence. Split at the top-level
|
|
2889
|
+
// " and <is|are|has|have|can>" seam, re-attach the shared subject to the
|
|
2890
|
+
// second half, and run each half through this same lane in order — two
|
|
2891
|
+
// ordinary teach payloads, no new storage shape. A second clause that
|
|
2892
|
+
// names its OWN subject ("… and the weather is nice") is not a shared-
|
|
2893
|
+
// subject conjunction: the first half still stores, and the reply names
|
|
2894
|
+
// the clause it left alone — never a silent partial store either way.
|
|
2895
|
+
const conjSrc = (wrapped ?? raw).replace(/[.!?]+\s*$/, "");
|
|
2896
|
+
if (memoryDir && !QUESTION_LEAD_RE.test(conjSrc) && /\s+and\s+/i.test(conjSrc)
|
|
2897
|
+
&& !(await hasMidSentenceInterrogative(conjSrc))) {
|
|
2898
|
+
const rewrap = (half) => (wrapped != null ? `remember that ${half}` : half);
|
|
2899
|
+
const recurse = (half) => teachLane(rewrap(half), { memoryDir, sessionId, lexicon, cache });
|
|
2900
|
+
const stripNoted = (t) => String(t).replace(/^noted — remembered(?:\s+\d+\s+facts?)?:\s*/i, "").trim();
|
|
2901
|
+
const shared = conjSrc.match(/^(.+?)\s+and\s+((?:is|are|has|have|can)\b.+)$/i);
|
|
2902
|
+
const sharedSubject = shared ? shared[1].match(/^(.+?)\s+(?:is|are|has|have|can)\b/i)?.[1]?.trim() : null;
|
|
2903
|
+
if (shared && sharedSubject) {
|
|
2904
|
+
const firstHalf = shared[1].trim();
|
|
2905
|
+
const secondHalf = `${sharedSubject} ${shared[2].trim()}`;
|
|
2906
|
+
const first = await recurse(firstHalf);
|
|
2907
|
+
const second = await recurse(secondHalf);
|
|
2908
|
+
const firstOk = !!first && !first.miss;
|
|
2909
|
+
const secondOk = !!second && !second.miss;
|
|
2910
|
+
if (firstOk && secondOk) {
|
|
2911
|
+
return {
|
|
2912
|
+
text: `noted — remembered both: ${stripNoted(first.text)}; and ${stripNoted(second.text)}`,
|
|
2913
|
+
via: "assert", miss: false,
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
if (firstOk || secondOk) {
|
|
2917
|
+
const ok = firstOk ? first : second;
|
|
2918
|
+
const badHalf = firstOk ? secondHalf : firstHalf;
|
|
2919
|
+
const bad = firstOk ? second : first;
|
|
2920
|
+
return {
|
|
2921
|
+
text: `noted — remembered: ${stripNoted(ok.text)}. The other half ("${badHalf}") I couldn't store`
|
|
2922
|
+
+ `${bad ? ` — ${stripNoted(bad.text)}` : ", it isn't a fact shape I recognize."}`,
|
|
2923
|
+
via: "assert", miss: false,
|
|
2924
|
+
};
|
|
2925
|
+
}
|
|
2926
|
+
if (first || second) {
|
|
2927
|
+
return {
|
|
2928
|
+
text: `I couldn't store either half of that. "${firstHalf}": ${first ? stripNoted(first.text) : "not a fact shape I recognize."} `
|
|
2929
|
+
+ `"${secondHalf}": ${second ? stripNoted(second.text) : "not a fact shape I recognize."}`,
|
|
2930
|
+
via: "teach-miss", miss: true,
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2933
|
+
// neither half even recognized — fall through to the ordinary cascade
|
|
2934
|
+
} else if (!shared) {
|
|
2935
|
+
const ownSubject = conjSrc.match(
|
|
2936
|
+
/^(.+?\s+(?:is|are|has|have|can)\s+.+?)\s+and\s+((?:(?:the|a|an|every|each|all|some|my|your|their|his|her|its)\s+)?[\w'-]+(?:\s+[\w'-]+)?\s+(?:is|are|has|have|can)\b.+)$/i,
|
|
2937
|
+
);
|
|
2938
|
+
if (ownSubject) {
|
|
2939
|
+
const first = await recurse(ownSubject[1].trim());
|
|
2940
|
+
if (first && !first.miss) {
|
|
2941
|
+
return {
|
|
2942
|
+
text: `${first.text} — the second part ("${ownSubject[2].trim()}") names its own subject, so I didn't store it; teach it as its own sentence if you meant it.`,
|
|
2943
|
+
via: "assert", miss: false,
|
|
2944
|
+
};
|
|
2945
|
+
}
|
|
2946
|
+
// the first half didn't store — fall through to the ordinary cascade
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2643
2951
|
// PRONOUN-SUBJECT GUARD — tried against BOTH surfaces (bare and remember-
|
|
2644
2952
|
// wrapped; trailing punctuation stripped the same way the OWNS/SOME_A_FEW
|
|
2645
2953
|
// lanes below do) before anything else in this function, so a pronoun
|
|
@@ -2776,6 +3084,40 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2776
3084
|
if (stored) return stored;
|
|
2777
3085
|
}
|
|
2778
3086
|
|
|
3087
|
+
// GENITIVE RELATIONAL FACT — "ahab is john's father" / "john's father is
|
|
3088
|
+
// ahab": the two possessive surfaces of the relational fact just above,
|
|
3089
|
+
// stored through the SAME predicate mint so every read-back ("who is the
|
|
3090
|
+
// father of john") answers all three phrasings identically. Same gating.
|
|
3091
|
+
const genitive = ownSrc.match(GENITIVE_RELATION_TEACH_RE);
|
|
3092
|
+
if (genitive && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
3093
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
3094
|
+
subject: genitive[1], predicate: await generalVerbPredicate(genitive[3]), object: genitive[2],
|
|
3095
|
+
});
|
|
3096
|
+
if (stored) return stored;
|
|
3097
|
+
}
|
|
3098
|
+
const genitiveRev = ownSrc.match(GENITIVE_RELATION_TEACH_REV_RE);
|
|
3099
|
+
if (genitiveRev && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
3100
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
3101
|
+
subject: genitiveRev[3], predicate: await generalVerbPredicate(genitiveRev[2]), object: genitiveRev[1],
|
|
3102
|
+
});
|
|
3103
|
+
if (stored) return stored;
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
// VERB-INFLECTED RELATIONAL FACT — "ahab fathered john": the past-tense
|
|
3107
|
+
// verb surface of the relational fact above, minted through the SAME
|
|
3108
|
+
// generalVerbPredicate so "who is the father of john" reads every phrasing
|
|
3109
|
+
// back identically. matchRelationalVerbTeach carries the closed guards
|
|
3110
|
+
// (name-shaped sides, POS-confirmed nouns, a lemma-confirmed inflected
|
|
3111
|
+
// past) that keep "the build failed" an honest non-match.
|
|
3112
|
+
const relVerb = memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion
|
|
3113
|
+
? await matchRelationalVerbTeach(ownSrc) : null;
|
|
3114
|
+
if (relVerb) {
|
|
3115
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
3116
|
+
subject: relVerb.subject, predicate: await generalVerbPredicate(relVerb.base), object: relVerb.object,
|
|
3117
|
+
});
|
|
3118
|
+
if (stored) return stored;
|
|
3119
|
+
}
|
|
3120
|
+
|
|
2779
3121
|
// HAS-A-METHOD TEACH — "every/a/an/the <N1> has a/an <N2> method": a
|
|
2780
3122
|
// possession-of-capability claim, stored as an ordinary Fact via the SAME
|
|
2781
3123
|
// HAS_A_PREDICATE generalVerbTeach's own
|
|
@@ -2875,7 +3217,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2875
3217
|
} catch { /* malformed slots — fall through to the ordinary honest-miss cascade */ }
|
|
2876
3218
|
}
|
|
2877
3219
|
|
|
2878
|
-
// ACTION-RULE TEACH — the
|
|
3220
|
+
// ACTION-RULE TEACH — the five action frames plus the render binding (see
|
|
2879
3221
|
// the ACTION_*_TEACH_RE docblock). Each sentence stores its own Rule
|
|
2880
3222
|
// individual under a shared "<verb> <prep>" name. A role word that names
|
|
2881
3223
|
// neither the taught subject class nor the literal "target" is an honest
|
|
@@ -2911,6 +3253,32 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2911
3253
|
} catch { /* malformed slots — fall through to the ordinary honest-miss cascade */ }
|
|
2912
3254
|
}
|
|
2913
3255
|
|
|
3256
|
+
const actionSigPassive = ownSrc.match(ACTION_SIGNATURE_PASSIVE_RE);
|
|
3257
|
+
if (actionSigPassive && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
3258
|
+
const participle = actionSigPassive[2].toLowerCase();
|
|
3259
|
+
const verb = await actionLemma(participle);
|
|
3260
|
+
// Same honesty rule as the effect frame's gerund: an unreduced participle
|
|
3261
|
+
// would mint a name no other rule sentence can share.
|
|
3262
|
+
if (verb !== participle && participle.startsWith(verb.slice(0, Math.min(3, verb.length)))) {
|
|
3263
|
+
try {
|
|
3264
|
+
const prep = actionSigPassive[3].toLowerCase();
|
|
3265
|
+
const { appendRule, RULE_KIND_ACTION_SIGNATURE } = await import("./memory/core.mjs");
|
|
3266
|
+
const { id } = await appendRule(memoryDir, {
|
|
3267
|
+
name: `${verb} ${prep}`,
|
|
3268
|
+
kind: RULE_KIND_ACTION_SIGNATURE,
|
|
3269
|
+
slots: { subjectClass: actionSigPassive[1], targetClass: actionSigPassive[4] },
|
|
3270
|
+
provenance: teachProvenanceTag(sessionId, new Date().toISOString()),
|
|
3271
|
+
});
|
|
3272
|
+
if (id) {
|
|
3273
|
+
return {
|
|
3274
|
+
text: `noted — remembered: you can ${verb} a ${actionSigPassive[1].toLowerCase()} ${prep} a ${actionSigPassive[4].toLowerCase()}`,
|
|
3275
|
+
via: "assert", miss: false,
|
|
3276
|
+
};
|
|
3277
|
+
}
|
|
3278
|
+
} catch { /* malformed slots — fall through to the ordinary honest-miss cascade */ }
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
|
|
2914
3282
|
const precondNothing = ownSrc.match(ACTION_PRECOND_NOTHING_RE);
|
|
2915
3283
|
if (precondNothing && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
2916
3284
|
const role = actionRoleFor(precondNothing[7], precondNothing[2]);
|
|
@@ -2984,6 +3352,31 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2984
3352
|
} catch { /* malformed slots — fall through to the ordinary honest-miss cascade */ }
|
|
2985
3353
|
}
|
|
2986
3354
|
|
|
3355
|
+
const actionConstraint = ownSrc.match(ACTION_CONSTRAINT_TEACH_RE);
|
|
3356
|
+
if (actionConstraint && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
3357
|
+
try {
|
|
3358
|
+
const verb = await actionLemma(actionConstraint[1]);
|
|
3359
|
+
const prep = actionConstraint[3].toLowerCase();
|
|
3360
|
+
const { appendRule, RULE_KIND_ACTION_CONSTRAINT } = await import("./memory/core.mjs");
|
|
3361
|
+
const { id } = await appendRule(memoryDir, {
|
|
3362
|
+
name: `${verb} ${prep}`,
|
|
3363
|
+
kind: RULE_KIND_ACTION_CONSTRAINT,
|
|
3364
|
+
slots: {
|
|
3365
|
+
left: actionConstraint[5].toLowerCase(),
|
|
3366
|
+
right: actionConstraint[6].toLowerCase(),
|
|
3367
|
+
guard: actionConstraint[7].toLowerCase(),
|
|
3368
|
+
},
|
|
3369
|
+
provenance: teachProvenanceTag(sessionId, new Date().toISOString()),
|
|
3370
|
+
});
|
|
3371
|
+
if (id) {
|
|
3372
|
+
return {
|
|
3373
|
+
text: `noted — remembered: to ${verb} ${prep}, the ${actionConstraint[5].toLowerCase()} may not be with the ${actionConstraint[6].toLowerCase()} without the ${actionConstraint[7].toLowerCase()}`,
|
|
3374
|
+
via: "assert", miss: false,
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
} catch { /* malformed slots — fall through to the ordinary honest-miss cascade */ }
|
|
3378
|
+
}
|
|
3379
|
+
|
|
2987
3380
|
const actionEffect = ownSrc.match(ACTION_EFFECT_TEACH_RE);
|
|
2988
3381
|
if (actionEffect && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
2989
3382
|
const gerund = actionEffect[1].toLowerCase();
|
|
@@ -2997,11 +3390,22 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2997
3390
|
via: "teach-miss", miss: true,
|
|
2998
3391
|
};
|
|
2999
3392
|
}
|
|
3000
|
-
|
|
3393
|
+
// "makes IT rest on the target" leaves the role capture empty — the
|
|
3394
|
+
// pronoun can only mean the thing being moved, so it reads as the
|
|
3395
|
+
// subject-class word.
|
|
3396
|
+
const subjectWord = actionEffect[5] ?? actionEffect[2];
|
|
3397
|
+
const namedSubjectRole = actionRoleFor(subjectWord, actionEffect[2]);
|
|
3398
|
+
// A subject word naming neither the subject class nor "target" is
|
|
3399
|
+
// CLASS-BOUND: a companion that travels with every move ("ferrying a
|
|
3400
|
+
// passenger onto a bank makes the FARMER stand on the target"). Stored as
|
|
3401
|
+
// the bare class word; compileDomain (src/domain.mjs) requires the class
|
|
3402
|
+
// to have exactly one member at plan time, so a typo'd word fails loudly
|
|
3403
|
+
// there rather than silently minting a role here.
|
|
3404
|
+
const subjectRole = namedSubjectRole ?? subjectWord.toLowerCase();
|
|
3001
3405
|
const objectRole = actionRoleFor(actionEffect[8], actionEffect[2]);
|
|
3002
|
-
if (!
|
|
3406
|
+
if (!objectRole || subjectRole === objectRole) {
|
|
3003
3407
|
return {
|
|
3004
|
-
text: `I can't place "${
|
|
3408
|
+
text: `I can't place "${actionEffect[8]}" in that rule — the effect must end at the ${actionEffect[2]} or the target (e.g. "makes the ${actionEffect[2]} rest on the target").`,
|
|
3005
3409
|
via: "teach-miss", miss: true,
|
|
3006
3410
|
};
|
|
3007
3411
|
}
|
|
@@ -3021,7 +3425,8 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3021
3425
|
});
|
|
3022
3426
|
if (id) {
|
|
3023
3427
|
return {
|
|
3024
|
-
text: `noted — remembered: ${gerund} a ${actionEffect[2].toLowerCase()} ${prep} a ${actionEffect[4].toLowerCase()} makes the ${
|
|
3428
|
+
text: `noted — remembered: ${gerund} a ${actionEffect[2].toLowerCase()} ${prep} a ${actionEffect[4].toLowerCase()} makes the ${subjectWord.toLowerCase()} ${actionEffect[6].toLowerCase()} ${actionEffect[7].toLowerCase()} the ${actionEffect[8].toLowerCase()}`
|
|
3429
|
+
+ (namedSubjectRole ? "" : ` (the ${subjectRole} rides along on every ${verb} move — its class must have exactly one member when we plan)`),
|
|
3025
3430
|
via: "assert", miss: false,
|
|
3026
3431
|
};
|
|
3027
3432
|
}
|
|
@@ -3125,6 +3530,24 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3125
3530
|
// the structural grammar's own typo-tolerant retry to answer for real.
|
|
3126
3531
|
const subjectWord = raw.match(/^([\w'-]+)/)?.[1];
|
|
3127
3532
|
if (subjectWord && (await subjectIsNounOrPropn(subjectWord))) {
|
|
3533
|
+
// A PLURAL explicit-capability surface ("wrens can hum") whose
|
|
3534
|
+
// SINGULAR is a grounded term stores under the singular first — the
|
|
3535
|
+
// spelling the grounding fact and every query-side variant fold use —
|
|
3536
|
+
// instead of letting the general-verb mint below reify the plural
|
|
3537
|
+
// verbatim (a fact "can a wren hum" could never read back). An
|
|
3538
|
+
// ungrounded singular falls through unchanged.
|
|
3539
|
+
const canShape = matchBareCanTeach(raw);
|
|
3540
|
+
const canSingular = canShape ? singularizeSurface(canShape.subject) : null;
|
|
3541
|
+
if (canShape && canSingular !== canShape.subject) {
|
|
3542
|
+
let canLex = lexicon;
|
|
3543
|
+
if (!canLex) { const { loadLexicon } = await import("./grammar/lexicon.mjs"); canLex = loadLexicon(); }
|
|
3544
|
+
if (await isGroundedTerm(canSingular, canLex, memoryDir, cache)) {
|
|
3545
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
3546
|
+
subject: canSingular, predicate: await generalVerbPredicate("can"), object: canShape.verb,
|
|
3547
|
+
});
|
|
3548
|
+
if (stored) return stored;
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3128
3551
|
const gv = await generalVerbTeach(raw);
|
|
3129
3552
|
if (gv) {
|
|
3130
3553
|
const stored = await teachFact(memoryDir, sessionId, gv);
|
|
@@ -3135,7 +3558,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3135
3558
|
|
|
3136
3559
|
let payload = null;
|
|
3137
3560
|
if (wrapped && /\b(?:is|are)\b/i.test(wrapped)) payload = wrapped;
|
|
3138
|
-
else if ((BARE_DECLARATIVE_RE.test(raw) || COMPARATIVE_TEACH_RE.test(raw)) && !QUESTION_LEAD_RE.test(raw) && !(await hasMidSentenceInterrogative(raw))) payload = raw;
|
|
3561
|
+
else if ((BARE_DECLARATIVE_RE.test(raw) || COMPARATIVE_TEACH_RE.test(raw) || matchBareHabitualTeach(raw) || matchBareCanTeach(raw)) && !QUESTION_LEAD_RE.test(raw) && !(await hasMidSentenceInterrogative(raw))) payload = raw;
|
|
3139
3562
|
if (!payload) {
|
|
3140
3563
|
// "remember margo eats ribs", re-escaping here through a combination
|
|
3141
3564
|
// that mechanism's own deliberate subject-shape restriction doesn't
|
|
@@ -3174,6 +3597,31 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3174
3597
|
const stored = await assertTurn(cand, { memoryDir, sessionId, focus: null, lexicon, cache });
|
|
3175
3598
|
if (stored) return { text: stored.answer, via: "assert", miss: false };
|
|
3176
3599
|
}
|
|
3600
|
+
// CAPABILITY over a GROUNDED subject — "penguins swim" (habitual) or "a
|
|
3601
|
+
// penguin can swim" (explicit) after "every penguin is a thing". The ACE
|
|
3602
|
+
// candidates above only parse closed-lexicon words, so a subject grounded
|
|
3603
|
+
// by a PRIOR taught fact (or an anchor root) still fell through to the
|
|
3604
|
+
// generic decline. Same closed shapes, same capability predicate the ACE
|
|
3605
|
+
// path itself stores. The subject's naive singular is tried too, so the
|
|
3606
|
+
// explicit plural surface ("penguins can swim") reaches the same stored
|
|
3607
|
+
// spelling the grounding fact used.
|
|
3608
|
+
const habitualTeach = matchBareHabitualTeach(payload) || matchBareCanTeach(payload);
|
|
3609
|
+
if (habitualTeach) {
|
|
3610
|
+
let habLex = lexicon;
|
|
3611
|
+
if (!habLex) { const { loadLexicon } = await import("./grammar/lexicon.mjs"); habLex = loadLexicon(); }
|
|
3612
|
+
// The singular is preferred so an explicit plural surface ("penguins
|
|
3613
|
+
// can swim") stores under the same spelling the grounding fact (and
|
|
3614
|
+
// every query-side variant fold) uses; a proper noun that only looks
|
|
3615
|
+
// plural ("redis") falls back to its own spelling.
|
|
3616
|
+
for (const subj of new Set([singularizeSurface(habitualTeach.subject), habitualTeach.subject])) {
|
|
3617
|
+
if (await isGroundedTerm(subj, habLex, memoryDir, cache)) {
|
|
3618
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
3619
|
+
subject: subj, predicate: await generalVerbPredicate("can"), object: habitualTeach.verb,
|
|
3620
|
+
});
|
|
3621
|
+
if (stored) return stored;
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3177
3625
|
// The real ACE grammar just declined (unknown words / not the membership
|
|
3178
3626
|
// shape) — try the narrow unknown-SUBJECT direct-write fallback before
|
|
3179
3627
|
// falling to the honest-miss cascade. Covers BOTH the bare and the
|
|
@@ -4347,6 +4795,19 @@ const RELATION_FACT_YESNO_RE =
|
|
|
4347
4795
|
const RELATION_WHO_ASK_RE =
|
|
4348
4796
|
/^(?:who|what)\s+(?:is|are)\s+(?:the|an?)\s+([a-z][\w-]*)\s+of\s+([\w'-]+(?:\s+[A-Z][\w'-]*)?)[?.!\s]*$/i;
|
|
4349
4797
|
|
|
4798
|
+
/** "who is john's father" — the GENITIVE surface of RELATION_WHO_ASK_RE.
|
|
4799
|
+
* A pure rewrite onto that shape (the matchWhyIsa approach): returns a
|
|
4800
|
+
* match-shaped array with the same slot order RELATION_WHO_ASK_RE produces
|
|
4801
|
+
* ([1]=relation, [2]=object), so the dispatch block below serves both
|
|
4802
|
+
* surfaces with no second lane. The possessive token excludes apostrophes
|
|
4803
|
+
* ([\w-]+) so the 's split is unambiguous. */
|
|
4804
|
+
const GENITIVE_WHO_ASK_RE =
|
|
4805
|
+
/^(?:who|what)\s+(?:is|are|was|were)\s+([\w-]+(?:\s+[A-Z][\w-]*)?)'s\s+([a-z][\w-]*)[?.!\s]*$/i;
|
|
4806
|
+
function matchGenitiveWhoAsk(q) {
|
|
4807
|
+
const g = String(q).match(GENITIVE_WHO_ASK_RE);
|
|
4808
|
+
return g ? [g[0], g[2], g[1]] : null;
|
|
4809
|
+
}
|
|
4810
|
+
|
|
4350
4811
|
/** "list the descendants of ahab" — the REACHABILITY-SET list query: a
|
|
4351
4812
|
* genuine KIND-CHANGE from RELATION_FACT_YESNO_RE just above — every entity
|
|
4352
4813
|
* reachable from the named start entity through a taught `recursive` Rule,
|
|
@@ -4443,6 +4904,66 @@ const WHAT_HAS_RE = /^what\s+has\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
|
4443
4904
|
// — actively misleading for a pure vocabulary query.
|
|
4444
4905
|
const WHAT_USED_FOR_RE = /^what\s+(?:(?:can\s+be|is)\s+used\s+for|is\s+for)\s+(.+?)[?.!\s]*$/i;
|
|
4445
4906
|
|
|
4907
|
+
/** "where is disk-1[ now]" — the bare where question about a TAUGHT individual.
|
|
4908
|
+
* The term capture is lazy so an optional trailing "now" stays out of it; any
|
|
4909
|
+
* other tail ("where is X defined") lands in the capture, finds no locative
|
|
4910
|
+
* fact subject named that, and falls through to the code-graph where lane
|
|
4911
|
+
* unchanged. Consumed by factAnswer's (a-pre4) reader. */
|
|
4912
|
+
const WHERE_IS_FACT_RE = /^where(?:'s|\s+is|\s+are)\s+(.+?)(?:\s+now)?\s*[?.!]*$/i;
|
|
4913
|
+
/** The closed locative tail of a folded prepositional-verb predicate
|
|
4914
|
+
* (mgx:rest-on, mgx:stand-on, mgx:sit-in, …) — what makes a taught fact a
|
|
4915
|
+
* LOCATION answer rather than any arbitrary relation. */
|
|
4916
|
+
const LOCATIVE_FACT_PREDICATE_RE = /^mgx:[a-z]+-(?:on|in|at|inside|under|below|above|near|beside|behind|by)$/;
|
|
4917
|
+
|
|
4918
|
+
// CAN_ASK_RE's remaining paraphrase-ladder siblings, all over the same
|
|
4919
|
+
// mgx:capableOf facts:
|
|
4920
|
+
// - DO_VERB_ASK_RE: the do-support yes/no ("do birds fly", "does a dog
|
|
4921
|
+
// bark") plus its quantified form ("do all birds fly" — answered
|
|
4922
|
+
// generically, never universally: the facts are generic, and claiming
|
|
4923
|
+
// "all" from them would overclaim). Requires a SINGLE trailing verb
|
|
4924
|
+
// word, so it stays disjoint from DOES_HAVE_ASK_RE (" have " in the
|
|
4925
|
+
// middle) and the derived FORWARD_YESNO_MARKERS readers (verb phrase
|
|
4926
|
+
// + object after it).
|
|
4927
|
+
// - WHAT_CAN_VERB_RE: the reverse-by-verb open list ("what can fly") —
|
|
4928
|
+
// the capability mirror of WHAT_USED_FOR_RE just above. "be …" tails
|
|
4929
|
+
// are excluded (WHAT_USED_FOR_RE's own "what can be used for" lead);
|
|
4930
|
+
// "… do" tails belong to WHAT_CAN_DO_RE and are guarded at the call
|
|
4931
|
+
// site.
|
|
4932
|
+
// - WHICH_KIND_CAN_RE: the kind-restricted form ("which animals can
|
|
4933
|
+
// fly") — reverse-by-verb filtered to subjects the memory can tie to
|
|
4934
|
+
// the named kind via a direct isa-family fact.
|
|
4935
|
+
const DO_VERB_ASK_RE = /^(?:do|does)\s+(all\s+|every\s+)?(?:an?\s+|the\s+)?([\w'-]+(?:\s+[\w'-]+)*?)\s+([a-z-]+)[?.!\s]*$/i;
|
|
4936
|
+
const WHAT_CAN_VERB_RE = /^what\s+can\s+(?!be\s)(.+?)[?.!\s]*$/i;
|
|
4937
|
+
const WHICH_KIND_CAN_RE = /^(?:which|what)\s+([\w'-]+(?:\s+[\w'-]+)*?)\s+can\s+(.+?)[?.!\s]*$/i;
|
|
4938
|
+
|
|
4939
|
+
/** SUPERLATIVE over TAUGHT COMPARATIVES — "which disk is smallest" / "what is
|
|
4940
|
+
* the smallest disk" answered from the mgx:<comparative>-than facts the
|
|
4941
|
+
* comparative teach frame mints ("disk-1 is smaller than disk-2"). The
|
|
4942
|
+
* superlative slot is closed by SHAPE, the same discipline as
|
|
4943
|
+
* COMPARATIVE_SRC: an -est word, best/worst, or a most/least + adjective
|
|
4944
|
+
* pair — never a hand-list of adjectives. Entirely fact-side: the
|
|
4945
|
+
* code-graph superlative lane (parseSuperlative's entity-kind metrics) is a
|
|
4946
|
+
* different question over different data and is untouched — this reader
|
|
4947
|
+
* only ever answers when taught comparative pairs for the named kind exist. */
|
|
4948
|
+
const SUPERLATIVE_WORD_SRC = "(?:most|least)\\s+[a-z][\\w-]*|[a-z][\\w-]*est|best|worst";
|
|
4949
|
+
const WHICH_KIND_SUPERLATIVE_RE = new RegExp(`^which\\s+([\\w'-]+)\\s+(?:is|are)\\s+(?:the\\s+)?(${SUPERLATIVE_WORD_SRC})[?.!\\s]*$`, "i");
|
|
4950
|
+
const WHAT_IS_SUPERLATIVE_KIND_RE = new RegExp(`^what(?:'s|s|\\s+is)\\s+the\\s+(${SUPERLATIVE_WORD_SRC})\\s+([\\w'-]+)[?.!\\s]*$`, "i");
|
|
4951
|
+
|
|
4952
|
+
/** Map a superlative surface onto the comparative base its taught facts were
|
|
4953
|
+
* minted under: <adj>est → <adj>er (the shared stem keeps a doubled
|
|
4954
|
+
* consonant intact: biggest → bigger), best → better, worst → worse,
|
|
4955
|
+
* "most X" → "more X", "least X" → "less X". Returns null for a word that
|
|
4956
|
+
* only LOOKS superlative ("honest" maps to no comparative anyone teaches —
|
|
4957
|
+
* the resulting predicate simply never has facts). */
|
|
4958
|
+
function comparativeOfSuperlative(superlative) {
|
|
4959
|
+
const s = String(superlative || "").toLowerCase().trim().replace(/\s+/g, " ");
|
|
4960
|
+
if (s === "best") return "better";
|
|
4961
|
+
if (s === "worst") return "worse";
|
|
4962
|
+
const graded = s.match(/^(most|least)\s+([a-z][\w-]*)$/);
|
|
4963
|
+
if (graded) return `${graded[1] === "most" ? "more" : "less"} ${graded[2]}`;
|
|
4964
|
+
return /[a-z]est$/.test(s) && s.length > 4 ? `${s.slice(0, -3)}er` : null;
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4446
4967
|
// The SAME gap as mgx:usedFor above is systemic — "what causes fire", "what is
|
|
4447
4968
|
// made of wood" would otherwise fall through to the same misleading
|
|
4448
4969
|
// code-graph miss. DERIVES a reverse-by-object regex for every
|
|
@@ -4542,8 +5063,41 @@ function uniqueFacts(rows) {
|
|
|
4542
5063
|
* (via factRows/memoryFacts below), and loadMemory's own Backend-B branch
|
|
4543
5064
|
* returns the handle's `payload` directly with ZERO fs calls — so a caller
|
|
4544
5065
|
* that hands this a handle already carrying the embedded page's full graph
|
|
4545
|
-
* gets a pure, disk-free traversal, no bundle-time module shimming needed.
|
|
5066
|
+
* gets a pure, disk-free traversal, no bundle-time module shimming needed.
|
|
5067
|
+
* Every return additionally carries the additive `goal` field when one is
|
|
5068
|
+
* deducible (withDeducedGoal, below) — the ledger page's chat dock renders
|
|
5069
|
+
* it as its own "Goal (inferred)" line; every other consumer reads named
|
|
5070
|
+
* fields and is unaffected. */
|
|
4546
5071
|
export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle = {}, cache = null) {
|
|
5072
|
+
return withDeducedGoal(await factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle, cache), envelope, query);
|
|
5073
|
+
}
|
|
5074
|
+
|
|
5075
|
+
/** Attach the additive `goal` field to a fact reader's return: the same
|
|
5076
|
+
* table-driven deduction runAsk applies (deduceGoalFromParsed over the parsed
|
|
5077
|
+
* AST, plus the general-verb revision), extended to the bare-question shapes
|
|
5078
|
+
* the readers recognize with no envelope at all — the ledger page's chat dock
|
|
5079
|
+
* calls them with `envelope: null`, so there is no AST to deduce from.
|
|
5080
|
+
* Existing phrasing only, never free text; a goal-less shape passes through
|
|
5081
|
+
* without the field, so the dock (like chat) renders no line for it. */
|
|
5082
|
+
function withDeducedGoal(res, envelope, query) {
|
|
5083
|
+
if (!res || res.goal !== undefined) return res;
|
|
5084
|
+
const q = String(query || "").trim();
|
|
5085
|
+
let goal = deduceGoalFromParsed(envelope?.parsed);
|
|
5086
|
+
if (!goal && res.generalVerbQuery) goal = TAUGHT_FACT_LOOKUP_GOAL;
|
|
5087
|
+
if (!goal) {
|
|
5088
|
+
const yesNo = q.match(RELATION_FACT_YESNO_RE);
|
|
5089
|
+
const whoAsk = yesNo ? null : (q.match(RELATION_WHO_ASK_RE) || matchGenitiveWhoAsk(q));
|
|
5090
|
+
const role = yesNo ? yesNo[2] : whoAsk ? whoAsk[1] : null;
|
|
5091
|
+
if (role && !ISA_IDIOM_ROLE_WORDS.has(role.toLowerCase())) goal = TAUGHT_FACT_LOOKUP_GOAL;
|
|
5092
|
+
}
|
|
5093
|
+
if (!goal) {
|
|
5094
|
+
const whatIs = q.match(BARE_WHATIS_RE);
|
|
5095
|
+
if (whatIs) goal = deduceGoalFromParsed({ shape: "meta", object: whatIs[1] });
|
|
5096
|
+
}
|
|
5097
|
+
return goal ? { ...res, goal } : res;
|
|
5098
|
+
}
|
|
5099
|
+
|
|
5100
|
+
async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle = {}, cache = null) {
|
|
4547
5101
|
let normFactTerm;
|
|
4548
5102
|
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
4549
5103
|
const q = String(query).trim();
|
|
@@ -4594,6 +5148,98 @@ export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle
|
|
|
4594
5148
|
return { text: shown.join("\n") + extra, replace: true, ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}) };
|
|
4595
5149
|
}
|
|
4596
5150
|
|
|
5151
|
+
// (a-pre3) SUPERLATIVE over TAUGHT COMPARATIVES — "which disk is smallest" /
|
|
5152
|
+
// "what is the smallest disk" resolved from mgx:<comparative>-than facts.
|
|
5153
|
+
// Checked BEFORE (a) for the same reason as (a-pre)/(a-pre2): the "what is
|
|
5154
|
+
// the …" surface would otherwise be swallowed as one literal meta term.
|
|
5155
|
+
// Answers ONLY when the taught pairs for the named kind form a single
|
|
5156
|
+
// unambiguous total chain; a partial order (two heads nothing compares) or
|
|
5157
|
+
// a contradiction loop is an honest can't-order decline that names the gap.
|
|
5158
|
+
// No taught pairs at all → falls through untouched, so the code-graph
|
|
5159
|
+
// superlative lane and the ordinary miss messaging keep their turns.
|
|
5160
|
+
const whichSup = q.match(WHICH_KIND_SUPERLATIVE_RE);
|
|
5161
|
+
const whatSup = whichSup ? null : q.match(WHAT_IS_SUPERLATIVE_KIND_RE);
|
|
5162
|
+
const supKindRaw = whichSup ? whichSup[1] : whatSup?.[2];
|
|
5163
|
+
const supWord = whichSup ? whichSup[2] : whatSup?.[1];
|
|
5164
|
+
const supCompBase = supKindRaw && supWord ? comparativeOfSuperlative(supWord) : null;
|
|
5165
|
+
if (supCompBase) {
|
|
5166
|
+
const supPredicate = `mgx:${supCompBase.replace(/\s+/g, "-")}-than`;
|
|
5167
|
+
const rows = await factRows(memoryDir, cache);
|
|
5168
|
+
const kindVariants = factTermVariants(normFactTerm, supKindRaw);
|
|
5169
|
+
const kindSingular = [...kindVariants].sort((a, b) => a.length - b.length)[0];
|
|
5170
|
+
const memberOfKind = (node) => kindVariants.has(node)
|
|
5171
|
+
|| node.startsWith(`${kindSingular}-`) || node.startsWith(`${kindSingular} `)
|
|
5172
|
+
|| rows.some((g) => ISA_PREDICATES.has(g.predicate) && g.subject === node && kindVariants.has(g.object));
|
|
5173
|
+
const pairs = uniqueFacts(rows.filter((f) => f.predicate === supPredicate && isOperatorTaught(f)))
|
|
5174
|
+
.filter((f) => f.subject !== f.object && memberOfKind(f.subject) && memberOfKind(f.object));
|
|
5175
|
+
if (pairs.length) {
|
|
5176
|
+
const nodes = new Set();
|
|
5177
|
+
const inDeg = new Map();
|
|
5178
|
+
for (const f of pairs) {
|
|
5179
|
+
nodes.add(f.subject); nodes.add(f.object);
|
|
5180
|
+
inDeg.set(f.object, (inDeg.get(f.object) || 0) + 1);
|
|
5181
|
+
if (!inDeg.has(f.subject)) inDeg.set(f.subject, inDeg.get(f.subject) || 0);
|
|
5182
|
+
}
|
|
5183
|
+
// A unique topological order IS the single unambiguous total chain:
|
|
5184
|
+
// exactly one zero-in-degree node must exist at every step, and each
|
|
5185
|
+
// step's winner is then directly compared to the next (a unique order
|
|
5186
|
+
// forces the consecutive edge). Two candidates at any step = a pair
|
|
5187
|
+
// nothing compares; no candidate = the taught facts loop.
|
|
5188
|
+
const remaining = new Map(inDeg);
|
|
5189
|
+
const order = [];
|
|
5190
|
+
let declined = null;
|
|
5191
|
+
while (remaining.size) {
|
|
5192
|
+
const sources = [...remaining.keys()].filter((n) => remaining.get(n) === 0);
|
|
5193
|
+
if (sources.length !== 1) {
|
|
5194
|
+
declined = sources.length === 0
|
|
5195
|
+
? {
|
|
5196
|
+
text: `I can't order the ${kindSingular}s — the "${supCompBase} than" facts I have loop back on themselves, so no ${supWord} exists. /memory to inspect them.`,
|
|
5197
|
+
replace: true, miss: true,
|
|
5198
|
+
}
|
|
5199
|
+
: {
|
|
5200
|
+
text: `I can't pick the ${supWord} ${kindSingular} from what I know — nothing compares ${sources[0]} and ${sources[1]}. Teach me, e.g. "${sources[0]} is ${supCompBase} than ${sources[1]}".`,
|
|
5201
|
+
replace: true, miss: true,
|
|
5202
|
+
};
|
|
5203
|
+
break;
|
|
5204
|
+
}
|
|
5205
|
+
const head = sources[0];
|
|
5206
|
+
order.push(head);
|
|
5207
|
+
remaining.delete(head);
|
|
5208
|
+
for (const f of pairs) {
|
|
5209
|
+
if (f.subject === head && remaining.has(f.object)) remaining.set(f.object, remaining.get(f.object) - 1);
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
if (declined) return declined;
|
|
5213
|
+
const steps = order.slice(0, -1).map((n, i) => pairs.find((f) => f.subject === n && f.object === order[i + 1]));
|
|
5214
|
+
if (steps.every(Boolean)) {
|
|
5215
|
+
const cite = steps.map((g) => `${factPhrase(g)}${g.provenance ? ` (source: ${g.provenance})` : ""}`).join("; ");
|
|
5216
|
+
return { text: `${order[0]} — ${cite}; so ${order[0]} is the ${supWord} ${kindSingular}`, replace: true };
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5219
|
+
}
|
|
5220
|
+
|
|
5221
|
+
// (a-pre4) "where is disk-1" over TAUGHT LOCATIVE FACTS — the where shape
|
|
5222
|
+
// belongs to the code graph (shape=where, "where is X defined"), so a taught
|
|
5223
|
+
// individual with a location fact ("disk-1 rests on peg-a") otherwise dies on
|
|
5224
|
+
// the "no module matching" miss. Miss-gated AND hit-gated: consulted only
|
|
5225
|
+
// after the code lane already missed, and takes over only when a locative
|
|
5226
|
+
// fact row for that exact subject exists — a real module answer, and every
|
|
5227
|
+
// no-fact miss, is untouched.
|
|
5228
|
+
const whereQ = miss ? q.match(WHERE_IS_FACT_RE) : null;
|
|
5229
|
+
if (whereQ) {
|
|
5230
|
+
const variants = factTermVariants(normFactTerm, whereQ[1]);
|
|
5231
|
+
const hits = (await factRows(memoryDir, cache))
|
|
5232
|
+
.filter((f) => LOCATIVE_FACT_PREDICATE_RE.test(f.predicate) && variants.has(f.subject));
|
|
5233
|
+
if (hits.length) {
|
|
5234
|
+
const ranked = rankByBiasThenTrust(uniqueFacts(hits), biasByBundle);
|
|
5235
|
+
const lines = ranked.map(renderFactLine);
|
|
5236
|
+
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
5237
|
+
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
5238
|
+
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
5239
|
+
return { text: shown.join("\n") + extra, replace: true, ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}) };
|
|
5240
|
+
}
|
|
5241
|
+
}
|
|
5242
|
+
|
|
4597
5243
|
// (a) meta-shaped questions ("what is a module", "what does cache mean") — the
|
|
4598
5244
|
// parsed object term, matched against fact SUBJECTS; consulted for hits (append
|
|
4599
5245
|
// alongside the schema-docs answer) and misses (facts answer alone) alike.
|
|
@@ -4622,6 +5268,17 @@ export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle
|
|
|
4622
5268
|
// tail, verbatim.
|
|
4623
5269
|
if (m) metaTerm = stripTrailingScopeFiller(m[1]);
|
|
4624
5270
|
}
|
|
5271
|
+
// An ambiguous parse tie ({ambiguousParse}) reaches this lane with
|
|
5272
|
+
// envelope.parsed nulled and miss=false, so NEITHER branch above arms —
|
|
5273
|
+
// but when one tied reading is META and memory holds facts for its term
|
|
5274
|
+
// ("what is a test drive": meta "test drive" vs tests "drive"), those
|
|
5275
|
+
// facts belong under the disambiguation. Without this, the meta branch's
|
|
5276
|
+
// graph-only "isn't a term in this graph's own vocabulary" line is the
|
|
5277
|
+
// last word on a term the user has explicitly taught.
|
|
5278
|
+
if (!metaTerm && envelope?.ambiguous && Array.isArray(envelope.candidateParses)) {
|
|
5279
|
+
const metaCand = envelope.candidateParses.find((c) => c?.shape === "meta" && c.object);
|
|
5280
|
+
if (metaCand) metaTerm = stripTrailingScopeFiller(String(metaCand.object));
|
|
5281
|
+
}
|
|
4625
5282
|
if (metaTerm) {
|
|
4626
5283
|
// "what is a tree used for" parses (grammar.mjs T5) to the
|
|
4627
5284
|
// WHOLE tail "tree used for" as one literal term — split off a trailing
|
|
@@ -4754,8 +5411,11 @@ export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle
|
|
|
4754
5411
|
const knownCan = facts.filter((f) => f.predicate === "mgx:capableOf" && subj.has(f.subject));
|
|
4755
5412
|
if (knownCan.length) {
|
|
4756
5413
|
const shown = knownCan.slice(0, 3).map(renderFactLine).join("; ");
|
|
5414
|
+
// The teach hint names the subject as the GRAPH stores it (singular),
|
|
5415
|
+
// not as the user typed it — 'teach me: "a birds can swim"' is a
|
|
5416
|
+
// garbled hint that can't round-trip.
|
|
4757
5417
|
return {
|
|
4758
|
-
text: `I can't confirm that — nothing I remember says ${can[1]} can ${can[2]}. I do know: ${shown}. If it's true, teach me: "a ${
|
|
5418
|
+
text: `I can't confirm that — nothing I remember says ${can[1]} can ${can[2]}. I do know: ${shown}. If it's true, teach me: "a ${knownCan[0].subject} can ${can[2]}".`,
|
|
4759
5419
|
replace: true,
|
|
4760
5420
|
miss: true,
|
|
4761
5421
|
};
|
|
@@ -4779,6 +5439,42 @@ export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle
|
|
|
4779
5439
|
return null;
|
|
4780
5440
|
}
|
|
4781
5441
|
|
|
5442
|
+
// (b2c) "do birds fly" — the do-support surface of (b2), same capableOf
|
|
5443
|
+
// lookup. The quantified form ("do all birds fly") is answered generically
|
|
5444
|
+
// and says so: the stored facts are generic, and a bare "yes" would claim
|
|
5445
|
+
// universality the memory can't support. NEVER returns null on a non-match
|
|
5446
|
+
// (falls through instead): the shape is looser than (b2)'s, so a do-lead
|
|
5447
|
+
// question some later reader owns must keep its turn. The can't-confirm
|
|
5448
|
+
// branch is additionally miss-gated for the same reason.
|
|
5449
|
+
const doAsk = q.match(DO_VERB_ASK_RE);
|
|
5450
|
+
if (doAsk) {
|
|
5451
|
+
const facts = await memoryFacts(memoryDir);
|
|
5452
|
+
const universal = !!doAsk[1];
|
|
5453
|
+
const subj = factTermVariants(normFactTerm, doAsk[2]);
|
|
5454
|
+
const obj = factTermVariants(normFactTerm, doAsk[3]);
|
|
5455
|
+
const hit = facts.find(
|
|
5456
|
+
(f) => f.predicate === "mgx:capableOf" && subj.has(f.subject) && obj.has(f.object),
|
|
5457
|
+
);
|
|
5458
|
+
if (hit && universal) {
|
|
5459
|
+
return {
|
|
5460
|
+
text: `I can't speak for all ${doAsk[2]} — what I remember is generic, not universal. I do know: ${renderFactLine(hit)}.`,
|
|
5461
|
+
replace: true,
|
|
5462
|
+
};
|
|
5463
|
+
}
|
|
5464
|
+
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
5465
|
+
if (miss) {
|
|
5466
|
+
const knownCan = facts.filter((f) => f.predicate === "mgx:capableOf" && subj.has(f.subject));
|
|
5467
|
+
if (knownCan.length) {
|
|
5468
|
+
const shown = knownCan.slice(0, 3).map(renderFactLine).join("; ");
|
|
5469
|
+
return {
|
|
5470
|
+
text: `I can't confirm that — nothing I remember says ${doAsk[2]} can ${doAsk[3]}. I do know: ${shown}. If it's true, teach me: "a ${knownCan[0].subject} can ${doAsk[3]}".`,
|
|
5471
|
+
replace: true,
|
|
5472
|
+
miss: true,
|
|
5473
|
+
};
|
|
5474
|
+
}
|
|
5475
|
+
}
|
|
5476
|
+
}
|
|
5477
|
+
|
|
4782
5478
|
// (b3) "what can a dog do" — every remembered mgx:capableOf fact for the
|
|
4783
5479
|
// subject, open-list. Reuses the meta-lane's subject-hits/rank/render/
|
|
4784
5480
|
// paginate recipe (lane (a) above) verbatim, with the predicate hardcoded.
|
|
@@ -4795,6 +5491,75 @@ export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle
|
|
|
4795
5491
|
return { text: shown.join("\n") + extra, replace: true, ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}) };
|
|
4796
5492
|
}
|
|
4797
5493
|
|
|
5494
|
+
// (b3b) "which animals can fly" — reverse-by-verb over mgx:capableOf,
|
|
5495
|
+
// restricted to subjects an isa-family chain ties to the named kind within
|
|
5496
|
+
// a bounded hop budget (findIsaChain, the same rooted proof search the
|
|
5497
|
+
// is-a ladder's live chase uses; maxHops matches its enlarged-tree budget),
|
|
5498
|
+
// so "every sparrow is a bird" + "bird is a kind of animal" surfaces
|
|
5499
|
+
// sparrow under "which animals…". A chain longer than one hop is cited on
|
|
5500
|
+
// the answer line. When capable subjects exist but NONE provably belongs
|
|
5501
|
+
// to the kind, the answer says so and still lists them — honest about the
|
|
5502
|
+
// missing link instead of a silent empty. Only takes over on real
|
|
5503
|
+
// capability hits; otherwise falls through (never returns null: "which X
|
|
5504
|
+
// can Y" phrasings this reader doesn't own must keep their turn).
|
|
5505
|
+
const whichCan = q.match(WHICH_KIND_CAN_RE);
|
|
5506
|
+
if (whichCan) {
|
|
5507
|
+
const kindVariants = factTermVariants(normFactTerm, whichCan[1]);
|
|
5508
|
+
const verbVariants = factTermVariants(normFactTerm, whichCan[2]);
|
|
5509
|
+
const facts = await factRows(memoryDir, cache);
|
|
5510
|
+
const capable = uniqueFacts(facts.filter((f) => f.predicate === "mgx:capableOf" && verbVariants.has(f.object)));
|
|
5511
|
+
if (capable.length) {
|
|
5512
|
+
const { findIsaChain, SUBCLASS_PREDICATE: SC_PRED, TYPE_PREDICATE: TYPE_PRED } = await import("./syllogise.mjs");
|
|
5513
|
+
const subClassRows = facts.filter((f) => f.predicate === SC_PRED);
|
|
5514
|
+
const typeRows = facts.filter((f) => f.predicate === TYPE_PRED);
|
|
5515
|
+
const subClassEdges = subClassRows.map((f) => [f.subject, f.object]);
|
|
5516
|
+
const typeEdges = typeRows.map((f) => [f.subject, f.object]);
|
|
5517
|
+
const rowForStep = (step) => (step.predicate === SC_PRED ? subClassRows : typeRows)
|
|
5518
|
+
.find((g) => g.subject === step.subject && g.object === step.object);
|
|
5519
|
+
const chainBySubject = new Map();
|
|
5520
|
+
const inKind = capable.filter((f) => {
|
|
5521
|
+
if (kindVariants.has(f.subject)) return true;
|
|
5522
|
+
if (!chainBySubject.has(f.subject)) {
|
|
5523
|
+
chainBySubject.set(f.subject, findIsaChain(f.subject, kindVariants, typeEdges, subClassEdges, { maxHops: 3 }));
|
|
5524
|
+
}
|
|
5525
|
+
return !!chainBySubject.get(f.subject);
|
|
5526
|
+
});
|
|
5527
|
+
const ranked = rankByBiasThenTrust(inKind.length ? inKind : capable, biasByBundle);
|
|
5528
|
+
const lines = ranked.map((f) => {
|
|
5529
|
+
const chain = inKind.length ? chainBySubject.get(f.subject) : null;
|
|
5530
|
+
if (!chain || chain.length < 2) return renderFactLine(f);
|
|
5531
|
+
const steps = chain.map(rowForStep);
|
|
5532
|
+
if (!steps.every(Boolean)) return renderFactLine(f);
|
|
5533
|
+
const cite = steps.map((g) => `${factPhrase(g)}${g.provenance ? ` (source: ${g.provenance})` : ""}`).join("; ");
|
|
5534
|
+
return `${renderFactLine(f)} — via: ${cite}`;
|
|
5535
|
+
});
|
|
5536
|
+
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
5537
|
+
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
5538
|
+
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
5539
|
+
const preamble = inKind.length ? "" : `nothing I remember ties these to "${whichCan[1]}", but:\n`;
|
|
5540
|
+
return { text: preamble + shown.join("\n") + extra, replace: true, ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}) };
|
|
5541
|
+
}
|
|
5542
|
+
}
|
|
5543
|
+
|
|
5544
|
+
// (b3c) "what can fly" — the unrestricted reverse-by-verb sibling of (b3b):
|
|
5545
|
+
// every capableOf fact whose OBJECT matches. The "… do" tail is (b3)'s
|
|
5546
|
+
// shape, guarded out so a zero-hit "what can a cat do" never gets misread
|
|
5547
|
+
// here as a hunt for the capability "a cat do". Same fall-through
|
|
5548
|
+
// discipline as (b3b).
|
|
5549
|
+
const canVerb = q.match(WHAT_CAN_VERB_RE);
|
|
5550
|
+
if (canVerb && canVerb[1].trim().split(/\s+/).at(-1)?.toLowerCase() !== "do") {
|
|
5551
|
+
const verbVariants = factTermVariants(normFactTerm, canVerb[1]);
|
|
5552
|
+
const hits = (await factRows(memoryDir, cache)).filter((f) => f.predicate === "mgx:capableOf" && verbVariants.has(f.object));
|
|
5553
|
+
if (hits.length) {
|
|
5554
|
+
const ranked = rankByBiasThenTrust(uniqueFacts(hits), biasByBundle);
|
|
5555
|
+
const lines = ranked.map(renderFactLine);
|
|
5556
|
+
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
5557
|
+
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
5558
|
+
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
5559
|
+
return { text: shown.join("\n") + extra, replace: true, ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}) };
|
|
5560
|
+
}
|
|
5561
|
+
}
|
|
5562
|
+
|
|
4798
5563
|
// (b4) "what has a wheel" — the REVERSE-by-OBJECT mirror of every other
|
|
4799
5564
|
// reader in this cascade: filters factRows on mgx:hasA where the OBJECT
|
|
4800
5565
|
// (not subject) matches, so every subject sharing that object surfaces
|
|
@@ -5280,8 +6045,14 @@ function inheritsChain(graph, startId) {
|
|
|
5280
6045
|
* (c) REVERSE membership — "what is a Y" reports Y's members (object-side), and
|
|
5281
6046
|
* "what kind of thing is an X" reports X's own type (subject-side first).
|
|
5282
6047
|
* Miss-only and run AFTER factAnswer returns null, so it never shadows the
|
|
5283
|
-
* subject-side answer or a schema hit. Returns { text, replace:true } or null
|
|
6048
|
+
* subject-side answer or a schema hit. Returns { text, replace:true } or null,
|
|
6049
|
+
* plus factAnswer's same additive `goal` field when one is deducible
|
|
6050
|
+
* (withDeducedGoal). */
|
|
5284
6051
|
export async function factReadBack(memoryDir, query, envelope, miss, graph = null, focusLabel = null, biasByBundle = {}, cache = null) {
|
|
6052
|
+
return withDeducedGoal(await factReadBackReaders(memoryDir, query, envelope, miss, graph, focusLabel, biasByBundle, cache), envelope, query);
|
|
6053
|
+
}
|
|
6054
|
+
|
|
6055
|
+
async function factReadBackReaders(memoryDir, query, envelope, miss, graph = null, focusLabel = null, biasByBundle = {}, cache = null) {
|
|
5285
6056
|
if (!miss) return null;
|
|
5286
6057
|
let normFactTerm;
|
|
5287
6058
|
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
@@ -5531,7 +6302,7 @@ export async function factReadBack(memoryDir, query, envelope, miss, graph = nul
|
|
|
5531
6302
|
// sharing it with (a0): RELATION_WHO_ASK_RE and RELATION_FACT_YESNO_RE never
|
|
5532
6303
|
// both match the same query (one starts with "who", the other with
|
|
5533
6304
|
// "is/are/was/were"), so the two blocks never run in the same call.
|
|
5534
|
-
const whoAsk = qHedge.match(RELATION_WHO_ASK_RE);
|
|
6305
|
+
const whoAsk = qHedge.match(RELATION_WHO_ASK_RE) || matchGenitiveWhoAsk(qHedge);
|
|
5535
6306
|
if (whoAsk) {
|
|
5536
6307
|
const relationName = whoAsk[1].trim().toLowerCase();
|
|
5537
6308
|
const rawObject = whoAsk[2].trim();
|
|
@@ -7145,7 +7916,7 @@ async function compareAnswer(query, { graph, config, source }) {
|
|
|
7145
7916
|
const cmp = renderCompare(g, indA, indB);
|
|
7146
7917
|
if (!cmp) {
|
|
7147
7918
|
return {
|
|
7148
|
-
text: `I can only compare two entities of the SAME kind right now — "${indA.label}" is a ${indA.class || "Entity"} and "${indB.label}" is a ${indB.class || "Entity"}.`,
|
|
7919
|
+
text: `I can only compare two entities of the SAME kind right now — "${indA.label}" is a ${classDisplayName(indA.class || "Entity")} and "${indB.label}" is a ${classDisplayName(indB.class || "Entity")}.`,
|
|
7149
7920
|
ents: [indA, indB],
|
|
7150
7921
|
};
|
|
7151
7922
|
}
|
|
@@ -7353,7 +8124,8 @@ function actionLabel(name, subject, target) {
|
|
|
7353
8124
|
async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", }) {
|
|
7354
8125
|
const q = String(query).trim();
|
|
7355
8126
|
|
|
7356
|
-
const
|
|
8127
|
+
const thatGoal = q.match(GOAL_TEACH_RE);
|
|
8128
|
+
const goalMatch = thatGoal || q.match(GOAL_TEACH_INFINITIVE_RE);
|
|
7357
8129
|
if (goalMatch) {
|
|
7358
8130
|
const { normFactTerm } = await import("./memory/core.mjs");
|
|
7359
8131
|
const verb = await verbLemma(goalMatch[3]);
|
|
@@ -7369,7 +8141,11 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
7369
8141
|
predicate: `${verb}-${goalMatch[4].toLowerCase()}`,
|
|
7370
8142
|
object: normFactTerm(goalMatch[5]),
|
|
7371
8143
|
};
|
|
7372
|
-
const tail =
|
|
8144
|
+
const tail = thatGoal
|
|
8145
|
+
? q.replace(/^the\s+goal\s+is\s+that\s+/i, "").replace(/[.!?]+$/, "")
|
|
8146
|
+
// The infinitive voicing restates as the that-form, so the goal check's
|
|
8147
|
+
// own "done — …" line and the confirmation read identically either way.
|
|
8148
|
+
: `${goalMatch[1] ? `${goalMatch[1].toLowerCase()} ` : ""}${goalMatch[2].toLowerCase()} ${verb}s ${goalMatch[4].toLowerCase()} ${goalMatch[5].toLowerCase()}`;
|
|
7373
8149
|
const prev = planHolder.state && Array.isArray(planHolder.state.goals) && !planHolder.state.done ? planHolder.state : null;
|
|
7374
8150
|
planHolder.state = {
|
|
7375
8151
|
goals: [...(prev?.goals ?? []), spec],
|
|
@@ -7614,8 +8390,19 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7614
8390
|
answer = content;
|
|
7615
8391
|
if (envJson) { try { envelope = JSON.parse(envJson); } catch { envelope = null; } }
|
|
7616
8392
|
} catch (e) {
|
|
7617
|
-
|
|
7618
|
-
|
|
8393
|
+
const thrown = String(e?.message || e);
|
|
8394
|
+
// A graph-less session's ask dispatch fails reading the never-configured
|
|
8395
|
+
// graph artifact — an internal error string, not an answer. Swap in an
|
|
8396
|
+
// honest wall; the teach/fact lanes below still get their turn and
|
|
8397
|
+
// replace it whenever they can store or answer instead. A missing config
|
|
8398
|
+
// gets the same wall: with no config at all, no dispatch could ever have
|
|
8399
|
+
// loaded a graph, whatever the internal error spelled.
|
|
8400
|
+
answer = !graph && (!config || /^cannot read graph artifact\b/.test(thrown))
|
|
8401
|
+
? "I can't answer that as a code question — no code graph is loaded in this session. "
|
|
8402
|
+
+ "I can still remember and answer taught facts (try \"every disk is a game piece\"), "
|
|
8403
|
+
+ "or run `tmct init` in a repo to index one."
|
|
8404
|
+
: thrown;
|
|
8405
|
+
note(trace, `intermediate: the ask engine threw — ${thrown}`);
|
|
7619
8406
|
}
|
|
7620
8407
|
// NARRATE: the direct parse/traversal receipt, straight off ask()'s own
|
|
7621
8408
|
// envelope, with zero extra instrumentation of ask.mjs: `parsed` is the
|
|
@@ -7781,6 +8568,11 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7781
8568
|
deduced = planLane.deduced;
|
|
7782
8569
|
note(trace, `goal: ${deduced} (revised — the plan lane answered)`);
|
|
7783
8570
|
}
|
|
8571
|
+
// Same rule as the teach lane below: a canonical whose verb only
|
|
8572
|
+
// matched through the fuzzy repair tier ("rests" read as "tests")
|
|
8573
|
+
// misdescribes a plan-lane turn, so it's dropped; an exact parse keeps
|
|
8574
|
+
// its receipt.
|
|
8575
|
+
if (envelope?.parsed?.fuzzyVerb) canonical = null;
|
|
7784
8576
|
note(trace, `lane: (1p) PLAN — ${planLane.note}`);
|
|
7785
8577
|
}
|
|
7786
8578
|
}
|
|
@@ -7815,6 +8607,67 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7815
8607
|
// STACCATO_PRONOUN_RE-no-focus branch ALWAYS returns a tailored nudge for
|
|
7816
8608
|
// this exact shape, never null.
|
|
7817
8609
|
const isStaccatoPronounNoFocus = STACCATO_PRONOUN_RE.test(String(query).trim()) && !focus?.label;
|
|
8610
|
+
// A bare plural-membership declarative ("dogs are animals" — exactly 3
|
|
8611
|
+
// words) needs the SAME deferral: it's an unambiguous TEACH shape (lane 4),
|
|
8612
|
+
// but isConversational's ≤3-word catch-all claims it first. Gated on BOTH
|
|
8613
|
+
// sides singularizing to KNOWN lexicon nouns, so real chatter ("these are
|
|
8614
|
+
// yours") stays with the orientation card.
|
|
8615
|
+
let isPluralMembershipTeach = false;
|
|
8616
|
+
// A bare habitual naming a subject grounded NOWHERE ("penguins swim", no
|
|
8617
|
+
// prior grounding) gets an honest grounding hint instead of the
|
|
8618
|
+
// orientation card — computed here, rendered inside the conversational
|
|
8619
|
+
// branch below so a turn something real answers never shows it.
|
|
8620
|
+
let habitualGroundingHint = null;
|
|
8621
|
+
// "ahab fathered john" — a bare verb-inflected relational teach (exactly
|
|
8622
|
+
// the shape teachLane's own frame stores) needs the SAME deferral, or the
|
|
8623
|
+
// ≤3-word catch-all claims it first. "john likes mary" (present tense)
|
|
8624
|
+
// stays wrapper-required BY DESIGN — it gets a nudge at the wrapped form,
|
|
8625
|
+
// never a store.
|
|
8626
|
+
let isBareRelationalVerbTeach = false;
|
|
8627
|
+
let bareTeachWrapperNudge = null;
|
|
8628
|
+
{
|
|
8629
|
+
const bareLine = String(query).trim();
|
|
8630
|
+
const pm = bareLine.match(/^([\w-]+)\s+are\s+([\w-]+)[.!?]*$/i);
|
|
8631
|
+
const habitual = pm || QUESTION_LEAD_RE.test(bareLine)
|
|
8632
|
+
? null : (matchBareHabitualTeach(bareLine) || matchBareCanTeach(bareLine));
|
|
8633
|
+
if (pm || habitual) {
|
|
8634
|
+
try {
|
|
8635
|
+
const { loadLexicon, lookupNoun } = await import("./grammar/lexicon.mjs");
|
|
8636
|
+
const lex = loadLexicon();
|
|
8637
|
+
if (pm) {
|
|
8638
|
+
const s = singularizeSurface(pm[1].toLowerCase());
|
|
8639
|
+
const o = singularizeSurface(pm[2].toLowerCase());
|
|
8640
|
+
isPluralMembershipTeach = s !== pm[1].toLowerCase() && !!lookupNoun(lex, s) && !!lookupNoun(lex, o);
|
|
8641
|
+
} else {
|
|
8642
|
+
// The bare habitual/capability siblings ("dogs bark", "a dog
|
|
8643
|
+
// barks", "wrens can sing") — same deferral, same known-subject
|
|
8644
|
+
// gate, so real chatter never diverts. The naive singular is tried
|
|
8645
|
+
// too: matchBareCanTeach keeps the surface plural ("wrens"), but
|
|
8646
|
+
// the grounding fact was stored under the singular.
|
|
8647
|
+
const subjects = [...new Set([habitual.subject, singularizeSurface(habitual.subject)])];
|
|
8648
|
+
isPluralMembershipTeach = subjects.some((s) => !!lookupNoun(lex, s));
|
|
8649
|
+
if (!isPluralMembershipTeach && memoryDir) {
|
|
8650
|
+
let grounded = false;
|
|
8651
|
+
for (const s of subjects) grounded = grounded || (await isGroundedByFact(s, memoryDir, cache));
|
|
8652
|
+
if (grounded) {
|
|
8653
|
+
// Grounded by a prior taught fact — defer the same way; the
|
|
8654
|
+
// teach lane's grounded-subject direct write stores it.
|
|
8655
|
+
isPluralMembershipTeach = true;
|
|
8656
|
+
} else {
|
|
8657
|
+
habitualGroundingHint = habitualGroundingHintText(
|
|
8658
|
+
bareLine.replace(/[.!?]+\s*$/, ""),
|
|
8659
|
+
{ subject: subjects[subjects.length - 1], verb: habitual.verb },
|
|
8660
|
+
);
|
|
8661
|
+
}
|
|
8662
|
+
}
|
|
8663
|
+
}
|
|
8664
|
+
} catch { /* lexicon unavailable — leave false, the ordinary path decides */ }
|
|
8665
|
+
} else if (memoryDir && !QUESTION_LEAD_RE.test(bareLine)
|
|
8666
|
+
&& bareLine.replace(/[.!?]+\s*$/, "").split(/\s+/).filter(Boolean).length <= 3) {
|
|
8667
|
+
if (await matchRelationalVerbTeach(bareLine)) isBareRelationalVerbTeach = true;
|
|
8668
|
+
else bareTeachWrapperNudge = await bareTeachWrapperNudgeText(bareLine);
|
|
8669
|
+
}
|
|
8670
|
+
}
|
|
7818
8671
|
// A vague relation touch ("what about cochange", "tell me about cochange",
|
|
7819
8672
|
// the staccato chain continuation "and cochange?") whose relation word has NO
|
|
7820
8673
|
// bare single-word VERB_TO_KIND form of its own needs the SAME deferral as
|
|
@@ -7842,7 +8695,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7842
8695
|
} catch { /* leave false — the ordinary path decides */ }
|
|
7843
8696
|
}
|
|
7844
8697
|
}
|
|
7845
|
-
const conversationalCandidateBaseGate = !handled && miss && !envelope?.parsed && !isWhatAboutContinuation && !isDescribePronounContinuation && !isExplainTouch && !isStaccatoNegation && !isVagueRelationTouch && !isStaccatoComparative && !isStaccatoPronounNoFocus;
|
|
8698
|
+
const conversationalCandidateBaseGate = !handled && miss && !envelope?.parsed && !isWhatAboutContinuation && !isDescribePronounContinuation && !isExplainTouch && !isStaccatoNegation && !isVagueRelationTouch && !isStaccatoComparative && !isStaccatoPronounNoFocus && !isPluralMembershipTeach && !isBareRelationalVerbTeach;
|
|
7846
8699
|
// A turn whose pronoun was bound to a vocabulary antecedent is PROVABLY a
|
|
7847
8700
|
// fact question ("can it bark" → "can dog bark") — never conversational,
|
|
7848
8701
|
// however short. Without this, the substituted 3-worder still trips
|
|
@@ -7878,12 +8731,23 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7878
8731
|
// gets a turn.
|
|
7879
8732
|
const reversePredicateShape = WHAT_USED_FOR_RE.test(gateQuery)
|
|
7880
8733
|
|| REVERSE_PREDICATE_MARKERS.some(({ re }) => re.test(gateQuery));
|
|
8734
|
+
// The capability family's SHORTEST members ("can birds fly", "do birds
|
|
8735
|
+
// fly", "what can bark" — all three words) trip isConversational()'s
|
|
8736
|
+
// word-count catch-all before factAnswer's capability readers ever run;
|
|
8737
|
+
// same divert-only-on-a-real-hit treatment as the reverse predicates
|
|
8738
|
+
// above.
|
|
8739
|
+
const capabilityAskShape = CAN_ASK_RE.test(gateQuery) || WHAT_CAN_DO_RE.test(gateQuery)
|
|
8740
|
+
|| DO_VERB_ASK_RE.test(gateQuery) || WHICH_KIND_CAN_RE.test(gateQuery) || WHAT_CAN_VERB_RE.test(gateQuery);
|
|
7881
8741
|
let bareMetaHit = null;
|
|
7882
|
-
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape)) {
|
|
8742
|
+
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape || capabilityAskShape)) {
|
|
7883
8743
|
if (memoryDir) {
|
|
7884
8744
|
bareMetaHit = (await factAnswer(memoryDir, gateQuery, envelope, miss, biasByBundle, cache))
|
|
7885
8745
|
?? (await factReadBack(memoryDir, gateQuery, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
|
|
7886
|
-
|
|
8746
|
+
// An honest-miss return never diverts the gate — EXCEPT the capability
|
|
8747
|
+
// family's can't-confirm, which names the subject's real capabilities
|
|
8748
|
+
// and a round-trip teach hint: strictly more useful than the
|
|
8749
|
+
// orientation card this gate would otherwise fall to.
|
|
8750
|
+
if (bareMetaHit?.miss && !capabilityAskShape) bareMetaHit = null;
|
|
7887
8751
|
// A bare "what is X" with NO taught fact but a KNOWN curated corpus term
|
|
7888
8752
|
// ("what is cache", no article) needs the same "only diverts on a REAL
|
|
7889
8753
|
// hit" treatment — curatedDefinitionAnswer otherwise only ever runs once
|
|
@@ -7920,10 +8784,30 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7920
8784
|
}
|
|
7921
8785
|
if (bareMetaHit) {
|
|
7922
8786
|
answer = bareMetaHit.replace ? bareMetaHit.text : `${answer}\n${bareMetaHit.text}`;
|
|
7923
|
-
|
|
8787
|
+
// Same discipline as lane (3): a fact-lane return flagged `miss` is an
|
|
8788
|
+
// honest miss in better words — the turn record keeps miss=true and via
|
|
8789
|
+
// stays untouched.
|
|
8790
|
+
if (!bareMetaHit.miss) { via = "fact"; recordMiss = false; }
|
|
8791
|
+
handled = true;
|
|
7924
8792
|
if (bareMetaHit.pending) factPending = bareMetaHit.pending;
|
|
7925
8793
|
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");
|
|
7926
8794
|
note(trace, "source: .tmct/memory Facts (see /memory for provenance per line)");
|
|
8795
|
+
} else if (isConversationalCandidate && habitualGroundingHint) {
|
|
8796
|
+
// A bare habitual teach ("penguins swim") naming a subject grounded
|
|
8797
|
+
// nowhere: an honest, actionable grounding hint beats the orientation
|
|
8798
|
+
// card — the card answers a question the user never asked.
|
|
8799
|
+
answer = habitualGroundingHint;
|
|
8800
|
+
via = "teach-miss"; handled = true;
|
|
8801
|
+
note(trace, "lane: (2) HABITUAL GROUNDING HINT — a bare habitual teach named an ungrounded subject; pointed at the grounding phrase instead of the orientation card");
|
|
8802
|
+
note(trace, "goal: teach/remember a new capability fact (subject not yet grounded)");
|
|
8803
|
+
} else if (isConversationalCandidate && bareTeachWrapperNudge) {
|
|
8804
|
+
// A bare name-verb-name declarative ("john likes mary"): stays
|
|
8805
|
+
// wrapper-required, so nothing stores — but pointing at the wrapped form
|
|
8806
|
+
// that DOES store beats the orientation card for the same reason.
|
|
8807
|
+
answer = bareTeachWrapperNudge;
|
|
8808
|
+
via = "teach-miss"; handled = true;
|
|
8809
|
+
note(trace, "lane: (2) BARE TEACH NUDGE — a bare name-verb-name declarative stays wrapper-required; suggested the remember-that form");
|
|
8810
|
+
note(trace, "goal: teach/remember a new fact (wrapper required for the bare form)");
|
|
7927
8811
|
} else if (isConversationalCandidate) {
|
|
7928
8812
|
// A conversational miss (a greeting, "what can you do", a very short non-code
|
|
7929
8813
|
// line) gets the friendly orientation (module-aware: empty → --repo/tmct init).
|
|
@@ -7950,8 +8834,22 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7950
8834
|
// reified fact is stronger evidence than a transcript echo. Subject-side facts
|
|
7951
8835
|
// first (factAnswer), then the reverse-membership read-back (factReadBack) so an
|
|
7952
8836
|
// asserted "every X is a Y" answers "what is a Y" too.
|
|
8837
|
+
// Raw query first (the long-standing contract), then ONE retry with the
|
|
8838
|
+
// normalized form — gated to the no-envelope bootstrap ONLY: on the FIRST
|
|
8839
|
+
// turn of a graph-less session the ask engine throws before its own
|
|
8840
|
+
// normalize pass runs, so a politeness-wrapped vocabulary question
|
|
8841
|
+
// ("could you tell me what a dog is") reaches this lane still wearing the
|
|
8842
|
+
// wrapper no reader matches. From turn 2 on (envelope present) the
|
|
8843
|
+
// pipeline unwraps it upstream, and an unrestricted retry would let
|
|
8844
|
+
// normalization-mangled text reach readers whose guards were written for
|
|
8845
|
+
// the raw surface (the pronoun-subject identity family).
|
|
8846
|
+
const normalizedForFacts = envelope ? null : normalizeQuery(String(query));
|
|
7953
8847
|
const fact = (await factAnswer(memoryDir, query, envelope, miss, biasByBundle, cache))
|
|
7954
|
-
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label, biasByBundle, cache))
|
|
8848
|
+
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label, biasByBundle, cache))
|
|
8849
|
+
?? (normalizedForFacts && normalizedForFacts !== String(query).trim()
|
|
8850
|
+
? (await factAnswer(memoryDir, normalizedForFacts, envelope, miss, biasByBundle, cache))
|
|
8851
|
+
?? (await factReadBack(memoryDir, normalizedForFacts, envelope, miss, graph, newFocus?.label, biasByBundle, cache))
|
|
8852
|
+
: null);
|
|
7955
8853
|
if (fact) {
|
|
7956
8854
|
answer = fact.replace ? fact.text : `${answer}\n${fact.text}`;
|
|
7957
8855
|
// A fact-lane return flagged `miss` is an HONEST MISS in better words
|
|
@@ -7971,7 +8869,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7971
8869
|
// question ("does margo eat ribs") never parses as a structural graph
|
|
7972
8870
|
// query at all.
|
|
7973
8871
|
if (fact.generalVerbQuery) {
|
|
7974
|
-
deduced =
|
|
8872
|
+
deduced = TAUGHT_FACT_LOOKUP_GOAL;
|
|
7975
8873
|
note(trace, `goal: ${deduced} (revised — a general-verb direct-question fact lookup answered this turn)`);
|
|
7976
8874
|
}
|
|
7977
8875
|
} else if (miss) {
|
|
@@ -8092,6 +8990,13 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
8092
8990
|
// line instead.
|
|
8093
8991
|
deduced = "teach/remember a new fact";
|
|
8094
8992
|
note(trace, `goal: ${deduced} (revised — the teach lane recognized this shape where the raw structural parse never should have)`);
|
|
8993
|
+
// A canonical whose verb only matched through the fuzzy edit-distance
|
|
8994
|
+
// tier ("disk-1 rests on peg-a." read as an ask about "tests") restates
|
|
8995
|
+
// a repair, not the sentence — under a teach confirmation that's
|
|
8996
|
+
// misleading, so it's dropped. An exact-vocabulary parse ("father is a
|
|
8997
|
+
// kind of parent" as inherits) keeps its canonical: it genuinely
|
|
8998
|
+
// restates the relation the teach stored.
|
|
8999
|
+
if (envelope?.parsed?.fuzzyVerb) canonical = null;
|
|
8095
9000
|
}
|
|
8096
9001
|
}
|
|
8097
9002
|
// (4b) #4 AUTHOR lane — "who is <Name>", "what did <Name> touch",
|
|
@@ -8431,23 +9336,29 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
8431
9336
|
if (!argText) return mk("/plan needs a request, e.g. `/plan of the modules impacted by X, which are untested`.", { miss: true });
|
|
8432
9337
|
if (!graph) return mk("no graph loaded — /plan needs a code graph to plan over.", { miss: true });
|
|
8433
9338
|
const { buildCapabilityPlanCtx, runCapabilityPlan, declaredCapabilityNames } = await import("./router/drive.mjs");
|
|
8434
|
-
const planCtx = await buildCapabilityPlanCtx({ config, source, tel, graph });
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
9339
|
+
const planCtx = await buildCapabilityPlanCtx({ config, source, tel, graph, memoryDir });
|
|
9340
|
+
try {
|
|
9341
|
+
const result = await runCapabilityPlan(argText, declaredCapabilityNames(), planCtx);
|
|
9342
|
+
if (result.refused) {
|
|
9343
|
+
const why = Array.isArray(result.why) ? result.why.join("; ") : result.why;
|
|
9344
|
+
const c1Why = result.c1Why && (Array.isArray(result.c1Why) ? result.c1Why.join("; ") : result.c1Why);
|
|
9345
|
+
note(trace, `result: no plan found — ${why}`);
|
|
9346
|
+
return mk(`no plan found — ${why}${c1Why ? ` (the direct router also declined: ${c1Why})` : ""}`, { miss: true });
|
|
9347
|
+
}
|
|
9348
|
+
note(trace, `result: ${result.driver} — ${result.calls.length} step(s)`);
|
|
9349
|
+
const lines = [`driver: ${result.driver}`, "", "steps:"];
|
|
9350
|
+
result.calls.forEach((c, i) => lines.push(` ${i + 1}. ${c.name} ${JSON.stringify(c.input || {})}`));
|
|
9351
|
+
if (result.composed !== undefined && result.composed !== null) {
|
|
9352
|
+
lines.push("", `composed answer (${result.composed.length}): ${result.composed.length ? result.composed.join(", ") : "(empty set)"}`);
|
|
9353
|
+
} else if (result.observed) {
|
|
9354
|
+
lines.push("", result.observed);
|
|
9355
|
+
}
|
|
9356
|
+
return mk(lines.join("\n"));
|
|
9357
|
+
} finally {
|
|
9358
|
+
// The taught registrations are per-ctx; unregister so the next /plan
|
|
9359
|
+
// turn re-reads the store instead of meeting a stale name collision.
|
|
9360
|
+
for (const dispose of planCtx.disposers || []) dispose();
|
|
8449
9361
|
}
|
|
8450
|
-
return mk(lines.join("\n"));
|
|
8451
9362
|
}
|
|
8452
9363
|
|
|
8453
9364
|
const spec = COMMANDS[name];
|
|
@@ -8858,7 +9769,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8858
9769
|
const sentences = splitSentences(workingLine);
|
|
8859
9770
|
if (sentences.length > 1) {
|
|
8860
9771
|
const lastSentence = sentences[sentences.length - 1];
|
|
8861
|
-
if (PLAN_SOLVE_RE.test(lastSentence) || GOAL_TEACH_RE.test(lastSentence) || LEGAL_MOVES_RE.test(lastSentence)) {
|
|
9772
|
+
if (PLAN_SOLVE_RE.test(lastSentence) || GOAL_TEACH_RE.test(lastSentence) || GOAL_TEACH_INFINITIVE_RE.test(lastSentence) || LEGAL_MOVES_RE.test(lastSentence)) {
|
|
8862
9773
|
let f = focus; let l = last; let ps = planHolder.state;
|
|
8863
9774
|
const receipts = [];
|
|
8864
9775
|
let finalRec = null;
|