@polycode-projects/the-mechanical-code-talker 2.10.3 → 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/memory/trust.mjs +21 -2
- package/src/domain/sense-split.mjs +203 -0
- package/src/services/chat-page-viz.mjs +87 -19
- package/src/services/chat-session.mjs +12 -7
- package/src/services/chat.mjs +320 -30
- 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 +54 -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 +135 -126
- 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";
|
|
@@ -5114,6 +5115,12 @@ const MODULE_ORIENT_SVO_RE = new RegExp(`^what\\s+(.+?)\\s+does${TRAILING_ADVERB
|
|
|
5114
5115
|
// terms. "what(?:'s|s|\s+is)" mirrors PERSONAL_ASSISTANT_NUDGE_RE's own
|
|
5115
5116
|
// tolerance for the bare "whats" contraction spelling, just below.
|
|
5116
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;
|
|
5117
5124
|
|
|
5118
5125
|
/** A module PATH as a reader types it — "src/core/store.mjs", "app/lib/b.mjs",
|
|
5119
5126
|
* or a bare "store.mjs". Requires a slash or a source-file extension, which is
|
|
@@ -5168,7 +5175,7 @@ async function moduleOrientLane(query, { graph }) {
|
|
|
5168
5175
|
// (stripFillerWords already eats "please"/"could you" as filler; the politeness
|
|
5169
5176
|
// regex only adds the "explain [to me]" wrapper on top).
|
|
5170
5177
|
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);
|
|
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);
|
|
5172
5179
|
// "what does src/core/store.mjs do" already reached the overview; the bare
|
|
5173
5180
|
// path and "what is <path>" did not, so the same module answered one
|
|
5174
5181
|
// phrasing and walled two. Both are claimed here rather than in ask.mjs,
|
|
@@ -5614,8 +5621,10 @@ export async function helpText() {
|
|
|
5614
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\")"],
|
|
5615
5622
|
["/capabilities", "what /plan can plan over: the built-in graph tools plus your taught actions"],
|
|
5616
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`)"],
|
|
5617
5626
|
["/narrate on|off", "verbose developer/debug mode: decision points, matched pattern, results+sources, goal per turn"],
|
|
5618
|
-
["/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"],
|
|
5619
5628
|
["/help", "this list"],
|
|
5620
5629
|
["/exit", "leave the session (also Ctrl+C / Ctrl+D)"],
|
|
5621
5630
|
];
|
|
@@ -5976,6 +5985,74 @@ function renderFactLine(f) {
|
|
|
5976
5985
|
return `i learned: ${factPhrase(f)}${cite}`;
|
|
5977
5986
|
}
|
|
5978
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
|
+
|
|
5979
6056
|
/** "a"/"an" for a term, through the SAME grammar-rules.toml "article" rule and
|
|
5980
6057
|
* finish.mjs's beginsWithVowelSound every other agreement site in this file
|
|
5981
6058
|
* uses — never a hardcoded "a", which is ungrammatical for a vowel-initial
|
|
@@ -7075,7 +7152,8 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7075
7152
|
// "disclosed, never dropped" contract). Unconfigured/tied bias degrades to
|
|
7076
7153
|
// trust-desc, byte-identical to before this feature existed.
|
|
7077
7154
|
hits = rankByBiasThenTrust(hits, biasByBundle);
|
|
7078
|
-
const lines = hits
|
|
7155
|
+
const { lines, grouped } = senseSplitFactList(hits, await factRows(memoryDir, cache), variants);
|
|
7156
|
+
if (grouped) return { ...grouped, replace: miss };
|
|
7079
7157
|
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
7080
7158
|
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
7081
7159
|
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
@@ -7624,12 +7702,13 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7624
7702
|
// renders (Part 6's "disclosed, never dropped" contract); literalHit/
|
|
7625
7703
|
// viaSubtype above already resolved off the pre-rank order.
|
|
7626
7704
|
hits = rankByBiasThenTrust(hits, biasByBundle);
|
|
7627
|
-
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}` };
|
|
7628
7709
|
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
7629
7710
|
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
7630
7711
|
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
7712
|
return { text: `${header}\n${shown.join("\n")}${extra}`, replace: true, ...(rest.length ? { pending: { items: rest.map((l) => l.trim()), noun: "facts" } } : {}) };
|
|
7634
7713
|
}
|
|
7635
7714
|
return null;
|
|
@@ -9863,11 +9942,20 @@ async function cleanMissPackKey(term, { graph, memoryDir, lexicon, cache }) {
|
|
|
9863
9942
|
if (!key) return null;
|
|
9864
9943
|
if (await resolveEntity(graph, term)) return null;
|
|
9865
9944
|
let normFactTerm;
|
|
9866
|
-
|
|
9945
|
+
let loadMemory;
|
|
9946
|
+
let readRuleRows;
|
|
9947
|
+
try { ({ normFactTerm, loadMemory, readRuleRows } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
9867
9948
|
const variants = factTermVariants(normFactTerm, term);
|
|
9868
9949
|
variants.add(key);
|
|
9869
9950
|
const rows = await factRows(memoryDir, cache);
|
|
9870
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 */ }
|
|
9871
9959
|
return key;
|
|
9872
9960
|
}
|
|
9873
9961
|
|
|
@@ -9935,22 +10023,87 @@ async function childPackFactsForKey(key, { memoryDir, env, cache }) {
|
|
|
9935
10023
|
})));
|
|
9936
10024
|
} catch { return null; }
|
|
9937
10025
|
if (cache) cache.rows = null;
|
|
10026
|
+
await synthesiseAroundTerm(memoryDir, key, cache);
|
|
9938
10027
|
return { key, count: row.facts.length };
|
|
9939
10028
|
}
|
|
9940
10029
|
|
|
9941
|
-
/** Store the article's first-sentence isa
|
|
9942
|
-
*
|
|
9943
|
-
*
|
|
9944
|
-
|
|
9945
|
-
|
|
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);
|
|
9946
10047
|
try {
|
|
9947
|
-
const {
|
|
9948
|
-
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
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);
|
|
9952
10057
|
if (cache) cache.rows = null;
|
|
9953
|
-
} 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;
|
|
9954
10107
|
}
|
|
9955
10108
|
|
|
9956
10109
|
/** The concept term a vague "what is a X" / "tell me about X" / "what does X mean" /
|
|
@@ -10144,6 +10297,11 @@ async function describeWrapperAnswer(query, { config, source, focus, graph, tel
|
|
|
10144
10297
|
// captured term, same class of gap stripTrailingDiscourseTag (ask-vocab.mjs)
|
|
10145
10298
|
// already fixes for the meta-whatis vocab lane.
|
|
10146
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;
|
|
10147
10305
|
if (DESCRIBE_PRONOUN_RE.test(term)) {
|
|
10148
10306
|
if (!focus?.label) return null; // no standing focus to resolve against — honest decline
|
|
10149
10307
|
term = focus.label;
|
|
@@ -11165,6 +11323,31 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11165
11323
|
});
|
|
11166
11324
|
}
|
|
11167
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
|
+
}
|
|
11168
11351
|
// COLLECTIVE PLURAL SUBJECT — see COLLECTIVE_FORWARD_RE. Members are the
|
|
11169
11352
|
// modules whose path carries the plural as a component; two or more make it
|
|
11170
11353
|
// a group question, answered as the disclosed union over every member. One
|
|
@@ -11776,15 +11959,15 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11776
11959
|
const coldPronounDecline = focus?.label ? null : coldPronounDeclineText(query);
|
|
11777
11960
|
if (bareMetaHit?.reference) {
|
|
11778
11961
|
// 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.
|
|
11962
|
+
// miss, the turn is no longer recorded as one, and the article's grounded
|
|
11963
|
+
// triples are stored after the answer composes.
|
|
11781
11964
|
answer = bareMetaHit.text;
|
|
11782
11965
|
via = "reference";
|
|
11783
11966
|
recordMiss = false;
|
|
11784
11967
|
handled = true;
|
|
11785
11968
|
note(trace, "lane: (2b) REFERENCE PACK — a bare \"what is X\" clean miss answered from the shipped reference pack, cited");
|
|
11786
11969
|
note(trace, `source: reference pack ${REFERENCE_PACK_NAME} — article "${bareMetaHit.reference.article.title}" (revid ${bareMetaHit.reference.article.revid})`);
|
|
11787
|
-
await
|
|
11970
|
+
await ingestReferenceArticle(memoryDir, bareMetaHit.reference.key, bareMetaHit.reference.article, cache, referenceProvenanceTag, lexicon);
|
|
11788
11971
|
} else if (bareMetaHit?.live) {
|
|
11789
11972
|
// The bare-form LIVE hit settles the same way, under live provenance.
|
|
11790
11973
|
answer = bareMetaHit.text;
|
|
@@ -11793,7 +11976,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11793
11976
|
handled = true;
|
|
11794
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");
|
|
11795
11978
|
note(trace, `source: live reference ${LIVE_PACK_NAME} — article "${bareMetaHit.live.article.title}" (revid ${bareMetaHit.live.article.revid})`);
|
|
11796
|
-
await
|
|
11979
|
+
await ingestReferenceArticle(memoryDir, bareMetaHit.live.key, bareMetaHit.live.article, cache, liveProvenanceTag, lexicon);
|
|
11797
11980
|
} else if (bareMetaHit) {
|
|
11798
11981
|
answer = bareMetaHit.replace ? bareMetaHit.text : `${answer}\n${bareMetaHit.text}`;
|
|
11799
11982
|
// Same discipline as lane (3): a fact-lane return flagged `miss` is an
|
|
@@ -12200,7 +12383,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12200
12383
|
recordMiss = false;
|
|
12201
12384
|
note(trace, "lane: (4h) REFERENCE PACK — a clean miss on a lexicon term answered from the shipped reference pack, cited");
|
|
12202
12385
|
note(trace, `source: reference pack ${REFERENCE_PACK_NAME} — article "${ref.article.title}" (revid ${ref.article.revid})`);
|
|
12203
|
-
await
|
|
12386
|
+
await ingestReferenceArticle(memoryDir, ref.key, ref.article, cache, referenceProvenanceTag, lexicon);
|
|
12204
12387
|
}
|
|
12205
12388
|
}
|
|
12206
12389
|
// The live Wikipedia supplement (opt-in), strictly AFTER both shipped
|
|
@@ -12216,7 +12399,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12216
12399
|
recordMiss = false;
|
|
12217
12400
|
note(trace, "lane: (4h) LIVE WIKIPEDIA — a clean miss answered from a live en.wikipedia.org lookup (opt-in), cited");
|
|
12218
12401
|
note(trace, `source: live reference ${LIVE_PACK_NAME} — article "${live.article.title}" (revid ${live.article.revid})`);
|
|
12219
|
-
await
|
|
12402
|
+
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon);
|
|
12220
12403
|
}
|
|
12221
12404
|
}
|
|
12222
12405
|
}
|
|
@@ -12314,6 +12497,23 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12314
12497
|
answer = `hypothetically, if ${counterfactualSubject[1].trim()} were removed: ${answer}`;
|
|
12315
12498
|
note(trace, `intermediate: COUNTERFACTUAL_RE matched — compiled to a real traversal, wrapped as hypothetical ("${counterfactualSubject[1].trim()}" removed)`);
|
|
12316
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
|
+
}
|
|
12317
12517
|
// The concept force answers WITH real example instances — those are the entities the
|
|
12318
12518
|
// turn "asked about" (the SchemaClass meta-node is documentation, not a code entity),
|
|
12319
12519
|
// so record + expand them, not the schema match.
|
|
@@ -12403,6 +12603,8 @@ const GOAL_BY_COMMAND = {
|
|
|
12403
12603
|
capabilities: "see what /plan can plan over — built-in query tools and taught actions",
|
|
12404
12604
|
syllogise: "materialize the entailed facts that follow from what's remembered about one term",
|
|
12405
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",
|
|
12406
12608
|
};
|
|
12407
12609
|
|
|
12408
12610
|
/** A slash-command → the mapped tool (or the /help, /focus, /narrate, unknown
|
|
@@ -12451,12 +12653,14 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
12451
12653
|
// state). A bare "/wiki" reports the CURRENT state and changes nothing.
|
|
12452
12654
|
if (name === "wiki") {
|
|
12453
12655
|
const arg = argText.toLowerCase();
|
|
12454
|
-
|
|
12455
|
-
|
|
12456
|
-
|
|
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.");
|
|
12457
12661
|
}
|
|
12458
|
-
const next = arg === "on";
|
|
12459
|
-
return mk(`live Wikipedia supplement ${next
|
|
12662
|
+
const next = arg === "supplement" ? "supplement" : arg === "on";
|
|
12663
|
+
return mk(`live Wikipedia supplement ${stateWord(next)}.`, { liveReferenceNext: next });
|
|
12460
12664
|
}
|
|
12461
12665
|
|
|
12462
12666
|
// /memory [verbose] — what tmct remembers, as text (the same renderer
|
|
@@ -12563,6 +12767,92 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
12563
12767
|
}
|
|
12564
12768
|
}
|
|
12565
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
|
+
|
|
12566
12856
|
// /plan <request> — the capability router (src/domain/router/*): plan+execute a
|
|
12567
12857
|
// compound ("of the modules impacted by X, which are untested", "assess X
|
|
12568
12858
|
// and then check Y") or maintenance-goal ("what most needs a test") request
|
|
@@ -31,6 +31,17 @@ export function edgePhrase(kind) {
|
|
|
31
31
|
return EDGE_PHRASE.get(String(kind || "")) || String(kind || "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Both packagers (build-electron-app.mjs, build-demo-site.mjs) place
|
|
35
|
+
// vendor/wink.js one level below the page they render, so the same loader
|
|
36
|
+
// script inlines into renderCodeExplorerHtml's `winkLoaderInline` slot for
|
|
37
|
+
// either channel.
|
|
38
|
+
export const VENDOR_WINK_LOADER_JS = `window.__WINK_LOADER__ = async function () {
|
|
39
|
+
var m = await import("./vendor/wink.js");
|
|
40
|
+
return { winkNLP: m.winkNLP, model: m.model };
|
|
41
|
+
};`;
|
|
42
|
+
|
|
43
|
+
export const DESKTOP_APP_URL = "https://gitlab.com/polycode-projects/the-mechanical-code-talker#the-code-explorer-desktop";
|
|
44
|
+
|
|
34
45
|
const LEDGER_ROW_LIMIT_DEFAULT = 4000;
|
|
35
46
|
|
|
36
47
|
/** Pure derivation over an entities payload (individuals + objectProperties).
|
|
@@ -247,9 +258,11 @@ const CLIENT_JS = String.raw`
|
|
|
247
258
|
* computeCodeExplorerData's output. `bundleInline` inlines the dock engine
|
|
248
259
|
* (for a single-file page / a data: URL); otherwise `bundleAvailable` links
|
|
249
260
|
* `./code-explorer.bundle.js`. `winkLoaderInline` optionally inlines a wink
|
|
250
|
-
* model loader as `window.__WINK_LOADER__`.
|
|
261
|
+
* model loader as `window.__WINK_LOADER__`. `showDesktopLink` adds a line
|
|
262
|
+
* pointing at the desktop app's README section — the desktop shell itself
|
|
263
|
+
* renders this page too and passes `false`, since it has nothing to point at.
|
|
251
264
|
*/
|
|
252
|
-
export function renderCodeExplorerHtml(data, { bundleInline = "", bundleAvailable = false, winkLoaderInline = "", sourceName = "demo code graph" } = {}) {
|
|
265
|
+
export function renderCodeExplorerHtml(data, { bundleInline = "", bundleAvailable = false, winkLoaderInline = "", sourceName = "demo code graph", showDesktopLink = false } = {}) {
|
|
253
266
|
const payloadJson = embedJson(data.payload);
|
|
254
267
|
const dataJson = embedJson({ ledger: data.ledger, hints: data.hints, focus: data.focus, meta: data.meta });
|
|
255
268
|
const title = escapeHtml(data.meta?.title || "code explorer");
|
|
@@ -304,6 +317,7 @@ ul.rows { list-style: none; margin: 0; padding: 0; }
|
|
|
304
317
|
<header>
|
|
305
318
|
<h1>tmct code explorer</h1>
|
|
306
319
|
<span class="sub">source: <span id="source-name">${escapeHtml(sourceName)}</span></span>
|
|
320
|
+
${showDesktopLink ? `<span class="sub">Also available as a <a href="${DESKTOP_APP_URL}">desktop app</a>.</span>` : ""}
|
|
307
321
|
<div class="pickers">
|
|
308
322
|
<button id="open-graph">Open graph…</button>
|
|
309
323
|
<button id="open-repo">Open repo…</button>
|