@polycode-projects/the-mechanical-code-talker 2.9.0 → 2.9.4

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 (37) hide show
  1. package/README.md +50 -28
  2. package/bin/tmct.mjs +176 -31
  3. package/corpus/LICENSES.json +7 -0
  4. package/corpus/sprites/src/sprite-facts.jsonl +1033 -0
  5. package/corpus/worlds/manifest.json +9 -9
  6. package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
  7. package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
  8. package/corpus/worlds/src/ashcombe-hall.jsonl +3 -0
  9. package/corpus/worlds/src/spider-fly.jsonl +17 -0
  10. package/package.json +37 -34
  11. package/src/adapters/corpus/wikipedia-live.mjs +145 -0
  12. package/src/adapters/memory/core.mjs +77 -12
  13. package/src/adapters/toml-config.mjs +6 -5
  14. package/src/domain/cli-verbs.mjs +4 -2
  15. package/src/domain/hanoi-lesson.mjs +10 -0
  16. package/src/domain/reference-pack.mjs +102 -0
  17. package/src/domain/spider-fly-world.mjs +54 -1
  18. package/src/domain/sprite-facts.mjs +0 -0
  19. package/src/services/adventure-viz.mjs +209 -67
  20. package/src/services/chat-page-viz.mjs +366 -37
  21. package/src/services/chat-session.mjs +38 -22
  22. package/src/services/chat.mjs +199 -20
  23. package/src/services/fold.mjs +28 -44
  24. package/src/services/import-file.mjs +7 -6
  25. package/src/services/init.mjs +10 -5
  26. package/src/services/ledger-viz.mjs +25 -34
  27. package/src/services/plan-viz.mjs +26 -26
  28. package/src/services/sessions.mjs +15 -3
  29. package/src/services/spider-fly-viz.mjs +51 -45
  30. package/src/services/sprite-catalog-viz.mjs +187 -3
  31. package/src/services/viz-theme.mjs +8 -0
  32. package/src/surfaces/web/adventure-browser-entry.mjs +23 -10
  33. package/src/surfaces/web/chat-browser-entry.mjs +17 -2
  34. package/src/surfaces/web/idb-persist.mjs +115 -0
  35. package/src/surfaces/web/memory-ask-browser.bundle.js +155 -25927
  36. package/src/surfaces/web/sprites-browser-entry.mjs +68 -0
  37. package/src/tools/memory-fallthrough.mjs +11 -4
@@ -116,12 +116,14 @@ export async function createSession({
116
116
  gitRoot = gitToplevel,
117
117
  ephemeral = false,
118
118
  narrate = false,
119
- // The storage-backend seam: "file" (default) keeps memoryDir a plain
120
- // repo-path string (Backend A). "memory" selects Backend B (zero disk I/O,
121
- // session-scoped). "sqlite" selects Backend C (a live node:sqlite
122
- // connection, lazily imported only when chosen). This is `tmct chat
123
- // --memory-backend <...>`'s already-resolved value; full precedence (this
124
- // param > TMCT_MEMORY_BACKEND env > tmct.toml > "default") resolved below.
119
+ liveReference = false,
120
+ // The storage-backend seam: the default (empty or "default") resolves to
121
+ // Backend C — the sqlite store at .tmct/memory/graph.sqlite, a live
122
+ // node:sqlite connection lazily imported on open. The flat-file Backend A is
123
+ // retired from routing. "memory" selects Backend B (zero disk I/O,
124
+ // session-scoped). This is `tmct chat --memory-backend <...>`'s
125
+ // already-resolved value; full precedence (this param > TMCT_MEMORY_BACKEND
126
+ // env > tmct.toml > the sqlite default) resolved below.
125
127
  memoryBackend = null,
126
128
  } = {}) {
127
129
  // EPHEMERAL mode (--ephemeral, or TMCT_EPHEMERAL=1): read the target graph but
@@ -135,6 +137,12 @@ export async function createSession({
135
137
  // narrate mode already on. Session-scoped and mutable — `/narrate on|off`
136
138
  // flips it turn-to-turn (see `turn()` below). Default OFF.
137
139
  let narrateOn = narrate || /^(1|true|yes)$/i.test(String(env.TMCT_NARRATE || ""));
140
+ // LIVE WIKIPEDIA supplement (--live-wikipedia, TMCT_LIVE_WIKIPEDIA=1, or
141
+ // tmct.toml's corpus tier3 — the tier that already means "reach for the
142
+ // network"): start the session with the live lookup enabled. Session-scoped
143
+ // and mutable — `/wiki on|off` flips it turn-to-turn, exactly like narrate.
144
+ // Default OFF; the toml tier is folded in below, once toml has resolved.
145
+ let liveReferenceOn = liveReference || /^(1|true|yes)$/i.test(String(env.TMCT_LIVE_WIKIPEDIA || ""));
138
146
  // Graph resolution order (delegates to src/services/cli-args.mjs's
139
147
  // resolveRuntimeConfig): explicit --graph path(s) win outright; then --repo
140
148
  // (never silently redirected by env); then TMCT_GRAPH_FILE env; then
@@ -190,6 +198,10 @@ export async function createSession({
190
198
  // to the shipped defaults for every key.
191
199
  const gameConfig = resolveGameConfig(toml);
192
200
 
201
+ // tmct.toml's corpus tier3 opts the session into the live supplement too —
202
+ // the flag/env tiers above stay authoritative when set.
203
+ if (toml?.corpus?.tier === "tier3") liveReferenceOn = true;
204
+
193
205
  // Ephemeral: keep config.graphFile pointing at the READ graph, but divert the
194
206
  // write base (repo → logs/memory/sessions) to a throwaway temp dir. The committed
195
207
  // target is never touched; the demo's memory simply doesn't persist across runs.
@@ -259,9 +271,9 @@ export async function createSession({
259
271
  };
260
272
 
261
273
  // `memoryDir` is the opaque token every memory/core.mjs call in this file
262
- // threads through unchanged. Backend A (default) keeps it the plain repo
263
- // string; Backend B/C swap in a handle instead. Precedence — CLI flag > env
264
- // > tmct.toml > default.
274
+ // threads through unchanged always a Backend B/C handle from
275
+ // openMemoryBackend now that the flat-file Backend A is retired from
276
+ // routing. Precedence — CLI flag > env > tmct.toml > the sqlite default.
265
277
  const backendChoice = String(memoryBackend || env.TMCT_MEMORY_BACKEND || toml?.memory?.backend || "").trim().toLowerCase();
266
278
  // openMemoryBackend is the ONE shared resolver for this seam — init.mjs's
267
279
  // corpus seed calls the exact same function, so a repo's seeded facts and
@@ -276,14 +288,14 @@ export async function createSession({
276
288
  // bootstrap (a fixture/provider graph never seeds), only once (the marker),
277
289
  // and never when TMCT_NO_SEED=1 opts out.
278
290
  //
279
- // Known Backend B/C limitation: seedBootstrapMemory/hasSeededVocabulary and
280
- // sessions.mjs's own per-turn utterance mirror all resolve their marker
281
- // file / repoDir directly off the STRING `repo` path, not the actual
282
- // Backend B/C handle so W3 seeding is skipped for a non-default backend,
283
- // and a Backend B/C session's Utterance/Session individuals still land in
284
- // an ordinary Backend-A .tmct/memory/graph.json. Taught FACTS themselves
285
- // are unaffected: only the conversational transcript mirror leaks onto
286
- // disk, never the facts.
291
+ // Known residual split: seedBootstrapMemory/hasSeededVocabulary resolve
292
+ // their marker file off the STRING `repo` path (correct — the marker is a
293
+ // file, not store content), so W3 seeding still only fires on the default
294
+ // token. And sessions.mjs's per-turn utterance mirror still writes an
295
+ // ordinary .tmct/memory/graph.json off the repo string a file no routed
296
+ // reader opens now that Backend A is retired from routing. Taught FACTS
297
+ // themselves are unaffected: only the conversational transcript mirror
298
+ // leaks onto disk, never the facts.
287
299
  let seeded = null;
288
300
  if (empty && backendChoice === "" && String(env.TMCT_NO_SEED || "") !== "1") {
289
301
  seeded = await seedBootstrapMemory(repo, env);
@@ -336,6 +348,7 @@ export async function createSession({
336
348
  get planState() { return planState; },
337
349
  get turns() { return turns; },
338
350
  get narrate() { return narrateOn; },
351
+ get liveReference() { return liveReferenceOn; },
339
352
  promptFor: () => promptFor(focus),
340
353
 
341
354
  /** One dispatched turn through the FULL sink sequencing (writeLog → writeSidecar
@@ -346,7 +359,7 @@ export async function createSession({
346
359
  async turn(line) {
347
360
  let result;
348
361
  try {
349
- result = await runTurn(line, { config, source, graph, focus, last, memoryDir, sessionId, env, lexicon, narrate: narrateOn, vocabHint, tel, biasByBundle, planState, gameConfig });
362
+ result = await runTurn(line, { config, source, graph, focus, last, memoryDir, sessionId, env, lexicon, narrate: narrateOn, liveReference: liveReferenceOn, vocabHint, tel, biasByBundle, planState, gameConfig });
350
363
  } catch (e) {
351
364
  const ts = new Date().toISOString();
352
365
  const message = e instanceof Error ? e.message : String(e);
@@ -357,13 +370,15 @@ export async function createSession({
357
370
  turns += 1;
358
371
  return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, prompt: promptFor(focus) };
359
372
  }
360
- const { answer, logLines, record, focus: nextFocus, last: nextLast, end, narrate: nextNarrate } = result;
373
+ const { answer, logLines, record, focus: nextFocus, last: nextLast, end, narrate: nextNarrate, liveReference: nextLiveReference } = result;
361
374
  focus = nextFocus;
362
375
  last = nextLast;
363
376
  if ("planState" in result) planState = result.planState;
364
- // /narrate on|off (runCommand) rides the turn RESULT the same way a focus
365
- // update does — apply it to this handle's session-scoped state.
377
+ // /narrate on|off and /wiki on|off (runCommand) ride the turn RESULT the
378
+ // same way a focus update does — apply them to this handle's
379
+ // session-scoped state.
366
380
  if (typeof nextNarrate === "boolean") narrateOn = nextNarrate;
381
+ if (typeof nextLiveReference === "boolean") liveReferenceOn = nextLiveReference;
367
382
  await writeLog(logLines.join("\n") + "\n");
368
383
  await writeSidecar(record);
369
384
  turnRecords.push(record);
@@ -416,12 +431,13 @@ export async function runChat({
416
431
  gitRoot = gitToplevel,
417
432
  ephemeral = false,
418
433
  narrate = false,
434
+ liveReference = false,
419
435
  memoryBackend = null,
420
436
  } = {}) {
421
437
  // createSession's first-run seed (~2-3s) produces ZERO output until it fully
422
438
  // resolves, which otherwise reads as `npm run chat` hanging with total silence.
423
439
  output.write("tmct — starting…\n");
424
- const session = await createSession({ repoPath, graphPaths, configPath, source, env, cwd, gitRoot, ephemeral, narrate, memoryBackend });
440
+ const session = await createSession({ repoPath, graphPaths, configPath, source, env, cwd, gitRoot, ephemeral, narrate, liveReference, memoryBackend });
425
441
 
426
442
  const dim = (s) => (env.NO_COLOR || !output.isTTY ? s : `\x1b[2m${s}\x1b[0m`);
427
443
  for (const line of session.bannerLines) output.write(dim(line) + "\n");
@@ -46,8 +46,12 @@ import { readConstructionFiles } from "../adapters/corpus/construction-banks.mjs
46
46
  import { fuzzyMatchInSet, fuzzyBound } from "../domain/interpret/fuzzy.mjs";
47
47
  import { loadLexicon, lookupNoun } from "../domain/grammar/lexicon.mjs";
48
48
  import { pickPhrase } from "../domain/answer-variants.mjs";
49
- import { REFERENCE_PACK_NAME, cleanMissReferenceTerm, renderReferenceAnswer, referenceProvenanceTag } from "../domain/reference-pack.mjs";
49
+ import {
50
+ REFERENCE_PACK_NAME, cleanMissReferenceTerm, renderReferenceAnswer, referenceProvenanceTag,
51
+ LIVE_PACK_NAME, cleanMissLiveTerm, renderLiveReferenceAnswer, liveProvenanceTag,
52
+ } from "../domain/reference-pack.mjs";
50
53
  import { getReferencePackProvider } from "../adapters/corpus/reference-pack.mjs";
54
+ import { getLiveReferenceProvider } from "../adapters/corpus/wikipedia-live.mjs";
51
55
  import { CHILD_PACK_NAME, childProvenanceTag } from "../domain/child-pack.mjs";
52
56
  import { getChildPackProvider } from "../adapters/corpus/child-pack.mjs";
53
57
  import { dialogueActForLane } from "../domain/dialogue-acts.mjs";
@@ -2391,6 +2395,15 @@ const BOARD_WHERE_EVERY_RE = /^where\s+(?:is|are)\s+(?:every|each|all(?:\s+the)?
2391
2395
  * question-shape ("was X Y?") this could ever misfire on. */
2392
2396
  const TEACH_PROPERTY_RE = /^(?:every\s+|each\s+|all\s+|the\s+)?(.+?)\s+(?:is|are|was|were)\s+(?!an?\b|the\b)([A-Za-z][\w-]*)$/i;
2393
2397
 
2398
+ /** The closed place-adverb set ("anywhere"/"everywhere"/"nowhere"/
2399
+ * "somewhere"). One of these sitting in an OBJECT slot ("http.mjs used is
2400
+ * anywhere") marks a garbled usage QUESTION, never a storable property or
2401
+ * relation object: no reader ever matches a fact whose object is a place
2402
+ * adverb, so storing one is a silent write with no possible read-back.
2403
+ * Every teach path that binds a free-form object refuses on it, and the
2404
+ * teach-offer generators never suggest a phrasing that contains one. */
2405
+ const PLACE_ADVERB_OBJECT_RE = /^(?:anywhere|everywhere|nowhere|somewhere)$/i;
2406
+
2394
2407
  /** The teach lane's provenance tag — mirrors grammar/assert.mjs's provenanceTag
2395
2408
  * shape under a distinct "teach:" family, so a taught fact is auditable apart
2396
2409
  * from the ACE-parsed asserts: teach:chat:<sessionId>@<ts>. core.mjs maps the
@@ -2735,12 +2748,18 @@ async function objectReadsAsNonNoun(word) {
2735
2748
  return false;
2736
2749
  }
2737
2750
  }
2738
- async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon }, cache = null) {
2751
+ async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon, classIntent = false }, cache = null) {
2739
2752
  if (!memoryDir) return null;
2740
2753
  const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
2741
2754
  if (!m) return null;
2742
2755
  const [, det, subjectRaw, verb, objectRaw] = m;
2743
- if (!/^(?:every|each|all|any)$/i.test((det || "").trim())) return null; // class-level mint needs a real universal quantifier
2756
+ // `classIntent` (an explicit "kind of"/"type of" infix in the ORIGINAL
2757
+ // sentence, detected by the caller before stripKindOf erased it) is the
2758
+ // same class-level signal a universal quantifier gives: "dog is a kind of
2759
+ // mammal" — tmct's OWN read-back phrasing for a subClassOf fact — names a
2760
+ // class relation, never one entity's property, so it earns the mint the
2761
+ // bare unmarked "module is banana" shape must still never get.
2762
+ if (!/^(?:every|each|all|any)$/i.test((det || "").trim()) && !classIntent) return null; // class-level mint needs a universal quantifier or an explicit kind-of infix
2744
2763
  const { loadLexicon, lookupNoun } = await import("../domain/grammar/lexicon.mjs");
2745
2764
  const lex = lexicon || loadLexicon();
2746
2765
  const subjectGrounded = await isGroundedTerm(subjectRaw, lex, memoryDir, cache);
@@ -2832,6 +2851,7 @@ async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon
2832
2851
  const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
2833
2852
  if (!m) return null;
2834
2853
  const [, , subjectRaw, , objectRaw] = m;
2854
+ if (PLACE_ADVERB_OBJECT_RE.test(objectRaw)) return null; // a place adverb is never a property
2835
2855
  const { loadLexicon, lookupNoun, classify } = await import("../domain/grammar/lexicon.mjs");
2836
2856
  const lex = lexicon || loadLexicon();
2837
2857
  // Y already a known NOUN or a fact-grounded CLASS term — a genuine class-
@@ -3162,6 +3182,7 @@ async function generalVerbTeach(payload) {
3162
3182
  // object no question can match.
3163
3183
  const object = folded.object.replace(/^(?:an?|the)\s+/i, "").trim();
3164
3184
  if (!subject || !object) return null; // no well-formed triple — honest decline (point 6)
3185
+ if (PLACE_ADVERB_OBJECT_RE.test(object)) return null; // a place adverb is never a real object
3165
3186
  return { subject, predicate: negated ? negatedPredicate(folded.predicate) : folded.predicate, object };
3166
3187
  }
3167
3188
 
@@ -3420,13 +3441,24 @@ function matchBareCanTeach(text) {
3420
3441
  * user to teach a class-membership fact that was never what they said. Only
3421
3442
  * a payload that ALREADY carried an article gets its article corrected —
3422
3443
  * the "every monkey is a animal" -> "an animal" case this function exists
3423
- * for in the first place. */
3444
+ * for in the first place.
3445
+ *
3446
+ * A PLURAL phrasing ("all spiders are venomous") folds its subject to the
3447
+ * singular the suggested "every …" rewrite grammatically requires — "every
3448
+ * spiders is venomous" is ungrammatical AND stores under a different
3449
+ * spelling than the singular every other frame uses, so following it
3450
+ * verbatim wrote a plural-keyed orphan fact. Gated on the "are" copula, the
3451
+ * same is/are safety distinction unknownSubjectFallback draws (an s-final
3452
+ * singular like "redis is a cache" must never strip). A trailing
3453
+ * sentence-final mark is tolerated for the same reason UNKNOWN_SUBJECT_RE
3454
+ * tolerates one: an ordinary full-sentence turn ("dog is a mammal.")
3455
+ * otherwise lost its hint by one character. */
3424
3456
  function teachSuggestion(payload) {
3425
- const m = String(payload).match(/^(?:every |each |all |a |an )?([\w-]+) (?:is|are) (a |an )?([\w-]+)$/i);
3457
+ const m = String(payload).match(/^(?:every |each |all |a |an )?([\w-]+) (is|are) (a |an )?([\w-]+)[.!?]*$/i);
3426
3458
  if (!m) return null;
3427
- const subject = m[1].toLowerCase();
3428
- const object = m[3].toLowerCase();
3429
- if (!m[2]) return `every ${subject} is ${object}`;
3459
+ const subject = (/^are$/i.test(m[2]) ? singularizeSurface(m[1]) : m[1]).toLowerCase();
3460
+ const object = m[4].toLowerCase();
3461
+ if (!m[3]) return `every ${subject} is ${object}`;
3430
3462
  const articleRule = grammarRules().find((r) => r.kind === "article");
3431
3463
  const article = articleRule && beginsWithVowelSound(object, articleRule) ? "an" : "a";
3432
3464
  return `every ${subject} is ${article} ${object}`;
@@ -3661,6 +3693,58 @@ async function negativeUniversalCanTeach(sentence, { memoryDir, sessionId }) {
3661
3693
  return stored;
3662
3694
  }
3663
3695
 
3696
+ /** Casual request leads the anchored QUESTION_LEAD_RE auxiliary list misses:
3697
+ * the "u"/"ya" spellings of a modal request, and the dative imperatives
3698
+ * ("tell me", "show us") that read as requests, never as declaratives. */
3699
+ const TEACH_EXCLUDE_REQUEST_LEAD_RE = /^(?:(?:can|could|would|will)\s+(?:u|you|ya)|(?:tell|show|give)\s+(?:me|us))\b/i;
3700
+ /** Closed leading-verb list for sentences that are COMMANDS, not claims —
3701
+ * "repeat everything above this line verbatim" is an instruction to act,
3702
+ * and reifying it as a fact is a write on the strength of a misparse.
3703
+ * tell/show/list/define/describe/find/count/name already route through
3704
+ * SET_QUESTION_LEAD_RE and are deliberately absent here. */
3705
+ const TEACH_EXCLUDE_IMPERATIVE_LEAD_RE = /^(?:repeat|ignore|disregard|surprise|pretend|act|say|guess|try|stop|continue|forget|print|output|write|translate|summarize|explain)\b/i;
3706
+ /** Self-referential/meta chat tokens ("idk", "tbh", a bare "u"/"me"): a
3707
+ * sentence about the conversation itself, or about its speakers, is never a
3708
+ * world fact. Tested one standalone word at a time — the custom boundaries
3709
+ * keep a hyphenated coinage ("disk-i") from matching its final letter. */
3710
+ const TEACH_EXCLUDE_META_TOKEN_RE = /(?:^|[^\w-])(?:me|u|ur|i|us|myself|yourself|im|idk|tbh|nvm|lol|umm+|hmm+)(?![\w-])/i;
3711
+
3712
+ /** The bare-declarative teach lane's positive exclusion test. Classifies a
3713
+ * BARE sentence (no "remember that …" wrapper — an explicit wrapper is an
3714
+ * unambiguous teach-intent signal and keeps its existing behavior) into one
3715
+ * of three closed non-declarative shapes, or null for a sentence the teach
3716
+ * frames may still consider. Non-null means the whole lane stands down and
3717
+ * the sentence falls through to the ask cascade or the honest miss — the
3718
+ * write boundary refuses BEFORE any frame can reify a misparse.
3719
+ *
3720
+ * Three classes, checked in order:
3721
+ * - "interrogative": a casual request lead QUESTION_LEAD_RE's anchored
3722
+ * first-word list misses, or a genuine mid-sentence interrogative
3723
+ * (hasMidSentenceInterrogative — run here unconditionally, where the
3724
+ * per-frame gates below only ever ran it on their own paths);
3725
+ * - "imperative": a closed leading command verb. The retract phrasings
3726
+ * ("forget that X is a Y", "forget that disk-1 rests on peg-b") are
3727
+ * carved out — they are this lane's own, deliberate write-boundary
3728
+ * actions, not misparses;
3729
+ * - "self-referential": a standalone meta/chat token anywhere in the
3730
+ * sentence. A pronoun-SUBJECT sentence ("i am a developer") is carved
3731
+ * out so it still reaches the pronoun guard's specific decline below —
3732
+ * same no-store outcome, better guidance than a silent fall-through.
3733
+ *
3734
+ * Runs applyPreambleFrames itself (idempotent on an already-peeled
3735
+ * sentence), so a stripped greeting can never leave a leading token that
3736
+ * misclassifies, and callers outside teachLane can hand it a raw surface. */
3737
+ async function teachExclusionReason(sentence) {
3738
+ const s = applyPreambleFrames(String(sentence || "").trim());
3739
+ if (TEACH_EXCLUDE_REQUEST_LEAD_RE.test(s) || (await hasMidSentenceInterrogative(s))) return "interrogative";
3740
+ const unpunctuated = s.replace(/[.!?]+\s*$/, "");
3741
+ if (TEACH_EXCLUDE_IMPERATIVE_LEAD_RE.test(s)
3742
+ && !RETRACT_FORGET_RE.test(unpunctuated) && !RETRACT_FORGET_LOCATIVE_RE.test(unpunctuated)) return "imperative";
3743
+ if (TEACH_EXCLUDE_META_TOKEN_RE.test(s) && !TEACH_PRONOUN_RE.test(s)) return "self-referential";
3744
+ return null;
3745
+ }
3746
+ export { teachExclusionReason };
3747
+
3664
3748
  async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null, planHolder = null }) {
3665
3749
  // A closed discourse-marker preamble ahead of a teach sentence ("howdy
3666
3750
  // pardner, remember that TaskController is fragile") would otherwise
@@ -3683,6 +3767,12 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
3683
3767
  if (QUESTION_LEAD_RE.test(correctMisspellings(rawInput))) return null;
3684
3768
  const m = rawInput.match(TEACH_RE);
3685
3769
  const wrappedInput = m ? m[1].trim() : null;
3770
+ // The positive exclusion test (teachExclusionReason, above) — BARE surface
3771
+ // only: an interrogative, imperative, or self-referential sentence never
3772
+ // reaches any teach frame, so a fresh casual phrasing can't slip past the
3773
+ // per-frame gates and reify as a fact. An explicit wrapper keeps its
3774
+ // existing, more permissive path.
3775
+ if (wrappedInput == null && (await teachExclusionReason(rawInput))) return null;
3686
3776
  // Refuse an existential BEFORE any frame below can read it as a universal:
3687
3777
  // every one of them stores "some men are fathers" as a premise meaning every
3688
3778
  // man, whether it keeps the quantifier as an attribute the reasoner doesn't
@@ -3712,7 +3802,12 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
3712
3802
  // kind of parent" normalizes to "a father is a parent", which
3713
3803
  // unknownSubjectFallback already stores as father ⊑ parent ("parent" is
3714
3804
  // already a lexicon noun).
3715
- const stripKindOf = (s) => (s == null ? s : s.replace(/\b(is|are|was|were)\s+(?:an?\s+)?(?:kind|type)\s+of\s+/i, "$1 a "));
3805
+ const kindOfInfixRe = /\b(is|are|was|were)\s+(?:an?\s+)?(?:kind|type)\s+of\s+/i;
3806
+ const stripKindOf = (s) => (s == null ? s : s.replace(kindOfInfixRe, "$1 a "));
3807
+ // Remembered BEFORE the strip erases it: an explicit "kind of"/"type of"
3808
+ // infix is an unambiguous class-level claim, and unknownObjectFallback's
3809
+ // quantifier gate accepts it as a peer of "every" (its own docblock).
3810
+ const kindOfClassIntent = kindOfInfixRe.test(wrappedInput ?? rawInput);
3716
3811
  // "my <class-noun> <Name> is/are …" — a THIRD natural phrasing of the exact
3717
3812
  // same "X is a Y" assertion this lane already teaches two other ways ("john
3718
3813
  // is a man", a bare name; "every cat is an animal", a universal quantifier) — a
@@ -4600,7 +4695,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
4600
4695
  // STATIC lexicon (or a prior taught fact) already grounds can mint a
4601
4696
  // brand-new object term. See unknownObjectFallback's own docblock for the
4602
4697
  // exact narrowing rules (the "both sides ungrounded" safety guard, etc.).
4603
- const objectFallback = await unknownObjectFallback(payload, { memoryDir, sessionId, lexicon }, cache);
4698
+ const objectFallback = await unknownObjectFallback(payload, { memoryDir, sessionId, lexicon, classIntent: kindOfClassIntent }, cache);
4604
4699
  if (objectFallback) return objectFallback;
4605
4700
  // ADJECTIVE-MINT fallback: tried right after unknownObjectFallback
4606
4701
  // declines, so a grounded subject (static
@@ -4617,7 +4712,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
4617
4712
  // wrapped "X is a Y" over known lexicon still lands as rdfs:subClassOf.
4618
4713
  if (wrapped) {
4619
4714
  const prop = wrapped.match(TEACH_PROPERTY_RE);
4620
- if (prop) {
4715
+ if (prop && !PLACE_ADVERB_OBJECT_RE.test(prop[2])) {
4621
4716
  const stored = await teachFact(memoryDir, sessionId, {
4622
4717
  subject: prop[1], predicate: HAS_PROPERTY_PREDICATE, object: prop[2],
4623
4718
  });
@@ -5270,6 +5365,7 @@ export async function helpText() {
5270
5365
  ["/capabilities", "what /plan can plan over: the built-in graph tools plus your taught actions"],
5271
5366
  ["/syllogise <term>", "work out and remember what follows from the facts about a term (needed for chains longer than 2 hops)"],
5272
5367
  ["/narrate on|off", "verbose developer/debug mode: decision points, matched pattern, results+sources, goal per turn"],
5368
+ ["/wiki on|off", "live Wikipedia supplement (default off): a question I can't answer also tries en.wikipedia.org (network), cited"],
5273
5369
  ["/help", "this list"],
5274
5370
  ["/exit", "leave the session (also Ctrl+C / Ctrl+D)"],
5275
5371
  ];
@@ -7666,7 +7762,8 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
7666
7762
  // only ever fires for "it"/"this"/"that" — never a personal pronoun like
7667
7763
  // "you", so `subject` itself would already carry the pronoun verbatim).
7668
7764
  if (subject && !/^there\b/i.test(subject) && !envelope?.parsed
7669
- && !IS_ADJECTIVE_YESNO_PRONOUN_SUBJECT_RE.test(rawSubject)) {
7765
+ && !IS_ADJECTIVE_YESNO_PRONOUN_SUBJECT_RE.test(rawSubject)
7766
+ && !PLACE_ADVERB_OBJECT_RE.test(emptyIsAdj[2].trim())) {
7670
7767
  return unknownAdjectiveOffer(subject, emptyIsAdj[2].trim().toLowerCase());
7671
7768
  }
7672
7769
  }
@@ -8815,7 +8912,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
8815
8912
  // subject known only under an UNRELATED property (e.g. "deprecated")
8816
8913
  // must not offer to teach "tested" when ask()'s own grammar already
8817
8914
  // resolved it structurally.
8818
- if (!envelope?.parsed) return unknownAdjectiveOffer(subject, adjective);
8915
+ if (!envelope?.parsed && !PLACE_ADVERB_OBJECT_RE.test(adjective)) return unknownAdjectiveOffer(subject, adjective);
8819
8916
  }
8820
8917
  }
8821
8918
 
@@ -9459,6 +9556,39 @@ async function referencePackMissAnswer(term, { graph, memoryDir, lexicon, env, c
9459
9556
  return key ? referencePackAnswerForKey(key, env) : null;
9460
9557
  }
9461
9558
 
9559
+ /** The LIVE variant of cleanMissPackKey — the same resolveEntity and
9560
+ * remembered-fact checks, but through cleanMissLiveTerm, which drops the
9561
+ * lexicon-membership wall: a word the lexicon has never met is exactly what
9562
+ * the live lookup exists for. Null means the turn proceeds byte-identically
9563
+ * to a live-off run. */
9564
+ async function cleanMissLiveKey(term, { graph, memoryDir, lexicon, cache }) {
9565
+ if (!term || !memoryDir) return null;
9566
+ let key = null;
9567
+ try { key = cleanMissLiveTerm(term, lexicon ?? undefined); } catch { key = null; }
9568
+ if (!key) return null;
9569
+ if (await resolveEntity(graph, term)) return null;
9570
+ let normFactTerm;
9571
+ try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
9572
+ const variants = factTermVariants(normFactTerm, term);
9573
+ variants.add(key);
9574
+ const rows = await factRows(memoryDir, cache);
9575
+ if (rows.some((f) => variants.has(f.subject) || variants.has(f.object))) return null;
9576
+ return key;
9577
+ }
9578
+
9579
+ /** The live Wikipedia lookup for an already-gated key: the article, or null
9580
+ * (toggle-off provider, network failure, throttle, drift-guard rejection —
9581
+ * all byte-identical to a live-off run). `onLiveLookup` is a notify-only
9582
+ * hook (the web page's "searching wikipedia…" statusline); its own failure
9583
+ * is swallowed too. */
9584
+ async function liveReferenceAnswerForKey(key, onLiveLookup) {
9585
+ try { if (typeof onLiveLookup === "function") onLiveLookup(key); } catch { /* notify-only */ }
9586
+ let article = null;
9587
+ try { article = await getLiveReferenceProvider().lookup(key); } catch { article = null; }
9588
+ if (!article) return null;
9589
+ return { key, article, text: renderLiveReferenceAnswer(key, article) };
9590
+ }
9591
+
9462
9592
  /** The child-pack half of learn-on-miss, for an already-gated key: look the
9463
9593
  * key up in the shipped child triples pack and append every fact under child
9464
9594
  * provenance, so the SAME question can be re-asked from the store. Null on a
@@ -9480,13 +9610,13 @@ async function childPackFactsForKey(key, { memoryDir, env, cache }) {
9480
9610
  /** Store the article's first-sentence isa as a subClassOf fact carrying
9481
9611
  * reference provenance — AFTER the cited answer composed, and failure-
9482
9612
  * tolerated: the answer stands whether or not the fact lands. */
9483
- async function appendReferenceIsaFact(memoryDir, key, article, cache) {
9613
+ async function appendReferenceIsaFact(memoryDir, key, article, cache, tagFor = referenceProvenanceTag) {
9484
9614
  if (!article?.isa) return;
9485
9615
  try {
9486
9616
  const { appendFact } = await import("../adapters/memory/core.mjs");
9487
9617
  await appendFact(memoryDir, {
9488
9618
  subject: key, predicate: "rdfs:subClassOf", object: article.isa,
9489
- provenance: referenceProvenanceTag(article),
9619
+ provenance: tagFor(article),
9490
9620
  });
9491
9621
  if (cache) cache.rows = null;
9492
9622
  } catch { /* tolerated — the cited answer is already composed */ }
@@ -10649,7 +10779,7 @@ const DECISION_RECALL_RE = /^(?:remind\s+me\s+)?what\s+(?:did\s+)?(?:we|i|you)\s
10649
10779
  * than silently accepted alongside the current location. */
10650
10780
  const MOVE_HISTORY_RE = /^where\s+did\s+(.+?)\s+(?:move|get\s+moved|go)(?:\s+to)?[?.!\s]*$/i;
10651
10781
 
10652
- async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null, tel = null, biasByBundle = {}, cache = null, vocabAntecedent = null, planHolder = null, gameConfig = DEFAULT_GAME_CONFIG }) {
10782
+ async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null, tel = null, biasByBundle = {}, cache = null, vocabAntecedent = null, planHolder = null, gameConfig = DEFAULT_GAME_CONFIG, liveReference = false, onLiveLookup = null }) {
10653
10783
  const ts = new Date().toISOString();
10654
10784
  // DISCOURSE ANAPHORA: a follow-up like "which of those are tested" / "count
10655
10785
  // them" filters or counts the PREVIOUS answer's entity set, threaded as
@@ -11251,6 +11381,14 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
11251
11381
  const ref = await referencePackAnswerForKey(key, env);
11252
11382
  if (ref) bareMetaHit = { text: ref.text, replace: true, reference: ref };
11253
11383
  }
11384
+ // The live Wikipedia supplement (opt-in), LAST — the shipped packs
11385
+ // always speak first, and a live null/failure leaves bareMetaHit
11386
+ // exactly as a live-off run would.
11387
+ if (!bareMetaHit && liveReference && refTerm) {
11388
+ const liveKey = await cleanMissLiveKey(refTerm, { graph, memoryDir, lexicon, cache });
11389
+ const live = liveKey ? await liveReferenceAnswerForKey(liveKey, onLiveLookup) : null;
11390
+ if (live) bareMetaHit = { text: live.text, replace: true, live };
11391
+ }
11254
11392
  }
11255
11393
  }
11256
11394
  // A bare "what is X" naming a REAL code-graph entity (not a taught fact,
@@ -11298,6 +11436,15 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
11298
11436
  note(trace, "lane: (2b) REFERENCE PACK — a bare \"what is X\" clean miss answered from the shipped reference pack, cited");
11299
11437
  note(trace, `source: reference pack ${REFERENCE_PACK_NAME} — article "${bareMetaHit.reference.article.title}" (revid ${bareMetaHit.reference.article.revid})`);
11300
11438
  await appendReferenceIsaFact(memoryDir, bareMetaHit.reference.key, bareMetaHit.reference.article, cache);
11439
+ } else if (bareMetaHit?.live) {
11440
+ // The bare-form LIVE hit settles the same way, under live provenance.
11441
+ answer = bareMetaHit.text;
11442
+ via = "reference";
11443
+ recordMiss = false;
11444
+ handled = true;
11445
+ note(trace, "lane: (2b) LIVE WIKIPEDIA — a bare \"what is X\" clean miss answered from a live en.wikipedia.org lookup (opt-in), cited");
11446
+ note(trace, `source: live reference ${LIVE_PACK_NAME} — article "${bareMetaHit.live.article.title}" (revid ${bareMetaHit.live.article.revid})`);
11447
+ await appendReferenceIsaFact(memoryDir, bareMetaHit.live.key, bareMetaHit.live.article, cache, liveProvenanceTag);
11301
11448
  } else if (bareMetaHit) {
11302
11449
  answer = bareMetaHit.replace ? bareMetaHit.text : `${answer}\n${bareMetaHit.text}`;
11303
11450
  // Same discipline as lane (3): a fact-lane return flagged `miss` is an
@@ -11707,6 +11854,22 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
11707
11854
  await appendReferenceIsaFact(memoryDir, ref.key, ref.article, cache);
11708
11855
  }
11709
11856
  }
11857
+ // The live Wikipedia supplement (opt-in), strictly AFTER both shipped
11858
+ // packs: its own gate (no lexicon-membership wall) may pass where the
11859
+ // pack gate could not, and a null/throwing lookup leaves the honest
11860
+ // miss byte-identical to a live-off run.
11861
+ if (miss && recordMiss && via === "composed" && liveReference && refTerm) {
11862
+ const liveKey = await cleanMissLiveKey(refTerm, { graph, memoryDir, lexicon, cache });
11863
+ const live = liveKey ? await liveReferenceAnswerForKey(liveKey, onLiveLookup) : null;
11864
+ if (live) {
11865
+ answer = live.text;
11866
+ via = "reference";
11867
+ recordMiss = false;
11868
+ note(trace, "lane: (4h) LIVE WIKIPEDIA — a clean miss answered from a live en.wikipedia.org lookup (opt-in), cited");
11869
+ note(trace, `source: live reference ${LIVE_PACK_NAME} — article "${live.article.title}" (revid ${live.article.revid})`);
11870
+ await appendReferenceIsaFact(memoryDir, live.key, live.article, cache, liveProvenanceTag);
11871
+ }
11872
+ }
11710
11873
  }
11711
11874
  // (5) #1 SHORT TAILORED MISS — replace ONLY the engine's full grammar cheat-sheet
11712
11875
  // wall (WALL_MISS_RE). Receipt-bearing misses keep their specific wording.
@@ -11890,24 +12053,26 @@ const GOAL_BY_COMMAND = {
11890
12053
  arch: "understand the overall architecture (package/module boundaries)",
11891
12054
  capabilities: "see what /plan can plan over — built-in query tools and taught actions",
11892
12055
  syllogise: "materialize the entailed facts that follow from what's remembered about one term",
12056
+ wiki: "toggle the live Wikipedia supplement for questions nothing local can answer",
11893
12057
  };
11894
12058
 
11895
12059
  /** A slash-command → the mapped tool (or the /help, /focus, /narrate, unknown
11896
12060
  * cases). Returns the same { answer, logLines, record, focus } shape as
11897
12061
  * runAsk. Also carries a `goal` field mirroring runAsk's own, so
11898
12062
  * withGoalLine's "Goal (inferred): …" line fires for command dispatches too. */
11899
- async function runCommand(line, { config, source, graph, focus, memoryDir, trace, narrate = false, tel = null, biasByBundle = {}, cache = null }) {
12063
+ async function runCommand(line, { config, source, graph, focus, memoryDir, trace, narrate = false, liveReference = false, tel = null, biasByBundle = {}, cache = null }) {
11900
12064
  const ts = new Date().toISOString();
11901
12065
  const sp = line.indexOf(" ");
11902
12066
  const name = (sp === -1 ? line.slice(1) : line.slice(1, sp)).toLowerCase();
11903
12067
  const argText = (sp === -1 ? "" : line.slice(sp + 1)).trim();
11904
- const mk = (answer, { resolvedIds = [], miss = false, newFocus = focus, narrateNext } = {}) => ({
12068
+ const mk = (answer, { resolvedIds = [], miss = false, newFocus = focus, narrateNext, liveReferenceNext } = {}) => ({
11905
12069
  answer,
11906
12070
  logLines: [ts, `> ${line}`, answer, ""],
11907
12071
  record: { type: "turn", ts, query: line, command: name, via: "command", resolvedIds, answeredIds: [], miss },
11908
12072
  focus: newFocus,
11909
12073
  goal: GOAL_BY_COMMAND[name] || "use a specific tool/command directly",
11910
12074
  ...(narrateNext !== undefined ? { narrate: narrateNext } : {}),
12075
+ ...(liveReferenceNext !== undefined ? { liveReference: liveReferenceNext } : {}),
11911
12076
  });
11912
12077
 
11913
12078
  if (name === "help") { note(trace, "goal: get oriented / learn available commands"); return mk(await helpText()); }
@@ -11931,6 +12096,20 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
11931
12096
  return mk(`narrate mode ${next ? "on" : "off"}.`, { narrateNext: next });
11932
12097
  }
11933
12098
 
12099
+ // /wiki on|off — the live Wikipedia supplement toggle (session-scoped,
12100
+ // exactly the /narrate pattern: the new state rides the turn RESULT as
12101
+ // `liveReference`, and each session shell applies it to its own mutable
12102
+ // state). A bare "/wiki" reports the CURRENT state and changes nothing.
12103
+ if (name === "wiki") {
12104
+ const arg = argText.toLowerCase();
12105
+ if (arg !== "on" && arg !== "off") {
12106
+ return mk(`live Wikipedia supplement is ${liveReference ? "on" : "off"} — /wiki on or /wiki off. `
12107
+ + "When on, a question I can't answer also tries en.wikipedia.org (network).");
12108
+ }
12109
+ const next = arg === "on";
12110
+ return mk(`live Wikipedia supplement ${next ? "on" : "off"}.`, { liveReferenceNext: next });
12111
+ }
12112
+
11934
12113
  // /memory [verbose] — what tmct remembers, as text (the same renderer
11935
12114
  // serves the `tmct memory` CLI).
11936
12115
  if (name === "memory") {
@@ -12871,7 +13050,7 @@ function vocabAntecedentFrom(last) {
12871
13050
  return m[1];
12872
13051
  }
12873
13052
 
12874
- export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, gameConfig = null, _noSplit = false } = {}) {
13053
+ export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, liveReference = false, onLiveLookup = null, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, gameConfig = null, _noSplit = false } = {}) {
12875
13054
  // Every game's tuning knobs (spider-fly's mass economy, guess-the-number's
12876
13055
  // bounds, the shared plan lane's search-depth cap) — a caller's own
12877
13056
  // gameConfig (chat-session.mjs resolves one per session from tmct.toml)
@@ -12934,7 +13113,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
12934
13113
  // the PLAN NEXT block below write planHolder.state; every other path leaves
12935
13114
  // it untouched, and the caller re-threads whatever comes back.
12936
13115
  const planHolder = { state: planState };
12937
- const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache, vocabAntecedent, planHolder, gameConfig: resolvedGameConfig };
13116
+ const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, liveReference, onLiveLookup, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache, vocabAntecedent, planHolder, gameConfig: resolvedGameConfig };
12938
13117
  // A DISPATCHED turn (count / slash-command / ask) becomes the new "last
12939
13118
  // answer" that why/say-more re-renders; a conversational turn does not.
12940
13119
  // Every dispatched turn's result passes through finish() here — the LAST