@polycode-projects/the-mechanical-code-talker 2.8.13 → 2.9.3

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 +208 -66
  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 +22 -26
  28. package/src/services/sessions.mjs +15 -3
  29. package/src/services/spider-fly-viz.mjs +52 -49
  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
package/README.md CHANGED
@@ -119,16 +119,22 @@ tmct> /exit
119
119
  ```
120
120
 
121
121
  **[Try it live in your browser →](https://polycode-projects.gitlab.io/the-mechanical-code-talker/)**
122
- is a real, interactive chat demo running client-side. Your browser runs the
123
- actual query engine, no server, no install. The page opens with a live chat
124
- you can talk to directly, then embeds the **memory ledger** (every fact as a
125
- readable sentence, drill by clicking the terms inside them), a Towers-of-Hanoi
126
- plan replayed move by move, and the two live games above spider-fly and the
127
- text adventure each with a link to open it full-screen.
122
+ runs the actual query engine client-side. No server, no install. The landing
123
+ page answers codebase questions live, and six more pages each ground their
124
+ own domain: a full chat seeded with 19,228 facts (the same nine bands as
125
+ `npm run init:xl`, capped to a 22.8 MB download), the **memory ledger**
126
+ (every fact as a readable sentence; drill by clicking the terms inside), a
127
+ Towers-of-Hanoi plan replayed move by move, the spider-and-fly and
128
+ text-adventure games, and a sprite gallery whose chat dock answers from
129
+ 1,033 generated sprite facts.
130
+ The site hosts its own copy of wink-nlp, ships its assets precompressed,
131
+ and a service worker precaches the big ones, so a second visit works
132
+ offline. `tmct chat --render spider-fly|adventure|sprites [--output <path>]`
133
+ writes any of the three view pages as one self-contained file.
128
134
 
129
135
  From a clone, two build scripts regenerate that demo so you can check it
130
- offline before it deploys. The example graph, the ledger page, and the
131
- engine copy land in `public/`. The in-page chat's query bundle is rebuilt
136
+ offline before it deploys. The example graph, the chat seed, the pages, and
137
+ the engine copy land in `public/`. The in-page chat's query bundle is rebuilt
132
138
  from the same `src/` the CLI runs:
133
139
 
134
140
  ```bash e2e cwd=repo
@@ -381,8 +387,7 @@ contradict each other it names the contradicting pair and stops rather than
381
387
  guessing on. Say `think of a number` to swap seats: tmct commits to a secret
382
388
  and answers your guesses honestly, reveals on request, and corrects you from
383
389
  its own record if you claim it already said `correct`. The behaviour is
384
- pinned by `test/corpus/games/guess-number.jsonl`, and the home page's demo
385
- rail plays the guesser side live in your browser.
390
+ pinned by `test/corpus/games/guess-number.jsonl`.
386
391
 
387
392
  **A text adventure.** Say `start the adventure` (or `play ashcombe hall`)
388
393
  and tmct loads a small country-house mystery from a lazily-fetched worlds
@@ -426,15 +431,25 @@ Facts first, prose second; if neither pack carries the term, the turn is the
426
431
  same honest miss it always was, byte for byte. An unknown word, a parse
427
432
  failure, or an ambiguous reading never consults a pack at all. The gate and
428
433
  both fallbacks are pinned by `test/corpus/reference.jsonl` and the
429
- chat-lane tests beside it, and the home page demos the article path live.
434
+ chat-lane tests beside it.
435
+
436
+ Live Wikipedia is the opt-in third tier, off by default. Turn it on with
437
+ `/wiki on` in a session, `--live-wikipedia` on the command line,
438
+ `TMCT_LIVE_WIKIPEDIA=1`, or `tier = "tier3"` under `[corpus]` in `tmct.toml`.
439
+ The browser chat page has the same switch ("ask Wikipedia when I don't
440
+ know"). When it is on and both shipped packs miss, tmct makes two requests
441
+ to en.wikipedia.org (a title search, then the page summary) and answers as a
442
+ cited read-out, CC BY-SA, pinned to the revision it read (provenance
443
+ `reference:wikipedia-live:<Title>@<revid>`). A failed lookup (no matching
444
+ title, a timeout, a rate limit) leaves the honest miss byte-identical.
430
445
 
431
446
  ## How it remembers
432
447
 
433
448
  tmct's memory has two layers, both fed by every parsed request and response and
434
449
  by cleaned session logs:
435
450
 
436
- - an **always-loaded OWL-labelled JSON graph** on disk under `.tmct/` (local
437
- artifact, never committed);
451
+ - an **always-loaded OWL-labelled graph** on disk under `.tmct/`, a SQLite
452
+ file by default (local artifact, never committed);
438
453
  - **text blocks under a PageRank-style index**, pulled into context on
439
454
  relevance rather than loaded wholesale.
440
455
 
@@ -456,20 +471,19 @@ reasoning).
456
471
 
457
472
  ### Memory backends
458
473
 
459
- The default memory backend writes an OWL-labelled JSON file under `.tmct/`.
460
- Two more exist: `memory` keeps taught facts in the process only, nothing
461
- written to disk; `sqlite` persists them to a local SQLite file instead
462
- (`.tmct/memory/graph.sqlite`). `tmct init --memory-backend sqlite` writes the
463
- choice into `tmct.toml` and every later `tmct chat` in that repo picks it up.
464
- Precedence is `--memory-backend` flag > `TMCT_MEMORY_BACKEND` env >
465
- tmct.toml's `[memory] backend` > the default. A library caller sets the same
466
- thing directly: `runChat({ memoryBackend: "sqlite" })`.
474
+ The default backend persists taught facts to a local SQLite file
475
+ (`.tmct/memory/graph.sqlite`). A second backend, `memory`, keeps them in the
476
+ process only and writes nothing to disk. `tmct init --memory-backend <name>`
477
+ writes the choice into `tmct.toml` and every later `tmct chat` in that repo
478
+ picks it up. Precedence is `--memory-backend` flag > `TMCT_MEMORY_BACKEND`
479
+ env > tmct.toml's `[memory] backend` > the sqlite default. A library caller
480
+ sets the same thing directly: `runChat({ memoryBackend: "memory" })`.
467
481
 
468
482
  Three init presets in `package.json` wrap the common setups. What each one
469
483
  runs (`--force` re-initializes the same directory):
470
484
 
471
485
  ```bash
472
- npx tmct init --memory-backend sqlite # npm run init:sqlite
486
+ npx tmct init # npm run init:sqlite — sqlite is already the default backend
473
487
  npx tmct init --force --with-persona human # npm run init:persona:human — the default persona, made explicit
474
488
  npx tmct init --force --with-persona empty # npm run init:persona:empty — no seeded vocabulary at all
475
489
  ```
@@ -577,16 +591,23 @@ Usage:
577
591
  the final answer (teach state first, trigger last)
578
592
  [--render blocks] with --prompt: when the final turn produced a plan,
579
593
  write it as a self-contained animated page
580
- [--output <path>] the rendered page's path (default plan.html)
594
+ [--render spider-fly|adventure|sprites] write that demo view as one self-contained
595
+ page (no --prompt; the game pages inline their engine)
596
+ [--output <path>] the rendered page's path (default plan.html for
597
+ blocks, <archetype>.html for the views)
581
598
  [--narrate] start with narrate mode on — a verbose, developer-facing
582
599
  trace of decision points/matched pattern/results/goal per
583
600
  turn, appended under a "--- narrate ---" marker (also
584
601
  TMCT_NARRATE=1; toggle mid-session with /narrate on|off)
602
+ [--live-wikipedia] start with the live Wikipedia supplement on — a question
603
+ nothing local can answer also tries en.wikipedia.org,
604
+ cited (network; also TMCT_LIVE_WIKIPEDIA=1 or tmct.toml
605
+ corpus tier3; toggle mid-session with /wiki on|off)
585
606
  [--plain] force the plain readline shell (the default when
586
607
  stdin/stdout is not a terminal)
587
608
  [--memory-backend <default|memory|sqlite>] storage backend for taught facts this
588
609
  session (CLI flag > TMCT_MEMORY_BACKEND env > tmct.toml's
589
- [memory] backend > "default", the flat .tmct/ JSON file)
610
+ [memory] backend > sqlite, .tmct/memory/graph.sqlite)
590
611
  ```
591
612
 
592
613
  `tmct memory` is the CLI-side view of the same data the `/memory` chat command shows:
@@ -742,14 +763,15 @@ TMCT_GRAPH_FILE env > tmct.toml graph_file/graph_files > --repo-derived
742
763
  "repo_path" fills the --repo tier when the flag is absent.
743
764
 
744
765
  Memory-backend precedence (chat; see src/services/chat.mjs createSession): --memory-backend
745
- flag > TMCT_MEMORY_BACKEND env > tmct.toml [memory] backend > "default" (the flat
746
- .tmct/ JSON file). Set it once with `tmct init --memory-backend <...>` and every
747
- later `tmct chat` in that repo picks it up with no flag needed.
766
+ flag > TMCT_MEMORY_BACKEND env > tmct.toml [memory] backend > sqlite (the built-in
767
+ default, .tmct/memory/graph.sqlite); "memory" keeps the store in-process only. Set it
768
+ once with `tmct init --memory-backend <...>` and every later `tmct chat` in that
769
+ repo picks it up with no flag needed.
748
770
  ```
749
771
 
750
772
  `npm run init:large` in `package.json` chains one `init` and five `import --corpus`
751
773
  calls to combine every shipped bundle (human persona + seon + conceptnet +
752
- aws/python/java) into ~7,380 facts on the default flat-JSON backend, a working
774
+ aws/python/java) into ~7,380 facts on the default sqlite backend, a working
753
775
  example to copy from. `init:xl` starts from the large persona tier and adds
754
776
  the wordnet-xl corpus (~72,000 facts); `init:xxl` swaps wordnet-xl for the
755
777
  full WordNet slice plus namenet (~239,000 facts, the biggest committed
package/bin/tmct.mjs CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2
2
  // tmct — The Mechanical Code Talker. The headline entry is CHAT: a bare
3
3
  // invocation drops you into a tolerant, offline, $0 prompt that guides you
4
4
  // toward precision queries about a repository (ELIZA/PARRY-style, but obsessed
@@ -34,6 +34,18 @@
34
34
  // static import here costs `tmct --help` nothing.
35
35
  import { renderUsage, unknownInvocationMessage } from "../src/domain/cli-verbs.mjs";
36
36
 
37
+ // The default memory backend rides node:sqlite, which Node still ships behind
38
+ // an ExperimentalWarning. The shebang and the package.json scripts both pass
39
+ // --disable-warning=ExperimentalWarning, but a direct `node bin/tmct.mjs`
40
+ // (spawnSync, some CI shells) bypasses both — so replace the default warning
41
+ // printer with one that swallows exactly that warning and re-prints everything
42
+ // else in Node's own format.
43
+ process.removeAllListeners("warning");
44
+ process.on("warning", (warning) => {
45
+ if (warning?.name === "ExperimentalWarning" && /sqlite/i.test(String(warning?.message || ""))) return;
46
+ process.stderr.write(`(node:${process.pid}) ${warning?.name || "Warning"}: ${warning?.message || warning}\n`);
47
+ });
48
+
37
49
  const HELP = `tmct — The Mechanical Code Talker
38
50
 
39
51
  A tolerant, offline, $0 chat that guides you toward precision queries about a
@@ -51,9 +63,10 @@ TMCT_GRAPH_FILE env > tmct.toml graph_file/graph_files > --repo-derived
51
63
  "repo_path" fills the --repo tier when the flag is absent.
52
64
 
53
65
  Memory-backend precedence (chat; see src/services/chat.mjs createSession): --memory-backend
54
- flag > TMCT_MEMORY_BACKEND env > tmct.toml [memory] backend > "default" (the flat
55
- .tmct/ JSON file). Set it once with \`tmct init --memory-backend <...>\` and every
56
- later \`tmct chat\` in that repo picks it up with no flag needed.
66
+ flag > TMCT_MEMORY_BACKEND env > tmct.toml [memory] backend > sqlite (the built-in
67
+ default, .tmct/memory/graph.sqlite); "memory" keeps the store in-process only. Set it
68
+ once with \`tmct init --memory-backend <...>\` and every later \`tmct chat\` in that
69
+ repo picks it up with no flag needed.
57
70
  `;
58
71
 
59
72
  const argv = process.argv.slice(2);
@@ -503,6 +516,86 @@ async function appendGraphFiles(repoRoot, graphPaths) {
503
516
  await writeConfig(repoRoot, cfg);
504
517
  }
505
518
 
519
+ /** Build one game page's browser engine bundle into a throwaway dir and
520
+ * return its text for inlining. The bundle builders live in scripts/ and
521
+ * need esbuild — a git checkout with dev dependencies, not the npm package —
522
+ * so an unbuildable engine is a clear refusal here, never a written page
523
+ * that hangs at "loading the engine". */
524
+ async function buildEngineBundleJs(builderFile) {
525
+ const { mkdtemp, readFile, rm } = await import("node:fs/promises");
526
+ const { tmpdir } = await import("node:os");
527
+ const { join } = await import("node:path");
528
+ let build;
529
+ try {
530
+ ({ main: build } = await import(new URL(`../scripts/${builderFile}`, import.meta.url).href));
531
+ } catch {
532
+ throw new Error(
533
+ `this render inlines the page's engine bundle, and its builder (scripts/${builderFile}) `
534
+ + "isn't available here — run it from a git checkout with dev dependencies installed (npm install)",
535
+ );
536
+ }
537
+ const dir = await mkdtemp(join(tmpdir(), "tmct-render-"));
538
+ try {
539
+ const { outPath } = await build(dir);
540
+ return await readFile(outPath, "utf8");
541
+ } finally {
542
+ await rm(dir, { recursive: true, force: true });
543
+ }
544
+ }
545
+
546
+ /** `--render spider-fly|adventure|sprites [--output <path>]` — write that demo
547
+ * view as ONE self-contained file: build-time data read here in Node (the
548
+ * same reads scripts/build-demo-site.mjs does for the deployed site), and,
549
+ * for the two game pages, the engine bundle built fresh and inlined instead
550
+ * of linked as a sibling — so the written file opens from file:// alone. */
551
+ async function writeStandaloneViewPage(archetype, rest) {
552
+ const { strFlag } = await import("../src/services/cli-args.mjs");
553
+ const { writeFile } = await import("node:fs/promises");
554
+ const { resolve: resolvePath } = await import("node:path");
555
+ const outPath = resolvePath(process.cwd(), strFlag(rest, ["--output", "--out"], `${archetype}.html`));
556
+ let html;
557
+ if (archetype === "sprites") {
558
+ const { readSpriteTemplateFiles } = await import("../src/adapters/corpus/sprite-template-files.mjs");
559
+ const { readSpriteLargeTemplateFiles } = await import("../src/adapters/corpus/sprite-large-template-files.mjs");
560
+ const { loadSpriteOntologyFactRows, renderSpriteCatalogHtml } = await import("../src/services/sprite-catalog-viz.mjs");
561
+ html = renderSpriteCatalogHtml({
562
+ iconTemplates: readSpriteTemplateFiles(),
563
+ largeTemplates: readSpriteLargeTemplateFiles(),
564
+ factRows: await loadSpriteOntologyFactRows(),
565
+ });
566
+ } else if (archetype === "spider-fly") {
567
+ const engineBundleJs = await buildEngineBundleJs("build-spider-fly-bundle.mjs");
568
+ // The large sprite tier, same as the site build: the only tier the
569
+ // emotion-carrying spider/fly templates exist in. Missing (e.g. outside
570
+ // this checkout) reads as [], and every agent falls back to the flat
571
+ // built-in registry.
572
+ const { readSpriteLargeTemplateFiles } = await import("../src/adapters/corpus/sprite-large-template-files.mjs");
573
+ const { renderSpiderFlyHtml } = await import("../src/services/spider-fly-viz.mjs");
574
+ html = renderSpiderFlyHtml({ spriteTemplates: readSpriteLargeTemplateFiles(), engineBundleJs });
575
+ } else {
576
+ const engineBundleJs = await buildEngineBundleJs("build-adventure-bundle.mjs");
577
+ const { getWorldsPackProvider } = await import("../src/adapters/corpus/worlds-pack.mjs");
578
+ const world = await getWorldsPackProvider().load("ashcombe-hall");
579
+ if (!world) {
580
+ throw new Error("the ashcombe-hall world isn't in the worlds pack (corpus/worlds/) — run `npm run gen:worlds-pack` first");
581
+ }
582
+ const { readSpriteTemplateFiles } = await import("../src/adapters/corpus/sprite-template-files.mjs");
583
+ const { renderAdventureHtml } = await import("../src/services/adventure-viz.mjs");
584
+ html = renderAdventureHtml({
585
+ worldPayload: {
586
+ name: world.name,
587
+ facts: world.facts.map((f) => ({ subject: f.subject, predicate: f.predicate, object: f.object })),
588
+ rules: world.rules.map((r) => ({ name: r.name, ruleKind: r.ruleKind, slots: r.slots })),
589
+ opening: world.meta?.opening || "",
590
+ },
591
+ spriteTemplates: readSpriteTemplateFiles(),
592
+ engineBundleJs,
593
+ });
594
+ }
595
+ await writeFile(outPath, html, "utf8");
596
+ process.stdout.write(`wrote ${outPath} (${(Buffer.byteLength(html, "utf8") / 1024).toFixed(0)} KB, self-contained)\n`);
597
+ }
598
+
506
599
  async function main() {
507
600
  const mode = process.argv[2];
508
601
 
@@ -520,6 +613,11 @@ async function main() {
520
613
  // session with the verbose developer/debug narrate mode already on. Default
521
614
  // OFF; `/narrate on`/`/narrate off` also toggles it mid-session.
522
615
  const narrate = rest.includes("--narrate");
616
+ // `--live-wikipedia` (or TMCT_LIVE_WIKIPEDIA=1 / tmct.toml corpus tier3,
617
+ // both read inside createSession): start with the live Wikipedia
618
+ // supplement on. Default OFF; `/wiki on`/`/wiki off` toggles it
619
+ // mid-session.
620
+ const liveReference = rest.includes("--live-wikipedia");
523
621
  // `--graph <path>` (repeatable) / `--config <path>` (src/services/cli-args.mjs): threaded
524
622
  // through as the new top tier of createSession's graph-resolution order (above
525
623
  // TMCT_GRAPH_FILE — see chat.mjs's own docblock at createSession). Omitted from
@@ -549,17 +647,23 @@ async function main() {
549
647
  // FINAL turn's answer prints to stdout, and the process exits. Piped stdin
550
648
  // stays the interactive fallback.
551
649
  const prompt = strFlag(rest, ["--prompt"]);
552
- // `--render <archetype> [--output <path>]` — after a one-shot --prompt whose
553
- // final turn produced a plan, write the self-contained animated plan page
554
- // (src/services/plan-viz.mjs). Requires --prompt: interactive chat has no single
555
- // final turn to render.
650
+ // `--render <archetype> [--output <path>]` — write one standalone page.
651
+ // "blocks" is the animated plan replay (src/services/plan-viz.mjs) and
652
+ // requires --prompt: interactive chat has no single final turn to render.
653
+ // The demo views (spider-fly / adventure / sprites) take no prompt — their
654
+ // data is read at render time, and the two game pages inline a freshly
655
+ // built engine bundle so the file opens from file:// with no siblings.
556
656
  let renderArchetype;
557
657
  try {
558
- renderArchetype = enumFlag(rest, ["--render"], ["blocks"]);
658
+ renderArchetype = enumFlag(rest, ["--render"], ["blocks", "spider-fly", "adventure", "sprites"]);
559
659
  } catch (e) {
560
660
  process.stderr.write(`tmct: ${e?.message || e}\n`);
561
661
  process.exit(2);
562
662
  }
663
+ if (renderArchetype && renderArchetype !== "blocks") {
664
+ await writeStandaloneViewPage(renderArchetype, rest);
665
+ return;
666
+ }
563
667
  if (renderArchetype && !prompt) {
564
668
  process.stderr.write("tmct: --render needs --prompt (a one-shot turn whose plan it renders)\n");
565
669
  process.exit(1);
@@ -567,7 +671,7 @@ async function main() {
567
671
  if (prompt) {
568
672
  const { createSession } = await import("../src/services/chat.mjs");
569
673
  const { splitSentences } = await import("../src/services/sentences.mjs");
570
- const session = await createSession({ repoPath, ephemeral, narrate, ...extra });
674
+ const session = await createSession({ repoPath, ephemeral, narrate, liveReference, ...extra });
571
675
  let finalAnswer = "";
572
676
  let finalPlan = null;
573
677
  let parts;
@@ -609,10 +713,10 @@ async function main() {
609
713
  const plain = rest.includes("--plain") || !process.stdin.isTTY || !process.stdout.isTTY;
610
714
  if (plain) {
611
715
  const { runChat } = await import("../src/services/chat.mjs");
612
- await runChat({ repoPath, ephemeral, narrate, ...extra });
716
+ await runChat({ repoPath, ephemeral, narrate, liveReference, ...extra });
613
717
  } else {
614
718
  const { runTui } = await import("../src/surfaces/tui/app.mjs");
615
- await runTui({ repoPath, ephemeral, narrate, ...extra });
719
+ await runTui({ repoPath, ephemeral, narrate, liveReference, ...extra });
616
720
  }
617
721
  return;
618
722
  }
@@ -626,9 +730,24 @@ async function main() {
626
730
  const rest = process.argv.slice(3);
627
731
  const verbose = rest.includes("--verbose") || rest.includes("-v");
628
732
  const { resolveRuntimeConfig } = await import("../src/services/cli-args.mjs");
629
- const { inspectMemory } = await import("../src/adapters/memory/inspect.mjs");
630
- const { repo } = await resolveRuntimeConfig({ argv: rest });
631
- process.stdout.write(await inspectMemory(repo, { verbose }) + "\n");
733
+ const { renderMemory } = await import("../src/adapters/memory/inspect.mjs");
734
+ const { loadMemory, openMemoryBackend } = await import("../src/adapters/memory/core.mjs");
735
+ const { loadBlockIndex } = await import("../src/adapters/memory/blocks.mjs");
736
+ const { repo, toml } = await resolveRuntimeConfig({ argv: rest });
737
+ // Same backend resolution as chat's createSession, minus the (nonexistent
738
+ // here) CLI-flag tier: env > tmct.toml > the sqlite default — so this verb
739
+ // inspects the store a chat session in this repo actually writes.
740
+ const backendChoice = String(process.env.TMCT_MEMORY_BACKEND || toml?.memory?.backend || "").trim().toLowerCase();
741
+ const { dir: memoryDir, close: closeMemoryStore } = await openMemoryBackend(repo, backendChoice);
742
+ try {
743
+ const memory = await loadMemory(memoryDir);
744
+ // The folded-block index is file-backed beside the session logs, not part
745
+ // of the memory store — read it off the repo path directly.
746
+ const blocks = await loadBlockIndex(repo);
747
+ process.stdout.write(renderMemory({ memory, blocks }, { verbose }) + "\n");
748
+ } finally {
749
+ await closeMemoryStore();
750
+ }
632
751
  return;
633
752
  }
634
753
 
@@ -1023,14 +1142,23 @@ async function main() {
1023
1142
  };
1024
1143
  const { resolveRuntimeConfig } = await import("../src/services/cli-args.mjs");
1025
1144
  const { syllogise, DEFAULT_MAX_ENVIRONMENTS } = await import("../src/domain/syllogise.mjs");
1026
- const { loadMemory, readFactRows, appendFacts, loadSyllogiseState, saveSyllogiseState } = await import("../src/adapters/memory/core.mjs");
1027
- const { repo } = await resolveRuntimeConfig({ argv: rest });
1028
- const res = await syllogise(repo, {
1029
- depth: numFlag("--depth", 32), budget: numFlag("--budget", 50),
1030
- maxEnvironments: numFlag("--max-environments", DEFAULT_MAX_ENVIRONMENTS),
1031
- full: rest.includes("--full"),
1032
- store: { loadMemory, readFactRows, appendFacts, loadSyllogiseState, saveSyllogiseState },
1033
- });
1145
+ const { loadMemory, readFactRows, appendFacts, loadSyllogiseState, saveSyllogiseState, openMemoryBackend } = await import("../src/adapters/memory/core.mjs");
1146
+ const { repo, toml } = await resolveRuntimeConfig({ argv: rest });
1147
+ // Same env > tmct.toml > default backend resolution as `tmct memory` — the
1148
+ // entailed facts must land in the store chat reads back.
1149
+ const backendChoice = String(process.env.TMCT_MEMORY_BACKEND || toml?.memory?.backend || "").trim().toLowerCase();
1150
+ const { dir: memoryDir, close: closeMemoryStore } = await openMemoryBackend(repo, backendChoice);
1151
+ let res;
1152
+ try {
1153
+ res = await syllogise(memoryDir, {
1154
+ depth: numFlag("--depth", 32), budget: numFlag("--budget", 50),
1155
+ maxEnvironments: numFlag("--max-environments", DEFAULT_MAX_ENVIRONMENTS),
1156
+ full: rest.includes("--full"),
1157
+ store: { loadMemory, readFactRows, appendFacts, loadSyllogiseState, saveSyllogiseState },
1158
+ });
1159
+ } finally {
1160
+ await closeMemoryStore();
1161
+ }
1034
1162
  process.stdout.write(
1035
1163
  `tmct syllogise — derived ${res.count} entailed fact(s) (mode ${res.mode}${res.mode === "delta" ? `, Δ${res.deltaSize}` : ""}, depth ${res.depth}, budget ${res.budget})`
1036
1164
  + (res.environmentsAdded > 0 ? `, ${res.environmentsAdded} alternate environment(s) recorded` : "")
@@ -1074,12 +1202,22 @@ async function main() {
1074
1202
  const focus = strFlag(rest, ["--focus"]);
1075
1203
  const term = strFlag(rest, ["--term"]); // seeds via normFactTerm; --focus wins when both are given
1076
1204
  const outPath = resolve(process.cwd(), strFlag(rest, ["--output", "--out"], "ledger.html"));
1077
- const { repo } = await resolveRuntimeConfig({ argv: rest });
1078
- const data = await computeLedgerData(repo, {
1079
- ...(focus ? { focus } : {}),
1080
- ...(!focus && term ? { term } : {}),
1081
- ...(rowLimit != null ? { rowLimit } : {}),
1082
- });
1205
+ const { repo, toml } = await resolveRuntimeConfig({ argv: rest });
1206
+ // Same env > tmct.toml > default backend resolution as `tmct memory` — the
1207
+ // ledger must render the store chat actually wrote.
1208
+ const { openMemoryBackend } = await import("../src/adapters/memory/core.mjs");
1209
+ const backendChoice = String(process.env.TMCT_MEMORY_BACKEND || toml?.memory?.backend || "").trim().toLowerCase();
1210
+ const { dir: memoryDir, close: closeMemoryStore } = await openMemoryBackend(repo, backendChoice);
1211
+ let data;
1212
+ try {
1213
+ data = await computeLedgerData(memoryDir, {
1214
+ ...(focus ? { focus } : {}),
1215
+ ...(!focus && term ? { term } : {}),
1216
+ ...(rowLimit != null ? { rowLimit } : {}),
1217
+ });
1218
+ } finally {
1219
+ await closeMemoryStore();
1220
+ }
1083
1221
  // readMemoryAskBundle never throws; an empty string renders the page with
1084
1222
  // an honest "chat unavailable" note instead of the dock (e.g. a fresh
1085
1223
  // checkout before the bundle's first build).
@@ -1198,12 +1336,18 @@ async function main() {
1198
1336
  process.stderr.write("tmct plan: needs a request, e.g. `tmct plan \"of the modules impacted by X, which are untested\"`\n");
1199
1337
  process.exit(2);
1200
1338
  }
1201
- const { repo, config } = await resolveRuntimeConfig({ argv: rest });
1339
+ const { repo, config, toml } = await resolveRuntimeConfig({ argv: rest });
1202
1340
  const { buildCapabilityPlanCtx, runCapabilityPlan, declaredCapabilityNames } = await import("../src/domain/router/drive.mjs");
1203
1341
  const { capabilityPlanDeps } = await import("../src/services/chat.mjs");
1342
+ // Same env > tmct.toml > default backend resolution as `tmct memory` — the
1343
+ // taught world/rule records the planner registers live in the store chat
1344
+ // wrote them to.
1345
+ const { openMemoryBackend } = await import("../src/adapters/memory/core.mjs");
1346
+ const backendChoice = String(process.env.TMCT_MEMORY_BACKEND || toml?.memory?.backend || "").trim().toLowerCase();
1347
+ const { dir: memoryDir, close: closeMemoryStore } = await openMemoryBackend(repo, backendChoice);
1204
1348
  let ctx;
1205
1349
  try {
1206
- ctx = await buildCapabilityPlanCtx({ ...capabilityPlanDeps(), config, memoryDir: repo });
1350
+ ctx = await buildCapabilityPlanCtx({ ...capabilityPlanDeps(), config, memoryDir });
1207
1351
  } catch (e) {
1208
1352
  process.stderr.write(`tmct plan: could not load the graph — ${e?.message || e}\n`);
1209
1353
  process.exit(1);
@@ -1256,6 +1400,7 @@ async function main() {
1256
1400
  }
1257
1401
  } finally {
1258
1402
  for (const dispose of ctx.disposers || []) dispose();
1403
+ await closeMemoryStore();
1259
1404
  }
1260
1405
  return;
1261
1406
  }
@@ -135,6 +135,13 @@
135
135
  "shareAlike": false,
136
136
  "notice": "LICENSE"
137
137
  },
138
+ {
139
+ "path": "corpus/sprites/",
140
+ "upstream": "first-party (mechanically generated by scripts/gen-sprite-facts.mjs from this repository's own hand-authored sprite templates, data/sprites/ and data/sprites-large/)",
141
+ "license": "MPL-2.0",
142
+ "shareAlike": false,
143
+ "notice": "LICENSE"
144
+ },
138
145
  {
139
146
  "path": "corpus/generated/ace-surface-variants.jsonl",
140
147
  "upstream": "mechanically generated from Open English WordNet / SemCor example sentences (CC-BY-4.0) and corpus/prose/, which includes CC-BY-SA-4.0 Wikipedia text",