@polycode-projects/the-mechanical-code-talker 1.10.14 → 1.11.5
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 +121 -114
- package/ROADMAP.md +14 -4
- package/bin/tmct.mjs +167 -77
- package/data/games/crates.txt +24 -0
- package/data/games/hanoi-3.txt +30 -0
- package/data/games/river.txt +33 -0
- package/package.json +1 -1
- package/src/ask.mjs +119 -6
- package/src/chat.mjs +1515 -52
- package/src/codegraph.mjs +24 -11
- package/src/domain.mjs +350 -0
- package/src/import-file.mjs +86 -0
- package/src/init.mjs +46 -1
- package/src/interpret/normalize.mjs +46 -0
- package/src/interpret/strategies/keywords.mjs +13 -9
- package/src/ledger-viz.mjs +635 -0
- package/src/memory/core.mjs +100 -17
- package/src/memory/shacl.mjs +20 -7
- package/src/memory-ask-browser-entry.mjs +9 -7
- package/src/memory-ask-browser.bundle.js +5648 -1177
- package/src/plan-viz.mjs +409 -0
- package/src/router/drive.mjs +122 -13
- package/src/router/guardrail.mjs +5 -0
- package/src/router/registry.mjs +55 -11
- package/src/router/resolver.mjs +13 -0
- package/src/router/taught.mjs +84 -0
- package/src/sentences.mjs +19 -0
- package/src/syllogise.mjs +154 -38
- package/src/viz-theme.mjs +66 -0
- package/src/wink-model.mjs +12 -6
- package/src/ask-browser-entry.mjs +0 -19
- package/src/ask-browser.bundle.js +0 -5411
- 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,7 +35,9 @@ 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";
|
|
40
|
+
import { splitSentences } from "./sentences.mjs";
|
|
37
41
|
import {
|
|
38
42
|
VERB_TO_KIND, WHERE_MARKERS, MENTION_MARKERS, ENTITY_TO_TYPE, PASSIVE_PARTICIPLE_TO_KIND,
|
|
39
43
|
stripTrailingScopeFiller, stripTrailingDiscourseTag, EDGE_NOUN_TO_METRIC, RELATIONS,
|
|
@@ -101,6 +105,10 @@ const GOAL_BY_KIND = {
|
|
|
101
105
|
};
|
|
102
106
|
const goalNoun = (entityType) => (entityType ? `${String(entityType).toLowerCase()}(s)` : "entities");
|
|
103
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
|
+
|
|
104
112
|
/** Deduce a one-line goal statement from the ask engine's parsed AST — either
|
|
105
113
|
* the plain-clause form ({shape,kind,entityType,object[,subject]}) or the
|
|
106
114
|
* compositional form ({node:...}, ask.mjs's §compositional grammar). Returns
|
|
@@ -1463,7 +1471,7 @@ function moduleOverviewText(graph, ind) {
|
|
|
1463
1471
|
parts.push(testedBy.length
|
|
1464
1472
|
? `covered by ${testedBy.length} test module${testedBy.length === 1 ? "" : "s"}`
|
|
1465
1473
|
: "no recorded tests");
|
|
1466
|
-
const cls = (ind.class || "entity")
|
|
1474
|
+
const cls = classDisplayName(ind.class || "entity");
|
|
1467
1475
|
const pointer = pickPhrase("full-breakdown", ind.id, "for the full breakdown");
|
|
1468
1476
|
return `${ind.label} is a ${cls} — ${parts.join("; ")}. `
|
|
1469
1477
|
+ `/describe ${ind.label} ${pointer}.`;
|
|
@@ -1532,13 +1540,17 @@ const WALL_MISS_ANYWHERE_RE = /couldn't parse this as a graph question\. Try:/;
|
|
|
1532
1540
|
// assert/memory path; when it can't be stored, say what CAN be remembered
|
|
1533
1541
|
// instead of the grammar wall or a silent data loss.
|
|
1534
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;
|
|
1535
|
-
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;
|
|
1536
1544
|
/** "X is <comparative> than Y" — the comparative teach/ask surface. The
|
|
1537
1545
|
* comparative slot is closed by SHAPE (-er word, better/worse, or a
|
|
1538
1546
|
* more/less + adjective pair), never a hand-list of adjectives. */
|
|
1539
1547
|
const COMPARATIVE_SRC = "(?:[a-z]+er|better|worse|(?:more|less)\\s+[a-z]+)";
|
|
1540
1548
|
const COMPARATIVE_TEACH_RE = new RegExp(`^(?:the\\s+|an?\\s+)?([\\w'-]+(?:\\s+[\\w'-]+)?)\\s+(?:is|are)\\s+(${COMPARATIVE_SRC})\\s+than\\s+(.+)$`, "i");
|
|
1541
1549
|
const COMPARATIVE_ASK_RE = new RegExp(`^(?:is|are)\\s+(.+?)\\s+(${COMPARATIVE_SRC})\\s+than\\s+(.+?)[?.!\\s]*$`, "i");
|
|
1550
|
+
/** The one closed preposition set shared by every frame that folds a
|
|
1551
|
+
* preposition into a minted predicate (the general-verb teach/query lanes
|
|
1552
|
+
* and the action-rule frames) — a single source so the set never forks. */
|
|
1553
|
+
const PREP_SRC = "on|in|at|onto|upon|under|over|beside|near|behind|above|below|inside|outside";
|
|
1542
1554
|
/** Interrogative / auxiliary leads that make an "X is a Y"-shaped line a QUESTION
|
|
1543
1555
|
* ("what is a cache", "is a module a component"), never a teach declarative. */
|
|
1544
1556
|
const QUESTION_LEAD_RE = /^(?:what|who|which|where|when|why|how|is|are|do|does|did|can|could|should|would|will|has|have)\b/i;
|
|
@@ -1596,12 +1608,9 @@ const HAS_PROPERTY_PREDICATE = "mgx:hasProperty";
|
|
|
1596
1608
|
// "some/a few Xs are Ys" shape) stay obviously in that same family rather than
|
|
1597
1609
|
// re-typing the CURIE string at each call site.
|
|
1598
1610
|
const SUBCLASS_PREDICATE = "rdfs:subClassOf";
|
|
1599
|
-
//
|
|
1600
|
-
//
|
|
1601
|
-
//
|
|
1602
|
-
// obviously in that same family, interoperable with corpus HasA data on the
|
|
1603
|
-
// read side, rather than minting a redundant mgx:has.
|
|
1604
|
-
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.
|
|
1605
1614
|
|
|
1606
1615
|
// mgx:sourceType's own closed kind set (memory/core.mjs) splits "the operator
|
|
1607
1616
|
// said it" across two tags depending which lane wrote it (ace: -> "operator",
|
|
@@ -1674,6 +1683,46 @@ const OWNS_PASSIVE_TEACH_RE = /^(.+?)\s+(?:is|are|was|were)\s+owned\s+by\s+([A-Z
|
|
|
1674
1683
|
const RELATION_FACT_TEACH_RE =
|
|
1675
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;
|
|
1676
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
|
+
|
|
1677
1726
|
/** "every/a/an/the <N1> has a/an <N2> method" — the HAS-A-METHOD teach
|
|
1678
1727
|
* declarative: a possession-of-capability claim about a class/entity's
|
|
1679
1728
|
* method ("every Component has a render method", "a Widget has a render
|
|
@@ -1755,6 +1804,107 @@ const FILTER_RULE_TEACH_RE =
|
|
|
1755
1804
|
const RECURSIVE_RULE_TEACH_RE =
|
|
1756
1805
|
/^an?\s+([a-z][\w-]*)\s+(?:is|are)\s+an?\s+([a-z][\w-]*),?\s+or\s+an?\s+([a-z][\w-]*)\s+of\s+an?\s+\1[.!?]*$/i;
|
|
1757
1806
|
|
|
1807
|
+
/** ACTION-RULE TEACH FRAMES — a world-mutating action taught one sentence at
|
|
1808
|
+
* a time, each sentence its own Rule individual (kind action-signature /
|
|
1809
|
+
* action-precond / action-effect / action-constraint) sharing one rule name ("<verb> <prep>",
|
|
1810
|
+
* e.g. "move onto"). src/domain.mjs collects the family by name
|
|
1811
|
+
* (findRulesByName) and grounds it over class members at plan time; nothing
|
|
1812
|
+
* in the teach lane executes an action. Predicate slot values are stored
|
|
1813
|
+
* BARE ("rest-on") because normFactTerm strips a mgx: prefix from slot
|
|
1814
|
+
* values; readers re-attach it. The class/role words are single tokens, the
|
|
1815
|
+
* preposition set is PREP_SRC, the comparative slot is COMPARATIVE_SRC. */
|
|
1816
|
+
const ACTION_SIGNATURE_TEACH_RE = new RegExp(
|
|
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");
|
|
1823
|
+
const ACTION_PRECOND_NOTHING_RE = new RegExp(
|
|
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");
|
|
1825
|
+
const ACTION_PRECOND_COMPARATIVE_RE = new RegExp(
|
|
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");
|
|
1827
|
+
const ACTION_EFFECT_TEACH_RE = new RegExp(
|
|
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");
|
|
1839
|
+
/** "a disk renders as a block" — the render-template binding, an ordinary
|
|
1840
|
+
* Fact on the curated mgx:rendersAs predicate (camelCase, so the
|
|
1841
|
+
* general-verb preposition fold can never suffix it). */
|
|
1842
|
+
const RENDERS_AS_TEACH_RE = /^an?\s+([a-z][\w-]*)\s+renders\s+as\s+an?\s+([a-z][\w-]*)[.!?]*$/i;
|
|
1843
|
+
// Bare-copula instance membership: the subject MUST contain a hyphen
|
|
1844
|
+
// (disk-1, peg-a) — see bareTaxonomyTeach's reasoning.
|
|
1845
|
+
const INSTANCE_TYPE_TEACH_RE = /^([a-z][\w]*(?:-[\w]+)+)\s+is\s+an?\s+([a-z][\w-]+)[.!?]*$/i;
|
|
1846
|
+
// Bare article-led kind-of taxonomy: "a disk is a kind of game piece".
|
|
1847
|
+
const BARE_KINDOF_TEACH_RE = /^an?\s+([a-z][\w-]+)\s+is\s+a\s+kind\s+of\s+(?:an?\s+)?([a-z][\w-]+(?:\s+[a-z][\w-]+)?)([.!?]*)$/i;
|
|
1848
|
+
|
|
1849
|
+
/** Verb → lemma via the prose adapter, degrading to the word itself. */
|
|
1850
|
+
async function verbLemma(word) {
|
|
1851
|
+
const w = String(word || "").toLowerCase();
|
|
1852
|
+
try {
|
|
1853
|
+
const { proseLemma } = await import("./prose-nlp.mjs");
|
|
1854
|
+
const lemma = proseLemma();
|
|
1855
|
+
return lemma ? lemma(w) : w;
|
|
1856
|
+
} catch { return w; }
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/** Pre-ask declarative taxonomy teaches. Checked BEFORE the ask engine: "a
|
|
1860
|
+
* disk is a kind of game piece." otherwise parses as an inherits QUESTION
|
|
1861
|
+
* and dies on term resolution, even though an article-led declarative with
|
|
1862
|
+
* no question lead is a statement. Two closed shapes only:
|
|
1863
|
+
* - instance membership with a HYPHENATED subject ("disk-1 is a disk") —
|
|
1864
|
+
* hyphenated/numbered coinages are unambiguous individual names, so this
|
|
1865
|
+
* stays clear of the plain-word bare "X is a Y" declines the tier-5
|
|
1866
|
+
* fabrication fixes deliberately preserve;
|
|
1867
|
+
* - article-led "is a kind of" taxonomy with a multi-word object — the
|
|
1868
|
+
* infix is unambiguous taxonomy-teach intent and the ACE path can't parse
|
|
1869
|
+
* the two-word object; single-word objects stay with the ACE path. */
|
|
1870
|
+
async function bareTaxonomyTeach(line, { memoryDir, sessionId }) {
|
|
1871
|
+
if (!memoryDir || QUESTION_LEAD_RE.test(line)) return null;
|
|
1872
|
+
const inst = line.match(INSTANCE_TYPE_TEACH_RE);
|
|
1873
|
+
if (inst) {
|
|
1874
|
+
return teachFact(memoryDir, sessionId, {
|
|
1875
|
+
subject: inst[1], predicate: "rdfs:subClassOf", object: inst[2],
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
const kindOf = line.match(BARE_KINDOF_TEACH_RE);
|
|
1879
|
+
if (kindOf) {
|
|
1880
|
+
// Defer to the ACE assert path exactly where it succeeds: a single-word
|
|
1881
|
+
// object with no trailing punctuation ("a father is a kind of parent" —
|
|
1882
|
+
// the pinned README transcript's shape, with its richer receipt). The ACE
|
|
1883
|
+
// path dies on multi-word objects and on trailing punctuation (the
|
|
1884
|
+
// period rides into term resolution), so those store here.
|
|
1885
|
+
const singleWordObject = !/\s/.test(kindOf[2]);
|
|
1886
|
+
const noTrailingPunct = kindOf[3] === "";
|
|
1887
|
+
if (singleWordObject && noTrailingPunct) return null;
|
|
1888
|
+
return teachFact(memoryDir, sessionId, {
|
|
1889
|
+
subject: kindOf[1], predicate: "rdfs:subClassOf", object: kindOf[2],
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
return null;
|
|
1893
|
+
}
|
|
1894
|
+
// The plan lane's closed recognizer set. The goal frame is plan-lane state,
|
|
1895
|
+
// not a Rule — goals accumulate on the session's planState slot.
|
|
1896
|
+
const GOAL_TEACH_RE = new RegExp(
|
|
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");
|
|
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;
|
|
1905
|
+
const LEGAL_MOVES_RE = /^what\s+moves\s+are\s+legal(?:\s+now)?[?.!\s]*$/i;
|
|
1906
|
+
const PLAN_NEXT_RE = /^(?:next|next\s+move|go\s+on|continue)[.!?\s]*$/i;
|
|
1907
|
+
|
|
1758
1908
|
/** "<X> is <adjective>" — the property teach payload (wrapper-REQUIRED): a lazy
|
|
1759
1909
|
* subject and a single bare complement word. Never matches the "is a <noun>"
|
|
1760
1910
|
* membership shape (that stays the ACE grammar's), so "remember that cache is
|
|
@@ -2391,13 +2541,100 @@ async function subjectIsNounOrPropn(word) {
|
|
|
2391
2541
|
}
|
|
2392
2542
|
}
|
|
2393
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
|
+
|
|
2394
2631
|
// ---- General verb-to-predicate DIRECT-QUESTION retrieval: "does margo eat
|
|
2395
2632
|
// ribs" / "what does margo eat" against a fact taught via generalVerbTeach.
|
|
2396
2633
|
// Wired into factReadBack, gated on an already-true `miss` so a real graph
|
|
2397
2634
|
// query is never shadowed. Reuses generalVerbTeach's own exclude guards and
|
|
2398
2635
|
// generalVerbPredicate, plus the SAME adverb-skip, so the two never disagree. ----
|
|
2399
2636
|
const GENERAL_VERB_YESNO_RE = new RegExp(`^(?:does|did)\\s+([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+)\\s+(.+?)[?.!\\s]*$`, "i");
|
|
2400
|
-
const GENERAL_VERB_OPEN_RE = new RegExp(`^what\\s+(?:does|did)\\s+([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+(?:\\s+(
|
|
2637
|
+
const GENERAL_VERB_OPEN_RE = new RegExp(`^what\\s+(?:does|did)\\s+([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+(?:\\s+(?:${PREP_SRC}))?)[?.!\\s]*$`, "i");
|
|
2401
2638
|
/** GENERAL_VERB_EXCLUDE_RE was written for generalVerbTeach's fully-conjugated
|
|
2402
2639
|
* declarative verb ("X OWNS Y", "X MAINTAINS Y") — but "does/did X <verb> Y"
|
|
2403
2640
|
* captures the BARE INFINITIVE after do-support ("does X OWN Y", never "does X
|
|
@@ -2413,7 +2650,7 @@ const GENERAL_VERB_QUERY_EXCLUDE_RE = /^(?:be|own|maintain)$/i;
|
|
|
2413
2650
|
* minted predicate: "disk-1 rests on peg-a" stores mgx:rest-on with object
|
|
2414
2651
|
* "peg-a", never mgx:rest with the meaning-bearing "on" buried inside the
|
|
2415
2652
|
* object where no read-back can match it. */
|
|
2416
|
-
const GENERAL_VERB_PREP_RE =
|
|
2653
|
+
const GENERAL_VERB_PREP_RE = new RegExp(`^(${PREP_SRC})\\s+(.+)$`, "i");
|
|
2417
2654
|
/** Fold a leading preposition from `objectRaw` into a minted mgx:<lemma>
|
|
2418
2655
|
* predicate. Curated predicates (mgx:hasA, mgx:capableOf — anything not the
|
|
2419
2656
|
* plain lowercase mint shape) are never suffixed. Returns {predicate,
|
|
@@ -2433,8 +2670,85 @@ function assertCandidates(payload) {
|
|
|
2433
2670
|
const p = String(payload).trim();
|
|
2434
2671
|
const out = [p];
|
|
2435
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
|
+
}
|
|
2436
2701
|
return [...new Set(out)];
|
|
2437
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
|
+
|
|
2438
2752
|
/** The "every X is a Y" rewrite of a declarative, for the "did you mean …"
|
|
2439
2753
|
* hint. Real a/an agreement (never a hardcoded "a", which is ungrammatical
|
|
2440
2754
|
* for a vowel-initial Y — "every monkey is a animal") reuses finish.mjs's
|
|
@@ -2451,6 +2765,21 @@ function teachSuggestion(payload) {
|
|
|
2451
2765
|
return `every ${subject} is ${article} ${object}`;
|
|
2452
2766
|
}
|
|
2453
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
|
+
|
|
2454
2783
|
/** PRONOUN-SUBJECT GUARD: "remember you are a womble" and the literal "every
|
|
2455
2784
|
* you is a womble" would otherwise reach teachSuggestion/
|
|
2456
2785
|
* unknownSubjectFallback treating "you" like an ordinary unknown common
|
|
@@ -2555,6 +2884,70 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2555
2884
|
const raw = stripKindOf(stripYour(stripPossessiveNamedInstance(rawInput)));
|
|
2556
2885
|
const wrapped = stripKindOf(stripYour(stripPossessiveNamedInstance(wrappedInput)));
|
|
2557
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
|
+
|
|
2558
2951
|
// PRONOUN-SUBJECT GUARD — tried against BOTH surfaces (bare and remember-
|
|
2559
2952
|
// wrapped; trailing punctuation stripped the same way the OWNS/SOME_A_FEW
|
|
2560
2953
|
// lanes below do) before anything else in this function, so a pronoun
|
|
@@ -2573,7 +2966,11 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2573
2966
|
// `are` payload — see the payload-construction block below), which leaves
|
|
2574
2967
|
// whatever the structural grammar's own honest miss already said standing,
|
|
2575
2968
|
// rather than overwriting it with a wrong-reason refusal.
|
|
2576
|
-
|
|
2969
|
+
// The action-signature frame is the ONE pronoun-led teach shape ("you can
|
|
2970
|
+
// move a disk onto a peg") — the full-shape test keeps "you can fly"
|
|
2971
|
+
// declining right here.
|
|
2972
|
+
if (pronounMatch && !ACTION_SIGNATURE_TEACH_RE.test(pronounSrc)
|
|
2973
|
+
&& !(await hasMidSentenceInterrogative(pronounSrc))) {
|
|
2577
2974
|
const pronoun = pronounMatch[1];
|
|
2578
2975
|
return {
|
|
2579
2976
|
text: `I can't store a fact about "${pronoun}" as a class — pronouns aren't things I can classify. `
|
|
@@ -2687,6 +3084,40 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2687
3084
|
if (stored) return stored;
|
|
2688
3085
|
}
|
|
2689
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
|
+
|
|
2690
3121
|
// HAS-A-METHOD TEACH — "every/a/an/the <N1> has a/an <N2> method": a
|
|
2691
3122
|
// possession-of-capability claim, stored as an ordinary Fact via the SAME
|
|
2692
3123
|
// HAS_A_PREDICATE generalVerbTeach's own
|
|
@@ -2786,6 +3217,230 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2786
3217
|
} catch { /* malformed slots — fall through to the ordinary honest-miss cascade */ }
|
|
2787
3218
|
}
|
|
2788
3219
|
|
|
3220
|
+
// ACTION-RULE TEACH — the five action frames plus the render binding (see
|
|
3221
|
+
// the ACTION_*_TEACH_RE docblock). Each sentence stores its own Rule
|
|
3222
|
+
// individual under a shared "<verb> <prep>" name. A role word that names
|
|
3223
|
+
// neither the taught subject class nor the literal "target" is an honest
|
|
3224
|
+
// decline that RETURNS here — falling through would hand these shapes to
|
|
3225
|
+
// the general-verb lane below, which would mint a garbage predicate from
|
|
3226
|
+
// them (the silent-garble case this lane exists to prevent).
|
|
3227
|
+
const actionLemma = verbLemma;
|
|
3228
|
+
const actionRoleFor = (word, subjectClass) => {
|
|
3229
|
+
const w = String(word || "").toLowerCase();
|
|
3230
|
+
if (w === "target") return "target";
|
|
3231
|
+
if (w === String(subjectClass || "").toLowerCase()) return "subject";
|
|
3232
|
+
return null;
|
|
3233
|
+
};
|
|
3234
|
+
|
|
3235
|
+
const actionSig = ownSrc.match(ACTION_SIGNATURE_TEACH_RE);
|
|
3236
|
+
if (actionSig && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
3237
|
+
try {
|
|
3238
|
+
const verb = await actionLemma(actionSig[1]);
|
|
3239
|
+
const prep = actionSig[3].toLowerCase();
|
|
3240
|
+
const { appendRule, RULE_KIND_ACTION_SIGNATURE } = await import("./memory/core.mjs");
|
|
3241
|
+
const { id } = await appendRule(memoryDir, {
|
|
3242
|
+
name: `${verb} ${prep}`,
|
|
3243
|
+
kind: RULE_KIND_ACTION_SIGNATURE,
|
|
3244
|
+
slots: { subjectClass: actionSig[2], targetClass: actionSig[4] },
|
|
3245
|
+
provenance: teachProvenanceTag(sessionId, new Date().toISOString()),
|
|
3246
|
+
});
|
|
3247
|
+
if (id) {
|
|
3248
|
+
return {
|
|
3249
|
+
text: `noted — remembered: you can ${verb} a ${actionSig[2].toLowerCase()} ${prep} a ${actionSig[4].toLowerCase()}`,
|
|
3250
|
+
via: "assert", miss: false,
|
|
3251
|
+
};
|
|
3252
|
+
}
|
|
3253
|
+
} catch { /* malformed slots — fall through to the ordinary honest-miss cascade */ }
|
|
3254
|
+
}
|
|
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
|
+
|
|
3282
|
+
const precondNothing = ownSrc.match(ACTION_PRECOND_NOTHING_RE);
|
|
3283
|
+
if (precondNothing && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
3284
|
+
const role = actionRoleFor(precondNothing[7], precondNothing[2]);
|
|
3285
|
+
if (!role) {
|
|
3286
|
+
return {
|
|
3287
|
+
text: `I can't place "${precondNothing[7]}" in that rule — the last word must be "target" or the ${precondNothing[2]} itself (e.g. "nothing may ${precondNothing[5].toLowerCase()} ${precondNothing[6].toLowerCase()} the ${precondNothing[2]}").`,
|
|
3288
|
+
via: "teach-miss", miss: true,
|
|
3289
|
+
};
|
|
3290
|
+
}
|
|
3291
|
+
try {
|
|
3292
|
+
const verb = await actionLemma(precondNothing[1]);
|
|
3293
|
+
const prep = precondNothing[3].toLowerCase();
|
|
3294
|
+
const innerVerb = await actionLemma(precondNothing[5]);
|
|
3295
|
+
const scopeWord = precondNothing[4].toLowerCase();
|
|
3296
|
+
const { appendRule, RULE_KIND_ACTION_PRECOND } = await import("./memory/core.mjs");
|
|
3297
|
+
const { id } = await appendRule(memoryDir, {
|
|
3298
|
+
name: `${verb} ${prep}`,
|
|
3299
|
+
kind: RULE_KIND_ACTION_PRECOND,
|
|
3300
|
+
slots: {
|
|
3301
|
+
shape: "no-incoming",
|
|
3302
|
+
predicate: `${innerVerb}-${precondNothing[6].toLowerCase()}`,
|
|
3303
|
+
role,
|
|
3304
|
+
scope: scopeWord === "target" ? "any" : scopeWord,
|
|
3305
|
+
},
|
|
3306
|
+
provenance: teachProvenanceTag(sessionId, new Date().toISOString()),
|
|
3307
|
+
});
|
|
3308
|
+
if (id) {
|
|
3309
|
+
return {
|
|
3310
|
+
text: `noted — remembered: to ${verb} ${prep}, nothing may ${precondNothing[5].toLowerCase()} ${precondNothing[6].toLowerCase()} the ${role === "target" ? "target" : precondNothing[2].toLowerCase()}`,
|
|
3311
|
+
via: "assert", miss: false,
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
3314
|
+
} catch { /* malformed slots — fall through to the ordinary honest-miss cascade */ }
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
const precondComp = ownSrc.match(ACTION_PRECOND_COMPARATIVE_RE);
|
|
3318
|
+
if (precondComp && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
3319
|
+
const role = actionRoleFor(precondComp[5], precondComp[2]);
|
|
3320
|
+
const rightWord = precondComp[7].toLowerCase();
|
|
3321
|
+
const otherOk = role === "subject"
|
|
3322
|
+
? (rightWord === "target" || rightWord === precondComp[4].toLowerCase())
|
|
3323
|
+
: (role === "target" && rightWord === precondComp[2].toLowerCase());
|
|
3324
|
+
if (!role || !otherOk) {
|
|
3325
|
+
return {
|
|
3326
|
+
text: `I can't place "${!role ? precondComp[5] : precondComp[7]}" in that rule — the compared words must be the ${precondComp[2]} and the target (e.g. "the ${precondComp[2]} must be smaller than the target").`,
|
|
3327
|
+
via: "teach-miss", miss: true,
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
try {
|
|
3331
|
+
const verb = await actionLemma(precondComp[1]);
|
|
3332
|
+
const prep = precondComp[3].toLowerCase();
|
|
3333
|
+
const scopeWord = precondComp[4].toLowerCase();
|
|
3334
|
+
const { appendRule, RULE_KIND_ACTION_PRECOND } = await import("./memory/core.mjs");
|
|
3335
|
+
const { id } = await appendRule(memoryDir, {
|
|
3336
|
+
name: `${verb} ${prep}`,
|
|
3337
|
+
kind: RULE_KIND_ACTION_PRECOND,
|
|
3338
|
+
slots: {
|
|
3339
|
+
shape: "comparator",
|
|
3340
|
+
predicate: `${precondComp[6].toLowerCase().replace(/\s+/g, "-")}-than`,
|
|
3341
|
+
role,
|
|
3342
|
+
scope: scopeWord === "target" ? "any" : scopeWord,
|
|
3343
|
+
},
|
|
3344
|
+
provenance: teachProvenanceTag(sessionId, new Date().toISOString()),
|
|
3345
|
+
});
|
|
3346
|
+
if (id) {
|
|
3347
|
+
return {
|
|
3348
|
+
text: `noted — remembered: to ${verb} ${prep}, the ${precondComp[5].toLowerCase()} must be ${precondComp[6].toLowerCase()} than the ${rightWord}`,
|
|
3349
|
+
via: "assert", miss: false,
|
|
3350
|
+
};
|
|
3351
|
+
}
|
|
3352
|
+
} catch { /* malformed slots — fall through to the ordinary honest-miss cascade */ }
|
|
3353
|
+
}
|
|
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
|
+
|
|
3380
|
+
const actionEffect = ownSrc.match(ACTION_EFFECT_TEACH_RE);
|
|
3381
|
+
if (actionEffect && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
3382
|
+
const gerund = actionEffect[1].toLowerCase();
|
|
3383
|
+
const verb = await actionLemma(gerund);
|
|
3384
|
+
// An unreduced -ing form would mint a name ("moving onto") that can never
|
|
3385
|
+
// match the signature's ("move onto") — decline rather than store a rule
|
|
3386
|
+
// the interpreter can't collect.
|
|
3387
|
+
if (verb === gerund || !gerund.startsWith(verb.slice(0, Math.min(3, verb.length)))) {
|
|
3388
|
+
return {
|
|
3389
|
+
text: `I can't reduce "${actionEffect[1]}" to its verb right now — the lemmatizer isn't available. Retry later, or teach the other rule sentences first.`,
|
|
3390
|
+
via: "teach-miss", miss: true,
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
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();
|
|
3405
|
+
const objectRole = actionRoleFor(actionEffect[8], actionEffect[2]);
|
|
3406
|
+
if (!objectRole || subjectRole === objectRole) {
|
|
3407
|
+
return {
|
|
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").`,
|
|
3409
|
+
via: "teach-miss", miss: true,
|
|
3410
|
+
};
|
|
3411
|
+
}
|
|
3412
|
+
try {
|
|
3413
|
+
const prep = actionEffect[3].toLowerCase();
|
|
3414
|
+
const effVerb = await actionLemma(actionEffect[6]);
|
|
3415
|
+
const { appendRule, RULE_KIND_ACTION_EFFECT } = await import("./memory/core.mjs");
|
|
3416
|
+
const { id } = await appendRule(memoryDir, {
|
|
3417
|
+
name: `${verb} ${prep}`,
|
|
3418
|
+
kind: RULE_KIND_ACTION_EFFECT,
|
|
3419
|
+
slots: {
|
|
3420
|
+
predicate: `${effVerb}-${actionEffect[7].toLowerCase()}`,
|
|
3421
|
+
subjectRole,
|
|
3422
|
+
objectRole,
|
|
3423
|
+
},
|
|
3424
|
+
provenance: teachProvenanceTag(sessionId, new Date().toISOString()),
|
|
3425
|
+
});
|
|
3426
|
+
if (id) {
|
|
3427
|
+
return {
|
|
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)`),
|
|
3430
|
+
via: "assert", miss: false,
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3433
|
+
} catch { /* malformed slots — fall through to the ordinary honest-miss cascade */ }
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
const rendersAs = ownSrc.match(RENDERS_AS_TEACH_RE);
|
|
3437
|
+
if (rendersAs && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
3438
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
3439
|
+
subject: rendersAs[1], predicate: "mgx:rendersAs", object: rendersAs[2],
|
|
3440
|
+
});
|
|
3441
|
+
if (stored) return stored;
|
|
3442
|
+
}
|
|
3443
|
+
|
|
2789
3444
|
// "some Xs are Ys" / "a few Xs are Ys" — the plural class-
|
|
2790
3445
|
// membership quantifier shape. ACE has no quantifier-phrase pattern at all
|
|
2791
3446
|
// (parseAce never even attempts a fit), so this is ALWAYS a direct write,
|
|
@@ -2875,6 +3530,24 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2875
3530
|
// the structural grammar's own typo-tolerant retry to answer for real.
|
|
2876
3531
|
const subjectWord = raw.match(/^([\w'-]+)/)?.[1];
|
|
2877
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
|
+
}
|
|
2878
3551
|
const gv = await generalVerbTeach(raw);
|
|
2879
3552
|
if (gv) {
|
|
2880
3553
|
const stored = await teachFact(memoryDir, sessionId, gv);
|
|
@@ -2885,7 +3558,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2885
3558
|
|
|
2886
3559
|
let payload = null;
|
|
2887
3560
|
if (wrapped && /\b(?:is|are)\b/i.test(wrapped)) payload = wrapped;
|
|
2888
|
-
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;
|
|
2889
3562
|
if (!payload) {
|
|
2890
3563
|
// "remember margo eats ribs", re-escaping here through a combination
|
|
2891
3564
|
// that mechanism's own deliberate subject-shape restriction doesn't
|
|
@@ -2912,7 +3585,8 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2912
3585
|
if (comp) {
|
|
2913
3586
|
const compPredicate = `mgx:${comp[2].toLowerCase().replace(/\s+/g, "-")}-than`;
|
|
2914
3587
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
2915
|
-
subject: comp[1].trim(), predicate: compPredicate,
|
|
3588
|
+
subject: comp[1].trim(), predicate: compPredicate,
|
|
3589
|
+
object: comp[3].trim().replace(/[.!?]+$/, ""),
|
|
2916
3590
|
});
|
|
2917
3591
|
if (stored) return stored;
|
|
2918
3592
|
}
|
|
@@ -2923,6 +3597,31 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2923
3597
|
const stored = await assertTurn(cand, { memoryDir, sessionId, focus: null, lexicon, cache });
|
|
2924
3598
|
if (stored) return { text: stored.answer, via: "assert", miss: false };
|
|
2925
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
|
+
}
|
|
2926
3625
|
// The real ACE grammar just declined (unknown words / not the membership
|
|
2927
3626
|
// shape) — try the narrow unknown-SUBJECT direct-write fallback before
|
|
2928
3627
|
// falling to the honest-miss cascade. Covers BOTH the bare and the
|
|
@@ -3733,6 +4432,7 @@ const FACT_PREDICATE_PHRASES = {
|
|
|
3733
4432
|
"mgx:hasLastSubevent": "ends with",
|
|
3734
4433
|
"mgx:hasPrerequisite": "requires",
|
|
3735
4434
|
"mgx:ownedBy": "is owned by", // the teach lane's ownership frame ("Priya owns tasks.mjs")
|
|
4435
|
+
"mgx:rendersAs": "renders as", // the render-template binding ("a disk renders as a block")
|
|
3736
4436
|
"mgx:synonym": "means the same as",
|
|
3737
4437
|
"mgx:antonym": "is the opposite of",
|
|
3738
4438
|
"mgx:similarTo": "is similar to",
|
|
@@ -4095,6 +4795,19 @@ const RELATION_FACT_YESNO_RE =
|
|
|
4095
4795
|
const RELATION_WHO_ASK_RE =
|
|
4096
4796
|
/^(?:who|what)\s+(?:is|are)\s+(?:the|an?)\s+([a-z][\w-]*)\s+of\s+([\w'-]+(?:\s+[A-Z][\w'-]*)?)[?.!\s]*$/i;
|
|
4097
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
|
+
|
|
4098
4811
|
/** "list the descendants of ahab" — the REACHABILITY-SET list query: a
|
|
4099
4812
|
* genuine KIND-CHANGE from RELATION_FACT_YESNO_RE just above — every entity
|
|
4100
4813
|
* reachable from the named start entity through a taught `recursive` Rule,
|
|
@@ -4191,6 +4904,66 @@ const WHAT_HAS_RE = /^what\s+has\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
|
4191
4904
|
// — actively misleading for a pure vocabulary query.
|
|
4192
4905
|
const WHAT_USED_FOR_RE = /^what\s+(?:(?:can\s+be|is)\s+used\s+for|is\s+for)\s+(.+?)[?.!\s]*$/i;
|
|
4193
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
|
+
|
|
4194
4967
|
// The SAME gap as mgx:usedFor above is systemic — "what causes fire", "what is
|
|
4195
4968
|
// made of wood" would otherwise fall through to the same misleading
|
|
4196
4969
|
// code-graph miss. DERIVES a reverse-by-object regex for every
|
|
@@ -4290,8 +5063,41 @@ function uniqueFacts(rows) {
|
|
|
4290
5063
|
* (via factRows/memoryFacts below), and loadMemory's own Backend-B branch
|
|
4291
5064
|
* returns the handle's `payload` directly with ZERO fs calls — so a caller
|
|
4292
5065
|
* that hands this a handle already carrying the embedded page's full graph
|
|
4293
|
-
* 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. */
|
|
4294
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) {
|
|
4295
5101
|
let normFactTerm;
|
|
4296
5102
|
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
4297
5103
|
const q = String(query).trim();
|
|
@@ -4342,6 +5148,98 @@ export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle
|
|
|
4342
5148
|
return { text: shown.join("\n") + extra, replace: true, ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}) };
|
|
4343
5149
|
}
|
|
4344
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
|
+
|
|
4345
5243
|
// (a) meta-shaped questions ("what is a module", "what does cache mean") — the
|
|
4346
5244
|
// parsed object term, matched against fact SUBJECTS; consulted for hits (append
|
|
4347
5245
|
// alongside the schema-docs answer) and misses (facts answer alone) alike.
|
|
@@ -4370,6 +5268,17 @@ export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle
|
|
|
4370
5268
|
// tail, verbatim.
|
|
4371
5269
|
if (m) metaTerm = stripTrailingScopeFiller(m[1]);
|
|
4372
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
|
+
}
|
|
4373
5282
|
if (metaTerm) {
|
|
4374
5283
|
// "what is a tree used for" parses (grammar.mjs T5) to the
|
|
4375
5284
|
// WHOLE tail "tree used for" as one literal term — split off a trailing
|
|
@@ -4502,8 +5411,11 @@ export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle
|
|
|
4502
5411
|
const knownCan = facts.filter((f) => f.predicate === "mgx:capableOf" && subj.has(f.subject));
|
|
4503
5412
|
if (knownCan.length) {
|
|
4504
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.
|
|
4505
5417
|
return {
|
|
4506
|
-
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]}".`,
|
|
4507
5419
|
replace: true,
|
|
4508
5420
|
miss: true,
|
|
4509
5421
|
};
|
|
@@ -4527,6 +5439,42 @@ export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle
|
|
|
4527
5439
|
return null;
|
|
4528
5440
|
}
|
|
4529
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
|
+
|
|
4530
5478
|
// (b3) "what can a dog do" — every remembered mgx:capableOf fact for the
|
|
4531
5479
|
// subject, open-list. Reuses the meta-lane's subject-hits/rank/render/
|
|
4532
5480
|
// paginate recipe (lane (a) above) verbatim, with the predicate hardcoded.
|
|
@@ -4543,6 +5491,75 @@ export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle
|
|
|
4543
5491
|
return { text: shown.join("\n") + extra, replace: true, ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}) };
|
|
4544
5492
|
}
|
|
4545
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
|
+
|
|
4546
5563
|
// (b4) "what has a wheel" — the REVERSE-by-OBJECT mirror of every other
|
|
4547
5564
|
// reader in this cascade: filters factRows on mgx:hasA where the OBJECT
|
|
4548
5565
|
// (not subject) matches, so every subject sharing that object surfaces
|
|
@@ -5028,8 +6045,14 @@ function inheritsChain(graph, startId) {
|
|
|
5028
6045
|
* (c) REVERSE membership — "what is a Y" reports Y's members (object-side), and
|
|
5029
6046
|
* "what kind of thing is an X" reports X's own type (subject-side first).
|
|
5030
6047
|
* Miss-only and run AFTER factAnswer returns null, so it never shadows the
|
|
5031
|
-
* subject-side answer or a schema hit. Returns { text, replace:true } or null
|
|
5032
|
-
|
|
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). */
|
|
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) {
|
|
5033
6056
|
if (!miss) return null;
|
|
5034
6057
|
let normFactTerm;
|
|
5035
6058
|
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
@@ -5279,7 +6302,7 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
5279
6302
|
// sharing it with (a0): RELATION_WHO_ASK_RE and RELATION_FACT_YESNO_RE never
|
|
5280
6303
|
// both match the same query (one starts with "who", the other with
|
|
5281
6304
|
// "is/are/was/were"), so the two blocks never run in the same call.
|
|
5282
|
-
const whoAsk = qHedge.match(RELATION_WHO_ASK_RE);
|
|
6305
|
+
const whoAsk = qHedge.match(RELATION_WHO_ASK_RE) || matchGenitiveWhoAsk(qHedge);
|
|
5283
6306
|
if (whoAsk) {
|
|
5284
6307
|
const relationName = whoAsk[1].trim().toLowerCase();
|
|
5285
6308
|
const rawObject = whoAsk[2].trim();
|
|
@@ -6893,7 +7916,7 @@ async function compareAnswer(query, { graph, config, source }) {
|
|
|
6893
7916
|
const cmp = renderCompare(g, indA, indB);
|
|
6894
7917
|
if (!cmp) {
|
|
6895
7918
|
return {
|
|
6896
|
-
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")}.`,
|
|
6897
7920
|
ents: [indA, indB],
|
|
6898
7921
|
};
|
|
6899
7922
|
}
|
|
@@ -7071,7 +8094,227 @@ async function entityOfKindInText(graph, expectedClass, answerText) {
|
|
|
7071
8094
|
* otherwise the unchanged dispatchTool path (which also yields the no-graph error).
|
|
7072
8095
|
* A hit updates the focus to the resolved object. Grammar miss / ToolError → a
|
|
7073
8096
|
* normal answer, never a crash. */
|
|
7074
|
-
|
|
8097
|
+
/** Load the taught domain for the plan lane: fact rows + rule rows compiled
|
|
8098
|
+
* through src/domain.mjs. Fresh-loads memory (never the turn cache) because
|
|
8099
|
+
* the caller may have just written snapshot rows this same turn. */
|
|
8100
|
+
async function loadPlanContext(memoryDir) {
|
|
8101
|
+
const { loadMemory, readFactRows, readRuleRows } = await import("./memory/core.mjs");
|
|
8102
|
+
const { compileDomain, stateFromFacts } = await import("./domain.mjs");
|
|
8103
|
+
const payload = await loadMemory(memoryDir);
|
|
8104
|
+
const factRows = readFactRows(payload);
|
|
8105
|
+
const ruleRows = readRuleRows(payload);
|
|
8106
|
+
const domain = compileDomain(factRows, ruleRows);
|
|
8107
|
+
const state = stateFromFacts(factRows, domain);
|
|
8108
|
+
return { factRows, ruleRows, domain, state };
|
|
8109
|
+
}
|
|
8110
|
+
|
|
8111
|
+
/** Human label for a grounded action: name "move onto" + disk-1 + peg-c →
|
|
8112
|
+
* "move disk-1 onto peg-c". */
|
|
8113
|
+
function actionLabel(name, subject, target) {
|
|
8114
|
+
const sp = String(name).split(/\s+/);
|
|
8115
|
+
const verb = sp[0] || "move";
|
|
8116
|
+
const prep = sp.slice(1).join(" ") || "onto";
|
|
8117
|
+
return `${verb} ${subject} ${prep} ${target}`;
|
|
8118
|
+
}
|
|
8119
|
+
|
|
8120
|
+
/** THE PLAN LANE — the closed goal/solve/legal-moves recognizers over the
|
|
8121
|
+
* taught action rules (PLAN_HANOI's chat surface). Returns
|
|
8122
|
+
* { text, via, deduced, note, plan? } or null when the query is none of the
|
|
8123
|
+
* three shapes. Mutates planHolder.state (the session's plan slot). */
|
|
8124
|
+
async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", }) {
|
|
8125
|
+
const q = String(query).trim();
|
|
8126
|
+
|
|
8127
|
+
const thatGoal = q.match(GOAL_TEACH_RE);
|
|
8128
|
+
const goalMatch = thatGoal || q.match(GOAL_TEACH_INFINITIVE_RE);
|
|
8129
|
+
if (goalMatch) {
|
|
8130
|
+
const { normFactTerm } = await import("./memory/core.mjs");
|
|
8131
|
+
const verb = await verbLemma(goalMatch[3]);
|
|
8132
|
+
if (!verb) {
|
|
8133
|
+
return {
|
|
8134
|
+
text: `I can't reduce "${goalMatch[3]}" to a verb for that goal — try the plain form (e.g. "rests").`,
|
|
8135
|
+
via: "plan", deduced: "record the goal state for a later plan", note: "GOAL frame — verb lemma unavailable, honest decline",
|
|
8136
|
+
};
|
|
8137
|
+
}
|
|
8138
|
+
const spec = {
|
|
8139
|
+
universal: !!goalMatch[1],
|
|
8140
|
+
term: normFactTerm(goalMatch[2]),
|
|
8141
|
+
predicate: `${verb}-${goalMatch[4].toLowerCase()}`,
|
|
8142
|
+
object: normFactTerm(goalMatch[5]),
|
|
8143
|
+
};
|
|
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()}`;
|
|
8149
|
+
const prev = planHolder.state && Array.isArray(planHolder.state.goals) && !planHolder.state.done ? planHolder.state : null;
|
|
8150
|
+
planHolder.state = {
|
|
8151
|
+
goals: [...(prev?.goals ?? []), spec],
|
|
8152
|
+
goalTexts: [...(prev?.goalTexts ?? []), tail],
|
|
8153
|
+
actions: null, states: null, stepGoals: null, cursor: 0, done: false,
|
|
8154
|
+
};
|
|
8155
|
+
const n = planHolder.state.goals.length;
|
|
8156
|
+
return {
|
|
8157
|
+
text: `noted — the goal is that ${tail}.${n > 1 ? ` (${n} goals held)` : ""} Say "solve it" when the state is taught.`,
|
|
8158
|
+
via: "plan", deduced: "record the goal state for a later plan",
|
|
8159
|
+
note: "GOAL frame — goal spec accumulated on the session plan slot",
|
|
8160
|
+
};
|
|
8161
|
+
}
|
|
8162
|
+
|
|
8163
|
+
const wantsSolve = PLAN_SOLVE_RE.test(q);
|
|
8164
|
+
const wantsLegal = LEGAL_MOVES_RE.test(q);
|
|
8165
|
+
if (!wantsSolve && !wantsLegal) return null;
|
|
8166
|
+
|
|
8167
|
+
let ctx;
|
|
8168
|
+
try {
|
|
8169
|
+
ctx = await loadPlanContext(memoryDir);
|
|
8170
|
+
} catch (err) {
|
|
8171
|
+
return { text: `I can't read the taught domain: ${err?.message ?? err}`, via: "plan", deduced: "plan a move sequence", note: "plan lane — domain load failed" };
|
|
8172
|
+
}
|
|
8173
|
+
const { domain, state, factRows } = ctx;
|
|
8174
|
+
if (!domain.actions.length) {
|
|
8175
|
+
return {
|
|
8176
|
+
text: `no action rules taught yet — teach the game first (e.g. "you can move a disk onto a peg").`,
|
|
8177
|
+
via: "plan", deduced: "plan a move sequence (no action rules yet)", note: "plan lane — honest decline: no action rules",
|
|
8178
|
+
};
|
|
8179
|
+
}
|
|
8180
|
+
if (!state.length) {
|
|
8181
|
+
return {
|
|
8182
|
+
text: `no current state taught yet — state the board first (e.g. "disk-1 rests on peg-a").`,
|
|
8183
|
+
via: "plan", deduced: "plan a move sequence (no state yet)", note: "plan lane — honest decline: empty state",
|
|
8184
|
+
};
|
|
8185
|
+
}
|
|
8186
|
+
const { movesFromRules, stateKeyFor, compileGoal, PlanBudgetError } = await import("./domain.mjs");
|
|
8187
|
+
|
|
8188
|
+
if (wantsLegal) {
|
|
8189
|
+
let moves;
|
|
8190
|
+
try {
|
|
8191
|
+
moves = movesFromRules(state, domain);
|
|
8192
|
+
} catch (err) {
|
|
8193
|
+
if (err instanceof PlanBudgetError) {
|
|
8194
|
+
return { text: `too many possible moves to enumerate here (${err.message}) — narrow the classes involved.`, via: "plan", deduced: "list the legal moves (budget exceeded)", note: "plan lane — budget decline" };
|
|
8195
|
+
}
|
|
8196
|
+
throw err;
|
|
8197
|
+
}
|
|
8198
|
+
if (!moves.length) {
|
|
8199
|
+
return { text: "no legal moves from the current state.", via: "plan", deduced: "list the legal moves (none)", note: "plan lane — legal moves: none" };
|
|
8200
|
+
}
|
|
8201
|
+
const lines = moves.map((m, i) => ` ${i + 1}. ${actionLabel(m.action.name, m.action.subject, m.action.target)}`);
|
|
8202
|
+
return {
|
|
8203
|
+
text: `${moves.length} legal move${moves.length === 1 ? "" : "s"} from here:\n${lines.join("\n")}`,
|
|
8204
|
+
via: "plan", deduced: "list the legal moves from the current state",
|
|
8205
|
+
note: "plan lane — movesFromRules over the current snapshot, one ply, no search",
|
|
8206
|
+
};
|
|
8207
|
+
}
|
|
8208
|
+
|
|
8209
|
+
// "solve it" — the full search.
|
|
8210
|
+
if (!planHolder.state?.goals?.length) {
|
|
8211
|
+
return {
|
|
8212
|
+
text: `no goal set yet — teach one first (e.g. "the goal is that every disk rests on peg-c").`,
|
|
8213
|
+
via: "plan", deduced: "plan a move sequence (no goal yet)", note: "plan lane — honest decline: no goal",
|
|
8214
|
+
};
|
|
8215
|
+
}
|
|
8216
|
+
const goals = planHolder.state.goals;
|
|
8217
|
+
const goalText = planHolder.state.goalTexts.join("; ");
|
|
8218
|
+
let isGoal;
|
|
8219
|
+
try {
|
|
8220
|
+
isGoal = compileGoal(goals, domain);
|
|
8221
|
+
} catch (err) {
|
|
8222
|
+
return { text: `I can't compile that goal: ${err?.message ?? err}`, via: "plan", deduced: "plan a move sequence (uncompilable goal)", note: "plan lane — goal compile decline" };
|
|
8223
|
+
}
|
|
8224
|
+
const { findActionPath } = await import("./planning.mjs");
|
|
8225
|
+
let found;
|
|
8226
|
+
try {
|
|
8227
|
+
found = findActionPath(state, isGoal, (s) => movesFromRules(s, domain), { maxDepth: 300, stateKey: stateKeyFor });
|
|
8228
|
+
} catch (err) {
|
|
8229
|
+
if (err instanceof PlanBudgetError) {
|
|
8230
|
+
return { text: `the search space is too large (${err.message}) — narrow the classes involved.`, via: "plan", deduced: "plan a move sequence (budget exceeded)", note: "plan lane — budget decline" };
|
|
8231
|
+
}
|
|
8232
|
+
throw err;
|
|
8233
|
+
}
|
|
8234
|
+
if (!found) {
|
|
8235
|
+
return {
|
|
8236
|
+
text: `no plan found within 300 moves from the current state to: ${goalText}.`,
|
|
8237
|
+
via: "plan", deduced: "plan a move sequence (no path)", note: "plan lane — honest miss: findActionPath returned null",
|
|
8238
|
+
};
|
|
8239
|
+
}
|
|
8240
|
+
const n = found.actions.length;
|
|
8241
|
+
const actions = found.actions.map((a) => ({
|
|
8242
|
+
name: a.name, subject: a.subject, target: a.target,
|
|
8243
|
+
label: actionLabel(a.name, a.subject, a.target),
|
|
8244
|
+
}));
|
|
8245
|
+
const stepGoals = actions.map((a, i) =>
|
|
8246
|
+
`${a.label} (step ${i + 1} of ${n}, working toward: ${goalText})`);
|
|
8247
|
+
const renderHints = {};
|
|
8248
|
+
const ordering = [];
|
|
8249
|
+
for (const r of factRows) {
|
|
8250
|
+
if (r.predicate === "mgx:rendersAs") renderHints[r.subject] = r.object;
|
|
8251
|
+
else if (/-than$/.test(r.predicate)) ordering.push({ subject: r.subject, predicate: r.predicate, object: r.object });
|
|
8252
|
+
}
|
|
8253
|
+
const plan = {
|
|
8254
|
+
actions, states: found.states, stepGoals,
|
|
8255
|
+
goal: { text: goalText, specs: goals },
|
|
8256
|
+
domain: { classMembers: domain.classMembers, ordering, renderHints },
|
|
8257
|
+
};
|
|
8258
|
+
planHolder.state = {
|
|
8259
|
+
...planHolder.state, actions, states: found.states, stepGoals, cursor: 0, done: false, goalText,
|
|
8260
|
+
};
|
|
8261
|
+
const ruleNames = [...new Set(domain.actions.map((a) => a.name))].join('", "');
|
|
8262
|
+
const moveLines = actions.map((a, i) => ` ${i + 1}. ${a.label}`);
|
|
8263
|
+
const text = n === 0
|
|
8264
|
+
? `the goal already holds — nothing to do.`
|
|
8265
|
+
: `plan found — ${n} move${n === 1 ? "" : "s"} (shortest):\n${moveLines.join("\n")}\n\n` +
|
|
8266
|
+
`because — you taught me the "${ruleNames}" rule${domain.actions.length === 1 ? "" : "s"}` +
|
|
8267
|
+
`${ordering.length ? ` and ${ordering.length} ordering fact${ordering.length === 1 ? "" : "s"}` : ""}. ` +
|
|
8268
|
+
`Say "next" to make move 1, or ask "what moves are legal now".`;
|
|
8269
|
+
return {
|
|
8270
|
+
text, via: "plan",
|
|
8271
|
+
deduced: `plan a move sequence from the current state to the goal (${n} move${n === 1 ? "" : "s"})`,
|
|
8272
|
+
note: "plan lane — compileDomain + findActionPath over the taught rules; plan held on the session slot",
|
|
8273
|
+
plan,
|
|
8274
|
+
};
|
|
8275
|
+
}
|
|
8276
|
+
|
|
8277
|
+
/** Execute the active plan's next move: append the successor snapshot's rows
|
|
8278
|
+
* as @stepK facts, advance the cursor, and on the final step re-read the
|
|
8279
|
+
* store and confirm the goal from the WRITTEN facts (never assumed). */
|
|
8280
|
+
async function executePlanStep(planHolder, { memoryDir, sessionId = "" }) {
|
|
8281
|
+
const ps = planHolder.state;
|
|
8282
|
+
const k = ps.cursor + 1;
|
|
8283
|
+
const action = ps.actions[ps.cursor];
|
|
8284
|
+
const rows = ps.states[k];
|
|
8285
|
+
const { appendFact, loadMemory, readFactRows } = await import("./memory/core.mjs");
|
|
8286
|
+
for (const row of rows) {
|
|
8287
|
+
await appendFact(memoryDir, {
|
|
8288
|
+
subject: `${row.subject}@step${k}`, predicate: row.predicate, object: row.object,
|
|
8289
|
+
provenance: `plan:${sessionId || "chat"}:step${k}`,
|
|
8290
|
+
});
|
|
8291
|
+
}
|
|
8292
|
+
planHolder.state = { ...ps, cursor: k };
|
|
8293
|
+
const boardLine = rows.map((r) => `${r.subject} ${predicatePhrase(r.predicate)} ${r.object}`).join("; ");
|
|
8294
|
+
if (k < ps.actions.length) {
|
|
8295
|
+
return {
|
|
8296
|
+
text: `moved — ${action.label} (step ${k} of ${ps.actions.length}). board@step${k}: ${boardLine}`,
|
|
8297
|
+
deduced: ps.stepGoals[k] ? ps.stepGoals[k] : `continue the plan (step ${k + 1} of ${ps.actions.length})`,
|
|
8298
|
+
};
|
|
8299
|
+
}
|
|
8300
|
+
// Final step: confirm the goal against the store, from the written facts.
|
|
8301
|
+
const { compileDomain, stateFromFacts, compileGoal } = await import("./domain.mjs");
|
|
8302
|
+
const { readRuleRows } = await import("./memory/core.mjs");
|
|
8303
|
+
const payload = await loadMemory(memoryDir);
|
|
8304
|
+
const factRows = readFactRows(payload);
|
|
8305
|
+
const domain = compileDomain(factRows, readRuleRows(payload));
|
|
8306
|
+
const finalState = stateFromFacts(factRows, domain);
|
|
8307
|
+
const holds = compileGoal(ps.goals, domain)(finalState);
|
|
8308
|
+
planHolder.state = { ...planHolder.state, done: true };
|
|
8309
|
+
return {
|
|
8310
|
+
text: holds
|
|
8311
|
+
? `moved — ${action.label} (step ${k} of ${ps.actions.length}). board@step${k}: ${boardLine}\n\ndone — ${ps.goalText} (checked against board@step${k}'s written facts, not assumed).`
|
|
8312
|
+
: `moved — ${action.label} (step ${k} of ${ps.actions.length}). board@step${k}: ${boardLine}\n\nBUT the goal does NOT hold against the written facts — the plan or the state drifted; re-teach the state and solve again.`,
|
|
8313
|
+
deduced: holds ? `goal reached — ${ps.goalText} (${k} of ${k} steps)` : "plan finished but the goal check failed",
|
|
8314
|
+
};
|
|
8315
|
+
}
|
|
8316
|
+
|
|
8317
|
+
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null, tel = null, biasByBundle = {}, cache = null, vocabAntecedent = null, planHolder = null }) {
|
|
7075
8318
|
const ts = new Date().toISOString();
|
|
7076
8319
|
// DISCOURSE ANAPHORA: a follow-up like "which of those are tested" / "count
|
|
7077
8320
|
// them" filters or counts the PREVIOUS answer's entity set, threaded as
|
|
@@ -7147,8 +8390,19 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7147
8390
|
answer = content;
|
|
7148
8391
|
if (envJson) { try { envelope = JSON.parse(envJson); } catch { envelope = null; } }
|
|
7149
8392
|
} catch (e) {
|
|
7150
|
-
|
|
7151
|
-
|
|
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}`);
|
|
7152
8406
|
}
|
|
7153
8407
|
// NARRATE: the direct parse/traversal receipt, straight off ask()'s own
|
|
7154
8408
|
// envelope, with zero extra instrumentation of ask.mjs: `parsed` is the
|
|
@@ -7300,6 +8554,28 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7300
8554
|
note(trace, `lane: (1) META/SELF — bare self/session question recognized, answered via="${meta.via}"`);
|
|
7301
8555
|
}
|
|
7302
8556
|
}
|
|
8557
|
+
// (1p) PLAN — the goal/solve/legal-moves recognizers over taught action
|
|
8558
|
+
// rules. Sits ABOVE the conversational catch-all: "solve it" is three
|
|
8559
|
+
// short words and isConversational() would otherwise claim it into the
|
|
8560
|
+
// orientation card before this lane ever ran.
|
|
8561
|
+
let planResult = null;
|
|
8562
|
+
if (!handled && miss && memoryDir && planHolder) {
|
|
8563
|
+
const planLane = await planLaneAnswer(query, { memoryDir, planHolder, sessionId });
|
|
8564
|
+
if (planLane) {
|
|
8565
|
+
answer = planLane.text; via = planLane.via; recordMiss = false; handled = true;
|
|
8566
|
+
if (planLane.plan) planResult = planLane.plan;
|
|
8567
|
+
if (planLane.deduced) {
|
|
8568
|
+
deduced = planLane.deduced;
|
|
8569
|
+
note(trace, `goal: ${deduced} (revised — the plan lane answered)`);
|
|
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;
|
|
8576
|
+
note(trace, `lane: (1p) PLAN — ${planLane.note}`);
|
|
8577
|
+
}
|
|
8578
|
+
}
|
|
7303
8579
|
// "what about X" with a genuine PRIOR turn to continue is exempt from the
|
|
7304
8580
|
// conversational catch-all even when short/non-codeish: isConversational()
|
|
7305
8581
|
// can't see that discourseRewrite/describeWrapperAnswer haven't had their
|
|
@@ -7331,6 +8607,67 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7331
8607
|
// STACCATO_PRONOUN_RE-no-focus branch ALWAYS returns a tailored nudge for
|
|
7332
8608
|
// this exact shape, never null.
|
|
7333
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
|
+
}
|
|
7334
8671
|
// A vague relation touch ("what about cochange", "tell me about cochange",
|
|
7335
8672
|
// the staccato chain continuation "and cochange?") whose relation word has NO
|
|
7336
8673
|
// bare single-word VERB_TO_KIND form of its own needs the SAME deferral as
|
|
@@ -7358,7 +8695,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7358
8695
|
} catch { /* leave false — the ordinary path decides */ }
|
|
7359
8696
|
}
|
|
7360
8697
|
}
|
|
7361
|
-
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;
|
|
7362
8699
|
// A turn whose pronoun was bound to a vocabulary antecedent is PROVABLY a
|
|
7363
8700
|
// fact question ("can it bark" → "can dog bark") — never conversational,
|
|
7364
8701
|
// however short. Without this, the substituted 3-worder still trips
|
|
@@ -7394,12 +8731,23 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7394
8731
|
// gets a turn.
|
|
7395
8732
|
const reversePredicateShape = WHAT_USED_FOR_RE.test(gateQuery)
|
|
7396
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);
|
|
7397
8741
|
let bareMetaHit = null;
|
|
7398
|
-
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape)) {
|
|
8742
|
+
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape || capabilityAskShape)) {
|
|
7399
8743
|
if (memoryDir) {
|
|
7400
8744
|
bareMetaHit = (await factAnswer(memoryDir, gateQuery, envelope, miss, biasByBundle, cache))
|
|
7401
8745
|
?? (await factReadBack(memoryDir, gateQuery, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
|
|
7402
|
-
|
|
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;
|
|
7403
8751
|
// A bare "what is X" with NO taught fact but a KNOWN curated corpus term
|
|
7404
8752
|
// ("what is cache", no article) needs the same "only diverts on a REAL
|
|
7405
8753
|
// hit" treatment — curatedDefinitionAnswer otherwise only ever runs once
|
|
@@ -7436,10 +8784,30 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7436
8784
|
}
|
|
7437
8785
|
if (bareMetaHit) {
|
|
7438
8786
|
answer = bareMetaHit.replace ? bareMetaHit.text : `${answer}\n${bareMetaHit.text}`;
|
|
7439
|
-
|
|
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;
|
|
7440
8792
|
if (bareMetaHit.pending) factPending = bareMetaHit.pending;
|
|
7441
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");
|
|
7442
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)");
|
|
7443
8811
|
} else if (isConversationalCandidate) {
|
|
7444
8812
|
// A conversational miss (a greeting, "what can you do", a very short non-code
|
|
7445
8813
|
// line) gets the friendly orientation (module-aware: empty → --repo/tmct init).
|
|
@@ -7466,8 +8834,22 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7466
8834
|
// reified fact is stronger evidence than a transcript echo. Subject-side facts
|
|
7467
8835
|
// first (factAnswer), then the reverse-membership read-back (factReadBack) so an
|
|
7468
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));
|
|
7469
8847
|
const fact = (await factAnswer(memoryDir, query, envelope, miss, biasByBundle, cache))
|
|
7470
|
-
?? (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);
|
|
7471
8853
|
if (fact) {
|
|
7472
8854
|
answer = fact.replace ? fact.text : `${answer}\n${fact.text}`;
|
|
7473
8855
|
// A fact-lane return flagged `miss` is an HONEST MISS in better words
|
|
@@ -7487,7 +8869,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7487
8869
|
// question ("does margo eat ribs") never parses as a structural graph
|
|
7488
8870
|
// query at all.
|
|
7489
8871
|
if (fact.generalVerbQuery) {
|
|
7490
|
-
deduced =
|
|
8872
|
+
deduced = TAUGHT_FACT_LOOKUP_GOAL;
|
|
7491
8873
|
note(trace, `goal: ${deduced} (revised — a general-verb direct-question fact lookup answered this turn)`);
|
|
7492
8874
|
}
|
|
7493
8875
|
} else if (miss) {
|
|
@@ -7608,6 +8990,13 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7608
8990
|
// line instead.
|
|
7609
8991
|
deduced = "teach/remember a new fact";
|
|
7610
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;
|
|
7611
9000
|
}
|
|
7612
9001
|
}
|
|
7613
9002
|
// (4b) #4 AUTHOR lane — "who is <Name>", "what did <Name> touch",
|
|
@@ -7833,7 +9222,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7833
9222
|
// `goal`: the SAME deduced string the debug trace's own "goal:" line
|
|
7834
9223
|
// carries. Only runAsk ever sets this field, so the always-on goal line is
|
|
7835
9224
|
// scoped to real ask-engine turns by construction.
|
|
7836
|
-
return { answer, logLines, record, focus: newFocus, detail, effectiveQuery, goal: deduced };
|
|
9225
|
+
return { answer, logLines, record, focus: newFocus, detail, effectiveQuery, goal: deduced, ...(planResult ? { plan: planResult } : {}) };
|
|
7837
9226
|
}
|
|
7838
9227
|
|
|
7839
9228
|
/** A non-ask, non-dispatch chat turn (count answer, /stats) — the same
|
|
@@ -7947,23 +9336,29 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
7947
9336
|
if (!argText) return mk("/plan needs a request, e.g. `/plan of the modules impacted by X, which are untested`.", { miss: true });
|
|
7948
9337
|
if (!graph) return mk("no graph loaded — /plan needs a code graph to plan over.", { miss: true });
|
|
7949
9338
|
const { buildCapabilityPlanCtx, runCapabilityPlan, declaredCapabilityNames } = await import("./router/drive.mjs");
|
|
7950
|
-
const planCtx = await buildCapabilityPlanCtx({ config, source, tel, graph });
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
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();
|
|
7965
9361
|
}
|
|
7966
|
-
return mk(lines.join("\n"));
|
|
7967
9362
|
}
|
|
7968
9363
|
|
|
7969
9364
|
const spec = COMMANDS[name];
|
|
@@ -8255,7 +9650,7 @@ function vocabAntecedentFrom(last) {
|
|
|
8255
9650
|
return m ? m[1] : null;
|
|
8256
9651
|
}
|
|
8257
9652
|
|
|
8258
|
-
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null } = {}) {
|
|
9653
|
+
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, _noSplit = false } = {}) {
|
|
8259
9654
|
const line = String(input ?? "").trim();
|
|
8260
9655
|
// ONE fresh, empty cache for this turn only — every factRows() reader
|
|
8261
9656
|
// reached from this call shares it, so the first reader computes
|
|
@@ -8297,7 +9692,11 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8297
9692
|
// vocabHint: createSession computes this ONCE per session; a direct
|
|
8298
9693
|
// runTurn() caller that doesn't pass one gets it computed here instead.
|
|
8299
9694
|
const resolvedVocabHint = vocabHint ?? vocabExampleHint(await hasSeededVocabulary(memoryDir));
|
|
8300
|
-
|
|
9695
|
+
// The session's in-progress plan rides a mutable holder: the plan lane and
|
|
9696
|
+
// the PLAN NEXT block below write planHolder.state; every other path leaves
|
|
9697
|
+
// it untouched, and the caller re-threads whatever comes back.
|
|
9698
|
+
const planHolder = { state: planState };
|
|
9699
|
+
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache, vocabAntecedent, planHolder };
|
|
8301
9700
|
// A DISPATCHED turn (count / slash-command / ask) becomes the new "last
|
|
8302
9701
|
// answer" that why/say-more re-renders; a conversational turn does not.
|
|
8303
9702
|
// Every dispatched turn's result passes through finish() here — the LAST
|
|
@@ -8338,6 +9737,21 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8338
9737
|
const convo = vocabAntecedent ? null : conversationalTurn(workingLine, ctx);
|
|
8339
9738
|
if (convo) return withNarration(convo, trace, "casual/social — no graph intent");
|
|
8340
9739
|
|
|
9740
|
+
// PLAN NEXT — "next"/"continue" with an ACTIVE plan executes the plan's
|
|
9741
|
+
// next move as a snapshot write. Checked BEFORE the MORE_RE pager because
|
|
9742
|
+
// MORE_RE owns the same words; with no active plan this block never fires
|
|
9743
|
+
// and paging behaves exactly as before.
|
|
9744
|
+
if (memoryDir && PLAN_NEXT_RE.test(workingLine)
|
|
9745
|
+
&& planHolder.state && !planHolder.state.done
|
|
9746
|
+
&& Array.isArray(planHolder.state.actions) && planHolder.state.cursor < planHolder.state.actions.length) {
|
|
9747
|
+
const step = await executePlanStep(planHolder, { memoryDir, sessionId });
|
|
9748
|
+
note(trace, `goal: ${step.deduced}`);
|
|
9749
|
+
note(trace, "lane: PLAN NEXT — executed the active plan's next move as an @stepK snapshot write");
|
|
9750
|
+
const rec = withLast(plainTurn(workingLine, step.text, { via: "plan", focus }), step.deduced);
|
|
9751
|
+
rec.planState = planHolder.state;
|
|
9752
|
+
return rec;
|
|
9753
|
+
}
|
|
9754
|
+
|
|
8341
9755
|
// "more" — page the remainder of a previous long listing, if one is held. Gated on
|
|
8342
9756
|
// an actual pending remainder so a bare "more" with nothing to continue falls through
|
|
8343
9757
|
// to the ordinary path (an honest miss), never a pretend page.
|
|
@@ -8347,6 +9761,39 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8347
9761
|
return withLast(morePage(workingLine, ctx), "continue viewing a previous long listing");
|
|
8348
9762
|
}
|
|
8349
9763
|
|
|
9764
|
+
// Multi-sentence PLAN pre-split — one message carrying state sentences plus
|
|
9765
|
+
// a goal/trigger ("disk-1 rests on disk-2. … the goal is that …. solve it.")
|
|
9766
|
+
// runs each sentence as its own nested turn, threading focus/last/planState
|
|
9767
|
+
// through, and answers with the final turn's result behind brief receipts.
|
|
9768
|
+
if (!_noSplit && memoryDir) {
|
|
9769
|
+
const sentences = splitSentences(workingLine);
|
|
9770
|
+
if (sentences.length > 1) {
|
|
9771
|
+
const lastSentence = sentences[sentences.length - 1];
|
|
9772
|
+
if (PLAN_SOLVE_RE.test(lastSentence) || GOAL_TEACH_RE.test(lastSentence) || GOAL_TEACH_INFINITIVE_RE.test(lastSentence) || LEGAL_MOVES_RE.test(lastSentence)) {
|
|
9773
|
+
let f = focus; let l = last; let ps = planHolder.state;
|
|
9774
|
+
const receipts = [];
|
|
9775
|
+
let finalRec = null;
|
|
9776
|
+
for (const sentence of sentences) {
|
|
9777
|
+
const r = await runTurn(sentence, {
|
|
9778
|
+
config, source, graph, focus: f, last: l, memoryDir, sessionId, env, lexicon,
|
|
9779
|
+
narrate: false, vocabHint, tel, biasByBundle, planState: ps, _noSplit: true,
|
|
9780
|
+
});
|
|
9781
|
+
f = r.focus ?? f;
|
|
9782
|
+
l = r.last ?? l;
|
|
9783
|
+
if ("planState" in r) ps = r.planState;
|
|
9784
|
+
finalRec = r;
|
|
9785
|
+
receipts.push(String(r.answer ?? "").split("\n")[0]);
|
|
9786
|
+
}
|
|
9787
|
+
const receiptLines = receipts.slice(0, -1).map((t) => `• ${t}`).join("\n");
|
|
9788
|
+
const combined = { ...finalRec, answer: receiptLines ? `${receiptLines}\n\n${finalRec.answer}` : finalRec.answer };
|
|
9789
|
+
combined.planState = ps;
|
|
9790
|
+
combined.focus = f;
|
|
9791
|
+
combined.last = l;
|
|
9792
|
+
return combined;
|
|
9793
|
+
}
|
|
9794
|
+
}
|
|
9795
|
+
}
|
|
9796
|
+
|
|
8350
9797
|
if (workingLine.startsWith("/")) return withLast(await runCommand(workingLine, ctx), "use a specific tool/command directly");
|
|
8351
9798
|
// Declarative ACE sentences ("every module is a artifact") ASSERT into tmct's
|
|
8352
9799
|
// own memory and confirm — they are statements to remember, not graph queries.
|
|
@@ -8359,6 +9806,15 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8359
9806
|
note(trace, "lane: assertTurn — grammar/ace.mjs parseAce matched a full triple with no residue");
|
|
8360
9807
|
return withLast(asserted, "teach/remember a new fact");
|
|
8361
9808
|
}
|
|
9809
|
+
// Bare declarative taxonomy (hyphenated-instance membership, article-led
|
|
9810
|
+
// kind-of) — see bareTaxonomyTeach. Checked here because the ask engine
|
|
9811
|
+
// would otherwise parse these statements as inherits QUESTIONS.
|
|
9812
|
+
const taxonomy = await bareTaxonomyTeach(workingLine, ctx);
|
|
9813
|
+
if (taxonomy) {
|
|
9814
|
+
note(trace, "goal: teach/remember a new fact (bare declarative taxonomy)");
|
|
9815
|
+
note(trace, "lane: bareTaxonomyTeach — hyphenated-instance or article-led kind-of declarative, stored before the ask engine could parse it as a question");
|
|
9816
|
+
return withLast(plainTurn(workingLine, taxonomy.text, { via: taxonomy.via, miss: taxonomy.miss, focus }), "teach/remember a new fact");
|
|
9817
|
+
}
|
|
8362
9818
|
}
|
|
8363
9819
|
// MEMORY-STORE counts first ("how many facts / utterances do you know") — the
|
|
8364
9820
|
// memory graph owns Facts + Utterances, so these are answerable and consistent
|
|
@@ -8415,7 +9871,11 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8415
9871
|
note(trace, "lane: answerCount — a header-count aggregate question, answered mechanically off the graph header, never dispatched to the ask engine");
|
|
8416
9872
|
return withLast(plainTurn(workingLine, count, { via: "count", focus }), "get a count of a graph kind");
|
|
8417
9873
|
}
|
|
8418
|
-
|
|
9874
|
+
{
|
|
9875
|
+
const rec = withLast(await runAsk(workingLine, ctx), "unclear — no goal signal computed by the ask engine");
|
|
9876
|
+
rec.planState = planHolder.state;
|
|
9877
|
+
return rec;
|
|
9878
|
+
}
|
|
8419
9879
|
}
|
|
8420
9880
|
|
|
8421
9881
|
// ---- W3: seedMemory → bootstrap (first run in a graph-less repo) ----
|
|
@@ -8728,6 +10188,7 @@ export async function createSession({
|
|
|
8728
10188
|
let turns = 0;
|
|
8729
10189
|
let focus = null; // the current focus entity ({id,label}) — threaded turn to turn
|
|
8730
10190
|
let last = null; // the last dispatched answer ({query,answer,detail}) — why/say-more re-renders it
|
|
10191
|
+
let planState = null; // the in-progress plan (goals/moves/cursor) — cleared by completion or a fresh goal, never by an aside
|
|
8731
10192
|
let closed = false;
|
|
8732
10193
|
|
|
8733
10194
|
return {
|
|
@@ -8737,6 +10198,7 @@ export async function createSession({
|
|
|
8737
10198
|
// prompt/expand-hint without reaching into runTurn's threading.
|
|
8738
10199
|
get focus() { return focus; },
|
|
8739
10200
|
get lastAnswer() { return last; },
|
|
10201
|
+
get planState() { return planState; },
|
|
8740
10202
|
get turns() { return turns; },
|
|
8741
10203
|
get narrate() { return narrateOn; },
|
|
8742
10204
|
promptFor: () => promptFor(focus),
|
|
@@ -8748,7 +10210,7 @@ export async function createSession({
|
|
|
8748
10210
|
async turn(line) {
|
|
8749
10211
|
let result;
|
|
8750
10212
|
try {
|
|
8751
|
-
result = await runTurn(line, { config, source, graph, focus, last, memoryDir, sessionId, env, lexicon, narrate: narrateOn, vocabHint, tel, biasByBundle });
|
|
10213
|
+
result = await runTurn(line, { config, source, graph, focus, last, memoryDir, sessionId, env, lexicon, narrate: narrateOn, vocabHint, tel, biasByBundle, planState });
|
|
8752
10214
|
} catch (e) {
|
|
8753
10215
|
const ts = new Date().toISOString();
|
|
8754
10216
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -8762,6 +10224,7 @@ export async function createSession({
|
|
|
8762
10224
|
const { answer, logLines, record, focus: nextFocus, last: nextLast, end, narrate: nextNarrate } = result;
|
|
8763
10225
|
focus = nextFocus;
|
|
8764
10226
|
last = nextLast;
|
|
10227
|
+
if ("planState" in result) planState = result.planState;
|
|
8765
10228
|
// /narrate on|off (runCommand) rides the turn RESULT the same way a focus
|
|
8766
10229
|
// update does — apply it to this handle's session-scoped state.
|
|
8767
10230
|
if (typeof nextNarrate === "boolean") narrateOn = nextNarrate;
|
|
@@ -8777,7 +10240,7 @@ export async function createSession({
|
|
|
8777
10240
|
});
|
|
8778
10241
|
await upsertGraph(record.ts);
|
|
8779
10242
|
turns += 1;
|
|
8780
|
-
return { answer, end: Boolean(end), prompt: promptFor(focus) };
|
|
10243
|
+
return { answer, end: Boolean(end), prompt: promptFor(focus), plan: result.plan ?? null };
|
|
8781
10244
|
},
|
|
8782
10245
|
|
|
8783
10246
|
/** End-of-session close: end lines in both artifacts, the final graph upsert
|