@polycode-projects/the-mechanical-code-talker 4.1.1 → 4.1.3
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 +31 -18
- package/bin/tmct.mjs +3 -0
- package/data/templates/responses.jsonl +3 -0
- package/package.json +2 -1
- package/src/adapters/memory/core.mjs +1358 -196
- package/src/adapters/memory/inspect.mjs +11 -0
- package/src/adapters/memory/shacl.mjs +38 -0
- package/src/adapters/p2p/webrtc-transport.mjs +28 -5
- package/src/domain/ask-vocab.mjs +39 -0
- package/src/domain/ask.mjs +183 -34
- package/src/domain/grammar/assert.mjs +8 -2
- package/src/domain/hanoi-board.mjs +232 -0
- package/src/domain/ingest-facts.mjs +120 -0
- package/src/domain/interpret/normalize.mjs +49 -0
- package/src/domain/memory/compaction.mjs +284 -0
- package/src/domain/memory/resolution.mjs +171 -0
- package/src/domain/memory/trust.mjs +175 -5
- package/src/domain/memory-facts.mjs +139 -0
- package/src/domain/p2p/facts.mjs +21 -0
- package/src/domain/p2p/peer-id.mjs +15 -0
- package/src/domain/p2p/provenance-relabel.mjs +13 -2
- package/src/domain/p2p/sync-filter.mjs +5 -1
- package/src/domain/p2p/wire.mjs +7 -4
- package/src/domain/scene-compose.mjs +2 -2
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/services/adventure-viz.mjs +5 -1
- package/src/services/adventure.mjs +70 -44
- package/src/services/chat-page-viz.mjs +381 -310
- package/src/services/chat.mjs +273 -155
- package/src/services/code-explorer-viz.mjs +141 -54
- package/src/services/index.mjs +1 -1
- package/src/services/ingest-viz.mjs +134 -9
- package/src/services/ledger-viz.mjs +7 -4
- package/src/services/memory-panel-viz.mjs +8 -3
- package/src/services/mud-turn.mjs +11 -8
- package/src/services/mud-viz.mjs +441 -206
- package/src/services/p2p-room.mjs +110 -23
- package/src/services/plan-viz.mjs +63 -4
- package/src/services/research-viz.mjs +18 -7
- package/src/services/share-overlay-viz.mjs +623 -0
- package/src/services/spider-fly-viz.mjs +2 -2
- package/src/services/sprite-catalog-viz.mjs +303 -78
- package/src/surfaces/web/adventure-browser-entry.mjs +27 -5
- package/src/surfaces/web/chat-browser-entry.mjs +37 -10
- package/src/surfaces/web/code-explorer-browser-entry.mjs +4 -3
- package/src/surfaces/web/ingest-browser-entry.mjs +73 -12
- package/src/surfaces/web/ledger-browser-entry.mjs +32 -7
- package/src/surfaces/web/memory-ask-browser.bundle.js +149 -116
- package/src/surfaces/web/mud-browser-entry.mjs +38 -7
- package/src/surfaces/web/p2p-browser-entry.mjs +1 -1
- package/src/surfaces/web/plan-browser-entry.mjs +33 -2
- package/src/surfaces/web/research-browser-entry.mjs +11 -19
- package/src/surfaces/web/sprites-browser-entry.mjs +39 -8
- package/src/surfaces/web/tmct-surface.mjs +12 -0
- package/src/surfaces/web/turn-session.mjs +10 -3
package/src/services/chat.mjs
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// loop, src/surfaces/tui/app.mjs's Ink shell).
|
|
19
19
|
|
|
20
20
|
import { join, dirname } from "node:path";
|
|
21
|
-
import { dispatchTool, loadGraph, TOOLS } from "../tools/server.mjs";
|
|
21
|
+
import { dispatchTool, dispatchToolStructured, loadGraph, TOOLS } from "../tools/server.mjs";
|
|
22
22
|
import { ToolError } from "../adapters/config.mjs";
|
|
23
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";
|
|
@@ -38,8 +38,9 @@ import { splitSentences, carriesASentenceBoundary } from "./sentences.mjs";
|
|
|
38
38
|
import {
|
|
39
39
|
VERB_TO_KIND, WHERE_MARKERS, MENTION_MARKERS, ENTITY_TO_TYPE, PASSIVE_PARTICIPLE_TO_KIND,
|
|
40
40
|
stripTrailingScopeFiller, stripTrailingDiscourseTag, EDGE_NOUN_TO_METRIC, RELATIONS, LIST_TRIGGERS,
|
|
41
|
+
locativePreposition,
|
|
41
42
|
} from "../domain/ask-vocab.mjs";
|
|
42
|
-
import { COUNTERFACTUAL_RE, correctMisspellings, applyPreambleFrames, expandContractions, normalizeQuery, stripFillerWords, escapeRegex, kindNounAnaphoraHint } from "../domain/interpret/normalize.mjs";
|
|
43
|
+
import { COUNTERFACTUAL_RE, correctMisspellings, applyPreambleFrames, expandContractions, normalizeQuery, stripFillerWords, escapeRegex, kindNounAnaphoraHint, datedTeachSuffix } from "../domain/interpret/normalize.mjs";
|
|
43
44
|
import { setDefaultNlpAdapter } from "../domain/interpret/nlp-registry.mjs";
|
|
44
45
|
import { setConstructionBanks } from "../domain/interpret/strategies/constructions.mjs";
|
|
45
46
|
import { nlpAdapter } from "../adapters/ask-nlp.mjs";
|
|
@@ -84,12 +85,6 @@ export { uuidv7 };
|
|
|
84
85
|
// importing createSession/runChat/SESSION_LOG_DIR/PROMPT from chat.mjs.
|
|
85
86
|
export { createSession, runChat, SESSION_LOG_DIR, PROMPT } from "./chat-session.mjs";
|
|
86
87
|
|
|
87
|
-
/** dispatchTool("tmct_ask", …) returns the prose answer plus a delimited
|
|
88
|
-
* machine-readable envelope; the TUI shows the prose only. Reused verbatim
|
|
89
|
-
* when chat builds the same string from a direct ask() call (the
|
|
90
|
-
* focus/contextId path), so runTurn parses one envelope shape either way. */
|
|
91
|
-
const ASK_ENVELOPE_DELIM = "\n\n---tmct_ask---\n";
|
|
92
|
-
|
|
93
88
|
/** The context pronouns a focus can stand in for — a bare `it`/`this`/`that`/`here`
|
|
94
89
|
* as a command arg reuses the focus, and the ask engine resolves the same words
|
|
95
90
|
* in a question against the contextId we thread through. */
|
|
@@ -593,8 +588,12 @@ const RESTRICTOR_VERB_RE = new RegExp(
|
|
|
593
588
|
* node (parseAggregate) already evaluates a restrictor tail correctly via
|
|
594
589
|
* parseSetPhrase, so once the tail names a real relation verb
|
|
595
590
|
* (RESTRICTOR_VERB_RE), decline here and let the turn fall through to the
|
|
596
|
-
* real ask engine instead of returning a misleading bare total.
|
|
597
|
-
|
|
591
|
+
* real ask engine instead of returning a misleading bare total.
|
|
592
|
+
*
|
|
593
|
+
* `uiContext` picks the empty-graph remedy: only a terminal session can run
|
|
594
|
+
* the index/--repo commands, so a page names what it can actually offer
|
|
595
|
+
* instead (see vocabExampleHint's own split). */
|
|
596
|
+
export function answerCount(graph, query, { uiContext = "cli" } = {}) {
|
|
598
597
|
if (!graph) return null;
|
|
599
598
|
// ANAPHORIC counts ("how many of those are tested", "count them", "how many of
|
|
600
599
|
// them") count the PREVIOUS answer's set, not a graph kind — decline so the turn
|
|
@@ -620,6 +619,9 @@ export function answerCount(graph, query) {
|
|
|
620
619
|
// When no code graph is loaded, countableKinds(graph) is genuinely
|
|
621
620
|
// empty — an honest, non-dangling message pointing at how to load one.
|
|
622
621
|
if (!kinds.length) {
|
|
622
|
+
if (uiContext === "browser") {
|
|
623
|
+
return `I can't count "${noun}" — this page holds taught facts only, so there's no code structure to count.`;
|
|
624
|
+
}
|
|
623
625
|
return `I can't count "${noun}" — no code graph is loaded yet, so there's nothing to count ` +
|
|
624
626
|
`(index this repo with "tmct index", point me at another with --repo, or run "npm run example:mini").`;
|
|
625
627
|
}
|
|
@@ -890,16 +892,12 @@ const MEMORY_CLASS_PLURALS = {
|
|
|
890
892
|
const MEMORY_CLASS_LIST_TRIGGER_RE = /^(?:list|show(?:\s+me)?)\s+(?:all\s+|the\s+)?([a-z][a-z-]*)\s*(.*)$/i;
|
|
891
893
|
const MEMORY_CLASS_COUNT_TRIGGER_RE = /^(?:how\s+many|number\s+of|count(?:\s+the)?)\s+(?:all\s+)?([a-z][a-z-]*)\s*(.*)$/i;
|
|
892
894
|
|
|
893
|
-
/** One display line per stored individual of a class
|
|
894
|
-
*
|
|
895
|
-
*
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
if (row) return renderFactLine(row);
|
|
900
|
-
}
|
|
901
|
-
return String(ind.label || ind.id || "").trim();
|
|
902
|
-
}
|
|
895
|
+
/** One display line per stored individual of a class — its own label. Facts
|
|
896
|
+
* never come through here: they are listed per TRIPLE, through the same
|
|
897
|
+
* renderFactLine every other fact list uses, because the store holds one
|
|
898
|
+
* record per asserting SOURCE and listing those would print a corroborated
|
|
899
|
+
* triple once per source that vouched for it. */
|
|
900
|
+
const memoryClassLine = (ind) => String(ind.label || ind.id || "").trim();
|
|
903
901
|
|
|
904
902
|
/** Recognise "list <memory-class>" (any class) and "how many <meta-class>"
|
|
905
903
|
* (Session/Source/Rule — Fact/Utterance counts stay with answerMemoryCount) and
|
|
@@ -934,11 +932,11 @@ async function answerMemoryClassQuery(memoryDir, query) {
|
|
|
934
932
|
try { ({ loadMemory, readFactRows } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
935
933
|
let mem;
|
|
936
934
|
try { mem = await loadMemory(memoryDir); } catch { return null; }
|
|
937
|
-
const
|
|
935
|
+
const rows = cls === "Fact" ? readFactRows(mem) : null;
|
|
936
|
+
const inds = rows || (mem.individuals || []).filter((i) => (i.class || "") === cls);
|
|
938
937
|
if (countM) return { text: `${inds.length} ${inds.length === 1 ? plural.replace(/s$/, "") : plural}.`, kind: "count" };
|
|
939
938
|
if (!inds.length) return { text: `I don't have any ${plural} stored yet.`, miss: true };
|
|
940
|
-
const
|
|
941
|
-
const lines = inds.map((ind) => memoryClassLine(cls, ind, factByLabel));
|
|
939
|
+
const lines = rows ? rows.map(renderFactLine) : inds.map(memoryClassLine);
|
|
942
940
|
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
943
941
|
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
944
942
|
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
@@ -1551,13 +1549,21 @@ const T_WHY_EMPTY = "miss-no-previous-answer";
|
|
|
1551
1549
|
/** Empty / degenerate-graph variants (#3/#5): shown when the loaded graph has 0
|
|
1552
1550
|
* modules (a graph-less bootstrap OR a graph.json with no code entities). They
|
|
1553
1551
|
* orient toward `--repo`/`tmct init` + the seeded vocabulary instead of
|
|
1554
|
-
* over-promising "ask me about this codebase".
|
|
1552
|
+
* over-promising "ask me about this codebase". The `_BROWSER` twins carry the
|
|
1553
|
+
* same content with the CLI-only remedy (`--repo`, `tmct index`,
|
|
1554
|
+
* `npm run example:mini`) swapped for what a page visitor can actually do —
|
|
1555
|
+
* selected via uiTemplateId()'s uiContext branch, the same split
|
|
1556
|
+
* offRampClause/vocabExampleHint already apply to the surrounding prose. */
|
|
1555
1557
|
const T_GREETING_EMPTY = "conversational-greeting-empty";
|
|
1558
|
+
const T_GREETING_EMPTY_BROWSER = "conversational-greeting-empty-browser";
|
|
1556
1559
|
const T_ORIENTATION_EMPTY = "orientation-empty";
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
*
|
|
1560
|
+
const T_ORIENTATION_EMPTY_BROWSER = "orientation-empty-browser";
|
|
1561
|
+
/** IDENTITY answers — self-description and the "no LLM" clarification. Neither
|
|
1562
|
+
* varies with graph state (empty vs. populated); identity-self DOES vary with
|
|
1563
|
+
* uiContext (its `--repo <path>` clause is CLI-only), so it alone carries a
|
|
1564
|
+
* `_BROWSER` twin. */
|
|
1560
1565
|
const T_IDENTITY_SELF = "identity-self";
|
|
1566
|
+
const T_IDENTITY_SELF_BROWSER = "identity-self-browser";
|
|
1561
1567
|
const T_IDENTITY_NOT_LLM = "identity-not-an-llm";
|
|
1562
1568
|
const T_IDENTITY_NO_FEELINGS = "identity-no-feelings";
|
|
1563
1569
|
/** Confirms the honest-miss promise itself when a user asks about it directly
|
|
@@ -1932,6 +1938,21 @@ export function renderVerbose(last) {
|
|
|
1932
1938
|
return { text: lines.join("\n"), empty: false };
|
|
1933
1939
|
}
|
|
1934
1940
|
|
|
1941
|
+
/** Picks a template id's browser twin when `uiContext` is "browser", else the
|
|
1942
|
+
* CLI id unchanged — the same surface split offRampClause/vocabExampleHint
|
|
1943
|
+
* apply to hand-written prose, applied here to which DATA row gets rendered. */
|
|
1944
|
+
const uiTemplateId = (uiContext, id, browserId) => (uiContext === "browser" ? browserId : id);
|
|
1945
|
+
|
|
1946
|
+
/** The "here's what you CAN ask" tail on a decline that names no term of its own
|
|
1947
|
+
* (the SQL-statement and arithmetic shapes below). `ctx.vocabHint` already
|
|
1948
|
+
* carries the session-gated vocabulary/teach pointer, so only the CODE-graph
|
|
1949
|
+
* half needs the surface split: pointing at a repo is a command a terminal can
|
|
1950
|
+
* run and a page cannot. */
|
|
1951
|
+
const offRampClause = (ctx) => {
|
|
1952
|
+
const hint = ctx.vocabHint || 'Try "what is a dog" for vocabulary.';
|
|
1953
|
+
return ctx.uiContext === "browser" ? hint : `${hint} Or point me at a repo with --repo <path>.`;
|
|
1954
|
+
};
|
|
1955
|
+
|
|
1935
1956
|
/** Recognise a conversational expression and return a templated turn result, or null
|
|
1936
1957
|
* to fall through to counts/ask. Handled BEFORE slash-commands' non-slash siblings:
|
|
1937
1958
|
* greetings, thanks, help/orientation, farewell (ends the session via `end:true`),
|
|
@@ -2011,7 +2032,9 @@ function conversationalTurn(line, ctx) {
|
|
|
2011
2032
|
// modules leads with the (now provably-correct) vocabulary hint instead of
|
|
2012
2033
|
// over-promising "ask me about this codebase". Phrase-specific variants (good
|
|
2013
2034
|
// morning, hello there) keep their wording; only the default greeting swaps.
|
|
2014
|
-
const id = (!T_GREETING_BY_PHRASE[greetHit] && noCodeGraph(ctx.graph))
|
|
2035
|
+
const id = (!T_GREETING_BY_PHRASE[greetHit] && noCodeGraph(ctx.graph))
|
|
2036
|
+
? uiTemplateId(ctx.uiContext, T_GREETING_EMPTY, T_GREETING_EMPTY_BROWSER)
|
|
2037
|
+
: (T_GREETING_BY_PHRASE[greetHit] || T_GREETING);
|
|
2015
2038
|
note(ctx.trace, `pattern: template "${id}" (data/templates/responses.jsonl)`);
|
|
2016
2039
|
return mk(t(id, { vocabHint: ctx.vocabHint }), { lane: "greeting" });
|
|
2017
2040
|
}
|
|
@@ -2055,8 +2078,7 @@ function conversationalTurn(line, ctx) {
|
|
|
2055
2078
|
note(ctx.trace, "goal: nonsense input shaped like a SQL statement — a targeted decline, not the identity blurb");
|
|
2056
2079
|
note(ctx.trace, "lane: conversational — SQL-statement decline (SQL_STATEMENT_RE)");
|
|
2057
2080
|
return mk(
|
|
2058
|
-
|
|
2059
|
-
+ "Try \"what is a dog\" for vocabulary, or point me at a repo with --repo <path>.",
|
|
2081
|
+
`That reads like a SQL statement, not a question about a code graph or taught facts. ${offRampClause(ctx)}`,
|
|
2060
2082
|
{ lane: "help" },
|
|
2061
2083
|
);
|
|
2062
2084
|
}
|
|
@@ -2064,15 +2086,14 @@ function conversationalTurn(line, ctx) {
|
|
|
2064
2086
|
note(ctx.trace, "goal: arithmetic — not a code/vocabulary question, an honest decline");
|
|
2065
2087
|
note(ctx.trace, "lane: conversational — arithmetic decline (ARITHMETIC_RE)");
|
|
2066
2088
|
return mk(
|
|
2067
|
-
|
|
2068
|
-
+ "Try \"what is a dog\" for vocabulary, or point me at a repo with --repo <path>.",
|
|
2089
|
+
`I don't do arithmetic — I answer questions about a code graph or taught facts. ${offRampClause(ctx)}`,
|
|
2069
2090
|
{ lane: "help" },
|
|
2070
2091
|
);
|
|
2071
2092
|
}
|
|
2072
2093
|
if (identityPhraseMatch(raw)) {
|
|
2073
2094
|
note(ctx.trace, "goal: identity — who/what tmct is, not a capability listing");
|
|
2074
2095
|
note(ctx.trace, "lane: conversational — identity (IDENTITY_PHRASES closed set)");
|
|
2075
|
-
return mk(t(T_IDENTITY_SELF), { lane: "help" });
|
|
2096
|
+
return mk(t(uiTemplateId(ctx.uiContext, T_IDENTITY_SELF, T_IDENTITY_SELF_BROWSER), { vocabHint: ctx.vocabHint }), { lane: "help" });
|
|
2076
2097
|
}
|
|
2077
2098
|
{
|
|
2078
2099
|
const metaHit = META_COMMAND_ANSWERS.find(([re]) => re.test(raw));
|
|
@@ -2092,7 +2113,7 @@ function conversationalTurn(line, ctx) {
|
|
|
2092
2113
|
|| CAPABILITY_PHRASES.some((re) => re.test(applyPreambleFrames(raw))) || ORIENT_OPENERS.has(q)) {
|
|
2093
2114
|
note(ctx.trace, "goal: get oriented — what can tmct answer, how do I start");
|
|
2094
2115
|
note(ctx.trace, "lane: conversational — help/orientation (CAPABILITY_PHRASES/ORIENT_OPENERS / bare help / ?)");
|
|
2095
|
-
return mk(orientationAnswer(ctx.templates, ctx.graph, ctx.vocabHint), { lane: "help" });
|
|
2116
|
+
return mk(orientationAnswer(ctx.templates, ctx.graph, ctx.vocabHint, ctx.uiContext), { lane: "help" });
|
|
2096
2117
|
}
|
|
2097
2118
|
// Fuzzy-typo fallback (A4): every exact/collapsed closed-set lookup above missed —
|
|
2098
2119
|
// try a bounded edit-distance match against the flattened conversational phrase
|
|
@@ -2108,9 +2129,13 @@ function conversationalTurn(line, ctx) {
|
|
|
2108
2129
|
note(ctx.trace, `lane: conversational — fuzzy typo tolerance (${bucket})`);
|
|
2109
2130
|
if (bucket === "bye") return mk(t(T_FAREWELL), { end: true });
|
|
2110
2131
|
if (bucket === "thanks") return mk(t(T_THANKS), { lane: "thanks" });
|
|
2111
|
-
if (bucket === "identity")
|
|
2112
|
-
|
|
2113
|
-
|
|
2132
|
+
if (bucket === "identity") {
|
|
2133
|
+
return mk(t(uiTemplateId(ctx.uiContext, T_IDENTITY_SELF, T_IDENTITY_SELF_BROWSER), { vocabHint: ctx.vocabHint }), { lane: "help" });
|
|
2134
|
+
}
|
|
2135
|
+
if (bucket === "capability") return mk(orientationAnswer(ctx.templates, ctx.graph, ctx.vocabHint, ctx.uiContext), { lane: "help" });
|
|
2136
|
+
const id = (!T_GREETING_BY_PHRASE[fuzzyHit] && noCodeGraph(ctx.graph))
|
|
2137
|
+
? uiTemplateId(ctx.uiContext, T_GREETING_EMPTY, T_GREETING_EMPTY_BROWSER)
|
|
2138
|
+
: (T_GREETING_BY_PHRASE[fuzzyHit] || T_GREETING);
|
|
2114
2139
|
return mk(t(id, { vocabHint: ctx.vocabHint }), { lane: "greeting" });
|
|
2115
2140
|
}
|
|
2116
2141
|
}
|
|
@@ -2165,10 +2190,14 @@ function orientationExamples(graph) {
|
|
|
2165
2190
|
}
|
|
2166
2191
|
|
|
2167
2192
|
/** The orientation surface, module-aware: the empty variant (→ the provably-correct
|
|
2168
|
-
* vocabulary hint + --repo/tmct init) when there's no code
|
|
2169
|
-
* (with live {example1}/{example2} query examples from
|
|
2170
|
-
|
|
2171
|
-
|
|
2193
|
+
* vocabulary hint + --repo/tmct init, or its browser twin) when there's no code
|
|
2194
|
+
* graph, the standard one (with live {example1}/{example2} query examples from
|
|
2195
|
+
* the loaded graph) otherwise. */
|
|
2196
|
+
function orientationAnswer(templates, graph, vocabHint, uiContext = "cli") {
|
|
2197
|
+
if (noCodeGraph(graph)) {
|
|
2198
|
+
const id = uiTemplateId(uiContext, T_ORIENTATION_EMPTY, T_ORIENTATION_EMPTY_BROWSER);
|
|
2199
|
+
return tRender(templates, id, { vocabHint }) ?? TEMPLATES_UNAVAILABLE;
|
|
2200
|
+
}
|
|
2172
2201
|
return tRender(templates, T_ORIENTATION, orientationExamples(graph)) ?? TEMPLATES_UNAVAILABLE;
|
|
2173
2202
|
}
|
|
2174
2203
|
|
|
@@ -2179,20 +2208,24 @@ function orientationAnswer(templates, graph, vocabHint) {
|
|
|
2179
2208
|
const ORIENTATION_EMPTY_FALLBACK = "I'm tmct — a deterministic, offline chat assistant (no LLM). "
|
|
2180
2209
|
+ "For code structure (imports, calls, definitions) run `tmct index` here, point me at a repo with `--repo <path>`, "
|
|
2181
2210
|
+ "or try the shipped example `npm run example:mini`. /help for commands.";
|
|
2211
|
+
const ORIENTATION_EMPTY_FALLBACK_BROWSER = "I'm tmct — a deterministic, offline chat assistant (no LLM). "
|
|
2212
|
+
+ "Teach me a fact directly, e.g. \"every bug is an issue\", or ask about what's already loaded. /help for commands.";
|
|
2182
2213
|
|
|
2183
2214
|
/** A dynamic orientation string for the meta/self lane: a /stats-style overview
|
|
2184
2215
|
* when a code graph is loaded, else the honest empty-graph orientation — rendered
|
|
2185
|
-
* through the SAME template (T_ORIENTATION_EMPTY)
|
|
2186
|
-
* branch uses, so there is exactly one copy of
|
|
2187
|
-
* two hand-duplicated strings. */
|
|
2188
|
-
function orientationText(graph, templates, vocabHint) {
|
|
2216
|
+
* through the SAME template (T_ORIENTATION_EMPTY/T_ORIENTATION_EMPTY_BROWSER)
|
|
2217
|
+
* conversationalTurn's orientation branch uses, so there is exactly one copy of
|
|
2218
|
+
* that wording to keep in sync, not two hand-duplicated strings. */
|
|
2219
|
+
function orientationText(graph, templates, vocabHint, uiContext = "cli") {
|
|
2189
2220
|
// A null (never-loaded) graph reads as "unknown, not empty" to noCodeGraph,
|
|
2190
2221
|
// which is the right call for the greeting/orientation CARD — but there is no
|
|
2191
2222
|
// entity count to render from one either, so the empty wording is the only
|
|
2192
2223
|
// truthful thing left to say. Without this the entity tally below threw on a
|
|
2193
2224
|
// graph-less runTurn.
|
|
2194
2225
|
if (!graph || noCodeGraph(graph)) {
|
|
2195
|
-
|
|
2226
|
+
const id = uiTemplateId(uiContext, T_ORIENTATION_EMPTY, T_ORIENTATION_EMPTY_BROWSER);
|
|
2227
|
+
const fallback = uiContext === "browser" ? ORIENTATION_EMPTY_FALLBACK_BROWSER : ORIENTATION_EMPTY_FALLBACK;
|
|
2228
|
+
return tRender(templates, id, { vocabHint }) ?? fallback;
|
|
2196
2229
|
}
|
|
2197
2230
|
const by = (cls) => (graph.individuals || []).filter((i) => (i.class || "") === cls).length;
|
|
2198
2231
|
const parts = [];
|
|
@@ -2635,13 +2668,33 @@ const INSTANCE_TYPE_TEACH_RE = /^([a-z][\w]*(?:-[\w]+)+)\s+is\s+an?\s+([a-z][\w-
|
|
|
2635
2668
|
// Bare article-led kind-of taxonomy: "a disk is a kind of game piece".
|
|
2636
2669
|
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;
|
|
2637
2670
|
|
|
2671
|
+
/** Wink lemmatises a token with no sentence around it, so a surface that is
|
|
2672
|
+
* both a plural noun and a 3rd-person-singular verb comes back as the NOUN
|
|
2673
|
+
* singular: "lives" → "life", "halves" → "half", "shelves" → "shelf". Every
|
|
2674
|
+
* caller here has already bound the word to a VERB slot, so the noun reading
|
|
2675
|
+
* is the wrong one, and left alone it mints an mgx:life-in predicate that
|
|
2676
|
+
* reads back as "ann lifes in paris".
|
|
2677
|
+
*
|
|
2678
|
+
* Spelling identifies the noun reading on its own: an "-f"/"-fe" noun forms
|
|
2679
|
+
* its plural in "-ves", and no verb base ends in a bare "-v", so a "-ves"
|
|
2680
|
+
* surface whose lemma ends in "-f"/"-fe" was read as that plural. The verb
|
|
2681
|
+
* base is then the surface minus its "s" ("lives" → "live", "shelves" →
|
|
2682
|
+
* "shelve"). Deliberately this one spelling pair and no more (same accepted
|
|
2683
|
+
* trade as singularizeSurface, no real morphology) — a "-ves" verb wink
|
|
2684
|
+
* already reads correctly ("leaves" → "leave", "moves" → "move") keeps its
|
|
2685
|
+
* lemma untouched. */
|
|
2686
|
+
function verbReadingOfLemma(surface, lemma) {
|
|
2687
|
+
if (!/ves$/i.test(surface) || !/fe?$/i.test(lemma)) return lemma;
|
|
2688
|
+
return surface.slice(0, -1);
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2638
2691
|
/** Verb → lemma via the prose adapter, degrading to the word itself. */
|
|
2639
2692
|
async function verbLemma(word) {
|
|
2640
2693
|
const w = String(word || "").toLowerCase();
|
|
2641
2694
|
try {
|
|
2642
2695
|
const { proseLemma } = await import("../adapters/prose-nlp.mjs");
|
|
2643
2696
|
const lemma = proseLemma();
|
|
2644
|
-
return lemma ? lemma(w) : w;
|
|
2697
|
+
return lemma ? verbReadingOfLemma(w, lemma(w)) : w;
|
|
2645
2698
|
} catch { return w; }
|
|
2646
2699
|
}
|
|
2647
2700
|
|
|
@@ -2829,7 +2882,7 @@ const teachProvenanceTag = (sessionId, ts) => `teach:chat${sessionId ? `:${sessi
|
|
|
2829
2882
|
/** Reify one teach-lane fact + confirm (shared by the property and ownership
|
|
2830
2883
|
* frames). Lazy + failure-tolerated: a write failure degrades to null (the
|
|
2831
2884
|
* teach-miss text stands), never a crash. */
|
|
2832
|
-
async function teachFact(memoryDir, sessionId, { subject, predicate, object, quantifier = "" }) {
|
|
2885
|
+
async function teachFact(memoryDir, sessionId, { subject, predicate, object, quantifier = "", observedAt = "", dateText = "" }) {
|
|
2833
2886
|
try {
|
|
2834
2887
|
const { appendFact, normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
2835
2888
|
const s = normFactTerm(subject);
|
|
@@ -2839,9 +2892,13 @@ async function teachFact(memoryDir, sessionId, { subject, predicate, object, qua
|
|
|
2839
2892
|
subject: s, predicate, object: o,
|
|
2840
2893
|
provenance: teachProvenanceTag(sessionId, new Date().toISOString()),
|
|
2841
2894
|
...(quantifier ? { quantifier } : {}),
|
|
2895
|
+
...(observedAt ? { observedAt } : {}),
|
|
2842
2896
|
});
|
|
2843
2897
|
const phrase = predicatePhrase(predicate);
|
|
2844
|
-
|
|
2898
|
+
// The dated-teach frame's own echo: the user typed the date, so the
|
|
2899
|
+
// acknowledgment shows it was registered rather than silently dropped.
|
|
2900
|
+
const dateSuffix = observedAt && dateText ? ` (as of ${dateText})` : "";
|
|
2901
|
+
return { text: `noted — remembered: ${s} ${phrase} ${o}${dateSuffix}`, via: "assert", miss: false };
|
|
2845
2902
|
} catch {
|
|
2846
2903
|
return null;
|
|
2847
2904
|
}
|
|
@@ -3094,7 +3151,7 @@ async function ungroundedPairHint(payload, lexicon, memoryDir, cache = null, gra
|
|
|
3094
3151
|
* the SUBJECT, not about the "remember that" wrapper). Only the "every"
|
|
3095
3152
|
* determiner records a quantifier (point 3: "a"/bare/"your" read as one
|
|
3096
3153
|
* specific entity, not a class-level generalization). */
|
|
3097
|
-
async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }, cache = null) {
|
|
3154
|
+
async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon, observedAt, dateText }, cache = null) {
|
|
3098
3155
|
if (!memoryDir) return null;
|
|
3099
3156
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
3100
3157
|
if (!m) return null;
|
|
@@ -3137,14 +3194,14 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }
|
|
|
3137
3194
|
if (lookupNoun(lex, objectRaw) || GENERIC_ANCHOR_NOUNS.has(String(objectRaw).toLowerCase())
|
|
3138
3195
|
|| (await isGroundedByFact(objectRaw, memoryDir, cache))) {
|
|
3139
3196
|
return teachFact(memoryDir, sessionId, {
|
|
3140
|
-
subject, predicate: SUBCLASS_PREDICATE, object: objectRaw, quantifier,
|
|
3197
|
+
subject, predicate: SUBCLASS_PREDICATE, object: objectRaw, quantifier, observedAt, dateText,
|
|
3141
3198
|
});
|
|
3142
3199
|
}
|
|
3143
3200
|
if (lookupAdjective(lex, objectRaw)) {
|
|
3144
3201
|
// property assertions are about ONE specific entity — never a quantifier,
|
|
3145
3202
|
// even when phrased with "every" (point 3).
|
|
3146
3203
|
return teachFact(memoryDir, sessionId, {
|
|
3147
|
-
subject, predicate: HAS_PROPERTY_PREDICATE, object: objectRaw,
|
|
3204
|
+
subject, predicate: HAS_PROPERTY_PREDICATE, object: objectRaw, observedAt, dateText,
|
|
3148
3205
|
});
|
|
3149
3206
|
}
|
|
3150
3207
|
return null; // Y unknown too — decline honestly, never guess
|
|
@@ -3221,7 +3278,7 @@ async function objectReadsAsNonNoun(word) {
|
|
|
3221
3278
|
return false;
|
|
3222
3279
|
}
|
|
3223
3280
|
}
|
|
3224
|
-
async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon, classIntent = false, graph = null }, cache = null) {
|
|
3281
|
+
async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon, classIntent = false, graph = null, observedAt, dateText }, cache = null) {
|
|
3225
3282
|
if (!memoryDir) return null;
|
|
3226
3283
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
3227
3284
|
if (!m) return null;
|
|
@@ -3266,7 +3323,7 @@ async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon, c
|
|
|
3266
3323
|
? (lookupNoun(lex, subjectRaw)?.lemma || singularizeSurface(subjectRaw))
|
|
3267
3324
|
: subjectRaw;
|
|
3268
3325
|
return teachFact(memoryDir, sessionId, {
|
|
3269
|
-
subject, predicate: SUBCLASS_PREDICATE, object: objectRaw, quantifier,
|
|
3326
|
+
subject, predicate: SUBCLASS_PREDICATE, object: objectRaw, quantifier, observedAt, dateText,
|
|
3270
3327
|
});
|
|
3271
3328
|
}
|
|
3272
3329
|
|
|
@@ -3319,7 +3376,7 @@ async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon, c
|
|
|
3319
3376
|
* otherwise provide. "the cache is bespoke" and "Mary is female" both carry
|
|
3320
3377
|
* one of those signals (the leading "the", and capitalization,
|
|
3321
3378
|
* respectively); "module is banana" carries none. */
|
|
3322
|
-
async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon, graph = null }, cache = null) {
|
|
3379
|
+
async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon, graph = null, observedAt, dateText }, cache = null) {
|
|
3323
3380
|
if (!memoryDir) return null;
|
|
3324
3381
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
3325
3382
|
if (!m) return null;
|
|
@@ -3358,7 +3415,7 @@ async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon
|
|
|
3358
3415
|
? (lookupNoun(lex, subjectRaw)?.lemma || singularizeSurface(subjectRaw))
|
|
3359
3416
|
: subjectRaw;
|
|
3360
3417
|
return teachFact(memoryDir, sessionId, {
|
|
3361
|
-
subject: classSubject, predicate: HAS_PROPERTY_PREDICATE, object: objectRaw, quantifier: "every",
|
|
3418
|
+
subject: classSubject, predicate: HAS_PROPERTY_PREDICATE, object: objectRaw, quantifier: "every", observedAt, dateText,
|
|
3362
3419
|
});
|
|
3363
3420
|
}
|
|
3364
3421
|
// Subject-side groundedness — strip a leading "the"/"a"/"an" first
|
|
@@ -3379,7 +3436,7 @@ async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon
|
|
|
3379
3436
|
const subjectGrounded = capitalized || factGrounded || genericAnchor || lexiconGrounded;
|
|
3380
3437
|
if (!subjectGrounded) return null; // no deliberate-entity signal — never a guessed mint
|
|
3381
3438
|
return teachFact(memoryDir, sessionId, {
|
|
3382
|
-
subject: subjectRaw, predicate: HAS_PROPERTY_PREDICATE, object: objectRaw,
|
|
3439
|
+
subject: subjectRaw, predicate: HAS_PROPERTY_PREDICATE, object: objectRaw, observedAt, dateText,
|
|
3383
3440
|
});
|
|
3384
3441
|
}
|
|
3385
3442
|
|
|
@@ -3725,9 +3782,7 @@ async function generalVerbPredicate(verb) {
|
|
|
3725
3782
|
// "can a X <verb>" reader finds it (same reasoning as HAS_A above).
|
|
3726
3783
|
if (v === "can") return "mgx:capableOf";
|
|
3727
3784
|
try {
|
|
3728
|
-
const
|
|
3729
|
-
const lemma = proseLemma();
|
|
3730
|
-
const l = lemma ? lemma(v) : v;
|
|
3785
|
+
const l = await verbLemma(v);
|
|
3731
3786
|
if (l === "have") return HAS_A_PREDICATE;
|
|
3732
3787
|
return normFactPredicate(`mgx:${l}`);
|
|
3733
3788
|
} catch {
|
|
@@ -4304,7 +4359,7 @@ const NEGATIVE_UNIVERSAL_TEACH_RE = /^no\s+([\w-]+)\s+(is|are)\s+(?:an?\s+)?(?:(
|
|
|
4304
4359
|
/** The mint (or the reflexive refusal) for a NEGATIVE_UNIVERSAL_TEACH_RE
|
|
4305
4360
|
* match, shared by teachLane and the ACE-path reflexive gate: null when the
|
|
4306
4361
|
* sentence isn't this shape. */
|
|
4307
|
-
async function negativeUniversalTeach(sentence, { memoryDir, sessionId }) {
|
|
4362
|
+
async function negativeUniversalTeach(sentence, { memoryDir, sessionId, observedAt, dateText }) {
|
|
4308
4363
|
const m = String(sentence || "").trim().match(NEGATIVE_UNIVERSAL_TEACH_RE);
|
|
4309
4364
|
if (!m || !memoryDir) return null;
|
|
4310
4365
|
const plural = m[2].toLowerCase() === "are";
|
|
@@ -4318,7 +4373,7 @@ async function negativeUniversalTeach(sentence, { memoryDir, sessionId }) {
|
|
|
4318
4373
|
}
|
|
4319
4374
|
const { DISJOINT_PREDICATE } = await import("../domain/syllogise.mjs");
|
|
4320
4375
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
4321
|
-
subject, predicate: DISJOINT_PREDICATE, object,
|
|
4376
|
+
subject, predicate: DISJOINT_PREDICATE, object, observedAt, dateText,
|
|
4322
4377
|
});
|
|
4323
4378
|
if (!stored) {
|
|
4324
4379
|
return {
|
|
@@ -4345,13 +4400,13 @@ const NEGATIVE_UNIVERSAL_CAN_TEACH_RE = /^no\s+([\w-]+)\s+can\s+([a-z][\w-]*)[.!
|
|
|
4345
4400
|
/** The mint for a NEGATIVE_UNIVERSAL_CAN_TEACH_RE match, mirroring
|
|
4346
4401
|
* negativeUniversalTeach's own shape: null when the sentence isn't this
|
|
4347
4402
|
* shape. */
|
|
4348
|
-
async function negativeUniversalCanTeach(sentence, { memoryDir, sessionId }) {
|
|
4403
|
+
async function negativeUniversalCanTeach(sentence, { memoryDir, sessionId, observedAt, dateText }) {
|
|
4349
4404
|
const m = String(sentence || "").trim().match(NEGATIVE_UNIVERSAL_CAN_TEACH_RE);
|
|
4350
4405
|
if (!m || !memoryDir) return null;
|
|
4351
4406
|
const subject = singularizeSurface(m[1]);
|
|
4352
4407
|
const verb = m[2].toLowerCase();
|
|
4353
4408
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
4354
|
-
subject, predicate: NEG_CAPABLE_OF_PREDICATE, object: verb,
|
|
4409
|
+
subject, predicate: NEG_CAPABLE_OF_PREDICATE, object: verb, observedAt, dateText,
|
|
4355
4410
|
});
|
|
4356
4411
|
if (!stored) {
|
|
4357
4412
|
return {
|
|
@@ -4414,7 +4469,26 @@ async function teachExclusionReason(sentence) {
|
|
|
4414
4469
|
}
|
|
4415
4470
|
export { teachExclusionReason };
|
|
4416
4471
|
|
|
4417
|
-
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null, planHolder = null, graph = null, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
4472
|
+
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null, planHolder = null, graph = null, gameConfig = DEFAULT_GAME_CONFIG, observedAt = "", dateText = "" }) {
|
|
4473
|
+
// THE DATED TEACH FRAME — "<sentence> as of <date>" carries an explicit
|
|
4474
|
+
// mgx:observedAt past the same shapes this lane already teaches. Tried
|
|
4475
|
+
// ONCE, recursively, on the suffix-stripped text, the same
|
|
4476
|
+
// "rewrap and recurse into this same lane" idiom the conjunction pre-pass
|
|
4477
|
+
// below already uses. A miss on the stripped text falls through to the
|
|
4478
|
+
// untouched ORIGINAL query below — a question that happens to end in
|
|
4479
|
+
// "as of 2019" must keep asking, never get silently rewritten (see
|
|
4480
|
+
// datedTeachSuffix's own docblock) — and a dated teach that fails to parse
|
|
4481
|
+
// must never fall back to storing the same sentence undated.
|
|
4482
|
+
if (!observedAt && memoryDir) {
|
|
4483
|
+
const suffix = datedTeachSuffix(String(query));
|
|
4484
|
+
if (suffix) {
|
|
4485
|
+
const dated = await teachLane(suffix.stripped, {
|
|
4486
|
+
memoryDir, sessionId, lexicon, cache, planHolder, graph, gameConfig,
|
|
4487
|
+
observedAt: suffix.observedAt, dateText: suffix.dateText,
|
|
4488
|
+
});
|
|
4489
|
+
if (dated && !dated.miss) return dated;
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4418
4492
|
// A closed discourse-marker preamble ahead of a teach sentence ("howdy
|
|
4419
4493
|
// pardner, remember that TaskController is fragile") would otherwise
|
|
4420
4494
|
// corrupt TEACH_RE's own match, so strip it first. applyPreambleFrames is
|
|
@@ -4530,7 +4604,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4530
4604
|
if (memoryDir && !QUESTION_LEAD_RE.test(conjSrc) && /\s+and\s+/i.test(conjSrc)
|
|
4531
4605
|
&& !(await hasMidSentenceInterrogative(conjSrc))) {
|
|
4532
4606
|
const rewrap = (half) => (wrapped != null ? `remember that ${half}` : half);
|
|
4533
|
-
const recurse = (half) => teachLane(rewrap(half), { memoryDir, sessionId, lexicon, cache, planHolder, graph, gameConfig });
|
|
4607
|
+
const recurse = (half) => teachLane(rewrap(half), { memoryDir, sessionId, lexicon, cache, planHolder, graph, gameConfig, observedAt, dateText });
|
|
4534
4608
|
const stripNoted = (t) => String(t).replace(/^noted — remembered(?:\s+\d+\s+facts?)?:\s*/i, "").trim();
|
|
4535
4609
|
const shared = conjSrc.match(/^(.+?)\s+and\s+((?:is|are|has|have|can)\b.+)$/i);
|
|
4536
4610
|
const sharedSubject = shared ? shared[1].match(/^(.+?)\s+(?:is|are|has|have|can)\b/i)?.[1]?.trim() : null;
|
|
@@ -4688,7 +4762,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4688
4762
|
const positive = priorRows.find((r) => r.subject === negSubject && r.predicate === SUBCLASS_PREDICATE && r.object === negObject);
|
|
4689
4763
|
if (positive) {
|
|
4690
4764
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
4691
|
-
subject: retractSubject, predicate: NEG_SUBCLASS_PREDICATE, object: retractObject,
|
|
4765
|
+
subject: retractSubject, predicate: NEG_SUBCLASS_PREDICATE, object: retractObject, observedAt, dateText,
|
|
4692
4766
|
});
|
|
4693
4767
|
if (stored) {
|
|
4694
4768
|
return {
|
|
@@ -4822,8 +4896,8 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4822
4896
|
const negUniversalSrc = (wrapped ?? raw).replace(/[.!?]+\s*$/, "");
|
|
4823
4897
|
if (memoryDir && !QUESTION_LEAD_RE.test(negUniversalSrc)
|
|
4824
4898
|
&& !(await hasMidSentenceInterrogative(negUniversalSrc))) {
|
|
4825
|
-
const negUniversal = await negativeUniversalTeach(negUniversalSrc, { memoryDir, sessionId })
|
|
4826
|
-
|| await negativeUniversalCanTeach(negUniversalSrc, { memoryDir, sessionId });
|
|
4899
|
+
const negUniversal = await negativeUniversalTeach(negUniversalSrc, { memoryDir, sessionId, observedAt, dateText })
|
|
4900
|
+
|| await negativeUniversalCanTeach(negUniversalSrc, { memoryDir, sessionId, observedAt, dateText });
|
|
4827
4901
|
if (negUniversal) return negUniversal;
|
|
4828
4902
|
}
|
|
4829
4903
|
}
|
|
@@ -4847,7 +4921,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4847
4921
|
if (own && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion
|
|
4848
4922
|
&& (wrapped || /^[A-Z]/.test(own[1]) || /^[A-Z]/.test(own[2]))) {
|
|
4849
4923
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
4850
|
-
subject: own[2], predicate: OWNED_BY_PREDICATE, object: own[1],
|
|
4924
|
+
subject: own[2], predicate: OWNED_BY_PREDICATE, object: own[1], observedAt, dateText,
|
|
4851
4925
|
});
|
|
4852
4926
|
if (stored) return stored;
|
|
4853
4927
|
}
|
|
@@ -4865,7 +4939,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4865
4939
|
&& (wrapped || /^[A-Z]/.test(ownPassive[2]) || /^[A-Z]/.test(ownPassiveSubject)
|
|
4866
4940
|
|| MODULE_PATH_RE.test(ownPassiveSubject))) {
|
|
4867
4941
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
4868
|
-
subject: ownPassive[1], predicate: OWNED_BY_PREDICATE, object: ownPassive[2],
|
|
4942
|
+
subject: ownPassive[1], predicate: OWNED_BY_PREDICATE, object: ownPassive[2], observedAt, dateText,
|
|
4869
4943
|
});
|
|
4870
4944
|
if (stored) return stored;
|
|
4871
4945
|
}
|
|
@@ -4878,7 +4952,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4878
4952
|
const relatedTo = ownSrc.match(RELATED_TO_TEACH_RE);
|
|
4879
4953
|
if (relatedTo && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
4880
4954
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
4881
|
-
subject: relatedTo[1], predicate: "mgx:relatedTo", object: relatedTo[2],
|
|
4955
|
+
subject: relatedTo[1], predicate: "mgx:relatedTo", object: relatedTo[2], observedAt, dateText,
|
|
4882
4956
|
});
|
|
4883
4957
|
if (stored) return stored;
|
|
4884
4958
|
}
|
|
@@ -4894,7 +4968,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4894
4968
|
const rel = ownSrc.match(RELATION_FACT_TEACH_RE);
|
|
4895
4969
|
if (rel && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
4896
4970
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
4897
|
-
subject: rel[1], predicate: await generalVerbPredicate(rel[2]), object: rel[3],
|
|
4971
|
+
subject: rel[1], predicate: await generalVerbPredicate(rel[2]), object: rel[3], observedAt, dateText,
|
|
4898
4972
|
});
|
|
4899
4973
|
if (stored) return stored;
|
|
4900
4974
|
}
|
|
@@ -4906,14 +4980,14 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4906
4980
|
const genitive = ownSrc.match(GENITIVE_RELATION_TEACH_RE);
|
|
4907
4981
|
if (genitive && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
4908
4982
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
4909
|
-
subject: genitive[1], predicate: await generalVerbPredicate(genitive[3]), object: genitive[2],
|
|
4983
|
+
subject: genitive[1], predicate: await generalVerbPredicate(genitive[3]), object: genitive[2], observedAt, dateText,
|
|
4910
4984
|
});
|
|
4911
4985
|
if (stored) return stored;
|
|
4912
4986
|
}
|
|
4913
4987
|
const genitiveRev = ownSrc.match(GENITIVE_RELATION_TEACH_REV_RE);
|
|
4914
4988
|
if (genitiveRev && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
4915
4989
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
4916
|
-
subject: genitiveRev[3], predicate: await generalVerbPredicate(genitiveRev[2]), object: genitiveRev[1],
|
|
4990
|
+
subject: genitiveRev[3], predicate: await generalVerbPredicate(genitiveRev[2]), object: genitiveRev[1], observedAt, dateText,
|
|
4917
4991
|
});
|
|
4918
4992
|
if (stored) return stored;
|
|
4919
4993
|
}
|
|
@@ -4928,7 +5002,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4928
5002
|
? await matchRelationalVerbTeach(ownSrc) : null;
|
|
4929
5003
|
if (relVerb) {
|
|
4930
5004
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
4931
|
-
subject: relVerb.subject, predicate: await generalVerbPredicate(relVerb.base), object: relVerb.object,
|
|
5005
|
+
subject: relVerb.subject, predicate: await generalVerbPredicate(relVerb.base), object: relVerb.object, observedAt, dateText,
|
|
4932
5006
|
});
|
|
4933
5007
|
if (stored) return stored;
|
|
4934
5008
|
}
|
|
@@ -4948,7 +5022,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4948
5022
|
const hasMethod = ownSrc.match(TEACH_HAS_METHOD_RE);
|
|
4949
5023
|
if (hasMethod && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
4950
5024
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
4951
|
-
subject: hasMethod[1], predicate: HAS_A_PREDICATE, object: `${hasMethod[2]} method`,
|
|
5025
|
+
subject: hasMethod[1], predicate: HAS_A_PREDICATE, object: `${hasMethod[2]} method`, observedAt, dateText,
|
|
4952
5026
|
});
|
|
4953
5027
|
if (stored) return stored;
|
|
4954
5028
|
}
|
|
@@ -5251,7 +5325,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
5251
5325
|
const rendersAs = ownSrc.match(RENDERS_AS_TEACH_RE);
|
|
5252
5326
|
if (rendersAs && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
5253
5327
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
5254
|
-
subject: rendersAs[1], predicate: "mgx:rendersAs", object: rendersAs[2],
|
|
5328
|
+
subject: rendersAs[1], predicate: "mgx:rendersAs", object: rendersAs[2], observedAt, dateText,
|
|
5255
5329
|
});
|
|
5256
5330
|
if (stored) return stored;
|
|
5257
5331
|
}
|
|
@@ -5269,7 +5343,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
5269
5343
|
if (wrapped && memoryDir && !QUESTION_LEAD_RE.test(wrapped) && !(await hasMidSentenceInterrogative(wrapped))) {
|
|
5270
5344
|
const gv = await generalVerbTeach(wrapped);
|
|
5271
5345
|
if (gv) {
|
|
5272
|
-
const stored = await teachFact(memoryDir, sessionId, gv);
|
|
5346
|
+
const stored = await teachFact(memoryDir, sessionId, { ...gv, observedAt, dateText });
|
|
5273
5347
|
if (stored) return stored;
|
|
5274
5348
|
}
|
|
5275
5349
|
} else if (!wrapped && memoryDir && !QUESTION_LEAD_RE.test(correctMisspellings(raw))
|
|
@@ -5323,14 +5397,14 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
5323
5397
|
if (!canLex) { const { loadLexicon } = await import("../domain/grammar/lexicon.mjs"); canLex = loadLexicon(); }
|
|
5324
5398
|
if (await isGroundedTerm(canSingular, canLex, memoryDir, cache, graph)) {
|
|
5325
5399
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
5326
|
-
subject: canSingular, predicate: await capabilityPredicate(canShape.negated), object: canShape.verb,
|
|
5400
|
+
subject: canSingular, predicate: await capabilityPredicate(canShape.negated), object: canShape.verb, observedAt, dateText,
|
|
5327
5401
|
});
|
|
5328
5402
|
if (stored) return stored;
|
|
5329
5403
|
}
|
|
5330
5404
|
}
|
|
5331
5405
|
const gv = await generalVerbTeach(raw);
|
|
5332
5406
|
if (gv) {
|
|
5333
|
-
const stored = await teachFact(memoryDir, sessionId, gv);
|
|
5407
|
+
const stored = await teachFact(memoryDir, sessionId, { ...gv, observedAt, dateText });
|
|
5334
5408
|
if (stored) return stored;
|
|
5335
5409
|
}
|
|
5336
5410
|
}
|
|
@@ -5369,7 +5443,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
5369
5443
|
const compPredicate = `mgx:${comp[2].toLowerCase().replace(/\s+/g, "-")}-than`;
|
|
5370
5444
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
5371
5445
|
subject: comp[1].trim(), predicate: compPredicate,
|
|
5372
|
-
object: comp[3].trim().replace(/[.!?]+$/, ""),
|
|
5446
|
+
object: comp[3].trim().replace(/[.!?]+$/, ""), observedAt, dateText,
|
|
5373
5447
|
});
|
|
5374
5448
|
if (stored) return stored;
|
|
5375
5449
|
}
|
|
@@ -5388,7 +5462,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
5388
5462
|
const pred = `mgx:${pp[2].toLowerCase()}-${pp[3].toLowerCase()}`;
|
|
5389
5463
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
5390
5464
|
subject: pp[1].trim(), predicate: negated ? negatedPredicate(pred) : pred,
|
|
5391
|
-
object: participleObject(pp[4]),
|
|
5465
|
+
object: participleObject(pp[4]), observedAt, dateText,
|
|
5392
5466
|
});
|
|
5393
5467
|
if (stored) return stored;
|
|
5394
5468
|
}
|
|
@@ -5403,11 +5477,11 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
5403
5477
|
const subject = np[1].trim();
|
|
5404
5478
|
const relPred = `mgx:${np[4].toLowerCase()}-${np[5].toLowerCase()}`;
|
|
5405
5479
|
const isaStored = await teachFact(memoryDir, sessionId, {
|
|
5406
|
-
subject, predicate: SUBCLASS_PREDICATE, object: singularizeSurface(np[3]),
|
|
5480
|
+
subject, predicate: SUBCLASS_PREDICATE, object: singularizeSurface(np[3]), observedAt, dateText,
|
|
5407
5481
|
});
|
|
5408
5482
|
const relStored = await teachFact(memoryDir, sessionId, {
|
|
5409
5483
|
subject, predicate: negated ? negatedPredicate(relPred) : relPred,
|
|
5410
|
-
object: participleObject(np[6]),
|
|
5484
|
+
object: participleObject(np[6]), observedAt, dateText,
|
|
5411
5485
|
});
|
|
5412
5486
|
const stripNoted = (t) => String(t).replace(/^noted — remembered(?:\s+\d+\s+facts?)?:\s*/i, "").trim();
|
|
5413
5487
|
if (isaStored && relStored) return { text: `noted — remembered both: ${stripNoted(isaStored.text)}; and ${stripNoted(relStored.text)}`, via: "assert", miss: false };
|
|
@@ -5420,7 +5494,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
5420
5494
|
const pred = `mgx:same-${same[3].toLowerCase()}-as`;
|
|
5421
5495
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
5422
5496
|
subject: same[1].trim(), predicate: negated ? negatedPredicate(pred) : pred,
|
|
5423
|
-
object: same[2].trim(),
|
|
5497
|
+
object: same[2].trim(), observedAt, dateText,
|
|
5424
5498
|
});
|
|
5425
5499
|
if (stored) return stored;
|
|
5426
5500
|
}
|
|
@@ -5429,7 +5503,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
5429
5503
|
// assertTurn ITSELF records the "every" quantifier (point 3) on a plain
|
|
5430
5504
|
// universal success, so every caller (this loop AND the top-level
|
|
5431
5505
|
// declarative-sentence dispatch in runTurn) gets it uniformly.
|
|
5432
|
-
const stored = await assertTurn(cand, { memoryDir, sessionId, focus: null, lexicon, cache });
|
|
5506
|
+
const stored = await assertTurn(cand, { memoryDir, sessionId, focus: null, lexicon, cache, observedAt, dateText });
|
|
5433
5507
|
if (stored) return { text: stored.answer, via: "assert", miss: false };
|
|
5434
5508
|
}
|
|
5435
5509
|
// CAPABILITY over a GROUNDED subject — "penguins swim" (habitual) or "a
|
|
@@ -5451,7 +5525,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
5451
5525
|
for (const subj of new Set([singularizeSurface(habitualTeach.subject), habitualTeach.subject])) {
|
|
5452
5526
|
if (await isGroundedTerm(subj, habLex, memoryDir, cache, graph)) {
|
|
5453
5527
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
5454
|
-
subject: subj, predicate: await capabilityPredicate(habitualTeach.negated), object: habitualTeach.verb,
|
|
5528
|
+
subject: subj, predicate: await capabilityPredicate(habitualTeach.negated), object: habitualTeach.verb, observedAt, dateText,
|
|
5455
5529
|
});
|
|
5456
5530
|
if (stored) return stored;
|
|
5457
5531
|
}
|
|
@@ -5463,14 +5537,14 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
5463
5537
|
// wrapped surface (payload is already unwrapped either way) — see
|
|
5464
5538
|
// unknownSubjectFallback's own docblock for the exact narrowing rules
|
|
5465
5539
|
// (object must still be known, etc.).
|
|
5466
|
-
const fallback = await unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }, cache);
|
|
5540
|
+
const fallback = await unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon, observedAt, dateText }, cache);
|
|
5467
5541
|
if (fallback) return fallback;
|
|
5468
5542
|
// MIRROR mint fallback: the known-subject/unknown-object asymmetry —
|
|
5469
5543
|
// tried right after the unknown-subject case declines, so a subject the
|
|
5470
5544
|
// STATIC lexicon (or a prior taught fact) already grounds can mint a
|
|
5471
5545
|
// brand-new object term. See unknownObjectFallback's own docblock for the
|
|
5472
5546
|
// exact narrowing rules (the "both sides ungrounded" safety guard, etc.).
|
|
5473
|
-
const objectFallback = await unknownObjectFallback(payload, { memoryDir, sessionId, lexicon, classIntent: kindOfClassIntent, graph }, cache);
|
|
5547
|
+
const objectFallback = await unknownObjectFallback(payload, { memoryDir, sessionId, lexicon, classIntent: kindOfClassIntent, graph, observedAt, dateText }, cache);
|
|
5474
5548
|
if (objectFallback) return objectFallback;
|
|
5475
5549
|
// ADJECTIVE-MINT fallback: tried right after unknownObjectFallback
|
|
5476
5550
|
// declines, so a grounded subject (static
|
|
@@ -5479,7 +5553,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
5479
5553
|
// docblock for the exact narrowing rules (the "both sides ungrounded"
|
|
5480
5554
|
// safety guard, and why this must be a standalone function rather than
|
|
5481
5555
|
// nested inside unknownSubjectFallback).
|
|
5482
|
-
const adjectiveFallback = await unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon, graph }, cache);
|
|
5556
|
+
const adjectiveFallback = await unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon, graph, observedAt, dateText }, cache);
|
|
5483
5557
|
if (adjectiveFallback) return adjectiveFallback;
|
|
5484
5558
|
// PROPERTY teach — "remember/note that <X> is <adjective>": wrapper-REQUIRED
|
|
5485
5559
|
// (a bare "X is deprecated" is never silently reified), and only after the
|
|
@@ -5489,7 +5563,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
5489
5563
|
const prop = wrapped.match(TEACH_PROPERTY_RE);
|
|
5490
5564
|
if (prop && !PLACE_ADVERB_OBJECT_RE.test(prop[2])) {
|
|
5491
5565
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
5492
|
-
subject: prop[1], predicate: HAS_PROPERTY_PREDICATE, object: prop[2],
|
|
5566
|
+
subject: prop[1], predicate: HAS_PROPERTY_PREDICATE, object: prop[2], observedAt, dateText,
|
|
5493
5567
|
});
|
|
5494
5568
|
if (stored) return stored;
|
|
5495
5569
|
}
|
|
@@ -5591,12 +5665,15 @@ const NO_FOCUS_WHATS_IN_HERE_RE = /^what(?:'s|s|\s+is)\s+in\s+here\??$/i;
|
|
|
5591
5665
|
* ACE lexicon: an abstract "every X is a Y" invites a curious user to
|
|
5592
5666
|
* substitute intuitive-but-unknown words — "every cache is a thing" — which
|
|
5593
5667
|
* the closed lexicon then rejects; "every bug is an issue" parses and stores. */
|
|
5594
|
-
async function memorySummary(memoryDir, graph) {
|
|
5668
|
+
async function memorySummary(memoryDir, graph, uiContext = "cli") {
|
|
5595
5669
|
const rows = memoryDir ? await memoryFacts(memoryDir) : [];
|
|
5596
5670
|
if (!rows.length) {
|
|
5671
|
+
const seedClause = uiContext === "browser"
|
|
5672
|
+
? 'teach me directly, e.g. "every bug is an issue"'
|
|
5673
|
+
: 'run `tmct init` to seed a starter vocabulary, or teach me directly, e.g. "every bug is an issue"';
|
|
5597
5674
|
const hook = moduleCountOf(graph) > 0
|
|
5598
5675
|
? 'ask about this codebase\'s structure (imports, calls, definitions), or teach me with "every X is a Y"'
|
|
5599
|
-
:
|
|
5676
|
+
: seedClause;
|
|
5600
5677
|
return `I haven't been told any facts yet — ${hook}. /memory to inspect, /help for commands.`;
|
|
5601
5678
|
}
|
|
5602
5679
|
const preds = new Set(rows.map((f) => f.predicate).filter(Boolean));
|
|
@@ -5779,7 +5856,7 @@ async function moduleOrientLane(query, { graph }) {
|
|
|
5779
5856
|
return null;
|
|
5780
5857
|
}
|
|
5781
5858
|
|
|
5782
|
-
async function metaLane(query, { graph, memoryDir, last = null, templates = null, vocabHint = null, focus = null }) {
|
|
5859
|
+
async function metaLane(query, { graph, memoryDir, last = null, templates = null, vocabHint = null, focus = null, uiContext = "cli" }) {
|
|
5783
5860
|
// Preamble-peeled twin of `q`: a self-intro/greeting lead ("I'm new here,
|
|
5784
5861
|
// what should I read first") wraps exactly the orientation questions this
|
|
5785
5862
|
// lane owns, and the anchored META_ORIENT_RE can't see past it. Peeling
|
|
@@ -5787,10 +5864,10 @@ async function metaLane(query, { graph, memoryDir, last = null, templates = null
|
|
|
5787
5864
|
const peeled = applyPreambleFrames(String(query).trim()).toLowerCase().replace(/[?.!]+$/, "").replace(/\s+/g, " ").trim();
|
|
5788
5865
|
const q = String(query).trim().toLowerCase().replace(/[?.!]+$/, "").replace(/\s+/g, " ");
|
|
5789
5866
|
if (WHAT_KNOW_RE.test(q) || q === "what have you learned" || q === "what have you learnt") {
|
|
5790
|
-
return { text: await memorySummary(memoryDir, graph), via: "meta" };
|
|
5867
|
+
return { text: await memorySummary(memoryDir, graph, uiContext), via: "meta" };
|
|
5791
5868
|
}
|
|
5792
5869
|
if (peeled !== q && META_ORIENT_RE.test(peeled)) {
|
|
5793
|
-
const text = orientationText(graph, templates, vocabHint);
|
|
5870
|
+
const text = orientationText(graph, templates, vocabHint, uiContext);
|
|
5794
5871
|
return { text: last?.answer === text ? META_ORIENT_REPEAT_ONELINER : text, via: "meta" };
|
|
5795
5872
|
}
|
|
5796
5873
|
if (META_ORIENT_RE.test(q)) {
|
|
@@ -5800,7 +5877,7 @@ async function metaLane(query, { graph, memoryDir, last = null, templates = null
|
|
|
5800
5877
|
// reprints orientationText(graph) verbatim on every repeat, never
|
|
5801
5878
|
// collapsing. Mirrors ORIENTATION_REPEAT_ONELINER's identity-check
|
|
5802
5879
|
// pattern exactly, with its own distinct oneliner text.
|
|
5803
|
-
const text = orientationText(graph, templates, vocabHint);
|
|
5880
|
+
const text = orientationText(graph, templates, vocabHint, uiContext);
|
|
5804
5881
|
return { text: last?.answer === text ? META_ORIENT_REPEAT_ONELINER : text, via: "meta" };
|
|
5805
5882
|
}
|
|
5806
5883
|
// A bare "what is in here" with NO standing focus — see
|
|
@@ -5815,7 +5892,7 @@ async function metaLane(query, { graph, memoryDir, last = null, templates = null
|
|
|
5815
5892
|
if (!focus?.label) {
|
|
5816
5893
|
const stripped = normalizeQuery(String(query)).trim().replace(/[?.!]+$/, "").trim();
|
|
5817
5894
|
if (NO_FOCUS_WHATS_IN_HERE_RE.test(stripped)) {
|
|
5818
|
-
const text = orientationText(graph, templates, vocabHint);
|
|
5895
|
+
const text = orientationText(graph, templates, vocabHint, uiContext);
|
|
5819
5896
|
return { text: last?.answer === text ? META_ORIENT_REPEAT_ONELINER : text, via: "meta" };
|
|
5820
5897
|
}
|
|
5821
5898
|
}
|
|
@@ -5961,7 +6038,7 @@ function nudgeName(captured, focus) {
|
|
|
5961
6038
|
* for the opinion gate: it must fire BEFORE the short-miss's "is a <thing> a
|
|
5962
6039
|
* <kind>" membership hint would (the caller runs this whole step before the
|
|
5963
6040
|
* short-miss rewrite). */
|
|
5964
|
-
function nudgeAnswer(query, focus, vocabHint = null) {
|
|
6041
|
+
function nudgeAnswer(query, focus, vocabHint = null, uiContext = "cli") {
|
|
5965
6042
|
const q = String(query).trim().replace(/[?.!]+$/, "").replace(/\s+/g, " ");
|
|
5966
6043
|
if (PERSONAL_ASSISTANT_NUDGE_RE.test(q)) {
|
|
5967
6044
|
// A hardcoded "what is a dog" example here would be a lie in any UNSEEDED
|
|
@@ -5971,9 +6048,14 @@ function nudgeAnswer(query, focus, vocabHint = null) {
|
|
|
5971
6048
|
// summary). vocabHint (threaded from runAsk/runTurn's own
|
|
5972
6049
|
// hasSeededVocabulary check) is ALREADY the correct session-gated clause:
|
|
5973
6050
|
// "what is a dog" when seeded, `tmct init` otherwise — reused verbatim
|
|
5974
|
-
// instead of a second, ungated copy.
|
|
6051
|
+
// instead of a second, ungated copy. Its own last-resort fallback (a
|
|
6052
|
+
// session that passed no hint at all) still has to pick a remedy, so it
|
|
6053
|
+
// splits on the surface for the same reason vocabExampleHint does.
|
|
6054
|
+
const fallback = uiContext === "browser"
|
|
6055
|
+
? 'Teach me a fact, e.g. "every bug is an issue".'
|
|
6056
|
+
: "Run `tmct init` to seed a starter vocabulary.";
|
|
5975
6057
|
return "I don't have access to that — I'm a deterministic code/vocabulary assistant, not a general assistant. "
|
|
5976
|
-
+ `Ask me about code structure ("which modules import <name>"). ${vocabHint ||
|
|
6058
|
+
+ `Ask me about code structure ("which modules import <name>"). ${vocabHint || fallback}`;
|
|
5977
6059
|
}
|
|
5978
6060
|
if (OPINION_NUDGE_RE.test(q)) {
|
|
5979
6061
|
const name = focus?.label || "<name>";
|
|
@@ -6114,7 +6196,7 @@ async function presuppositionNudge(query, { graph, memoryDir }) {
|
|
|
6114
6196
|
|| (f.predicate === `tmct:${adjective}` && f.object === "true"))) || null;
|
|
6115
6197
|
}
|
|
6116
6198
|
}
|
|
6117
|
-
lines.push(`${objEnt.label} ${adjective} — ${propHit ? `yes (source: ${propHit.provenance})` : "I have no fact saying so"}`);
|
|
6199
|
+
lines.push(`${objEnt.label} ${adjective} — ${propHit ? `yes (source: ${citationProvenance(propHit.provenance)})` : "I have no fact saying so"}`);
|
|
6118
6200
|
}
|
|
6119
6201
|
const verdict = lines.join("; ");
|
|
6120
6202
|
return { text: holds ? `${verdict}. ${subjEnt.label} does ${split.verb} ${objEnt.label}.` : `${verdict} — the premise doesn't hold.` };
|
|
@@ -6523,6 +6605,14 @@ function splitMetaPredicate(term) {
|
|
|
6523
6605
|
return { subject: t, predicate: null };
|
|
6524
6606
|
}
|
|
6525
6607
|
|
|
6608
|
+
// A peer-taught tag's `#node:<id>` segment keys the fact (PLAN_FACT.md) but is
|
|
6609
|
+
// never meant for a reader — strip it wherever provenance is cited in prose,
|
|
6610
|
+
// mirroring the same stable-node-id-is-not-shown rule chat-page-viz.mjs's own
|
|
6611
|
+
// node roster already applies.
|
|
6612
|
+
function citationProvenance(provenance) {
|
|
6613
|
+
return provenance.replace(/#node:[0-9a-f]+/g, "");
|
|
6614
|
+
}
|
|
6615
|
+
|
|
6526
6616
|
/** One rendered fact line. An OPERATOR-asserted fact keeps the true first-person
|
|
6527
6617
|
* provenance ("you told me: …"). A CORPUS fact is presented as clean DATA with its
|
|
6528
6618
|
* source cited, not "i learned: …" — that phrase over-claims and anthropomorphises
|
|
@@ -6534,7 +6624,7 @@ function splitMetaPredicate(term) {
|
|
|
6534
6624
|
* that it's lower-confidence, so a distinct, honest hedge ("possibly: …")
|
|
6535
6625
|
* applies here instead. Provenance stays VERBATIM in every case. */
|
|
6536
6626
|
function renderFactLine(f) {
|
|
6537
|
-
const cite = f.provenance ? ` (source: ${f.provenance})` : "";
|
|
6627
|
+
const cite = f.provenance ? ` (source: ${citationProvenance(f.provenance)})` : "";
|
|
6538
6628
|
// ace:chat = the ACE-parsed operator assert; teach:chat = the teach lane's
|
|
6539
6629
|
// natural frames — both are things the operator SAID, so both read first-person.
|
|
6540
6630
|
if (f.provenance.includes("ace:chat") || f.provenance.includes("teach:chat")) return `you told me: ${factPhrase(f)}${cite}`;
|
|
@@ -6690,7 +6780,7 @@ function isaPolarityReply(hit, negHit) {
|
|
|
6690
6780
|
* inconsistency as a derivation — so neither side wins, same discipline as
|
|
6691
6781
|
* isaPolarityReply's both-sides verdict. */
|
|
6692
6782
|
function isaInconsistencyRefusal(posFact, disjointFact) {
|
|
6693
|
-
const cite = (f) => `${factPhrase(f)}${f.provenance ? ` (source: ${f.provenance})` : ""}`;
|
|
6783
|
+
const cite = (f) => `${factPhrase(f)}${f.provenance ? ` (source: ${citationProvenance(f.provenance)})` : ""}`;
|
|
6694
6784
|
return {
|
|
6695
6785
|
text: `you've told me both ${cite(posFact)} and ${cite(disjointFact)} — together those contradict, and I won't derive an answer from an inconsistency. `
|
|
6696
6786
|
+ `To settle it, say "forget that ${posFact.subject} is ${indefiniteArticleFor(posFact.object)} ${posFact.object}".`,
|
|
@@ -6709,7 +6799,7 @@ function isaInconsistencyRefusal(posFact, disjointFact) {
|
|
|
6709
6799
|
* premise's object — sound for any chain length, though today's only caller
|
|
6710
6800
|
* (the live cax-sco/scm-sco chase below) ever passes exactly two. */
|
|
6711
6801
|
function renderIsaChain(premises) {
|
|
6712
|
-
const step = (f) => `${factPhrase(f)}${f.provenance ? ` (source: ${f.provenance})` : ""}`;
|
|
6802
|
+
const step = (f) => `${factPhrase(f)}${f.provenance ? ` (source: ${citationProvenance(f.provenance)})` : ""}`;
|
|
6713
6803
|
const first = premises[0];
|
|
6714
6804
|
const last = premises[premises.length - 1];
|
|
6715
6805
|
return `${premises.map(step).join("; ")}; so ${first.subject} is a ${last.object}`;
|
|
@@ -7158,10 +7248,18 @@ const WHAT_USED_FOR_RE = /^what\s+(?:(?:can\s+be|is)\s+used\s+for|is\s+for)\s+(.
|
|
|
7158
7248
|
* fact subject named that, and falls through to the code-graph where lane
|
|
7159
7249
|
* unchanged. Consumed by factAnswer's (a-pre4) reader. */
|
|
7160
7250
|
const WHERE_IS_FACT_RE = /^where(?:'s|\s+is|\s+are)\s+(.+?)(?:\s+now)?\s*[?.!]*$/i;
|
|
7161
|
-
/**
|
|
7162
|
-
*
|
|
7163
|
-
*
|
|
7164
|
-
|
|
7251
|
+
/** "where does ann live[ now]" — the auxiliary-fronted sibling of
|
|
7252
|
+
* WHERE_IS_FACT_RE, over the same taught locative facts. The trailing verb
|
|
7253
|
+
* carries no meaning here (locativePreposition, imported from ask-vocab.mjs,
|
|
7254
|
+
* matches on the predicate's own folded preposition, not the query's
|
|
7255
|
+
* surface verb), so it is dropped rather than captured — group 1 is the
|
|
7256
|
+
* subject alone, same shape WHERE_IS_FACT_RE's reader already expects. */
|
|
7257
|
+
const WHERE_DOES_FACT_RE = /^where\s+(?:does|do|did)\s+(.+?)\s+[a-z][a-z'-]*(?:\s+now)?\s*[?.!]*$/i;
|
|
7258
|
+
// The closed locative tail of a folded prepositional-verb predicate
|
|
7259
|
+
// (mgx:rest-on, mgx:stand-on, mgx:sit-in, …) — what makes a taught fact a
|
|
7260
|
+
// LOCATION answer rather than any arbitrary relation — lives once, in
|
|
7261
|
+
// ask-vocab.mjs's LOCATIVE_PREPOSITIONS/locativePreposition; this file reads
|
|
7262
|
+
// it through locativePreposition rather than keeping its own copy.
|
|
7165
7263
|
/** "what is on peg-a" / "what's on peg-a" — the reverse-by-OBJECT mirror of
|
|
7166
7264
|
* WHERE_IS_FACT_RE, over the same taught locative facts. The bare copula
|
|
7167
7265
|
* carries no verb to mint a predicate from, so the PREPOSITION is the anchor:
|
|
@@ -7248,7 +7346,7 @@ function renderIsaCite(chain, facts) {
|
|
|
7248
7346
|
(f) => f.predicate === step.predicate && f.subject === step.subject && f.object === step.object,
|
|
7249
7347
|
));
|
|
7250
7348
|
if (!steps.length || !steps.every(Boolean)) return null;
|
|
7251
|
-
return steps.map((g) => `${factPhrase(g)}${g.provenance ? ` (source: ${g.provenance})` : ""}`).join("; ");
|
|
7349
|
+
return steps.map((g) => `${factPhrase(g)}${g.provenance ? ` (source: ${citationProvenance(g.provenance)})` : ""}`).join("; ");
|
|
7252
7350
|
}
|
|
7253
7351
|
|
|
7254
7352
|
/** THE capability answer — every reader that asks "can X do Y" renders through
|
|
@@ -7636,7 +7734,7 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7636
7734
|
if (declined) return declined;
|
|
7637
7735
|
const steps = order.slice(0, -1).map((n, i) => pairs.find((f) => f.subject === n && f.object === order[i + 1]));
|
|
7638
7736
|
if (steps.every(Boolean)) {
|
|
7639
|
-
const cite = steps.map((g) => `${factPhrase(g)}${g.provenance ? ` (source: ${g.provenance})` : ""}`).join("; ");
|
|
7737
|
+
const cite = steps.map((g) => `${factPhrase(g)}${g.provenance ? ` (source: ${citationProvenance(g.provenance)})` : ""}`).join("; ");
|
|
7640
7738
|
return { text: `${order[0]} — ${cite}; so ${order[0]} is the ${supWord} ${kindSingular}`, replace: true };
|
|
7641
7739
|
}
|
|
7642
7740
|
}
|
|
@@ -7649,11 +7747,11 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7649
7747
|
// after the code lane already missed, and takes over only when a locative
|
|
7650
7748
|
// fact row for that exact subject exists — a real module answer, and every
|
|
7651
7749
|
// no-fact miss, is untouched.
|
|
7652
|
-
const whereQ = miss ? q.match(WHERE_IS_FACT_RE) : null;
|
|
7750
|
+
const whereQ = miss ? (q.match(WHERE_IS_FACT_RE) || q.match(WHERE_DOES_FACT_RE)) : null;
|
|
7653
7751
|
if (whereQ) {
|
|
7654
7752
|
const variants = factTermVariants(normFactTerm, whereQ[1]);
|
|
7655
7753
|
const hits = (await factRows(memoryDir, cache))
|
|
7656
|
-
.filter((f) =>
|
|
7754
|
+
.filter((f) => locativePreposition(f.predicate) !== null && variants.has(f.subject));
|
|
7657
7755
|
if (hits.length) {
|
|
7658
7756
|
const ranked = rankByBiasThenTrust(uniqueFacts(hits), biasByBundle);
|
|
7659
7757
|
const lines = ranked.map(renderFactLine);
|
|
@@ -7677,7 +7775,7 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7677
7775
|
const prep = whatIsPrepQ[1].toLowerCase();
|
|
7678
7776
|
const variants = factTermVariants(normFactTerm, whatIsPrepQ[2].replace(/^(?:an?|the)\s+/i, "").trim());
|
|
7679
7777
|
const hits = (await factRows(memoryDir, cache)).filter(
|
|
7680
|
-
(f) =>
|
|
7778
|
+
(f) => locativePreposition(f.predicate) === prep && variants.has(f.object),
|
|
7681
7779
|
);
|
|
7682
7780
|
if (hits.length) {
|
|
7683
7781
|
const ranked = rankByBiasThenTrust(uniqueFacts(hits), biasByBundle);
|
|
@@ -8216,7 +8314,7 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
8216
8314
|
if (!chain || chain.length < 2) return renderFactLine(f);
|
|
8217
8315
|
const steps = chain.map(rowForStep);
|
|
8218
8316
|
if (!steps.every(Boolean)) return renderFactLine(f);
|
|
8219
|
-
const cite = steps.map((g) => `${factPhrase(g)}${g.provenance ? ` (source: ${g.provenance})` : ""}`).join("; ");
|
|
8317
|
+
const cite = steps.map((g) => `${factPhrase(g)}${g.provenance ? ` (source: ${citationProvenance(g.provenance)})` : ""}`).join("; ");
|
|
8220
8318
|
return `${renderFactLine(f)} — via: ${cite}`;
|
|
8221
8319
|
});
|
|
8222
8320
|
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
@@ -9259,7 +9357,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9259
9357
|
const key = af.id || `${af.subject}|${af.predicate}|${af.object}`;
|
|
9260
9358
|
if (seenAlias.has(key)) continue;
|
|
9261
9359
|
seenAlias.add(key);
|
|
9262
|
-
parts.push(`${factPhrase(af)}${af.provenance ? ` (source: ${af.provenance})` : ""}`);
|
|
9360
|
+
parts.push(`${factPhrase(af)}${af.provenance ? ` (source: ${citationProvenance(af.provenance)})` : ""}`);
|
|
9263
9361
|
}
|
|
9264
9362
|
}
|
|
9265
9363
|
return `${node.entity} — ${parts.join("; ")}`;
|
|
@@ -9438,7 +9536,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9438
9536
|
const kindEcho = stripTrailingDiscourseTag(isaAsk[2]).trim();
|
|
9439
9537
|
const chain = [posFact, ...(objFact ? [objFact] : [])].map(renderFactLine).join("; ");
|
|
9440
9538
|
return {
|
|
9441
|
-
text: `no — ${chain}; and ${factPhrase(disjointFact)}${disjointFact.provenance ? ` (source: ${disjointFact.provenance})` : ""} `
|
|
9539
|
+
text: `no — ${chain}; and ${factPhrase(disjointFact)}${disjointFact.provenance ? ` (source: ${citationProvenance(disjointFact.provenance)})` : ""} `
|
|
9442
9540
|
+ `— so ${isaSubject} can never be ${indefiniteArticleFor(kindEcho)} ${kindEcho}.`,
|
|
9443
9541
|
replace: true,
|
|
9444
9542
|
};
|
|
@@ -9794,7 +9892,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9794
9892
|
const witness = findAcrossVariants(subjVariants, objVariants, (s, o) => proveCardinalityAtLeast(cardSubClassEdges, cardinalityRestrictionEdges, s, o, m, {}));
|
|
9795
9893
|
if (witness) {
|
|
9796
9894
|
const restrictionFact = rows.find((f) => f.predicate === CARD_SC_PREDICATE && f.subject === witness.viaClass && f.object === witness.viaRestriction);
|
|
9797
|
-
const cite = restrictionFact?.provenance ? ` (source: ${restrictionFact.provenance})` : "";
|
|
9895
|
+
const cite = restrictionFact?.provenance ? ` (source: ${citationProvenance(restrictionFact.provenance)})` : "";
|
|
9798
9896
|
const kindWord = witness.kind === "exactly" ? "exactly" : "at least";
|
|
9799
9897
|
const plural = (w, n) => `${w}${n === 1 ? "" : "s"}`;
|
|
9800
9898
|
// Premise-derived trust for THIS
|
|
@@ -9843,7 +9941,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9843
9941
|
const witness = findAcrossVariants(subjVariants, objVariants, (s, o) => proveMaxCardinalityZeroDenial(cardSubClassEdges, cardinalityRestrictionEdges, s, o, {}));
|
|
9844
9942
|
if (witness) {
|
|
9845
9943
|
const restrictionFact = rows.find((f) => f.predicate === CARD_SC_PREDICATE && f.subject === witness.viaClass && f.object === witness.viaRestriction);
|
|
9846
|
-
const cite = restrictionFact?.provenance ? ` (source: ${restrictionFact.provenance})` : "";
|
|
9944
|
+
const cite = restrictionFact?.provenance ? ` (source: ${citationProvenance(restrictionFact.provenance)})` : "";
|
|
9847
9945
|
// Same discipline as the
|
|
9848
9946
|
// cardinality-monotonicity reader just above (see its own comment).
|
|
9849
9947
|
const cardPremiseTrusts = [
|
|
@@ -11506,12 +11604,6 @@ async function entityOfKindInText(graph, expectedClass, answerText) {
|
|
|
11506
11604
|
return null;
|
|
11507
11605
|
}
|
|
11508
11606
|
|
|
11509
|
-
/** A bare question → tmct_ask. When a focus is set AND the graph is in hand we
|
|
11510
|
-
* call ask() directly to thread the focus as contextId (so a pronoun like "it"
|
|
11511
|
-
* resolves to the focus) — building the SAME delimited string dispatchTool emits;
|
|
11512
|
-
* otherwise the unchanged dispatchTool path (which also yields the no-graph error).
|
|
11513
|
-
* A hit updates the focus to the resolved object. Grammar miss / ToolError → a
|
|
11514
|
-
* normal answer, never a crash. */
|
|
11515
11607
|
/** Load the taught domain for the plan lane: fact rows + rule rows compiled
|
|
11516
11608
|
* through src/domain.mjs. Fresh-loads memory (never the turn cache) because
|
|
11517
11609
|
* the caller may have just written snapshot rows this same turn. */
|
|
@@ -11539,15 +11631,15 @@ function actionLabel(name, subject, target) {
|
|
|
11539
11631
|
* sentence never says, and a preposition doesn't imply one — "on" reads as
|
|
11540
11632
|
* rest-on, stand-on, sit-on or lie-on with equal warrant, so any prep→verb
|
|
11541
11633
|
* table here would be invention. The taught facts answer instead: every
|
|
11542
|
-
* locative fact (
|
|
11543
|
-
* member of the goal's class whose preposition is
|
|
11544
|
-
* its verb. Returns the candidates, sorted.
|
|
11545
|
-
* several is the caller's decline. */
|
|
11634
|
+
* locative fact (ask-vocab.mjs's locativePreposition, over its closed
|
|
11635
|
+
* predicate tail) about a member of the goal's class whose preposition is
|
|
11636
|
+
* the one typed contributes its verb. Returns the candidates, sorted.
|
|
11637
|
+
* Exactly one is an answer; none or several is the caller's decline. */
|
|
11546
11638
|
function goalVerbsFromTaughtFacts(factRows, domain, { universal, term, prep }) {
|
|
11547
11639
|
const subjects = new Set(universal ? domain?.classMembers?.[term] || [] : [term]);
|
|
11548
11640
|
const verbs = new Set();
|
|
11549
11641
|
for (const row of factRows || []) {
|
|
11550
|
-
if (
|
|
11642
|
+
if (locativePreposition(row.predicate) === null) continue;
|
|
11551
11643
|
if (!subjects.has(row.subject)) continue;
|
|
11552
11644
|
const [factVerb, factPrep] = row.predicate.slice("mgx:".length).split("-");
|
|
11553
11645
|
if (factPrep === prep) verbs.add(factVerb);
|
|
@@ -12577,7 +12669,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12577
12669
|
let answer;
|
|
12578
12670
|
let envelope = null;
|
|
12579
12671
|
try {
|
|
12580
|
-
let
|
|
12672
|
+
let content;
|
|
12581
12673
|
if (graph?.individuals?.length || (graph && (focus?.id || prev.length))) {
|
|
12582
12674
|
// Direct ask() whenever the caller HANDED US a graph with something in it.
|
|
12583
12675
|
// The focus/prev pair is threaded through it (contextId so "it" binds, prev
|
|
@@ -12586,15 +12678,16 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12586
12678
|
// below reads the CONFIG's graph instead — which an in-process session (a
|
|
12587
12679
|
// page's own world facts, say) has no file for. Gating on history alone
|
|
12588
12680
|
// refused every cold turn against a perfectly good graph and only started
|
|
12589
|
-
// answering once a focus happened to be set.
|
|
12590
|
-
// envelope dispatchTool emits, so the parse below is identical either way.
|
|
12681
|
+
// answering once a focus happened to be set.
|
|
12591
12682
|
const { ask } = await import("../domain/ask.mjs");
|
|
12592
12683
|
const r = ask(graph, askQuery, { contextId: effectiveContextId, prev });
|
|
12593
|
-
|
|
12684
|
+
content = r.content;
|
|
12685
|
+
envelope = r.tmct_ask;
|
|
12594
12686
|
} else {
|
|
12595
|
-
|
|
12687
|
+
const r = await dispatchToolStructured("tmct_ask", { query: askQuery }, { config, source, tel });
|
|
12688
|
+
content = r.content;
|
|
12689
|
+
envelope = r.data ?? null;
|
|
12596
12690
|
}
|
|
12597
|
-
const [content, envJson] = text.split(ASK_ENVELOPE_DELIM);
|
|
12598
12691
|
// ask.mjs is shared with the web GUI surface (src/surfaces/web), whose
|
|
12599
12692
|
// graph view really does have clickable nodes to select — its own
|
|
12600
12693
|
// "click a node first, or name it directly" wording is correct THERE,
|
|
@@ -12609,11 +12702,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12609
12702
|
/needs a selected node to refer to — click a node first, or name it directly\.$/,
|
|
12610
12703
|
"isn't resolved to anything yet — name the term directly, or ask a question that resolves one first.",
|
|
12611
12704
|
);
|
|
12612
|
-
if (envJson) { try { envelope = JSON.parse(envJson); } catch { envelope = null; } }
|
|
12613
12705
|
// Typed discourse referents the answer established (the ask envelope's
|
|
12614
12706
|
// additive `discourse` field, emitted beside the eval where the answer's
|
|
12615
12707
|
// content is still typed) register into the session's record here — the
|
|
12616
|
-
// one point both ask paths (direct call and
|
|
12708
|
+
// one point both ask paths (direct call and dispatchToolStructured) converge.
|
|
12617
12709
|
if (discourseHolder && Array.isArray(envelope?.discourse)) {
|
|
12618
12710
|
for (const { lane, bound, ...spec } of envelope.discourse) {
|
|
12619
12711
|
discourseHolder.record = registerReferent(discourseHolder.record, {
|
|
@@ -12832,7 +12924,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12832
12924
|
// codebase", "how do i start") → a summary / orientation, answered before the
|
|
12833
12925
|
// fact-dump readers so "what do you know" gets a summary, not raw facts.
|
|
12834
12926
|
if (!handled && miss) {
|
|
12835
|
-
const meta = await metaLane(query, { graph, memoryDir, last, templates, vocabHint, focus });
|
|
12927
|
+
const meta = await metaLane(query, { graph, memoryDir, last, templates, vocabHint, focus, uiContext });
|
|
12836
12928
|
if (meta) {
|
|
12837
12929
|
// A lane may answer with a better-worded decline (the module-orient
|
|
12838
12930
|
// residue guard) — still a miss in the turn record, like the isa
|
|
@@ -13228,7 +13320,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13228
13320
|
// 3 words with no STRUCT_WORDS token, so without this guard
|
|
13229
13321
|
// isConversational would discard a correct, already-composed answer for
|
|
13230
13322
|
// the generic orientation wall.
|
|
13231
|
-
const orientation = orientationAnswer(templates, graph, vocabHint);
|
|
13323
|
+
const orientation = orientationAnswer(templates, graph, vocabHint, uiContext);
|
|
13232
13324
|
const repeat = last?.answer === orientation;
|
|
13233
13325
|
answer = repeat ? ORIENTATION_REPEAT_ONELINER : orientation;
|
|
13234
13326
|
via = "template"; handled = true;
|
|
@@ -13473,7 +13565,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13473
13565
|
// must never become a recallable answer. The opinion gate fires HERE, before the
|
|
13474
13566
|
// short-miss's "is a <thing> a <kind>" membership hint could claim the line.
|
|
13475
13567
|
if (miss && recordMiss && via === "composed") {
|
|
13476
|
-
const nudged = nudgeAnswer(query, newFocus, vocabHint);
|
|
13568
|
+
const nudged = nudgeAnswer(query, newFocus, vocabHint, uiContext);
|
|
13477
13569
|
if (nudged) {
|
|
13478
13570
|
answer = nudged; via = "miss";
|
|
13479
13571
|
note(trace, "lane: (4c) CAPABILITY NUDGE — the question asked tmct to do something outside its scope (opinion/generation/risk-scoring)");
|
|
@@ -14324,22 +14416,34 @@ async function everySentenceTeaches(sentences, lexicon) {
|
|
|
14324
14416
|
}
|
|
14325
14417
|
}
|
|
14326
14418
|
|
|
14327
|
-
async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, cache = null }) {
|
|
14419
|
+
async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, cache = null, observedAt: observedAtIn = "", dateText: dateTextIn = "" }) {
|
|
14328
14420
|
// A trailing "?" marks a question, and a question never writes — the ACE
|
|
14329
14421
|
// fragment happily parses "dog have tail?" as the declarative it is not,
|
|
14330
14422
|
// which stored a Fact at teach trust over a FLOW-0 vocabulary question.
|
|
14331
14423
|
if (/\?\s*$/.test(String(line).trim())) return null;
|
|
14332
14424
|
try {
|
|
14425
|
+
// THE DATED TEACH FRAME — "<sentence> as of <date>". A caller that
|
|
14426
|
+
// already resolved a date (teachLane, recursing into its own
|
|
14427
|
+
// assertCandidates loop with the suffix already stripped) passes
|
|
14428
|
+
// observedAt/dateText straight through; the standalone entry point (the
|
|
14429
|
+
// top-level declarative-sentence dispatch in runTurn) probes `line`
|
|
14430
|
+
// itself. Either way the ACE parse below runs on `probeLine` — the
|
|
14431
|
+
// suffix-stripped text when a date was found, `line` unchanged otherwise
|
|
14432
|
+
// — so an ordinary sentence's behavior never changes.
|
|
14433
|
+
const suffix = observedAtIn ? null : datedTeachSuffix(line);
|
|
14434
|
+
const probeLine = suffix ? suffix.stripped : line;
|
|
14435
|
+
const observedAt = observedAtIn || suffix?.observedAt || "";
|
|
14436
|
+
const dateText = dateTextIn || suffix?.dateText || "";
|
|
14333
14437
|
const { parseAce, parseAceAmbiguous } = await import("../domain/grammar/ace.mjs");
|
|
14334
14438
|
// A session handle carries its own loaded lexicon (createSession loads it once);
|
|
14335
14439
|
// a bare runTurn (no handle) lazy-loads the cached core lexicon. The lexicon is
|
|
14336
14440
|
// immutable, so sharing one reference across concurrent handles is re-entrant.
|
|
14337
14441
|
let lex = lexicon;
|
|
14338
14442
|
if (!lex) { const { loadLexicon } = await import("../domain/grammar/lexicon.mjs"); lex = loadLexicon(); }
|
|
14339
|
-
const ambiguous = parseAceAmbiguous(
|
|
14443
|
+
const ambiguous = parseAceAmbiguous(probeLine, lex);
|
|
14340
14444
|
if (ambiguous) {
|
|
14341
14445
|
const { normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
14342
|
-
const answer = renderAmbiguousAssert(
|
|
14446
|
+
const answer = renderAmbiguousAssert(probeLine, ambiguous, normFactTerm);
|
|
14343
14447
|
// Genuinely ambiguous — no single triple was committed, so the canonical
|
|
14344
14448
|
// form is every surviving reading's own would-be triple set, same idiom
|
|
14345
14449
|
// as ask.mjs's canonicalOf() for a parse-level tie.
|
|
@@ -14351,7 +14455,7 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, c
|
|
|
14351
14455
|
};
|
|
14352
14456
|
return plainTurn(line, answer, { command: "assert", via: "assert", focus, canonical, goal: "teach/remember a new fact" });
|
|
14353
14457
|
}
|
|
14354
|
-
const parse = parseAce(
|
|
14458
|
+
const parse = parseAce(probeLine, lex);
|
|
14355
14459
|
if (!parse || !parse.triples?.length || parse.residue?.length) return null;
|
|
14356
14460
|
const { assertSentence } = await import("../domain/grammar/assert.mjs");
|
|
14357
14461
|
const { normFactTerm, appendFact } = await import("../adapters/memory/core.mjs");
|
|
@@ -14368,10 +14472,11 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, c
|
|
|
14368
14472
|
{ command: "assert", via: "teach-miss", miss: true, focus });
|
|
14369
14473
|
}
|
|
14370
14474
|
const ts = new Date().toISOString();
|
|
14371
|
-
const res = await assertSentence(memoryDir,
|
|
14475
|
+
const res = await assertSentence(memoryDir, probeLine, {
|
|
14372
14476
|
lexicon: lex,
|
|
14373
14477
|
provenance: { source: "chat", sessionId, ts },
|
|
14374
14478
|
appendFact,
|
|
14479
|
+
...(observedAt ? { observedAt } : {}),
|
|
14375
14480
|
});
|
|
14376
14481
|
if (!res || !res.ids?.length) return null;
|
|
14377
14482
|
// A plain universal "every X is a Y" ALSO records the "every" quantifier
|
|
@@ -14379,12 +14484,13 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, c
|
|
|
14379
14484
|
// lane. Gated on the literal typed determiner: only "every" reads as a
|
|
14380
14485
|
// class-level generalization. Best-effort: the base fact is already
|
|
14381
14486
|
// durably stored either way, so a failure here is swallowed.
|
|
14382
|
-
if (/^every\s+/i.test(String(
|
|
14487
|
+
if (/^every\s+/i.test(String(probeLine).trim())) {
|
|
14383
14488
|
const triple = res.triples.find((t) => t.predicate === "rdfs:subClassOf");
|
|
14384
14489
|
if (triple) {
|
|
14385
14490
|
try {
|
|
14386
14491
|
await appendFact(memoryDir, {
|
|
14387
14492
|
subject: triple.subject, predicate: "rdfs:subClassOf", object: triple.object, quantifier: "every",
|
|
14493
|
+
...(observedAt ? { observedAt } : {}),
|
|
14388
14494
|
});
|
|
14389
14495
|
} catch { /* best-effort — the base fact is already stored either way */ }
|
|
14390
14496
|
}
|
|
@@ -14416,7 +14522,10 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, c
|
|
|
14416
14522
|
if (para) paraphraseSuffix = ` (${para})`;
|
|
14417
14523
|
} catch { /* best-effort — the literal confirmation above is already correct either way */ }
|
|
14418
14524
|
}
|
|
14419
|
-
|
|
14525
|
+
// The dated-teach frame's own echo: the user typed the date, so the
|
|
14526
|
+
// acknowledgment shows it was registered rather than silently dropped.
|
|
14527
|
+
const dateSuffix = observedAt && dateText ? ` (as of ${dateText})` : "";
|
|
14528
|
+
const answer = `noted — remembered ${n} fact${n === 1 ? "" : "s"}: ${shown}${paraphraseSuffix}${dateSuffix}`;
|
|
14420
14529
|
// The canonical restatement of what was committed — `machine` is the same
|
|
14421
14530
|
// fact(s) in the compact notation ask.mjs's canonicalOf() uses for
|
|
14422
14531
|
// query-side parses, so both lanes share one consistent syntax.
|
|
@@ -15166,7 +15275,7 @@ async function dispatchTurn(input, { config, source = defaultSource, graph = nul
|
|
|
15166
15275
|
const trace = narrate ? [] : null;
|
|
15167
15276
|
// vocabHint: createSession computes this ONCE per session; a direct
|
|
15168
15277
|
// runTurn() caller that doesn't pass one gets it computed here instead.
|
|
15169
|
-
const resolvedVocabHint = vocabHint ?? vocabExampleHint(await hasSeededVocabulary(memoryDir));
|
|
15278
|
+
const resolvedVocabHint = vocabHint ?? vocabExampleHint(await hasSeededVocabulary(memoryDir), uiContext);
|
|
15170
15279
|
// The session's in-progress plan rides a mutable holder: the plan lane and
|
|
15171
15280
|
// the PLAN NEXT block below write planHolder.state; every other path leaves
|
|
15172
15281
|
// it untouched, and the caller re-threads whatever comes back.
|
|
@@ -15647,7 +15756,7 @@ async function dispatchTurn(input, { config, source = defaultSource, graph = nul
|
|
|
15647
15756
|
}
|
|
15648
15757
|
// Aggregate/count questions are answered mechanically off the loaded graph header,
|
|
15649
15758
|
// BEFORE falling through to the ask engine (focus unchanged — a count names no entity).
|
|
15650
|
-
const count = answerCount(graph, workingLine);
|
|
15759
|
+
const count = answerCount(graph, workingLine, { uiContext });
|
|
15651
15760
|
if (count != null) {
|
|
15652
15761
|
// An "I can't count <noun>" from a bare kind may still be answerable from an
|
|
15653
15762
|
// ASSERTED vocabulary fact ("every class is a type" → "how many types" = the
|
|
@@ -15714,9 +15823,18 @@ export async function hasSeededVocabulary(repo) {
|
|
|
15714
15823
|
* pair too: an abstract "every X is a Y" invites a curious user to fill X/Y
|
|
15715
15824
|
* with an intuitive-but-unknown word and hit the teach-miss dead-end right
|
|
15716
15825
|
* after being offered the pattern. "every bug is an issue" is confirmed to
|
|
15717
|
-
* parse and store, so the offer resolves if copied verbatim.
|
|
15718
|
-
|
|
15719
|
-
|
|
15720
|
-
|
|
15826
|
+
* parse and store, so the offer resolves if copied verbatim.
|
|
15827
|
+
*
|
|
15828
|
+
* `uiContext` picks the REMEDY the same way: a terminal session can run
|
|
15829
|
+
* `tmct init` against a real directory, a page has no filesystem and no argv,
|
|
15830
|
+
* so naming the command there sends the reader after something the surface
|
|
15831
|
+
* can't do. The teach pointer works on both, so the browser arm keeps only
|
|
15832
|
+
* that. This clause is threaded through runAsk as `vocabHint` and reused by
|
|
15833
|
+
* every dead-end that needs to name an exit, so one split here covers them
|
|
15834
|
+
* all rather than each miss carrying its own copy. */
|
|
15835
|
+
export function vocabExampleHint(seeded, uiContext = "cli") {
|
|
15836
|
+
if (seeded) return 'Try "what is a dog" for general vocabulary.';
|
|
15837
|
+
return uiContext === "browser"
|
|
15838
|
+
? 'Teach me directly, e.g. "every bug is an issue".'
|
|
15721
15839
|
: 'Run `tmct init` to seed a starter vocabulary, or teach me directly, e.g. "every bug is an issue".';
|
|
15722
15840
|
}
|