@polycode-projects/the-mechanical-code-talker 2.10.3 → 2.11.0
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 +68 -12
- package/bin/tmct.mjs +5 -2
- package/corpus/sprites/src/sprite-facts.jsonl +18 -0
- package/corpus/worlds/manifest.json +5 -5
- package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
- package/corpus/worlds/src/ashcombe-hall.jsonl +27 -0
- package/data/sprites/book-icon.toml +12 -0
- package/data/sprites/cellar-icon.toml +12 -0
- package/data/sprites/drawing-room-icon.toml +13 -0
- package/data/sprites/garden-icon.toml +12 -0
- package/data/sprites/kitchen-icon.toml +13 -0
- package/data/sprites/library-icon.toml +12 -0
- package/data/sprites/pan-icon.toml +11 -0
- package/data/sprites/study-icon.toml +12 -0
- package/package.json +7 -2
- package/src/adapters/corpus/wikipedia-live.mjs +182 -26
- package/src/adapters/corpus/worlds-pack.mjs +8 -2
- package/src/adapters/memory/core.mjs +8 -1
- package/src/adapters/toml-config.mjs +6 -0
- package/src/domain/cli-verbs.mjs +2 -0
- package/src/domain/memory/trust.mjs +32 -2
- package/src/domain/sense-split.mjs +203 -0
- package/src/domain/worlds-pack.mjs +50 -0
- package/src/services/adventure-autoplay.mjs +5 -2
- package/src/services/adventure-viz.mjs +301 -33
- package/src/services/adventure.mjs +162 -14
- package/src/services/chat-page-viz.mjs +341 -197
- package/src/services/chat-session.mjs +24 -9
- package/src/services/chat.mjs +580 -47
- package/src/services/code-explorer-viz.mjs +198 -76
- package/src/services/extract-facts.mjs +384 -82
- package/src/services/fold.mjs +1 -1
- package/src/services/ingest-viz.mjs +637 -0
- package/src/services/ledger-viz.mjs +209 -0
- package/src/services/memory-panel-viz.mjs +159 -0
- package/src/services/research.mjs +266 -0
- package/src/services/sentences.mjs +19 -0
- package/src/services/session-log-format.mjs +64 -0
- package/src/services/sessions.mjs +56 -22
- package/src/services/spider-fly-turn.mjs +54 -1
- package/src/services/spider-fly-viz.mjs +41 -23
- package/src/surfaces/web/adventure-browser-entry.mjs +9 -5
- package/src/surfaces/web/chat-browser-entry.mjs +32 -11
- package/src/surfaces/web/code-explorer-browser-entry.mjs +27 -11
- package/src/surfaces/web/ingest-browser-entry.mjs +208 -0
- package/src/surfaces/web/ledger-browser-entry.mjs +24 -5
- package/src/surfaces/web/memory-ask-browser.bundle.js +134 -125
- package/src/surfaces/web/memory-stats.mjs +53 -0
- package/src/tools/definitions.mjs +14 -0
- package/src/tools/handlers/index.mjs +2 -0
- package/src/tools/handlers/tmct-ingest.mjs +43 -0
- package/src/tools/server.mjs +5 -2
package/src/services/chat.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// `it`/`this`/`that` refer back to whatever the last command or answer
|
|
8
8
|
// resolved.
|
|
9
9
|
//
|
|
10
|
-
// Sessions are logged to <repo>/SESSION_LOG_DIR/session-<uuidv7>.
|
|
10
|
+
// Sessions are logged to <repo>/SESSION_LOG_DIR/session-<uuidv7>.md, plus a
|
|
11
11
|
// structured sidecar (.tmct/sessions/session-<uuidv7>.jsonl, sessions.mjs) and
|
|
12
12
|
// a `Session` individual upserted into graph.json per turn.
|
|
13
13
|
//
|
|
@@ -51,12 +51,14 @@ import {
|
|
|
51
51
|
LIVE_PACK_NAME, cleanMissLiveTerm, renderLiveReferenceAnswer, liveProvenanceTag,
|
|
52
52
|
} from "../domain/reference-pack.mjs";
|
|
53
53
|
import { getReferencePackProvider } from "../adapters/corpus/reference-pack.mjs";
|
|
54
|
-
import { getLiveReferenceProvider } from "../adapters/corpus/wikipedia-live.mjs";
|
|
54
|
+
import { getLiveReferenceProvider, getResearchProvider } from "../adapters/corpus/wikipedia-live.mjs";
|
|
55
|
+
import { researchTurn, researchSnapshot, resolveResearchConfig, RESEARCH_DEFAULTS } from "./research.mjs";
|
|
55
56
|
import { CHILD_PACK_NAME, childProvenanceTag } from "../domain/child-pack.mjs";
|
|
56
57
|
import { getChildPackProvider } from "../adapters/corpus/child-pack.mjs";
|
|
57
58
|
import { dialogueActForLane } from "../domain/dialogue-acts.mjs";
|
|
59
|
+
import { subClassParents, ancestryChain, clusterSenses } from "../domain/sense-split.mjs";
|
|
58
60
|
import { relatedForTerm } from "../domain/skos-view.mjs";
|
|
59
|
-
import { adventureTurn, unclaimedAdventureOpening } from "./adventure.mjs";
|
|
61
|
+
import { adventureTurn, unclaimedAdventureOpening, foldWorldState } from "./adventure.mjs";
|
|
60
62
|
import { spiderFlyTurn } from "./spider-fly-turn.mjs";
|
|
61
63
|
import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
|
|
62
64
|
|
|
@@ -3137,7 +3139,8 @@ async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon
|
|
|
3137
3139
|
// sentence said ("tony never eats ribs" -> tony eats ribs) — a truthful teach
|
|
3138
3140
|
// read back as a confident lie. It belongs to NEG_MARKER_SRC below.
|
|
3139
3141
|
const TEACH_ADVERB_SKIP_SRC = "(?:(?:usually|often|sometimes|rarely|always|typically|generally|"
|
|
3140
|
-
+ "occasionally|frequently|normally|regularly|commonly|mostly|currently|still|also|really|actually
|
|
3142
|
+
+ "occasionally|frequently|normally|regularly|commonly|mostly|currently|still|also|really|actually|"
|
|
3143
|
+
+ "closely|strongly|directly)\\s+)?";
|
|
3141
3144
|
/** The negation markers a teach/query frame recognizes, in ONE place so the
|
|
3142
3145
|
* teach side and the query side can never disagree about what negates a
|
|
3143
3146
|
* sentence — the same discipline TEACH_ADVERB_SKIP_SRC is shared under. */
|
|
@@ -3162,6 +3165,96 @@ function splitTeachNegation(payload) {
|
|
|
3162
3165
|
return { payload: `${m[1]} ${canFamily ? "can " : ""}${m[3]}`.trim(), negated: true };
|
|
3163
3166
|
}
|
|
3164
3167
|
const GENERAL_VERB_TEACH_RE = new RegExp(`^([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+)\\s+(.+?)[.!?]*$`, "i");
|
|
3168
|
+
/** The closed participle set the relational teach frames read as "X is
|
|
3169
|
+
* <participle> <prep> Y" — a past participle whose own form is the word, so it
|
|
3170
|
+
* reads back with no morphology. Closed by list (templates over general
|
|
3171
|
+
* grammar), so the frame can never widen onto an arbitrary "-ed" adjective. */
|
|
3172
|
+
const TEACH_PARTICIPLE_SRC = "connected|related|associated|linked|based|derived|composed|made|used|known|located|found|involved|concerned";
|
|
3173
|
+
/** The prepositions those participles take. A closed set, folded into the
|
|
3174
|
+
* minted predicate (mgx:<participle>-<prep>) the same way PREP_SRC folds into
|
|
3175
|
+
* the general-verb frame's. */
|
|
3176
|
+
const TEACH_PARTICIPLE_PREP_SRC = "with|to|from|by|of|in|on|for|as|about|into";
|
|
3177
|
+
/** "X is <participle> <prep> Y" — "sales are closely connected with marketing"
|
|
3178
|
+
* → sales mgx:connected-with marketing. The subject is one or two tokens (the
|
|
3179
|
+
* same bound the comparative/unknown-subject frames use), an optional adverb
|
|
3180
|
+
* is skipped, and the object is captured for a determiner-strip + 3-token cap
|
|
3181
|
+
* by its handler. */
|
|
3182
|
+
const PARTICIPLE_PREP_TEACH_RE = new RegExp(
|
|
3183
|
+
`^(?:the\\s+|an?\\s+)?([\\w'-]+(?:\\s+[\\w'-]+)?)\\s+(?:is|are|was|were)\\s+${TEACH_ADVERB_SKIP_SRC}(${TEACH_PARTICIPLE_SRC})\\s+(${TEACH_PARTICIPLE_PREP_SRC})\\s+(.+)$`,
|
|
3184
|
+
"i",
|
|
3185
|
+
);
|
|
3186
|
+
/** "X is a <noun> <participle> <prep> Y" — a copula-NP with a trailing
|
|
3187
|
+
* participle clause: "sales are activities related to selling" decomposes into
|
|
3188
|
+
* the class-membership half (sales ⊑ activity, through the ordinary mint/assert
|
|
3189
|
+
* path) AND the relational half (sales mgx:related-to selling). The NP head is
|
|
3190
|
+
* a single token, followed by a closed participle — which keeps this disjoint
|
|
3191
|
+
* from PARTICIPLE_PREP_TEACH_RE, where the participle sits right after the
|
|
3192
|
+
* copula. */
|
|
3193
|
+
const COPULA_NP_PARTICIPLE_TEACH_RE = new RegExp(
|
|
3194
|
+
`^(?:the\\s+|an?\\s+)?([\\w'-]+(?:\\s+[\\w'-]+)?)\\s+(is|are|was|were)\\s+(?:an?\\s+)?([\\w'-]+)\\s+(${TEACH_PARTICIPLE_SRC})\\s+(${TEACH_PARTICIPLE_PREP_SRC})\\s+(.+)$`,
|
|
3195
|
+
"i",
|
|
3196
|
+
);
|
|
3197
|
+
/** "A and B have/share the same <noun>" — "sales and marketing have the same
|
|
3198
|
+
* goal" → sales mgx:same-goal-as marketing. A closed shape; the conjunction
|
|
3199
|
+
* pre-pass leaves it alone (its second clause never opens with is/are/has/
|
|
3200
|
+
* have/can), so it reaches the teach dispatch whole. */
|
|
3201
|
+
const SAME_NOUN_TEACH_RE = /^(?:the\s+)?([\w'-]+)\s+and\s+(?:the\s+)?([\w'-]+)\s+(?:have|has|share|shares)\s+(?:the\s+)?same\s+([\w'-]+)[.!?]*$/i;
|
|
3202
|
+
/** "the letter is in the garden" — a locative teach whose subject the running
|
|
3203
|
+
* adventure world already places somewhere. Group 1 is the subject; the world
|
|
3204
|
+
* place is left to the fold, since the sentence is stored as a note either
|
|
3205
|
+
* way. */
|
|
3206
|
+
const LOCATIVE_TEACH_RE = /^(?:the\s+)?([\w'-]+)\s+(?:is|are|was|were)\s+(?:in|on|under|inside|at|near|behind|above|below)\s+(?:the\s+)?[\w'-]+/i;
|
|
3207
|
+
/** Fold a relational object down to its head phrase: cut at the first clause
|
|
3208
|
+
* boundary (a comma, semicolon, or a coordinating "or"/"and"), strip a leading
|
|
3209
|
+
* determiner, then cap at 3 tokens — "selling or the number of goods sold in a
|
|
3210
|
+
* period" folds to "selling", "the number of goods" to "number of goods".
|
|
3211
|
+
* Keeps a minted relational object bounded, the same discipline the general-
|
|
3212
|
+
* verb frame's own object fold uses. */
|
|
3213
|
+
function participleObject(raw) {
|
|
3214
|
+
const cleaned = String(raw).trim()
|
|
3215
|
+
.replace(/[.!?]+$/, "")
|
|
3216
|
+
.split(/\s*[,;]\s*|\s+(?:or|and)\s+/i)[0]
|
|
3217
|
+
.trim()
|
|
3218
|
+
.replace(/^(?:the|an?|its|his|her|their|our|my|your|some|any)\s+/i, "");
|
|
3219
|
+
return cleaned.split(/\s+/).slice(0, 3).join(" ");
|
|
3220
|
+
}
|
|
3221
|
+
/** Does a bare (unwrapped) sentence fit one of the relational teach frames —
|
|
3222
|
+
* including its negated twin, read through splitTeachNegation the same way the
|
|
3223
|
+
* general-verb and capability frames read theirs? Used only to admit the
|
|
3224
|
+
* sentence as a teach payload; the dispatch below re-matches and stores. */
|
|
3225
|
+
function matchesRelationalTeachFrame(sentence) {
|
|
3226
|
+
const { payload } = splitTeachNegation(String(sentence || "").trim());
|
|
3227
|
+
return PARTICIPLE_PREP_TEACH_RE.test(payload)
|
|
3228
|
+
|| COPULA_NP_PARTICIPLE_TEACH_RE.test(payload)
|
|
3229
|
+
|| SAME_NOUN_TEACH_RE.test(payload);
|
|
3230
|
+
}
|
|
3231
|
+
/** Does a relational-frame sentence name a code-graph entity? "the Router is
|
|
3232
|
+
* used by every handler" reads as a passive uses-CLAIM the ask engine verifies
|
|
3233
|
+
* against the graph ("No — no uses edge found…"), not a fact to store — the
|
|
3234
|
+
* participle+preposition frame would otherwise intercept it. When any term the
|
|
3235
|
+
* frame would store resolves to a graph entity, the frame yields so the graph
|
|
3236
|
+
* lane keeps the sentence. No graph (a bare/browser/ingest turn) means nothing
|
|
3237
|
+
* to yield to, so the frame proceeds. */
|
|
3238
|
+
async function relationalFrameNamesGraphEntity(sentence, graph) {
|
|
3239
|
+
if (!graph) return false;
|
|
3240
|
+
const { payload } = splitTeachNegation(String(sentence || "").trim());
|
|
3241
|
+
let terms = null;
|
|
3242
|
+
const pp = payload.match(PARTICIPLE_PREP_TEACH_RE);
|
|
3243
|
+
if (pp) terms = [pp[1], participleObject(pp[4])];
|
|
3244
|
+
else {
|
|
3245
|
+
const np = payload.match(COPULA_NP_PARTICIPLE_TEACH_RE);
|
|
3246
|
+
if (np) terms = [np[1], np[3], participleObject(np[6])];
|
|
3247
|
+
else {
|
|
3248
|
+
const same = payload.match(SAME_NOUN_TEACH_RE);
|
|
3249
|
+
if (same) terms = [same[1], same[2]];
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
if (!terms) return false;
|
|
3253
|
+
for (const t of terms) {
|
|
3254
|
+
try { if (await resolveEntity(graph, String(t).trim())) return true; } catch { /* unresolved is fine */ }
|
|
3255
|
+
}
|
|
3256
|
+
return false;
|
|
3257
|
+
}
|
|
3165
3258
|
/** Determiners/quantifiers that make the FIRST token an article, not a real
|
|
3166
3259
|
* bare-name subject ("every controller…", "the cache…") — GENERAL_VERB_TEACH_RE
|
|
3167
3260
|
* would otherwise happily bind them as a 1-token subject and misread the
|
|
@@ -3992,7 +4085,7 @@ async function teachExclusionReason(sentence) {
|
|
|
3992
4085
|
}
|
|
3993
4086
|
export { teachExclusionReason };
|
|
3994
4087
|
|
|
3995
|
-
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null, planHolder = null }) {
|
|
4088
|
+
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null, planHolder = null, graph = null }) {
|
|
3996
4089
|
// A closed discourse-marker preamble ahead of a teach sentence ("howdy
|
|
3997
4090
|
// pardner, remember that TaskController is fragile") would otherwise
|
|
3998
4091
|
// corrupt TEACH_RE's own match, so strip it first. applyPreambleFrames is
|
|
@@ -4868,7 +4961,10 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4868
4961
|
|
|
4869
4962
|
let payload = null;
|
|
4870
4963
|
if (wrapped && /\b(?:is|are)\b/i.test(wrapped)) payload = wrapped;
|
|
4871
|
-
else if ((BARE_DECLARATIVE_RE.test(raw) || COMPARATIVE_TEACH_RE.test(raw)
|
|
4964
|
+
else if ((BARE_DECLARATIVE_RE.test(raw) || COMPARATIVE_TEACH_RE.test(raw)
|
|
4965
|
+
|| (matchesRelationalTeachFrame(raw) && !(await relationalFrameNamesGraphEntity(raw, graph)))
|
|
4966
|
+
|| matchBareHabitualTeach(raw) || matchBareCanTeach(raw))
|
|
4967
|
+
&& !QUESTION_LEAD_RE.test(raw) && !(await hasMidSentenceInterrogative(raw))) payload = raw;
|
|
4872
4968
|
if (!payload) {
|
|
4873
4969
|
// "remember margo eats ribs", re-escaping here through a combination
|
|
4874
4970
|
// that mechanism's own deliberate subject-shape restriction doesn't
|
|
@@ -4900,6 +4996,58 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4900
4996
|
});
|
|
4901
4997
|
if (stored) return stored;
|
|
4902
4998
|
}
|
|
4999
|
+
// RELATIONAL teach frames — a participle+preposition claim
|
|
5000
|
+
// ("sales are closely connected with marketing"), a copula-NP with a
|
|
5001
|
+
// trailing participle ("sales are activities related to selling"), or a
|
|
5002
|
+
// shared-attribute claim ("sales and marketing have the same goal"). Each
|
|
5003
|
+
// reads through splitTeachNegation like its comparative/general-verb
|
|
5004
|
+
// siblings, so a negated form stores the mgxneg: twin.
|
|
5005
|
+
{
|
|
5006
|
+
const { payload: posPayload, negated } = splitTeachNegation(String(payload).trim());
|
|
5007
|
+
// (D) participle + preposition, checked ahead of the copula-NP form so a
|
|
5008
|
+
// bare participle right after the copula is never misread as a noun.
|
|
5009
|
+
const pp = posPayload.match(PARTICIPLE_PREP_TEACH_RE);
|
|
5010
|
+
if (pp) {
|
|
5011
|
+
const pred = `mgx:${pp[2].toLowerCase()}-${pp[3].toLowerCase()}`;
|
|
5012
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
5013
|
+
subject: pp[1].trim(), predicate: negated ? negatedPredicate(pred) : pred,
|
|
5014
|
+
object: participleObject(pp[4]),
|
|
5015
|
+
});
|
|
5016
|
+
if (stored) return stored;
|
|
5017
|
+
}
|
|
5018
|
+
// (E) copula-NP + trailing participle — decomposed into the
|
|
5019
|
+
// class-membership half (subject ⊑ singular(NP head)) and the relational
|
|
5020
|
+
// half. The copula-NP shape is a deliberate declarative, strong enough to
|
|
5021
|
+
// mint the membership directly, so it lands even when neither term is in
|
|
5022
|
+
// the lexicon (the acceptance the ingest pipeline needs). Either half may
|
|
5023
|
+
// stand on its own if the other's write fails.
|
|
5024
|
+
const np = posPayload.match(COPULA_NP_PARTICIPLE_TEACH_RE);
|
|
5025
|
+
if (np) {
|
|
5026
|
+
const subject = np[1].trim();
|
|
5027
|
+
const relPred = `mgx:${np[4].toLowerCase()}-${np[5].toLowerCase()}`;
|
|
5028
|
+
const isaStored = await teachFact(memoryDir, sessionId, {
|
|
5029
|
+
subject, predicate: SUBCLASS_PREDICATE, object: singularizeSurface(np[3]),
|
|
5030
|
+
});
|
|
5031
|
+
const relStored = await teachFact(memoryDir, sessionId, {
|
|
5032
|
+
subject, predicate: negated ? negatedPredicate(relPred) : relPred,
|
|
5033
|
+
object: participleObject(np[6]),
|
|
5034
|
+
});
|
|
5035
|
+
const stripNoted = (t) => String(t).replace(/^noted — remembered(?:\s+\d+\s+facts?)?:\s*/i, "").trim();
|
|
5036
|
+
if (isaStored && relStored) return { text: `noted — remembered both: ${stripNoted(isaStored.text)}; and ${stripNoted(relStored.text)}`, via: "assert", miss: false };
|
|
5037
|
+
if (relStored) return relStored;
|
|
5038
|
+
if (isaStored) return isaStored;
|
|
5039
|
+
}
|
|
5040
|
+
// (F) shared attribute — "A and B have the same <noun>".
|
|
5041
|
+
const same = posPayload.match(SAME_NOUN_TEACH_RE);
|
|
5042
|
+
if (same) {
|
|
5043
|
+
const pred = `mgx:same-${same[3].toLowerCase()}-as`;
|
|
5044
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
5045
|
+
subject: same[1].trim(), predicate: negated ? negatedPredicate(pred) : pred,
|
|
5046
|
+
object: same[2].trim(),
|
|
5047
|
+
});
|
|
5048
|
+
if (stored) return stored;
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
4903
5051
|
for (const cand of assertCandidates(payload)) {
|
|
4904
5052
|
// assertTurn ITSELF records the "every" quantifier (point 3) on a plain
|
|
4905
5053
|
// universal success, so every caller (this loop AND the top-level
|
|
@@ -5114,6 +5262,12 @@ const MODULE_ORIENT_SVO_RE = new RegExp(`^what\\s+(.+?)\\s+does${TRAILING_ADVERB
|
|
|
5114
5262
|
// terms. "what(?:'s|s|\s+is)" mirrors PERSONAL_ASSISTANT_NUDGE_RE's own
|
|
5115
5263
|
// tolerance for the bare "whats" contraction spelling, just below.
|
|
5116
5264
|
const MODULE_PURPOSE_RE = /^what(?:'s|s|\s+is)\s+(.+?)\s+(?:for|about)\??$/i;
|
|
5265
|
+
// "what is the purpose of the validate module" — the purpose-of phrasing of the
|
|
5266
|
+
// SAME module-grain overview, asking by the module's role rather than "for"/
|
|
5267
|
+
// "does". The captured object ("the validate module", "validate") is resolved
|
|
5268
|
+
// through the SAME exact-unique resolveEntity gate below; a non-module term
|
|
5269
|
+
// simply fails to resolve and the lane declines, so this never misroutes.
|
|
5270
|
+
const MODULE_PURPOSE_OF_RE = /^what(?:'s|s|\s+is)\s+the\s+(?:purpose|point|role|job|function)\s+of\s+(.+?)\??$/i;
|
|
5117
5271
|
|
|
5118
5272
|
/** A module PATH as a reader types it — "src/core/store.mjs", "app/lib/b.mjs",
|
|
5119
5273
|
* or a bare "store.mjs". Requires a slash or a source-file extension, which is
|
|
@@ -5168,7 +5322,7 @@ async function moduleOrientLane(query, { graph }) {
|
|
|
5168
5322
|
// (stripFillerWords already eats "please"/"could you" as filler; the politeness
|
|
5169
5323
|
// regex only adds the "explain [to me]" wrapper on top).
|
|
5170
5324
|
q = stripFillerWords(applyPreambleFrames(correctMisspellings(q))).replace(MODULE_ORIENT_POLITENESS_RE, "");
|
|
5171
|
-
const m = q.match(MODULE_ORIENT_RE) || q.match(MODULE_PURPOSE_RE) || q.match(MODULE_ORIENT_SVO_RE);
|
|
5325
|
+
const m = q.match(MODULE_ORIENT_RE) || q.match(MODULE_PURPOSE_OF_RE) || q.match(MODULE_PURPOSE_RE) || q.match(MODULE_ORIENT_SVO_RE);
|
|
5172
5326
|
// "what does src/core/store.mjs do" already reached the overview; the bare
|
|
5173
5327
|
// path and "what is <path>" did not, so the same module answered one
|
|
5174
5328
|
// phrasing and walled two. Both are claimed here rather than in ask.mjs,
|
|
@@ -5614,8 +5768,11 @@ export async function helpText() {
|
|
|
5614
5768
|
["/plan <request>", "the capability router: plan+execute a compound or maintenance-goal request (\"of the modules impacted by X, which are untested\", \"what most needs a test\")"],
|
|
5615
5769
|
["/capabilities", "what /plan can plan over: the built-in graph tools plus your taught actions"],
|
|
5616
5770
|
["/syllogise <term>", "work out and remember what follows from the facts about a term (needed for chains longer than 2 hops)"],
|
|
5771
|
+
["/export <path>", "write the memory store to a file, as JSONL (the same shape `tmct memory --export` writes)"],
|
|
5772
|
+
["/ingest <path>", "read a local text file and store every fact the recognizer grounds from it (same recognizer as `tmct extract`)"],
|
|
5617
5773
|
["/narrate on|off", "verbose developer/debug mode: decision points, matched pattern, results+sources, goal per turn"],
|
|
5618
|
-
["/wiki on|off", "live Wikipedia
|
|
5774
|
+
["/wiki on|off|supplement|always", "live Wikipedia (default off): on tries en.wikipedia.org when I can't answer (network), cited; supplement also adds a read-out under every grounded vocabulary answer; always widens that to every grounded answer"],
|
|
5775
|
+
["research <topic> [limit N]", "fetch the topic from Simple English Wikipedia (the explicit ask is the network consent), store what it grounds, and queue its linked topics — \"research next\" steps the queue; also status/stop"],
|
|
5619
5776
|
["/help", "this list"],
|
|
5620
5777
|
["/exit", "leave the session (also Ctrl+C / Ctrl+D)"],
|
|
5621
5778
|
];
|
|
@@ -5895,6 +6052,14 @@ function predicatePhrase(predicate) {
|
|
|
5895
6052
|
// "is smaller than" (never a 3sg fold — "smallers" isn't a word)
|
|
5896
6053
|
const comp = /^mgx:([a-z]+(?:-[a-z]+)*)-than$/i.exec(p);
|
|
5897
6054
|
if (comp) return `is ${comp[1].replace(/-/g, " ")} than`;
|
|
6055
|
+
// a participle + preposition renders as its copula surface: mgx:connected-with
|
|
6056
|
+
// -> "is connected with" (the participle is already a participle, so no 3sg
|
|
6057
|
+
// fold — "connecteds" isn't a word)
|
|
6058
|
+
const part = new RegExp(`^mgx:(${TEACH_PARTICIPLE_SRC})-([a-z]+)$`, "i").exec(p);
|
|
6059
|
+
if (part) return `is ${part[1].toLowerCase()} ${part[2].toLowerCase()}`;
|
|
6060
|
+
// a shared-attribute predicate: mgx:same-goal-as -> "has the same goal as"
|
|
6061
|
+
const same = /^mgx:same-([a-z]+)-as$/i.exec(p);
|
|
6062
|
+
if (same) return `has the same ${same[1].toLowerCase()} as`;
|
|
5898
6063
|
const m = /^mgx:([a-z]+)(?:-([a-z]+))?$/i.exec(p);
|
|
5899
6064
|
if (!m) return predicate;
|
|
5900
6065
|
// a folded preposition renders back naturally: mgx:rest-on -> "rests on"
|
|
@@ -5976,6 +6141,74 @@ function renderFactLine(f) {
|
|
|
5976
6141
|
return `i learned: ${factPhrase(f)}${cite}`;
|
|
5977
6142
|
}
|
|
5978
6143
|
|
|
6144
|
+
const SENSE_CITE_RE = / \(source: [^)]*\)$/;
|
|
6145
|
+
|
|
6146
|
+
/** Append an is-a object's superclass chain to its rendered fact line, before
|
|
6147
|
+
* the citation: "rover is a kind of dog" becomes "rover is a kind of dog →
|
|
6148
|
+
* canine → mammal → animal". Only the subject-side is-a lines of the queried
|
|
6149
|
+
* term get a chain; every other line renders unchanged. */
|
|
6150
|
+
function renderFactLineWithChain(f, parents, subjectVariants) {
|
|
6151
|
+
const base = renderFactLine(f);
|
|
6152
|
+
if (!ISA_PREDICATES.has(f.predicate) || !subjectVariants.has(f.subject)) return base;
|
|
6153
|
+
const chain = ancestryChain(f.object, parents, { cap: 6 });
|
|
6154
|
+
if (chain.length <= 1) return base;
|
|
6155
|
+
const suffix = ` → ${chain.slice(1).join(" → ")}`;
|
|
6156
|
+
const cite = base.match(SENSE_CITE_RE);
|
|
6157
|
+
return cite ? base.slice(0, cite.index) + suffix + cite[0] : base + suffix;
|
|
6158
|
+
}
|
|
6159
|
+
|
|
6160
|
+
/** Render a subject-scan fact list with each is-a object's superclass chain
|
|
6161
|
+
* shown, and — when the subject's is-a objects split into distinct concepts
|
|
6162
|
+
* (a `dog` sense and a `scout` sense of one "rover") — grouped by concept.
|
|
6163
|
+
* Grouping is presentation only: every fact still renders and is cited, in
|
|
6164
|
+
* the same order, under a "<subject>, the <concept>:" heading.
|
|
6165
|
+
*
|
|
6166
|
+
* Returns `{ lines, grouped }`. `lines` is the flat, chain-enhanced rendering
|
|
6167
|
+
* (indented by `indent`) the caller uses when senses do not split. `grouped`
|
|
6168
|
+
* is a ready `{ text, replace, pending? }` answer when they do, else null. */
|
|
6169
|
+
function senseSplitFactList(hits, rows, subjectVariants, { indent = "" } = {}) {
|
|
6170
|
+
const subClassEdges = rows.filter((f) => f.predicate === SUBCLASS_PREDICATE).map((f) => [f.subject, f.object]);
|
|
6171
|
+
const parents = subClassParents(subClassEdges);
|
|
6172
|
+
const lines = hits.map((f) => `${indent}${renderFactLineWithChain(f, parents, subjectVariants)}`);
|
|
6173
|
+
|
|
6174
|
+
const isaSubjectFacts = hits.filter((f) => ISA_PREDICATES.has(f.predicate) && subjectVariants.has(f.subject));
|
|
6175
|
+
const isaObjects = [...new Set(isaSubjectFacts.map((f) => f.object))];
|
|
6176
|
+
if (isaObjects.length < 2) return { lines, grouped: null };
|
|
6177
|
+
const disjointEdges = rows.filter((f) => f.predicate === "owl:disjointWith").map((f) => [f.subject, f.object]);
|
|
6178
|
+
const { split, clusters } = clusterSenses(isaObjects, { parents, disjointEdges });
|
|
6179
|
+
if (!split) return { lines, grouped: null };
|
|
6180
|
+
|
|
6181
|
+
const subject = isaSubjectFacts[0].subject;
|
|
6182
|
+
const clusterOf = new Map();
|
|
6183
|
+
for (const c of clusters) for (const o of c.objects) clusterOf.set(o, c);
|
|
6184
|
+
const otherHits = hits.filter((f) => !(ISA_PREDICATES.has(f.predicate) && subjectVariants.has(f.subject)));
|
|
6185
|
+
|
|
6186
|
+
const blocks = [];
|
|
6187
|
+
const restItems = [];
|
|
6188
|
+
let shownCount = 0;
|
|
6189
|
+
const addLine = (f) => {
|
|
6190
|
+
const rendered = renderFactLineWithChain(f, parents, subjectVariants);
|
|
6191
|
+
if (shownCount < FACT_ANSWER_CAP) { shownCount += 1; return `${indent}${rendered}`; }
|
|
6192
|
+
restItems.push(rendered);
|
|
6193
|
+
return null;
|
|
6194
|
+
};
|
|
6195
|
+
for (const c of clusters) {
|
|
6196
|
+
const clusterLines = isaSubjectFacts.filter((f) => clusterOf.get(f.object) === c).map(addLine).filter(Boolean);
|
|
6197
|
+
if (clusterLines.length) blocks.push(`${indent}${subject}, the ${c.label}:\n${clusterLines.join("\n")}`);
|
|
6198
|
+
}
|
|
6199
|
+
if (otherHits.length) {
|
|
6200
|
+
const otherLines = otherHits.map(addLine).filter(Boolean);
|
|
6201
|
+
if (otherLines.length) blocks.push(`${indent}also about ${subject}:\n${otherLines.join("\n")}`);
|
|
6202
|
+
}
|
|
6203
|
+
const extra = restItems.length ? `\n${indent}…and ${restItems.length} more — say 'more' to see them.` : "";
|
|
6204
|
+
const grouped = {
|
|
6205
|
+
text: blocks.join("\n") + extra,
|
|
6206
|
+
replace: true,
|
|
6207
|
+
...(restItems.length ? { pending: { items: restItems, noun: "facts" } } : {}),
|
|
6208
|
+
};
|
|
6209
|
+
return { lines, grouped };
|
|
6210
|
+
}
|
|
6211
|
+
|
|
5979
6212
|
/** "a"/"an" for a term, through the SAME grammar-rules.toml "article" rule and
|
|
5980
6213
|
* finish.mjs's beginsWithVowelSound every other agreement site in this file
|
|
5981
6214
|
* uses — never a hardcoded "a", which is ungrammatical for a vowel-initial
|
|
@@ -7075,7 +7308,8 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7075
7308
|
// "disclosed, never dropped" contract). Unconfigured/tied bias degrades to
|
|
7076
7309
|
// trust-desc, byte-identical to before this feature existed.
|
|
7077
7310
|
hits = rankByBiasThenTrust(hits, biasByBundle);
|
|
7078
|
-
const lines = hits
|
|
7311
|
+
const { lines, grouped } = senseSplitFactList(hits, await factRows(memoryDir, cache), variants);
|
|
7312
|
+
if (grouped) return { ...grouped, replace: miss };
|
|
7079
7313
|
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
7080
7314
|
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
7081
7315
|
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
@@ -7624,12 +7858,13 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7624
7858
|
// renders (Part 6's "disclosed, never dropped" contract); literalHit/
|
|
7625
7859
|
// viaSubtype above already resolved off the pre-rank order.
|
|
7626
7860
|
hits = rankByBiasThenTrust(hits, biasByBundle);
|
|
7627
|
-
const
|
|
7861
|
+
const header = `${hits.length} remembered fact${hits.length === 1 ? "" : "s"} about ${term}`
|
|
7862
|
+
+ `${viaSubtype ? " (including its known subtypes)" : ""}:`;
|
|
7863
|
+
const { lines, grouped } = senseSplitFactList(hits, rows, variants, { indent: " " });
|
|
7864
|
+
if (grouped) return { ...grouped, text: `${header}\n${grouped.text}` };
|
|
7628
7865
|
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
7629
7866
|
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
7630
7867
|
const extra = rest.length ? `\n …and ${rest.length} more — say 'more' to see them.` : "";
|
|
7631
|
-
const header = `${hits.length} remembered fact${hits.length === 1 ? "" : "s"} about ${term}`
|
|
7632
|
-
+ `${viaSubtype ? " (including its known subtypes)" : ""}:`;
|
|
7633
7868
|
return { text: `${header}\n${shown.join("\n")}${extra}`, replace: true, ...(rest.length ? { pending: { items: rest.map((l) => l.trim()), noun: "facts" } } : {}) };
|
|
7634
7869
|
}
|
|
7635
7870
|
return null;
|
|
@@ -9863,11 +10098,20 @@ async function cleanMissPackKey(term, { graph, memoryDir, lexicon, cache }) {
|
|
|
9863
10098
|
if (!key) return null;
|
|
9864
10099
|
if (await resolveEntity(graph, term)) return null;
|
|
9865
10100
|
let normFactTerm;
|
|
9866
|
-
|
|
10101
|
+
let loadMemory;
|
|
10102
|
+
let readRuleRows;
|
|
10103
|
+
try { ({ normFactTerm, loadMemory, readRuleRows } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
9867
10104
|
const variants = factTermVariants(normFactTerm, term);
|
|
9868
10105
|
variants.add(key);
|
|
9869
10106
|
const rows = await factRows(memoryDir, cache);
|
|
9870
10107
|
if (rows.some((f) => variants.has(f.subject) || variants.has(f.object))) return null;
|
|
10108
|
+
// A taught RULE that owns this term outranks any pack load: surfacing
|
|
10109
|
+
// unrelated conceptnet content over the user's own taught concept is worse
|
|
10110
|
+
// than the honest miss the decline leaves standing.
|
|
10111
|
+
try {
|
|
10112
|
+
const ruleNames = readRuleRows(await loadMemory(memoryDir)).map((r) => normFactTerm(r.name)).filter(Boolean);
|
|
10113
|
+
if (ruleNames.some((n) => variants.has(n))) return null;
|
|
10114
|
+
} catch { /* tolerated — the fact gate above already ran */ }
|
|
9871
10115
|
return key;
|
|
9872
10116
|
}
|
|
9873
10117
|
|
|
@@ -9924,7 +10168,7 @@ async function liveReferenceAnswerForKey(key, onLiveLookup) {
|
|
|
9924
10168
|
* key up in the shipped child triples pack and append every fact under child
|
|
9925
10169
|
* provenance, so the SAME question can be re-asked from the store. Null on a
|
|
9926
10170
|
* pack miss or any failure — the turn then proceeds byte-identically. */
|
|
9927
|
-
async function childPackFactsForKey(key, { memoryDir, env, cache }) {
|
|
10171
|
+
async function childPackFactsForKey(key, { memoryDir, env, cache, synthesisBudget = AUTO_SYNTHESIS_BUDGET }) {
|
|
9928
10172
|
let row = null;
|
|
9929
10173
|
try { row = await getChildPackProvider(env).lookup(key); } catch { row = null; }
|
|
9930
10174
|
if (!row?.facts?.length) return null;
|
|
@@ -9935,22 +10179,87 @@ async function childPackFactsForKey(key, { memoryDir, env, cache }) {
|
|
|
9935
10179
|
})));
|
|
9936
10180
|
} catch { return null; }
|
|
9937
10181
|
if (cache) cache.rows = null;
|
|
10182
|
+
await synthesiseAroundTerm(memoryDir, key, cache, synthesisBudget);
|
|
9938
10183
|
return { key, count: row.facts.length };
|
|
9939
10184
|
}
|
|
9940
10185
|
|
|
9941
|
-
/** Store the article's first-sentence isa
|
|
9942
|
-
*
|
|
9943
|
-
*
|
|
9944
|
-
|
|
9945
|
-
|
|
10186
|
+
/** Store every triple the article's summary grounds — its first-sentence isa
|
|
10187
|
+
* plus each candidate the optimistic tier reads from the rest of the summary —
|
|
10188
|
+
* all under the article's own provenance, so a learned load becomes durable
|
|
10189
|
+
* knowledge rather than a single isa fact. Runs AFTER the cited answer composed
|
|
10190
|
+
* and is failure-tolerated: the answer stands whether or not the facts land.
|
|
10191
|
+
* The optimistic tier is pure (no recognizer re-entry), so this stays cheap on
|
|
10192
|
+
* the chat turn. Returns the count stored. */
|
|
10193
|
+
async function ingestReferenceArticle(memoryDir, key, article, cache, tagFor = referenceProvenanceTag, lexicon = null, synthesisBudget = AUTO_SYNTHESIS_BUDGET) {
|
|
10194
|
+
if (!article) return 0;
|
|
10195
|
+
const provenance = tagFor(article);
|
|
10196
|
+
const facts = [];
|
|
10197
|
+
const seen = new Set();
|
|
10198
|
+
const add = (subject, predicate, object) => {
|
|
10199
|
+
const id = `${subject}\0${predicate}\0${object}`;
|
|
10200
|
+
if (subject && object && subject !== object && !seen.has(id)) { seen.add(id); facts.push({ subject, predicate, object, provenance }); }
|
|
10201
|
+
};
|
|
10202
|
+
if (article.isa) add(key, "rdfs:subClassOf", article.isa);
|
|
9946
10203
|
try {
|
|
9947
|
-
const {
|
|
9948
|
-
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
10204
|
+
const { optimisticTriples } = await import("./extract-facts.mjs");
|
|
10205
|
+
for (const sentence of splitSentences(article.summary || article.text || "")) {
|
|
10206
|
+
for (const t of optimisticTriples(sentence, { lexicon: lexicon ?? undefined })) add(t.subject, t.predicate, t.object);
|
|
10207
|
+
}
|
|
10208
|
+
} catch { /* the isa alone still lands below */ }
|
|
10209
|
+
if (!facts.length) return 0;
|
|
10210
|
+
try {
|
|
10211
|
+
const { appendFacts } = await import("../adapters/memory/core.mjs");
|
|
10212
|
+
await appendFacts(memoryDir, facts);
|
|
9952
10213
|
if (cache) cache.rows = null;
|
|
9953
|
-
} catch {
|
|
10214
|
+
} catch { return 0; }
|
|
10215
|
+
await synthesiseAroundTerm(memoryDir, key, cache, synthesisBudget);
|
|
10216
|
+
return facts.length;
|
|
10217
|
+
}
|
|
10218
|
+
|
|
10219
|
+
// A learn-on-miss load stores a handful of new facts; the auto-synthesis pass
|
|
10220
|
+
// that connects them to the rest of the store is deliberately small — a low
|
|
10221
|
+
// budget, focus expanded through the loaded term — so it stays a per-ingest
|
|
10222
|
+
// materialisation, not the whole-store maintenance job /syllogise runs.
|
|
10223
|
+
const AUTO_SYNTHESIS_BUDGET = 12;
|
|
10224
|
+
|
|
10225
|
+
/** After a learn-on-miss load stored new facts about `term`, run a bounded,
|
|
10226
|
+
* focus-scoped forward-chaining pass so the new facts connect to what's
|
|
10227
|
+
* already remembered — the auto sibling of the /syllogise command. Derived
|
|
10228
|
+
* facts carry entailed:* provenance at their discounted trust and are
|
|
10229
|
+
* retractable. Failure-tolerated: a synthesis miss never disturbs the answer
|
|
10230
|
+
* the load already composed. Returns the count derived. */
|
|
10231
|
+
async function synthesiseAroundTerm(memoryDir, term, cache, budget = AUTO_SYNTHESIS_BUDGET) {
|
|
10232
|
+
if (!memoryDir || !term || budget <= 0) return 0;
|
|
10233
|
+
try {
|
|
10234
|
+
const { syllogise } = await import("../domain/syllogise.mjs");
|
|
10235
|
+
const { loadMemory, readFactRows, appendFacts, normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
10236
|
+
const res = await syllogise(memoryDir, {
|
|
10237
|
+
focus: [...factTermVariants(normFactTerm, term)],
|
|
10238
|
+
expandFocus: true,
|
|
10239
|
+
budget,
|
|
10240
|
+
store: { loadMemory, readFactRows, appendFacts },
|
|
10241
|
+
});
|
|
10242
|
+
if (res?.count && cache) cache.rows = null;
|
|
10243
|
+
return res?.count || 0;
|
|
10244
|
+
} catch { return 0; }
|
|
10245
|
+
}
|
|
10246
|
+
|
|
10247
|
+
/** The term an explicit "ask Wikipedia" phrasing names — "what does wikipedia
|
|
10248
|
+
* say about X", "ask wikipedia about X", "X on wikipedia" — or null when the
|
|
10249
|
+
* line isn't such a request. Unlike the clean-miss gate, this fires even when
|
|
10250
|
+
* local facts could answer: the user asked Wikipedia specifically. */
|
|
10251
|
+
const WIKIPEDIA_ASK_RES = [
|
|
10252
|
+
/^what\s+(?:does|do)\s+wikipedia\s+say\s+(?:about\s+)?(.+?)[?.!\s]*$/i,
|
|
10253
|
+
/^ask\s+wikipedia\s+(?:about\s+)?(.+?)[?.!\s]*$/i,
|
|
10254
|
+
/^(?:look\s+up\s+|tell\s+me\s+about\s+|what\s+(?:is|are)\s+(?:an?\s+|the\s+)?)?(.+?)\s+on\s+wikipedia[?.!\s]*$/i,
|
|
10255
|
+
];
|
|
10256
|
+
function wikipediaAskTerm(query) {
|
|
10257
|
+
const q = String(query || "").trim();
|
|
10258
|
+
for (const re of WIKIPEDIA_ASK_RES) {
|
|
10259
|
+
const m = q.match(re);
|
|
10260
|
+
if (m && m[1] && m[1].trim()) return m[1].trim().replace(/^(?:an?|the)\s+/i, "");
|
|
10261
|
+
}
|
|
10262
|
+
return null;
|
|
9954
10263
|
}
|
|
9955
10264
|
|
|
9956
10265
|
/** The concept term a vague "what is a X" / "tell me about X" / "what does X mean" /
|
|
@@ -10144,6 +10453,11 @@ async function describeWrapperAnswer(query, { config, source, focus, graph, tel
|
|
|
10144
10453
|
// captured term, same class of gap stripTrailingDiscourseTag (ask-vocab.mjs)
|
|
10145
10454
|
// already fixes for the meta-whatis vocab lane.
|
|
10146
10455
|
term = stripTrailingDiscourseTag(term);
|
|
10456
|
+
// "tell me about the router thing" / "the logging stuff" — a vague filler
|
|
10457
|
+
// noun wrapped around a real term. Strip it so the describe lane resolves
|
|
10458
|
+
// the term itself; an unresolvable remainder still declines to the ordinary
|
|
10459
|
+
// miss below, so this only ever widens what grounds, never misroutes.
|
|
10460
|
+
term = term.replace(/\s+(?:thing|things|thingy|stuff)$/i, "").trim() || term;
|
|
10147
10461
|
if (DESCRIBE_PRONOUN_RE.test(term)) {
|
|
10148
10462
|
if (!focus?.label) return null; // no standing focus to resolve against — honest decline
|
|
10149
10463
|
term = focus.label;
|
|
@@ -11122,8 +11436,12 @@ const DECISION_RECALL_RE = /^(?:remind\s+me\s+)?what\s+(?:did\s+)?(?:we|i|you)\s
|
|
|
11122
11436
|
* than silently accepted alongside the current location. */
|
|
11123
11437
|
const MOVE_HISTORY_RE = /^where\s+did\s+(.+?)\s+(?:move|get\s+moved|go)(?:\s+to)?[?.!\s]*$/i;
|
|
11124
11438
|
|
|
11125
|
-
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, gameConfig = DEFAULT_GAME_CONFIG, liveReference = false, onLiveLookup = null }) {
|
|
11439
|
+
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, gameConfig = DEFAULT_GAME_CONFIG, liveReference = false, onLiveLookup = null, uiContext = "cli", synthesisBudget = AUTO_SYNTHESIS_BUDGET }) {
|
|
11126
11440
|
const ts = new Date().toISOString();
|
|
11441
|
+
// The surface this turn runs on ("cli" default; "browser" from a web entry) —
|
|
11442
|
+
// the honest-miss tail below points a browser/adventure miss at the teach
|
|
11443
|
+
// lane instead of the CLI-only --repo/tmct-init remedy.
|
|
11444
|
+
const browser = uiContext === "browser";
|
|
11127
11445
|
// DISCOURSE ANAPHORA: a follow-up like "which of those are tested" / "count
|
|
11128
11446
|
// them" filters or counts the PREVIOUS answer's entity set, threaded as
|
|
11129
11447
|
// ask()'s `prev`. Prefers the FULL id set (`allIds`) over `matches`, since a
|
|
@@ -11165,6 +11483,31 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11165
11483
|
});
|
|
11166
11484
|
}
|
|
11167
11485
|
}
|
|
11486
|
+
// EXPLICIT WIKIPEDIA ASK — "what does wikipedia say about X" / "ask wikipedia
|
|
11487
|
+
// about X" / "X on wikipedia". Unlike the clean-miss packs, this fires even
|
|
11488
|
+
// when local facts could answer: the user named the source. It still honours
|
|
11489
|
+
// the network opt-in (a live lookup is a network request), so with the toggle
|
|
11490
|
+
// off it points at /wiki on rather than reaching the network.
|
|
11491
|
+
{
|
|
11492
|
+
const wikiTerm = wikipediaAskTerm(query);
|
|
11493
|
+
if (wikiTerm) {
|
|
11494
|
+
note(trace, "goal: read what Wikipedia says about a named term (explicit source request)");
|
|
11495
|
+
if (!liveReference) {
|
|
11496
|
+
note(trace, "lane: WIKIPEDIA ASK — the explicit request needs the network opt-in; live Wikipedia is off");
|
|
11497
|
+
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 });
|
|
11498
|
+
}
|
|
11499
|
+
let liveKey = null;
|
|
11500
|
+
try { liveKey = cleanMissLiveTerm(wikiTerm, lexicon ?? undefined); } catch { liveKey = null; }
|
|
11501
|
+
const live = liveKey ? await liveReferenceAnswerForKey(liveKey, onLiveLookup) : null;
|
|
11502
|
+
if (live) {
|
|
11503
|
+
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon, synthesisBudget);
|
|
11504
|
+
note(trace, `lane: WIKIPEDIA ASK — answered from a live en.wikipedia.org lookup, cited (article "${live.article.title}", revid ${live.article.revid})`);
|
|
11505
|
+
return plainTurn(query, live.text, { via: "reference", miss: false, focus });
|
|
11506
|
+
}
|
|
11507
|
+
note(trace, "lane: WIKIPEDIA ASK — no matching live article (no title, timeout, throttle, or drift-guard reject)");
|
|
11508
|
+
return plainTurn(query, `I couldn't reach a matching Wikipedia article for "${wikiTerm}" just now.`, { via: "miss", miss: true, focus });
|
|
11509
|
+
}
|
|
11510
|
+
}
|
|
11168
11511
|
// COLLECTIVE PLURAL SUBJECT — see COLLECTIVE_FORWARD_RE. Members are the
|
|
11169
11512
|
// modules whose path carries the plural as a component; two or more make it
|
|
11170
11513
|
// a group question, answered as the disclosed union over every member. One
|
|
@@ -11320,8 +11663,12 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11320
11663
|
// examples are the wrong audience here), an unseeded one at the seed/
|
|
11321
11664
|
// teach pair — vocabHint already carries exactly that split.
|
|
11322
11665
|
answer = (!graph || noCodeGraph(graph)) && (!config || e?.emptyGraph || /^cannot read graph artifact\b/.test(thrown))
|
|
11666
|
+
// A browser session has no `tmct init in a repo` to reach for, so its
|
|
11667
|
+
// fallback drops that CLI-only remedy and keeps just the teach pointer.
|
|
11323
11668
|
? `I can't answer that as a code question — no code graph is loaded in this session. ${vocabHint
|
|
11324
|
-
||
|
|
11669
|
+
|| (browser
|
|
11670
|
+
? "I can still remember and answer taught facts (try \"every bug is an issue\")."
|
|
11671
|
+
: "I can still remember and answer taught facts (try \"every bug is an issue\"), or run `tmct init` in a repo to index one.")}`
|
|
11325
11672
|
: thrown;
|
|
11326
11673
|
note(trace, `intermediate: the ask engine threw — ${thrown}`);
|
|
11327
11674
|
}
|
|
@@ -11720,7 +12067,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11720
12067
|
if (!bareMetaHit) {
|
|
11721
12068
|
const refTerm = metaTermOf(factQuery, envelope);
|
|
11722
12069
|
const key = refTerm ? await cleanMissPackKey(refTerm, { graph, memoryDir, lexicon, cache }) : null;
|
|
11723
|
-
const learned = key ? await childPackFactsForKey(key, { memoryDir, env, cache }) : null;
|
|
12070
|
+
const learned = key ? await childPackFactsForKey(key, { memoryDir, env, cache, synthesisBudget }) : null;
|
|
11724
12071
|
if (learned) {
|
|
11725
12072
|
const fact = (await factAnswer(memoryDir, factQuery, envelope, miss, biasByBundle, cache, newFocus?.label))
|
|
11726
12073
|
?? (await factReadBack(memoryDir, factQuery, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
|
|
@@ -11776,15 +12123,15 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11776
12123
|
const coldPronounDecline = focus?.label ? null : coldPronounDeclineText(query);
|
|
11777
12124
|
if (bareMetaHit?.reference) {
|
|
11778
12125
|
// The bare-form reference hit mirrors (4h): the cited answer replaces the
|
|
11779
|
-
// miss, the turn is no longer recorded as one, and the article's
|
|
11780
|
-
// stored after the answer composes.
|
|
12126
|
+
// miss, the turn is no longer recorded as one, and the article's grounded
|
|
12127
|
+
// triples are stored after the answer composes.
|
|
11781
12128
|
answer = bareMetaHit.text;
|
|
11782
12129
|
via = "reference";
|
|
11783
12130
|
recordMiss = false;
|
|
11784
12131
|
handled = true;
|
|
11785
12132
|
note(trace, "lane: (2b) REFERENCE PACK — a bare \"what is X\" clean miss answered from the shipped reference pack, cited");
|
|
11786
12133
|
note(trace, `source: reference pack ${REFERENCE_PACK_NAME} — article "${bareMetaHit.reference.article.title}" (revid ${bareMetaHit.reference.article.revid})`);
|
|
11787
|
-
await
|
|
12134
|
+
await ingestReferenceArticle(memoryDir, bareMetaHit.reference.key, bareMetaHit.reference.article, cache, referenceProvenanceTag, lexicon, synthesisBudget);
|
|
11788
12135
|
} else if (bareMetaHit?.live) {
|
|
11789
12136
|
// The bare-form LIVE hit settles the same way, under live provenance.
|
|
11790
12137
|
answer = bareMetaHit.text;
|
|
@@ -11793,7 +12140,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11793
12140
|
handled = true;
|
|
11794
12141
|
note(trace, "lane: (2b) LIVE WIKIPEDIA — a bare \"what is X\" clean miss answered from a live en.wikipedia.org lookup (opt-in), cited");
|
|
11795
12142
|
note(trace, `source: live reference ${LIVE_PACK_NAME} — article "${bareMetaHit.live.article.title}" (revid ${bareMetaHit.live.article.revid})`);
|
|
11796
|
-
await
|
|
12143
|
+
await ingestReferenceArticle(memoryDir, bareMetaHit.live.key, bareMetaHit.live.article, cache, liveProvenanceTag, lexicon, synthesisBudget);
|
|
11797
12144
|
} else if (bareMetaHit) {
|
|
11798
12145
|
answer = bareMetaHit.replace ? bareMetaHit.text : `${answer}\n${bareMetaHit.text}`;
|
|
11799
12146
|
// Same discipline as lane (3): a fact-lane return flagged `miss` is an
|
|
@@ -12020,7 +12367,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12020
12367
|
// (4) #2 TEACH lane — a teach-shaped would-miss nothing above answered: route to
|
|
12021
12368
|
// memory, or say what CAN be remembered (LOUD), never the wall / a silent drop.
|
|
12022
12369
|
if (miss && recordMiss && via === "composed") {
|
|
12023
|
-
const taught = await teachLane(query, { memoryDir, sessionId, lexicon, cache, planHolder });
|
|
12370
|
+
const taught = await teachLane(query, { memoryDir, sessionId, lexicon, cache, planHolder, graph });
|
|
12024
12371
|
if (taught) {
|
|
12025
12372
|
answer = taught.text; via = taught.via; recordMiss = taught.miss;
|
|
12026
12373
|
if (!taught.miss) dialogueLaneOverride = "teach";
|
|
@@ -12039,6 +12386,26 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12039
12386
|
// kind of parent" as inherits) keeps its canonical: it genuinely
|
|
12040
12387
|
// restates the relation the teach stored.
|
|
12041
12388
|
if (envelope?.parsed?.fuzzyVerb) canonical = null;
|
|
12389
|
+
// A mid-game locative teach is accepted and stored as the player's own
|
|
12390
|
+
// note, but a running adventure's world only moves through actions — so
|
|
12391
|
+
// when the taught subject is a term the world already places, the
|
|
12392
|
+
// confirmation says so plainly. (The fold itself keeps taught rows out of
|
|
12393
|
+
// the world state; this is the matching UX.)
|
|
12394
|
+
if (!taught.miss && planHolder?.state?.adventure) {
|
|
12395
|
+
const loc = String(query).trim().match(LOCATIVE_TEACH_RE);
|
|
12396
|
+
if (loc) {
|
|
12397
|
+
let placed = false;
|
|
12398
|
+
try {
|
|
12399
|
+
const { normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
12400
|
+
const world = foldWorldState(await factRows(memoryDir, cache));
|
|
12401
|
+
placed = world.placements.has(normFactTerm(loc[1]));
|
|
12402
|
+
} catch { placed = false; }
|
|
12403
|
+
if (placed) {
|
|
12404
|
+
answer = `${answer}\n(noted as your note — the game world itself only changes through actions like go, take and open.)`;
|
|
12405
|
+
note(trace, "intermediate: mid-game locative teach — stored as a note; the adventure fold only moves through actions");
|
|
12406
|
+
}
|
|
12407
|
+
}
|
|
12408
|
+
}
|
|
12042
12409
|
}
|
|
12043
12410
|
}
|
|
12044
12411
|
// (4b) #4 AUTHOR lane — "who is <Name>", "what did <Name> touch",
|
|
@@ -12179,7 +12546,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12179
12546
|
if (miss && recordMiss && via === "composed" && memoryDir) {
|
|
12180
12547
|
const refTerm = metaTermOf(query, envelope);
|
|
12181
12548
|
const key = refTerm ? await cleanMissPackKey(refTerm, { graph, memoryDir, lexicon, cache }) : null;
|
|
12182
|
-
const learned = key ? await childPackFactsForKey(key, { memoryDir, env, cache }) : null;
|
|
12549
|
+
const learned = key ? await childPackFactsForKey(key, { memoryDir, env, cache, synthesisBudget }) : null;
|
|
12183
12550
|
if (learned) {
|
|
12184
12551
|
const fact = (await factAnswer(memoryDir, query, envelope, miss, biasByBundle, cache, newFocus?.label))
|
|
12185
12552
|
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
|
|
@@ -12200,7 +12567,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12200
12567
|
recordMiss = false;
|
|
12201
12568
|
note(trace, "lane: (4h) REFERENCE PACK — a clean miss on a lexicon term answered from the shipped reference pack, cited");
|
|
12202
12569
|
note(trace, `source: reference pack ${REFERENCE_PACK_NAME} — article "${ref.article.title}" (revid ${ref.article.revid})`);
|
|
12203
|
-
await
|
|
12570
|
+
await ingestReferenceArticle(memoryDir, ref.key, ref.article, cache, referenceProvenanceTag, lexicon, synthesisBudget);
|
|
12204
12571
|
}
|
|
12205
12572
|
}
|
|
12206
12573
|
// The live Wikipedia supplement (opt-in), strictly AFTER both shipped
|
|
@@ -12216,7 +12583,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12216
12583
|
recordMiss = false;
|
|
12217
12584
|
note(trace, "lane: (4h) LIVE WIKIPEDIA — a clean miss answered from a live en.wikipedia.org lookup (opt-in), cited");
|
|
12218
12585
|
note(trace, `source: live reference ${LIVE_PACK_NAME} — article "${live.article.title}" (revid ${live.article.revid})`);
|
|
12219
|
-
await
|
|
12586
|
+
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon, synthesisBudget);
|
|
12220
12587
|
}
|
|
12221
12588
|
}
|
|
12222
12589
|
}
|
|
@@ -12238,12 +12605,25 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12238
12605
|
note(trace, `lane: (5) SHORT TAILORED MISS — every lane above declined; ${repeat ? "REPEAT collapsed to one-liner (wall kindness)" : "the full grammar wall was shortened + tailored to the query's keywords"}`);
|
|
12239
12606
|
}
|
|
12240
12607
|
// #4 HONEST-EMPTY POLISH — an empty CODE graph: any still-standing engine
|
|
12241
|
-
// dead-end (an honest empty, the short miss, the bootstrap note) carries the
|
|
12242
|
-
// toward a real graph, unless it already points there. Only when
|
|
12608
|
+
// dead-end (an honest empty, the short miss, the bootstrap note) carries the
|
|
12609
|
+
// exit toward a real graph, unless it already points there. Only when
|
|
12610
|
+
// genuinely empty. The CLI keeps the --repo/example pointer verbatim; a
|
|
12611
|
+
// browser or a live adventure has no such command to reach for, so each gets
|
|
12612
|
+
// a teach-forward pointer (and the adventure also names the world asides that
|
|
12613
|
+
// are guaranteed to hit).
|
|
12614
|
+
const adventureLive = !!planHolder?.state?.adventure;
|
|
12243
12615
|
if (recordMiss && (via === "composed" || via === "miss")
|
|
12244
12616
|
&& noCodeGraph(graph) && !/--repo|tmct init|no code graph/i.test(answer)) {
|
|
12245
|
-
|
|
12246
|
-
|
|
12617
|
+
if (adventureLive) {
|
|
12618
|
+
answer = `${answer}\n(I don't know that yet — you can teach me: say "remember: <thing> is a <kind>". Or ask the world: "look", "where is the key", "talk to the butler".)`;
|
|
12619
|
+
note(trace, "intermediate: HONEST-EMPTY POLISH — a live adventure miss points at the teach lane and the world asides, not the --repo remedy");
|
|
12620
|
+
} else if (browser) {
|
|
12621
|
+
answer = `${answer}\n(I don't know that yet — you can teach me: say "remember: <thing> is a <kind>".)`;
|
|
12622
|
+
note(trace, "intermediate: HONEST-EMPTY POLISH — a browser miss points at the teach lane, not the CLI-only --repo remedy");
|
|
12623
|
+
} else {
|
|
12624
|
+
answer = `${answer}\n(this repo has no code graph — for structure, point me at a \`.tmct/graph.json\` with \`--repo <path>\` or run \`npm run example:mini\`; tmct doesn't index code itself.)`;
|
|
12625
|
+
note(trace, "intermediate: HONEST-EMPTY POLISH — the loaded graph has 0 modules, so the dead-end got a --repo/tmct init pointer appended");
|
|
12626
|
+
}
|
|
12247
12627
|
}
|
|
12248
12628
|
// TEACH-OFFER: a "what is X" miss where X is genuinely unknown EVERYWHERE —
|
|
12249
12629
|
// not a real graph entity, not a schema/vocab term, and not already in
|
|
@@ -12314,6 +12694,27 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12314
12694
|
answer = `hypothetically, if ${counterfactualSubject[1].trim()} were removed: ${answer}`;
|
|
12315
12695
|
note(trace, `intermediate: COUNTERFACTUAL_RE matched — compiled to a real traversal, wrapped as hypothetical ("${counterfactualSubject[1].trim()}" removed)`);
|
|
12316
12696
|
}
|
|
12697
|
+
// LIVE SUPPLEMENT (/wiki supplement, and its superset /wiki always): a
|
|
12698
|
+
// grounded answer also carries what Wikipedia says about its subject —
|
|
12699
|
+
// corroboration, not rescue. Scoped to a clean vocabulary subject (a
|
|
12700
|
+
// "what is X" / "tell me about X" term), never doubled onto an answer that
|
|
12701
|
+
// already IS a Wikipedia read-out. Failure-tolerated, and network-gated by
|
|
12702
|
+
// the same toggle (both values are truthy, so the rescue lanes above already
|
|
12703
|
+
// ran). "always" widens the term fallback to an ordinary grounded ask's own
|
|
12704
|
+
// parsed object, so a plain code/fact answer also gets corroborated; the
|
|
12705
|
+
// adapter's throttle bounds the request rate.
|
|
12706
|
+
if ((liveReference === "supplement" || liveReference === "always") && !recordMiss && via !== "reference") {
|
|
12707
|
+
const supplementTerm = metaTermOf(query, envelope) || vagueTouchTermOf(query)
|
|
12708
|
+
|| (liveReference === "always" ? envelope?.parsed?.object : null);
|
|
12709
|
+
let liveKey = null;
|
|
12710
|
+
try { liveKey = supplementTerm ? cleanMissLiveTerm(supplementTerm, lexicon ?? undefined) : null; } catch { liveKey = null; }
|
|
12711
|
+
const live = liveKey ? await liveReferenceAnswerForKey(liveKey, onLiveLookup) : null;
|
|
12712
|
+
if (live) {
|
|
12713
|
+
answer = `${answer}\nWikipedia adds: ${live.text}`;
|
|
12714
|
+
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon, synthesisBudget);
|
|
12715
|
+
note(trace, `intermediate: LIVE SUPPLEMENT — appended a cited en.wikipedia.org read-out for "${supplementTerm}" (supplement mode)`);
|
|
12716
|
+
}
|
|
12717
|
+
}
|
|
12317
12718
|
// The concept force answers WITH real example instances — those are the entities the
|
|
12318
12719
|
// turn "asked about" (the SchemaClass meta-node is documentation, not a code entity),
|
|
12319
12720
|
// so record + expand them, not the schema match.
|
|
@@ -12403,6 +12804,8 @@ const GOAL_BY_COMMAND = {
|
|
|
12403
12804
|
capabilities: "see what /plan can plan over — built-in query tools and taught actions",
|
|
12404
12805
|
syllogise: "materialize the entailed facts that follow from what's remembered about one term",
|
|
12405
12806
|
wiki: "toggle the live Wikipedia supplement for questions nothing local can answer",
|
|
12807
|
+
export: "write the memory store to a file, in the standard JSONL shape",
|
|
12808
|
+
ingest: "read a local text file and store every fact the recognizer grounds from it",
|
|
12406
12809
|
};
|
|
12407
12810
|
|
|
12408
12811
|
/** A slash-command → the mapped tool (or the /help, /focus, /narrate, unknown
|
|
@@ -12451,12 +12854,15 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
12451
12854
|
// state). A bare "/wiki" reports the CURRENT state and changes nothing.
|
|
12452
12855
|
if (name === "wiki") {
|
|
12453
12856
|
const arg = argText.toLowerCase();
|
|
12454
|
-
|
|
12455
|
-
|
|
12456
|
-
|
|
12857
|
+
const stateWord = (v) => (v === "always" ? "always" : v === "supplement" ? "supplement" : v ? "on" : "off");
|
|
12858
|
+
if (arg !== "on" && arg !== "off" && arg !== "supplement" && arg !== "always") {
|
|
12859
|
+
return mk(`live Wikipedia supplement is ${stateWord(liveReference)} — /wiki on, /wiki off, /wiki supplement, or /wiki always. `
|
|
12860
|
+
+ "When on, a question I can't answer also tries en.wikipedia.org (network); "
|
|
12861
|
+
+ "supplement adds a cited Wikipedia read-out under every grounded vocabulary answer too; "
|
|
12862
|
+
+ "always widens that to every grounded answer.");
|
|
12457
12863
|
}
|
|
12458
|
-
const next = arg === "on";
|
|
12459
|
-
return mk(`live Wikipedia supplement ${next
|
|
12864
|
+
const next = arg === "always" ? "always" : arg === "supplement" ? "supplement" : arg === "on";
|
|
12865
|
+
return mk(`live Wikipedia supplement ${stateWord(next)}.`, { liveReferenceNext: next });
|
|
12460
12866
|
}
|
|
12461
12867
|
|
|
12462
12868
|
// /memory [verbose] — what tmct remembers, as text (the same renderer
|
|
@@ -12563,6 +12969,92 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
12563
12969
|
}
|
|
12564
12970
|
}
|
|
12565
12971
|
|
|
12972
|
+
// /export <path> — write the whole memory store as JSONL, the SAME shape
|
|
12973
|
+
// `tmct memory --export` and the tmct_export cold tool already emit
|
|
12974
|
+
// (serializeFactsJsonl, export-jsonl.mjs) — so a chat session can take its
|
|
12975
|
+
// facts with it without dropping to a shell.
|
|
12976
|
+
if (name === "export") {
|
|
12977
|
+
note(trace, "goal: write the memory store to a file, in the standard JSONL shape");
|
|
12978
|
+
if (!memoryDir) return mk("no memory store here — /export works inside a repo session.", { miss: true });
|
|
12979
|
+
if (!argText) return mk("/export needs a path, e.g. `/export facts.jsonl`.", { miss: true });
|
|
12980
|
+
try {
|
|
12981
|
+
const { loadMemory } = await import("../adapters/memory/core.mjs");
|
|
12982
|
+
const { serializeFactsJsonl } = await import("../adapters/memory/export-jsonl.mjs");
|
|
12983
|
+
const { writeFile } = await import("node:fs/promises");
|
|
12984
|
+
const { resolve } = await import("node:path");
|
|
12985
|
+
const jsonl = serializeFactsJsonl(await loadMemory(memoryDir));
|
|
12986
|
+
const out = resolve(process.cwd(), argText);
|
|
12987
|
+
await writeFile(out, jsonl, "utf8");
|
|
12988
|
+
const count = jsonl ? jsonl.trimEnd().split("\n").length : 0;
|
|
12989
|
+
note(trace, `result: wrote ${count} fact(s) to ${out}`);
|
|
12990
|
+
return mk(`wrote ${count} fact${count === 1 ? "" : "s"} to ${argText}.`);
|
|
12991
|
+
} catch (e) {
|
|
12992
|
+
return mk(String(e?.message || e), { miss: true }); // a broken store/path reads as its own clean error
|
|
12993
|
+
}
|
|
12994
|
+
}
|
|
12995
|
+
|
|
12996
|
+
// /ingest <path> — the TUI/CLI counterpart to `tmct extract`: read a local
|
|
12997
|
+
// text file, run each sentence through the SAME recognizer the teach lane
|
|
12998
|
+
// already grounds sentences with (runTurn itself — the identical per-
|
|
12999
|
+
// sentence pass extract-facts.mjs's own CLI wrapper runs), and store every
|
|
13000
|
+
// grounded fact into THIS session's own memory store. Deliberately does
|
|
13001
|
+
// NOT call extract-facts.mjs's own main(): that entry point resolves its
|
|
13002
|
+
// OWN memoryDir from a --repo path (or an ephemeral scratch dir), so it
|
|
13003
|
+
// can never target the session's already-open backend handle — grounding
|
|
13004
|
+
// through this session's live memoryDir is what makes an ingested fact
|
|
13005
|
+
// answerable in the SAME conversation, not just written to disk somewhere.
|
|
13006
|
+
if (name === "ingest") {
|
|
13007
|
+
note(trace, "goal: ingest a local text file into the memory store, sentence by sentence");
|
|
13008
|
+
if (!memoryDir) return mk("no memory store here — /ingest works inside a repo session.", { miss: true });
|
|
13009
|
+
if (!argText) return mk("/ingest needs a path, e.g. `/ingest notes.txt`.", { miss: true });
|
|
13010
|
+
const { resolve } = await import("node:path");
|
|
13011
|
+
const { readFile } = await import("node:fs/promises");
|
|
13012
|
+
const filePath = resolve(process.cwd(), argText);
|
|
13013
|
+
let text;
|
|
13014
|
+
try {
|
|
13015
|
+
text = await readFile(filePath, "utf8");
|
|
13016
|
+
} catch (e) {
|
|
13017
|
+
return mk(`couldn't read ${argText} — ${e?.code === "ENOENT" ? "no such file." : String(e?.message || e)}`, { miss: true });
|
|
13018
|
+
}
|
|
13019
|
+
const { splitSentencesPreservingPaths } = await import("./sentences.mjs");
|
|
13020
|
+
const { loadMemory, readFactRows, appendFact } = await import("../adapters/memory/core.mjs");
|
|
13021
|
+
const { touchedFactRows } = await import("../domain/memory/touched-facts.mjs");
|
|
13022
|
+
const sourceTag = filePath.split(/[\\/]/).pop();
|
|
13023
|
+
const sentences = splitSentencesPreservingPaths(text);
|
|
13024
|
+
let recognizedSentences = 0;
|
|
13025
|
+
let factCount = 0;
|
|
13026
|
+
for (const sentence of sentences) {
|
|
13027
|
+
const before = readFactRows(await loadMemory(memoryDir));
|
|
13028
|
+
const { record: ingestRecord } = await runTurn(sentence, { config, memoryDir, sessionId: uuidv7() });
|
|
13029
|
+
if (ingestRecord?.via !== "assert" || ingestRecord?.miss) continue;
|
|
13030
|
+
const after = readFactRows(await loadMemory(memoryDir));
|
|
13031
|
+
const rows = touchedFactRows(before, after);
|
|
13032
|
+
if (!rows.length) continue;
|
|
13033
|
+
recognizedSentences += 1;
|
|
13034
|
+
for (const row of rows) {
|
|
13035
|
+
await appendFact(memoryDir, {
|
|
13036
|
+
subject: row.subject, predicate: row.predicate, object: row.object,
|
|
13037
|
+
provenance: `extracted:${sourceTag}`, quantifier: row.quantifier || "",
|
|
13038
|
+
});
|
|
13039
|
+
factCount += 1;
|
|
13040
|
+
}
|
|
13041
|
+
}
|
|
13042
|
+
if (cache) cache.rows = null; // the fact-rows cache predates these writes
|
|
13043
|
+
const skipped = sentences.length - recognizedSentences;
|
|
13044
|
+
note(trace, `result: ${sentences.length} sentence(s), ${recognizedSentences} recognized, ${factCount} fact row(s), ${skipped} skipped`);
|
|
13045
|
+
if (!factCount) {
|
|
13046
|
+
return mk(
|
|
13047
|
+
`read ${sentences.length} sentence${sentences.length === 1 ? "" : "s"} from ${argText} — none grounded into a `
|
|
13048
|
+
+ "recognized fact shape (an honest, expected gap; this is an attempt, not full NLU).",
|
|
13049
|
+
{ miss: true },
|
|
13050
|
+
);
|
|
13051
|
+
}
|
|
13052
|
+
return mk(
|
|
13053
|
+
`ingested ${factCount} fact${factCount === 1 ? "" : "s"} from ${argText} `
|
|
13054
|
+
+ `(${recognizedSentences} of ${sentences.length} sentence${sentences.length === 1 ? "" : "s"} recognized).`,
|
|
13055
|
+
);
|
|
13056
|
+
}
|
|
13057
|
+
|
|
12566
13058
|
// /plan <request> — the capability router (src/domain/router/*): plan+execute a
|
|
12567
13059
|
// compound ("of the modules impacted by X, which are untested", "assess X
|
|
12568
13060
|
// and then check Y") or maintenance-goal ("what most needs a test") request
|
|
@@ -13457,7 +13949,7 @@ function vocabAntecedentFrom(last) {
|
|
|
13457
13949
|
return m[1];
|
|
13458
13950
|
}
|
|
13459
13951
|
|
|
13460
|
-
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, liveReference = false, onLiveLookup = null, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, gameConfig = null, _noSplit = false } = {}) {
|
|
13952
|
+
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, liveReference = false, onLiveLookup = null, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, gameConfig = null, uiContext = "cli", synthesisBudget = AUTO_SYNTHESIS_BUDGET, researchState = null, researchConfig = null, _noSplit = false } = {}) {
|
|
13461
13953
|
// Every game's tuning knobs (spider-fly's mass economy, guess-the-number's
|
|
13462
13954
|
// bounds, the shared plan lane's search-depth cap) — a caller's own
|
|
13463
13955
|
// gameConfig (chat-session.mjs resolves one per session from tmct.toml)
|
|
@@ -13522,7 +14014,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13522
14014
|
// the PLAN NEXT block below write planHolder.state; every other path leaves
|
|
13523
14015
|
// it untouched, and the caller re-threads whatever comes back.
|
|
13524
14016
|
const planHolder = { state: planState };
|
|
13525
|
-
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, liveReference, onLiveLookup, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache, vocabAntecedent, planHolder, gameConfig: resolvedGameConfig };
|
|
14017
|
+
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, liveReference, onLiveLookup, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache, vocabAntecedent, planHolder, gameConfig: resolvedGameConfig, uiContext, synthesisBudget };
|
|
13526
14018
|
// A DISPATCHED turn (count / slash-command / ask) becomes the new "last
|
|
13527
14019
|
// answer" that why/say-more re-renders; a conversational turn does not.
|
|
13528
14020
|
// Every dispatched turn's result passes through finish() here — the LAST
|
|
@@ -13660,6 +14152,47 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13660
14152
|
}
|
|
13661
14153
|
}
|
|
13662
14154
|
|
|
14155
|
+
// RESEARCH — "research <topic>[, limit N]" runs a Simple English Wikipedia
|
|
14156
|
+
// queue through the same ingest path a live-Wikipedia rescue uses: depth 0
|
|
14157
|
+
// now, the lead section's linked topics queued for "research next" (which
|
|
14158
|
+
// the web pages' auto-play button submits turn by turn). The explicit
|
|
14159
|
+
// request is the network consent for its own fetches — unlike the
|
|
14160
|
+
// clean-miss rescue, which fires on an ordinary question and so stays
|
|
14161
|
+
// behind /wiki on. Queue state threads turn-to-turn as researchState, the
|
|
14162
|
+
// same way planState does.
|
|
14163
|
+
{
|
|
14164
|
+
const researchHolder = { state: researchState };
|
|
14165
|
+
const resolvedResearchConfig = researchConfig ?? RESEARCH_DEFAULTS;
|
|
14166
|
+
const rTurn = await researchTurn(workingLine, {
|
|
14167
|
+
holder: researchHolder,
|
|
14168
|
+
memoryDir,
|
|
14169
|
+
lexicon,
|
|
14170
|
+
provider: getResearchProvider({ minIntervalMs: resolvedResearchConfig.minIntervalMs }),
|
|
14171
|
+
config: resolvedResearchConfig,
|
|
14172
|
+
planActive: Boolean(planHolder.state && !planHolder.state.done),
|
|
14173
|
+
pagerActive: Boolean(Array.isArray(last?.detail?.pending?.items) && last.detail.pending.items.length),
|
|
14174
|
+
notify: onLiveLookup,
|
|
14175
|
+
ingest: (key, article, tag) =>
|
|
14176
|
+
ingestReferenceArticle(memoryDir, key, article, factRowsCache, () => tag, lexicon, synthesisBudget),
|
|
14177
|
+
});
|
|
14178
|
+
if (rTurn) {
|
|
14179
|
+
note(trace, `lane: ${rTurn.note}`);
|
|
14180
|
+
note(trace, `goal: ${rTurn.goal}`);
|
|
14181
|
+
const result = plainTurn(workingLine, rTurn.text, { via: "research", miss: !!rTurn.miss, focus });
|
|
14182
|
+
result.lane = "research";
|
|
14183
|
+
const snapshot = researchSnapshot(researchHolder.state);
|
|
14184
|
+
if (snapshot) result.record.research = snapshot;
|
|
14185
|
+
const rec = withLast(result, rTurn.goal);
|
|
14186
|
+
rec.planState = planHolder.state;
|
|
14187
|
+
rec.researchState = researchHolder.state;
|
|
14188
|
+
// Present on EVERY research turn — null when the run ended or never
|
|
14189
|
+
// started — so a UI can tell "queue cleared" from "not a research
|
|
14190
|
+
// turn" (where the field is absent entirely).
|
|
14191
|
+
rec.research = snapshot;
|
|
14192
|
+
return rec;
|
|
14193
|
+
}
|
|
14194
|
+
}
|
|
14195
|
+
|
|
13663
14196
|
// Conversational layer next (greetings, thanks, help, bye, why/say-more) — these
|
|
13664
14197
|
// resolve no entity and carry their own preserved `last`. Bypasses withLast (a
|
|
13665
14198
|
// conversational turn is never finish()'d / never becomes a new `last`), so the
|