@polycode-projects/the-mechanical-code-talker 6.0.21 → 7.0.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.
Files changed (36) hide show
  1. package/README.md +5 -15
  2. package/bin/tmct.mjs +8 -3
  3. package/package.json +1 -5
  4. package/src/adapters/memory/core.mjs +108 -96
  5. package/src/domain/agent-traits.mjs +1 -1
  6. package/src/domain/answer-variants.json +1 -1
  7. package/src/domain/cli-verbs.mjs +1 -0
  8. package/src/domain/memory/causal-stability.mjs +22 -5
  9. package/src/domain/memory/fact-order.mjs +3 -3
  10. package/src/domain/memory/provenance-time.mjs +35 -0
  11. package/src/domain/memory/retraction.mjs +3 -5
  12. package/src/domain/memory/trust.mjs +11 -11
  13. package/src/domain/news-feed.mjs +1 -1
  14. package/src/domain/seeded-random.mjs +6 -6
  15. package/src/services/adventure.mjs +10 -41
  16. package/src/services/chat-page-viz.mjs +12 -1111
  17. package/src/services/chat-session.mjs +20 -6
  18. package/src/services/chat.mjs +269 -197
  19. package/src/services/extract-facts.mjs +1 -1
  20. package/src/services/import-file.mjs +1 -1
  21. package/src/services/mud-viz.mjs +21 -1082
  22. package/src/services/mudiii-viz.mjs +0 -4
  23. package/src/services/pill-complete.mjs +5 -8
  24. package/src/services/predator-prey.mjs +3 -5
  25. package/src/surfaces/web/memory-ask-browser.bundle.js +107 -107
  26. package/src/surfaces/web/mud-browser-entry.mjs +8 -48
  27. package/test-benchmarks/agentbench/README.md +7 -10
  28. package/src/adapters/p2p/webrtc-transport.mjs +0 -169
  29. package/src/domain/p2p/facts.mjs +0 -102
  30. package/src/domain/p2p/peer-id.mjs +0 -47
  31. package/src/domain/p2p/provenance-relabel.mjs +0 -37
  32. package/src/domain/p2p/sync-filter.mjs +0 -43
  33. package/src/domain/p2p/wire.mjs +0 -126
  34. package/src/services/p2p-room.mjs +0 -848
  35. package/src/services/share-overlay-viz.mjs +0 -623
  36. package/src/surfaces/web/p2p-browser-entry.mjs +0 -39
@@ -1741,7 +1741,7 @@ const HONEST_MISS_PHRASES = [
1741
1741
  ];
1742
1742
  /** "whats 2+2" — a bare arithmetic expression, not a code/vocabulary question
1743
1743
  * at all. With no closed-set match of its own, this fell into the SAME
1744
- * "≤3 words, not code-ish" catch-all a genuine orientation opener
1744
+ * ≤3-word catch-all a genuine orientation opener
1745
1745
  * ("what's up", "so what is this") uses, giving the non-sequitur identity
1746
1746
  * blurb where an honest "I don't do arithmetic" decline belongs. Deliberately
1747
1747
  * excludes "-" from the operator set: this domain's OWN dates ("what
@@ -1759,8 +1759,17 @@ const ARITHMETIC_RE = /\d+\s*[+*/]\s*\d+/;
1759
1759
  * ("update the readme") is never caught here — "table"/"from"/"into"/"set"
1760
1760
  * are the words that make this unambiguously SQL rather than English. */
1761
1761
  const SQL_STATEMENT_RE = /^(?:(?:drop|truncate|alter)\s+table|delete\s+from|insert\s+into|update\s+[a-z0-9_.]+\s+set)\s+[a-z0-9_.]+\b/i;
1762
- /** The structural verbs/nouns that mark a near-miss code question (→ keep the
1763
- * precise grammar hint, not the friendly nudge). */
1762
+ /** Structural verbs and relation nouns the teach lanes must not mint a general
1763
+ * predicate from while the code-graph switch is on. "a commit touches b" and
1764
+ * "a class contains a method" describe the graph's own edge vocabulary, so a
1765
+ * session declared as asking about code would shadow the built-in relation
1766
+ * with a user-taught one of the same name.
1767
+ *
1768
+ * With the switch off these are ordinary English and mint like any other
1769
+ * verb: "ada uses a spoon", "the box contains apples", "a tester tests".
1770
+ * Every reader takes `codeGraphMode` and consults this set only when it is
1771
+ * true, so the declared switch decides and the wording never does. Every
1772
+ * entry is lowercase; the lanes lowercase the candidate verb first. */
1764
1773
  const STRUCT_WORDS = new Set([
1765
1774
  "import", "imports", "call", "calls", "use", "uses", "define", "defines", "defined",
1766
1775
  "class", "classes", "function", "functions", "module", "modules", "method", "methods",
@@ -1775,35 +1784,48 @@ const STRUCT_WORDS = new Set([
1775
1784
  "testing", "defining", "touching", "extending", "inheritance", "coverage", "member", "members",
1776
1785
  ]);
1777
1786
 
1778
- /** Is this raw/normalized query "code-ish" (a dotted/pathed/CamelCase name, "()",
1779
- * or a structural keyword)? Shared by isConversational and the fuzzy-typo fallback
1780
- * so neither ever grabs a genuine near-miss structural question. */
1781
- function looksCodeish(raw, q) {
1782
- return /[a-z][A-Z]|[_./]|\(\)/.test(raw) || q.split(/\s+/).some((w) => STRUCT_WORDS.has(w));
1783
- }
1784
-
1785
- /** Does this look like small-talk / an orientation request rather than a
1786
- * (near-miss) structural question? Greetings & help/identity phrases always
1787
- * qualify; a very short input with no code-ish token does too. */
1788
- export function isConversational(query) {
1787
+ /** Is the line one of the enumerated small-talk phrasings — a greeting, a thanks,
1788
+ * an acknowledgement, a "what can you do", a "who are you", an "are you an AI"?
1789
+ * Every one of these is a closed list, so a line qualifies by being a member and
1790
+ * never by how it happens to be spelled. */
1791
+ function matchesSmallTalkPhrase(query) {
1789
1792
  const raw = String(query).trim();
1790
1793
  const q = raw.toLowerCase().replace(/[.!?]+$/, "").trim();
1791
1794
  if (GREET.has(q) || THANKS.has(q) || OK_ACK.has(q)) return true;
1792
1795
  if (CAPABILITY_PHRASES.some((re) => re.test(raw))) return true;
1793
1796
  if (IDENTITY_PHRASES.some((re) => re.test(raw))) return true;
1794
- if (aiIdentityMatch(raw)) return true;
1795
- const codeish = looksCodeish(raw, q);
1796
- // The catch-all counts words, so a contraction decides the turn on
1797
- // punctuation alone: "what's on peg-a" counts 3 and gets the orientation
1798
- // card, "what is on peg-a" counts 4 and gets the answer. Write the
1799
- // contraction out for the count only.
1800
- //
1801
- // The count is the whole reason this is expandContractions and not
1802
- // normalizeQuery: the fuller pass strips filler, which takes the count DOWN,
1803
- // and sends "please describe a dog" and "tell me about a dog" to the card
1804
- // instead. `q` itself is left alone so the GREET/THANKS/OK_ACK membership
1805
- // above reads the text as typed, and looksCodeish reads `raw`.
1806
- return expandContractions(q).split(/\s+/).filter(Boolean).length <= 3 && !codeish;
1797
+ return aiIdentityMatch(raw);
1798
+ }
1799
+
1800
+ /** Is the line short enough for the chat lane to treat as an orientation opener
1801
+ * rather than a question? Three words or fewer, counted after contractions are
1802
+ * written out — so a contraction decides the turn on punctuation alone
1803
+ * otherwise: "what's on peg-a" counts 3 and gets the orientation card, "what is
1804
+ * on peg-a" counts 4 and gets the answer.
1805
+ *
1806
+ * The count is the whole reason this is expandContractions and not
1807
+ * normalizeQuery: the fuller pass strips filler, which takes the count DOWN,
1808
+ * and sends "please describe a dog" and "tell me about a dog" to the card
1809
+ * instead. */
1810
+ function isSmallTalkLength(query) {
1811
+ const q = String(query).trim().toLowerCase().replace(/[.!?]+$/, "").trim();
1812
+ return expandContractions(q).split(/\s+/).filter(Boolean).length <= 3;
1813
+ }
1814
+
1815
+ /** Does this look like small-talk / an orientation request rather than a
1816
+ * structural question? This is the chat lane's rule: the enumerated phrasings
1817
+ * always qualify, and a very short input does too, unless the session declared
1818
+ * code-graph mode. The tool seam uses the phrase lists alone (askFallback) —
1819
+ * a three-word tool request there is a request, not an opener.
1820
+ *
1821
+ * `codeGraphMode` is the session's declared switch (`/code-graph on|off`, or
1822
+ * the `codeGraphMode` option a host sets). Nothing here reads the text for
1823
+ * code-shaped tokens: what the user is asking about is declared, never
1824
+ * guessed. With the switch on, every line is a question about the code graph,
1825
+ * so the length rule stands down. */
1826
+ export function isConversational(query, { codeGraphMode = false } = {}) {
1827
+ if (matchesSmallTalkPhrase(query)) return true;
1828
+ return !codeGraphMode && isSmallTalkLength(query);
1807
1829
  }
1808
1830
 
1809
1831
  /** The words that, in the slot of a short "what is X" / "who is X", name the
@@ -1853,9 +1875,11 @@ const BACKED_TOOLS = new Set([
1853
1875
  * "untested") → that tool with its argument bound from the exact arg key the
1854
1876
  * dispatchTool switch reads (COMMANDS above). Only when the tool is
1855
1877
  * declared by the caller.
1856
- * 2. Otherwise, a non-conversational structural question → tmct_ask{query:…},
1857
- * when tmct_ask is declared. Small-talk (isConversational) never emits a
1858
- * call it falls through to a text answer.
1878
+ * 2. Otherwise, a structural question → tmct_ask{query:…}, when tmct_ask is
1879
+ * declared. Only the enumerated small-talk phrasings
1880
+ * (matchesSmallTalkPhrase) fall through to a text answer instead. The chat
1881
+ * lane's length rule is not applied here: a host asks this seam for a tool
1882
+ * by sending a request, and "what calls fnAlpha" is three words.
1859
1883
  *
1860
1884
  * Returns { name, input } or null (→ answer as text).
1861
1885
  */
@@ -1884,10 +1908,11 @@ export function selectTool(text, declaredNames) {
1884
1908
  return askFallback(t, declaredNames);
1885
1909
  }
1886
1910
 
1887
- /** The tmct_ask fallback: emit tmct_ask{query} for a non-conversational line when
1888
- * the caller declared tmct_ask; otherwise null (→ text answer). */
1911
+ /** The tmct_ask fallback: emit tmct_ask{query} when the caller declared tmct_ask
1912
+ * and the line isn't one of the enumerated small-talk phrasings; otherwise null
1913
+ * (→ text answer). */
1889
1914
  function askFallback(text, declaredNames) {
1890
- if (declaredNames.has("tmct_ask") && BACKED_TOOLS.has("tmct_ask") && !isConversational(text)) {
1915
+ if (declaredNames.has("tmct_ask") && BACKED_TOOLS.has("tmct_ask") && !matchesSmallTalkPhrase(text)) {
1891
1916
  return { name: "tmct_ask", input: { query: text } };
1892
1917
  }
1893
1918
  return null;
@@ -1910,42 +1935,25 @@ export function capabilityPlanDeps() {
1910
1935
  };
1911
1936
  }
1912
1937
 
1913
- /** Scoped exemption for the bare-meta-fact lane (2b/2c, further down this file)
1914
- * ONLY never a change to looksCodeish()/isConversational() themselves, and
1915
- * never used for the generic orientation-card fallback. A bare "what is
1916
- * TaskController?" (CamelCase COMPOUND class name, no article) hits
1917
- * looksCodeish()'s `/[a-z][A-Z]/` branch, so isConversational() returns false
1918
- * and the whole isConversationalCandidate gate — including the bare-meta-fact
1919
- * lookup that "what is a TaskController" (articled) already resolves through
1920
- * — never runs. This re-tests the SAME non-CamelCase codeish reasons (paths,
1921
- * dotted refs, `()` calls, STRUCT_WORDS) looksCodeish already covers, so a
1922
- * genuine near-miss structural question ("what is foo.bar()", "what is
1923
- * import") is unaffected. */
1924
- function isBareCamelCaseMetaQuestion(query) {
1938
+ /** A short bare "what is X?" / "is X <adjective>?" with no article — the shape
1939
+ * the bare-meta-fact lane (2b/2c, further down this file) reads. Used ONLY to
1940
+ * widen that lane's gate, never the generic orientation-card fallback. Lane
1941
+ * (2b) still diverts only on a REAL hit, so a term with nothing behind it
1942
+ * falls through to the ordinary card either way. */
1943
+ function isBareMetaQuestionShape(query) {
1925
1944
  const raw = String(query).trim();
1926
1945
  const q = raw.toLowerCase().replace(/[.!?]+$/, "").trim();
1927
- const nonCamelCodeish = /[_./]|\(\)/.test(raw) || q.split(/\s+/).some((w) => STRUCT_WORDS.has(w));
1928
- if (nonCamelCodeish || q.split(/\s+/).filter(Boolean).length > 3) return false;
1946
+ if (q.split(/\s+/).filter(Boolean).length > 3) return false;
1929
1947
  return BARE_WHATIS_RE.test(raw) || IS_ADJECTIVE_YESNO_RE.test(raw);
1930
1948
  }
1931
1949
 
1932
- /** The same scoped exemption for the WRAPPERLESS form, lane (2c) only: a bare
1933
- * "TaskController" typed on its own. isBareCamelCaseMetaQuestion above needs a
1934
- * "what is X" / "is X <adjective>" wrapper, so a bare CamelCase name still
1935
- * stops at looksCodeish()'s `/[a-z][A-Z]/` branch while its lowercase twin
1936
- * ("task") reaches the lane and answers.
1937
- *
1938
- * A single unbroken word is the whole shape (2c looks the raw line up as a
1939
- * label), and that shape is what keeps the exemption at the CamelCase reason
1940
- * and nothing else: a path, a dotted ref, a `()` call or any multi-word
1941
- * near-miss structural question ("what is import") can't be one bare word, and
1942
- * every STRUCT_WORDS member is lowercase, so the CamelCase requirement leaves
1943
- * them all where they are. Lane (2c) still only diverts on a real, unique
1944
- * graph hit — an unknown CamelCase word answers exactly as it does now. */
1945
- function isBareCamelCaseEntityName(query) {
1946
- const raw = String(query).trim();
1947
- if (!/^[A-Za-z][A-Za-z0-9]*$/.test(raw)) return false;
1948
- return /[a-z][A-Z]/.test(raw);
1950
+ /** The WRAPPERLESS form the same lane reads, (2c) only: a bare "TaskController"
1951
+ * or "task" typed on its own, with no "what is X" wrapper around it. One
1952
+ * unbroken word is the whole shape lane (2c) looks the raw line up as a
1953
+ * graph label and diverts only on a real, unique hit, so a word naming
1954
+ * nothing answers exactly as it did before. */
1955
+ function isBareEntityNameShape(query) {
1956
+ return /^[A-Za-z][A-Za-z0-9]*$/.test(String(query).trim());
1949
1957
  }
1950
1958
 
1951
1959
  // ---- the response-template library (W1: templates → render path) ----
@@ -2175,9 +2183,9 @@ function closedOrCollapsed(q, set, idx) {
2175
2183
  * - fold an exact word-repeated clause ("bye bye") to one instance before
2176
2184
  * matching BYE — informal reduplication for emphasis, not a new phrase
2177
2185
  * Still the SAME closed THANKS/BYE sets underneath (same closedOrCollapsed
2178
- * matcher) — only the SEGMENTATION generalizes. Bounded to short, non-codeish
2179
- * lines (same discipline as isConversational/fuzzyConversationalMatch) so a
2180
- * genuine structural question is never grabbed. A single-clause line (no
2186
+ * matcher) — only the SEGMENTATION generalizes. Bounded to short lines (same
2187
+ * discipline as isConversational/fuzzyConversationalMatch), and off entirely
2188
+ * in code-graph mode. A single-clause line (no
2181
2189
  * comma/semicolon/"and") is left to the exact whole-line checks above/below —
2182
2190
  * this only handles the MULTI-clause case those can't. Returns "bye"/"thanks"/
2183
2191
  * null; bye wins when a line carries both — a farewell should end the
@@ -2253,12 +2261,13 @@ const isClosingFillerClause = (c) => CLOSING_FILLER_CLAUSES.has(c) || CLOSING_FI
2253
2261
  * "thanks for all your help") — stripped before the closed THANKS lookup so the
2254
2262
  * bare "thanks" underneath matches. */
2255
2263
  const THANKS_HELP_TAIL_RE = /\s+for\s+(?:the\s+|your\s+|all\s+|all\s+the\s+|all\s+your\s+)?help\s*$/i;
2256
- function farewellOrThanksSignal(raw, q) {
2264
+ function farewellOrThanksSignal(q, codeGraphMode) {
2265
+ if (codeGraphMode) return null;
2257
2266
  const words = q.split(/\s+/).filter(Boolean);
2258
2267
  // The upper bound is generous because the real safety is the per-clause gate
2259
2268
  // below (every non-thanks clause must itself be small-talk-shaped or a curated
2260
2269
  // closing-filler clause), not the total word count.
2261
- if (words.length < 2 || words.length > 16 || looksCodeish(raw, q)) return null;
2270
+ if (words.length < 2 || words.length > 16) return null;
2262
2271
  const clauses = conversationalClauses(q);
2263
2272
  if (clauses.length < 2) return null; // single-clause lines: the exact whole-line checks own this
2264
2273
  // OK_ACK is deliberately NOT a signal here (unlike the exact whole-line check
@@ -2269,7 +2278,7 @@ function farewellOrThanksSignal(raw, q) {
2269
2278
  // as a thanks-signal regressed exactly that live case. THANKS itself is more
2270
2279
  // specific (genuine gratitude words rarely lead into an unrelated question),
2271
2280
  // but still gated below: a THANKS-hit only counts when every OTHER clause is
2272
- // itself small-talk-shaped (≤3 words, non-codeish) — the SAME bound
2281
+ // itself small-talk-shaped (≤3 words) — the SAME bound
2273
2282
  // isConversational's own catch-all uses — OR a curated closing-filler clause
2274
2283
  // (CLOSING_FILLER_CLAUSES, above) — so "cheers, what does X do" is still left
2275
2284
  // to the existing THANKS_PREAMBLE_RE lane, never grabbed here.
@@ -2286,7 +2295,7 @@ function farewellOrThanksSignal(raw, q) {
2286
2295
  if (byeHit) return "bye";
2287
2296
  const thanksHit = thanksClauseIdx >= 0 && clauses.every((c, i) => i === thanksClauseIdx
2288
2297
  || isClosingFillerClause(c)
2289
- || (c.split(/\s+/).filter(Boolean).length <= 3 && !looksCodeish(c, c.toLowerCase())));
2298
+ || c.split(/\s+/).filter(Boolean).length <= 3);
2290
2299
  return thanksHit ? "thanks" : null;
2291
2300
  }
2292
2301
 
@@ -2294,9 +2303,9 @@ function farewellOrThanksSignal(raw, q) {
2294
2303
  * line, an ack lead-in peeled off a dismissal, or a short line whose every word
2295
2304
  * is laughter / an ack / a single-word dismissal with at least one laughter or
2296
2305
  * dismissal word (so a bare "ok"/"sure" still falls to the ack lane, not here).
2297
- * Never fires on a codeish line. */
2298
- function dismissalSignal(q) {
2299
- if (looksCodeish(q, q)) return false;
2306
+ * Stands down entirely in code-graph mode. */
2307
+ function dismissalSignal(q, codeGraphMode) {
2308
+ if (codeGraphMode) return false;
2300
2309
  if (DISMISSAL.has(q) || LAUGHTER.has(q)) return true;
2301
2310
  const words = q.split(/\s+/).filter(Boolean);
2302
2311
  if (words.length < 2 || words.length > 5) return false;
@@ -2353,14 +2362,17 @@ function expandShorthandContractions(text) {
2353
2362
  /** UNIQUE within-bound fuzzy match of the whole trimmed line against
2354
2363
  * CONVERSATIONAL_PHRASES — the "helo"/"thnx"/"byee" tier, plus (after shorthand
2355
2364
  * contraction expansion above) "waht r u"/"wat r u"-style texting shorthand.
2356
- * Restricted to short (≤4-word), non-code-ish inputs (looksCodeish, shared with
2357
- * isConversational) so a genuine near-miss structural question is never grabbed;
2358
- * a distance tie is refused, never guessed (same discipline as fuzzyVocabWord). */
2359
- function fuzzyConversationalMatch(raw) {
2365
+ * Restricted to short (≤4-word) inputs, and stands down entirely in code-graph
2366
+ * mode; a distance tie is refused, never guessed (same discipline as
2367
+ * fuzzyVocabWord). A leading "/" is refused outright: the slash is tmct's own
2368
+ * command sigil, so "/help" is a command the user declared, not a line one
2369
+ * edit away from the small-talk phrase "help". */
2370
+ function fuzzyConversationalMatch(raw, codeGraphMode) {
2371
+ if (codeGraphMode || String(raw).trim().startsWith("/")) return null;
2360
2372
  const expanded = expandShorthandContractions(raw);
2361
2373
  const q = collapseRuns(expanded.toLowerCase().replace(/[.!?]+$/, "").trim());
2362
2374
  const words = q.split(/\s+/).filter(Boolean);
2363
- if (!words.length || words.length > 4 || looksCodeish(raw, q)) return null;
2375
+ if (!words.length || words.length > 4) return null;
2364
2376
  return fuzzyMatchInSet(q, CONVERSATIONAL_PHRASES, Math.min(2, fuzzyBound(q)));
2365
2377
  }
2366
2378
 
@@ -2406,14 +2418,18 @@ const identitySelfTemplateId = (uiContext, codeDomainActive) =>
2406
2418
 
2407
2419
  /** The "here's what you CAN ask" tail on a decline that names no term of its own
2408
2420
  * (the SQL-statement and arithmetic shapes below). `ctx.vocabHint` already
2409
- * carries the session-gated vocabulary/teach pointer, so only the CODE-graph
2410
- * half needs the surface split: pointing at a repo is a command a terminal can
2411
- * run and a page cannot. */
2421
+ * carries the session-gated vocabulary/teach pointer. The repo pointer needs
2422
+ * two things on top: a session that declared code-graph mode, and a terminal
2423
+ * to run the command in. */
2412
2424
  const offRampClause = (ctx) => {
2413
2425
  const hint = ctx.vocabHint || 'Try "what is a dog" for vocabulary.';
2414
- return ctx.uiContext === "browser" ? hint : `${hint} Or point me at a repo with --repo <path>.`;
2426
+ return (ctx.uiContext === "browser" || !ctx.codeGraphMode) ? hint : `${hint} Or point me at a repo with --repo <path>.`;
2415
2427
  };
2416
2428
 
2429
+ /** What a decline that names no term of its own says it DOES answer. The code
2430
+ * graph is named only when the session declared code-graph mode. */
2431
+ const answerableSubjects = (ctx) => (ctx.codeGraphMode ? "a code graph or taught facts" : "taught facts");
2432
+
2417
2433
  /** Recognise a conversational expression and return a templated turn result, or null
2418
2434
  * to fall through to counts/ask. Handled BEFORE slash-commands' non-slash siblings:
2419
2435
  * greetings, thanks, help/orientation, farewell (ends the session via `end:true`),
@@ -2462,7 +2478,7 @@ function conversationalTurn(line, ctx) {
2462
2478
  // "thanks, bye") — see farewellOrThanksSignal's own docblock. Never fires
2463
2479
  // on a single-clause line (those are the exact checks just above/below),
2464
2480
  // so this only ADDS coverage, never shadows it.
2465
- const signal = farewellOrThanksSignal(raw, q);
2481
+ const signal = farewellOrThanksSignal(q, ctx.codeGraphMode);
2466
2482
  if (signal === "bye") {
2467
2483
  note(ctx.trace, "goal: casual/social — ending the session (no graph intent)");
2468
2484
  note(ctx.trace, "lane: conversational — farewell (multi-clause phrase-shape match)");
@@ -2520,7 +2536,7 @@ function conversationalTurn(line, ctx) {
2520
2536
  return mk(t(T_THANKS), { lane: "thanks" });
2521
2537
  }
2522
2538
  }
2523
- if (dismissalSignal(q)) {
2539
+ if (dismissalSignal(q, ctx.codeGraphMode)) {
2524
2540
  note(ctx.trace, "goal: casual/social — dismissal/laughter, no graph intent");
2525
2541
  note(ctx.trace, "lane: conversational — dismissal (DISMISSAL/LAUGHTER closed set)");
2526
2542
  return mk(t(T_DISMISSAL), { lane: "thanks" });
@@ -2544,7 +2560,7 @@ function conversationalTurn(line, ctx) {
2544
2560
  note(ctx.trace, "goal: nonsense input shaped like a SQL statement — a targeted decline, not the identity blurb");
2545
2561
  note(ctx.trace, "lane: conversational — SQL-statement decline (SQL_STATEMENT_RE)");
2546
2562
  return mk(
2547
- `That reads like a SQL statement, not a question about a code graph or taught facts. ${offRampClause(ctx)}`,
2563
+ `That reads like a SQL statement, not a question about ${answerableSubjects(ctx)}. ${offRampClause(ctx)}`,
2548
2564
  { lane: "help" },
2549
2565
  );
2550
2566
  }
@@ -2552,7 +2568,7 @@ function conversationalTurn(line, ctx) {
2552
2568
  note(ctx.trace, "goal: arithmetic — not a code/vocabulary question, an honest decline");
2553
2569
  note(ctx.trace, "lane: conversational — arithmetic decline (ARITHMETIC_RE)");
2554
2570
  return mk(
2555
- `I don't do arithmetic — I answer questions about a code graph or taught facts. ${offRampClause(ctx)}`,
2571
+ `I don't do arithmetic — I answer questions about ${answerableSubjects(ctx)}. ${offRampClause(ctx)}`,
2556
2572
  { lane: "help" },
2557
2573
  );
2558
2574
  }
@@ -2583,12 +2599,11 @@ function conversationalTurn(line, ctx) {
2583
2599
  }
2584
2600
  // Fuzzy-typo fallback (A4): every exact/collapsed closed-set lookup above missed —
2585
2601
  // try a bounded edit-distance match against the flattened conversational phrase
2586
- // pool ("helo", "thnx", "wat r u", "byee"), restricted to short non-code-ish
2587
- // input so a genuine near-miss structural question is never grabbed. Skipped
2588
- // entirely mid-game (see gameActive above) never reached the CLI's own
2589
- // process-exit path from a guess before this fix existed.
2602
+ // pool ("helo", "thnx", "wat r u", "byee"), restricted to short input.
2603
+ // Skipped entirely mid-game (see gameActive above) never reached the CLI's
2604
+ // own process-exit path from a guess before this fix existed.
2590
2605
  if (!gameActive) {
2591
- const fuzzyHit = fuzzyConversationalMatch(raw);
2606
+ const fuzzyHit = fuzzyConversationalMatch(raw, ctx.codeGraphMode);
2592
2607
  if (fuzzyHit) {
2593
2608
  const bucket = classifyConversational(fuzzyHit);
2594
2609
  note(ctx.trace, `goal: casual/social or orientation — fuzzy-typo match "${raw}" → "${fuzzyHit}"`);
@@ -5127,7 +5142,8 @@ async function subjectIsNounOrPropn(word) {
5127
5142
  * guards that keep non-relational pasts out:
5128
5143
  * - neither side may lead with a determiner ("the build failed yesterday")
5129
5144
  * or a closed-class word;
5130
- * - the verb may not be a closed-class or structural word;
5145
+ * - the verb may not be a closed-class word, nor a structural one while the
5146
+ * code-graph switch is on;
5131
5147
  * - both name heads must POS-tag NOUN/PROPN (the same wink adapter
5132
5148
  * subjectIsNounOrPropn uses — "john failed spectacularly" tags its tail
5133
5149
  * ADV and declines). No wink → no signal, never a store;
@@ -5138,7 +5154,7 @@ async function subjectIsNounOrPropn(word) {
5138
5154
  * path (generalVerbTeach) would mint.
5139
5155
  * Returns { subject, verb, base, object }; `base` is what the caller mints
5140
5156
  * through generalVerbPredicate. */
5141
- async function matchRelationalVerbTeach(text) {
5157
+ async function matchRelationalVerbTeach(text, { codeGraphMode = false } = {}) {
5142
5158
  const line = String(text || "").trim();
5143
5159
  const m = line.match(RELATION_VERB_TEACH_RE);
5144
5160
  if (!m) return null;
@@ -5146,7 +5162,8 @@ async function matchRelationalVerbTeach(text) {
5146
5162
  const verb = verbRaw.toLowerCase();
5147
5163
  const strip = pastVerbBase(verb);
5148
5164
  if (!strip) return null;
5149
- if (GENERAL_VERB_NOT_A_VERB_RE.test(verb) || STRUCT_WORDS.has(verb)) return null;
5165
+ if (GENERAL_VERB_NOT_A_VERB_RE.test(verb)) return null;
5166
+ if (codeGraphMode && STRUCT_WORDS.has(verb)) return null;
5150
5167
  const subjWords = subjectRaw.split(/\s+/);
5151
5168
  const objWords = objectRaw.split(/\s+/);
5152
5169
  for (const head of [subjWords[0], objWords[0]]) {
@@ -5182,17 +5199,19 @@ async function matchRelationalVerbTeach(text) {
5182
5199
  * orientation card. This recognizes the shape ONLY well enough to point at
5183
5200
  * the wrapped form that does store; it never stores anything itself. Closed
5184
5201
  * the same way matchRelationalVerbTeach is: no determiner/closed-class
5185
- * heads, no structural/discourse verb, subject POS-tags NOUN/PROPN, object
5186
- * tags NOUN/PROPN/ADJ (wink tags bare lowercase names like "mary" ADJ;
5202
+ * heads, no discourse verb, no structural verb while the code-graph switch is
5203
+ * on, subject POS-tags NOUN/PROPN, object tags NOUN/PROPN/ADJ (wink tags bare
5204
+ * lowercase names like "mary" ADJ;
5187
5205
  * a genuine adverb tail — "dog barks loudly" — still declines). */
5188
- async function bareTeachWrapperNudgeText(text) {
5206
+ async function bareTeachWrapperNudgeText(text, { codeGraphMode = false } = {}) {
5189
5207
  const line = String(text || "").trim().replace(/[.!?]+\s*$/, "");
5190
5208
  const m = line.match(/^([\w'-]+)\s+([a-z][\w-]*s)\s+([\w'-]+)$/i);
5191
5209
  if (!m) return null;
5192
5210
  const [, subj, verbRaw, obj] = m;
5193
5211
  const verb = verbRaw.toLowerCase();
5194
5212
  if (/^(?:is|was|does)$/.test(verb)) return null;
5195
- if (GENERAL_VERB_NOT_A_VERB_RE.test(verb) || STRUCT_WORDS.has(verb) || HABITUAL_VERB_EXCLUDE.has(verb)) return null;
5213
+ if (GENERAL_VERB_NOT_A_VERB_RE.test(verb) || HABITUAL_VERB_EXCLUDE.has(verb)) return null;
5214
+ if (codeGraphMode && STRUCT_WORDS.has(verb)) return null;
5196
5215
  for (const head of [subj, obj]) {
5197
5216
  if (GENERAL_VERB_DETERMINER_RE.test(head) || GENERAL_VERB_NOT_A_VERB_RE.test(head)) return null;
5198
5217
  }
@@ -5253,7 +5272,7 @@ function foldPrepositionIntoPredicate(predicate, objectRaw) {
5253
5272
  /** Sentence forms to try asserting for a teach payload: the payload as-is, and
5254
5273
  * (if it carries no determiner) its "every …" universal — the ACE-OWL shape the
5255
5274
  * grammar actually lands. */
5256
- function assertCandidates(payload) {
5275
+ function assertCandidates(payload, { codeGraphMode = false } = {}) {
5257
5276
  const p = String(payload).trim();
5258
5277
  const out = [p];
5259
5278
  if (!/^(?:every|each|all|a|an)\b/i.test(p)) out.push(`every ${p}`);
@@ -5279,7 +5298,7 @@ function assertCandidates(payload) {
5279
5298
  // still has to ground through the teach path (this rewrite, or teachLane's
5280
5299
  // grounded-subject direct write), so a subject grounded nowhere
5281
5300
  // ("penguins swim" with no prior grounding) stays an honest decline.
5282
- const habitual = matchBareHabitualTeach(p);
5301
+ const habitual = matchBareHabitualTeach(p, { codeGraphMode });
5283
5302
  if (habitual) {
5284
5303
  const articleRule = grammarRules().find((r) => r.kind === "article");
5285
5304
  const article = articleRule && beginsWithVowelSound(habitual.subject, articleRule) ? "an" : "a";
@@ -5292,9 +5311,9 @@ function assertCandidates(payload) {
5292
5311
  * "dogs bark" (plural subject + base verb) and "a dog barks" (articled
5293
5312
  * singular + 3sg verb), both meaning the capability fact "a dog can
5294
5313
  * bark". Returns {subject, verb} folded to the singular/base forms, or
5295
- * null. Deliberately closed: structural verbs (imports/calls/tests …)
5296
- * are excluded so a truncated code query never reads as a capability
5297
- * claim, and the plural surface's verb must be a BASE form (no
5314
+ * null. Deliberately closed: with the code-graph switch on, structural verbs
5315
+ * (imports/calls/tests …) are excluded so a truncated code query never reads
5316
+ * as a capability claim, and the plural surface's verb must be a BASE form (no
5298
5317
  * plural-looking "s" tail — "dogs animals" is not a habitual sentence;
5299
5318
  * "pass"/"miss"-style "ss" verbs stay eligible). */
5300
5319
  /** Words that sit in the habitual shapes' verb slot without being verbs —
@@ -5323,15 +5342,16 @@ const LEADING_DISCOURSE_ADVERB_RE = new RegExp(
5323
5342
  export function stripLeadingDiscourseAdverb(text) {
5324
5343
  return String(text || "").trim().replace(LEADING_DISCOURSE_ADVERB_RE, "");
5325
5344
  }
5326
- function matchBareHabitualTeach(text) {
5345
+ function matchBareHabitualTeach(text, { codeGraphMode = false } = {}) {
5327
5346
  const t = stripLeadingDiscourseAdverb(String(text || "").trim());
5347
+ const structural = (word) => codeGraphMode && STRUCT_WORDS.has(word);
5328
5348
  const plural = t.match(/^(?:all\s+|every\s+)?([\w-]+s)\s+([a-z][\w-]*)[.!?]*$/i);
5329
- if (plural && !STRUCT_WORDS.has(plural[2].toLowerCase()) && !HABITUAL_VERB_EXCLUDE.has(plural[2].toLowerCase()) && !/[^s]s$/i.test(plural[2])) {
5349
+ if (plural && !structural(plural[2].toLowerCase()) && !HABITUAL_VERB_EXCLUDE.has(plural[2].toLowerCase()) && !/[^s]s$/i.test(plural[2])) {
5330
5350
  const subject = singularizeSurface(plural[1].toLowerCase());
5331
5351
  if (subject !== plural[1].toLowerCase()) return { subject, verb: plural[2].toLowerCase() };
5332
5352
  }
5333
5353
  const singular = t.match(/^an?\s+([\w-]+)\s+([a-z][\w-]*s)[.!?]*$/i);
5334
- if (singular && !STRUCT_WORDS.has(singular[2].toLowerCase()) && !HABITUAL_VERB_EXCLUDE.has(singular[2].toLowerCase())) {
5354
+ if (singular && !structural(singular[2].toLowerCase()) && !HABITUAL_VERB_EXCLUDE.has(singular[2].toLowerCase())) {
5335
5355
  const verb = singularizeSurface(singular[2].toLowerCase());
5336
5356
  if (verb !== singular[2].toLowerCase()) return { subject: singular[1].toLowerCase(), verb };
5337
5357
  }
@@ -5353,12 +5373,13 @@ function matchBareHabitualTeach(text) {
5353
5373
  * capability frame, and "penguins never fly" is a habitual surface that lands
5354
5374
  * on generalVerbTeach's own split instead. */
5355
5375
  const BARE_CAN_TEACH_RE = /^(?:an?\s+|every\s+|all\s+)?([\w-]+)\s+(can|cannot|can't|can not)\s+([a-z][\w-]*)[.!?]*$/i;
5356
- function matchBareCanTeach(text) {
5376
+ function matchBareCanTeach(text, { codeGraphMode = false } = {}) {
5357
5377
  const m = String(text || "").trim().match(BARE_CAN_TEACH_RE);
5358
5378
  if (!m) return null;
5359
5379
  const subject = m[1].toLowerCase();
5360
5380
  const verb = m[3].toLowerCase();
5361
- if (STRUCT_WORDS.has(verb) || HABITUAL_VERB_EXCLUDE.has(verb) || GENERAL_VERB_NOT_A_VERB_RE.test(verb)) return null;
5381
+ if (HABITUAL_VERB_EXCLUDE.has(verb) || GENERAL_VERB_NOT_A_VERB_RE.test(verb)) return null;
5382
+ if (codeGraphMode && STRUCT_WORDS.has(verb)) return null;
5362
5383
  if (GENERAL_VERB_DETERMINER_RE.test(subject) || GENERAL_VERB_NOT_A_VERB_RE.test(subject)) return null;
5363
5384
  return { subject, verb, negated: m[2].toLowerCase() !== "can" };
5364
5385
  }
@@ -5782,7 +5803,7 @@ async function teachExclusionReason(sentence) {
5782
5803
  }
5783
5804
  export { teachExclusionReason };
5784
5805
 
5785
- async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null, planHolder = null, graph = null, gameConfig = DEFAULT_GAME_CONFIG, observedAt = "", dateText = "" }) {
5806
+ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null, planHolder = null, graph = null, gameConfig = DEFAULT_GAME_CONFIG, observedAt = "", dateText = "", codeGraphMode = false }) {
5786
5807
  // THE DATED TEACH FRAME — "<sentence> as of <date>" carries an explicit
5787
5808
  // mgx:observedAt past the same shapes this lane already teaches. Tried
5788
5809
  // ONCE, recursively, on the suffix-stripped text, the same
@@ -5796,7 +5817,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
5796
5817
  const suffix = datedTeachSuffix(String(query));
5797
5818
  if (suffix) {
5798
5819
  const dated = await teachLane(suffix.stripped, {
5799
- memoryDir, sessionId, lexicon, cache, planHolder, graph, gameConfig,
5820
+ memoryDir, sessionId, lexicon, cache, planHolder, graph, gameConfig, codeGraphMode,
5800
5821
  observedAt: suffix.observedAt, dateText: suffix.dateText,
5801
5822
  });
5802
5823
  if (dated && !dated.miss) return dated;
@@ -5917,7 +5938,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
5917
5938
  if (memoryDir && !QUESTION_LEAD_RE.test(conjSrc) && /\s+and\s+/i.test(conjSrc)
5918
5939
  && !(await hasMidSentenceInterrogative(conjSrc))) {
5919
5940
  const rewrap = (half) => (wrapped != null ? `remember that ${half}` : half);
5920
- const recurse = (half) => teachLane(rewrap(half), { memoryDir, sessionId, lexicon, cache, planHolder, graph, gameConfig, observedAt, dateText });
5941
+ const recurse = (half) => teachLane(rewrap(half), { memoryDir, sessionId, lexicon, cache, planHolder, graph, gameConfig, observedAt, dateText, codeGraphMode });
5921
5942
  const stripNoted = (t) => String(t).replace(/^noted — remembered(?:\s+\d+\s+facts?)?:\s*/i, "").trim();
5922
5943
  const shared = conjSrc.match(/^(.+?)\s+and\s+((?:is|are|has|have|can)\b.+)$/i);
5923
5944
  const sharedSubject = shared ? shared[1].match(/^(.+?)\s+(?:is|are|has|have|can)\b/i)?.[1]?.trim() : null;
@@ -6511,7 +6532,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
6511
6532
  // (name-shaped sides, POS-confirmed nouns, a lemma-confirmed inflected
6512
6533
  // past) that keep "the build failed" an honest non-match.
6513
6534
  const relVerb = memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion
6514
- ? await matchRelationalVerbTeach(ownSrc) : null;
6535
+ ? await matchRelationalVerbTeach(ownSrc, { codeGraphMode }) : null;
6515
6536
  if (relVerb) {
6516
6537
  const stored = await teachFact(memoryDir, sessionId, {
6517
6538
  subject: relVerb.subject, predicate: await generalVerbPredicate(relVerb.base), object: relVerb.object, observedAt, dateText,
@@ -6864,7 +6885,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
6864
6885
  // BARE path: "grace mentors alan" — no "remember"/"note" wrapper at all.
6865
6886
  // Without this, such a sentence reaches neither this frame NOR an honest
6866
6887
  // miss, landing on the raw structural wall instead (or, at exactly <=3
6867
- // words with no code-ish token, the UNRELATED isConversational()
6888
+ // words, the UNRELATED isConversational()
6868
6889
  // orientation card — see subjectIsNounOrPropn's own docblock for why a
6869
6890
  // plain wrapper-required gate can't safely widen to bare sentences on
6870
6891
  // shape alone: "tell me a joke" fits the identical SVO shape and must
@@ -6928,7 +6949,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
6928
6949
  // instead of letting the general-verb mint below reify the plural
6929
6950
  // verbatim (a fact "can a wren hum" could never read back). An
6930
6951
  // ungrounded singular falls through unchanged.
6931
- const canShape = matchBareCanTeach(raw);
6952
+ const canShape = matchBareCanTeach(raw, { codeGraphMode });
6932
6953
  const canSingular = canShape ? singularizeSurface(canShape.subject) : null;
6933
6954
  if (canShape && canSingular !== canShape.subject) {
6934
6955
  let canLex = lexicon;
@@ -6952,7 +6973,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
6952
6973
  if (wrapped && /\b(?:is|are)\b/i.test(wrapped)) payload = wrapped;
6953
6974
  else if ((BARE_DECLARATIVE_RE.test(raw) || COMPARATIVE_TEACH_RE.test(raw)
6954
6975
  || (matchesRelationalTeachFrame(raw) && !(await relationalFrameNamesGraphEntity(raw, graph)))
6955
- || matchBareHabitualTeach(raw) || matchBareCanTeach(raw))
6976
+ || matchBareHabitualTeach(raw, { codeGraphMode }) || matchBareCanTeach(raw, { codeGraphMode }))
6956
6977
  && !QUESTION_LEAD_RE.test(raw) && !(await hasMidSentenceInterrogative(raw))) payload = raw;
6957
6978
  if (!payload) {
6958
6979
  // "remember margo eats ribs", re-escaping here through a combination
@@ -7050,7 +7071,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
7050
7071
  if (stored) return stored;
7051
7072
  }
7052
7073
  }
7053
- for (const cand of assertCandidates(payload)) {
7074
+ for (const cand of assertCandidates(payload, { codeGraphMode })) {
7054
7075
  // assertTurn ITSELF records the "every" quantifier (point 3) on a plain
7055
7076
  // universal success, so every caller (this loop AND the top-level
7056
7077
  // declarative-sentence dispatch in runTurn) gets it uniformly.
@@ -7065,7 +7086,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
7065
7086
  // path itself stores. The subject's naive singular is tried too, so the
7066
7087
  // explicit plural surface ("penguins can swim") reaches the same stored
7067
7088
  // spelling the grounding fact used.
7068
- const habitualTeach = matchBareHabitualTeach(payload) || matchBareCanTeach(payload);
7089
+ const habitualTeach = matchBareHabitualTeach(payload, { codeGraphMode }) || matchBareCanTeach(payload, { codeGraphMode });
7069
7090
  if (habitualTeach) {
7070
7091
  let habLex = lexicon;
7071
7092
  if (!habLex) { const { loadLexicon } = await import("../domain/grammar/lexicon.mjs"); habLex = loadLexicon(); }
@@ -7149,7 +7170,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
7149
7170
  const { parseAce } = await import("../domain/grammar/ace.mjs");
7150
7171
  let lex = lexicon;
7151
7172
  if (!lex) { const { loadLexicon } = await import("../domain/grammar/lexicon.mjs"); lex = loadLexicon(); }
7152
- for (const cand of assertCandidates(payload)) {
7173
+ for (const cand of assertCandidates(payload, { codeGraphMode })) {
7153
7174
  const parse = parseAce(cand, lex);
7154
7175
  if (parse?.residue?.length) { unknown = [...new Set(parse.residue.map((w) => String(w).toLowerCase()))]; break; }
7155
7176
  }
@@ -7495,8 +7516,7 @@ async function metaLane(query, { graph, memoryDir, last = null, templates = null
7495
7516
  if (moduleOrient) return moduleOrient;
7496
7517
  // The sha-authorship form ("who authored a1b2c3d") can be as short as
7497
7518
  // THREE words, which the conversational-orientation branch (step 2) would grab
7498
- // before the author step (4b) is reached a bare hex sha is not "code-ish" to
7499
- // isConversational. The form is closed + unambiguous (7-40 hex chars), so the
7519
+ // before the author step (4b) is reached. The form is closed + unambiguous (7-40 hex chars), so the
7500
7520
  // meta lane delegates it to the author lane here. Unknown/ambiguous shas return
7501
7521
  // null and fall through unchanged.
7502
7522
  if (AUTHOR_SHA_RE.test(q)) return authorLane(q, { graph });
@@ -7592,9 +7612,9 @@ const PERSONAL_ASSISTANT_NUDGE_RE = new RegExp(
7592
7612
  * from a prior result set is a capability the engine genuinely doesn't
7593
7613
  * have yet (even the fully-spelled "which of those is not X" doesn't
7594
7614
  * compile — parsePredicateFilter has no negation branch).
7595
- * Without this, both fall to the generic orientation card (a short,
7596
- * non-codeish turn trips isConversational's ≤3-word catch-all) or the raw
7597
- * grammar wall (a codeish one, e.g. a path) — neither names what actually
7615
+ * Without this, both fall to the generic orientation card (a short turn trips
7616
+ * isConversational's ≤3-word catch-all) or the raw grammar wall (a longer one,
7617
+ * e.g. a path) — neither names what actually
7598
7618
  * went wrong. This is an honest, GUIDING nudge, never a fabricated filtered
7599
7619
  * answer and never a bare wall. */
7600
7620
  const STACCATO_NEGATION_RE = /^(?:and\s+)?(?:not|except(?:\s+for)?)\s+(.+?)(?:\s+then|\s+though)?[?.!]*$/i;
@@ -7606,7 +7626,7 @@ const NEGATION_PRONOUN_RE = /^(?:it|that|this|those|them)(?:\s+ones?)?$/i;
7606
7626
  * Genuinely unanswerable as a real graph query, never fabricated: tmct's
7607
7627
  * superlative only ever names the single top (or bottom) match for a metric
7608
7628
  * (evalSuperlative) — it has no "runner-up"/"next ranked" or "greater than a
7609
- * number" capability to reach for. Without this, both a short/non-codeish
7629
+ * number" capability to reach for. Without this, both a short
7610
7630
  * phrasing ("more than that") and the wall these route to (once the
7611
7631
  * isConversational catch-all is deferred below) fall to the generic
7612
7632
  * orientation card or the raw grammar wall — neither says what actually went
@@ -7831,6 +7851,17 @@ const NO_GRAPH_BOOTSTRAP_WALL_LEAD = "I can't answer that as a code question —
7831
7851
  /** The no-code-domain sibling of the lead above: same role (the bootstrap
7832
7852
  * wall's opening line), no code vocabulary. */
7833
7853
  const NEUTRAL_BOOTSTRAP_WALL_LEAD = "I couldn't ground that in anything I know.";
7854
+ /** What a turn says when the session declared code-graph mode and no code
7855
+ * graph stands behind it. The declaration is the caller's, so the unmet
7856
+ * declaration is reported as the error it is, with no remedy command
7857
+ * attached. */
7858
+ const CODE_GRAPH_MISSING_ERROR = "code-graph mode is on, but this session has no code graph behind it.";
7859
+ /** Does this ANSWER speak the code lane's own vocabulary? Every index-miss the
7860
+ * ask engine composes says "found in the index", and the empty-index note
7861
+ * names the code index outright. Read off what we are about to SAY, never off
7862
+ * what the user typed: which subject a session will talk about is declared,
7863
+ * and this only checks that an answer honours the declaration. */
7864
+ const speaksCodeIndex = (answer) => /found in the index|no code index/i.test(String(answer));
7834
7865
 
7835
7866
  /** The orientation-repeat one-liner. The conversational
7836
7867
  * orientation branch sits OUTSIDE the composed-only wall-shortening gate (it
@@ -7904,6 +7935,10 @@ export async function helpText(codeDomainActive = false, helpRows = undefined) {
7904
7935
  ["list <kind>", "list what you've taught under a class (\"list letters\"); \"list facts\" lists memory itself"],
7905
7936
  ["forget that <X> is a <Y>", "withdraw a fact you taught, and anything derived from it — the phrasing the retract lane reads"],
7906
7937
  ["/narrate on|off", "verbose developer/debug mode: decision points, matched pattern, results+sources, goal per turn"],
7938
+ // The switch names the code domain, so it lists beside the other
7939
+ // code-domain rows: a session with nothing to index would be reading about
7940
+ // a graph it hasn't got. Typing it still works either way.
7941
+ ...(codeDomainActive ? [["/code-graph on|off", "read every line as a question about the code graph (default off, and never inferred from what you type)"]] : []),
7907
7942
  ["/wiki on|off|supplement|always", "live Wikipedia (default off): on tries en.wikipedia.org when I can't answer (network), cited; supplement also adds a read-out under every grounded vocabulary answer; always widens that to every grounded answer"],
7908
7943
  ["/wikipedia | /wikidata", "which source \"research <topic>\" fetches from for the rest of the session: Simple English Wikipedia's prose (the default) or Wikidata's structured claims. tmct.toml's [research] source sets the starting value; tmct chat --research-source overrides it per invocation"],
7909
7944
  ["research <topic> [limit N] [depth D]", "fetch the topic from the session's research source (Simple English Wikipedia by default — /wikidata switches it) (the explicit ask is the network consent), store what it grounds, and queue its linked topics — \"research next\" steps the queue; also status/stop. limit N caps the links queued per topic, depth D how many hops the queue follows (1 by default); a run also stops at its total node budget"],
@@ -12204,7 +12239,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
12204
12239
  // verdict or an ex-falso clash replaces the miss outright; an exhausted
12205
12240
  // budget merges onto whichever of the three recovery texts below still
12206
12241
  // renders, so the miss itself stays byte-identical and only gains the
12207
- // marker chatbench/infbench read apart from a parse miss.
12242
+ // marker infbench reads apart from a parse miss.
12208
12243
  const autoProve = await autoProveFallback(memoryDir, cache, rows, subjectWord, subjCandidates, objVariants, kindWord);
12209
12244
  if (autoProve?.text) return autoProve;
12210
12245
  const askProveBudgetExhausted = !!autoProve?.budgetExhausted;
@@ -14191,7 +14226,7 @@ async function conceptForceAnswer(query, envelope, { graph, config, source, memo
14191
14226
  const definition = (await seonDefinitions()).get(term) ?? null;
14192
14227
  if (!definition) return null;
14193
14228
  // The runChat shell hands the loaded graph straight in; the pure runTurn(config)
14194
- // path (tests, test-benchmarks/chatbench) does not, so load it the same way dispatchTool does when
14229
+ // path (tests, the bench harnesses) does not, so load it the same way dispatchTool does when
14195
14230
  // it's missing. Failure-tolerated: no loadable graph → no concept force.
14196
14231
  let g = graph;
14197
14232
  if (!g && config && source) {
@@ -15260,7 +15295,7 @@ const ARCH_OVERVIEW_PHRASES = [
15260
15295
  new RegExp(`^${ARCH_OVERVIEW_LEAD}(?:(?:an?|the)\\s+)?(?:overview|map|diagram)\\s+${ARCH_OVERVIEW_OF_REPO}(?:\\s+here)?\\??$`, "i"),
15261
15296
  ];
15262
15297
 
15263
- async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint: sessionVocabHint = null, tel = null, biasByBundle = {}, cache = null, vocabAntecedent = null, planHolder = null, discourseHolder = null, gameConfig = DEFAULT_GAME_CONFIG, liveReference = false, onLiveLookup = null, sourceSkips = null, uiContext = "cli", synthesisBudget = AUTO_SYNTHESIS_BUDGET, codeDomainActive = false }) {
15298
+ async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint: sessionVocabHint = null, tel = null, biasByBundle = {}, cache = null, vocabAntecedent = null, planHolder = null, discourseHolder = null, gameConfig = DEFAULT_GAME_CONFIG, liveReference = false, onLiveLookup = null, sourceSkips = null, uiContext = "cli", synthesisBudget = AUTO_SYNTHESIS_BUDGET, codeDomainActive = false, codeGraphMode = false, ingested = false }) {
15264
15299
  // The session-wide hint names a term this session can PROVE resolves; when
15265
15300
  // the question itself named a subject the lexicon knows, the hint names that
15266
15301
  // instead, so a miss on "how many eyes does a human have" points at "human"
@@ -15897,7 +15932,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
15897
15932
  }
15898
15933
  }
15899
15934
  // "what about X" with a genuine PRIOR turn to continue is exempt from the
15900
- // conversational catch-all even when short/non-codeish: isConversational()
15935
+ // conversational catch-all even when short: isConversational()
15901
15936
  // can't see that discourseRewrite/describeWrapperAnswer haven't had their
15902
15937
  // turn yet. Same exemption for the bare-connective sibling shape ("and
15903
15938
  // Widget?", STACCATO_SWAP_RE), gated the SAME way discourseRewrite gates it.
@@ -15968,7 +16003,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
15968
16003
  const bareLine = String(query).trim();
15969
16004
  const pm = bareLine.match(/^([\w-]+)\s+are\s+([\w-]+)[.!?]*$/i);
15970
16005
  const habitual = pm || QUESTION_LEAD_RE.test(bareLine)
15971
- ? null : (matchBareHabitualTeach(bareLine) || matchBareCanTeach(bareLine));
16006
+ ? null : (matchBareHabitualTeach(bareLine, { codeGraphMode }) || matchBareCanTeach(bareLine, { codeGraphMode }));
15972
16007
  if (pm || habitual) {
15973
16008
  try {
15974
16009
  const { loadLexicon, lookupNoun, lookupAdjective } = await import("../domain/grammar/lexicon.mjs");
@@ -16010,8 +16045,8 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
16010
16045
  } catch { /* lexicon unavailable — leave false, the ordinary path decides */ }
16011
16046
  } else if (memoryDir && !QUESTION_LEAD_RE.test(bareLine)
16012
16047
  && bareLine.replace(/[.!?]+\s*$/, "").split(/\s+/).filter(Boolean).length <= 3) {
16013
- if (await matchRelationalVerbTeach(bareLine)) isBareRelationalVerbTeach = true;
16014
- else bareTeachWrapperNudge = await bareTeachWrapperNudgeText(bareLine);
16048
+ if (await matchRelationalVerbTeach(bareLine, { codeGraphMode })) isBareRelationalVerbTeach = true;
16049
+ else bareTeachWrapperNudge = await bareTeachWrapperNudgeText(bareLine, { codeGraphMode });
16015
16050
  }
16016
16051
  }
16017
16052
  // A vague relation touch ("what about cochange", "tell me about cochange",
@@ -16048,7 +16083,13 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
16048
16083
  // isConversational's word-count catch-all into the orientation blurb, and
16049
16084
  // that blurb (a dispatched turn) then becomes `last`, wiping the very
16050
16085
  // antecedent the next pronoun turn needs.
16051
- const isConversationalCandidate = conversationalCandidateBaseGate && !vocabAntecedent && isConversational(query);
16086
+ //
16087
+ // `ingested` is the same kind of declaration as codeGraphMode: a caller
16088
+ // feeding a document a sentence at a time (`tmct extract`, /ingest) says so,
16089
+ // and the short-input catch-all stands down. A sentence out of a file was
16090
+ // never small talk, however few words it runs to.
16091
+ const isConversationalCandidate = conversationalCandidateBaseGate && !vocabAntecedent && !ingested
16092
+ && isConversational(query, { codeGraphMode });
16052
16093
  const liveGameOwnWord = gameOwnWord(query, planHolder);
16053
16094
  // "what is X" with NO article ("what is john") is BOTH conversational-shaped
16054
16095
  // (isConversational() would claim it) AND a legitimate bare meta/fact-lookup
@@ -16074,13 +16115,13 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
16074
16115
  const bareNounMatch = correctMisspellings(String(query).trim()).replace(/[?.!]+\s*$/, "").trim()
16075
16116
  .match(/^(?:the\s+|a\s+|an\s+)?([a-z][a-z-]*)$/i);
16076
16117
  const bareNounShape = !!bareNounMatch;
16077
- // `isBareCamelCaseMetaQuestion` ORs in alongside isConversationalCandidate
16078
- // for THIS lane only a bare "what is TaskController" (CamelCase compound,
16079
- // no article) is otherwise excluded solely because isConversational()'s
16080
- // codeish check fires on the CamelCase transition. Shares the SAME base gate
16081
- // so it's never looser; a CamelCase term with no real hit still falls
16082
- // through to the ordinary orientation-card fallback further down.
16083
- const isBareCamelCaseWhatisCandidate = conversationalCandidateBaseGate && isBareCamelCaseMetaQuestion(gateQuery);
16118
+ // `isBareMetaQuestionShape` ORs in alongside isConversationalCandidate
16119
+ // for THIS lane only, so a bare "what is TaskController" still reaches the
16120
+ // bare-meta lookup in code-graph mode, where the conversational candidate
16121
+ // gate stands down. Shares the SAME base gate so it's never looser; a term
16122
+ // with no real hit still falls through to the ordinary orientation-card
16123
+ // fallback further down.
16124
+ const isBareMetaQuestionCandidate = conversationalCandidateBaseGate && isBareMetaQuestionShape(gateQuery);
16084
16125
  // The SAME divert-only-on-a-real-hit gate covers factAnswer's
16085
16126
  // WHAT_USED_FOR_RE/REVERSE_PREDICATE_MARKERS reverse-predicate shapes too —
16086
16127
  // for the shortest members of the family ("what wants happiness") it's
@@ -16102,8 +16143,14 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
16102
16143
  // relations only on a real hit, so a name with no facts still falls to the
16103
16144
  // ordinary card.
16104
16145
  const whoIsShape = WHO_IS_BARE_RE.test(gateQuery);
16146
+ // "where is ann" is three words, the same length as the vocabulary openers
16147
+ // above, and factReadBack's locative reader surfaces the taught fact only on
16148
+ // a real hit — so "where is up" with nothing behind it still falls to the
16149
+ // ordinary card. Its auxiliary-fronted sibling ("where does ann live")
16150
+ // reads the same stored fact and takes the same gate.
16151
+ const whereIsShape = WHERE_IS_FACT_RE.test(gateQuery) || WHERE_DOES_FACT_RE.test(gateQuery);
16105
16152
  let bareMetaHit = null;
16106
- if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape || capabilityAskShape || bareNounShape || whoIsShape)) {
16153
+ if ((isConversationalCandidate || isBareMetaQuestionCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape || capabilityAskShape || bareNounShape || whoIsShape || whereIsShape)) {
16107
16154
  if (memoryDir) {
16108
16155
  // The bare noun asks its own "what is a X" — the readers never see the
16109
16156
  // single word, so the vocabulary route is the constructed question's.
@@ -16175,14 +16222,14 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
16175
16222
  // returns non-null for an EXACT label match, so ordinary small talk is
16176
16223
  // unaffected.
16177
16224
  //
16178
- // `isBareCamelCaseEntityCandidate` ORs in for THIS lane the way
16179
- // isBareCamelCaseWhatisCandidate does for (2b) a bare "TaskController" is
16180
- // excluded from isConversationalCandidate solely by the CamelCase transition,
16181
- // while a bare "task" reaches the lane. Same base gate and same
16182
- // `!vocabAntecedent`, so it's never looser than the gate it joins.
16183
- const isBareCamelCaseEntityCandidate = conversationalCandidateBaseGate && !vocabAntecedent
16184
- && isBareCamelCaseEntityName(query);
16185
- if (!bareMetaHit && (isConversationalCandidate || isBareCamelCaseEntityCandidate) && graph) {
16225
+ // `isBareEntityNameCandidate` ORs in for THIS lane the way
16226
+ // isBareMetaQuestionCandidate does for (2b), so a bare entity name still
16227
+ // reaches the lane in code-graph mode, where the conversational candidate
16228
+ // gate stands down. Same base gate and same `!vocabAntecedent`, so it's
16229
+ // never looser than the gate it joins.
16230
+ const isBareEntityNameCandidate = conversationalCandidateBaseGate && !vocabAntecedent
16231
+ && isBareEntityNameShape(query);
16232
+ if (!bareMetaHit && (isConversationalCandidate || isBareEntityNameCandidate) && graph) {
16186
16233
  const { metaFallbackEntityAnswer } = await import("../domain/ask.mjs");
16187
16234
  const fallback = metaFallbackEntityAnswer(graph, String(query).trim());
16188
16235
  if (fallback) bareMetaHit = { text: fallback.text, replace: true };
@@ -16297,12 +16344,11 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
16297
16344
  // wall-shortening gate below, so it needs its own repeat collapse,
16298
16345
  // mirroring WALL_REPEAT_ONELINER.
16299
16346
  //
16300
- // `!envelope?.parsed`: isConversational() is a TEXT-ONLY heuristic it
16301
- // has no way to know the query already compiled to a real structural AST
16302
- // shape. A pronoun-shortened follow-up ("who touched it") can be exactly
16303
- // 3 words with no STRUCT_WORDS token, so without this guard
16304
- // isConversational would discard a correct, already-composed answer for
16305
- // the generic orientation wall.
16347
+ // `!envelope?.parsed`: isConversational() counts words and nothing else
16348
+ // it has no way to know the query already compiled to a real structural
16349
+ // AST shape. A pronoun-shortened follow-up ("who touched it") is exactly
16350
+ // 3 words, so without this guard isConversational would discard a correct,
16351
+ // already-composed answer for the generic orientation wall.
16306
16352
  const orientation = orientationAnswer(templates, graph, vocabHint, uiContext, codeDomainActive);
16307
16353
  const repeat = last?.answer === orientation;
16308
16354
  answer = repeat ? ORIENTATION_REPEAT_ONELINER : orientation;
@@ -16504,7 +16550,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
16504
16550
  // the vocabulary for something the vocabulary had nothing to do with.
16505
16551
  let taught = null;
16506
16552
  try {
16507
- taught = await teachLane(query, { memoryDir, sessionId, lexicon, cache, planHolder, graph, gameConfig });
16553
+ taught = await teachLane(query, { memoryDir, sessionId, lexicon, cache, planHolder, graph, gameConfig, codeGraphMode });
16508
16554
  } catch (error) {
16509
16555
  if (!isPersistUnavailable(error)) throw error;
16510
16556
  taught = persistUnavailableAnswer();
@@ -16849,47 +16895,48 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
16849
16895
  }
16850
16896
  }
16851
16897
  // #4 HONEST-EMPTY POLISH — an empty CODE graph: any still-standing engine
16852
- // dead-end (an honest empty, the short miss, the bootstrap note) carries the
16853
- // exit toward a real graph, unless it already points there or unless the
16854
- // turn already names its own recovery (a self-contained decline, or a
16855
- // teach-offer about to land). Only when genuinely empty. The CLI keeps the
16856
- // --repo/example pointer verbatim; a browser or a live adventure has no
16857
- // such command to reach for, so each gets a teach-forward pointer (and the
16858
- // adventure also names the world asides that are guaranteed to hit).
16859
- // A live adventure keeps its polish even beside a teach-offer: the world
16860
- // asides ("look", "talk to the butler") are guidance the offer can't carry.
16898
+ // dead-end (an honest empty, the short miss, the bootstrap note) carries a
16899
+ // teach-forward pointer, unless the turn already names its own recovery (a
16900
+ // self-contained decline, or a teach-offer about to land). Only when
16901
+ // genuinely empty. A live adventure keeps its polish even beside a
16902
+ // teach-offer: the world asides ("look", "talk to the butler") are guidance
16903
+ // the offer can't carry.
16904
+ //
16905
+ // With code-graph mode ON and no code graph behind the session, the turn
16906
+ // reports that as an error. The mode is the caller's declaration, and an
16907
+ // unmet declaration is a failure, not a prompt to go index something. With
16908
+ // the mode OFF the miss stands as written: nothing here reaches for
16909
+ // code-graph wording on a session that never asked for it.
16861
16910
  const adventureLive = !!planHolder?.state?.adventure;
16862
- if (recordMiss && (via === "composed" || via === "miss") && !selfContainedMiss
16863
- && (adventureLive || !teachOffer)
16911
+ const missStanding = recordMiss && (via === "composed" || via === "miss") && !selfContainedMiss;
16912
+ const codeGraphMissing = codeGraphMode && !(graph && moduleCountOf(graph) > 0);
16913
+ if (missStanding && !adventureLive && codeGraphMissing) {
16914
+ answer = CODE_GRAPH_MISSING_ERROR;
16915
+ via = "miss";
16916
+ genericWallMiss = true;
16917
+ teachOffer = null;
16918
+ note(trace, "intermediate: CODE-GRAPH MODE — the switch is on and no code graph stands behind the session, so the turn reports that error");
16919
+ } else if (missStanding && (adventureLive || !teachOffer)
16864
16920
  && noCodeGraph(graph) && !/--repo|tmct init|no code graph/i.test(answer)) {
16865
16921
  if (adventureLive) {
16866
16922
  answer = `${answer}\n(I don't know that yet — you can teach me: say "remember: <thing> is a <kind>". Or ask the world: "look", "where is the key", "talk to the butler".)`;
16867
- note(trace, "intermediate: HONEST-EMPTY POLISH — a live adventure miss points at the teach lane and the world asides, not the --repo remedy");
16923
+ note(trace, "intermediate: HONEST-EMPTY POLISH — a live adventure miss points at the teach lane and the world asides");
16868
16924
  } else if (browser) {
16869
16925
  answer = `${answer}\n(I don't know that yet — you can teach me: say "remember: <thing> is a <kind>".)`;
16870
- note(trace, "intermediate: HONEST-EMPTY POLISH — a browser miss points at the teach lane, not the CLI-only --repo remedy");
16871
- } else if (looksCodeish(String(query), String(query).toLowerCase())) {
16872
- if (codeDomainActive) {
16873
- answer = `${answer}\n(this repo has no code graph index it with \`tmct index\`, point me at a \`.tmct/graph.json\` with \`--repo <path>\`, or run \`npm run example:mini\`.)`;
16874
- note(trace, "intermediate: HONEST-EMPTY POLISH the loaded graph has 0 modules, so the dead-end got a tmct index/--repo pointer appended");
16875
- } else {
16876
- // No code domain active this session: a code-shaped miss here would
16877
- // otherwise repeat the engine's own code-graph vocabulary. Decline to
16878
- // the same neutral wall a genuinely unparsed line gets — as if this
16879
- // lane's grammar had never matched — rather than pointing at a
16880
- // command that has nothing here to index.
16881
- answer = vocabHint
16882
- ? `I couldn't read that as a question I can answer. ${vocabHint} Type /help for all query shapes.`
16883
- : shortMissHint(query);
16884
- via = "miss";
16885
- genericWallMiss = true;
16886
- note(trace, "intermediate: HONEST-EMPTY POLISH — no code domain active, so the code-shaped miss declined to the ordinary wall instead of a code-graph pointer");
16887
- }
16926
+ note(trace, "intermediate: HONEST-EMPTY POLISH — a browser miss points at the teach lane");
16927
+ } else if (speaksCodeIndex(answer)) {
16928
+ // Nobody declared code-graph mode and there is no code graph here, so an
16929
+ // answer that speaks the code lane's own vocabulary is reporting on
16930
+ // something this session was never told to have. Decline to the same wall
16931
+ // a genuinely unparsed line gets.
16932
+ answer = vocabHint
16933
+ ? `I couldn't read that as a question I can answer. ${vocabHint} Type /help for all query shapes.`
16934
+ : shortMissHint(query);
16935
+ via = "miss";
16936
+ genericWallMiss = true;
16937
+ note(trace, "intermediate: HONEST-EMPTY POLISH — the miss spoke code-index vocabulary with the switch off, so it declined to the ordinary wall");
16888
16938
  } else {
16889
- // Nothing in the question is code-shaped, so indexing a repo would not
16890
- // help. "who won the 2031 world cup" used to carry the pointer anyway,
16891
- // which reads as a remedy for a question the remedy cannot touch.
16892
- note(trace, "intermediate: HONEST-EMPTY POLISH — held back: nothing in the question is code-shaped, so the index/--repo remedy would not apply");
16939
+ note(trace, "intermediate: HONEST-EMPTY POLISH — held back: the miss names no code index, so it stands as written");
16893
16940
  }
16894
16941
  }
16895
16942
  if (teachOffer) {
@@ -16988,8 +17035,8 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
16988
17035
  // trailer's own gate. Absent on a point answer and on every miss.
16989
17036
  ...(enumerationLane && !recordMiss ? { enumerationLane } : {}),
16990
17037
  // The automatic /prove fallback's own budget wall — the same marker
16991
- // /prove's own explicit budget wall stamps, so chatbench/infbench can
16992
- // count a budget miss apart from a parse miss.
17038
+ // /prove's own explicit budget wall stamps, so infbench can count a
17039
+ // budget miss apart from a parse miss.
16993
17040
  ...(budgetExhausted ? { budgetExhausted: true } : {}),
16994
17041
  };
16995
17042
  const logLines = [ts, `> ${query}`, answer, ""];
@@ -17151,12 +17198,12 @@ export function renderDeclaredGoals(goals) {
17151
17198
  return lines.join("\n").replace(/\n+$/, "");
17152
17199
  }
17153
17200
 
17154
- async function runCommand(line, { config, source, graph, focus, memoryDir, trace, narrate = false, liveReference = false, researchSource = null, tel = null, biasByBundle = {}, cache = null, codeDomainActive = false, laneVocab = null, domainPacks = null, lexicon = null, newsState = null, newsConfig = null, newsProviders = null }) {
17201
+ async function runCommand(line, { config, source, graph, focus, memoryDir, trace, narrate = false, liveReference = false, researchSource = null, tel = null, biasByBundle = {}, cache = null, codeDomainActive = false, codeGraphMode = false, laneVocab = null, domainPacks = null, lexicon = null, newsState = null, newsConfig = null, newsProviders = null }) {
17155
17202
  const ts = new Date().toISOString();
17156
17203
  const sp = line.indexOf(" ");
17157
17204
  const name = (sp === -1 ? line.slice(1) : line.slice(1, sp)).toLowerCase();
17158
17205
  const argText = (sp === -1 ? "" : line.slice(sp + 1)).trim();
17159
- const mk = (answer, { resolvedIds = [], miss = false, newFocus = focus, narrateNext, liveReferenceNext, researchSourceNext, newsStateNext } = {}) => ({
17206
+ const mk = (answer, { resolvedIds = [], miss = false, newFocus = focus, narrateNext, liveReferenceNext, codeGraphModeNext, researchSourceNext, newsStateNext } = {}) => ({
17160
17207
  answer,
17161
17208
  logLines: [ts, `> ${line}`, answer, ""],
17162
17209
  record: { type: "turn", ts, query: line, command: name, via: "command", resolvedIds, answeredIds: [], miss },
@@ -17164,6 +17211,7 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
17164
17211
  goal: GOAL_BY_COMMAND[name] || "use a specific tool/command directly",
17165
17212
  ...(narrateNext !== undefined ? { narrate: narrateNext } : {}),
17166
17213
  ...(liveReferenceNext !== undefined ? { liveReference: liveReferenceNext } : {}),
17214
+ ...(codeGraphModeNext !== undefined ? { codeGraphMode: codeGraphModeNext } : {}),
17167
17215
  ...(researchSourceNext !== undefined ? { researchSource: researchSourceNext } : {}),
17168
17216
  ...(newsStateNext !== undefined ? { newsState: newsStateNext } : {}),
17169
17217
  });
@@ -17196,6 +17244,30 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
17196
17244
  return mk(`narrate mode ${next ? "on" : "off"}.`, { narrateNext: next });
17197
17245
  }
17198
17246
 
17247
+ // /code-graph on|off — whether this session reads its lines as questions
17248
+ // about a code graph (session-scoped, the /narrate and /wiki pattern: the new
17249
+ // state rides the turn RESULT as `codeGraphMode`, and each session shell
17250
+ // applies it to its own mutable state). Default OFF. Nothing else turns it
17251
+ // on: no lane infers code intent from the words a user typed, and no loaded
17252
+ // graph flips it by itself. A bare "/code-graph" reports the CURRENT state
17253
+ // and changes nothing.
17254
+ //
17255
+ // Turning it on in a session with no code graph behind it reports that error
17256
+ // and still sets the mode — the switch answers to the caller, not to what
17257
+ // happens to be loaded.
17258
+ if (name === "code-graph") {
17259
+ const arg = argText.toLowerCase();
17260
+ if (arg !== "on" && arg !== "off") {
17261
+ return mk(`code-graph mode is ${codeGraphMode ? "on" : "off"} — /code-graph on or /code-graph off to change it. `
17262
+ + "When on, I read every line as a question about the code graph. When off, I answer nothing about code.");
17263
+ }
17264
+ const next = arg === "on";
17265
+ if (next && !(graph && moduleCountOf(graph) > 0)) {
17266
+ return mk(`code-graph mode on. ${CODE_GRAPH_MISSING_ERROR}`, { codeGraphModeNext: true, miss: true });
17267
+ }
17268
+ return mk(`code-graph mode ${next ? "on" : "off"}.`, { codeGraphModeNext: next });
17269
+ }
17270
+
17199
17271
  // /wiki on|off — the live Wikipedia supplement toggle (session-scoped,
17200
17272
  // exactly the /narrate pattern: the new state rides the turn RESULT as
17201
17273
  // `liveReference`, and each session shell applies it to its own mutable
@@ -17590,7 +17662,7 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
17590
17662
  let factCount = 0;
17591
17663
  for (const sentence of sentences) {
17592
17664
  const before = readFactRows(await loadMemory(memoryDir));
17593
- const { record: ingestRecord } = await runTurn(sentence, { config, memoryDir, sessionId: uuidv7() });
17665
+ const { record: ingestRecord } = await runTurn(sentence, { config, memoryDir, sessionId: uuidv7(), ingested: true });
17594
17666
  if (ingestRecord?.via !== "assert" || ingestRecord?.miss) continue;
17595
17667
  const after = readFactRows(await loadMemory(memoryDir));
17596
17668
  const rows = touchedFactRows(before, after);
@@ -18695,7 +18767,7 @@ export async function runTurn(input, options = {}) {
18695
18767
  return { ...result, ...(await factsTouchedSince(memoryDir, before)) };
18696
18768
  }
18697
18769
 
18698
- async function dispatchTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, liveReference = false, researchSource = null, onLiveLookup = null, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, gameConfig = null, uiContext = "cli", synthesisBudget = AUTO_SYNTHESIS_BUDGET, researchState = null, researchConfig = null, newsState = null, newsConfig = null, newsProviders = null, discourse = null, _noSplit = false, actingSubject = "player", codeDomainActive = null, laneVocab = null, domainPacks = null, retrieval = null } = {}) {
18770
+ async function dispatchTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, liveReference = false, researchSource = null, onLiveLookup = null, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, gameConfig = null, uiContext = "cli", synthesisBudget = AUTO_SYNTHESIS_BUDGET, researchState = null, researchConfig = null, newsState = null, newsConfig = null, newsProviders = null, discourse = null, _noSplit = false, actingSubject = "player", codeDomainActive = null, codeGraphMode = false, ingested = false, laneVocab = null, domainPacks = null, retrieval = null } = {}) {
18699
18771
  // Every game's tuning knobs (spider-fly's mass economy, guess-the-number's
18700
18772
  // bounds, the shared plan lane's search-depth cap) — a caller's own
18701
18773
  // gameConfig (chat-session.mjs resolves one per session from tmct.toml)
@@ -18792,7 +18864,7 @@ async function dispatchTurn(input, { config, source = defaultSource, graph = nul
18792
18864
  // circuit breaker had already given up on it. A name lands here only when
18793
18865
  // the skip changed what served the answer, and the trailer says so.
18794
18866
  const sourceSkips = new Set();
18795
- const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, liveReference, researchSource, onLiveLookup, sourceSkips, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache, vocabAntecedent, planHolder, discourseHolder, gameConfig: resolvedGameConfig, uiContext, synthesisBudget, codeDomainActive: domainActive, laneVocab: laneVocabValue, domainPacks: domainPacksValue, newsState, newsConfig, newsProviders };
18867
+ const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, liveReference, researchSource, onLiveLookup, sourceSkips, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache, vocabAntecedent, planHolder, discourseHolder, gameConfig: resolvedGameConfig, uiContext, synthesisBudget, codeDomainActive: domainActive, codeGraphMode, ingested, laneVocab: laneVocabValue, domainPacks: domainPacksValue, newsState, newsConfig, newsProviders };
18796
18868
  // A DISPATCHED turn (count / slash-command / ask) becomes the new "last
18797
18869
  // answer" that why/say-more re-renders; a conversational turn does not.
18798
18870
  // Every dispatched turn's result passes through finish() here — the LAST