@polycode-projects/the-mechanical-code-talker 1.8.18 → 1.9.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 +27 -6
- package/ROADMAP.md +1 -1
- package/bin/tmct.mjs +167 -20
- package/corpus/generated/README.md +2 -2
- package/corpus/namenet/LICENSE-NOTICE +68 -0
- package/corpus/namenet/generate.mjs +309 -0
- package/corpus/namenet/manifest.json +20 -0
- package/corpus/namenet/namenet.jsonl +7260 -0
- package/corpus/wordnet/LICENSE-NOTICE +49 -0
- package/corpus/wordnet/generate.mjs +333 -0
- package/corpus/wordnet/manifest.json +34 -0
- package/corpus/wordnet/wordnet-full.jsonl +192498 -0
- package/corpus/wordnet/wordnet-xl.jsonl +23805 -0
- package/package.json +4 -2
- package/src/ask-browser-entry.mjs +13 -2
- package/src/ask-browser.bundle.js +272 -19
- package/src/chat.mjs +177 -27
- package/src/cli-args.mjs +20 -1
- package/src/codegraph.mjs +306 -1
- package/src/corpus/conceptnet-map.toml +17 -12
- package/src/corpus/conceptnet.mjs +23 -1
- package/src/extensions.mjs +44 -1
- package/src/init.mjs +99 -46
- package/src/interpret/normalize.mjs +1 -3
- package/src/memory/core.mjs +57 -1
- package/src/memory/trust.mjs +27 -6
- package/src/memory-ask-browser-entry.mjs +36 -0
- package/src/memory-ask-browser.bundle.js +5544 -0
- package/src/toml-config.mjs +11 -1
- package/src/viz.mjs +459 -59
package/src/chat.mjs
CHANGED
|
@@ -4044,6 +4044,11 @@ const FACT_PREDICATE_PHRASES = {
|
|
|
4044
4044
|
"mgx:hasLastSubevent": "ends with",
|
|
4045
4045
|
"mgx:hasPrerequisite": "requires",
|
|
4046
4046
|
"mgx:ownedBy": "is owned by", // the teach lane's ownership frame ("Priya owns tasks.mjs")
|
|
4047
|
+
"mgx:synonym": "means the same as",
|
|
4048
|
+
"mgx:antonym": "is the opposite of",
|
|
4049
|
+
"mgx:similarTo": "is similar to",
|
|
4050
|
+
"mgx:relatedTo": "is related to",
|
|
4051
|
+
"mgx:symbolOf": "is a symbol of",
|
|
4047
4052
|
};
|
|
4048
4053
|
|
|
4049
4054
|
/** Bug 3 (2026-07-09) point 3b: the MECHANICAL fallback for a predicate this
|
|
@@ -4138,16 +4143,26 @@ function splitMetaPredicate(term) {
|
|
|
4138
4143
|
|
|
4139
4144
|
/** One rendered fact line. An OPERATOR-asserted fact keeps the true first-person
|
|
4140
4145
|
* provenance ("you told me: …"). A CORPUS fact is presented as clean DATA with its
|
|
4141
|
-
* source cited
|
|
4142
|
-
*
|
|
4143
|
-
* for themselves.
|
|
4146
|
+
* source cited, not "i learned: …" — that phrase over-claims and anthropomorphises
|
|
4147
|
+
* a first-person experience the bot never had; the relation and its provenance
|
|
4148
|
+
* speak for themselves. A WEAK-corpus fact (memory/trust.mjs SOURCE_PRIOR.corpusWeak
|
|
4149
|
+
* — real data, low-precision relation, e.g. ConceptNet's undirected /r/RelatedTo)
|
|
4150
|
+
* still isn't "i learned" (same anthropomorphism problem), but reads identically to
|
|
4151
|
+
* a solid corpus fact loses the only reader-visible signal that it's lower-
|
|
4152
|
+
* confidence — re-examined 2026-07-12 (TOO_HARD_AUDIT.md): the prior blanket "corpus
|
|
4153
|
+
* never hedges" rule predates corpus data having any confidence spread at all, so a
|
|
4154
|
+
* distinct, honest hedge ("possibly: …") applies here instead of either extreme.
|
|
4155
|
+
* Provenance stays VERBATIM in every case. */
|
|
4144
4156
|
function renderFactLine(f) {
|
|
4145
4157
|
const cite = f.provenance ? ` (source: ${f.provenance})` : "";
|
|
4146
4158
|
// ace:chat = the ACE-parsed operator assert; teach:chat = the teach lane's
|
|
4147
4159
|
// natural frames — both are things the operator SAID, so both read first-person.
|
|
4148
4160
|
if (f.provenance.includes("ace:chat") || f.provenance.includes("teach:chat")) return `you told me: ${factPhrase(f)}${cite}`;
|
|
4149
|
-
//
|
|
4150
|
-
//
|
|
4161
|
+
// WEAK corpus facts (lower trust, e.g. RelatedTo) — real, cited, but hedged as
|
|
4162
|
+
// uncertain rather than either flatly stated or falsely claimed as "learned".
|
|
4163
|
+
if (f.provenance.includes("corpus-weak:")) return `possibly: ${factPhrase(f)}${cite}`;
|
|
4164
|
+
// SOLID corpus facts are background DATA — present the relation plainly, cited
|
|
4165
|
+
// to its source, never "i learned: …" (a first-person claim over corpus data).
|
|
4151
4166
|
if (f.provenance.includes("corpus:")) return `${factPhrase(f)}${cite}`;
|
|
4152
4167
|
return `i learned: ${factPhrase(f)}${cite}`;
|
|
4153
4168
|
}
|
|
@@ -4501,6 +4516,49 @@ const FACT_ANSWER_CAP = 32;
|
|
|
4501
4516
|
const CAN_ASK_RE = /^(?:can|could)\s+(?:an?\s+)?([\w'-]+(?:\s+[\w'-]+)*?)\s+([a-z]+)[?.!\s]*$/i;
|
|
4502
4517
|
const WHAT_CAN_DO_RE = /^what\s+can\s+(?:an?\s+)?(.+?)\s+do[?.!\s]*$/i;
|
|
4503
4518
|
const WHAT_HAS_RE = /^what\s+has\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
4519
|
+
// "what is used for riding" / "what can be used for riding" / "what is for
|
|
4520
|
+
// riding" — the reverse-by-object mirror of BUG 1's forward reader ("what is
|
|
4521
|
+
// a tree used for"), missing until now: BUG 1 only ever filtered a KNOWN
|
|
4522
|
+
// subject's facts down to mgx:usedFor; nothing answered the reverse question
|
|
4523
|
+
// (object known, subject unknown), so it fell all the way through to the
|
|
4524
|
+
// code-graph miss cascade — actively misleading for a pure vocabulary query.
|
|
4525
|
+
const WHAT_USED_FOR_RE = /^what\s+(?:(?:can\s+be|is)\s+used\s+for|is\s+for)\s+(.+?)[?.!\s]*$/i;
|
|
4526
|
+
|
|
4527
|
+
// Live-caught 2026-07-12 follow-up: the SAME gap as mgx:usedFor above turned
|
|
4528
|
+
// out to be systemic, not one-off — "what causes fire", "what is made of
|
|
4529
|
+
// wood", "what is found in a kitchen", "what wants food" all fell through to
|
|
4530
|
+
// the same misleading code-graph miss, for the same reason (no reverse-by-
|
|
4531
|
+
// object reader existed for these predicates either). Rather than hand-roll
|
|
4532
|
+
// one more one-off regex per predicate, this DERIVES a reverse-by-object
|
|
4533
|
+
// regex for every FACT_PREDICATE_PHRASES entry that's safe to reverse — the
|
|
4534
|
+
// same "derivation, not a curated subset" philosophy TRAILING_PREDICATE_MARKERS
|
|
4535
|
+
// already uses for the forward direction (see that const's own docblock).
|
|
4536
|
+
// Excluded, each for a specific reason:
|
|
4537
|
+
// - rdfs:subClassOf, mgx:hasA, mgx:capableOf, mgx:usedFor — already have
|
|
4538
|
+
// their own dedicated, richer reverse readers (WHAT_INHERITS_RE/
|
|
4539
|
+
// WHAT_HAS_RE/WHAT_CAN_DO_RE/WHAT_USED_FOR_RE above).
|
|
4540
|
+
// - mgx:ownedBy — already has its own dedicated "who owns X" reader
|
|
4541
|
+
// (WHO_OWNS_RE) — a WHO question, not a WHAT question, so it would never
|
|
4542
|
+
// collide, but is excluded anyway to keep exactly one reader per relation.
|
|
4543
|
+
// - rdf:type ("is a") and mgx:hasProperty ("is") — too short/generic to
|
|
4544
|
+
// safely anchor a reverse question: "what is X" already belongs to the
|
|
4545
|
+
// meta lane's own vocabulary lookup, and reversing it here would mean
|
|
4546
|
+
// guessing whether the user meant "define X" or "what has property X"
|
|
4547
|
+
// from word order alone.
|
|
4548
|
+
// - owl:disjointWith ("is not a") and mgx:receivesAction ("can be") — both
|
|
4549
|
+
// broad enough that "what is not a X" / "what can be X" read as much more
|
|
4550
|
+
// likely to be a different question shape than a genuine reverse lookup.
|
|
4551
|
+
const REVERSE_PREDICATE_EXCLUDE = new Set([
|
|
4552
|
+
"rdfs:subClassOf", "rdf:type", "mgx:hasA", "mgx:capableOf", "mgx:usedFor",
|
|
4553
|
+
"mgx:ownedBy", "owl:disjointWith", "mgx:hasProperty", "mgx:receivesAction",
|
|
4554
|
+
]);
|
|
4555
|
+
const REVERSE_PREDICATE_MARKERS = Object.entries(FACT_PREDICATE_PHRASES)
|
|
4556
|
+
.filter(([predicate]) => !REVERSE_PREDICATE_EXCLUDE.has(predicate))
|
|
4557
|
+
.map(([predicate, phrase]) => ({
|
|
4558
|
+
predicate,
|
|
4559
|
+
re: new RegExp(`^what\\s+${escapeRegex(phrase)}\\s+(.+?)[?.!\\s]*$`, "i"),
|
|
4560
|
+
}))
|
|
4561
|
+
.sort((a, b) => b.re.source.length - a.re.source.length); // longest phrase first
|
|
4504
4562
|
// Widened 2026-07-11 (live-caught follow-up to the ambiguousParse fix, commit
|
|
4505
4563
|
// 5c858bf): on the FIRST turn of a graph-less session, dispatchTool's
|
|
4506
4564
|
// loadGraph() throws its own documented "the graph is empty... this repo
|
|
@@ -4542,12 +4600,69 @@ function uniqueFacts(rows) {
|
|
|
4542
4600
|
/** W4 seam: answer (or extend) a vocabulary/definition question from the MEMORY
|
|
4543
4601
|
* graph's Facts. Returns { text, replace } — `replace:false` means the engine's
|
|
4544
4602
|
* own (schema-docs) answer stands and the fact lines are appended under it —
|
|
4545
|
-
* or null when memory holds nothing relevant (misses stay unchanged).
|
|
4546
|
-
|
|
4603
|
+
* or null when memory holds nothing relevant (misses stay unchanged).
|
|
4604
|
+
* Exported (PLAN_VIZ_MEMORY.md Bug 1 fix) so src/memory-ask-browser-entry.mjs
|
|
4605
|
+
* can re-export it for `tmct viz`'s embedded "Ask the graph" panel — the ONLY
|
|
4606
|
+
* reason this is `export` rather than module-private; the function's own
|
|
4607
|
+
* behavior is unchanged (same signature, same logic, answers identically in
|
|
4608
|
+
* the CLI and the browser bundle). `memoryDir` may be memory/core.mjs's
|
|
4609
|
+
* Backend-B in-memory handle (`createInMemoryStore()`) as well as a real repo
|
|
4610
|
+
* path — every I/O this function does routes through `loadMemory(memoryDir)`
|
|
4611
|
+
* (via factRows/memoryFacts below), and loadMemory's own Backend-B branch
|
|
4612
|
+
* returns the handle's `payload` directly with ZERO fs calls — so a caller
|
|
4613
|
+
* that hands this a handle already carrying the embedded page's full graph
|
|
4614
|
+
* gets a pure, disk-free traversal, no bundle-time module shimming needed. */
|
|
4615
|
+
export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle = {}) {
|
|
4547
4616
|
let normFactTerm;
|
|
4548
4617
|
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
4549
4618
|
const q = String(query).trim();
|
|
4550
4619
|
|
|
4620
|
+
// (a-pre) "what is used for riding" / "what can be used for riding" / "what
|
|
4621
|
+
// is for riding" — the reverse-by-OBJECT mirror of BUG 1's forward reader
|
|
4622
|
+
// ("what is a tree used for", the (a) block just below). Checked BEFORE (a)
|
|
4623
|
+
// deliberately: this phrasing's leading "what is …" ALSO matches (a)'s own
|
|
4624
|
+
// BARE_WHATIS_RE, which would otherwise greedily treat "used for riding" as
|
|
4625
|
+
// one literal term to define — a guaranteed miss, since no vocabulary term
|
|
4626
|
+
// is ever named "used for riding" — and (a) always returns (hit, honest
|
|
4627
|
+
// per-relation "no", or null), never falling through to a later reader. Only
|
|
4628
|
+
// takes over when it finds a REAL hit; a non-match or zero-hit case falls
|
|
4629
|
+
// through unchanged to (a) and beyond, so ordinary miss messaging is
|
|
4630
|
+
// untouched. mgx:usedFor instead of mgx:hasA; same filter/rank/render/
|
|
4631
|
+
// paginate recipe as (b4)'s WHAT_HAS_RE below.
|
|
4632
|
+
const usedForQ = q.match(WHAT_USED_FOR_RE);
|
|
4633
|
+
if (usedForQ) {
|
|
4634
|
+
const variants = factTermVariants(normFactTerm, usedForQ[1]);
|
|
4635
|
+
const hits = (await factRows(memoryDir)).filter((f) => f.predicate === "mgx:usedFor" && variants.has(f.object));
|
|
4636
|
+
if (hits.length) {
|
|
4637
|
+
const ranked = rankByBiasThenTrust(uniqueFacts(hits), biasByBundle);
|
|
4638
|
+
const lines = ranked.map(renderFactLine);
|
|
4639
|
+
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
4640
|
+
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
4641
|
+
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
4642
|
+
return { text: shown.join("\n") + extra, replace: true, ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}) };
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4646
|
+
// (a-pre2) The generic derived cascade for every other reversible predicate
|
|
4647
|
+
// (REVERSE_PREDICATE_MARKERS, see its own docblock for the exclusion list
|
|
4648
|
+
// and why). Same checked-before-the-meta-lane placement and same
|
|
4649
|
+
// only-take-over-on-a-real-hit discipline as (a-pre) just above — a phrase
|
|
4650
|
+
// like "is found in"/"is made of" also starts with "what is …", so it must
|
|
4651
|
+
// run before (a) can greedily claim the whole tail as a literal term.
|
|
4652
|
+
for (const { predicate, re } of REVERSE_PREDICATE_MARKERS) {
|
|
4653
|
+
const m = q.match(re);
|
|
4654
|
+
if (!m) continue;
|
|
4655
|
+
const variants = factTermVariants(normFactTerm, m[1]);
|
|
4656
|
+
const hits = (await factRows(memoryDir)).filter((f) => f.predicate === predicate && variants.has(f.object));
|
|
4657
|
+
if (!hits.length) continue; // try the next candidate marker, don't give up yet
|
|
4658
|
+
const ranked = rankByBiasThenTrust(uniqueFacts(hits), biasByBundle);
|
|
4659
|
+
const lines = ranked.map(renderFactLine);
|
|
4660
|
+
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
4661
|
+
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
4662
|
+
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
4663
|
+
return { text: shown.join("\n") + extra, replace: true, ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}) };
|
|
4664
|
+
}
|
|
4665
|
+
|
|
4551
4666
|
// (a) meta-shaped questions ("what is a module", "what does cache mean") — the
|
|
4552
4667
|
// parsed object term, matched against fact SUBJECTS; consulted for hits (append
|
|
4553
4668
|
// alongside the schema-docs answer) and misses (facts answer alone) alike.
|
|
@@ -7817,8 +7932,22 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7817
7932
|
// further down is UNCHANGED, so a CamelCase term with no real hit still falls
|
|
7818
7933
|
// through to its existing miss handling, never the generic orientation card.
|
|
7819
7934
|
const isBareCamelCaseWhatisCandidate = conversationalCandidateBaseGate && isBareCamelCaseMetaQuestion(gateQuery);
|
|
7935
|
+
// 2026-07-12 follow-up to the used-for/reverse-predicate fix (factAnswer's
|
|
7936
|
+
// WHAT_USED_FOR_RE/REVERSE_PREDICATE_MARKERS, above): the SAME race BUG 2/
|
|
7937
|
+
// Tier-5/CamelCase already fixed above hits these too, and for the shortest
|
|
7938
|
+
// members of the family it's actually MORE likely to fire — "what wants
|
|
7939
|
+
// happiness" is exactly 3 words, none of them in STRUCT_WORDS, so
|
|
7940
|
+
// isConversational() claims it before factAnswer ever gets a turn, even
|
|
7941
|
+
// though a real mgx:desires fact answers it correctly once reached (proven:
|
|
7942
|
+
// the longer "what can be used for riding" already worked, since 5 words
|
|
7943
|
+
// clears isConversational's <=3-word gate outright — only the short
|
|
7944
|
+
// members of this family were ever actually broken). Same discipline as
|
|
7945
|
+
// every sibling exemption on this gate: matching the shape alone changes
|
|
7946
|
+
// nothing by itself, factAnswer below still only diverts on a REAL hit.
|
|
7947
|
+
const reversePredicateShape = WHAT_USED_FOR_RE.test(gateQuery)
|
|
7948
|
+
|| REVERSE_PREDICATE_MARKERS.some(({ re }) => re.test(gateQuery));
|
|
7820
7949
|
let bareMetaHit = null;
|
|
7821
|
-
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape)) {
|
|
7950
|
+
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape)) {
|
|
7822
7951
|
if (memoryDir) {
|
|
7823
7952
|
bareMetaHit = (await factAnswer(memoryDir, gateQuery, envelope, miss, biasByBundle))
|
|
7824
7953
|
?? (await factReadBack(memoryDir, gateQuery, envelope, miss, graph, newFocus?.label, biasByBundle));
|
|
@@ -9142,9 +9271,11 @@ export async function createSession({
|
|
|
9142
9271
|
// module-global state). "sqlite" selects Backend C (createSqliteMemoryStore
|
|
9143
9272
|
// — a live node:sqlite connection kept open for the session's lifetime,
|
|
9144
9273
|
// lazily imported only when this is actually chosen). TMCT_MEMORY_BACKEND
|
|
9145
|
-
// mirrors the TMCT_EPHEMERAL/TMCT_NARRATE on/off env convention.
|
|
9146
|
-
//
|
|
9147
|
-
//
|
|
9274
|
+
// mirrors the TMCT_EPHEMERAL/TMCT_NARRATE on/off env convention. This
|
|
9275
|
+
// parameter IS `bin/tmct.mjs`'s `tmct chat --memory-backend <...>` CLI flag
|
|
9276
|
+
// (a library/test caller can still set it directly) — the full precedence
|
|
9277
|
+
// (this param > TMCT_MEMORY_BACKEND env > tmct.toml's `[memory] backend` >
|
|
9278
|
+
// "default") is resolved below, once `toml` is known.
|
|
9148
9279
|
memoryBackend = null,
|
|
9149
9280
|
} = {}) {
|
|
9150
9281
|
// EPHEMERAL mode (--ephemeral, or TMCT_EPHEMERAL=1): read the target graph but
|
|
@@ -9185,6 +9316,13 @@ export async function createSession({
|
|
|
9185
9316
|
// instead of the whole repo.
|
|
9186
9317
|
let repo;
|
|
9187
9318
|
let config;
|
|
9319
|
+
// tmct.toml's normalized knobs (src/toml-config.mjs), captured alongside
|
|
9320
|
+
// `config` in whichever branch below resolves the graph path — used further
|
|
9321
|
+
// down for the memory-backend precedence (`toml.memory.backend`), so that
|
|
9322
|
+
// knob is honoured the same way regardless of which graph-resolution tier
|
|
9323
|
+
// fired. `null` when no branch could read a tmct.toml (never fatal — the
|
|
9324
|
+
// backend precedence below just skips this tier).
|
|
9325
|
+
let toml = null;
|
|
9188
9326
|
const explicitGraphs = (graphPaths || []).filter(Boolean);
|
|
9189
9327
|
if (explicitGraphs.length) {
|
|
9190
9328
|
repo = repoPath || gitRoot(cwd) || cwd;
|
|
@@ -9192,6 +9330,11 @@ export async function createSession({
|
|
|
9192
9330
|
config = resolvedGraphs.length > 1
|
|
9193
9331
|
? { graphFile: resolvedGraphs[0], graphFiles: resolvedGraphs }
|
|
9194
9332
|
: { graphFile: resolvedGraphs[0] };
|
|
9333
|
+
try {
|
|
9334
|
+
const argv = ["--repo", repo];
|
|
9335
|
+
if (configPath) argv.push("--config", configPath);
|
|
9336
|
+
({ toml } = await resolveRuntimeConfig({ argv, cwd, env: {}, gitRoot }));
|
|
9337
|
+
} catch { toml = null; }
|
|
9195
9338
|
} else if (repoPath) {
|
|
9196
9339
|
repo = repoPath;
|
|
9197
9340
|
// env is deliberately withheld from resolveRuntimeConfig here (passed as
|
|
@@ -9202,17 +9345,22 @@ export async function createSession({
|
|
|
9202
9345
|
// honored too.
|
|
9203
9346
|
const argv = ["--repo", repoPath];
|
|
9204
9347
|
if (configPath) argv.push("--config", configPath);
|
|
9205
|
-
({ config } = await resolveRuntimeConfig({ argv, cwd, env: {}, gitRoot }));
|
|
9348
|
+
({ config, toml } = await resolveRuntimeConfig({ argv, cwd, env: {}, gitRoot }));
|
|
9206
9349
|
} else {
|
|
9207
9350
|
const root = gitRoot(cwd);
|
|
9208
9351
|
repo = root || cwd;
|
|
9209
9352
|
const envGraph = env.TMCT_GRAPH_FILE && String(env.TMCT_GRAPH_FILE).trim();
|
|
9210
9353
|
if (envGraph) {
|
|
9211
9354
|
config = loadConfig(env, cwd);
|
|
9355
|
+
try {
|
|
9356
|
+
const argv = [];
|
|
9357
|
+
if (configPath) argv.push("--config", configPath);
|
|
9358
|
+
({ toml } = await resolveRuntimeConfig({ argv, cwd, env: {}, gitRoot }));
|
|
9359
|
+
} catch { toml = null; }
|
|
9212
9360
|
} else {
|
|
9213
9361
|
const argv = [];
|
|
9214
9362
|
if (configPath) argv.push("--config", configPath);
|
|
9215
|
-
({ config } = await resolveRuntimeConfig({ argv, cwd, env, gitRoot }));
|
|
9363
|
+
({ config, toml } = await resolveRuntimeConfig({ argv, cwd, env, gitRoot }));
|
|
9216
9364
|
}
|
|
9217
9365
|
}
|
|
9218
9366
|
|
|
@@ -9297,20 +9445,22 @@ export async function createSession({
|
|
|
9297
9445
|
// `closeMemoryStore` is a no-op unless Backend C actually opened a
|
|
9298
9446
|
// connection (Backend C's node:sqlite import is lazy — it only happens if
|
|
9299
9447
|
// this branch is actually taken).
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9448
|
+
//
|
|
9449
|
+
// Precedence — CLI flag > env > tmct.toml > default — matches the graph-path
|
|
9450
|
+
// precedence documented above: `memoryBackend` here is `tmct chat
|
|
9451
|
+
// --memory-backend <...>`'s already-resolved value; TMCT_MEMORY_BACKEND is
|
|
9452
|
+
// the env tier; `toml.memory.backend` is tmct.toml's `[memory] backend`
|
|
9453
|
+
// (src/toml-config.mjs). A toml value of "default" (or anything unrecognized)
|
|
9454
|
+
// falls through to Backend A below, same as an absent value always has.
|
|
9455
|
+
const backendChoice = String(memoryBackend || env.TMCT_MEMORY_BACKEND || toml?.memory?.backend || "").trim().toLowerCase();
|
|
9456
|
+
// openMemoryBackend (memory/core.mjs) is the ONE shared resolver for this
|
|
9457
|
+
// seam — src/init.mjs's corpus seed and bin/tmct.mjs's --corpus/--ontology/
|
|
9458
|
+
// --lexicon activation now call the exact same function, so a repo's
|
|
9459
|
+
// seeded facts and its chat-taught facts always land in the same backend
|
|
9460
|
+
// (a split-brain bug found in review: init used to always seed Backend A
|
|
9461
|
+
// regardless of the configured backend).
|
|
9462
|
+
const { openMemoryBackend } = await import("./memory/core.mjs");
|
|
9463
|
+
const { dir: memoryDir, close: closeMemoryStore } = await openMemoryBackend(repo, backendChoice);
|
|
9314
9464
|
|
|
9315
9465
|
const empty = graph.individuals.length === 0;
|
|
9316
9466
|
// W3: FIRST RUN in a graph-less repo seeds a capped ConceptNet slice into
|
package/src/cli-args.mjs
CHANGED
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
// precedence chain — built on top of toml-config.mjs's already-tested
|
|
6
6
|
// mergeEffective/normalizeConfig (arg > toml > default), not a rebuild of it.
|
|
7
7
|
//
|
|
8
|
-
//
|
|
8
|
+
// Four tiny flag helpers (pure, no I/O) plus the one async resolver:
|
|
9
9
|
// strFlag(rest, names, dflt) → single value, last flag occurrence wins
|
|
10
10
|
// repeatedFlag(rest, names) → every value for a repeatable flag (e.g. --graph)
|
|
11
11
|
// boolFlag(rest, names) → true if any of `names` appears at all
|
|
12
|
+
// enumFlag(rest, names, choices) → strFlag, validated against a closed set
|
|
13
|
+
// (throws a clear error naming the flag + the choices — the shared shape
|
|
14
|
+
// for a closed-choice option like `--memory-backend default|memory|sqlite`,
|
|
15
|
+
// matching `--with-persona`'s own "unknown name" error style)
|
|
12
16
|
// resolveRuntimeConfig({argv, cwd, env, gitRoot}) → the resolved repo/config
|
|
13
17
|
//
|
|
14
18
|
// Graph-path precedence (documented once, here — every subcommand shares it):
|
|
@@ -69,6 +73,21 @@ export function boolFlag(rest, names) {
|
|
|
69
73
|
return rest.some((r) => list.includes(r));
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
/** Closed-choice single-value flag: `strFlag` plus validation against
|
|
77
|
+
* `choices`. Returns `undefined` when absent (never a default — the caller
|
|
78
|
+
* decides what "absent" means, same as an omitted `strFlag` call). Throws a
|
|
79
|
+
* clear, user-facing error naming the flag and the valid choices when a
|
|
80
|
+
* value IS given but isn't one of them — validate-before-any-disk-write,
|
|
81
|
+
* the same discipline `tmct init --with-persona <unknown>` already uses. */
|
|
82
|
+
export function enumFlag(rest, names, choices) {
|
|
83
|
+
const val = strFlag(rest, names, undefined);
|
|
84
|
+
if (val !== undefined && !choices.includes(val)) {
|
|
85
|
+
const flagName = asList(names)[0];
|
|
86
|
+
throw new Error(`invalid ${flagName} "${val}". Choices: ${choices.join(", ")}.`);
|
|
87
|
+
}
|
|
88
|
+
return val;
|
|
89
|
+
}
|
|
90
|
+
|
|
72
91
|
/**
|
|
73
92
|
* Resolve one subcommand invocation's repo root, tmct.toml, and graph
|
|
74
93
|
* path(s) — the shared precedence chain every subcommand (chat/memory/init/
|