@polycode-projects/the-mechanical-code-talker 3.0.5 → 3.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -16
- package/bin/tmct.mjs +1 -1
- package/corpus/child/README.md +2 -2
- package/corpus/child/index.json.gz +0 -0
- package/corpus/child/manifest.json +74 -74
- package/corpus/child/shards/child-00.jsonl.gz +0 -0
- package/corpus/child/shards/child-01.jsonl.gz +0 -0
- package/corpus/child/shards/child-02.jsonl.gz +0 -0
- package/corpus/child/shards/child-03.jsonl.gz +0 -0
- package/corpus/child/shards/child-04.jsonl.gz +0 -0
- package/corpus/child/shards/child-05.jsonl.gz +0 -0
- package/corpus/child/shards/child-06.jsonl.gz +0 -0
- package/corpus/child/shards/child-07.jsonl.gz +0 -0
- package/corpus/child/shards/child-08.jsonl.gz +0 -0
- package/corpus/child/shards/child-09.jsonl.gz +0 -0
- package/corpus/child/shards/child-0a.jsonl.gz +0 -0
- package/corpus/child/shards/child-0b.jsonl.gz +0 -0
- package/corpus/child/shards/child-0c.jsonl.gz +0 -0
- package/corpus/child/shards/child-0d.jsonl.gz +0 -0
- package/corpus/child/shards/child-0e.jsonl.gz +0 -0
- package/corpus/child/shards/child-0f.jsonl.gz +0 -0
- package/corpus/child/shards/child-10.jsonl.gz +0 -0
- package/corpus/child/shards/child-11.jsonl.gz +0 -0
- package/corpus/child/shards/child-12.jsonl.gz +0 -0
- package/corpus/child/shards/child-13.jsonl.gz +0 -0
- package/corpus/child/shards/child-14.jsonl.gz +0 -0
- package/corpus/child/shards/child-15.jsonl.gz +0 -0
- package/corpus/child/shards/child-16.jsonl.gz +0 -0
- package/corpus/child/shards/child-17.jsonl.gz +0 -0
- package/corpus/child/shards/child-18.jsonl.gz +0 -0
- package/corpus/child/shards/child-19.jsonl.gz +0 -0
- package/corpus/child/shards/child-1a.jsonl.gz +0 -0
- package/corpus/child/shards/child-1b.jsonl.gz +0 -0
- package/corpus/child/shards/child-1c.jsonl.gz +0 -0
- package/corpus/child/shards/child-1d.jsonl.gz +0 -0
- package/corpus/child/shards/child-1e.jsonl.gz +0 -0
- package/corpus/child/shards/child-1f.jsonl.gz +0 -0
- package/corpus/conceptnet/quality-filter.mjs +28 -4
- package/corpus/tier2/generate.mjs +4 -8
- package/corpus/tier2/human-large.jsonl +0 -2
- package/corpus/tier2/human-medium.jsonl +0 -2
- package/corpus/tier2/manifest.json +6 -6
- package/data/templates/constructions/digest-sentence-structures.toml +146 -1
- package/package.json +18 -15
- package/src/adapters/corpus/digest-bank.mjs +15 -3
- package/src/domain/answer-variants.json +1 -1
- package/src/domain/ask-vocab.mjs +4 -0
- package/src/domain/ask.mjs +101 -12
- package/src/domain/codegraph.mjs +25 -8
- package/src/domain/digest/compose.mjs +142 -10
- package/src/domain/digest/config.json +5 -0
- package/src/domain/digest/structures.mjs +39 -14
- package/src/domain/interpret/normalize.mjs +2 -2
- package/src/domain/reference-pack.mjs +12 -3
- package/src/domain/router/call-validator.mjs +1 -1
- package/src/domain/router/drive.mjs +2 -2
- package/src/domain/router/set-algebra.mjs +1 -1
- package/src/services/adventure.mjs +34 -19
- package/src/services/chat-page-viz.mjs +16 -5
- package/src/services/chat.mjs +290 -88
- package/src/services/extract-facts.mjs +46 -4
- package/src/services/index.mjs +6 -3
- package/src/services/ledger-viz.mjs +1 -1
- package/src/services/research.mjs +80 -3
- package/src/surfaces/web/chat-browser-entry.mjs +23 -3
- package/src/surfaces/web/digest-client.mjs +8 -3
- package/src/surfaces/web/graph-ask-browser-entry.mjs +41 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +113 -113
package/src/services/chat.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { join, dirname } from "node:path";
|
|
21
21
|
import { dispatchTool, loadGraph, TOOLS } from "../tools/server.mjs";
|
|
22
22
|
import { ToolError } from "../adapters/config.mjs";
|
|
23
|
-
import { parseEntities, edgesOfKind, moduleCountOf, renderAuthorCard, renderAuthorTouches, renderCommitAuthor, resolveSymbol, renderCompare } from "../domain/codegraph.mjs";
|
|
23
|
+
import { parseEntities, edgesOfKind, moduleCountOf, packageCounts, modulesOf, renderAuthorCard, renderAuthorTouches, renderCommitAuthor, resolveSymbol, renderCompare } from "../domain/codegraph.mjs";
|
|
24
24
|
import { classDisplayName, DYNAMIC_TAIL_OK_RE } from "../domain/ask.mjs";
|
|
25
25
|
import { emptyRecord as emptyDiscourseRecord, advanceTurn as advanceDiscourseTurn, register as registerReferent, bind as bindDiscourseForm } from "../domain/discourse.mjs";
|
|
26
26
|
import { uuidv7 } from "../adapters/uuid.mjs";
|
|
@@ -273,9 +273,12 @@ function withNarration(result, trace, fallbackGoal) {
|
|
|
273
273
|
* above. Appending (not prepending) keeps the many tests that pin composed
|
|
274
274
|
* answers with a start-anchored regex intact.
|
|
275
275
|
*
|
|
276
|
-
* `result.goal` is set by runAsk
|
|
277
|
-
* (GOAL_BY_COMMAND, below)
|
|
278
|
-
*
|
|
276
|
+
* `result.goal` is set by runAsk, by runCommand's own mk()
|
|
277
|
+
* (GOAL_BY_COMMAND, below), and by the plainTurn call sites that already
|
|
278
|
+
* hold a precise goal string (a teach confirmation, a taxonomy/SKOS or
|
|
279
|
+
* memory-store lookup, a plan-lane step, and runAsk's own honest-decline
|
|
280
|
+
* early returns). A plain numeric count is the one turn type that stays
|
|
281
|
+
* silent — it never sets the field, so this is a no-op there by
|
|
279
282
|
* construction. Also a no-op when `result.goal` is null/empty, so an
|
|
280
283
|
* unclear turn never grows a "Goal (inferred): unclear" line.
|
|
281
284
|
*
|
|
@@ -459,6 +462,7 @@ const COUNT_NOUNS = {
|
|
|
459
462
|
class: "Class", classes: "Class",
|
|
460
463
|
function: "Function", functions: "Function", func: "Function", funcs: "Function",
|
|
461
464
|
module: "Module", modules: "Module", file: "Module", files: "Module",
|
|
465
|
+
package: "Package", packages: "Package",
|
|
462
466
|
method: "Method", methods: "Method",
|
|
463
467
|
attribute: "Attribute", attributes: "Attribute",
|
|
464
468
|
variable: "GlobalVariable", variables: "GlobalVariable", global: "GlobalVariable", globals: "GlobalVariable",
|
|
@@ -469,18 +473,25 @@ const COUNT_NOUNS = {
|
|
|
469
473
|
/** class → [singular, plural] display noun, for echoing a count back in English. */
|
|
470
474
|
const CLASS_LABELS = {
|
|
471
475
|
Class: ["class", "classes"], Function: ["function", "functions"],
|
|
472
|
-
Module: ["module", "modules"],
|
|
476
|
+
Module: ["module", "modules"], Package: ["package", "packages"],
|
|
477
|
+
Method: ["method", "methods"],
|
|
473
478
|
Attribute: ["attribute", "attributes"], GlobalVariable: ["variable", "variables"],
|
|
474
479
|
Commit: ["commit", "commits"], Session: ["session", "sessions"],
|
|
475
480
|
};
|
|
476
481
|
const classNoun = (cls, n) => { const [s, p] = CLASS_LABELS[cls] || [cls, `${cls}s`]; return n === 1 ? s : p; };
|
|
477
482
|
|
|
478
|
-
/** Count individuals of a class in the loaded graph (live, not the header field).
|
|
479
|
-
|
|
483
|
+
/** Count individuals of a class in the loaded graph (live, not the header field).
|
|
484
|
+
* No individual is ever stored with class "Package" — packages are the
|
|
485
|
+
* directories grouping the modules, derived the same way the architecture map
|
|
486
|
+
* derives them. */
|
|
487
|
+
const countClass = (graph, cls) => (cls === "Package"
|
|
488
|
+
? packageCounts(modulesOf(graph)).size
|
|
489
|
+
: graph.individuals.filter((i) => (i.class || "") === cls).length);
|
|
480
490
|
|
|
481
491
|
/** The classes this graph can actually count, as a human list ("classes, functions, …"). */
|
|
482
492
|
function countableKinds(graph) {
|
|
483
493
|
const present = new Set(graph.individuals.map((i) => i.class).filter(Boolean));
|
|
494
|
+
if (present.has("Module")) present.add("Package");
|
|
484
495
|
return Object.keys(CLASS_LABELS).filter((c) => present.has(c)).map((c) => CLASS_LABELS[c][1]);
|
|
485
496
|
}
|
|
486
497
|
|
|
@@ -917,7 +928,7 @@ async function answerMemoryClassQuery(memoryDir, query) {
|
|
|
917
928
|
let mem;
|
|
918
929
|
try { mem = await loadMemory(memoryDir); } catch { return null; }
|
|
919
930
|
const inds = (mem.individuals || []).filter((i) => (i.class || "") === cls);
|
|
920
|
-
if (countM) return { text: `${inds.length} ${inds.length === 1 ? plural.replace(/s$/, "") : plural}
|
|
931
|
+
if (countM) return { text: `${inds.length} ${inds.length === 1 ? plural.replace(/s$/, "") : plural}.`, kind: "count" };
|
|
921
932
|
if (!inds.length) return { text: `I don't have any ${plural} stored yet.`, miss: true };
|
|
922
933
|
const factByLabel = cls === "Fact" ? new Map(readFactRows(mem).map((r) => [r.id, r])) : new Map();
|
|
923
934
|
const lines = inds.map((ind) => memoryClassLine(cls, ind, factByLabel));
|
|
@@ -1076,13 +1087,15 @@ const CAPABILITY_PHRASES = [
|
|
|
1076
1087
|
/^(?:what(?:'s|s|\s+is)|give me|show me|gimme) the big picture(?:\s+(?:here|(?:on|of|for|about)\s+(?:this|the)\s+(?:app|codebase|repo|repository|project|code)))?\??$/i,
|
|
1077
1088
|
/^(?:give me|what's) the lay of the land\??$/i,
|
|
1078
1089
|
// "give me an overview" / "an overview" — the plain-word sibling of "the
|
|
1079
|
-
// big picture" just above
|
|
1080
|
-
//
|
|
1081
|
-
//
|
|
1082
|
-
//
|
|
1083
|
-
//
|
|
1084
|
-
//
|
|
1085
|
-
|
|
1090
|
+
// big picture" just above. Without a closed entry the word "overview"
|
|
1091
|
+
// prose-matches real symbols in an indexed graph (moduleOverviewText) and
|
|
1092
|
+
// the describe rescue dumps that symbol's card. The detailed forms ("give
|
|
1093
|
+
// me a detailed overview of X") carry a mandatory "detailed"+of-term and
|
|
1094
|
+
// stay with the completions rescue, untouched by this anchor. An explicit
|
|
1095
|
+
// of-this-repo tail is NOT matched here: that names the repo as the
|
|
1096
|
+
// subject, so ARCH_OVERVIEW_PHRASES answers it with the architecture map
|
|
1097
|
+
// instead of this card.
|
|
1098
|
+
/^(?:(?:can|could|would) you\s+)?(?:give me|show me|gimme)\s+an overview(?:\s+here)?\??$/i,
|
|
1086
1099
|
/^an overview(?:\s+please)?\??$/i,
|
|
1087
1100
|
// "what have we got here"/"what've we got here" — a casual, self-answering
|
|
1088
1101
|
// opener (matches after a leading "so" strips via LEADING_CONNECTIVE_RE,
|
|
@@ -1560,15 +1573,8 @@ const BYE = new Set([
|
|
|
1560
1573
|
"bye", "goodbye", "quit", "exit", "see ya", "see you", "cya", "later", "farewell",
|
|
1561
1574
|
"peace", "peace out", "im off", "i'm off", "gtg", "gotta go", "catch you later",
|
|
1562
1575
|
"farewell then",
|
|
1563
|
-
// "gtg thx" — the SAME "gtg" farewell above, immediately followed by a
|
|
1564
|
-
// thanks word with no delimiter between them (so farewellOrThanksSignal's
|
|
1565
|
-
// comma/semicolon clause split never sees two clauses to work with).
|
|
1566
|
-
// Whole-phrase entry rather than a general "bye word + thanks word, no
|
|
1567
|
-
// delimiter" mechanism — closed and hand-curated, this exact reported
|
|
1568
|
-
// phrasing only.
|
|
1569
|
-
"gtg thx",
|
|
1570
1576
|
// "good day to you" deliberately does NOT live here: it's a formal-register
|
|
1571
|
-
// GREETING, not a farewell.
|
|
1577
|
+
// GREETING, not a farewell. farewellClause is checked before GREET in
|
|
1572
1578
|
// conversationalTurn, so having it here would silently end the session on
|
|
1573
1579
|
// a plain formal greeting — every turn piped after it dropped with no log
|
|
1574
1580
|
// entry, a worse outcome than any wall.
|
|
@@ -1674,15 +1680,39 @@ const CLAUSE_SPLIT_RE = /\s*[,;]\s*(?:and\s+)?|\s+and\s+/;
|
|
|
1674
1680
|
function conversationalClauses(q) {
|
|
1675
1681
|
return q.split(CLAUSE_SPLIT_RE).map((c) => c.trim()).filter(Boolean);
|
|
1676
1682
|
}
|
|
1677
|
-
/**
|
|
1678
|
-
*
|
|
1679
|
-
*
|
|
1680
|
-
*
|
|
1681
|
-
*
|
|
1682
|
-
|
|
1683
|
+
/** A thanks phrase and a bye phrase butted together with NO delimiter between
|
|
1684
|
+
* them, in either order ("gtg thx", "thanks bye"). conversationalClauses
|
|
1685
|
+
* splits on a comma, semicolon or a standalone "and", so these lines arrive
|
|
1686
|
+
* as one clause and match neither closed set whole. Splitting at each word
|
|
1687
|
+
* boundary and matching the two halves against their own sets keeps both
|
|
1688
|
+
* halves closed — no new literal, and no phrase is recognized here that
|
|
1689
|
+
* isn't already recognized alone. Without this the leading thanks word reads
|
|
1690
|
+
* as a bare subject noun and the farewell reads as its verb, so "thanks bye"
|
|
1691
|
+
* parses as a habitual-teach ("a thank can bye"). */
|
|
1692
|
+
function gluedThanksBye(clause) {
|
|
1693
|
+
const words = clause.split(/\s+/).filter(Boolean);
|
|
1694
|
+
for (let split = 1; split < words.length; split += 1) {
|
|
1695
|
+
const head = words.slice(0, split).join(" ");
|
|
1696
|
+
const tail = words.slice(split).join(" ");
|
|
1697
|
+
const headIsBye = !!closedOrCollapsed(head, BYE, BYE_COLLAPSED);
|
|
1698
|
+
const tailIsBye = !!closedOrCollapsed(tail, BYE, BYE_COLLAPSED);
|
|
1699
|
+
const headIsThanks = !!closedOrCollapsed(head, THANKS, THANKS_COLLAPSED);
|
|
1700
|
+
const tailIsThanks = !!closedOrCollapsed(tail, THANKS, THANKS_COLLAPSED);
|
|
1701
|
+
if ((headIsBye && tailIsThanks) || (headIsThanks && tailIsBye)) return true;
|
|
1702
|
+
}
|
|
1703
|
+
return false;
|
|
1704
|
+
}
|
|
1705
|
+
/** Does this clause end the session? The closed BYE set, or informal
|
|
1706
|
+
* reduplication of one of its entries ("bye bye", "no no" — general, not
|
|
1707
|
+
* specific to any one word), or a thanks word glued to a bye word. Shared by
|
|
1708
|
+
* the single-clause whole-line check and the multi-clause scan below, so
|
|
1709
|
+
* "bye bye" resolves the same way whether or not a comma follows it. Bye wins
|
|
1710
|
+
* over thanks: a farewell should end the session even alongside gratitude. */
|
|
1711
|
+
function farewellClause(clause) {
|
|
1683
1712
|
if (closedOrCollapsed(clause, BYE, BYE_COLLAPSED)) return true;
|
|
1684
1713
|
const folded = clause.match(REPEATED_WORD_RE);
|
|
1685
|
-
|
|
1714
|
+
if (folded && closedOrCollapsed(folded[1], BYE, BYE_COLLAPSED)) return true;
|
|
1715
|
+
return gluedThanksBye(clause);
|
|
1686
1716
|
}
|
|
1687
1717
|
/** Closing-filler clauses — the CONTENT half of a farewell/thanks sentence
|
|
1688
1718
|
* ("thanks, that's everything for now") that isn't itself gratitude or bye
|
|
@@ -1736,7 +1766,7 @@ function farewellOrThanksSignal(raw, q) {
|
|
|
1736
1766
|
const rawClause = clauses[i];
|
|
1737
1767
|
const ackMatch = rawClause.match(ACK_LEAD_RE);
|
|
1738
1768
|
const clause = ackMatch ? ackMatch[1].trim() : rawClause;
|
|
1739
|
-
if (
|
|
1769
|
+
if (farewellClause(clause)) { byeHit = true; break; }
|
|
1740
1770
|
const deIntensified = clause.replace(THANKS_HELP_TAIL_RE, "").replace(TRAILING_INTENSIFIER_RE, "").trim();
|
|
1741
1771
|
if (thanksClauseIdx < 0 && closedOrCollapsed(deIntensified, THANKS, THANKS_COLLAPSED)) thanksClauseIdx = i;
|
|
1742
1772
|
}
|
|
@@ -1874,9 +1904,9 @@ function conversationalTurn(line, ctx) {
|
|
|
1874
1904
|
...(end ? { end: true } : {}),
|
|
1875
1905
|
}, ctx.trace);
|
|
1876
1906
|
};
|
|
1877
|
-
if (
|
|
1907
|
+
if (farewellClause(q)) {
|
|
1878
1908
|
note(ctx.trace, "goal: casual/social — ending the session (no graph intent)");
|
|
1879
|
-
note(ctx.trace, "lane: conversational — farewell (BYE closed set, incl. bare reduplication e.g. \"bye bye\")");
|
|
1909
|
+
note(ctx.trace, "lane: conversational — farewell (BYE closed set, incl. bare reduplication e.g. \"bye bye\" and a thanks word glued to a bye word e.g. \"thanks bye\")");
|
|
1880
1910
|
return mk(t(T_FAREWELL), { end: true });
|
|
1881
1911
|
}
|
|
1882
1912
|
{
|
|
@@ -2746,6 +2776,20 @@ async function teachFact(memoryDir, sessionId, { subject, predicate, object, qua
|
|
|
2746
2776
|
// are Ys" recall. Deliberately NARROW: only the SUBJECT gets a free pass past
|
|
2747
2777
|
// parseAce's closed lexicon-noun gate, never the OBJECT. ----
|
|
2748
2778
|
|
|
2779
|
+
/** Singular nouns that already END in a bare sibilant "-s" and so form their
|
|
2780
|
+
* plural with "-es" (bus -> buses, gas -> gases, lens -> lenses). When a
|
|
2781
|
+
* "-ses" word strips two letters onto one of these, the "-es" was the plural
|
|
2782
|
+
* marker and the stem is correct; every OTHER "-ses" word kept a silent "-e"
|
|
2783
|
+
* and singularizes by stripping one letter (see singularizeSurface). Doubled
|
|
2784
|
+
* "-ss" stems (class, glass, boss) are caught by pattern, so only single-"s"
|
|
2785
|
+
* stems need listing here. */
|
|
2786
|
+
const BARE_SIBILANT_S_NOUNS = new Set([
|
|
2787
|
+
"bus", "gas", "plus", "minus", "surplus", "lens", "bias", "atlas", "canvas",
|
|
2788
|
+
"virus", "bonus", "campus", "census", "chorus", "circus", "focus", "status",
|
|
2789
|
+
"iris", "genius", "sinus", "walrus", "cactus", "fungus", "radius", "nucleus",
|
|
2790
|
+
"alias", "crocus", "abacus", "syllabus", "octopus", "platypus",
|
|
2791
|
+
]);
|
|
2792
|
+
|
|
2749
2793
|
/** Naive plural → singular fold for the "some/a few Xs are Ys" surface forms
|
|
2750
2794
|
* (mirrors factTermVariants' own naive -es/-s stripping, below, but returns
|
|
2751
2795
|
* ONE canonical spelling to STORE rather than a lookup Set of candidates to
|
|
@@ -2763,11 +2807,27 @@ async function teachFact(memoryDir, sessionId, { subject, predicate, object, qua
|
|
|
2763
2807
|
function singularizeSurface(word) {
|
|
2764
2808
|
const w = String(word || "").trim();
|
|
2765
2809
|
if (/[a-z]ies$/i.test(w)) return `${w.slice(0, -3)}y`;
|
|
2766
|
-
if (/
|
|
2810
|
+
if (/[a-z]ses$/i.test(w)) {
|
|
2811
|
+
// "-ses" is genuinely ambiguous. A base already ending in "-se" just adds
|
|
2812
|
+
// "-s" (collapse -> collapses, cause -> causes, rose -> roses), so the
|
|
2813
|
+
// singular strips ONE letter. A base ending in a bare sibilant "-s" adds
|
|
2814
|
+
// "-es" (bus -> buses, class -> classes, lens -> lenses), so the singular
|
|
2815
|
+
// strips TWO. Spelling alone can't separate every pair, so strip two only
|
|
2816
|
+
// when the two-char strip lands on a doubled "-ss" or a known bare-sibilant
|
|
2817
|
+
// "-s" noun; otherwise the base kept its silent "-e" and one letter comes
|
|
2818
|
+
// off. The other "-es" endings (-xes/-zes/-ches/-shes) keep the plain
|
|
2819
|
+
// two-char strip below.
|
|
2820
|
+
const stem = w.slice(0, -2);
|
|
2821
|
+
if (/ss$/i.test(stem) || BARE_SIBILANT_S_NOUNS.has(stem.toLowerCase())) return stem;
|
|
2822
|
+
return w.slice(0, -1);
|
|
2823
|
+
}
|
|
2824
|
+
if (/(xes|zes|ches|shes)$/i.test(w)) return w.slice(0, -2);
|
|
2767
2825
|
if (/[a-z]s$/i.test(w) && !/(?:ss|ous)$/i.test(w)) return w.slice(0, -1);
|
|
2768
2826
|
return w;
|
|
2769
2827
|
}
|
|
2770
2828
|
|
|
2829
|
+
export { singularizeSurface };
|
|
2830
|
+
|
|
2771
2831
|
/** "(every|each|all|a|an )?X is/are (a|an )?Y" — the shape the unknown-subject
|
|
2772
2832
|
* fallback recognizes (group 2 = X, group 4 = Y); group 1 (when present)
|
|
2773
2833
|
* names the determiner, so the caller can tell a genuine "every" universal
|
|
@@ -2862,22 +2922,27 @@ async function isGroundedByFact(term, memoryDir, cache = null) {
|
|
|
2862
2922
|
}
|
|
2863
2923
|
|
|
2864
2924
|
/** Shared "is this term grounded in ANY sense" aggregate — a static lexicon
|
|
2865
|
-
* word (any part of speech, via `classify`), a GENERIC_ANCHOR_NOUNS root,
|
|
2866
|
-
*
|
|
2867
|
-
* (isGroundedByFact, above)
|
|
2868
|
-
*
|
|
2869
|
-
*
|
|
2870
|
-
*
|
|
2871
|
-
*
|
|
2872
|
-
*
|
|
2873
|
-
|
|
2925
|
+
* word (any part of speech, via `classify`), a GENERIC_ANCHOR_NOUNS root, a
|
|
2926
|
+
* term already anchored by a previously taught isa-family fact
|
|
2927
|
+
* (isGroundedByFact, above), OR — when a code graph is supplied — a symbol
|
|
2928
|
+
* the code graph itself resolves (resolveSymbol, codegraph.mjs — the SAME
|
|
2929
|
+
* resolver /describe/`/members`/`/subclasses` already use, so "known to
|
|
2930
|
+
* describe" and "known to teach" can never disagree). Used by
|
|
2931
|
+
* unknownObjectFallback's subject/object groundedness checks below, where no
|
|
2932
|
+
* part-of-speech branching follows — just "known or not". (unknownSubjectFallback's
|
|
2933
|
+
* own object-known check, above/below, stays narrower and NOUN-specific — see
|
|
2934
|
+
* its own comment — so an object that's merely a known ADJECTIVE doesn't get
|
|
2935
|
+
* misrouted into the class/subClassOf branch instead of the property branch.) */
|
|
2936
|
+
async function isGroundedTerm(term, lex, memoryDir, cache = null, graph = null) {
|
|
2874
2937
|
const raw = String(term ?? "").trim();
|
|
2875
2938
|
if (!raw) return false;
|
|
2876
2939
|
if (GENERIC_ANCHOR_NOUNS.has(raw.toLowerCase())) return true;
|
|
2877
2940
|
const { classify } = await import("../domain/grammar/lexicon.mjs");
|
|
2878
2941
|
if (classify(raw, lex)) return true;
|
|
2942
|
+
if (graph && resolveSymbol(graph, raw)?.match) return true;
|
|
2879
2943
|
return isGroundedByFact(raw, memoryDir, cache);
|
|
2880
2944
|
}
|
|
2945
|
+
export { isGroundedTerm };
|
|
2881
2946
|
|
|
2882
2947
|
/** The "both sides ungrounded" grounding NUDGE: reuses teachSuggestion's own
|
|
2883
2948
|
* "compute a hint, APPEND it to the existing honest-miss message, never
|
|
@@ -2899,15 +2964,15 @@ async function isGroundedTerm(term, lex, memoryDir, cache = null) {
|
|
|
2899
2964
|
* unchanged) whenever the payload doesn't fit the shape, or at least one
|
|
2900
2965
|
* side IS already grounded — a DIFFERENT, more specific reason it declined,
|
|
2901
2966
|
* where this nudge would be actively unhelpful noise. */
|
|
2902
|
-
async function ungroundedPairHint(payload, lexicon, memoryDir, cache = null) {
|
|
2967
|
+
async function ungroundedPairHint(payload, lexicon, memoryDir, cache = null, graph = null) {
|
|
2903
2968
|
if (!memoryDir) return "";
|
|
2904
2969
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
2905
2970
|
if (!m) return "";
|
|
2906
2971
|
const [, , subjectRaw, , objectRaw] = m;
|
|
2907
2972
|
const { loadLexicon } = await import("../domain/grammar/lexicon.mjs");
|
|
2908
2973
|
const lex = lexicon || loadLexicon();
|
|
2909
|
-
if (await isGroundedTerm(subjectRaw, lex, memoryDir, cache)) return "";
|
|
2910
|
-
if (await isGroundedTerm(objectRaw, lex, memoryDir, cache)) return "";
|
|
2974
|
+
if (await isGroundedTerm(subjectRaw, lex, memoryDir, cache, graph)) return "";
|
|
2975
|
+
if (await isGroundedTerm(objectRaw, lex, memoryDir, cache, graph)) return "";
|
|
2911
2976
|
// Chaining the second term UNDER the first's now-grounded proper name
|
|
2912
2977
|
// ("every man is a john") is technically accepted by the grammar (once
|
|
2913
2978
|
// "john" is grounded, ANY term can be taught as a kind of it), but reads as
|
|
@@ -3070,7 +3135,7 @@ async function objectReadsAsNonNoun(word) {
|
|
|
3070
3135
|
return false;
|
|
3071
3136
|
}
|
|
3072
3137
|
}
|
|
3073
|
-
async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon, classIntent = false }, cache = null) {
|
|
3138
|
+
async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon, classIntent = false, graph = null }, cache = null) {
|
|
3074
3139
|
if (!memoryDir) return null;
|
|
3075
3140
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
3076
3141
|
if (!m) return null;
|
|
@@ -3084,9 +3149,9 @@ async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon, c
|
|
|
3084
3149
|
if (!/^(?:every|each|all|any)$/i.test((det || "").trim()) && !classIntent) return null; // class-level mint needs a universal quantifier or an explicit kind-of infix
|
|
3085
3150
|
const { loadLexicon, lookupNoun } = await import("../domain/grammar/lexicon.mjs");
|
|
3086
3151
|
const lex = lexicon || loadLexicon();
|
|
3087
|
-
const subjectGrounded = await isGroundedTerm(subjectRaw, lex, memoryDir, cache);
|
|
3152
|
+
const subjectGrounded = await isGroundedTerm(subjectRaw, lex, memoryDir, cache, graph);
|
|
3088
3153
|
if (!subjectGrounded) return null; // ungrounded subject isn't this fallback's asymmetry — never a guessed mint
|
|
3089
|
-
const objectGrounded = await isGroundedTerm(objectRaw, lex, memoryDir, cache);
|
|
3154
|
+
const objectGrounded = await isGroundedTerm(objectRaw, lex, memoryDir, cache, graph);
|
|
3090
3155
|
if (objectGrounded) return null; // object already known — nothing to mint
|
|
3091
3156
|
if (await objectReadsAsNonNoun(objectRaw)) return null; // reads like an adjective/verb, not a class noun — defer to unknownAdjectiveFallback
|
|
3092
3157
|
const quantifier = /^every$/i.test((det || "").trim()) ? "every" : "";
|
|
@@ -3168,7 +3233,7 @@ async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon, c
|
|
|
3168
3233
|
* otherwise provide. "the cache is bespoke" and "Mary is female" both carry
|
|
3169
3234
|
* one of those signals (the leading "the", and capitalization,
|
|
3170
3235
|
* respectively); "module is banana" carries none. */
|
|
3171
|
-
async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon }, cache = null) {
|
|
3236
|
+
async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon, graph = null }, cache = null) {
|
|
3172
3237
|
if (!memoryDir) return null;
|
|
3173
3238
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
3174
3239
|
if (!m) return null;
|
|
@@ -3201,7 +3266,7 @@ async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon
|
|
|
3201
3266
|
// a noun-shaped Y was already minted as a class upstream and never reaches
|
|
3202
3267
|
// this point.
|
|
3203
3268
|
const universalQuantifier = /^(?:every|each|all|any)$/i.test((det || "").trim());
|
|
3204
|
-
if (universalQuantifier && (await isGroundedTerm(subjectRaw, lex, memoryDir, cache))
|
|
3269
|
+
if (universalQuantifier && (await isGroundedTerm(subjectRaw, lex, memoryDir, cache, graph))
|
|
3205
3270
|
&& (lookupAdjective(lex, objectRaw) || (await objectReadsAsNonNoun(objectRaw)))) {
|
|
3206
3271
|
const classSubject = /^are$/i.test(verb)
|
|
3207
3272
|
? (lookupNoun(lex, subjectRaw)?.lemma || singularizeSurface(subjectRaw))
|
|
@@ -3868,8 +3933,27 @@ const HABITUAL_VERB_EXCLUDE = new Set([
|
|
|
3868
3933
|
"please", "thanks", "kindly", "anyway", "though", "indeed", "maybe",
|
|
3869
3934
|
"perhaps", "still", "too", "also", "instead", "now", "then", "here", "there",
|
|
3870
3935
|
]);
|
|
3936
|
+
/** Sentence-initial ordinal/temporal discourse adverbs — the "First", "Then",
|
|
3937
|
+
* "Next" … that thread a narrative across sentences without belonging to the
|
|
3938
|
+
* clause they lead. A closed set: a connective in this slot carries sequence,
|
|
3939
|
+
* not content, so stripping it lets "First a cell grows." read as the same
|
|
3940
|
+
* capability teach the bare "a cell grows." already does. */
|
|
3941
|
+
const LEADING_DISCOURSE_ADVERBS = [
|
|
3942
|
+
"first", "second", "third", "then", "next", "finally",
|
|
3943
|
+
"later", "meanwhile", "afterward", "afterwards",
|
|
3944
|
+
];
|
|
3945
|
+
const LEADING_DISCOURSE_ADVERB_RE = new RegExp(
|
|
3946
|
+
`^(?:${LEADING_DISCOURSE_ADVERBS.join("|")})\\b\\s*,?\\s+`, "i",
|
|
3947
|
+
);
|
|
3948
|
+
/** Strip one leading ordinal/temporal discourse adverb (case-insensitive,
|
|
3949
|
+
* optional trailing comma) from the start of `text`, leaving the clause that
|
|
3950
|
+
* followed it. A word not in the closed set, or one with no clause after it,
|
|
3951
|
+
* is left untouched. */
|
|
3952
|
+
export function stripLeadingDiscourseAdverb(text) {
|
|
3953
|
+
return String(text || "").trim().replace(LEADING_DISCOURSE_ADVERB_RE, "");
|
|
3954
|
+
}
|
|
3871
3955
|
function matchBareHabitualTeach(text) {
|
|
3872
|
-
const t = String(text || "").trim();
|
|
3956
|
+
const t = stripLeadingDiscourseAdverb(String(text || "").trim());
|
|
3873
3957
|
const plural = t.match(/^(?:all\s+|every\s+)?([\w-]+s)\s+([a-z][\w-]*)[.!?]*$/i);
|
|
3874
3958
|
if (plural && !STRUCT_WORDS.has(plural[2].toLowerCase()) && !HABITUAL_VERB_EXCLUDE.has(plural[2].toLowerCase()) && !/[^s]s$/i.test(plural[2])) {
|
|
3875
3959
|
const subject = singularizeSurface(plural[1].toLowerCase());
|
|
@@ -4360,7 +4444,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4360
4444
|
if (memoryDir && !QUESTION_LEAD_RE.test(conjSrc) && /\s+and\s+/i.test(conjSrc)
|
|
4361
4445
|
&& !(await hasMidSentenceInterrogative(conjSrc))) {
|
|
4362
4446
|
const rewrap = (half) => (wrapped != null ? `remember that ${half}` : half);
|
|
4363
|
-
const recurse = (half) => teachLane(rewrap(half), { memoryDir, sessionId, lexicon, cache, planHolder, gameConfig });
|
|
4447
|
+
const recurse = (half) => teachLane(rewrap(half), { memoryDir, sessionId, lexicon, cache, planHolder, graph, gameConfig });
|
|
4364
4448
|
const stripNoted = (t) => String(t).replace(/^noted — remembered(?:\s+\d+\s+facts?)?:\s*/i, "").trim();
|
|
4365
4449
|
const shared = conjSrc.match(/^(.+?)\s+and\s+((?:is|are|has|have|can)\b.+)$/i);
|
|
4366
4450
|
const sharedSubject = shared ? shared[1].match(/^(.+?)\s+(?:is|are|has|have|can)\b/i)?.[1]?.trim() : null;
|
|
@@ -5145,7 +5229,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
5145
5229
|
if (canShape && canSingular !== canShape.subject) {
|
|
5146
5230
|
let canLex = lexicon;
|
|
5147
5231
|
if (!canLex) { const { loadLexicon } = await import("../domain/grammar/lexicon.mjs"); canLex = loadLexicon(); }
|
|
5148
|
-
if (await isGroundedTerm(canSingular, canLex, memoryDir, cache)) {
|
|
5232
|
+
if (await isGroundedTerm(canSingular, canLex, memoryDir, cache, graph)) {
|
|
5149
5233
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
5150
5234
|
subject: canSingular, predicate: await capabilityPredicate(canShape.negated), object: canShape.verb,
|
|
5151
5235
|
});
|
|
@@ -5273,7 +5357,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
5273
5357
|
// every query-side variant fold) uses; a proper noun that only looks
|
|
5274
5358
|
// plural ("redis") falls back to its own spelling.
|
|
5275
5359
|
for (const subj of new Set([singularizeSurface(habitualTeach.subject), habitualTeach.subject])) {
|
|
5276
|
-
if (await isGroundedTerm(subj, habLex, memoryDir, cache)) {
|
|
5360
|
+
if (await isGroundedTerm(subj, habLex, memoryDir, cache, graph)) {
|
|
5277
5361
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
5278
5362
|
subject: subj, predicate: await capabilityPredicate(habitualTeach.negated), object: habitualTeach.verb,
|
|
5279
5363
|
});
|
|
@@ -5294,7 +5378,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
5294
5378
|
// STATIC lexicon (or a prior taught fact) already grounds can mint a
|
|
5295
5379
|
// brand-new object term. See unknownObjectFallback's own docblock for the
|
|
5296
5380
|
// exact narrowing rules (the "both sides ungrounded" safety guard, etc.).
|
|
5297
|
-
const objectFallback = await unknownObjectFallback(payload, { memoryDir, sessionId, lexicon, classIntent: kindOfClassIntent }, cache);
|
|
5381
|
+
const objectFallback = await unknownObjectFallback(payload, { memoryDir, sessionId, lexicon, classIntent: kindOfClassIntent, graph }, cache);
|
|
5298
5382
|
if (objectFallback) return objectFallback;
|
|
5299
5383
|
// ADJECTIVE-MINT fallback: tried right after unknownObjectFallback
|
|
5300
5384
|
// declines, so a grounded subject (static
|
|
@@ -5303,7 +5387,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
5303
5387
|
// docblock for the exact narrowing rules (the "both sides ungrounded"
|
|
5304
5388
|
// safety guard, and why this must be a standalone function rather than
|
|
5305
5389
|
// nested inside unknownSubjectFallback).
|
|
5306
|
-
const adjectiveFallback = await unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon }, cache);
|
|
5390
|
+
const adjectiveFallback = await unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon, graph }, cache);
|
|
5307
5391
|
if (adjectiveFallback) return adjectiveFallback;
|
|
5308
5392
|
// PROPERTY teach — "remember/note that <X> is <adjective>": wrapper-REQUIRED
|
|
5309
5393
|
// (a bare "X is deprecated" is never silently reified), and only after the
|
|
@@ -5366,7 +5450,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
5366
5450
|
// Grounding NUDGE: APPENDED, never a replacement, exactly like "did" above
|
|
5367
5451
|
// — see ungroundedPairHint's own docblock for why this is scoped to the
|
|
5368
5452
|
// "both sides ungrounded, fits the X is/are Y shape" case only.
|
|
5369
|
-
const groundingHint = await ungroundedPairHint(payload, lexicon, memoryDir, cache);
|
|
5453
|
+
const groundingHint = await ungroundedPairHint(payload, lexicon, memoryDir, cache, graph);
|
|
5370
5454
|
return {
|
|
5371
5455
|
text: `I couldn't store that —${why} I remember facts in the shape "every X is a Y", where X and Y are `
|
|
5372
5456
|
+ `words I know.${did}${groundingHint} Type /memory to see what I already remember.`,
|
|
@@ -6639,7 +6723,7 @@ function suggestibleSubjectPhrase(subject) {
|
|
|
6639
6723
|
* take the article. */
|
|
6640
6724
|
const QUANTIFIER_LEAD_RE = /^(?:every|each|all|any)\s+/i;
|
|
6641
6725
|
|
|
6642
|
-
function factTermVariants(normFactTerm, term) {
|
|
6726
|
+
export function factTermVariants(normFactTerm, term) {
|
|
6643
6727
|
const t = normFactTerm(term);
|
|
6644
6728
|
const v = new Set();
|
|
6645
6729
|
// The ask frames glue a quantifier onto the subject and looked up "every
|
|
@@ -8383,7 +8467,7 @@ const CARD_EXISTENCE_ASK_RE = /^does\s+an?\s+(.+?)\s+have\s+an?\s+(.+?)[?.!\s]*$
|
|
|
8383
8467
|
/** The 4 pattern-5 cardinality-restriction predicates buildCardinalityRestrictions
|
|
8384
8468
|
* reconstructs from — owl:onProperty (shared scaffolding with someValuesFrom
|
|
8385
8469
|
* restrictions too) is added alongside this set by each reader below, not
|
|
8386
|
-
* folded into it here, mirroring infbench/grade.mjs's own identically-named
|
|
8470
|
+
* folded into it here, mirroring test-benchmarks/infbench/grade.mjs's own identically-named
|
|
8387
8471
|
* set + separate owl:onProperty handling. */
|
|
8388
8472
|
const CARDINALITY_ROW_PREDICATES = new Set(["owl:cardinality", "owl:minCardinality", "owl:maxCardinality", "owl:onClass"]);
|
|
8389
8473
|
/** "who owns <X>" / "who maintains <X>" — the closed ownership read-back over
|
|
@@ -11158,7 +11242,7 @@ async function conceptForceAnswer(query, envelope, { graph, config, source, memo
|
|
|
11158
11242
|
const definition = (await seonDefinitions()).get(term) ?? null;
|
|
11159
11243
|
if (!definition) return null;
|
|
11160
11244
|
// The runChat shell hands the loaded graph straight in; the pure runTurn(config)
|
|
11161
|
-
// path (tests, chatbench) does not, so load it the same way dispatchTool does when
|
|
11245
|
+
// path (tests, test-benchmarks/chatbench) does not, so load it the same way dispatchTool does when
|
|
11162
11246
|
// it's missing. Failure-tolerated: no loadable graph → no concept force.
|
|
11163
11247
|
let g = graph;
|
|
11164
11248
|
if (!g && config && source) {
|
|
@@ -11927,6 +12011,15 @@ const MOVE_HISTORY_RE = /^where\s+did\s+(.+?)\s+(?:move|get\s+moved|go)(?:\s+to)
|
|
|
11927
12011
|
* the honest miss. */
|
|
11928
12012
|
const TEMPORAL_COMPARISON_RE = /^(?:was|is)\s+(this one|that one|it|this|that)\s+(before|after)\s+(.+?)\s+(?:was|were)\s+(touched|changed|modified|edited|updated)[?.!\s]*$/i;
|
|
11929
12013
|
|
|
12014
|
+
/** "were those before logger.mjs was touched" — the plural sibling of
|
|
12015
|
+
* TEMPORAL_COMPARISON_RE. A plural bindable form (`those`/`them`/`these`)
|
|
12016
|
+
* binds a `set` referent a listing/filter answer established, and the
|
|
12017
|
+
* comparison runs over the whole set: each member is dated from the graph,
|
|
12018
|
+
* and the answer quantifies (all / none / M of N) rather than forcing one
|
|
12019
|
+
* date. A set whose members are not all datable refuses, the same honest
|
|
12020
|
+
* miss the singular lane takes on an undated referent. */
|
|
12021
|
+
const PLURAL_TEMPORAL_COMPARISON_RE = /^(?:were|are)\s+(those|them|these)\s+(before|after)\s+(.+?)\s+(?:was|were)\s+(touched|changed|modified|edited|updated)[?.!\s]*$/i;
|
|
12022
|
+
|
|
11930
12023
|
/** ARCHITECTURE-OVERVIEW intent — "show me the architecture", "what is the
|
|
11931
12024
|
* architecture of this repo": the whole-repo map the /arch command renders.
|
|
11932
12025
|
* A closed phrase set, because the literal word "architecture" is also a
|
|
@@ -11937,12 +12030,17 @@ const TEMPORAL_COMPARISON_RE = /^(?:was|is)\s+(this one|that one|it|this|that)\s
|
|
|
11937
12030
|
* an of-this-repo tail, or an overview/map noun); a query that NAMES a
|
|
11938
12031
|
* symbol ("describe renderArchitecture") never matches. */
|
|
11939
12032
|
const ARCH_OVERVIEW_LEAD = "(?:(?:can|could|would)\\s+you\\s+(?:please\\s+)?)?(?:(?:show|give)\\s+(?:me|us)\\s+|describe\\s+|explain\\s+|what(?:'s|s|\\s+is)\\s+)?";
|
|
11940
|
-
const
|
|
12033
|
+
const ARCH_OVERVIEW_OF_REPO = "(?:of|for)\\s+(?:this|the)\\s+(?:app|codebase|repo|repository|project|code)";
|
|
12034
|
+
const ARCH_OVERVIEW_TAIL = `(?:\\s+${ARCH_OVERVIEW_OF_REPO})?`;
|
|
11941
12035
|
const ARCH_OVERVIEW_PHRASES = [
|
|
11942
12036
|
// Article-carried: "the architecture" alone, or wrapped/tailed.
|
|
11943
12037
|
new RegExp(`^${ARCH_OVERVIEW_LEAD}the\\s+architecture(?:\\s+(?:overview|map|diagram))?${ARCH_OVERVIEW_TAIL}(?:\\s+here)?\\??$`, "i"),
|
|
11944
12038
|
// Article-less: anchored by the of-this-repo tail or the overview/map noun instead.
|
|
11945
|
-
new RegExp(`^${ARCH_OVERVIEW_LEAD}architecture\\s+(
|
|
12039
|
+
new RegExp(`^${ARCH_OVERVIEW_LEAD}architecture\\s+(?:${ARCH_OVERVIEW_OF_REPO}|overview|map|diagram)\\??$`, "i"),
|
|
12040
|
+
// Architecture-less: the topic noun stands in for the word, so the of-this-repo
|
|
12041
|
+
// tail is REQUIRED here. A bare "give me an overview" names no subject and
|
|
12042
|
+
// falls through to the ordinary lanes.
|
|
12043
|
+
new RegExp(`^${ARCH_OVERVIEW_LEAD}(?:(?:an?|the)\\s+)?(?:overview|map|diagram)\\s+${ARCH_OVERVIEW_OF_REPO}(?:\\s+here)?\\??$`, "i"),
|
|
11946
12044
|
];
|
|
11947
12045
|
|
|
11948
12046
|
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, discourseHolder = null, gameConfig = DEFAULT_GAME_CONFIG, liveReference = false, onLiveLookup = null, uiContext = "cli", synthesisBudget = AUTO_SYNTHESIS_BUDGET }) {
|
|
@@ -11955,9 +12053,19 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11955
12053
|
// them" filters or counts the PREVIOUS answer's entity set, threaded as
|
|
11956
12054
|
// ask()'s `prev`. Prefers the FULL id set (`allIds`) over `matches`, since a
|
|
11957
12055
|
// concept-force listing caps `matches` at MAX_EXAMPLES.
|
|
11958
|
-
|
|
12056
|
+
let prev = (last?.detail?.allIds && last.detail.allIds.length)
|
|
11959
12057
|
? last.detail.allIds.filter(Boolean)
|
|
11960
12058
|
: (last?.detail?.matches || []).map((m) => m?.id).filter(Boolean);
|
|
12059
|
+
// A count erases its own matches to [], so the previous turn carries no id
|
|
12060
|
+
// set for the next "which of those" to narrow. When the carry is empty, fall
|
|
12061
|
+
// back to the session record: a standing `set` referent that "those" binds
|
|
12062
|
+
// keeps a narrowed set alive across a counting hop. No same-turn plural tie
|
|
12063
|
+
// is reachable yet (no lane registers two set referents in one turn), so this
|
|
12064
|
+
// probe needs no tie refusal.
|
|
12065
|
+
if (!prev.length && discourseHolder) {
|
|
12066
|
+
const boundSet = bindDiscourseForm(discourseHolder.record, "those");
|
|
12067
|
+
if (boundSet?.referent?.ids?.length) prev = boundSet.referent.ids.filter(Boolean);
|
|
12068
|
+
}
|
|
11961
12069
|
// The query the ENGINE parses: a "what about X" continuation is rewritten to the
|
|
11962
12070
|
// prior shape with X swapped in; everything else parses verbatim. The record and
|
|
11963
12071
|
// transcript keep the user's ACTUAL words (`query`), only the parse target changes.
|
|
@@ -11992,12 +12100,17 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11992
12100
|
if (cmp) {
|
|
11993
12101
|
const [, form, cmpOp, clauseSubject, participle] = cmp;
|
|
11994
12102
|
const verb = participle.toLowerCase();
|
|
12103
|
+
const temporalGoal = "compare a prior answer's dated referent against a freshly read event (cross-turn temporal composition)";
|
|
11995
12104
|
const refMiss = (text) => {
|
|
11996
|
-
note(trace,
|
|
12105
|
+
note(trace, `goal: ${temporalGoal}`);
|
|
11997
12106
|
note(trace, `lane: TEMPORAL_COMPARISON_RE — "${form}" could not compose a comparison; a specific miss names why, never the teach-offer cascade`);
|
|
11998
|
-
return plainTurn(query, text, { via: "miss", miss: true, focus });
|
|
12107
|
+
return plainTurn(query, text, { via: "miss", miss: true, focus, goal: temporalGoal });
|
|
11999
12108
|
};
|
|
12000
|
-
const bound = discourseHolder ? bindDiscourseForm(discourseHolder.record, form) : null;
|
|
12109
|
+
const bound = discourseHolder ? bindDiscourseForm(discourseHolder.record, form, { tieRefuses: true }) : null;
|
|
12110
|
+
if (bound?.tie) {
|
|
12111
|
+
const options = joinOr(bound.tie.map((r) => r.label));
|
|
12112
|
+
return refMiss(`"${form}" could mean ${options} — which do you mean?`);
|
|
12113
|
+
}
|
|
12001
12114
|
if (!bound?.referent) {
|
|
12002
12115
|
return refMiss(`I don't have a referent for "${form}" yet — nothing answered earlier in this conversation binds it. Ask about the event first (e.g. "when was ${clauseSubject} last ${verb}"), then ask the comparison again.`);
|
|
12003
12116
|
}
|
|
@@ -12021,14 +12134,88 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12021
12134
|
const holds = cmpOp.toLowerCase() === "before" ? refDay < clauseDay : refDay > clauseDay;
|
|
12022
12135
|
const relation = refDay < clauseDay ? "came before" : refDay > clauseDay ? "came after" : "landed on the same day as";
|
|
12023
12136
|
const text = `${holds ? "Yes" : "No"} — ${bound.referent.label} (${refDay}) ${relation} ${clauseSubject} was last ${verb} (${freshCommit.label}, ${clauseDay}).`;
|
|
12024
|
-
note(trace,
|
|
12137
|
+
note(trace, `goal: ${temporalGoal}`);
|
|
12025
12138
|
note(trace, `lane: TEMPORAL_COMPARISON_RE — "${form}" bound ${bound.referent.label} (${refDay}) through the discourse record; the embedded clause re-ran as its own when-question`);
|
|
12026
|
-
const turn = plainTurn(query, text, { via: "composed", miss: false, focus });
|
|
12139
|
+
const turn = plainTurn(query, text, { via: "composed", miss: false, focus, goal: temporalGoal });
|
|
12027
12140
|
const cited = [graph.byId?.get?.(bound.referent.ids[0]), freshCommit].filter(Boolean);
|
|
12028
12141
|
turn.detail = { traversal: `discourse ${bound.referent.ref} (${refDay}) vs last-${verb} of ${clauseSubject} (${clauseDay})`, matches: cited };
|
|
12029
12142
|
return turn;
|
|
12030
12143
|
}
|
|
12031
12144
|
}
|
|
12145
|
+
// TEMPORAL COMPARISON OVER A PLURAL ANTECEDENT — "were those before
|
|
12146
|
+
// logger.mjs was touched": the plural sibling of the block above. `those`
|
|
12147
|
+
// binds a `set` referent a prior listing/filter established, and the whole
|
|
12148
|
+
// set is compared against the freshly read clause date. Each member is dated
|
|
12149
|
+
// from the graph (the set referent carries member ids, not per-member dates,
|
|
12150
|
+
// so the dates are read here the same way the clause is read fresh), and the
|
|
12151
|
+
// answer quantifies over the set — all, none, or M of N — rather than
|
|
12152
|
+
// forcing the set onto a single date. A set whose members are not all
|
|
12153
|
+
// datable refuses honestly, and an unbound form, an undatable clause, or a
|
|
12154
|
+
// missing graph keep today's miss, the same specific declines the singular
|
|
12155
|
+
// lane makes. Checked here, before the ask engine, for the same reason the
|
|
12156
|
+
// singular lane is: otherwise "those before logger.mjs was" reaches the
|
|
12157
|
+
// keyword-spot strategy as multi-token patient wreckage and the teach-offer
|
|
12158
|
+
// cascade reads it as a subject to learn facts about.
|
|
12159
|
+
{
|
|
12160
|
+
const cmp = String(query).trim().match(PLURAL_TEMPORAL_COMPARISON_RE);
|
|
12161
|
+
if (cmp) {
|
|
12162
|
+
const [, form, cmpOp, clauseSubject, participle] = cmp;
|
|
12163
|
+
const verb = participle.toLowerCase();
|
|
12164
|
+
const temporalGoal = "compare a prior answer's dated set against a freshly read event (cross-turn temporal composition over a plural antecedent)";
|
|
12165
|
+
const refMiss = (text) => {
|
|
12166
|
+
note(trace, `goal: ${temporalGoal}`);
|
|
12167
|
+
note(trace, `lane: PLURAL_TEMPORAL_COMPARISON_RE — "${form}" could not compose a set comparison; a specific miss names why, never the teach-offer cascade`);
|
|
12168
|
+
return plainTurn(query, text, { via: "miss", miss: true, focus, goal: temporalGoal });
|
|
12169
|
+
};
|
|
12170
|
+
const bound = discourseHolder ? bindDiscourseForm(discourseHolder.record, form, { tieRefuses: true }) : null;
|
|
12171
|
+
if (bound?.tie) {
|
|
12172
|
+
const options = joinOr(bound.tie.map((r) => r.label));
|
|
12173
|
+
return refMiss(`"${form}" could mean ${options} — which set do you mean?`);
|
|
12174
|
+
}
|
|
12175
|
+
if (!bound?.referent) {
|
|
12176
|
+
return refMiss(`I don't have a set for "${form}" yet — nothing answered earlier in this conversation binds it. Ask for the set first (e.g. "what changed before <commit>"), then ask the comparison again.`);
|
|
12177
|
+
}
|
|
12178
|
+
const set = bound.referent;
|
|
12179
|
+
const total = set.ids.length;
|
|
12180
|
+
if (!graph) {
|
|
12181
|
+
return refMiss(`"${form}" refers to ${set.label}, but I need a code graph to date its members and when ${clauseSubject} was last ${verb} — no code graph is loaded.`);
|
|
12182
|
+
}
|
|
12183
|
+
const dateOfId = (id) => {
|
|
12184
|
+
const ind = graph.byId?.get?.(id);
|
|
12185
|
+
return ind ? String((ind.attributes || []).find((a) => a.key === "date")?.value || "").slice(0, 10) : "";
|
|
12186
|
+
};
|
|
12187
|
+
const memberDates = set.ids.map(dateOfId);
|
|
12188
|
+
const undated = memberDates.filter((d) => !d).length;
|
|
12189
|
+
if (undated) {
|
|
12190
|
+
return refMiss(`"${form}" refers to ${set.label}, but not every member carries a date I can compare — ${undated} of the ${total} have no date on record, so I can't place the set before or after ${clauseSubject} was ${verb}.`);
|
|
12191
|
+
}
|
|
12192
|
+
const { ask } = await import("../domain/ask.mjs");
|
|
12193
|
+
const fresh = ask(graph, `when was ${clauseSubject} ${participle}`);
|
|
12194
|
+
const freshHit = (!fresh?.tmct_ask?.miss && !fresh?.tmct_ask?.ambiguous) ? fresh?.tmct_ask?.matches?.[0] : null;
|
|
12195
|
+
const freshCommit = freshHit?.id ? graph.byId?.get?.(freshHit.id) : null;
|
|
12196
|
+
const clauseDay = freshCommit?.class === "Commit"
|
|
12197
|
+
? String((freshCommit.attributes || []).find((a) => a.key === "date")?.value || "").slice(0, 10)
|
|
12198
|
+
: "";
|
|
12199
|
+
if (!clauseDay) {
|
|
12200
|
+
return refMiss(`"${form}" refers to ${set.label}, but I couldn't date when ${clauseSubject} was last ${verb} in this index — so I can't compare the set to it.`);
|
|
12201
|
+
}
|
|
12202
|
+
const before = cmpOp.toLowerCase() === "before";
|
|
12203
|
+
const satisfied = memberDates.filter((d) => before ? d < clauseDay : d > clauseDay).length;
|
|
12204
|
+
const relation = before ? "came before" : "came after";
|
|
12205
|
+
const tail = `${clauseSubject} was last ${verb} (${freshCommit.label}, ${clauseDay})`;
|
|
12206
|
+
const text = satisfied === total
|
|
12207
|
+
? `Yes — all ${set.label} ${relation} ${tail}.`
|
|
12208
|
+
: satisfied === 0
|
|
12209
|
+
? `No — none of the ${set.label} ${relation} ${tail}.`
|
|
12210
|
+
: `Partly — ${satisfied} of the ${set.label} ${relation} ${tail}; the other ${total - satisfied} did not.`;
|
|
12211
|
+
note(trace, `goal: ${temporalGoal}`);
|
|
12212
|
+
note(trace, `lane: PLURAL_TEMPORAL_COMPARISON_RE — "${form}" bound ${set.label} (${total} dated members) through the discourse record; ${satisfied}/${total} ${relation} the freshly read clause (${clauseDay})`);
|
|
12213
|
+
const turn = plainTurn(query, text, { via: "composed", miss: false, focus, goal: temporalGoal });
|
|
12214
|
+
const cited = [...set.ids.map((id) => graph.byId?.get?.(id)), freshCommit].filter(Boolean);
|
|
12215
|
+
turn.detail = { traversal: `discourse ${set.ref} (${total} members) vs last-${verb} of ${clauseSubject} (${clauseDay})`, matches: cited };
|
|
12216
|
+
return turn;
|
|
12217
|
+
}
|
|
12218
|
+
}
|
|
12032
12219
|
// RENAME HISTORY — "what was X called before" and its siblings. The index
|
|
12033
12220
|
// records current names only, and without this gate "called" fuzzes onto
|
|
12034
12221
|
// the calls relation ("before" simply drops), so the reply read as fluent
|
|
@@ -12043,7 +12230,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12043
12230
|
note(trace, "goal: recover a name history the index does not record (honest decline)");
|
|
12044
12231
|
note(trace, "lane: RENAME_HISTORY_RE — the index carries no rename data, so the calls-relation misread is refused by name");
|
|
12045
12232
|
return plainTurn(query, `I can't say what ${ent ? ent.label : `"${term}"`} was called before — this index records current names only, no rename history. ${named}${ent ? ` here; "who touched ${ent.label}" lists its recorded commits` : ""}.`, {
|
|
12046
|
-
via: "miss", miss: true, focus,
|
|
12233
|
+
via: "miss", miss: true, focus, goal: "recover a name history the index does not record (honest decline)",
|
|
12047
12234
|
});
|
|
12048
12235
|
}
|
|
12049
12236
|
}
|
|
@@ -12055,10 +12242,11 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12055
12242
|
{
|
|
12056
12243
|
const wikiTerm = wikipediaAskTerm(query);
|
|
12057
12244
|
if (wikiTerm) {
|
|
12058
|
-
|
|
12245
|
+
const wikiGoal = "read what Wikipedia says about a named term (explicit source request)";
|
|
12246
|
+
note(trace, `goal: ${wikiGoal}`);
|
|
12059
12247
|
if (!liveReference) {
|
|
12060
12248
|
note(trace, "lane: WIKIPEDIA ASK — the explicit request needs the network opt-in; live Wikipedia is off");
|
|
12061
|
-
return plainTurn(query, `live Wikipedia is off, so I won't reach the network. Turn it on with /wiki on (it fetches from en.wikipedia.org), then ask again.`, { via: "miss", miss: true, focus });
|
|
12249
|
+
return plainTurn(query, `live Wikipedia is off, so I won't reach the network. Turn it on with /wiki on (it fetches from en.wikipedia.org), then ask again.`, { via: "miss", miss: true, focus, goal: wikiGoal });
|
|
12062
12250
|
}
|
|
12063
12251
|
let liveKey = null;
|
|
12064
12252
|
try { liveKey = cleanMissLiveTerm(wikiTerm, lexicon ?? undefined); } catch { liveKey = null; }
|
|
@@ -12066,10 +12254,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12066
12254
|
if (live) {
|
|
12067
12255
|
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon, synthesisBudget);
|
|
12068
12256
|
note(trace, `lane: WIKIPEDIA ASK — answered from a live en.wikipedia.org lookup, cited (article "${live.article.title}", revid ${live.article.revid})`);
|
|
12069
|
-
return plainTurn(query, live.text, { via: "reference", miss: false, focus });
|
|
12257
|
+
return plainTurn(query, live.text, { via: "reference", miss: false, focus, goal: wikiGoal });
|
|
12070
12258
|
}
|
|
12071
12259
|
note(trace, "lane: WIKIPEDIA ASK — no matching live article (no title, timeout, throttle, or drift-guard reject)");
|
|
12072
|
-
return plainTurn(query, `I couldn't reach a matching Wikipedia article for "${wikiTerm}" just now.`, { via: "miss", miss: true, focus });
|
|
12260
|
+
return plainTurn(query, `I couldn't reach a matching Wikipedia article for "${wikiTerm}" just now.`, { via: "miss", miss: true, focus, goal: wikiGoal });
|
|
12073
12261
|
}
|
|
12074
12262
|
}
|
|
12075
12263
|
// COLLECTIVE PLURAL SUBJECT — see COLLECTIVE_FORWARD_RE. Members are the
|
|
@@ -12095,9 +12283,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12095
12283
|
const text = labels.length
|
|
12096
12284
|
? `the ${stem} here are ${memberList} — together they ${verb}: ${joinList(labels)}.`
|
|
12097
12285
|
: `the ${stem} here are ${memberList} — none of them has ${verb} edges in the index.`;
|
|
12098
|
-
|
|
12286
|
+
const groupGoal = `read a forward relation over a module GROUP (${members.length} members), unioned with the set disclosed`;
|
|
12287
|
+
note(trace, `goal: ${groupGoal}`);
|
|
12099
12288
|
note(trace, `lane: COLLECTIVE_FORWARD_RE — "${stem}" resolved to ${members.length} modules; answered the union, never a silent single best-match`);
|
|
12100
|
-
const turn = plainTurn(query, text, { via: "composed", miss: !labels.length, focus });
|
|
12289
|
+
const turn = plainTurn(query, text, { via: "composed", miss: !labels.length, focus, goal: groupGoal });
|
|
12101
12290
|
turn.detail = { traversal: `${verb} edges unioned over ${memberList}`, matches: [...union.keys()].map((id) => graph.byId?.get?.(id)).filter(Boolean) };
|
|
12102
12291
|
return turn;
|
|
12103
12292
|
}
|
|
@@ -12122,7 +12311,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12122
12311
|
note(trace, "goal: recover a move history the index does not record (premise denied, current location cited)");
|
|
12123
12312
|
note(trace, "lane: MOVE_HISTORY_RE — no move data exists; the current location answers with the premise named");
|
|
12124
12313
|
return plainTurn(query, `this index records current locations only, so I can't confirm ${ent.label} moved anywhere.${located}`, {
|
|
12125
|
-
via: "composed", miss: false, focus,
|
|
12314
|
+
via: "composed", miss: false, focus, goal: "recover a move history the index does not record (premise denied, current location cited)",
|
|
12126
12315
|
});
|
|
12127
12316
|
}
|
|
12128
12317
|
}
|
|
@@ -12140,7 +12329,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12140
12329
|
note(trace, `lane: DECISION_RECALL_RE — routed to the folded-session recall surface, ${recalled ? "a relevant block answered" : "nothing relevant folded (honest miss)"}`);
|
|
12141
12330
|
return plainTurn(query, recalled
|
|
12142
12331
|
?? `I don't have a recorded decision about "${term}" — I keep facts and session transcripts, and nothing folded mentions deciding on it. "what did i ask before" lists the last session's questions.`, {
|
|
12143
|
-
via: recalled ? "recall" : "miss", miss: !recalled, focus,
|
|
12332
|
+
via: recalled ? "recall" : "miss", miss: !recalled, focus, goal: "recall a decision from the conversation record (session-recall surface)",
|
|
12144
12333
|
});
|
|
12145
12334
|
}
|
|
12146
12335
|
}
|
|
@@ -12152,7 +12341,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12152
12341
|
const summary = await recallSummary(memoryDir);
|
|
12153
12342
|
note(trace, summary ? "source: memory/fold.mjs recallSummary" : "intermediate: no folded session blocks yet — nothing to recall");
|
|
12154
12343
|
return plainTurn(query, summary ?? "nothing to recall yet — no earlier session has been folded into memory.", {
|
|
12155
|
-
via: "recall", miss: !summary, focus,
|
|
12344
|
+
via: "recall", miss: !summary, focus, goal: "recall what was discussed earlier (explicit recall phrasing)",
|
|
12156
12345
|
});
|
|
12157
12346
|
}
|
|
12158
12347
|
// An explicit "this file"/"that module" kind-noun scope signal is collapsed
|
|
@@ -12212,10 +12401,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12212
12401
|
// content is still typed) register into the session's record here — the
|
|
12213
12402
|
// one point both ask paths (direct call and dispatchTool) converge.
|
|
12214
12403
|
if (discourseHolder && Array.isArray(envelope?.discourse)) {
|
|
12215
|
-
for (const { lane, ...spec } of envelope.discourse) {
|
|
12404
|
+
for (const { lane, bound, ...spec } of envelope.discourse) {
|
|
12216
12405
|
discourseHolder.record = registerReferent(discourseHolder.record, {
|
|
12217
12406
|
...spec, from: { turn: discourseHolder.record.turn, lane, query: askQuery },
|
|
12218
|
-
});
|
|
12407
|
+
}, { bound: !!bound });
|
|
12219
12408
|
}
|
|
12220
12409
|
}
|
|
12221
12410
|
} catch (e) {
|
|
@@ -13400,7 +13589,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13400
13589
|
|
|
13401
13590
|
/** A non-ask, non-dispatch chat turn (count answer, /stats) — the same
|
|
13402
13591
|
* { answer, logLines, record, focus } shape, recorded like any other turn. */
|
|
13403
|
-
function plainTurn(query, answer, { command, via = "composed", miss = false, focus = null, canonical = null } = {}) {
|
|
13592
|
+
function plainTurn(query, answer, { command, via = "composed", miss = false, focus = null, canonical = null, goal = null } = {}) {
|
|
13404
13593
|
const ts = new Date().toISOString();
|
|
13405
13594
|
return {
|
|
13406
13595
|
answer,
|
|
@@ -13412,6 +13601,10 @@ function plainTurn(query, answer, { command, via = "composed", miss = false, foc
|
|
|
13412
13601
|
canonical,
|
|
13413
13602
|
},
|
|
13414
13603
|
focus,
|
|
13604
|
+
// Absent by default, so an untouched plainTurn call stays trailer-free.
|
|
13605
|
+
// A caller that already holds a precise goal string passes it here, and
|
|
13606
|
+
// withGoalLine appends the "Goal (inferred): …" line for that turn.
|
|
13607
|
+
...(goal ? { goal } : {}),
|
|
13415
13608
|
};
|
|
13416
13609
|
}
|
|
13417
13610
|
|
|
@@ -13895,7 +14088,7 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, c
|
|
|
13895
14088
|
.map((t) => `fact(${JSON.stringify(normFactTerm(t.subject))}, ${JSON.stringify(t.predicate)}, ${JSON.stringify(normFactTerm(t.object))})`)
|
|
13896
14089
|
.join(", ")).join(" | "),
|
|
13897
14090
|
};
|
|
13898
|
-
return plainTurn(line, answer, { command: "assert", via: "assert", focus, canonical });
|
|
14091
|
+
return plainTurn(line, answer, { command: "assert", via: "assert", focus, canonical, goal: "teach/remember a new fact" });
|
|
13899
14092
|
}
|
|
13900
14093
|
const parse = parseAce(line, lex);
|
|
13901
14094
|
if (!parse || !parse.triples?.length || parse.residue?.length) return null;
|
|
@@ -13972,7 +14165,7 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, c
|
|
|
13972
14165
|
.map((t) => `fact(${JSON.stringify(normFactTerm(t.subject))}, ${JSON.stringify(t.predicate)}, ${JSON.stringify(normFactTerm(t.object))})`)
|
|
13973
14166
|
.join(", "),
|
|
13974
14167
|
};
|
|
13975
|
-
return plainTurn(line, answer, { command: "assert", via: "assert", focus, canonical });
|
|
14168
|
+
return plainTurn(line, answer, { command: "assert", via: "assert", focus, canonical, goal: "teach/remember a new fact" });
|
|
13976
14169
|
} catch {
|
|
13977
14170
|
return null; // grammar unavailable / write failed — fall through to the engine
|
|
13978
14171
|
}
|
|
@@ -14062,6 +14255,10 @@ function matchImpactIntent(line) {
|
|
|
14062
14255
|
return null;
|
|
14063
14256
|
}
|
|
14064
14257
|
const joinList = (a) => (a.length > 1 ? `${a.slice(0, -1).join(", ")} and ${a[a.length - 1]}` : (a[0] ?? ""));
|
|
14258
|
+
// A discourse tie lists its options with a short Oxford-comma "or" join,
|
|
14259
|
+
// deliberately not the resolver's longer numbered ambiguity format.
|
|
14260
|
+
const joinOr = (a) => (a.length > 2 ? `${a.slice(0, -1).join(", ")}, or ${a[a.length - 1]}`
|
|
14261
|
+
: a.length === 2 ? `${a[0]} or ${a[1]}` : (a[0] ?? ""));
|
|
14065
14262
|
|
|
14066
14263
|
/** Render the next page of a held remainder (pending: {items:[str], noun}). Returns a
|
|
14067
14264
|
* plain turn whose `detail.pending` carries what's still unseen (null when the batch
|
|
@@ -14749,7 +14946,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
14749
14946
|
// otherwise read as a declarative or an orientation ask.
|
|
14750
14947
|
{
|
|
14751
14948
|
const advTurn = await adventureTurn(workingLine, {
|
|
14752
|
-
planHolder, memoryDir, sessionId, env, lexicon, graph, cache: factRowsCache, isPlanFrameLine,
|
|
14949
|
+
planHolder, memoryDir, sessionId, env, lexicon, graph, cache: factRowsCache, isPlanFrameLine, discourseHolder,
|
|
14753
14950
|
});
|
|
14754
14951
|
if (advTurn) {
|
|
14755
14952
|
note(trace, `lane: ${advTurn.note}`);
|
|
@@ -14878,7 +15075,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
14878
15075
|
const step = await executePlanStep(planHolder, { memoryDir, sessionId, gameConfig: resolvedGameConfig });
|
|
14879
15076
|
note(trace, `goal: ${step.deduced}`);
|
|
14880
15077
|
note(trace, "lane: PLAN NEXT — executed the active plan's next move as an @stepK snapshot write");
|
|
14881
|
-
const stepTurn = plainTurn(workingLine, step.text, { via: "plan", focus });
|
|
15078
|
+
const stepTurn = plainTurn(workingLine, step.text, { via: "plan", focus, goal: step.deduced });
|
|
14882
15079
|
stepTurn.lane = "imperative";
|
|
14883
15080
|
const rec = withLast(stepTurn, step.deduced);
|
|
14884
15081
|
rec.planState = planHolder.state;
|
|
@@ -14899,7 +15096,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
14899
15096
|
if (follow) {
|
|
14900
15097
|
note(trace, `goal: ${follow.deduced}`);
|
|
14901
15098
|
note(trace, `lane: ${follow.note}`);
|
|
14902
|
-
const rec = withLast(plainTurn(workingLine, follow.text, { via: "plan", focus }), follow.deduced);
|
|
15099
|
+
const rec = withLast(plainTurn(workingLine, follow.text, { via: "plan", focus, goal: follow.deduced }), follow.deduced);
|
|
14903
15100
|
rec.planState = planHolder.state;
|
|
14904
15101
|
return rec;
|
|
14905
15102
|
}
|
|
@@ -15021,7 +15218,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
15021
15218
|
note(trace, "goal: teach/remember a new fact (bare declarative taxonomy)");
|
|
15022
15219
|
note(trace, "lane: bareTaxonomyTeach — hyphenated-instance or article-led kind-of declarative, stored before the ask engine could parse it as a question");
|
|
15023
15220
|
const taxonomyTurn = plainTurn(workingLine, taxonomy.text, { via: taxonomy.via, miss: taxonomy.miss, focus });
|
|
15024
|
-
if (!taxonomy.miss) taxonomyTurn.lane = "teach";
|
|
15221
|
+
if (!taxonomy.miss) { taxonomyTurn.lane = "teach"; taxonomyTurn.goal = "teach/remember a new fact"; }
|
|
15025
15222
|
return withLast(taxonomyTurn, "teach/remember a new fact");
|
|
15026
15223
|
}
|
|
15027
15224
|
}
|
|
@@ -15066,6 +15263,10 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
15066
15263
|
note(trace, `goal: ${goal}`);
|
|
15067
15264
|
note(trace, "lane: answerMemoryClassQuery — matched a memory-store class noun, answered off the .tmct/memory store's own individuals");
|
|
15068
15265
|
const turn = plainTurn(workingLine, memClass.text, { via: memClass.miss ? "miss" : "fact", miss: !!memClass.miss, focus });
|
|
15266
|
+
// A bare numeric count ("1 source.") stays silent, matching every other
|
|
15267
|
+
// count lane's tested contract — only a real list enumeration gets the
|
|
15268
|
+
// trailer. answerMemoryClassQuery serves both shapes through one lane.
|
|
15269
|
+
if (!memClass.miss && memClass.kind !== "count") turn.goal = goal;
|
|
15069
15270
|
if (memClass.pending) turn.detail = { traversal: null, matches: [], pending: memClass.pending };
|
|
15070
15271
|
return withLast(turn, goal);
|
|
15071
15272
|
}
|
|
@@ -15091,6 +15292,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
15091
15292
|
note(trace, `goal: ${goal}`);
|
|
15092
15293
|
note(trace, "lane: answerMembershipList — matched a bare 'list <noun>' over taught isa-facts whose OBJECT is that class");
|
|
15093
15294
|
const turn = plainTurn(workingLine, memberList.text, { via: memberList.miss ? "miss" : "fact", miss: !!memberList.miss, focus });
|
|
15295
|
+
if (!memberList.miss) turn.goal = goal;
|
|
15094
15296
|
if (memberList.pending) turn.detail = { traversal: null, matches: [], pending: memberList.pending };
|
|
15095
15297
|
return withLast(turn, goal);
|
|
15096
15298
|
}
|