@polycode-projects/the-mechanical-code-talker 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +306 -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.0",
|
|
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,124 @@ 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
|
+
function conceptTermOf(query, envelope) {
|
|
1268
|
+
const base = metaTermOf(query, envelope);
|
|
1269
|
+
if (base) return base;
|
|
1270
|
+
const q = String(query).trim();
|
|
1271
|
+
const m = q.match(/^tell me about\s+(?:an?\s+)?(.+?)[?.!\s]*$/i)
|
|
1272
|
+
// "[and/so/…] what about X" with no good discourse continuation — the concept
|
|
1273
|
+
// KIND word is a concept touch, not a bare module lookup (DEAD-END 4). Gated
|
|
1274
|
+
// downstream by CONCEPT_CLASS/RELATION_TERM, so a real entity name declines here.
|
|
1275
|
+
|| q.match(/^(?:(?:and|so|but|ok|okay|now|then)\s+)*what about\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i);
|
|
1276
|
+
return m ? m[1].trim() : null;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/** The RELATION term a vague touch names — reuses conceptTermOf's shapes ("what is
|
|
1280
|
+
* X"/"what does X mean"/"tell me about X"/"what about X"), plus the relation-only
|
|
1281
|
+
* openers the graph parser reads as something else: "what are the imports", "what
|
|
1282
|
+
* calls are there", "what is calling". Null when the line isn't such a touch. Gated
|
|
1283
|
+
* downstream by RELATION_TERM, so this only has to recognize the SHAPE. */
|
|
1284
|
+
function relationTermOf(query, envelope) {
|
|
1285
|
+
const base = conceptTermOf(query, envelope);
|
|
1286
|
+
if (base) return base;
|
|
1287
|
+
const q = String(query).trim().toLowerCase().replace(/[?.!]+$/, "").replace(/\s+/g, " ");
|
|
1288
|
+
let m;
|
|
1289
|
+
// "what are the imports", "what is the containment", "what are all the calls"
|
|
1290
|
+
if ((m = q.match(/^what\s+(?:are|is)\s+(?:all\s+)?(?:the\s+)?([a-z][a-z-]*?)(?:\s+(?:edges|relationships|relations))?$/))) return m[1];
|
|
1291
|
+
// "what calls are there", "what imports are there"
|
|
1292
|
+
if ((m = q.match(/^what\s+([a-z][a-z-]*?)\s+are\s+there$/))) return m[1];
|
|
1293
|
+
// "what is calling", "what is importing" (bare gerund, no object)
|
|
1294
|
+
if ((m = q.match(/^what\s+(?:is|are)\s+([a-z][a-z-]*ing)$/))) return m[1];
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/** THE RELATION CONCEPT FORCE — compose the three-band answer (curated relation
|
|
1299
|
+
* definition + real example EDGES + pre-validated follow-ups) for a vague touch on a
|
|
1300
|
+
* relation/edge kind ("what about imports", "what are the calls", "tell me about
|
|
1301
|
+
* contains"), or null when it isn't one: not a recognizable relation touch, not a
|
|
1302
|
+
* known edge concept (RELATION_TERM), no curated definition, or the graph has NO
|
|
1303
|
+
* edges of that kind (composeRelation's own honest-miss gate). Loads the definition
|
|
1304
|
+
* from the shipped corpus/seon/relations.jsonl, so it works without per-repo memory
|
|
1305
|
+
* seeding. Lazy + failure-tolerated throughout. Returns { text, pending }. */
|
|
1306
|
+
async function relationForceAnswer(query, envelope, { graph, config, source, templates }) {
|
|
1307
|
+
const rawTerm = relationTermOf(query, envelope);
|
|
1308
|
+
if (!rawTerm) return null;
|
|
1309
|
+
let composeRelation; let RELATION_TERM;
|
|
1310
|
+
try { ({ composeRelation, RELATION_TERM } = await import("./concept.mjs")); }
|
|
1311
|
+
catch { return null; }
|
|
1312
|
+
const term = String(rawTerm).toLowerCase();
|
|
1313
|
+
if (!RELATION_TERM[term]) return null; // not an enumerable edge concept — ordinary path owns it
|
|
1314
|
+
const definition = (await relationDefinitions()).get(RELATION_TERM[term]) ?? null;
|
|
1315
|
+
if (!definition) return null;
|
|
1316
|
+
// Same graph-load fallback as conceptForceAnswer: the shell hands the loaded graph
|
|
1317
|
+
// straight in; the pure runTurn(config) path loads it the way dispatchTool does.
|
|
1318
|
+
let g = graph;
|
|
1319
|
+
if (!g && config && source) {
|
|
1320
|
+
try { g = parseEntities(await source.fetchEntities(config)); } catch { g = null; }
|
|
1321
|
+
}
|
|
1322
|
+
if (!g) return null;
|
|
1323
|
+
let composed;
|
|
1324
|
+
try { composed = composeRelation(g, term, { definition }); }
|
|
1325
|
+
catch { return null; }
|
|
1326
|
+
if (!composed) return null;
|
|
1327
|
+
const rendered = tRender(templates, T_CONCEPT, {
|
|
1328
|
+
definition: composed.definition, examples: composed.examples, followups: composed.followups,
|
|
1329
|
+
});
|
|
1330
|
+
const text = rendered ?? `${composed.definition}\n${composed.examples}${composed.followups}`;
|
|
1331
|
+
const pending = composed.remainder && composed.remainder.length
|
|
1332
|
+
? { items: composed.remainder, noun: composed.noun }
|
|
1333
|
+
: null;
|
|
1334
|
+
return { text, pending };
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/** THE CONCEPT FORCE — compose the three-band answer (corpus/seon definition + real
|
|
1338
|
+
* graph/memory instances + pre-validated follow-ups) for a vague concept touch, or
|
|
1339
|
+
* null when it isn't one: not a "what is a X"/"tell me about X" shape, not a known
|
|
1340
|
+
* enumerable concept (CONCEPT_CLASS), no curated definition, or NO instances anywhere
|
|
1341
|
+
* (composeConcept's own honest-miss gate). Loads the definition DIRECTLY from the
|
|
1342
|
+
* shipped corpus/seon file (seonDefinitions), so it works without per-repo memory
|
|
1343
|
+
* seeding; the memory fact rows only ADD remembered "A is a X" examples when present.
|
|
1344
|
+
* Lazy + failure-tolerated throughout (chat.mjs ethos). Returns { text, instances }. */
|
|
1345
|
+
async function conceptForceAnswer(query, envelope, { graph, config, source, memoryDir, templates }) {
|
|
1346
|
+
const rawTerm = conceptTermOf(query, envelope);
|
|
1347
|
+
if (!rawTerm) return null;
|
|
1348
|
+
let normFactTerm; let composeConcept; let CONCEPT_CLASS;
|
|
1349
|
+
try {
|
|
1350
|
+
({ normFactTerm } = await import("./memory/core.mjs"));
|
|
1351
|
+
({ composeConcept, CONCEPT_CLASS } = await import("./concept.mjs"));
|
|
1352
|
+
} catch { return null; }
|
|
1353
|
+
const term = normFactTerm(rawTerm);
|
|
1354
|
+
if (!CONCEPT_CLASS[term]) return null; // not an enumerable code concept — ordinary path owns it
|
|
1355
|
+
const definition = (await seonDefinitions()).get(term) ?? null;
|
|
1356
|
+
if (!definition) return null;
|
|
1357
|
+
// The runChat shell hands the loaded graph straight in; the pure runTurn(config)
|
|
1358
|
+
// path (tests, chatbench) does not, so load it the same way dispatchTool does when
|
|
1359
|
+
// it's missing. Failure-tolerated: no loadable graph → no concept force.
|
|
1360
|
+
let g = graph;
|
|
1361
|
+
if (!g && config && source) {
|
|
1362
|
+
try { g = parseEntities(await source.fetchEntities(config)); } catch { g = null; }
|
|
1363
|
+
}
|
|
1364
|
+
if (!g) return null;
|
|
1365
|
+
const rows = memoryDir ? await factRows(memoryDir) : [];
|
|
1366
|
+
let composed;
|
|
1367
|
+
try { composed = composeConcept(g, term, { definition, factRows: rows }); }
|
|
1368
|
+
catch { return null; }
|
|
1369
|
+
if (!composed) return null;
|
|
1370
|
+
const rendered = tRender(templates, T_CONCEPT, {
|
|
1371
|
+
definition: composed.definition, examples: composed.examples, followups: composed.followups,
|
|
1372
|
+
});
|
|
1373
|
+
const text = rendered ?? `${composed.definition}\n${composed.examples}${composed.followups}`;
|
|
1374
|
+
const pending = composed.remainder && composed.remainder.length
|
|
1375
|
+
? { items: composed.remainder, noun: composed.noun }
|
|
1376
|
+
: null;
|
|
1377
|
+
return { text, instances: composed.instances, pending };
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1211
1380
|
/** A bare question → tmct_ask. When a focus is set AND the graph is in hand we
|
|
1212
1381
|
* call ask() directly to thread the focus as contextId (so a pronoun like "it"
|
|
1213
1382
|
* resolves to the focus) — building the SAME delimited string dispatchTool emits;
|
|
@@ -1270,6 +1439,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1270
1439
|
// orientation swap below is template wording, so those turns carry via:"template".
|
|
1271
1440
|
let via = "composed";
|
|
1272
1441
|
let recordMiss = miss;
|
|
1442
|
+
let factPending = null; // a truncated fact listing's held remainder (for "more" paging)
|
|
1273
1443
|
// MISS handling. The intent lanes + short-miss are RECOGNIZER-gated on the query
|
|
1274
1444
|
// text AND only consulted on a would-miss, so a real graph query — a hit, an honest
|
|
1275
1445
|
// empty with a receipt, a fuzzy repair — is never hijacked. Order: (1) META/SELF
|
|
@@ -1301,6 +1471,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1301
1471
|
answer = fact.replace ? fact.text : `${answer}\n${fact.text}`;
|
|
1302
1472
|
via = "fact";
|
|
1303
1473
|
recordMiss = false;
|
|
1474
|
+
if (fact.pending) factPending = fact.pending; // a truncated fact list → paginable remainder
|
|
1304
1475
|
} else if (miss) {
|
|
1305
1476
|
// W2: after the honest miss is composed, consult the folded-session memory. A
|
|
1306
1477
|
// relevant enough block ANSWERS — recalled Q/A framed + cited first, with the
|
|
@@ -1323,6 +1494,38 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1323
1494
|
const def = await curatedDefinitionAnswer(query, envelope, { memoryDir, lexicon });
|
|
1324
1495
|
if (def) { answer = def.text; via = "corpus/seon"; recordMiss = false; }
|
|
1325
1496
|
}
|
|
1497
|
+
// THE CONCEPT FORCE (concept.mjs) — a vague "what is a X" / "tell me about X" that
|
|
1498
|
+
// names a KNOWN code concept WITH real instances composes the three-band answer
|
|
1499
|
+
// (definition + real examples + pre-validated follow-ups), superseding the bare
|
|
1500
|
+
// schema-doc / curated-definition surface (both corpus-sourced). It declines unless
|
|
1501
|
+
// the term is a known, instance-bearing concept, so a precise query, an unknown
|
|
1502
|
+
// term, or an instance-less concept is never hijacked — the ordinary answer stands.
|
|
1503
|
+
// Runs after the corpus-fact/curated branches (via composed|corpus/seon) but not
|
|
1504
|
+
// over a "you told me" fact, a meta/self summary, or the conversational lanes.
|
|
1505
|
+
let conceptInstances = null;
|
|
1506
|
+
let conceptPending = null;
|
|
1507
|
+
if (via === "composed" || via === "corpus/seon") {
|
|
1508
|
+
const concept = await conceptForceAnswer(query, envelope, { graph, config, source, memoryDir, templates });
|
|
1509
|
+
if (concept) {
|
|
1510
|
+
answer = concept.text; via = "corpus/seon"; recordMiss = false;
|
|
1511
|
+
conceptInstances = concept.instances;
|
|
1512
|
+
conceptPending = concept.pending;
|
|
1513
|
+
} else {
|
|
1514
|
+
// THE RELATION CONCEPT FORCE — the noun force declined, so try the edge-kind
|
|
1515
|
+
// touch ("what about imports", "what are the calls", "tell me about contains").
|
|
1516
|
+
// Same three-band shape over real EDGES; declines unless the term is a known,
|
|
1517
|
+
// edge-bearing relation, so a precise query / unknown word is never hijacked.
|
|
1518
|
+
// For a "what about <relation>" this also SUPERSEDES the discourse rewrite's
|
|
1519
|
+
// dead-end (rewriting the prior question with a relation word rarely resolves) —
|
|
1520
|
+
// but only when the touched word is a relation concept; a real entity name in
|
|
1521
|
+
// "what about X" declines here and the discourse continuation stands.
|
|
1522
|
+
const relation = await relationForceAnswer(query, envelope, { graph, config, source, templates });
|
|
1523
|
+
if (relation) {
|
|
1524
|
+
answer = relation.text; via = "corpus/seon"; recordMiss = false;
|
|
1525
|
+
conceptPending = relation.pending;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1326
1529
|
// (4) #2 TEACH lane — a teach-shaped would-miss nothing above answered: route to
|
|
1327
1530
|
// memory, or say what CAN be remembered (LOUD), never the wall / a silent drop.
|
|
1328
1531
|
if (miss && recordMiss && via === "composed") {
|
|
@@ -1339,7 +1542,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1339
1542
|
// toward a real graph, unless it already points there. Only when genuinely empty.
|
|
1340
1543
|
if (recordMiss && (via === "composed" || via === "miss")
|
|
1341
1544
|
&& noCodeGraph(graph) && !/--repo|tmct init|no code graph/i.test(answer)) {
|
|
1342
|
-
answer = `${answer}\n(this repo has no code graph —
|
|
1545
|
+
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
1546
|
}
|
|
1344
1547
|
// W5 (flag-gated, default OFF): an unknown-term miss may consult the LOCAL
|
|
1345
1548
|
// committed corpus slice — a hit APPENDS a grounded, licence-cited aside under
|
|
@@ -1351,11 +1554,23 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1351
1554
|
via = "corpus";
|
|
1352
1555
|
}
|
|
1353
1556
|
}
|
|
1354
|
-
|
|
1557
|
+
// The concept force answers WITH real example instances — those are the entities the
|
|
1558
|
+
// turn "asked about" (the SchemaClass meta-node is documentation, not a code entity),
|
|
1559
|
+
// so record + expand them, not the schema match.
|
|
1560
|
+
const finalAnsweredIds = conceptInstances ? conceptInstances.map((i) => i.id) : answeredIds;
|
|
1561
|
+
const record = { type: "turn", ts, query, via, resolvedIds, answeredIds: finalAnsweredIds, miss: recordMiss };
|
|
1355
1562
|
const logLines = [ts, `> ${query}`, answer, ""];
|
|
1356
1563
|
// `detail` feeds why/say-more's verbose re-render: the traversal receipt + the
|
|
1357
|
-
// matched entities the terse render trims (see renderVerbose).
|
|
1358
|
-
|
|
1564
|
+
// matched entities the terse render trims (see renderVerbose). `pending` carries a
|
|
1565
|
+
// truncated listing's held remainder for "more" paging — the concept/relation force
|
|
1566
|
+
// holds it on conceptPending (the relation force resolves no instance ids, so it can
|
|
1567
|
+
// still page even with an empty matches set); a fact listing holds it on factPending.
|
|
1568
|
+
const pending = conceptPending ?? factPending;
|
|
1569
|
+
const detail = conceptInstances
|
|
1570
|
+
? { traversal: envelope?.traversal || null, matches: conceptInstances, ...(pending ? { pending } : {}) }
|
|
1571
|
+
: (envelope
|
|
1572
|
+
? { traversal: envelope.traversal || null, matches: envelope.matches || [], ...(pending ? { pending } : {}) }
|
|
1573
|
+
: (pending ? { traversal: null, matches: [], pending } : null));
|
|
1359
1574
|
return { answer, logLines, record, focus: newFocus, detail };
|
|
1360
1575
|
}
|
|
1361
1576
|
|
|
@@ -1482,6 +1697,29 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null })
|
|
|
1482
1697
|
* subject), `answeredIds` the entity ids an ask answer cited; a slash-command turn
|
|
1483
1698
|
* also carries its `command` name. Both drive the mgx:asksAbout graph append.
|
|
1484
1699
|
*/
|
|
1700
|
+
// ---- "more" pagination — a long examples/facts listing shows the first PAGE
|
|
1701
|
+
// entries and holds the remainder on the turn's `last.detail.pending`; a bare
|
|
1702
|
+
// "more"/"show more"/"the rest" in the NEXT turn renders the next batch, advancing
|
|
1703
|
+
// the same pending state. Any other (real) query produces a fresh `last` without
|
|
1704
|
+
// `pending`, so the remainder is naturally cleared — no stale continuation. ----
|
|
1705
|
+
const PAGE = 32;
|
|
1706
|
+
const MORE_RE = /^(?:more|show more|see more|the rest|next|continue|go on)\b[.!?]*$/i;
|
|
1707
|
+
const joinList = (a) => (a.length > 1 ? `${a.slice(0, -1).join(", ")} and ${a[a.length - 1]}` : (a[0] ?? ""));
|
|
1708
|
+
|
|
1709
|
+
/** Render the next page of a held remainder (pending: {items:[str], noun}). Returns a
|
|
1710
|
+
* plain turn whose `detail.pending` carries what's still unseen (null when the batch
|
|
1711
|
+
* finished the list), so a follow-on "more" continues. */
|
|
1712
|
+
function morePage(query, { last, focus }) {
|
|
1713
|
+
const p = last.detail.pending;
|
|
1714
|
+
const batch = p.items.slice(0, PAGE);
|
|
1715
|
+
const rest = p.items.slice(PAGE);
|
|
1716
|
+
const tail = rest.length ? ` …and ${rest.length} more — say 'more' to see them.` : "";
|
|
1717
|
+
const answer = `${joinList(batch)}.${tail}`;
|
|
1718
|
+
const turn = plainTurn(query, answer, { via: "count", focus });
|
|
1719
|
+
turn.detail = { traversal: null, matches: [], ...(rest.length ? { pending: { items: rest, noun: p.noun } } : {}) };
|
|
1720
|
+
return turn;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1485
1723
|
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null } = {}) {
|
|
1486
1724
|
const line = String(input ?? "").trim();
|
|
1487
1725
|
const templates = await chatTemplates(); // failure-tolerated: null degrades, never throws
|
|
@@ -1511,6 +1749,13 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
1511
1749
|
const convo = conversationalTurn(line, ctx);
|
|
1512
1750
|
if (convo) return convo;
|
|
1513
1751
|
|
|
1752
|
+
// "more" — page the remainder of a previous long listing, if one is held. Gated on
|
|
1753
|
+
// an actual pending remainder so a bare "more" with nothing to continue falls through
|
|
1754
|
+
// to the ordinary path (an honest miss), never a pretend page.
|
|
1755
|
+
if (MORE_RE.test(line) && Array.isArray(last?.detail?.pending?.items) && last.detail.pending.items.length) {
|
|
1756
|
+
return withLast(morePage(line, ctx));
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1514
1759
|
if (line.startsWith("/")) return withLast(await runCommand(line, ctx));
|
|
1515
1760
|
// Declarative ACE sentences ("every module is a artifact") ASSERT into tmct's
|
|
1516
1761
|
// own memory and confirm — they are statements to remember, not graph queries.
|
|
@@ -1546,15 +1791,20 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
1546
1791
|
|
|
1547
1792
|
// ---- W3: seedMemory → bootstrap (first run in a graph-less repo) ----
|
|
1548
1793
|
|
|
1549
|
-
/**
|
|
1550
|
-
*
|
|
1551
|
-
*
|
|
1552
|
-
*
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1794
|
+
/** The first-run bootstrap seeds the WHOLE shipped ConceptNet band (no cap) — the
|
|
1795
|
+
* operator's "seed all 40k" call. `undefined` means seedMemory writes every
|
|
1796
|
+
* seedable fact in the committed slice (~6.3k). The batched appendFacts write
|
|
1797
|
+
* (src/memory/core.mjs) makes this a single O(N) pass — the full slice seeds in a
|
|
1798
|
+
* couple of seconds, inside a session-start budget, so it still runs synchronously
|
|
1799
|
+
* and complete. A finite value here would re-impose the old cap; keep it undefined
|
|
1800
|
+
* to mean "all". (Kept as a named export so init.mjs and tests share the intent.) */
|
|
1801
|
+
export const SEED_LIMIT = undefined;
|
|
1802
|
+
|
|
1803
|
+
/** Which predicates the seed lists FIRST (stable order — see seedMemory's `prefer`):
|
|
1804
|
+
* the definitional band leads, so the on-disk memory opens with the vocabulary that
|
|
1805
|
+
* answers "what is a cache?"-style questions rather than location trivia. With the
|
|
1806
|
+
* cap lifted this only sets ORDER (every fact seeds either way), but a well-ordered
|
|
1807
|
+
* memory keeps inspection and any future re-cap honest. */
|
|
1558
1808
|
export const SEED_PREFER = ["rdfs:subClassOf", "rdf:type", "mgx:usedFor", "mgx:partOf", "mgx:capableOf"];
|
|
1559
1809
|
|
|
1560
1810
|
/** The seed marker: its presence means this repo's memory already carries the
|
|
@@ -1653,7 +1903,15 @@ export async function createSession({
|
|
|
1653
1903
|
env = process.env,
|
|
1654
1904
|
cwd = process.cwd(),
|
|
1655
1905
|
gitRoot = gitToplevel,
|
|
1906
|
+
ephemeral = false,
|
|
1656
1907
|
} = {}) {
|
|
1908
|
+
// EPHEMERAL mode (--ephemeral, or TMCT_EPHEMERAL=1): read the target graph but
|
|
1909
|
+
// write NOTHING back into it. The shipped examples run this way so a demo never
|
|
1910
|
+
// dirties the committed code graph (`npm run example:mini` used to fold a session
|
|
1911
|
+
// into examples/*/.tmct/graph.json and rewrite it). We still read config.graphFile
|
|
1912
|
+
// for structure; only the WRITE base (logs, memory, sessions) is diverted to an OS
|
|
1913
|
+
// temp dir and the read-time graph upsert is suppressed.
|
|
1914
|
+
ephemeral = ephemeral || /^(1|true|yes)$/i.test(String(env.TMCT_EPHEMERAL || ""));
|
|
1657
1915
|
// Graph resolution order for the chat surface (documented; --repo wins):
|
|
1658
1916
|
// 1. --repo <path> → pins <path>/.tmct/graph.json (repo AND graph).
|
|
1659
1917
|
// 2. TMCT_GRAPH_FILE env → loads that graph anywhere (loadConfig reads it), so
|
|
@@ -1677,6 +1935,11 @@ export async function createSession({
|
|
|
1677
1935
|
config = envGraph ? loadConfig(env, cwd) : { graphFile: join(repo, DEFAULT_GRAPH_REL) };
|
|
1678
1936
|
}
|
|
1679
1937
|
|
|
1938
|
+
// Ephemeral: keep config.graphFile pointing at the READ graph, but divert the
|
|
1939
|
+
// write base (repo → logs/memory/sessions) to a throwaway temp dir. The committed
|
|
1940
|
+
// target is never touched; the demo's memory simply doesn't persist across runs.
|
|
1941
|
+
if (ephemeral) repo = await mkdtemp(join(tmpdir(), "tmct-ephemeral-"));
|
|
1942
|
+
|
|
1680
1943
|
// Load the graph once up front — the banner needs the module count, and focus/`it`
|
|
1681
1944
|
// resolution and contextId threading need it in hand. A missing artifact loads as
|
|
1682
1945
|
// the empty bootstrap graph (source.mjs) — the banner says so; never an error.
|
|
@@ -1723,6 +1986,7 @@ export async function createSession({
|
|
|
1723
1986
|
// artifact mid-session must degrade the recording, never kill the chat.
|
|
1724
1987
|
const turnRecords = [];
|
|
1725
1988
|
const upsertGraph = async (ended) => {
|
|
1989
|
+
if (ephemeral) return; // a demo/read-only session never writes back to the graph
|
|
1726
1990
|
if (!turnRecords.length) return; // a zero-turn session never pollutes the graph
|
|
1727
1991
|
try { await appendSessionToGraph(config.graphFile, { id: sessionId, started: startIso, ended, turns: turnRecords }); }
|
|
1728
1992
|
catch { /* best-effort — see above */ }
|
|
@@ -1752,8 +2016,10 @@ export async function createSession({
|
|
|
1752
2016
|
// the honest seed line appears ONLY on the run that actually seeded — the count
|
|
1753
2017
|
// is the TOTAL appended, split into the curated SEON ontology + the ConceptNet band.
|
|
1754
2018
|
...(seeded ? [`seeded ${seeded.appended} starter facts (${seeded.seon} curated SEON + ${seeded.conceptnet} ConceptNet) — /memory to inspect`] : []),
|
|
1755
|
-
// no code
|
|
1756
|
-
|
|
2019
|
+
// no code graph → point at how to GET one (a graph producer / --repo / the shipped
|
|
2020
|
+
// example), honest that `tmct init` seeds VOCABULARY, not a code graph, and at what
|
|
2021
|
+
// IS answerable now. tmct reads graphs; it never indexes code itself.
|
|
2022
|
+
...(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
2023
|
"pass --repo <path> to target a different repo",
|
|
1758
2024
|
"ask a question, or /help for commands (/stats for an overview) — /exit to leave",
|
|
1759
2025
|
];
|
|
@@ -1826,8 +2092,9 @@ export async function runChat({
|
|
|
1826
2092
|
env = process.env,
|
|
1827
2093
|
cwd = process.cwd(),
|
|
1828
2094
|
gitRoot = gitToplevel,
|
|
2095
|
+
ephemeral = false,
|
|
1829
2096
|
} = {}) {
|
|
1830
|
-
const session = await createSession({ repoPath, source, env, cwd, gitRoot });
|
|
2097
|
+
const session = await createSession({ repoPath, source, env, cwd, gitRoot, ephemeral });
|
|
1831
2098
|
|
|
1832
2099
|
const dim = (s) => (env.NO_COLOR || !output.isTTY ? s : `\x1b[2m${s}\x1b[0m`);
|
|
1833
2100
|
for (const line of session.bannerLines) output.write(dim(line) + "\n");
|