@polycode-projects/the-mechanical-code-talker 2.10.2 → 2.10.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -10
- package/bin/tmct.mjs +5 -2
- package/package.json +3 -1
- package/src/adapters/memory/core.mjs +8 -1
- package/src/domain/cli-verbs.mjs +2 -0
- package/src/domain/domain.mjs +49 -21
- package/src/domain/memory/trust.mjs +21 -2
- package/src/domain/sense-split.mjs +203 -0
- package/src/services/adventure.mjs +73 -1
- package/src/services/chat-page-viz.mjs +87 -19
- package/src/services/chat-session.mjs +12 -7
- package/src/services/chat.mjs +384 -38
- package/src/services/code-explorer-viz.mjs +16 -2
- package/src/services/extract-facts.mjs +293 -81
- package/src/services/fold.mjs +1 -1
- package/src/services/ingest-viz.mjs +388 -0
- package/src/services/ledger-viz.mjs +110 -0
- package/src/services/session-log-format.mjs +64 -0
- package/src/services/sessions.mjs +56 -22
- package/src/services/spider-fly-turn.mjs +119 -1
- package/src/services/spider-fly-viz.mjs +21 -19
- package/src/surfaces/web/chat-browser-entry.mjs +10 -6
- package/src/surfaces/web/ingest-browser-entry.mjs +126 -0
- package/src/surfaces/web/ledger-browser-entry.mjs +15 -2
- package/src/surfaces/web/memory-ask-browser.bundle.js +128 -119
- 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
|
//
|
|
@@ -55,6 +55,7 @@ import { getLiveReferenceProvider } from "../adapters/corpus/wikipedia-live.mjs"
|
|
|
55
55
|
import { CHILD_PACK_NAME, childProvenanceTag } from "../domain/child-pack.mjs";
|
|
56
56
|
import { getChildPackProvider } from "../adapters/corpus/child-pack.mjs";
|
|
57
57
|
import { dialogueActForLane } from "../domain/dialogue-acts.mjs";
|
|
58
|
+
import { subClassParents, ancestryChain, clusterSenses } from "../domain/sense-split.mjs";
|
|
58
59
|
import { relatedForTerm } from "../domain/skos-view.mjs";
|
|
59
60
|
import { adventureTurn, unclaimedAdventureOpening } from "./adventure.mjs";
|
|
60
61
|
import { spiderFlyTurn } from "./spider-fly-turn.mjs";
|
|
@@ -2555,6 +2556,10 @@ const GOAL_CONJUNCT_RE = new RegExp(
|
|
|
2555
2556
|
// three only REPORT.
|
|
2556
2557
|
const PLAN_WHAT_NEXT_RE = /^(?:what(?:'s|\s+is)?|whats)\s+the\s+next\s+move[?.!\s]*$/i;
|
|
2557
2558
|
const PLAN_MOVE_COUNT_RE = /^how\s+many\s+moves(?:\s+(?:are\s+(?:there|left)|remain(?:ing)?|left|to\s+go|in\s+the\s+plan|total))?[?.!\s]*$/i;
|
|
2559
|
+
// "what is the goal" while a goal is held — a read-back off planState, so a
|
|
2560
|
+
// mid-plan aside never falls to the child-pack lane and answers from corpus
|
|
2561
|
+
// vocabulary about the word "goal".
|
|
2562
|
+
const PLAN_GOAL_READBACK_RE = /^(?:what(?:'s|\s+is)\s+(?:the\s+|my\s+)?goal|remind\s+me\s+(?:of\s+|what\s+)?the\s+goal(?:\s+is)?|what\s+am\s+i\s+solving\s+for|what\s+goal(?:'s|\s+is)\s+(?:set|held))[?.!\s]*$/i;
|
|
2558
2563
|
// "is that really the minimum number of moves?" / "could there be a shorter
|
|
2559
2564
|
// plan than that?" — a confirmation of the planner's own optimality claim,
|
|
2560
2565
|
// not a request to count anything (without this it fell to the unrelated
|
|
@@ -5110,6 +5115,12 @@ const MODULE_ORIENT_SVO_RE = new RegExp(`^what\\s+(.+?)\\s+does${TRAILING_ADVERB
|
|
|
5110
5115
|
// terms. "what(?:'s|s|\s+is)" mirrors PERSONAL_ASSISTANT_NUDGE_RE's own
|
|
5111
5116
|
// tolerance for the bare "whats" contraction spelling, just below.
|
|
5112
5117
|
const MODULE_PURPOSE_RE = /^what(?:'s|s|\s+is)\s+(.+?)\s+(?:for|about)\??$/i;
|
|
5118
|
+
// "what is the purpose of the validate module" — the purpose-of phrasing of the
|
|
5119
|
+
// SAME module-grain overview, asking by the module's role rather than "for"/
|
|
5120
|
+
// "does". The captured object ("the validate module", "validate") is resolved
|
|
5121
|
+
// through the SAME exact-unique resolveEntity gate below; a non-module term
|
|
5122
|
+
// simply fails to resolve and the lane declines, so this never misroutes.
|
|
5123
|
+
const MODULE_PURPOSE_OF_RE = /^what(?:'s|s|\s+is)\s+the\s+(?:purpose|point|role|job|function)\s+of\s+(.+?)\??$/i;
|
|
5113
5124
|
|
|
5114
5125
|
/** A module PATH as a reader types it — "src/core/store.mjs", "app/lib/b.mjs",
|
|
5115
5126
|
* or a bare "store.mjs". Requires a slash or a source-file extension, which is
|
|
@@ -5164,7 +5175,7 @@ async function moduleOrientLane(query, { graph }) {
|
|
|
5164
5175
|
// (stripFillerWords already eats "please"/"could you" as filler; the politeness
|
|
5165
5176
|
// regex only adds the "explain [to me]" wrapper on top).
|
|
5166
5177
|
q = stripFillerWords(applyPreambleFrames(correctMisspellings(q))).replace(MODULE_ORIENT_POLITENESS_RE, "");
|
|
5167
|
-
const m = q.match(MODULE_ORIENT_RE) || q.match(MODULE_PURPOSE_RE) || q.match(MODULE_ORIENT_SVO_RE);
|
|
5178
|
+
const m = q.match(MODULE_ORIENT_RE) || q.match(MODULE_PURPOSE_OF_RE) || q.match(MODULE_PURPOSE_RE) || q.match(MODULE_ORIENT_SVO_RE);
|
|
5168
5179
|
// "what does src/core/store.mjs do" already reached the overview; the bare
|
|
5169
5180
|
// path and "what is <path>" did not, so the same module answered one
|
|
5170
5181
|
// phrasing and walled two. Both are claimed here rather than in ask.mjs,
|
|
@@ -5610,8 +5621,10 @@ export async function helpText() {
|
|
|
5610
5621
|
["/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\")"],
|
|
5611
5622
|
["/capabilities", "what /plan can plan over: the built-in graph tools plus your taught actions"],
|
|
5612
5623
|
["/syllogise <term>", "work out and remember what follows from the facts about a term (needed for chains longer than 2 hops)"],
|
|
5624
|
+
["/export <path>", "write the memory store to a file, as JSONL (the same shape `tmct memory --export` writes)"],
|
|
5625
|
+
["/ingest <path>", "read a local text file and store every fact the recognizer grounds from it (same recognizer as `tmct extract`)"],
|
|
5613
5626
|
["/narrate on|off", "verbose developer/debug mode: decision points, matched pattern, results+sources, goal per turn"],
|
|
5614
|
-
["/wiki on|off", "live Wikipedia
|
|
5627
|
+
["/wiki on|off|supplement", "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 answer"],
|
|
5615
5628
|
["/help", "this list"],
|
|
5616
5629
|
["/exit", "leave the session (also Ctrl+C / Ctrl+D)"],
|
|
5617
5630
|
];
|
|
@@ -5798,8 +5811,38 @@ const FACT_PREDICATE_PHRASES = {
|
|
|
5798
5811
|
"mgx:similarTo": "is similar to",
|
|
5799
5812
|
"mgx:relatedTo": "is related to",
|
|
5800
5813
|
"mgx:symbolOf": "is a symbol of",
|
|
5814
|
+
// A loaded adventure world's placement predicates, so a describe read-back of
|
|
5815
|
+
// a visible prop reads as English ("lamp is in the study") instead of the
|
|
5816
|
+
// mechanical -s fold garbling them ("lamp locateds in study"). The world's
|
|
5817
|
+
// SECRET/mechanics predicates (a hidden object's location, the objective
|
|
5818
|
+
// marker, the lock/open/NPC internals) are kept out of the describe lane
|
|
5819
|
+
// entirely by WORLD_INTERNAL_PREDICATES below, so they never render at all.
|
|
5820
|
+
"mgx:currently-in": "is in",
|
|
5821
|
+
"mgx:located-in": "is in",
|
|
5822
|
+
"mgx:fixed-in": "is fixed in",
|
|
5823
|
+
"mgx:stands-locked-in": "stands locked in",
|
|
5824
|
+
"mgx:works-in": "works in",
|
|
5801
5825
|
};
|
|
5802
5826
|
|
|
5827
|
+
/** The world-mechanics predicates the generic describe read-back must never
|
|
5828
|
+
* surface: a hidden object's location and the objective marker spoil the
|
|
5829
|
+
* puzzle, and the lock/container/open/NPC-schedule flags are datatype internals
|
|
5830
|
+
* the adventure's own readers answer in-game. Mirrors adventure.mjs's own
|
|
5831
|
+
* VIEW_EXCLUDED_PREDICATES — the same discipline the room-look digest uses. */
|
|
5832
|
+
const WORLD_INTERNAL_PREDICATES = new Set([
|
|
5833
|
+
"mgx:hidden-in", "mgx:is-objective", "mgx:unlocks-with",
|
|
5834
|
+
"mgx:is-npc", "mgx:acts-on-turn", "mgx:acts-toward",
|
|
5835
|
+
"mgx:is-container", "mgx:is-open",
|
|
5836
|
+
]);
|
|
5837
|
+
|
|
5838
|
+
/** The world PLACEMENT predicates carry curated phrases above so they render as
|
|
5839
|
+
* English, but they must stay OUT of the query-marker families derived from
|
|
5840
|
+
* FACT_PREDICATE_PHRASES — "what is in the study" is a members-of-class query,
|
|
5841
|
+
* not a reverse placement lookup, and "is in" is far too broad an anchor. */
|
|
5842
|
+
const WORLD_PLACEMENT_PREDICATES = new Set([
|
|
5843
|
+
"mgx:currently-in", "mgx:located-in", "mgx:fixed-in", "mgx:stands-locked-in", "mgx:works-in",
|
|
5844
|
+
]);
|
|
5845
|
+
|
|
5803
5846
|
/** The MECHANICAL fallback for a predicate this table has no curated entry
|
|
5804
5847
|
* for — specifically generalVerbTeach's minted "mgx:<lemma>" predicates
|
|
5805
5848
|
* ("mgx:eat", "mgx:drive", …) — the mechanical INVERSE of singularizeSurface's
|
|
@@ -5889,6 +5932,7 @@ function relationRoleWord(predicate) {
|
|
|
5889
5932
|
// (no curated second table) — the single-letter "a" is excluded, too short
|
|
5890
5933
|
// to anchor on without risking eating a genuine multi-word subject.
|
|
5891
5934
|
const TRAILING_PREDICATE_MARKERS = Object.entries(FACT_PREDICATE_PHRASES)
|
|
5935
|
+
.filter(([predicate]) => !WORLD_PLACEMENT_PREDICATES.has(predicate))
|
|
5892
5936
|
.map(([predicate, phrase]) => {
|
|
5893
5937
|
const m = /^(?:is|are)\s+(.+)$/i.exec(phrase);
|
|
5894
5938
|
return m ? { predicate, marker: m[1].trim().toLowerCase() } : null;
|
|
@@ -5941,6 +5985,74 @@ function renderFactLine(f) {
|
|
|
5941
5985
|
return `i learned: ${factPhrase(f)}${cite}`;
|
|
5942
5986
|
}
|
|
5943
5987
|
|
|
5988
|
+
const SENSE_CITE_RE = / \(source: [^)]*\)$/;
|
|
5989
|
+
|
|
5990
|
+
/** Append an is-a object's superclass chain to its rendered fact line, before
|
|
5991
|
+
* the citation: "rover is a kind of dog" becomes "rover is a kind of dog →
|
|
5992
|
+
* canine → mammal → animal". Only the subject-side is-a lines of the queried
|
|
5993
|
+
* term get a chain; every other line renders unchanged. */
|
|
5994
|
+
function renderFactLineWithChain(f, parents, subjectVariants) {
|
|
5995
|
+
const base = renderFactLine(f);
|
|
5996
|
+
if (!ISA_PREDICATES.has(f.predicate) || !subjectVariants.has(f.subject)) return base;
|
|
5997
|
+
const chain = ancestryChain(f.object, parents, { cap: 6 });
|
|
5998
|
+
if (chain.length <= 1) return base;
|
|
5999
|
+
const suffix = ` → ${chain.slice(1).join(" → ")}`;
|
|
6000
|
+
const cite = base.match(SENSE_CITE_RE);
|
|
6001
|
+
return cite ? base.slice(0, cite.index) + suffix + cite[0] : base + suffix;
|
|
6002
|
+
}
|
|
6003
|
+
|
|
6004
|
+
/** Render a subject-scan fact list with each is-a object's superclass chain
|
|
6005
|
+
* shown, and — when the subject's is-a objects split into distinct concepts
|
|
6006
|
+
* (a `dog` sense and a `scout` sense of one "rover") — grouped by concept.
|
|
6007
|
+
* Grouping is presentation only: every fact still renders and is cited, in
|
|
6008
|
+
* the same order, under a "<subject>, the <concept>:" heading.
|
|
6009
|
+
*
|
|
6010
|
+
* Returns `{ lines, grouped }`. `lines` is the flat, chain-enhanced rendering
|
|
6011
|
+
* (indented by `indent`) the caller uses when senses do not split. `grouped`
|
|
6012
|
+
* is a ready `{ text, replace, pending? }` answer when they do, else null. */
|
|
6013
|
+
function senseSplitFactList(hits, rows, subjectVariants, { indent = "" } = {}) {
|
|
6014
|
+
const subClassEdges = rows.filter((f) => f.predicate === SUBCLASS_PREDICATE).map((f) => [f.subject, f.object]);
|
|
6015
|
+
const parents = subClassParents(subClassEdges);
|
|
6016
|
+
const lines = hits.map((f) => `${indent}${renderFactLineWithChain(f, parents, subjectVariants)}`);
|
|
6017
|
+
|
|
6018
|
+
const isaSubjectFacts = hits.filter((f) => ISA_PREDICATES.has(f.predicate) && subjectVariants.has(f.subject));
|
|
6019
|
+
const isaObjects = [...new Set(isaSubjectFacts.map((f) => f.object))];
|
|
6020
|
+
if (isaObjects.length < 2) return { lines, grouped: null };
|
|
6021
|
+
const disjointEdges = rows.filter((f) => f.predicate === "owl:disjointWith").map((f) => [f.subject, f.object]);
|
|
6022
|
+
const { split, clusters } = clusterSenses(isaObjects, { parents, disjointEdges });
|
|
6023
|
+
if (!split) return { lines, grouped: null };
|
|
6024
|
+
|
|
6025
|
+
const subject = isaSubjectFacts[0].subject;
|
|
6026
|
+
const clusterOf = new Map();
|
|
6027
|
+
for (const c of clusters) for (const o of c.objects) clusterOf.set(o, c);
|
|
6028
|
+
const otherHits = hits.filter((f) => !(ISA_PREDICATES.has(f.predicate) && subjectVariants.has(f.subject)));
|
|
6029
|
+
|
|
6030
|
+
const blocks = [];
|
|
6031
|
+
const restItems = [];
|
|
6032
|
+
let shownCount = 0;
|
|
6033
|
+
const addLine = (f) => {
|
|
6034
|
+
const rendered = renderFactLineWithChain(f, parents, subjectVariants);
|
|
6035
|
+
if (shownCount < FACT_ANSWER_CAP) { shownCount += 1; return `${indent}${rendered}`; }
|
|
6036
|
+
restItems.push(rendered);
|
|
6037
|
+
return null;
|
|
6038
|
+
};
|
|
6039
|
+
for (const c of clusters) {
|
|
6040
|
+
const clusterLines = isaSubjectFacts.filter((f) => clusterOf.get(f.object) === c).map(addLine).filter(Boolean);
|
|
6041
|
+
if (clusterLines.length) blocks.push(`${indent}${subject}, the ${c.label}:\n${clusterLines.join("\n")}`);
|
|
6042
|
+
}
|
|
6043
|
+
if (otherHits.length) {
|
|
6044
|
+
const otherLines = otherHits.map(addLine).filter(Boolean);
|
|
6045
|
+
if (otherLines.length) blocks.push(`${indent}also about ${subject}:\n${otherLines.join("\n")}`);
|
|
6046
|
+
}
|
|
6047
|
+
const extra = restItems.length ? `\n${indent}…and ${restItems.length} more — say 'more' to see them.` : "";
|
|
6048
|
+
const grouped = {
|
|
6049
|
+
text: blocks.join("\n") + extra,
|
|
6050
|
+
replace: true,
|
|
6051
|
+
...(restItems.length ? { pending: { items: restItems, noun: "facts" } } : {}),
|
|
6052
|
+
};
|
|
6053
|
+
return { lines, grouped };
|
|
6054
|
+
}
|
|
6055
|
+
|
|
5944
6056
|
/** "a"/"an" for a term, through the SAME grammar-rules.toml "article" rule and
|
|
5945
6057
|
* finish.mjs's beginsWithVowelSound every other agreement site in this file
|
|
5946
6058
|
* uses — never a hardcoded "a", which is ungrammatical for a vowel-initial
|
|
@@ -6654,7 +6766,7 @@ const REVERSE_PREDICATE_EXCLUDE = new Set([
|
|
|
6654
6766
|
"mgx:ownedBy", "owl:disjointWith", "mgx:hasProperty", "mgx:receivesAction",
|
|
6655
6767
|
]);
|
|
6656
6768
|
const REVERSE_PREDICATE_MARKERS = Object.entries(FACT_PREDICATE_PHRASES)
|
|
6657
|
-
.filter(([predicate]) => !REVERSE_PREDICATE_EXCLUDE.has(predicate))
|
|
6769
|
+
.filter(([predicate]) => !REVERSE_PREDICATE_EXCLUDE.has(predicate) && !WORLD_PLACEMENT_PREDICATES.has(predicate))
|
|
6658
6770
|
.map(([predicate, phrase]) => ({
|
|
6659
6771
|
predicate,
|
|
6660
6772
|
re: new RegExp(`^what\\s+${escapeRegex(phrase)}\\s+(.+?)[?.!\\s]*$`, "i"),
|
|
@@ -6680,7 +6792,7 @@ const FORWARD_YESNO_EXCLUDE = new Set([
|
|
|
6680
6792
|
"mgx:ownedBy",
|
|
6681
6793
|
]);
|
|
6682
6794
|
const FORWARD_YESNO_MARKERS = Object.entries(FACT_PREDICATE_PHRASES)
|
|
6683
|
-
.filter(([predicate]) => !FORWARD_YESNO_EXCLUDE.has(predicate))
|
|
6795
|
+
.filter(([predicate]) => !FORWARD_YESNO_EXCLUDE.has(predicate) && !WORLD_PLACEMENT_PREDICATES.has(predicate))
|
|
6684
6796
|
.map(([predicate, phrase]) => {
|
|
6685
6797
|
let re;
|
|
6686
6798
|
if (phrase === "can be") {
|
|
@@ -7003,8 +7115,11 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7003
7115
|
const variants = factTermVariants(normFactTerm, subject);
|
|
7004
7116
|
// factRows (trust+sourceIds-bearing), not the plain memoryFacts shape — the
|
|
7005
7117
|
// bias-weighted ranking below needs each hit's sourceIds to resolve which
|
|
7006
|
-
// bundle it came from (memory/bias.mjs's biasForRow).
|
|
7007
|
-
|
|
7118
|
+
// bundle it came from (memory/bias.mjs's biasForRow). A live world's secret
|
|
7119
|
+
// and mechanics predicates are dropped so "what is the letter" never reads
|
|
7120
|
+
// back where it's hidden or that it's the objective (WORLD_INTERNAL_PREDICATES).
|
|
7121
|
+
const subjectHits = (await factRows(memoryDir, cache))
|
|
7122
|
+
.filter((f) => variants.has(f.subject) && !WORLD_INTERNAL_PREDICATES.has(f.predicate));
|
|
7008
7123
|
let hits = predicate ? subjectHits.filter((f) => f.predicate === predicate) : subjectHits;
|
|
7009
7124
|
if (!hits.length) {
|
|
7010
7125
|
// The subject itself is known, but not under this specific relation —
|
|
@@ -7037,7 +7152,8 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7037
7152
|
// "disclosed, never dropped" contract). Unconfigured/tied bias degrades to
|
|
7038
7153
|
// trust-desc, byte-identical to before this feature existed.
|
|
7039
7154
|
hits = rankByBiasThenTrust(hits, biasByBundle);
|
|
7040
|
-
const lines = hits
|
|
7155
|
+
const { lines, grouped } = senseSplitFactList(hits, await factRows(memoryDir, cache), variants);
|
|
7156
|
+
if (grouped) return { ...grouped, replace: miss };
|
|
7041
7157
|
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
7042
7158
|
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
7043
7159
|
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
@@ -7533,6 +7649,12 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7533
7649
|
hits = rows.filter((f) => overlaps(f.subject) || overlaps(f.object));
|
|
7534
7650
|
}
|
|
7535
7651
|
}
|
|
7652
|
+
// A live adventure world's mechanics never leak through the describe lane:
|
|
7653
|
+
// "what is the letter" must not read back where it's hidden or that it's the
|
|
7654
|
+
// objective, and those datatype internals render as garbled non-English
|
|
7655
|
+
// besides. The adventure's own where/openness readers answer the legitimate
|
|
7656
|
+
// in-game questions from the world fold.
|
|
7657
|
+
hits = hits.filter((f) => !WORLD_INTERNAL_PREDICATES.has(f.predicate));
|
|
7536
7658
|
// A genuinely empty result here is a real miss: "what do you know about
|
|
7537
7659
|
// the last commit" needs a TEACH-OFFER, not a bare wall — added as a LATE
|
|
7538
7660
|
// runTurn-level addition, below, alongside the sibling "what is X" offer,
|
|
@@ -7580,12 +7702,13 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7580
7702
|
// renders (Part 6's "disclosed, never dropped" contract); literalHit/
|
|
7581
7703
|
// viaSubtype above already resolved off the pre-rank order.
|
|
7582
7704
|
hits = rankByBiasThenTrust(hits, biasByBundle);
|
|
7583
|
-
const
|
|
7705
|
+
const header = `${hits.length} remembered fact${hits.length === 1 ? "" : "s"} about ${term}`
|
|
7706
|
+
+ `${viaSubtype ? " (including its known subtypes)" : ""}:`;
|
|
7707
|
+
const { lines, grouped } = senseSplitFactList(hits, rows, variants, { indent: " " });
|
|
7708
|
+
if (grouped) return { ...grouped, text: `${header}\n${grouped.text}` };
|
|
7584
7709
|
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
7585
7710
|
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
7586
7711
|
const extra = rest.length ? `\n …and ${rest.length} more — say 'more' to see them.` : "";
|
|
7587
|
-
const header = `${hits.length} remembered fact${hits.length === 1 ? "" : "s"} about ${term}`
|
|
7588
|
-
+ `${viaSubtype ? " (including its known subtypes)" : ""}:`;
|
|
7589
7712
|
return { text: `${header}\n${shown.join("\n")}${extra}`, replace: true, ...(rest.length ? { pending: { items: rest.map((l) => l.trim()), noun: "facts" } } : {}) };
|
|
7590
7713
|
}
|
|
7591
7714
|
return null;
|
|
@@ -9819,11 +9942,20 @@ async function cleanMissPackKey(term, { graph, memoryDir, lexicon, cache }) {
|
|
|
9819
9942
|
if (!key) return null;
|
|
9820
9943
|
if (await resolveEntity(graph, term)) return null;
|
|
9821
9944
|
let normFactTerm;
|
|
9822
|
-
|
|
9945
|
+
let loadMemory;
|
|
9946
|
+
let readRuleRows;
|
|
9947
|
+
try { ({ normFactTerm, loadMemory, readRuleRows } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
9823
9948
|
const variants = factTermVariants(normFactTerm, term);
|
|
9824
9949
|
variants.add(key);
|
|
9825
9950
|
const rows = await factRows(memoryDir, cache);
|
|
9826
9951
|
if (rows.some((f) => variants.has(f.subject) || variants.has(f.object))) return null;
|
|
9952
|
+
// A taught RULE that owns this term outranks any pack load: surfacing
|
|
9953
|
+
// unrelated conceptnet content over the user's own taught concept is worse
|
|
9954
|
+
// than the honest miss the decline leaves standing.
|
|
9955
|
+
try {
|
|
9956
|
+
const ruleNames = readRuleRows(await loadMemory(memoryDir)).map((r) => normFactTerm(r.name)).filter(Boolean);
|
|
9957
|
+
if (ruleNames.some((n) => variants.has(n))) return null;
|
|
9958
|
+
} catch { /* tolerated — the fact gate above already ran */ }
|
|
9827
9959
|
return key;
|
|
9828
9960
|
}
|
|
9829
9961
|
|
|
@@ -9891,22 +10023,87 @@ async function childPackFactsForKey(key, { memoryDir, env, cache }) {
|
|
|
9891
10023
|
})));
|
|
9892
10024
|
} catch { return null; }
|
|
9893
10025
|
if (cache) cache.rows = null;
|
|
10026
|
+
await synthesiseAroundTerm(memoryDir, key, cache);
|
|
9894
10027
|
return { key, count: row.facts.length };
|
|
9895
10028
|
}
|
|
9896
10029
|
|
|
9897
|
-
/** Store the article's first-sentence isa
|
|
9898
|
-
*
|
|
9899
|
-
*
|
|
9900
|
-
|
|
9901
|
-
|
|
10030
|
+
/** Store every triple the article's summary grounds — its first-sentence isa
|
|
10031
|
+
* plus each candidate the optimistic tier reads from the rest of the summary —
|
|
10032
|
+
* all under the article's own provenance, so a learned load becomes durable
|
|
10033
|
+
* knowledge rather than a single isa fact. Runs AFTER the cited answer composed
|
|
10034
|
+
* and is failure-tolerated: the answer stands whether or not the facts land.
|
|
10035
|
+
* The optimistic tier is pure (no recognizer re-entry), so this stays cheap on
|
|
10036
|
+
* the chat turn. Returns the count stored. */
|
|
10037
|
+
async function ingestReferenceArticle(memoryDir, key, article, cache, tagFor = referenceProvenanceTag, lexicon = null) {
|
|
10038
|
+
if (!article) return 0;
|
|
10039
|
+
const provenance = tagFor(article);
|
|
10040
|
+
const facts = [];
|
|
10041
|
+
const seen = new Set();
|
|
10042
|
+
const add = (subject, predicate, object) => {
|
|
10043
|
+
const id = `${subject}\0${predicate}\0${object}`;
|
|
10044
|
+
if (subject && object && subject !== object && !seen.has(id)) { seen.add(id); facts.push({ subject, predicate, object, provenance }); }
|
|
10045
|
+
};
|
|
10046
|
+
if (article.isa) add(key, "rdfs:subClassOf", article.isa);
|
|
9902
10047
|
try {
|
|
9903
|
-
const {
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
10048
|
+
const { optimisticTriples } = await import("./extract-facts.mjs");
|
|
10049
|
+
for (const sentence of splitSentences(article.summary || article.text || "")) {
|
|
10050
|
+
for (const t of optimisticTriples(sentence, { lexicon: lexicon ?? undefined })) add(t.subject, t.predicate, t.object);
|
|
10051
|
+
}
|
|
10052
|
+
} catch { /* the isa alone still lands below */ }
|
|
10053
|
+
if (!facts.length) return 0;
|
|
10054
|
+
try {
|
|
10055
|
+
const { appendFacts } = await import("../adapters/memory/core.mjs");
|
|
10056
|
+
await appendFacts(memoryDir, facts);
|
|
9908
10057
|
if (cache) cache.rows = null;
|
|
9909
|
-
} catch {
|
|
10058
|
+
} catch { return 0; }
|
|
10059
|
+
await synthesiseAroundTerm(memoryDir, key, cache);
|
|
10060
|
+
return facts.length;
|
|
10061
|
+
}
|
|
10062
|
+
|
|
10063
|
+
// A learn-on-miss load stores a handful of new facts; the auto-synthesis pass
|
|
10064
|
+
// that connects them to the rest of the store is deliberately small — a low
|
|
10065
|
+
// budget, focus expanded through the loaded term — so it stays a per-ingest
|
|
10066
|
+
// materialisation, not the whole-store maintenance job /syllogise runs.
|
|
10067
|
+
const AUTO_SYNTHESIS_BUDGET = 12;
|
|
10068
|
+
|
|
10069
|
+
/** After a learn-on-miss load stored new facts about `term`, run a bounded,
|
|
10070
|
+
* focus-scoped forward-chaining pass so the new facts connect to what's
|
|
10071
|
+
* already remembered — the auto sibling of the /syllogise command. Derived
|
|
10072
|
+
* facts carry entailed:* provenance at their discounted trust and are
|
|
10073
|
+
* retractable. Failure-tolerated: a synthesis miss never disturbs the answer
|
|
10074
|
+
* the load already composed. Returns the count derived. */
|
|
10075
|
+
async function synthesiseAroundTerm(memoryDir, term, cache) {
|
|
10076
|
+
if (!memoryDir || !term) return 0;
|
|
10077
|
+
try {
|
|
10078
|
+
const { syllogise } = await import("../domain/syllogise.mjs");
|
|
10079
|
+
const { loadMemory, readFactRows, appendFacts, normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
10080
|
+
const res = await syllogise(memoryDir, {
|
|
10081
|
+
focus: [...factTermVariants(normFactTerm, term)],
|
|
10082
|
+
expandFocus: true,
|
|
10083
|
+
budget: AUTO_SYNTHESIS_BUDGET,
|
|
10084
|
+
store: { loadMemory, readFactRows, appendFacts },
|
|
10085
|
+
});
|
|
10086
|
+
if (res?.count && cache) cache.rows = null;
|
|
10087
|
+
return res?.count || 0;
|
|
10088
|
+
} catch { return 0; }
|
|
10089
|
+
}
|
|
10090
|
+
|
|
10091
|
+
/** The term an explicit "ask Wikipedia" phrasing names — "what does wikipedia
|
|
10092
|
+
* say about X", "ask wikipedia about X", "X on wikipedia" — or null when the
|
|
10093
|
+
* line isn't such a request. Unlike the clean-miss gate, this fires even when
|
|
10094
|
+
* local facts could answer: the user asked Wikipedia specifically. */
|
|
10095
|
+
const WIKIPEDIA_ASK_RES = [
|
|
10096
|
+
/^what\s+(?:does|do)\s+wikipedia\s+say\s+(?:about\s+)?(.+?)[?.!\s]*$/i,
|
|
10097
|
+
/^ask\s+wikipedia\s+(?:about\s+)?(.+?)[?.!\s]*$/i,
|
|
10098
|
+
/^(?: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,
|
|
10099
|
+
];
|
|
10100
|
+
function wikipediaAskTerm(query) {
|
|
10101
|
+
const q = String(query || "").trim();
|
|
10102
|
+
for (const re of WIKIPEDIA_ASK_RES) {
|
|
10103
|
+
const m = q.match(re);
|
|
10104
|
+
if (m && m[1] && m[1].trim()) return m[1].trim().replace(/^(?:an?|the)\s+/i, "");
|
|
10105
|
+
}
|
|
10106
|
+
return null;
|
|
9910
10107
|
}
|
|
9911
10108
|
|
|
9912
10109
|
/** The concept term a vague "what is a X" / "tell me about X" / "what does X mean" /
|
|
@@ -10100,6 +10297,11 @@ async function describeWrapperAnswer(query, { config, source, focus, graph, tel
|
|
|
10100
10297
|
// captured term, same class of gap stripTrailingDiscourseTag (ask-vocab.mjs)
|
|
10101
10298
|
// already fixes for the meta-whatis vocab lane.
|
|
10102
10299
|
term = stripTrailingDiscourseTag(term);
|
|
10300
|
+
// "tell me about the router thing" / "the logging stuff" — a vague filler
|
|
10301
|
+
// noun wrapped around a real term. Strip it so the describe lane resolves
|
|
10302
|
+
// the term itself; an unresolvable remainder still declines to the ordinary
|
|
10303
|
+
// miss below, so this only ever widens what grounds, never misroutes.
|
|
10304
|
+
term = term.replace(/\s+(?:thing|things|thingy|stuff)$/i, "").trim() || term;
|
|
10103
10305
|
if (DESCRIBE_PRONOUN_RE.test(term)) {
|
|
10104
10306
|
if (!focus?.label) return null; // no standing focus to resolve against — honest decline
|
|
10105
10307
|
term = focus.label;
|
|
@@ -10684,7 +10886,7 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", ga
|
|
|
10684
10886
|
if (wantsLegal) {
|
|
10685
10887
|
let moves;
|
|
10686
10888
|
try {
|
|
10687
|
-
moves = movesFromRules(state, domain);
|
|
10889
|
+
moves = movesFromRules(state, domain, { scope: "taught" });
|
|
10688
10890
|
} catch (err) {
|
|
10689
10891
|
if (err instanceof PlanBudgetError) {
|
|
10690
10892
|
return { text: `too many possible moves to enumerate here (${err.message}) — narrow the classes involved.`, via: "plan", deduced: "list the legal moves (budget exceeded)", note: "plan lane — budget decline" };
|
|
@@ -10774,7 +10976,7 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", ga
|
|
|
10774
10976
|
}
|
|
10775
10977
|
let isGoal;
|
|
10776
10978
|
try {
|
|
10777
|
-
isGoal = compileGoal(goals, domain);
|
|
10979
|
+
isGoal = compileGoal(goals, domain, { scope: "taught" });
|
|
10778
10980
|
} catch (err) {
|
|
10779
10981
|
return { text: `I can't compile that goal: ${err?.message ?? err}`, via: "plan", deduced: "plan a move sequence (uncompilable goal)", note: "plan lane — goal compile decline" };
|
|
10780
10982
|
}
|
|
@@ -10782,7 +10984,7 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", ga
|
|
|
10782
10984
|
const maxDepth = gameConfig?.planning?.maxDepth ?? DEFAULT_GAME_CONFIG.planning.maxDepth;
|
|
10783
10985
|
let found;
|
|
10784
10986
|
try {
|
|
10785
|
-
found = findActionPath(state, isGoal, (s) => movesFromRules(s, domain), { maxDepth, stateKey: stateKeyFor });
|
|
10987
|
+
found = findActionPath(state, isGoal, (s) => movesFromRules(s, domain, { scope: "taught" }), { maxDepth, stateKey: stateKeyFor });
|
|
10786
10988
|
} catch (err) {
|
|
10787
10989
|
if (err instanceof PlanBudgetError) {
|
|
10788
10990
|
return { text: `the search space is too large (${err.message}) — narrow the classes involved.`, via: "plan", deduced: "plan a move sequence (budget exceeded)", note: "plan lane — budget decline" };
|
|
@@ -10883,7 +11085,7 @@ async function executePlanStep(planHolder, { memoryDir, sessionId = "" }) {
|
|
|
10883
11085
|
const factRows = readFactRows(payload);
|
|
10884
11086
|
const domain = compileDomain(factRows, readRuleRows(payload));
|
|
10885
11087
|
const finalState = stateFromFacts(factRows, domain);
|
|
10886
|
-
const holds = compileGoal(ps.goals, domain)(finalState);
|
|
11088
|
+
const holds = compileGoal(ps.goals, domain, { scope: "taught" })(finalState);
|
|
10887
11089
|
planHolder.state = { ...planHolder.state, done: true };
|
|
10888
11090
|
return {
|
|
10889
11091
|
text: holds
|
|
@@ -10940,6 +11142,18 @@ async function planFollowUpAnswer(query, { memoryDir, planHolder, pendingPager =
|
|
|
10940
11142
|
};
|
|
10941
11143
|
}
|
|
10942
11144
|
|
|
11145
|
+
if (PLAN_GOAL_READBACK_RE.test(q)) {
|
|
11146
|
+
if (!ps || !(ps.goalTexts?.length || ps.goals?.length)) return null;
|
|
11147
|
+
const goalText = ps.goalTexts?.length ? ps.goalTexts.join("; ") : "the goal you set";
|
|
11148
|
+
const status = activePlan
|
|
11149
|
+
? ` A plan is ready — ${ps.actions.length} move${ps.actions.length === 1 ? "" : "s"}; say "next" to step through it.`
|
|
11150
|
+
: ' Say "solve it" when the board is taught.';
|
|
11151
|
+
return {
|
|
11152
|
+
text: `the goal is that ${goalText}.${status}`,
|
|
11153
|
+
deduced: "read back the held goal",
|
|
11154
|
+
note: "PLAN FOLLOW-UP — goal read-back from the held planState",
|
|
11155
|
+
};
|
|
11156
|
+
}
|
|
10943
11157
|
if (PLAN_WHAT_NEXT_RE.test(q)) {
|
|
10944
11158
|
if (!activePlan) return null;
|
|
10945
11159
|
if (ps.done || ps.cursor >= ps.actions.length) {
|
|
@@ -11109,6 +11323,31 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11109
11323
|
});
|
|
11110
11324
|
}
|
|
11111
11325
|
}
|
|
11326
|
+
// EXPLICIT WIKIPEDIA ASK — "what does wikipedia say about X" / "ask wikipedia
|
|
11327
|
+
// about X" / "X on wikipedia". Unlike the clean-miss packs, this fires even
|
|
11328
|
+
// when local facts could answer: the user named the source. It still honours
|
|
11329
|
+
// the network opt-in (a live lookup is a network request), so with the toggle
|
|
11330
|
+
// off it points at /wiki on rather than reaching the network.
|
|
11331
|
+
{
|
|
11332
|
+
const wikiTerm = wikipediaAskTerm(query);
|
|
11333
|
+
if (wikiTerm) {
|
|
11334
|
+
note(trace, "goal: read what Wikipedia says about a named term (explicit source request)");
|
|
11335
|
+
if (!liveReference) {
|
|
11336
|
+
note(trace, "lane: WIKIPEDIA ASK — the explicit request needs the network opt-in; live Wikipedia is off");
|
|
11337
|
+
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 });
|
|
11338
|
+
}
|
|
11339
|
+
let liveKey = null;
|
|
11340
|
+
try { liveKey = cleanMissLiveTerm(wikiTerm, lexicon ?? undefined); } catch { liveKey = null; }
|
|
11341
|
+
const live = liveKey ? await liveReferenceAnswerForKey(liveKey, onLiveLookup) : null;
|
|
11342
|
+
if (live) {
|
|
11343
|
+
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon);
|
|
11344
|
+
note(trace, `lane: WIKIPEDIA ASK — answered from a live en.wikipedia.org lookup, cited (article "${live.article.title}", revid ${live.article.revid})`);
|
|
11345
|
+
return plainTurn(query, live.text, { via: "reference", miss: false, focus });
|
|
11346
|
+
}
|
|
11347
|
+
note(trace, "lane: WIKIPEDIA ASK — no matching live article (no title, timeout, throttle, or drift-guard reject)");
|
|
11348
|
+
return plainTurn(query, `I couldn't reach a matching Wikipedia article for "${wikiTerm}" just now.`, { via: "miss", miss: true, focus });
|
|
11349
|
+
}
|
|
11350
|
+
}
|
|
11112
11351
|
// COLLECTIVE PLURAL SUBJECT — see COLLECTIVE_FORWARD_RE. Members are the
|
|
11113
11352
|
// modules whose path carries the plural as a component; two or more make it
|
|
11114
11353
|
// a group question, answered as the disclosed union over every member. One
|
|
@@ -11720,15 +11959,15 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11720
11959
|
const coldPronounDecline = focus?.label ? null : coldPronounDeclineText(query);
|
|
11721
11960
|
if (bareMetaHit?.reference) {
|
|
11722
11961
|
// The bare-form reference hit mirrors (4h): the cited answer replaces the
|
|
11723
|
-
// miss, the turn is no longer recorded as one, and the article's
|
|
11724
|
-
// stored after the answer composes.
|
|
11962
|
+
// miss, the turn is no longer recorded as one, and the article's grounded
|
|
11963
|
+
// triples are stored after the answer composes.
|
|
11725
11964
|
answer = bareMetaHit.text;
|
|
11726
11965
|
via = "reference";
|
|
11727
11966
|
recordMiss = false;
|
|
11728
11967
|
handled = true;
|
|
11729
11968
|
note(trace, "lane: (2b) REFERENCE PACK — a bare \"what is X\" clean miss answered from the shipped reference pack, cited");
|
|
11730
11969
|
note(trace, `source: reference pack ${REFERENCE_PACK_NAME} — article "${bareMetaHit.reference.article.title}" (revid ${bareMetaHit.reference.article.revid})`);
|
|
11731
|
-
await
|
|
11970
|
+
await ingestReferenceArticle(memoryDir, bareMetaHit.reference.key, bareMetaHit.reference.article, cache, referenceProvenanceTag, lexicon);
|
|
11732
11971
|
} else if (bareMetaHit?.live) {
|
|
11733
11972
|
// The bare-form LIVE hit settles the same way, under live provenance.
|
|
11734
11973
|
answer = bareMetaHit.text;
|
|
@@ -11737,7 +11976,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11737
11976
|
handled = true;
|
|
11738
11977
|
note(trace, "lane: (2b) LIVE WIKIPEDIA — a bare \"what is X\" clean miss answered from a live en.wikipedia.org lookup (opt-in), cited");
|
|
11739
11978
|
note(trace, `source: live reference ${LIVE_PACK_NAME} — article "${bareMetaHit.live.article.title}" (revid ${bareMetaHit.live.article.revid})`);
|
|
11740
|
-
await
|
|
11979
|
+
await ingestReferenceArticle(memoryDir, bareMetaHit.live.key, bareMetaHit.live.article, cache, liveProvenanceTag, lexicon);
|
|
11741
11980
|
} else if (bareMetaHit) {
|
|
11742
11981
|
answer = bareMetaHit.replace ? bareMetaHit.text : `${answer}\n${bareMetaHit.text}`;
|
|
11743
11982
|
// Same discipline as lane (3): a fact-lane return flagged `miss` is an
|
|
@@ -12144,7 +12383,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12144
12383
|
recordMiss = false;
|
|
12145
12384
|
note(trace, "lane: (4h) REFERENCE PACK — a clean miss on a lexicon term answered from the shipped reference pack, cited");
|
|
12146
12385
|
note(trace, `source: reference pack ${REFERENCE_PACK_NAME} — article "${ref.article.title}" (revid ${ref.article.revid})`);
|
|
12147
|
-
await
|
|
12386
|
+
await ingestReferenceArticle(memoryDir, ref.key, ref.article, cache, referenceProvenanceTag, lexicon);
|
|
12148
12387
|
}
|
|
12149
12388
|
}
|
|
12150
12389
|
// The live Wikipedia supplement (opt-in), strictly AFTER both shipped
|
|
@@ -12160,7 +12399,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12160
12399
|
recordMiss = false;
|
|
12161
12400
|
note(trace, "lane: (4h) LIVE WIKIPEDIA — a clean miss answered from a live en.wikipedia.org lookup (opt-in), cited");
|
|
12162
12401
|
note(trace, `source: live reference ${LIVE_PACK_NAME} — article "${live.article.title}" (revid ${live.article.revid})`);
|
|
12163
|
-
await
|
|
12402
|
+
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon);
|
|
12164
12403
|
}
|
|
12165
12404
|
}
|
|
12166
12405
|
}
|
|
@@ -12258,6 +12497,23 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12258
12497
|
answer = `hypothetically, if ${counterfactualSubject[1].trim()} were removed: ${answer}`;
|
|
12259
12498
|
note(trace, `intermediate: COUNTERFACTUAL_RE matched — compiled to a real traversal, wrapped as hypothetical ("${counterfactualSubject[1].trim()}" removed)`);
|
|
12260
12499
|
}
|
|
12500
|
+
// LIVE SUPPLEMENT (/wiki supplement): a grounded answer also carries what
|
|
12501
|
+
// Wikipedia says about its subject — corroboration, not rescue. Scoped to a
|
|
12502
|
+
// clean vocabulary subject (a "what is X" / "tell me about X" term), never a
|
|
12503
|
+
// code-graph entity, and never doubled onto an answer that already IS a
|
|
12504
|
+
// Wikipedia read-out. Failure-tolerated, and network-gated by the same toggle
|
|
12505
|
+
// (the "supplement" value is truthy, so the rescue lanes above already ran).
|
|
12506
|
+
if (liveReference === "supplement" && !recordMiss && via !== "reference") {
|
|
12507
|
+
const supplementTerm = metaTermOf(query, envelope) || vagueTouchTermOf(query);
|
|
12508
|
+
let liveKey = null;
|
|
12509
|
+
try { liveKey = supplementTerm ? cleanMissLiveTerm(supplementTerm, lexicon ?? undefined) : null; } catch { liveKey = null; }
|
|
12510
|
+
const live = liveKey ? await liveReferenceAnswerForKey(liveKey, onLiveLookup) : null;
|
|
12511
|
+
if (live) {
|
|
12512
|
+
answer = `${answer}\nWikipedia adds: ${live.text}`;
|
|
12513
|
+
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon);
|
|
12514
|
+
note(trace, `intermediate: LIVE SUPPLEMENT — appended a cited en.wikipedia.org read-out for "${supplementTerm}" (supplement mode)`);
|
|
12515
|
+
}
|
|
12516
|
+
}
|
|
12261
12517
|
// The concept force answers WITH real example instances — those are the entities the
|
|
12262
12518
|
// turn "asked about" (the SchemaClass meta-node is documentation, not a code entity),
|
|
12263
12519
|
// so record + expand them, not the schema match.
|
|
@@ -12347,6 +12603,8 @@ const GOAL_BY_COMMAND = {
|
|
|
12347
12603
|
capabilities: "see what /plan can plan over — built-in query tools and taught actions",
|
|
12348
12604
|
syllogise: "materialize the entailed facts that follow from what's remembered about one term",
|
|
12349
12605
|
wiki: "toggle the live Wikipedia supplement for questions nothing local can answer",
|
|
12606
|
+
export: "write the memory store to a file, in the standard JSONL shape",
|
|
12607
|
+
ingest: "read a local text file and store every fact the recognizer grounds from it",
|
|
12350
12608
|
};
|
|
12351
12609
|
|
|
12352
12610
|
/** A slash-command → the mapped tool (or the /help, /focus, /narrate, unknown
|
|
@@ -12395,12 +12653,14 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
12395
12653
|
// state). A bare "/wiki" reports the CURRENT state and changes nothing.
|
|
12396
12654
|
if (name === "wiki") {
|
|
12397
12655
|
const arg = argText.toLowerCase();
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
|
|
12656
|
+
const stateWord = (v) => (v === "supplement" ? "supplement" : v ? "on" : "off");
|
|
12657
|
+
if (arg !== "on" && arg !== "off" && arg !== "supplement") {
|
|
12658
|
+
return mk(`live Wikipedia supplement is ${stateWord(liveReference)} — /wiki on, /wiki off, or /wiki supplement. `
|
|
12659
|
+
+ "When on, a question I can't answer also tries en.wikipedia.org (network); "
|
|
12660
|
+
+ "supplement adds a cited Wikipedia read-out under every grounded answer too.");
|
|
12401
12661
|
}
|
|
12402
|
-
const next = arg === "on";
|
|
12403
|
-
return mk(`live Wikipedia supplement ${next
|
|
12662
|
+
const next = arg === "supplement" ? "supplement" : arg === "on";
|
|
12663
|
+
return mk(`live Wikipedia supplement ${stateWord(next)}.`, { liveReferenceNext: next });
|
|
12404
12664
|
}
|
|
12405
12665
|
|
|
12406
12666
|
// /memory [verbose] — what tmct remembers, as text (the same renderer
|
|
@@ -12507,6 +12767,92 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
12507
12767
|
}
|
|
12508
12768
|
}
|
|
12509
12769
|
|
|
12770
|
+
// /export <path> — write the whole memory store as JSONL, the SAME shape
|
|
12771
|
+
// `tmct memory --export` and the tmct_export cold tool already emit
|
|
12772
|
+
// (serializeFactsJsonl, export-jsonl.mjs) — so a chat session can take its
|
|
12773
|
+
// facts with it without dropping to a shell.
|
|
12774
|
+
if (name === "export") {
|
|
12775
|
+
note(trace, "goal: write the memory store to a file, in the standard JSONL shape");
|
|
12776
|
+
if (!memoryDir) return mk("no memory store here — /export works inside a repo session.", { miss: true });
|
|
12777
|
+
if (!argText) return mk("/export needs a path, e.g. `/export facts.jsonl`.", { miss: true });
|
|
12778
|
+
try {
|
|
12779
|
+
const { loadMemory } = await import("../adapters/memory/core.mjs");
|
|
12780
|
+
const { serializeFactsJsonl } = await import("../adapters/memory/export-jsonl.mjs");
|
|
12781
|
+
const { writeFile } = await import("node:fs/promises");
|
|
12782
|
+
const { resolve } = await import("node:path");
|
|
12783
|
+
const jsonl = serializeFactsJsonl(await loadMemory(memoryDir));
|
|
12784
|
+
const out = resolve(process.cwd(), argText);
|
|
12785
|
+
await writeFile(out, jsonl, "utf8");
|
|
12786
|
+
const count = jsonl ? jsonl.trimEnd().split("\n").length : 0;
|
|
12787
|
+
note(trace, `result: wrote ${count} fact(s) to ${out}`);
|
|
12788
|
+
return mk(`wrote ${count} fact${count === 1 ? "" : "s"} to ${argText}.`);
|
|
12789
|
+
} catch (e) {
|
|
12790
|
+
return mk(String(e?.message || e), { miss: true }); // a broken store/path reads as its own clean error
|
|
12791
|
+
}
|
|
12792
|
+
}
|
|
12793
|
+
|
|
12794
|
+
// /ingest <path> — the TUI/CLI counterpart to `tmct extract`: read a local
|
|
12795
|
+
// text file, run each sentence through the SAME recognizer the teach lane
|
|
12796
|
+
// already grounds sentences with (runTurn itself — the identical per-
|
|
12797
|
+
// sentence pass extract-facts.mjs's own CLI wrapper runs), and store every
|
|
12798
|
+
// grounded fact into THIS session's own memory store. Deliberately does
|
|
12799
|
+
// NOT call extract-facts.mjs's own main(): that entry point resolves its
|
|
12800
|
+
// OWN memoryDir from a --repo path (or an ephemeral scratch dir), so it
|
|
12801
|
+
// can never target the session's already-open backend handle — grounding
|
|
12802
|
+
// through this session's live memoryDir is what makes an ingested fact
|
|
12803
|
+
// answerable in the SAME conversation, not just written to disk somewhere.
|
|
12804
|
+
if (name === "ingest") {
|
|
12805
|
+
note(trace, "goal: ingest a local text file into the memory store, sentence by sentence");
|
|
12806
|
+
if (!memoryDir) return mk("no memory store here — /ingest works inside a repo session.", { miss: true });
|
|
12807
|
+
if (!argText) return mk("/ingest needs a path, e.g. `/ingest notes.txt`.", { miss: true });
|
|
12808
|
+
const { resolve } = await import("node:path");
|
|
12809
|
+
const { readFile } = await import("node:fs/promises");
|
|
12810
|
+
const filePath = resolve(process.cwd(), argText);
|
|
12811
|
+
let text;
|
|
12812
|
+
try {
|
|
12813
|
+
text = await readFile(filePath, "utf8");
|
|
12814
|
+
} catch (e) {
|
|
12815
|
+
return mk(`couldn't read ${argText} — ${e?.code === "ENOENT" ? "no such file." : String(e?.message || e)}`, { miss: true });
|
|
12816
|
+
}
|
|
12817
|
+
const { splitSentencesPreservingPaths } = await import("./sentences.mjs");
|
|
12818
|
+
const { loadMemory, readFactRows, appendFact } = await import("../adapters/memory/core.mjs");
|
|
12819
|
+
const { touchedFactRows } = await import("../domain/memory/touched-facts.mjs");
|
|
12820
|
+
const sourceTag = filePath.split(/[\\/]/).pop();
|
|
12821
|
+
const sentences = splitSentencesPreservingPaths(text);
|
|
12822
|
+
let recognizedSentences = 0;
|
|
12823
|
+
let factCount = 0;
|
|
12824
|
+
for (const sentence of sentences) {
|
|
12825
|
+
const before = readFactRows(await loadMemory(memoryDir));
|
|
12826
|
+
const { record: ingestRecord } = await runTurn(sentence, { config, memoryDir, sessionId: uuidv7() });
|
|
12827
|
+
if (ingestRecord?.via !== "assert" || ingestRecord?.miss) continue;
|
|
12828
|
+
const after = readFactRows(await loadMemory(memoryDir));
|
|
12829
|
+
const rows = touchedFactRows(before, after);
|
|
12830
|
+
if (!rows.length) continue;
|
|
12831
|
+
recognizedSentences += 1;
|
|
12832
|
+
for (const row of rows) {
|
|
12833
|
+
await appendFact(memoryDir, {
|
|
12834
|
+
subject: row.subject, predicate: row.predicate, object: row.object,
|
|
12835
|
+
provenance: `extracted:${sourceTag}`, quantifier: row.quantifier || "",
|
|
12836
|
+
});
|
|
12837
|
+
factCount += 1;
|
|
12838
|
+
}
|
|
12839
|
+
}
|
|
12840
|
+
if (cache) cache.rows = null; // the fact-rows cache predates these writes
|
|
12841
|
+
const skipped = sentences.length - recognizedSentences;
|
|
12842
|
+
note(trace, `result: ${sentences.length} sentence(s), ${recognizedSentences} recognized, ${factCount} fact row(s), ${skipped} skipped`);
|
|
12843
|
+
if (!factCount) {
|
|
12844
|
+
return mk(
|
|
12845
|
+
`read ${sentences.length} sentence${sentences.length === 1 ? "" : "s"} from ${argText} — none grounded into a `
|
|
12846
|
+
+ "recognized fact shape (an honest, expected gap; this is an attempt, not full NLU).",
|
|
12847
|
+
{ miss: true },
|
|
12848
|
+
);
|
|
12849
|
+
}
|
|
12850
|
+
return mk(
|
|
12851
|
+
`ingested ${factCount} fact${factCount === 1 ? "" : "s"} from ${argText} `
|
|
12852
|
+
+ `(${recognizedSentences} of ${sentences.length} sentence${sentences.length === 1 ? "" : "s"} recognized).`,
|
|
12853
|
+
);
|
|
12854
|
+
}
|
|
12855
|
+
|
|
12510
12856
|
// /plan <request> — the capability router (src/domain/router/*): plan+execute a
|
|
12511
12857
|
// compound ("of the modules impacted by X, which are untested", "assess X
|
|
12512
12858
|
// and then check Y") or maintenance-goal ("what most needs a test") request
|