@polycode-projects/the-mechanical-code-talker 0.5.0 → 0.6.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 +60 -24
- package/bin/tmct.mjs +56 -0
- package/corpus/README.md +77 -5
- package/corpus/conceptnet/README.md +54 -2
- package/corpus/conceptnet/quality-filter.mjs +95 -0
- package/corpus/conceptnet/slice.jsonl +0 -378
- package/corpus/seon/LICENSE-NOTICE +37 -0
- package/corpus/seon/README.md +121 -0
- package/corpus/seon/concepts.jsonl +238 -0
- package/corpus/seon/definitions.jsonl +288 -0
- package/corpus/tier2/aws.jsonl +39 -0
- package/corpus/tier2/generate.mjs +253 -0
- package/corpus/tier2/java.jsonl +31 -0
- package/corpus/tier2/manifest.json +48 -0
- package/corpus/tier2/python.jsonl +30 -0
- package/data/templates/responses.jsonl +2 -0
- package/package.json +10 -2
- package/src/ask-vocab.mjs +19 -1
- package/src/ask.mjs +11 -11
- package/src/chat.mjs +459 -33
- package/src/codegraph.mjs +28 -5
- package/src/conformance.mjs +166 -0
- package/src/corpus/conceptnet.mjs +24 -6
- package/src/memory/inspect.mjs +25 -0
- package/src/server.mjs +88 -7
package/src/chat.mjs
CHANGED
|
@@ -104,6 +104,37 @@ export const COMMANDS = {
|
|
|
104
104
|
* back to the focus for, and that update the focus on a successful resolve. */
|
|
105
105
|
const ENTITY_ARGS = new Set(["symbol", "module", "class"]);
|
|
106
106
|
|
|
107
|
+
/** System-command words that a forgiving shell accepts WITHOUT the leading "/":
|
|
108
|
+
* `stats`, `memory`, `describe X`, `members X`, … all work bare. "help" is left
|
|
109
|
+
* out on purpose — bare "help" stays the friendly orientation; "/help" is the
|
|
110
|
+
* full command list. */
|
|
111
|
+
const COMMAND_WORDS = new Set(["stats", "memory", "focus", ...Object.keys(COMMANDS)]);
|
|
112
|
+
|
|
113
|
+
/** Query connectives that mark a line as a COMPOSITIONAL question the ask engine
|
|
114
|
+
* should own, even when it happens to start with a command word ("untested modules
|
|
115
|
+
* IMPORTING x", "find functions THAT CALL y"). Their presence blocks slash-routing. */
|
|
116
|
+
const QUERY_CONNECTIVES = /\b(that|which|and|or|imports?|importing|calls?|calling|uses?|using|covers?|covering|tests?|testing|touch(?:es|ed|ing)?|inherits?|of|with|from|into|by|most|least)\b/i;
|
|
117
|
+
|
|
118
|
+
/** A bare leading command word → its slash form ("stats" → "/stats", "describe x"
|
|
119
|
+
* → "/describe x"), so the system commands are slash-optional. Conservative on the
|
|
120
|
+
* entity/arg commands: it routes a bare word or a SHORT name-like argument, but
|
|
121
|
+
* falls through (returns null) for a multi-word compositional query so the ask
|
|
122
|
+
* engine still owns things like "untested modules importing a.mjs". Returns null
|
|
123
|
+
* when the first token is not a command word. */
|
|
124
|
+
export function asBareCommand(line) {
|
|
125
|
+
const trimmed = String(line || "").trim();
|
|
126
|
+
if (!trimmed || trimmed.startsWith("/")) return null;
|
|
127
|
+
const [first, ...restTok] = trimmed.split(/\s+/);
|
|
128
|
+
if (!COMMAND_WORDS.has(first.toLowerCase())) return null;
|
|
129
|
+
const rest = restTok.join(" ");
|
|
130
|
+
// Zero-arg system commands are always the command; a bare command word is too.
|
|
131
|
+
if (!rest || first.toLowerCase() === "stats" || first.toLowerCase() === "memory") return `/${trimmed}`;
|
|
132
|
+
// Arg commands: route only a short, name-like argument (no query connectives),
|
|
133
|
+
// so "describe Widget" / "members my class" route but a compositional query does not.
|
|
134
|
+
if (restTok.length <= 3 && !QUERY_CONNECTIVES.test(rest)) return `/${trimmed}`;
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
107
138
|
// ---- aggregate / count queries — answered MECHANICALLY off the loaded graph
|
|
108
139
|
// header (individuals grouped by class, relation groups by predicate), not by
|
|
109
140
|
// dispatching to the ask engine. Deterministic, fully in-ethos. ----
|
|
@@ -191,6 +222,45 @@ async function countFromFacts(graph, memoryDir, query) {
|
|
|
191
222
|
return null;
|
|
192
223
|
}
|
|
193
224
|
|
|
225
|
+
// ---- memory-store counts (the .tmct/memory graph, distinct from the code graph
|
|
226
|
+
// answerCount reads) — so "how many facts do you know" is answerable, consistent
|
|
227
|
+
// with what `/memory` advertises. The code graph owns the structural kinds
|
|
228
|
+
// (classes/functions/modules/…); the memory store owns Facts + Utterances. Sessions
|
|
229
|
+
// stay with answerCount (chat writes Session individuals into the code graph as
|
|
230
|
+
// first-class temporal data — see sessions.mjs), so this never shadows them. ----
|
|
231
|
+
|
|
232
|
+
/** Nouns that name a MEMORY-STORE individual class, → the class to count. */
|
|
233
|
+
const MEMORY_COUNT_NOUNS = {
|
|
234
|
+
fact: "Fact", facts: "Fact",
|
|
235
|
+
utterance: "Utterance", utterances: "Utterance", said: "Utterance",
|
|
236
|
+
};
|
|
237
|
+
const MEMORY_CLASS_LABELS = { Fact: ["fact", "facts"], Utterance: ["utterance", "utterances"] };
|
|
238
|
+
|
|
239
|
+
/** Recognise a memory-store count question and answer it by loading the memory
|
|
240
|
+
* graph, or null (→ answerCount / the ask engine own it). Handles "how many facts",
|
|
241
|
+
* "how many utterances", and the bare "how many do you know" (→ facts). Lazy +
|
|
242
|
+
* failure-tolerated: no memory / a broken store → null, so the honest fall-through
|
|
243
|
+
* stands. */
|
|
244
|
+
async function answerMemoryCount(memoryDir, query) {
|
|
245
|
+
if (!memoryDir) return null;
|
|
246
|
+
const q = String(query).toLowerCase();
|
|
247
|
+
let cls = null;
|
|
248
|
+
// the bare "how many do you know" (no explicit noun) defaults to remembered facts
|
|
249
|
+
if (/\bhow many(?:\s+(?:things?|facts?))?\s+(?:do|d'?)\s+(?:you|u)\s+know\b/.test(q)) cls = "Fact";
|
|
250
|
+
if (!cls) {
|
|
251
|
+
const m = q.match(/\b(?:how many|number of|count(?:\s+the)?)\s+([a-z]+)\b/);
|
|
252
|
+
if (m) cls = MEMORY_COUNT_NOUNS[m[1]] || null;
|
|
253
|
+
}
|
|
254
|
+
if (!cls) return null;
|
|
255
|
+
let loadMemory;
|
|
256
|
+
try { ({ loadMemory } = await import("./memory/core.mjs")); } catch { return null; }
|
|
257
|
+
let mem;
|
|
258
|
+
try { mem = await loadMemory(memoryDir); } catch { return null; }
|
|
259
|
+
const n = (mem.individuals || []).filter((i) => (i.class || "") === cls).length;
|
|
260
|
+
const [sing, plur] = MEMORY_CLASS_LABELS[cls];
|
|
261
|
+
return `${n} ${n === 1 ? sing : plur}.`;
|
|
262
|
+
}
|
|
263
|
+
|
|
194
264
|
/** `/stats`: a one-screen overview of the graph — class counts, relationship
|
|
195
265
|
* (predicate) counts, and module/package totals — read straight off the header. */
|
|
196
266
|
export function renderStats(graph) {
|
|
@@ -269,6 +339,12 @@ const T_THANKS = "conversational-thanks";
|
|
|
269
339
|
const T_FAREWELL = "conversational-farewell";
|
|
270
340
|
const T_ORIENTATION = "orientation-friendly";
|
|
271
341
|
const T_WHY_EMPTY = "miss-no-previous-answer";
|
|
342
|
+
/** Empty / degenerate-graph variants (#3/#5): shown when the loaded graph has 0
|
|
343
|
+
* modules (a graph-less bootstrap OR a graph.json with no code entities). They
|
|
344
|
+
* orient toward `--repo`/`tmct init` + the seeded vocabulary instead of
|
|
345
|
+
* over-promising "ask me about this codebase". */
|
|
346
|
+
const T_GREETING_EMPTY = "conversational-greeting-empty";
|
|
347
|
+
const T_ORIENTATION_EMPTY = "orientation-empty";
|
|
272
348
|
|
|
273
349
|
/** The degraded line when the template library itself cannot load — a packaging
|
|
274
350
|
* failure said out loud, never a crashed turn or a silently different answer. */
|
|
@@ -369,9 +445,188 @@ function conversationalTurn(line, ctx) {
|
|
|
369
445
|
if (v.empty) return mk(tRender(ctx.templates, T_WHY_EMPTY) ?? v.text, { miss: true });
|
|
370
446
|
return mk(v.text, { via: "conversational" });
|
|
371
447
|
}
|
|
372
|
-
if (GREET.has(q))
|
|
448
|
+
if (GREET.has(q)) {
|
|
449
|
+
// #3 empty/degenerate-graph greeting: a plain "hi"/"hello" over a graph with 0
|
|
450
|
+
// modules orients toward --repo/tmct init instead of over-promising "ask me
|
|
451
|
+
// about this codebase". Phrase-specific variants (good morning, hello there)
|
|
452
|
+
// keep their wording; only the default greeting swaps.
|
|
453
|
+
const id = (!T_GREETING_BY_PHRASE[q] && noCodeGraph(ctx.graph)) ? T_GREETING_EMPTY : (T_GREETING_BY_PHRASE[q] || T_GREETING);
|
|
454
|
+
return mk(t(id));
|
|
455
|
+
}
|
|
373
456
|
if (THANKS.has(q)) return mk(t(T_THANKS));
|
|
374
|
-
if (q === "help" || q === "?" || HELP_PHRASES.some((re) => re.test(raw))) return mk(
|
|
457
|
+
if (q === "help" || q === "?" || HELP_PHRASES.some((re) => re.test(raw))) return mk(orientationAnswer(ctx.templates, ctx.graph));
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ---- #1/#2/#3 conversational-UX helpers: module-aware orientation, the short
|
|
462
|
+
// tailored miss, and the intent lanes (teach + meta/self). All are recognizer-
|
|
463
|
+
// gated and (for the lanes) only consulted on a would-miss, so ordinary graph
|
|
464
|
+
// queries are never hijacked. ----
|
|
465
|
+
|
|
466
|
+
/** Code entities (Modules) in the loaded graph — the "is there a code graph here"
|
|
467
|
+
* test. 0 means a graph-less bootstrap OR a graph.json with no code entities (the
|
|
468
|
+
* degenerate trap); both orient rather than over-promise. */
|
|
469
|
+
export function moduleCountOf(graph) {
|
|
470
|
+
if (!graph || !Array.isArray(graph.individuals)) return 0;
|
|
471
|
+
return graph.individuals.filter((i) => (i.class || "") === "Module").length;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** A KNOWN-empty code graph: a loaded graph object with 0 modules. A null graph
|
|
475
|
+
* (a bare runTurn that wasn't handed one) is "unknown", NOT empty — the empty
|
|
476
|
+
* orientation/greeting only fires when we actually hold an empty graph. */
|
|
477
|
+
const noCodeGraph = (graph) => !!graph && moduleCountOf(graph) === 0;
|
|
478
|
+
|
|
479
|
+
/** The orientation surface, module-aware: the empty variant (→ --repo/tmct init +
|
|
480
|
+
* seeded vocabulary) when there's no code graph, the standard one otherwise. */
|
|
481
|
+
function orientationAnswer(templates, graph) {
|
|
482
|
+
return tRender(templates, noCodeGraph(graph) ? T_ORIENTATION_EMPTY : T_ORIENTATION) ?? TEMPLATES_UNAVAILABLE;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** A dynamic orientation string for the meta/self lane: a /stats-style overview
|
|
486
|
+
* when a code graph is loaded, else the honest empty-graph orientation. */
|
|
487
|
+
function orientationText(graph) {
|
|
488
|
+
if (noCodeGraph(graph)) {
|
|
489
|
+
return "There's no code graph loaded here, so I can't answer structure questions yet. "
|
|
490
|
+
+ "Point me at your code with `--repo <path>` or run `tmct init` to index this repo. "
|
|
491
|
+
+ 'I do know some general vocabulary — try "what is a cache". /help for commands.';
|
|
492
|
+
}
|
|
493
|
+
const by = (cls) => (graph.individuals || []).filter((i) => (i.class || "") === cls).length;
|
|
494
|
+
const parts = [];
|
|
495
|
+
for (const [cls, sing, plur] of [["Module", "module", "modules"], ["Class", "class", "classes"], ["Function", "function", "functions"]]) {
|
|
496
|
+
const n = by(cls); if (n) parts.push(`${n} ${n === 1 ? sing : plur}`);
|
|
497
|
+
}
|
|
498
|
+
return `This is a tmct code graph — ${(graph.individuals || []).length} entities`
|
|
499
|
+
+ `${parts.length ? ` (${parts.join(", ")})` : ""}. `
|
|
500
|
+
+ 'Ask about imports, calls, definitions or history — e.g. "which modules import <name>", "what calls <name>". '
|
|
501
|
+
+ "/stats for the full overview, /help for commands.";
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// #1 SHORT, TAILORED MISS — the engine's full grammar cheat-sheet (rephraseHint)
|
|
505
|
+
// now lives ONLY behind /help. A genuine parse-miss gets ONE line: an honest miss
|
|
506
|
+
// + at most two example shapes chosen for what the user typed + a /help pointer.
|
|
507
|
+
// The opening "couldn't parse this as a graph question. Try:" is preserved (the
|
|
508
|
+
// honest-miss contract + the graded hm-joke case pin those words).
|
|
509
|
+
const MISS_EXAMPLES = {
|
|
510
|
+
import: ['"which modules import <name>"', '"what does <name> import"'],
|
|
511
|
+
export: ['"what does <name> export"', '"which modules import <name>"'],
|
|
512
|
+
call: ['"what calls <name>"', '"which functions call <name>"'],
|
|
513
|
+
test: ['"what tests <name>"', '"which functions are tested"'],
|
|
514
|
+
inherit: ['"which classes inherit from <name>"', '"what are the subclasses of <name>"'],
|
|
515
|
+
history: ['"when did <name> change"', '"who touched <name>"'],
|
|
516
|
+
define: ['"where is <name> defined"', '"where is <name> mentioned"'],
|
|
517
|
+
meaning: ['"what is a <ClassName>"', '"what does <term> mean"'],
|
|
518
|
+
count: ['"how many classes are there"', '"how many modules are there"'],
|
|
519
|
+
};
|
|
520
|
+
const MISS_DEFAULT = ['"which modules import <name>"', '"what calls <name>"'];
|
|
521
|
+
|
|
522
|
+
/** Choose up to two example shapes RELEVANT to the user's words. */
|
|
523
|
+
function tailoredExamples(q) {
|
|
524
|
+
// membership yes/no ("is a algorithm information") — the grammar wants an article
|
|
525
|
+
// before BOTH terms; hint the working shape rather than dumping the wall.
|
|
526
|
+
if (/^is\s+(?:an?\s+)?[\w-]+\b/.test(q)) return ['"is a <thing> a <kind>" (an article before the kind, too)'];
|
|
527
|
+
const has = (re) => re.test(q);
|
|
528
|
+
if (has(/\bimport/)) return MISS_EXAMPLES.import;
|
|
529
|
+
if (has(/\bexport/)) return MISS_EXAMPLES.export;
|
|
530
|
+
if (has(/\b(?:calls?|caller|callee)\b/)) return MISS_EXAMPLES.call;
|
|
531
|
+
if (has(/\b(?:tests?|cover|covering|tested)\b/)) return MISS_EXAMPLES.test;
|
|
532
|
+
if (has(/\b(?:inherit|subclass|extends?|superclass|hierarchy|base class|parent class)\b/)) return MISS_EXAMPLES.inherit;
|
|
533
|
+
if (has(/\b(?:history|when|changed?|commit|touch(?:e[ds])?|who)\b/)) return MISS_EXAMPLES.history;
|
|
534
|
+
if (has(/\b(?:defined?|where|located?|mention)\b/)) return MISS_EXAMPLES.define;
|
|
535
|
+
if (has(/\b(?:mean|means|meaning|definition|vocab)\b/) || /\bwhat(?:'s| is)? an? \w/.test(q)) return MISS_EXAMPLES.meaning;
|
|
536
|
+
if (has(/\b(?:how many|count|number of)\b/)) return MISS_EXAMPLES.count;
|
|
537
|
+
return MISS_DEFAULT;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** The one-line short miss. */
|
|
541
|
+
export function shortMissHint(query) {
|
|
542
|
+
const ex = tailoredExamples(String(query || "").toLowerCase());
|
|
543
|
+
return `couldn't parse this as a graph question. Try: ${ex.join(" or ")}. Type /help for all query shapes.`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** The exact opening of the engine's full grammar-wall miss — the ONLY miss the
|
|
547
|
+
* short-miss rewrites. Receipt-bearing misses (honest empties, unresolved terms,
|
|
548
|
+
* the empty-graph bootstrap note, compositional misses) never match, so their
|
|
549
|
+
* specific wording + traversal receipts stand. */
|
|
550
|
+
const WALL_MISS_RE = /^couldn't parse this as a graph question\. Try:/;
|
|
551
|
+
|
|
552
|
+
// #2 INTENT LANE — MEMORY/TEACH. "remember that X is a Y", "note that …", or a
|
|
553
|
+
// bare "X is a Y" declarative the graph parser couldn't handle → route to the
|
|
554
|
+
// assert/memory path; when it can't be stored, say what CAN be remembered
|
|
555
|
+
// (LOUD, the working shape) — never the grammar wall, never a silent data loss.
|
|
556
|
+
const TEACH_RE = /^(?:please\s+)?(?:remember|note|keep in mind|jot down|for the record|fyi)\b[:,]?\s*(?:that\s+)?(.+?)[.?!]*$/i;
|
|
557
|
+
const BARE_DECLARATIVE_RE = /^(?:every |each |all |a |an )?[\w-]+ (?:is|are) (?:a |an )?[\w-]+$/i;
|
|
558
|
+
/** Interrogative / auxiliary leads that make an "X is a Y"-shaped line a QUESTION
|
|
559
|
+
* ("what is a cache", "is a module a component"), never a teach declarative. */
|
|
560
|
+
const QUESTION_LEAD_RE = /^(?:what|who|which|where|when|why|how|is|are|do|does|did|can|could|should|would|will|has|have)\b/i;
|
|
561
|
+
|
|
562
|
+
/** Sentence forms to try asserting for a teach payload: the payload as-is, and
|
|
563
|
+
* (if it carries no determiner) its "every …" universal — the ACE-OWL shape the
|
|
564
|
+
* grammar actually lands. */
|
|
565
|
+
function assertCandidates(payload) {
|
|
566
|
+
const p = String(payload).trim();
|
|
567
|
+
const out = [p];
|
|
568
|
+
if (!/^(?:every|each|all|a|an)\b/i.test(p)) out.push(`every ${p}`);
|
|
569
|
+
return [...new Set(out)];
|
|
570
|
+
}
|
|
571
|
+
/** The "every X is a Y" rewrite of a declarative, for the "did you mean …" hint. */
|
|
572
|
+
function teachSuggestion(payload) {
|
|
573
|
+
const m = String(payload).match(/^(?:every |each |all |a |an )?([\w-]+) (?:is|are) (?:a |an )?([\w-]+)$/i);
|
|
574
|
+
return m ? `every ${m[1].toLowerCase()} is a ${m[2].toLowerCase()}` : null;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
578
|
+
const raw = String(query).trim();
|
|
579
|
+
let payload = null;
|
|
580
|
+
const m = raw.match(TEACH_RE);
|
|
581
|
+
if (m && /\b(?:is|are)\b/i.test(m[1])) payload = m[1].trim();
|
|
582
|
+
else if (BARE_DECLARATIVE_RE.test(raw) && !QUESTION_LEAD_RE.test(raw)) payload = raw;
|
|
583
|
+
if (!payload) return null;
|
|
584
|
+
// Try to store it (a live session provides the write target). assertTurn returns
|
|
585
|
+
// the "noted — remembered …" confirmation or null (grammar miss / unknown words).
|
|
586
|
+
if (memoryDir) {
|
|
587
|
+
for (const cand of assertCandidates(payload)) {
|
|
588
|
+
const stored = await assertTurn(cand, { memoryDir, sessionId, focus: null, lexicon });
|
|
589
|
+
if (stored) return { text: stored.answer, via: "assert", miss: false };
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const suggestion = teachSuggestion(payload);
|
|
593
|
+
const did = suggestion && suggestion !== payload.toLowerCase() ? ` Did you mean: "${suggestion}"?` : "";
|
|
594
|
+
return {
|
|
595
|
+
text: 'I couldn\'t store that — I remember facts in the shape "every X is a Y", where X and Y are '
|
|
596
|
+
+ `words I know.${did} Type /memory to see what I already remember.`,
|
|
597
|
+
via: "teach-miss", miss: true,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// #2 INTENT LANE — META/SELF. Bare self/session questions answered from stats /
|
|
602
|
+
// memory / orientation, never the grammar wall or the raw fact-dump. WOULD-MISS
|
|
603
|
+
// ONLY (the caller gates on a miss) and every pattern is a WHOLE-LINE self/session
|
|
604
|
+
// reference with no graph entity or predicate, so real graph queries ("what does X
|
|
605
|
+
// import", the meta "what does imports mean", "what did i ask before") never match.
|
|
606
|
+
const WHAT_KNOW_RE = /^what\s+(?:do\s+you|d'?you)\s+know(?:\s+so\s+far)?$/;
|
|
607
|
+
const META_ORIENT_RE = /^(?:what(?:'s| is| are)?\s+this(?:\s+(?:codebase|repo|repository|project|code|thing))?|what\s+(?:codebase|repo|repository|project)\s+is\s+this|what\s+am\s+i\s+looking\s+at|what\s+is\s+tmct|how\s+do\s+i\s+(?:start|begin|get\s+started|get\s+going|load\s+(?:my\s+)?code|index\s+(?:my\s+)?(?:code|repo|repository)|use\s+(?:this|you|tmct))|where\s+do\s+i\s+(?:start|begin))$/;
|
|
608
|
+
|
|
609
|
+
/** A SHORT memory summary (never a fact dump) for the bare "what do you know". */
|
|
610
|
+
async function memorySummary(memoryDir, graph) {
|
|
611
|
+
const rows = memoryDir ? await memoryFacts(memoryDir) : [];
|
|
612
|
+
if (!rows.length) {
|
|
613
|
+
const hook = moduleCountOf(graph) > 0
|
|
614
|
+
? 'ask about this codebase\'s structure (imports, calls, definitions), or teach me with "every X is a Y"'
|
|
615
|
+
: 'teach me with "every X is a Y", or try general vocabulary like "what is a cache"';
|
|
616
|
+
return `I haven't been told any facts yet — ${hook}. /memory to inspect, /help for commands.`;
|
|
617
|
+
}
|
|
618
|
+
const preds = new Set(rows.map((f) => f.predicate).filter(Boolean));
|
|
619
|
+
const n = rows.length;
|
|
620
|
+
return `I remember ${n} fact${n === 1 ? "" : "s"} across ${preds.size} relation `
|
|
621
|
+
+ `type${preds.size === 1 ? "" : "s"}. Ask "what do you know about <term>", or /memory to explore.`;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async function metaLane(query, { graph, memoryDir }) {
|
|
625
|
+
const q = String(query).trim().toLowerCase().replace(/[?.!]+$/, "").replace(/\s+/g, " ");
|
|
626
|
+
if (WHAT_KNOW_RE.test(q) || q === "what have you learned" || q === "what have you learnt") {
|
|
627
|
+
return { text: await memorySummary(memoryDir, graph), via: "meta" };
|
|
628
|
+
}
|
|
629
|
+
if (META_ORIENT_RE.test(q)) return { text: orientationText(graph), via: "meta" };
|
|
375
630
|
return null;
|
|
376
631
|
}
|
|
377
632
|
|
|
@@ -877,13 +1132,89 @@ function discourseRewrite(query, last) {
|
|
|
877
1132
|
return prevQ.replace(NAME_TOKEN_RE, () => newSubj);
|
|
878
1133
|
}
|
|
879
1134
|
|
|
1135
|
+
// ---- curated SEON definitions (corpus/seon/definitions.jsonl) ----
|
|
1136
|
+
// A "what is a <term>" for a LEXICON term prefers the curated one-sentence
|
|
1137
|
+
// definition — the richer surface form of the same curated SEON knowledge that the
|
|
1138
|
+
// concept seed reifies — over the bare seon concept fact / schema-docs / honest
|
|
1139
|
+
// miss. Cited via:"corpus/seon". Two guards keep it honest and test-safe:
|
|
1140
|
+
// - it only fires when this repo actually carries the SEON concept seed (a
|
|
1141
|
+
// corpus:seon fact about the term is in memory) — so a repo seeded with only
|
|
1142
|
+
// ConceptNet (or nothing) is byte-unchanged;
|
|
1143
|
+
// - a fact the USER personally asserted (ace:chat) still wins — you told me beats
|
|
1144
|
+
// the corpus definition.
|
|
1145
|
+
|
|
1146
|
+
let seonDefsPromise = null;
|
|
1147
|
+
/** Load corpus/seon/definitions.jsonl once → Map(normFactTerm(term) → definition).
|
|
1148
|
+
* Lazy + failure-tolerated (chat.mjs ethos): any failure degrades to an empty map. */
|
|
1149
|
+
function seonDefinitions() {
|
|
1150
|
+
if (!seonDefsPromise) {
|
|
1151
|
+
seonDefsPromise = (async () => {
|
|
1152
|
+
const { SEON_DEFINITIONS_FILE } = await import("./corpus/conceptnet.mjs");
|
|
1153
|
+
const { normFactTerm } = await import("./memory/core.mjs");
|
|
1154
|
+
const raw = await readFile(SEON_DEFINITIONS_FILE, "utf8");
|
|
1155
|
+
const map = new Map();
|
|
1156
|
+
for (const line of raw.split("\n")) {
|
|
1157
|
+
const t = line.trim();
|
|
1158
|
+
if (!t) continue;
|
|
1159
|
+
try {
|
|
1160
|
+
const row = JSON.parse(t);
|
|
1161
|
+
if (row.term && row.definition) map.set(normFactTerm(row.term), String(row.definition));
|
|
1162
|
+
} catch { /* skip a malformed line, never throw */ }
|
|
1163
|
+
}
|
|
1164
|
+
return map;
|
|
1165
|
+
})().catch(() => new Map());
|
|
1166
|
+
}
|
|
1167
|
+
return seonDefsPromise;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/** The meta term a "what is a X" / "what does X mean" / "define X" question asks
|
|
1171
|
+
* about — from the parse when present, else recognized directly (same required-
|
|
1172
|
+
* article discipline as the grammar's T5). Null when the line isn't such a form. */
|
|
1173
|
+
function metaTermOf(query, envelope) {
|
|
1174
|
+
if (envelope?.parsed?.shape === "meta" && envelope.parsed.object) return envelope.parsed.object;
|
|
1175
|
+
const q = String(query).trim();
|
|
1176
|
+
const m = q.match(/^what\s+(?:is|are)\s+an?\s+(.+?)[?.!\s]*$/i)
|
|
1177
|
+
|| q.match(/^what\s+(?:does|do)\s+(?:an?\s+)?(.+?)\s+means?[?.!\s]*$/i)
|
|
1178
|
+
|| q.match(/^define\s+(?:an?\s+)?(.+?)[?.!\s]*$/i);
|
|
1179
|
+
return m ? m[1].trim() : null;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/** The curated SEON definition to PREFER for a "what is a <lexicon term>", or null.
|
|
1183
|
+
* Gated: the term parses as a meta question, is a grammar-lexicon noun, has a
|
|
1184
|
+
* curated definition, this repo carries the SEON concept seed for it (a corpus:seon
|
|
1185
|
+
* fact), and the user has NOT personally asserted a fact about it. Returns { text,
|
|
1186
|
+
* term } or null. Lazy + failure-tolerated throughout. */
|
|
1187
|
+
async function curatedDefinitionAnswer(query, envelope, { memoryDir, lexicon }) {
|
|
1188
|
+
if (!memoryDir) return null;
|
|
1189
|
+
const term = metaTermOf(query, envelope);
|
|
1190
|
+
if (!term) return null;
|
|
1191
|
+
let normFactTerm;
|
|
1192
|
+
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
1193
|
+
// lexicon-noun gate: the curated defs are keyed on SE lexicon terms only.
|
|
1194
|
+
let lex = lexicon;
|
|
1195
|
+
try {
|
|
1196
|
+
if (!lex) { const { loadLexicon } = await import("./grammar/lexicon.mjs"); lex = loadLexicon(); }
|
|
1197
|
+
const { lookupNoun } = await import("./grammar/lexicon.mjs");
|
|
1198
|
+
if (!lookupNoun(lex, term)) return null;
|
|
1199
|
+
} catch { return null; }
|
|
1200
|
+
const def = (await seonDefinitions()).get(normFactTerm(term));
|
|
1201
|
+
if (!def) return null;
|
|
1202
|
+
// tie the definition to the SEON concept seed being present, and let a user fact win.
|
|
1203
|
+
const variants = factTermVariants(normFactTerm, term);
|
|
1204
|
+
const facts = await memoryFacts(memoryDir);
|
|
1205
|
+
const about = facts.filter((f) => variants.has(f.subject) || variants.has(f.object));
|
|
1206
|
+
if (about.some((f) => f.provenance.includes("ace:chat"))) return null; // you told me — that wins
|
|
1207
|
+
if (!about.some((f) => f.provenance.includes("corpus:seon"))) return null; // no SEON seed here
|
|
1208
|
+
return { text: `${def} (source: corpus/seon)`, term };
|
|
1209
|
+
}
|
|
1210
|
+
|
|
880
1211
|
/** A bare question → tmct_ask. When a focus is set AND the graph is in hand we
|
|
881
1212
|
* call ask() directly to thread the focus as contextId (so a pronoun like "it"
|
|
882
1213
|
* resolves to the focus) — building the SAME delimited string dispatchTool emits;
|
|
883
1214
|
* otherwise the unchanged dispatchTool path (which also yields the no-graph error).
|
|
884
1215
|
* A hit updates the focus to the resolved object. Grammar miss / ToolError → a
|
|
885
1216
|
* normal answer, never a crash. */
|
|
886
|
-
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, env }) {
|
|
1217
|
+
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env }) {
|
|
887
1218
|
const ts = new Date().toISOString();
|
|
888
1219
|
// DISCOURSE ANAPHORA (CHATBENCH_006 levers 1+2): a follow-up like "which of those
|
|
889
1220
|
// are tested" / "how many of those" / "count them" filters or counts the PREVIOUS
|
|
@@ -939,19 +1270,31 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
939
1270
|
// orientation swap below is template wording, so those turns carry via:"template".
|
|
940
1271
|
let via = "composed";
|
|
941
1272
|
let recordMiss = miss;
|
|
942
|
-
//
|
|
943
|
-
//
|
|
944
|
-
//
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1273
|
+
// MISS handling. The intent lanes + short-miss are RECOGNIZER-gated on the query
|
|
1274
|
+
// text AND only consulted on a would-miss, so a real graph query — a hit, an honest
|
|
1275
|
+
// empty with a receipt, a fuzzy repair — is never hijacked. Order: (1) META/SELF
|
|
1276
|
+
// lane (would-miss), (2) conversational orientation (would-miss), (3) memory
|
|
1277
|
+
// facts/recall (a fact EXTENDS a non-miss schema hit too — NOT miss-gated),
|
|
1278
|
+
// (4) TEACH lane (would-miss), (5) the short tailored miss (would-miss).
|
|
1279
|
+
let handled = false;
|
|
1280
|
+
// (1) #2 META/SELF: bare self/session questions ("what do you know", "what is this
|
|
1281
|
+
// codebase", "how do i start") → a summary / orientation, answered before the
|
|
1282
|
+
// fact-dump readers so "what do you know" gets a summary, not raw facts.
|
|
1283
|
+
if (miss) {
|
|
1284
|
+
const meta = await metaLane(query, { graph, memoryDir });
|
|
1285
|
+
if (meta) { answer = meta.text; via = meta.via; recordMiss = false; handled = true; }
|
|
1286
|
+
}
|
|
1287
|
+
if (!handled && miss && isConversational(query)) {
|
|
1288
|
+
// A conversational miss (a greeting, "what can you do", a very short non-code
|
|
1289
|
+
// line) gets the friendly orientation (module-aware: empty → --repo/tmct init).
|
|
1290
|
+
answer = orientationAnswer(templates, graph); via = "template"; handled = true;
|
|
1291
|
+
} else if (!handled && memoryDir) {
|
|
1292
|
+
// W4: vocabulary/definition questions consult the MEMORY graph's Facts alongside
|
|
1293
|
+
// the schema-docs surface — a remembered fact answers a miss OR extends a (non-
|
|
1294
|
+
// miss) schema hit, cited with its provenance verbatim. Checked BEFORE recall: a
|
|
1295
|
+
// reified fact is stronger evidence than a transcript echo. Subject-side facts
|
|
1296
|
+
// first (factAnswer), then the reverse-membership read-back (factReadBack) so an
|
|
1297
|
+
// asserted "every X is a Y" answers "what is a Y" too.
|
|
955
1298
|
const fact = (await factAnswer(memoryDir, query, envelope, miss))
|
|
956
1299
|
?? (await factReadBack(memoryDir, query, envelope, miss, graph));
|
|
957
1300
|
if (fact) {
|
|
@@ -970,6 +1313,34 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
970
1313
|
}
|
|
971
1314
|
}
|
|
972
1315
|
}
|
|
1316
|
+
// CURATED SEON DEFINITION (corpus/seon) — a "what is a <lexicon term>" prefers the
|
|
1317
|
+
// curated prose definition over the seon concept fact / schema-docs / honest miss.
|
|
1318
|
+
// Runs after the fact branch and overrides its corpus-fact answer (via:"fact"), but
|
|
1319
|
+
// curatedDefinitionAnswer itself defers to a user-asserted (ace:chat) fact, so a
|
|
1320
|
+
// "you told me" answer already standing is left untouched. Skips the meta/self +
|
|
1321
|
+
// conversational lanes (via:"meta"/"template"), which answer a different question.
|
|
1322
|
+
if (via === "composed" || via === "fact") {
|
|
1323
|
+
const def = await curatedDefinitionAnswer(query, envelope, { memoryDir, lexicon });
|
|
1324
|
+
if (def) { answer = def.text; via = "corpus/seon"; recordMiss = false; }
|
|
1325
|
+
}
|
|
1326
|
+
// (4) #2 TEACH lane — a teach-shaped would-miss nothing above answered: route to
|
|
1327
|
+
// memory, or say what CAN be remembered (LOUD), never the wall / a silent drop.
|
|
1328
|
+
if (miss && recordMiss && via === "composed") {
|
|
1329
|
+
const taught = await teachLane(query, { memoryDir, sessionId, lexicon });
|
|
1330
|
+
if (taught) { answer = taught.text; via = taught.via; recordMiss = taught.miss; }
|
|
1331
|
+
}
|
|
1332
|
+
// (5) #1 SHORT TAILORED MISS — replace ONLY the engine's full grammar cheat-sheet
|
|
1333
|
+
// wall (WALL_MISS_RE). Receipt-bearing misses keep their specific wording.
|
|
1334
|
+
if (miss && recordMiss && via === "composed" && WALL_MISS_RE.test(answer)) {
|
|
1335
|
+
answer = shortMissHint(query); via = "miss";
|
|
1336
|
+
}
|
|
1337
|
+
// #4 HONEST-EMPTY POLISH — an empty CODE graph: any still-standing engine
|
|
1338
|
+
// dead-end (an honest empty, the short miss, the bootstrap note) carries the exit
|
|
1339
|
+
// toward a real graph, unless it already points there. Only when genuinely empty.
|
|
1340
|
+
if (recordMiss && (via === "composed" || via === "miss")
|
|
1341
|
+
&& noCodeGraph(graph) && !/--repo|tmct init|no code graph/i.test(answer)) {
|
|
1342
|
+
answer = `${answer}\n(this repo has no code graph — try \`--repo <path>\` or \`tmct init\`.)`;
|
|
1343
|
+
}
|
|
973
1344
|
// W5 (flag-gated, default OFF): an unknown-term miss may consult the LOCAL
|
|
974
1345
|
// committed corpus slice — a hit APPENDS a grounded, licence-cited aside under
|
|
975
1346
|
// the honest miss (the miss itself stands; the aside is context, not an answer).
|
|
@@ -1128,7 +1499,14 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
1128
1499
|
return { ...finished, last: { query: line, answer: finished.answer, detail: finished.detail ?? null } };
|
|
1129
1500
|
};
|
|
1130
1501
|
|
|
1131
|
-
//
|
|
1502
|
+
// Slash-optional system commands: a bare leading command word ("stats",
|
|
1503
|
+
// "memory", "describe X") is routed to its slash form BEFORE the conversational
|
|
1504
|
+
// layer, so a forgiving shell answers "stats" the way it answers "/stats" instead
|
|
1505
|
+
// of falling through to the generic orientation.
|
|
1506
|
+
const bareCmd = asBareCommand(line);
|
|
1507
|
+
if (bareCmd) return withLast(await runCommand(bareCmd, ctx));
|
|
1508
|
+
|
|
1509
|
+
// Conversational layer next (greetings, thanks, help, bye, why/say-more) — these
|
|
1132
1510
|
// resolve no entity and carry their own preserved `last`.
|
|
1133
1511
|
const convo = conversationalTurn(line, ctx);
|
|
1134
1512
|
if (convo) return convo;
|
|
@@ -1142,6 +1520,15 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
1142
1520
|
const asserted = await assertTurn(line, ctx);
|
|
1143
1521
|
if (asserted) return withLast(asserted);
|
|
1144
1522
|
}
|
|
1523
|
+
// MEMORY-STORE counts first ("how many facts / utterances do you know") — the
|
|
1524
|
+
// memory graph owns Facts + Utterances, so these are answerable and consistent
|
|
1525
|
+
// with `/memory`. Checked before answerCount (which reads the CODE graph and would
|
|
1526
|
+
// otherwise say "I can't count facts"); it only speaks for a memory-class noun, so
|
|
1527
|
+
// structural counts (classes/functions/…) and sessions fall through unaffected.
|
|
1528
|
+
if (memoryDir) {
|
|
1529
|
+
const memCount = await answerMemoryCount(memoryDir, line);
|
|
1530
|
+
if (memCount != null) return withLast(plainTurn(line, memCount, { via: "count", focus }));
|
|
1531
|
+
}
|
|
1145
1532
|
// Aggregate/count questions are answered mechanically off the loaded graph header,
|
|
1146
1533
|
// BEFORE falling through to the ask engine (focus unchanged — a count names no entity).
|
|
1147
1534
|
const count = answerCount(graph, line);
|
|
@@ -1174,11 +1561,20 @@ export const SEED_PREFER = ["rdfs:subClassOf", "rdf:type", "mgx:usedFor", "mgx:p
|
|
|
1174
1561
|
* corpus seed, so re-runs skip without even reading the slice. */
|
|
1175
1562
|
export const SEED_MARKER_REL = join(".tmct", "memory", "corpus-seed.json");
|
|
1176
1563
|
|
|
1177
|
-
/** Seed the
|
|
1178
|
-
*
|
|
1179
|
-
*
|
|
1180
|
-
*
|
|
1181
|
-
*
|
|
1564
|
+
/** Seed the starter corpus into <repo>/.tmct/memory once, in TWO passes:
|
|
1565
|
+
* 1. the curated SEON ontology (corpus/seon/concepts.jsonl) FIRST and UNCAPPED
|
|
1566
|
+
* — it is small + fully curated (the SE vocabulary + orientation facts),
|
|
1567
|
+
* tagged "corpus:seon", so a fresh repo knows the curated terms before any
|
|
1568
|
+
* general ConceptNet noise;
|
|
1569
|
+
* 2. THEN the capped ConceptNet slice (the definitional band first, SEED_LIMIT
|
|
1570
|
+
* facts), tagged "corpus:conceptnet".
|
|
1571
|
+
* seon runs first so its curated facts win the content-hash idempotency race — a
|
|
1572
|
+
* term the ConceptNet slice also carries keeps the seon provenance. Idempotent
|
|
1573
|
+
* twice over (the marker short-circuits; seedMemory content-hashes fact ids) and
|
|
1574
|
+
* failure-tolerated: a missing/broken corpus degrades to the unseeded bootstrap —
|
|
1575
|
+
* never an error before the prompt. Returns { appended, skipped, total, seon,
|
|
1576
|
+
* conceptnet } on a fresh seed (the banner counts stay honest), null when
|
|
1577
|
+
* skipped/failed. */
|
|
1182
1578
|
async function seedBootstrapMemory(repo) {
|
|
1183
1579
|
const marker = join(repo, SEED_MARKER_REL);
|
|
1184
1580
|
try {
|
|
@@ -1186,11 +1582,22 @@ async function seedBootstrapMemory(repo) {
|
|
|
1186
1582
|
return null; // already seeded — the marker is authoritative
|
|
1187
1583
|
} catch { /* no marker → first run */ }
|
|
1188
1584
|
try {
|
|
1189
|
-
const { seedMemory } = await import("./corpus/conceptnet.mjs");
|
|
1190
|
-
|
|
1585
|
+
const { seedMemory, SEON_CONCEPTS_FILE } = await import("./corpus/conceptnet.mjs");
|
|
1586
|
+
// (1) curated SEON ontology — uncapped, seon-tagged, seeded FIRST.
|
|
1587
|
+
const seon = await seedMemory(repo, { slicePath: SEON_CONCEPTS_FILE, provenancePrefix: "corpus:seon" });
|
|
1588
|
+
// (2) the capped ConceptNet band — byte-identical to the prior single seed.
|
|
1589
|
+
const conceptnet = await seedMemory(repo, { limit: SEED_LIMIT, prefer: SEED_PREFER });
|
|
1590
|
+
const res = {
|
|
1591
|
+
appended: seon.appended + conceptnet.appended,
|
|
1592
|
+
skipped: seon.skipped + conceptnet.skipped,
|
|
1593
|
+
total: seon.total + conceptnet.total,
|
|
1594
|
+
seon: seon.appended,
|
|
1595
|
+
conceptnet: conceptnet.appended,
|
|
1596
|
+
};
|
|
1191
1597
|
await mkdir(dirname(marker), { recursive: true });
|
|
1192
1598
|
await writeFile(marker, JSON.stringify({
|
|
1193
|
-
seededAt: new Date().toISOString(), limit: SEED_LIMIT,
|
|
1599
|
+
seededAt: new Date().toISOString(), limit: SEED_LIMIT,
|
|
1600
|
+
appended: res.appended, skipped: res.skipped, seon: res.seon, conceptnet: res.conceptnet,
|
|
1194
1601
|
}) + "\n");
|
|
1195
1602
|
return res;
|
|
1196
1603
|
} catch {
|
|
@@ -1247,16 +1654,27 @@ export async function createSession({
|
|
|
1247
1654
|
cwd = process.cwd(),
|
|
1248
1655
|
gitRoot = gitToplevel,
|
|
1249
1656
|
} = {}) {
|
|
1657
|
+
// Graph resolution order for the chat surface (documented; --repo wins):
|
|
1658
|
+
// 1. --repo <path> → pins <path>/.tmct/graph.json (repo AND graph).
|
|
1659
|
+
// 2. TMCT_GRAPH_FILE env → loads that graph anywhere (loadConfig reads it), so
|
|
1660
|
+
// `TMCT_GRAPH_FILE=<path> tmct chat` works even inside a git repo — the chat
|
|
1661
|
+
// surface used to ignore it (only the `cli` tool path honoured it). The repo
|
|
1662
|
+
// for logs/memory is still the git root / cwd; only the graph file is overridden.
|
|
1663
|
+
// 3. git root → <root>/.tmct/graph.json (the default target).
|
|
1664
|
+
// 4. cwd → <cwd>/.tmct/graph.json (not a git repo).
|
|
1250
1665
|
// Default the target to the GIT ROOT, not raw cwd: running from a nested package
|
|
1251
1666
|
// dir (npm sets cwd there) would otherwise index only that package's ~few modules
|
|
1252
|
-
// instead of the whole repo.
|
|
1667
|
+
// instead of the whole repo.
|
|
1253
1668
|
let repo;
|
|
1254
1669
|
let config;
|
|
1255
1670
|
if (repoPath) { repo = repoPath; config = configFor(repoPath); }
|
|
1256
1671
|
else {
|
|
1257
1672
|
const root = gitRoot(cwd);
|
|
1258
|
-
|
|
1259
|
-
|
|
1673
|
+
repo = root || cwd;
|
|
1674
|
+
const envGraph = env.TMCT_GRAPH_FILE && String(env.TMCT_GRAPH_FILE).trim();
|
|
1675
|
+
// TMCT_GRAPH_FILE (via loadConfig) overrides the repo-derived default graph path;
|
|
1676
|
+
// otherwise the repo's own .tmct/graph.json is the target.
|
|
1677
|
+
config = envGraph ? loadConfig(env, cwd) : { graphFile: join(repo, DEFAULT_GRAPH_REL) };
|
|
1260
1678
|
}
|
|
1261
1679
|
|
|
1262
1680
|
// Load the graph once up front — the banner needs the module count, and focus/`it`
|
|
@@ -1320,14 +1738,22 @@ export async function createSession({
|
|
|
1320
1738
|
if (empty && String(env.TMCT_NO_SEED || "") !== "1") {
|
|
1321
1739
|
seeded = await seedBootstrapMemory(repo);
|
|
1322
1740
|
}
|
|
1741
|
+
// #3/#5: 0 modules means no code graph to answer structure questions from —
|
|
1742
|
+
// whether the graph file is absent (empty bootstrap) OR present with no code
|
|
1743
|
+
// entities (the degenerate trap). Both get orienting, non-over-promising banner
|
|
1744
|
+
// + greeting messaging rather than a silent dead-end.
|
|
1745
|
+
const noCodeGraph = moduleCount === 0;
|
|
1323
1746
|
const bannerLines = [
|
|
1324
|
-
|
|
1325
|
-
//
|
|
1326
|
-
? `tmct chat — ${repo} — no graph loaded — starting empty; ` +
|
|
1747
|
+
noCodeGraph
|
|
1748
|
+
// No code graph: honest, orienting messaging — never an error before the prompt.
|
|
1749
|
+
? `tmct chat — ${repo} — no code graph loaded — ${empty ? "starting empty" : "graph has no code entities"}; ` +
|
|
1327
1750
|
`the conversation is remembered to ${DEFAULT_GRAPH_REL} — log ${logFile}`
|
|
1328
1751
|
: `tmct chat — ${repo} — ${moduleCount} module(s) — log ${logFile}`,
|
|
1329
|
-
// the honest seed line appears ONLY on the run that actually seeded
|
|
1330
|
-
|
|
1752
|
+
// the honest seed line appears ONLY on the run that actually seeded — the count
|
|
1753
|
+
// is the TOTAL appended, split into the curated SEON ontology + the ConceptNet band.
|
|
1754
|
+
...(seeded ? [`seeded ${seeded.appended} starter facts (${seeded.seon} curated SEON + ${seeded.conceptnet} ConceptNet) — /memory to inspect`] : []),
|
|
1755
|
+
// no code indexed → point at how to get one, and at what IS answerable now
|
|
1756
|
+
...(noCodeGraph ? ['no code indexed yet — run `tmct init` here or pass --repo <path>; meanwhile try "what is a cache"'] : []),
|
|
1331
1757
|
"pass --repo <path> to target a different repo",
|
|
1332
1758
|
"ask a question, or /help for commands (/stats for an overview) — /exit to leave",
|
|
1333
1759
|
];
|