@polycode-projects/the-mechanical-code-talker 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -3
- package/ROADMAP.md +45 -0
- package/bin/tmct.mjs +8 -2
- package/corpus/conceptnet/README.md +59 -52
- package/corpus/conceptnet/filter-dump.mjs +59 -2
- package/corpus/conceptnet/slice.jsonl +31067 -0
- package/corpus/seon/relations.jsonl +8 -0
- package/data/templates/responses.jsonl +3 -2
- package/package.json +6 -3
- package/src/ask.mjs +2 -2
- package/src/chat.mjs +314 -39
- package/src/concept.mjs +393 -0
- package/src/corpus/conceptnet.mjs +18 -13
- package/src/init.mjs +12 -9
- package/src/interpret/normalize.mjs +42 -0
- package/src/interpret/pipeline.mjs +2 -2
- package/src/memory/core.mjs +84 -0
- package/src/tui/app.mjs +26 -2
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{"relation":"imports","definition":"To import is to bring another module's definitions into the current one.","sense":"software"}
|
|
2
|
+
{"relation":"calls","definition":"A call is one function invoking another.","sense":"software"}
|
|
3
|
+
{"relation":"contains","definition":"Containment is a class or module holding a member (a method, attribute, or nested definition).","sense":"software"}
|
|
4
|
+
{"relation":"inherits","definition":"Inheritance is one class deriving its structure and behaviour from another.","sense":"software"}
|
|
5
|
+
{"relation":"tests","definition":"A test is code that exercises another unit and checks its behaviour.","sense":"software"}
|
|
6
|
+
{"relation":"defines","definition":"A definition is where a name (a class, function, or variable) is introduced.","sense":"software"}
|
|
7
|
+
{"relation":"touches","definition":"A touch is a commit changing a file or a symbol in the codebase.","sense":"software"}
|
|
8
|
+
{"relation":"cochange","definition":"Change-coupling is two files that tend to be changed together in the same commits.","sense":"software"}
|
|
@@ -62,9 +62,10 @@
|
|
|
62
62
|
{"id":"conversational-farewell","class":"conversational","register":"friendly","template":"Bye — flushing the session log. Come back with a question any time."}
|
|
63
63
|
{"id":"orientation-friendly","class":"orientation","register":"friendly","template":"I answer questions about THIS codebase's structure — imports, calls, definitions,\nhistory and counts. For example:\n which modules import walk.mjs\n what calls buildContextBundle\n how many classes are there\n/help for commands, /stats for an overview of the graph."}
|
|
64
64
|
{"id":"miss-no-previous-answer","class":"miss","register":"friendly","template":"No previous answer to expand yet — ask me a question first, then say \"why\" or \"say more\"."}
|
|
65
|
-
{"id":"conversational-greeting-empty","class":"conversational","register":"friendly","template":"Hi. There's no code graph loaded here
|
|
66
|
-
{"id":"orientation-empty","class":"orientation","register":"friendly","template":"There's no code graph loaded here, so I can't answer structure questions (imports, calls, definitions) yet.\
|
|
65
|
+
{"id":"conversational-greeting-empty","class":"conversational","register":"friendly","template":"Hi. There's no code graph loaded here — for code structure (imports, calls, definitions) I need a `.tmct/graph.json`: point me at one with `--repo <path>`, or try the shipped example `npm run example:mini`. (tmct reads graphs; it doesn't index code itself.) For general vocabulary, `tmct init` seeds concepts — try \"what is a cache\". /help for commands."}
|
|
66
|
+
{"id":"orientation-empty","class":"orientation","register":"friendly","template":"There's no code graph loaded here, so I can't answer structure questions (imports, calls, definitions) yet.\nFor those I need a `.tmct/graph.json` produced by a graph producer — point me at one with `--repo <path>`, or try the shipped example `npm run example:mini`. tmct reads graphs; it doesn't index code itself.\nFor general vocabulary, `tmct init` seeds concepts — try \"what is a cache\". /help for commands, /memory for what I remember."}
|
|
67
67
|
{"id":"technical-density","class":"count","register":"technical","template":"{subject} carries {count} {noun} across {scope} — a concentration well above what a codebase of this size typically sustains ({provenance})."}
|
|
68
68
|
{"id":"technical-comparison","class":"count","register":"technical","template":"At {count} {noun}, {subject} sits {comparison} the comparable-project baseline, a divergence that reflects deliberate structure rather than measurement noise ({provenance})."}
|
|
69
69
|
{"id":"technical-superlative","class":"count","register":"technical","template":"No {noun} in {scope} is more {metric} than {subject}; it leads the next candidate by a clear margin of {count} ({provenance})."}
|
|
70
70
|
{"id":"technical-ratio","class":"count","register":"technical","template":"{subject} sustains a ratio of {count} {noun} per {unit}, placing it in the upper band for projects of comparable {scope} ({provenance})."}
|
|
71
|
+
{"id":"concept-force","class":"concept","register":"friendly","template":"{definition}\n{examples}{followups}"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
|
|
@@ -78,8 +78,11 @@
|
|
|
78
78
|
"test": "node --test \"test/**/*.test.mjs\"",
|
|
79
79
|
"chat": "node bin/tmct.mjs",
|
|
80
80
|
"chat:repo": "node bin/tmct.mjs chat --repo",
|
|
81
|
-
"
|
|
82
|
-
"
|
|
81
|
+
"init": "node bin/tmct.mjs init",
|
|
82
|
+
"memory": "node bin/tmct.mjs memory",
|
|
83
|
+
"syllogise": "node bin/tmct.mjs syllogise",
|
|
84
|
+
"example:mini": "node bin/tmct.mjs chat --repo examples/mini-webapp --ephemeral",
|
|
85
|
+
"example:polyglot": "node bin/tmct.mjs chat --repo examples/polyglot --ephemeral",
|
|
83
86
|
"chatbench:run": "node chatbench/run.mjs",
|
|
84
87
|
"chatbench:judge": "node chatbench/judge.mjs",
|
|
85
88
|
"audit": "npm audit --audit-level=high",
|
package/src/ask.mjs
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
// grammar, split out of this file: normalization pre-pass, the two parsing
|
|
52
52
|
// strategies, and the bounded-fuzzy service. Re-exported below where existing
|
|
53
53
|
// callers/tests import them from here.
|
|
54
|
-
import { normalizeQuery, applyNegationFrames, matchNegationSet, STOPWORDS, splitWords, wordsOf } from "./interpret/normalize.mjs";
|
|
54
|
+
import { normalizeQuery, applyNegationFrames, applyPhrasingFrames, matchNegationSet, STOPWORDS, splitWords, wordsOf } from "./interpret/normalize.mjs";
|
|
55
55
|
import { editDistance, fuzzyBound } from "./interpret/fuzzy.mjs";
|
|
56
56
|
import { parseAnchored } from "./interpret/strategies/grammar.mjs";
|
|
57
57
|
import { parseKeywordSpot, findPhrase } from "./interpret/strategies/keywords.mjs";
|
|
@@ -168,7 +168,7 @@ export function parseQuery(query, { nlp = undefined } = {}) {
|
|
|
168
168
|
const adapter = nlp === undefined ? defaultNlp() : nlp;
|
|
169
169
|
const raw = String(query || "").trim().replace(/\s+/g, " ");
|
|
170
170
|
if (!raw) return null;
|
|
171
|
-
const text = applyNegationFrames(normalizeQuery(raw));
|
|
171
|
+
const text = applyPhrasingFrames(applyNegationFrames(normalizeQuery(raw)));
|
|
172
172
|
if (!text) return null;
|
|
173
173
|
// COMPOSITIONAL PARSE PATH (PLAN §5.16 P3) — the new PRIMARY layer: a recursive
|
|
174
174
|
// descent over CLAUSES for the compositional shapes (nested/relative, boolean,
|
package/src/chat.mjs
CHANGED
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
|
|
40
40
|
import { join, dirname } from "node:path";
|
|
41
41
|
import { createWriteStream } from "node:fs";
|
|
42
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
42
|
+
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
43
|
+
import { tmpdir } from "node:os";
|
|
43
44
|
import { createInterface } from "node:readline/promises";
|
|
44
45
|
import { spawnSync } from "node:child_process";
|
|
45
46
|
import { dispatchTool } from "./server.mjs";
|
|
@@ -304,6 +305,12 @@ const STRUCT_WORDS = new Set([
|
|
|
304
305
|
"subclass", "subclasses", "inherit", "inherits", "test", "tests", "touch", "touches",
|
|
305
306
|
"commit", "commits", "export", "exports", "caller", "callers", "callee", "callees",
|
|
306
307
|
"history", "where", "mentioned", "signature", "impact",
|
|
308
|
+
// relation-concept vocabulary (gerunds + relation nouns) so a SHORT relation touch
|
|
309
|
+
// ("what is calling", "what about inheritance") is a structural question, not
|
|
310
|
+
// small-talk — otherwise a ≤3-word relation touch is grabbed by the conversational
|
|
311
|
+
// orientation before the relation concept force can serve it.
|
|
312
|
+
"importing", "calling", "invoking", "inheriting", "containing", "contains", "containment",
|
|
313
|
+
"testing", "defining", "touching", "extending", "inheritance", "coverage", "member", "members",
|
|
307
314
|
]);
|
|
308
315
|
|
|
309
316
|
/** Does this look like small-talk / an orientation request rather than a
|
|
@@ -345,6 +352,9 @@ const T_WHY_EMPTY = "miss-no-previous-answer";
|
|
|
345
352
|
* over-promising "ask me about this codebase". */
|
|
346
353
|
const T_GREETING_EMPTY = "conversational-greeting-empty";
|
|
347
354
|
const T_ORIENTATION_EMPTY = "orientation-empty";
|
|
355
|
+
/** THE CONCEPT FORCE (concept.mjs): the three-band answer to a vague "what is a X"
|
|
356
|
+
* that names a known concept WITH instances — {definition}/{examples}/{followups}. */
|
|
357
|
+
const T_CONCEPT = "concept-force";
|
|
348
358
|
|
|
349
359
|
/** The degraded line when the template library itself cannot load — a packaging
|
|
350
360
|
* failure said out loud, never a crashed turn or a silently different answer. */
|
|
@@ -486,9 +496,10 @@ function orientationAnswer(templates, graph) {
|
|
|
486
496
|
* when a code graph is loaded, else the honest empty-graph orientation. */
|
|
487
497
|
function orientationText(graph) {
|
|
488
498
|
if (noCodeGraph(graph)) {
|
|
489
|
-
return "There's no code graph loaded here, so I can't answer structure questions yet. "
|
|
490
|
-
+ "
|
|
491
|
-
+
|
|
499
|
+
return "There's no code graph loaded here, so I can't answer structure questions (imports, calls, definitions) yet. "
|
|
500
|
+
+ "For those I need a `.tmct/graph.json` produced by a graph producer — point me at one with `--repo <path>`, "
|
|
501
|
+
+ "or try the shipped example `npm run example:mini`. tmct reads graphs; it doesn't index code itself. "
|
|
502
|
+
+ 'For general vocabulary, `tmct init` seeds concepts — try "what is a cache". /help for commands.';
|
|
492
503
|
}
|
|
493
504
|
const by = (cls) => (graph.individuals || []).filter((i) => (i.class || "") === cls).length;
|
|
494
505
|
const parts = [];
|
|
@@ -798,11 +809,18 @@ const FACT_PREDICATE_PHRASES = {
|
|
|
798
809
|
};
|
|
799
810
|
const factPhrase = (f) => `${f.subject} ${FACT_PREDICATE_PHRASES[f.predicate] || f.predicate} ${f.object}`;
|
|
800
811
|
|
|
801
|
-
/** One rendered fact line
|
|
802
|
-
* provenance
|
|
812
|
+
/** One rendered fact line. An OPERATOR-asserted fact keeps the true first-person
|
|
813
|
+
* provenance ("you told me: …"). A CORPUS fact is presented as clean DATA with its
|
|
814
|
+
* source cited — NEVER "i learned: …", which over-claims and anthropomorphises
|
|
815
|
+
* (especially when the corpus row is noise); the relation and its provenance speak
|
|
816
|
+
* for themselves. Provenance stays VERBATIM either way. */
|
|
803
817
|
function renderFactLine(f) {
|
|
804
|
-
const
|
|
805
|
-
|
|
818
|
+
const cite = f.provenance ? ` (source: ${f.provenance})` : "";
|
|
819
|
+
if (f.provenance.includes("ace:chat")) return `you told me: ${factPhrase(f)}${cite}`;
|
|
820
|
+
// CORPUS facts are background DATA — present the relation plainly, cited to its
|
|
821
|
+
// source, NEVER "i learned: …" (the footgun: a first-person claim over corpus noise).
|
|
822
|
+
if (f.provenance.includes("corpus:")) return `${factPhrase(f)}${cite}`;
|
|
823
|
+
return `i learned: ${factPhrase(f)}${cite}`;
|
|
806
824
|
}
|
|
807
825
|
|
|
808
826
|
/** Read every reified Fact out of the memory graph as plain {subject, predicate,
|
|
@@ -852,8 +870,8 @@ const ISA_ASK_RE = /^(?:is|are)\s+(?:an?\s+)?(.+?)\s+(?:a\s+kind\s+of|a\s+type\s
|
|
|
852
870
|
const ISA_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
|
|
853
871
|
/** "what do you know about caches" — the open recall-everything form. */
|
|
854
872
|
const KNOW_ABOUT_RE = /^what\s+do\s+you\s+know\s+about\s+(.+?)[?.!\s]*$/i;
|
|
855
|
-
/** How many facts a single answer lists before
|
|
856
|
-
const FACT_ANSWER_CAP =
|
|
873
|
+
/** How many facts a single answer lists before the remainder is paged with "more". */
|
|
874
|
+
const FACT_ANSWER_CAP = 32;
|
|
857
875
|
|
|
858
876
|
/** W4 seam: answer (or extend) a vocabulary/definition question from the MEMORY
|
|
859
877
|
* graph's Facts. Returns { text, replace } — `replace:false` means the engine's
|
|
@@ -880,9 +898,11 @@ async function factAnswer(memoryDir, query, envelope, miss) {
|
|
|
880
898
|
const variants = factTermVariants(normFactTerm, metaTerm);
|
|
881
899
|
const hits = (await memoryFacts(memoryDir)).filter((f) => variants.has(f.subject));
|
|
882
900
|
if (!hits.length) return null;
|
|
883
|
-
const
|
|
884
|
-
const
|
|
885
|
-
|
|
901
|
+
const lines = hits.map(renderFactLine);
|
|
902
|
+
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
903
|
+
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
904
|
+
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
905
|
+
return { text: shown.join("\n") + extra, replace: miss, ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}) };
|
|
886
906
|
}
|
|
887
907
|
if (!miss) return null;
|
|
888
908
|
|
|
@@ -907,9 +927,11 @@ async function factAnswer(memoryDir, query, envelope, miss) {
|
|
|
907
927
|
if (!hits.length) return null;
|
|
908
928
|
// echo the STORED spelling ("caches" asked → "cache" known), never a guess
|
|
909
929
|
const term = variants.has(hits[0].subject) ? hits[0].subject : hits[0].object;
|
|
910
|
-
const
|
|
911
|
-
const
|
|
912
|
-
|
|
930
|
+
const lines = hits.map((f) => ` ${renderFactLine(f)}`);
|
|
931
|
+
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
932
|
+
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
933
|
+
const extra = rest.length ? `\n …and ${rest.length} more — say 'more' to see them.` : "";
|
|
934
|
+
return { text: `${hits.length} remembered fact${hits.length === 1 ? "" : "s"} about ${term}:\n${shown.join("\n")}${extra}`, replace: true, ...(rest.length ? { pending: { items: rest.map((l) => l.trim()), noun: "facts" } } : {}) };
|
|
913
935
|
}
|
|
914
936
|
return null;
|
|
915
937
|
}
|
|
@@ -963,10 +985,11 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null) {
|
|
|
963
985
|
const isa = rows.filter((f) => ISA_PREDICATES.has(f.predicate));
|
|
964
986
|
const byTrust = (a, b) => b.trust - a.trust;
|
|
965
987
|
const renderMany = (hits) => {
|
|
966
|
-
const
|
|
967
|
-
const
|
|
968
|
-
const
|
|
969
|
-
|
|
988
|
+
const lines = hits.map(renderFactLine);
|
|
989
|
+
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
990
|
+
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
991
|
+
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
992
|
+
return { text: shown.join("\n") + extra, replace: true, ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}) };
|
|
970
993
|
};
|
|
971
994
|
|
|
972
995
|
// (d) WHOLE-STORE recall (CHATBENCH_006 lever 3) — "what did i tell you last time",
|
|
@@ -1001,9 +1024,11 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null) {
|
|
|
1001
1024
|
const hits = rows.filter((f) => variants.has(f.subject) || variants.has(f.object)).sort(byTrust);
|
|
1002
1025
|
if (!hits.length) return null;
|
|
1003
1026
|
const term = variants.has(hits[0].subject) ? hits[0].subject : hits[0].object;
|
|
1004
|
-
const
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1027
|
+
const lines = hits.map((f) => ` ${renderFactLine(f)}`);
|
|
1028
|
+
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
1029
|
+
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
1030
|
+
const extra = rest.length ? `\n …and ${rest.length} more — say 'more' to see them.` : "";
|
|
1031
|
+
return { text: `${hits.length} remembered fact${hits.length === 1 ? "" : "s"} about ${term}:\n${shown.join("\n")}${extra}`, replace: true, ...(rest.length ? { pending: { items: rest.map((l) => l.trim()), noun: "facts" } } : {}) };
|
|
1007
1032
|
}
|
|
1008
1033
|
|
|
1009
1034
|
// (c) REVERSE / "what kind of thing" membership. The meta form ("what is a Y")
|
|
@@ -1167,6 +1192,32 @@ function seonDefinitions() {
|
|
|
1167
1192
|
return seonDefsPromise;
|
|
1168
1193
|
}
|
|
1169
1194
|
|
|
1195
|
+
let seonRelsPromise = null;
|
|
1196
|
+
/** Load corpus/seon/relations.jsonl once → Map(relationTerm → definition), keyed on
|
|
1197
|
+
* the concept key ("imports","calls",…). Sits beside definitions.jsonl (same seon
|
|
1198
|
+
* dir), loaded the same lazy + failure-tolerated way — any failure degrades to an
|
|
1199
|
+
* empty map, so the relation force simply declines rather than throwing. */
|
|
1200
|
+
function relationDefinitions() {
|
|
1201
|
+
if (!seonRelsPromise) {
|
|
1202
|
+
seonRelsPromise = (async () => {
|
|
1203
|
+
const { SEON_DEFINITIONS_FILE } = await import("./corpus/conceptnet.mjs");
|
|
1204
|
+
const relFile = join(dirname(SEON_DEFINITIONS_FILE), "relations.jsonl");
|
|
1205
|
+
const raw = await readFile(relFile, "utf8");
|
|
1206
|
+
const map = new Map();
|
|
1207
|
+
for (const line of raw.split("\n")) {
|
|
1208
|
+
const t = line.trim();
|
|
1209
|
+
if (!t) continue;
|
|
1210
|
+
try {
|
|
1211
|
+
const row = JSON.parse(t);
|
|
1212
|
+
if (row.relation && row.definition) map.set(String(row.relation).toLowerCase(), String(row.definition));
|
|
1213
|
+
} catch { /* skip a malformed line, never throw */ }
|
|
1214
|
+
}
|
|
1215
|
+
return map;
|
|
1216
|
+
})().catch(() => new Map());
|
|
1217
|
+
}
|
|
1218
|
+
return seonRelsPromise;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1170
1221
|
/** The meta term a "what is a X" / "what does X mean" / "define X" question asks
|
|
1171
1222
|
* about — from the parse when present, else recognized directly (same required-
|
|
1172
1223
|
* article discipline as the grammar's T5). Null when the line isn't such a form. */
|
|
@@ -1208,6 +1259,132 @@ async function curatedDefinitionAnswer(query, envelope, { memoryDir, lexicon })
|
|
|
1208
1259
|
return { text: `${def} (source: corpus/seon)`, term };
|
|
1209
1260
|
}
|
|
1210
1261
|
|
|
1262
|
+
/** The concept term a vague "what is a X" / "tell me about X" / "what does X mean" /
|
|
1263
|
+
* "define X" asks about — metaTermOf's forms plus the "tell me about …" opener that
|
|
1264
|
+
* the graph parser reads as a count. Null when the line isn't such a touch. The
|
|
1265
|
+
* concept force is gated further downstream (a KNOWN, instance-bearing concept), so
|
|
1266
|
+
* this only has to recognize the SHAPE, not vet the term. */
|
|
1267
|
+
/** The VAGUE-TOUCH shapes ("tell me about X", "[and] what about X") — a concept
|
|
1268
|
+
* touch that is NOT the "what is a X" / "what does X mean" META shape. The meta shape
|
|
1269
|
+
* has its own established handling (a noun definition, a predicate definition, or the
|
|
1270
|
+
* honest ambiguity surround for a term that is BOTH a noun and a predicate — e.g.
|
|
1271
|
+
* "imports"), which the RELATION force must never preempt (frozen case
|
|
1272
|
+
* am-meta-imports). Gated downstream by CONCEPT_CLASS / RELATION_TERM, so a real
|
|
1273
|
+
* entity name declines here. */
|
|
1274
|
+
function vagueTouchTermOf(query) {
|
|
1275
|
+
const q = String(query).trim();
|
|
1276
|
+
const m = q.match(/^tell me about\s+(?:an?\s+)?(.+?)[?.!\s]*$/i)
|
|
1277
|
+
|| q.match(/^(?:(?:and|so|but|ok|okay|now|then)\s+)*what about\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i);
|
|
1278
|
+
return m ? m[1].trim() : null;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function conceptTermOf(query, envelope) {
|
|
1282
|
+
return metaTermOf(query, envelope) || vagueTouchTermOf(query);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/** The RELATION term a vague touch names — the VAGUE-touch shapes only ("tell me
|
|
1286
|
+
* about X", "what about X"), NOT the "what is a X"/"what does X mean" meta shape (a
|
|
1287
|
+
* relation term that is also a vocabulary word, like "imports", keeps its established
|
|
1288
|
+
* ambiguity/predicate-definition answer — frozen case am-meta-imports). Plus the
|
|
1289
|
+
* relation-only openers the graph parser reads as something else: "what are the
|
|
1290
|
+
* imports", "what calls are there", "what is calling". Null when the line isn't such a
|
|
1291
|
+
* touch. Gated downstream by RELATION_TERM, so this only has to recognize the SHAPE. */
|
|
1292
|
+
function relationTermOf(query, envelope) {
|
|
1293
|
+
const base = vagueTouchTermOf(query);
|
|
1294
|
+
if (base) return base;
|
|
1295
|
+
const q = String(query).trim().toLowerCase().replace(/[?.!]+$/, "").replace(/\s+/g, " ");
|
|
1296
|
+
let m;
|
|
1297
|
+
// "what are the imports", "what is the containment", "what are all the calls"
|
|
1298
|
+
if ((m = q.match(/^what\s+(?:are|is)\s+(?:all\s+)?(?:the\s+)?([a-z][a-z-]*?)(?:\s+(?:edges|relationships|relations))?$/))) return m[1];
|
|
1299
|
+
// "what calls are there", "what imports are there"
|
|
1300
|
+
if ((m = q.match(/^what\s+([a-z][a-z-]*?)\s+are\s+there$/))) return m[1];
|
|
1301
|
+
// "what is calling", "what is importing" (bare gerund, no object)
|
|
1302
|
+
if ((m = q.match(/^what\s+(?:is|are)\s+([a-z][a-z-]*ing)$/))) return m[1];
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/** THE RELATION CONCEPT FORCE — compose the three-band answer (curated relation
|
|
1307
|
+
* definition + real example EDGES + pre-validated follow-ups) for a vague touch on a
|
|
1308
|
+
* relation/edge kind ("what about imports", "what are the calls", "tell me about
|
|
1309
|
+
* contains"), or null when it isn't one: not a recognizable relation touch, not a
|
|
1310
|
+
* known edge concept (RELATION_TERM), no curated definition, or the graph has NO
|
|
1311
|
+
* edges of that kind (composeRelation's own honest-miss gate). Loads the definition
|
|
1312
|
+
* from the shipped corpus/seon/relations.jsonl, so it works without per-repo memory
|
|
1313
|
+
* seeding. Lazy + failure-tolerated throughout. Returns { text, pending }. */
|
|
1314
|
+
async function relationForceAnswer(query, envelope, { graph, config, source, templates }) {
|
|
1315
|
+
const rawTerm = relationTermOf(query, envelope);
|
|
1316
|
+
if (!rawTerm) return null;
|
|
1317
|
+
let composeRelation; let RELATION_TERM;
|
|
1318
|
+
try { ({ composeRelation, RELATION_TERM } = await import("./concept.mjs")); }
|
|
1319
|
+
catch { return null; }
|
|
1320
|
+
const term = String(rawTerm).toLowerCase();
|
|
1321
|
+
if (!RELATION_TERM[term]) return null; // not an enumerable edge concept — ordinary path owns it
|
|
1322
|
+
const definition = (await relationDefinitions()).get(RELATION_TERM[term]) ?? null;
|
|
1323
|
+
if (!definition) return null;
|
|
1324
|
+
// Same graph-load fallback as conceptForceAnswer: the shell hands the loaded graph
|
|
1325
|
+
// straight in; the pure runTurn(config) path loads it the way dispatchTool does.
|
|
1326
|
+
let g = graph;
|
|
1327
|
+
if (!g && config && source) {
|
|
1328
|
+
try { g = parseEntities(await source.fetchEntities(config)); } catch { g = null; }
|
|
1329
|
+
}
|
|
1330
|
+
if (!g) return null;
|
|
1331
|
+
let composed;
|
|
1332
|
+
try { composed = composeRelation(g, term, { definition }); }
|
|
1333
|
+
catch { return null; }
|
|
1334
|
+
if (!composed) return null;
|
|
1335
|
+
const rendered = tRender(templates, T_CONCEPT, {
|
|
1336
|
+
definition: composed.definition, examples: composed.examples, followups: composed.followups,
|
|
1337
|
+
});
|
|
1338
|
+
const text = rendered ?? `${composed.definition}\n${composed.examples}${composed.followups}`;
|
|
1339
|
+
const pending = composed.remainder && composed.remainder.length
|
|
1340
|
+
? { items: composed.remainder, noun: composed.noun }
|
|
1341
|
+
: null;
|
|
1342
|
+
return { text, pending };
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/** THE CONCEPT FORCE — compose the three-band answer (corpus/seon definition + real
|
|
1346
|
+
* graph/memory instances + pre-validated follow-ups) for a vague concept touch, or
|
|
1347
|
+
* null when it isn't one: not a "what is a X"/"tell me about X" shape, not a known
|
|
1348
|
+
* enumerable concept (CONCEPT_CLASS), no curated definition, or NO instances anywhere
|
|
1349
|
+
* (composeConcept's own honest-miss gate). Loads the definition DIRECTLY from the
|
|
1350
|
+
* shipped corpus/seon file (seonDefinitions), so it works without per-repo memory
|
|
1351
|
+
* seeding; the memory fact rows only ADD remembered "A is a X" examples when present.
|
|
1352
|
+
* Lazy + failure-tolerated throughout (chat.mjs ethos). Returns { text, instances }. */
|
|
1353
|
+
async function conceptForceAnswer(query, envelope, { graph, config, source, memoryDir, templates }) {
|
|
1354
|
+
const rawTerm = conceptTermOf(query, envelope);
|
|
1355
|
+
if (!rawTerm) return null;
|
|
1356
|
+
let normFactTerm; let composeConcept; let CONCEPT_CLASS;
|
|
1357
|
+
try {
|
|
1358
|
+
({ normFactTerm } = await import("./memory/core.mjs"));
|
|
1359
|
+
({ composeConcept, CONCEPT_CLASS } = await import("./concept.mjs"));
|
|
1360
|
+
} catch { return null; }
|
|
1361
|
+
const term = normFactTerm(rawTerm);
|
|
1362
|
+
if (!CONCEPT_CLASS[term]) return null; // not an enumerable code concept — ordinary path owns it
|
|
1363
|
+
const definition = (await seonDefinitions()).get(term) ?? null;
|
|
1364
|
+
if (!definition) return null;
|
|
1365
|
+
// The runChat shell hands the loaded graph straight in; the pure runTurn(config)
|
|
1366
|
+
// path (tests, chatbench) does not, so load it the same way dispatchTool does when
|
|
1367
|
+
// it's missing. Failure-tolerated: no loadable graph → no concept force.
|
|
1368
|
+
let g = graph;
|
|
1369
|
+
if (!g && config && source) {
|
|
1370
|
+
try { g = parseEntities(await source.fetchEntities(config)); } catch { g = null; }
|
|
1371
|
+
}
|
|
1372
|
+
if (!g) return null;
|
|
1373
|
+
const rows = memoryDir ? await factRows(memoryDir) : [];
|
|
1374
|
+
let composed;
|
|
1375
|
+
try { composed = composeConcept(g, term, { definition, factRows: rows }); }
|
|
1376
|
+
catch { return null; }
|
|
1377
|
+
if (!composed) return null;
|
|
1378
|
+
const rendered = tRender(templates, T_CONCEPT, {
|
|
1379
|
+
definition: composed.definition, examples: composed.examples, followups: composed.followups,
|
|
1380
|
+
});
|
|
1381
|
+
const text = rendered ?? `${composed.definition}\n${composed.examples}${composed.followups}`;
|
|
1382
|
+
const pending = composed.remainder && composed.remainder.length
|
|
1383
|
+
? { items: composed.remainder, noun: composed.noun }
|
|
1384
|
+
: null;
|
|
1385
|
+
return { text, instances: composed.instances, pending };
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1211
1388
|
/** A bare question → tmct_ask. When a focus is set AND the graph is in hand we
|
|
1212
1389
|
* call ask() directly to thread the focus as contextId (so a pronoun like "it"
|
|
1213
1390
|
* resolves to the focus) — building the SAME delimited string dispatchTool emits;
|
|
@@ -1270,6 +1447,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1270
1447
|
// orientation swap below is template wording, so those turns carry via:"template".
|
|
1271
1448
|
let via = "composed";
|
|
1272
1449
|
let recordMiss = miss;
|
|
1450
|
+
let factPending = null; // a truncated fact listing's held remainder (for "more" paging)
|
|
1273
1451
|
// MISS handling. The intent lanes + short-miss are RECOGNIZER-gated on the query
|
|
1274
1452
|
// text AND only consulted on a would-miss, so a real graph query — a hit, an honest
|
|
1275
1453
|
// empty with a receipt, a fuzzy repair — is never hijacked. Order: (1) META/SELF
|
|
@@ -1301,6 +1479,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1301
1479
|
answer = fact.replace ? fact.text : `${answer}\n${fact.text}`;
|
|
1302
1480
|
via = "fact";
|
|
1303
1481
|
recordMiss = false;
|
|
1482
|
+
if (fact.pending) factPending = fact.pending; // a truncated fact list → paginable remainder
|
|
1304
1483
|
} else if (miss) {
|
|
1305
1484
|
// W2: after the honest miss is composed, consult the folded-session memory. A
|
|
1306
1485
|
// relevant enough block ANSWERS — recalled Q/A framed + cited first, with the
|
|
@@ -1323,6 +1502,38 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1323
1502
|
const def = await curatedDefinitionAnswer(query, envelope, { memoryDir, lexicon });
|
|
1324
1503
|
if (def) { answer = def.text; via = "corpus/seon"; recordMiss = false; }
|
|
1325
1504
|
}
|
|
1505
|
+
// THE CONCEPT FORCE (concept.mjs) — a vague "what is a X" / "tell me about X" that
|
|
1506
|
+
// names a KNOWN code concept WITH real instances composes the three-band answer
|
|
1507
|
+
// (definition + real examples + pre-validated follow-ups), superseding the bare
|
|
1508
|
+
// schema-doc / curated-definition surface (both corpus-sourced). It declines unless
|
|
1509
|
+
// the term is a known, instance-bearing concept, so a precise query, an unknown
|
|
1510
|
+
// term, or an instance-less concept is never hijacked — the ordinary answer stands.
|
|
1511
|
+
// Runs after the corpus-fact/curated branches (via composed|corpus/seon) but not
|
|
1512
|
+
// over a "you told me" fact, a meta/self summary, or the conversational lanes.
|
|
1513
|
+
let conceptInstances = null;
|
|
1514
|
+
let conceptPending = null;
|
|
1515
|
+
if (via === "composed" || via === "corpus/seon") {
|
|
1516
|
+
const concept = await conceptForceAnswer(query, envelope, { graph, config, source, memoryDir, templates });
|
|
1517
|
+
if (concept) {
|
|
1518
|
+
answer = concept.text; via = "corpus/seon"; recordMiss = false;
|
|
1519
|
+
conceptInstances = concept.instances;
|
|
1520
|
+
conceptPending = concept.pending;
|
|
1521
|
+
} else {
|
|
1522
|
+
// THE RELATION CONCEPT FORCE — the noun force declined, so try the edge-kind
|
|
1523
|
+
// touch ("what about imports", "what are the calls", "tell me about contains").
|
|
1524
|
+
// Same three-band shape over real EDGES; declines unless the term is a known,
|
|
1525
|
+
// edge-bearing relation, so a precise query / unknown word is never hijacked.
|
|
1526
|
+
// For a "what about <relation>" this also SUPERSEDES the discourse rewrite's
|
|
1527
|
+
// dead-end (rewriting the prior question with a relation word rarely resolves) —
|
|
1528
|
+
// but only when the touched word is a relation concept; a real entity name in
|
|
1529
|
+
// "what about X" declines here and the discourse continuation stands.
|
|
1530
|
+
const relation = await relationForceAnswer(query, envelope, { graph, config, source, templates });
|
|
1531
|
+
if (relation) {
|
|
1532
|
+
answer = relation.text; via = "corpus/seon"; recordMiss = false;
|
|
1533
|
+
conceptPending = relation.pending;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1326
1537
|
// (4) #2 TEACH lane — a teach-shaped would-miss nothing above answered: route to
|
|
1327
1538
|
// memory, or say what CAN be remembered (LOUD), never the wall / a silent drop.
|
|
1328
1539
|
if (miss && recordMiss && via === "composed") {
|
|
@@ -1339,7 +1550,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1339
1550
|
// toward a real graph, unless it already points there. Only when genuinely empty.
|
|
1340
1551
|
if (recordMiss && (via === "composed" || via === "miss")
|
|
1341
1552
|
&& noCodeGraph(graph) && !/--repo|tmct init|no code graph/i.test(answer)) {
|
|
1342
|
-
answer = `${answer}\n(this repo has no code graph —
|
|
1553
|
+
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.)`;
|
|
1343
1554
|
}
|
|
1344
1555
|
// W5 (flag-gated, default OFF): an unknown-term miss may consult the LOCAL
|
|
1345
1556
|
// committed corpus slice — a hit APPENDS a grounded, licence-cited aside under
|
|
@@ -1351,11 +1562,23 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1351
1562
|
via = "corpus";
|
|
1352
1563
|
}
|
|
1353
1564
|
}
|
|
1354
|
-
|
|
1565
|
+
// The concept force answers WITH real example instances — those are the entities the
|
|
1566
|
+
// turn "asked about" (the SchemaClass meta-node is documentation, not a code entity),
|
|
1567
|
+
// so record + expand them, not the schema match.
|
|
1568
|
+
const finalAnsweredIds = conceptInstances ? conceptInstances.map((i) => i.id) : answeredIds;
|
|
1569
|
+
const record = { type: "turn", ts, query, via, resolvedIds, answeredIds: finalAnsweredIds, miss: recordMiss };
|
|
1355
1570
|
const logLines = [ts, `> ${query}`, answer, ""];
|
|
1356
1571
|
// `detail` feeds why/say-more's verbose re-render: the traversal receipt + the
|
|
1357
|
-
// matched entities the terse render trims (see renderVerbose).
|
|
1358
|
-
|
|
1572
|
+
// matched entities the terse render trims (see renderVerbose). `pending` carries a
|
|
1573
|
+
// truncated listing's held remainder for "more" paging — the concept/relation force
|
|
1574
|
+
// holds it on conceptPending (the relation force resolves no instance ids, so it can
|
|
1575
|
+
// still page even with an empty matches set); a fact listing holds it on factPending.
|
|
1576
|
+
const pending = conceptPending ?? factPending;
|
|
1577
|
+
const detail = conceptInstances
|
|
1578
|
+
? { traversal: envelope?.traversal || null, matches: conceptInstances, ...(pending ? { pending } : {}) }
|
|
1579
|
+
: (envelope
|
|
1580
|
+
? { traversal: envelope.traversal || null, matches: envelope.matches || [], ...(pending ? { pending } : {}) }
|
|
1581
|
+
: (pending ? { traversal: null, matches: [], pending } : null));
|
|
1359
1582
|
return { answer, logLines, record, focus: newFocus, detail };
|
|
1360
1583
|
}
|
|
1361
1584
|
|
|
@@ -1482,6 +1705,29 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null })
|
|
|
1482
1705
|
* subject), `answeredIds` the entity ids an ask answer cited; a slash-command turn
|
|
1483
1706
|
* also carries its `command` name. Both drive the mgx:asksAbout graph append.
|
|
1484
1707
|
*/
|
|
1708
|
+
// ---- "more" pagination — a long examples/facts listing shows the first PAGE
|
|
1709
|
+
// entries and holds the remainder on the turn's `last.detail.pending`; a bare
|
|
1710
|
+
// "more"/"show more"/"the rest" in the NEXT turn renders the next batch, advancing
|
|
1711
|
+
// the same pending state. Any other (real) query produces a fresh `last` without
|
|
1712
|
+
// `pending`, so the remainder is naturally cleared — no stale continuation. ----
|
|
1713
|
+
const PAGE = 32;
|
|
1714
|
+
const MORE_RE = /^(?:more|show more|see more|the rest|next|continue|go on)\b[.!?]*$/i;
|
|
1715
|
+
const joinList = (a) => (a.length > 1 ? `${a.slice(0, -1).join(", ")} and ${a[a.length - 1]}` : (a[0] ?? ""));
|
|
1716
|
+
|
|
1717
|
+
/** Render the next page of a held remainder (pending: {items:[str], noun}). Returns a
|
|
1718
|
+
* plain turn whose `detail.pending` carries what's still unseen (null when the batch
|
|
1719
|
+
* finished the list), so a follow-on "more" continues. */
|
|
1720
|
+
function morePage(query, { last, focus }) {
|
|
1721
|
+
const p = last.detail.pending;
|
|
1722
|
+
const batch = p.items.slice(0, PAGE);
|
|
1723
|
+
const rest = p.items.slice(PAGE);
|
|
1724
|
+
const tail = rest.length ? ` …and ${rest.length} more — say 'more' to see them.` : "";
|
|
1725
|
+
const answer = `${joinList(batch)}.${tail}`;
|
|
1726
|
+
const turn = plainTurn(query, answer, { via: "count", focus });
|
|
1727
|
+
turn.detail = { traversal: null, matches: [], ...(rest.length ? { pending: { items: rest, noun: p.noun } } : {}) };
|
|
1728
|
+
return turn;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1485
1731
|
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null } = {}) {
|
|
1486
1732
|
const line = String(input ?? "").trim();
|
|
1487
1733
|
const templates = await chatTemplates(); // failure-tolerated: null degrades, never throws
|
|
@@ -1511,6 +1757,13 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
1511
1757
|
const convo = conversationalTurn(line, ctx);
|
|
1512
1758
|
if (convo) return convo;
|
|
1513
1759
|
|
|
1760
|
+
// "more" — page the remainder of a previous long listing, if one is held. Gated on
|
|
1761
|
+
// an actual pending remainder so a bare "more" with nothing to continue falls through
|
|
1762
|
+
// to the ordinary path (an honest miss), never a pretend page.
|
|
1763
|
+
if (MORE_RE.test(line) && Array.isArray(last?.detail?.pending?.items) && last.detail.pending.items.length) {
|
|
1764
|
+
return withLast(morePage(line, ctx));
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1514
1767
|
if (line.startsWith("/")) return withLast(await runCommand(line, ctx));
|
|
1515
1768
|
// Declarative ACE sentences ("every module is a artifact") ASSERT into tmct's
|
|
1516
1769
|
// own memory and confirm — they are statements to remember, not graph queries.
|
|
@@ -1546,15 +1799,20 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
1546
1799
|
|
|
1547
1800
|
// ---- W3: seedMemory → bootstrap (first run in a graph-less repo) ----
|
|
1548
1801
|
|
|
1549
|
-
/**
|
|
1550
|
-
*
|
|
1551
|
-
*
|
|
1552
|
-
*
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1802
|
+
/** The first-run bootstrap seeds the WHOLE shipped ConceptNet band (no cap) — the
|
|
1803
|
+
* operator's "seed all 40k" call. `undefined` means seedMemory writes every
|
|
1804
|
+
* seedable fact in the committed slice (~6.3k). The batched appendFacts write
|
|
1805
|
+
* (src/memory/core.mjs) makes this a single O(N) pass — the full slice seeds in a
|
|
1806
|
+
* couple of seconds, inside a session-start budget, so it still runs synchronously
|
|
1807
|
+
* and complete. A finite value here would re-impose the old cap; keep it undefined
|
|
1808
|
+
* to mean "all". (Kept as a named export so init.mjs and tests share the intent.) */
|
|
1809
|
+
export const SEED_LIMIT = undefined;
|
|
1810
|
+
|
|
1811
|
+
/** Which predicates the seed lists FIRST (stable order — see seedMemory's `prefer`):
|
|
1812
|
+
* the definitional band leads, so the on-disk memory opens with the vocabulary that
|
|
1813
|
+
* answers "what is a cache?"-style questions rather than location trivia. With the
|
|
1814
|
+
* cap lifted this only sets ORDER (every fact seeds either way), but a well-ordered
|
|
1815
|
+
* memory keeps inspection and any future re-cap honest. */
|
|
1558
1816
|
export const SEED_PREFER = ["rdfs:subClassOf", "rdf:type", "mgx:usedFor", "mgx:partOf", "mgx:capableOf"];
|
|
1559
1817
|
|
|
1560
1818
|
/** The seed marker: its presence means this repo's memory already carries the
|
|
@@ -1653,7 +1911,15 @@ export async function createSession({
|
|
|
1653
1911
|
env = process.env,
|
|
1654
1912
|
cwd = process.cwd(),
|
|
1655
1913
|
gitRoot = gitToplevel,
|
|
1914
|
+
ephemeral = false,
|
|
1656
1915
|
} = {}) {
|
|
1916
|
+
// EPHEMERAL mode (--ephemeral, or TMCT_EPHEMERAL=1): read the target graph but
|
|
1917
|
+
// write NOTHING back into it. The shipped examples run this way so a demo never
|
|
1918
|
+
// dirties the committed code graph (`npm run example:mini` used to fold a session
|
|
1919
|
+
// into examples/*/.tmct/graph.json and rewrite it). We still read config.graphFile
|
|
1920
|
+
// for structure; only the WRITE base (logs, memory, sessions) is diverted to an OS
|
|
1921
|
+
// temp dir and the read-time graph upsert is suppressed.
|
|
1922
|
+
ephemeral = ephemeral || /^(1|true|yes)$/i.test(String(env.TMCT_EPHEMERAL || ""));
|
|
1657
1923
|
// Graph resolution order for the chat surface (documented; --repo wins):
|
|
1658
1924
|
// 1. --repo <path> → pins <path>/.tmct/graph.json (repo AND graph).
|
|
1659
1925
|
// 2. TMCT_GRAPH_FILE env → loads that graph anywhere (loadConfig reads it), so
|
|
@@ -1677,6 +1943,11 @@ export async function createSession({
|
|
|
1677
1943
|
config = envGraph ? loadConfig(env, cwd) : { graphFile: join(repo, DEFAULT_GRAPH_REL) };
|
|
1678
1944
|
}
|
|
1679
1945
|
|
|
1946
|
+
// Ephemeral: keep config.graphFile pointing at the READ graph, but divert the
|
|
1947
|
+
// write base (repo → logs/memory/sessions) to a throwaway temp dir. The committed
|
|
1948
|
+
// target is never touched; the demo's memory simply doesn't persist across runs.
|
|
1949
|
+
if (ephemeral) repo = await mkdtemp(join(tmpdir(), "tmct-ephemeral-"));
|
|
1950
|
+
|
|
1680
1951
|
// Load the graph once up front — the banner needs the module count, and focus/`it`
|
|
1681
1952
|
// resolution and contextId threading need it in hand. A missing artifact loads as
|
|
1682
1953
|
// the empty bootstrap graph (source.mjs) — the banner says so; never an error.
|
|
@@ -1723,6 +1994,7 @@ export async function createSession({
|
|
|
1723
1994
|
// artifact mid-session must degrade the recording, never kill the chat.
|
|
1724
1995
|
const turnRecords = [];
|
|
1725
1996
|
const upsertGraph = async (ended) => {
|
|
1997
|
+
if (ephemeral) return; // a demo/read-only session never writes back to the graph
|
|
1726
1998
|
if (!turnRecords.length) return; // a zero-turn session never pollutes the graph
|
|
1727
1999
|
try { await appendSessionToGraph(config.graphFile, { id: sessionId, started: startIso, ended, turns: turnRecords }); }
|
|
1728
2000
|
catch { /* best-effort — see above */ }
|
|
@@ -1752,8 +2024,10 @@ export async function createSession({
|
|
|
1752
2024
|
// the honest seed line appears ONLY on the run that actually seeded — the count
|
|
1753
2025
|
// is the TOTAL appended, split into the curated SEON ontology + the ConceptNet band.
|
|
1754
2026
|
...(seeded ? [`seeded ${seeded.appended} starter facts (${seeded.seon} curated SEON + ${seeded.conceptnet} ConceptNet) — /memory to inspect`] : []),
|
|
1755
|
-
// no code
|
|
1756
|
-
|
|
2027
|
+
// no code graph → point at how to GET one (a graph producer / --repo / the shipped
|
|
2028
|
+
// example), honest that `tmct init` seeds VOCABULARY, not a code graph, and at what
|
|
2029
|
+
// IS answerable now. tmct reads graphs; it never indexes code itself.
|
|
2030
|
+
...(noCodeGraph ? ['for code structure, point me at a .tmct/graph.json with --repo <path> or try `npm run example:mini` (tmct reads graphs, it doesn\'t index code); `tmct init` only seeds vocabulary — try "what is a cache"'] : []),
|
|
1757
2031
|
"pass --repo <path> to target a different repo",
|
|
1758
2032
|
"ask a question, or /help for commands (/stats for an overview) — /exit to leave",
|
|
1759
2033
|
];
|
|
@@ -1826,8 +2100,9 @@ export async function runChat({
|
|
|
1826
2100
|
env = process.env,
|
|
1827
2101
|
cwd = process.cwd(),
|
|
1828
2102
|
gitRoot = gitToplevel,
|
|
2103
|
+
ephemeral = false,
|
|
1829
2104
|
} = {}) {
|
|
1830
|
-
const session = await createSession({ repoPath, source, env, cwd, gitRoot });
|
|
2105
|
+
const session = await createSession({ repoPath, source, env, cwd, gitRoot, ephemeral });
|
|
1831
2106
|
|
|
1832
2107
|
const dim = (s) => (env.NO_COLOR || !output.isTTY ? s : `\x1b[2m${s}\x1b[0m`);
|
|
1833
2108
|
for (const line of session.bannerLines) output.write(dim(line) + "\n");
|