@polycode-projects/the-mechanical-code-talker 1.8.20 → 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 +144 -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
|
}
|
|
@@ -4508,6 +4523,42 @@ const WHAT_HAS_RE = /^what\s+has\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
|
4508
4523
|
// (object known, subject unknown), so it fell all the way through to the
|
|
4509
4524
|
// code-graph miss cascade — actively misleading for a pure vocabulary query.
|
|
4510
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
|
|
4511
4562
|
// Widened 2026-07-11 (live-caught follow-up to the ambiguousParse fix, commit
|
|
4512
4563
|
// 5c858bf): on the FIRST turn of a graph-less session, dispatchTool's
|
|
4513
4564
|
// loadGraph() throws its own documented "the graph is empty... this repo
|
|
@@ -4549,8 +4600,19 @@ function uniqueFacts(rows) {
|
|
|
4549
4600
|
/** W4 seam: answer (or extend) a vocabulary/definition question from the MEMORY
|
|
4550
4601
|
* graph's Facts. Returns { text, replace } — `replace:false` means the engine's
|
|
4551
4602
|
* own (schema-docs) answer stands and the fact lines are appended under it —
|
|
4552
|
-
* or null when memory holds nothing relevant (misses stay unchanged).
|
|
4553
|
-
|
|
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 = {}) {
|
|
4554
4616
|
let normFactTerm;
|
|
4555
4617
|
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
4556
4618
|
const q = String(query).trim();
|
|
@@ -4581,6 +4643,26 @@ async function factAnswer(memoryDir, query, envelope, miss, biasByBundle = {}) {
|
|
|
4581
4643
|
}
|
|
4582
4644
|
}
|
|
4583
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
|
+
|
|
4584
4666
|
// (a) meta-shaped questions ("what is a module", "what does cache mean") — the
|
|
4585
4667
|
// parsed object term, matched against fact SUBJECTS; consulted for hits (append
|
|
4586
4668
|
// alongside the schema-docs answer) and misses (facts answer alone) alike.
|
|
@@ -7850,8 +7932,22 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7850
7932
|
// further down is UNCHANGED, so a CamelCase term with no real hit still falls
|
|
7851
7933
|
// through to its existing miss handling, never the generic orientation card.
|
|
7852
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));
|
|
7853
7949
|
let bareMetaHit = null;
|
|
7854
|
-
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape)) {
|
|
7950
|
+
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape)) {
|
|
7855
7951
|
if (memoryDir) {
|
|
7856
7952
|
bareMetaHit = (await factAnswer(memoryDir, gateQuery, envelope, miss, biasByBundle))
|
|
7857
7953
|
?? (await factReadBack(memoryDir, gateQuery, envelope, miss, graph, newFocus?.label, biasByBundle));
|
|
@@ -9175,9 +9271,11 @@ export async function createSession({
|
|
|
9175
9271
|
// module-global state). "sqlite" selects Backend C (createSqliteMemoryStore
|
|
9176
9272
|
// — a live node:sqlite connection kept open for the session's lifetime,
|
|
9177
9273
|
// lazily imported only when this is actually chosen). TMCT_MEMORY_BACKEND
|
|
9178
|
-
// mirrors the TMCT_EPHEMERAL/TMCT_NARRATE on/off env convention.
|
|
9179
|
-
//
|
|
9180
|
-
//
|
|
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.
|
|
9181
9279
|
memoryBackend = null,
|
|
9182
9280
|
} = {}) {
|
|
9183
9281
|
// EPHEMERAL mode (--ephemeral, or TMCT_EPHEMERAL=1): read the target graph but
|
|
@@ -9218,6 +9316,13 @@ export async function createSession({
|
|
|
9218
9316
|
// instead of the whole repo.
|
|
9219
9317
|
let repo;
|
|
9220
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;
|
|
9221
9326
|
const explicitGraphs = (graphPaths || []).filter(Boolean);
|
|
9222
9327
|
if (explicitGraphs.length) {
|
|
9223
9328
|
repo = repoPath || gitRoot(cwd) || cwd;
|
|
@@ -9225,6 +9330,11 @@ export async function createSession({
|
|
|
9225
9330
|
config = resolvedGraphs.length > 1
|
|
9226
9331
|
? { graphFile: resolvedGraphs[0], graphFiles: resolvedGraphs }
|
|
9227
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; }
|
|
9228
9338
|
} else if (repoPath) {
|
|
9229
9339
|
repo = repoPath;
|
|
9230
9340
|
// env is deliberately withheld from resolveRuntimeConfig here (passed as
|
|
@@ -9235,17 +9345,22 @@ export async function createSession({
|
|
|
9235
9345
|
// honored too.
|
|
9236
9346
|
const argv = ["--repo", repoPath];
|
|
9237
9347
|
if (configPath) argv.push("--config", configPath);
|
|
9238
|
-
({ config } = await resolveRuntimeConfig({ argv, cwd, env: {}, gitRoot }));
|
|
9348
|
+
({ config, toml } = await resolveRuntimeConfig({ argv, cwd, env: {}, gitRoot }));
|
|
9239
9349
|
} else {
|
|
9240
9350
|
const root = gitRoot(cwd);
|
|
9241
9351
|
repo = root || cwd;
|
|
9242
9352
|
const envGraph = env.TMCT_GRAPH_FILE && String(env.TMCT_GRAPH_FILE).trim();
|
|
9243
9353
|
if (envGraph) {
|
|
9244
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; }
|
|
9245
9360
|
} else {
|
|
9246
9361
|
const argv = [];
|
|
9247
9362
|
if (configPath) argv.push("--config", configPath);
|
|
9248
|
-
({ config } = await resolveRuntimeConfig({ argv, cwd, env, gitRoot }));
|
|
9363
|
+
({ config, toml } = await resolveRuntimeConfig({ argv, cwd, env, gitRoot }));
|
|
9249
9364
|
}
|
|
9250
9365
|
}
|
|
9251
9366
|
|
|
@@ -9330,20 +9445,22 @@ export async function createSession({
|
|
|
9330
9445
|
// `closeMemoryStore` is a no-op unless Backend C actually opened a
|
|
9331
9446
|
// connection (Backend C's node:sqlite import is lazy — it only happens if
|
|
9332
9447
|
// this branch is actually taken).
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
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);
|
|
9347
9464
|
|
|
9348
9465
|
const empty = graph.individuals.length === 0;
|
|
9349
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/
|