@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
@@ -3,7 +3,7 @@
3
3
  "generated": "by scripts/build-worlds-pack.mjs from corpus/worlds/src/",
4
4
  "counts": {
5
5
  "worlds": 2,
6
- "facts": 528,
6
+ "facts": 548,
7
7
  "rules": 19
8
8
  },
9
9
  "budgets": {
@@ -14,13 +14,13 @@
14
14
  "sources": [
15
15
  {
16
16
  "file": "src/ashcombe-hall.jsonl",
17
- "bytes": 8458,
18
- "sha256": "79332b0700a8c8289ca4d8b5a9cf3dcff4c869fb1b45a72dd291617f61bdd184"
17
+ "bytes": 8795,
18
+ "sha256": "771ed4e7a5e9046235e8dc193b2696f83e6ef92a556ca1ca126dd070927912ce"
19
19
  },
20
20
  {
21
21
  "file": "src/spider-fly.jsonl",
22
- "bytes": 51706,
23
- "sha256": "b641936dec8fb3526dcab766268c25942c0e82ff9025bb853dad5f971bf8c029"
22
+ "bytes": 53693,
23
+ "sha256": "879c45652d0636c27f937e809e9883d0039b20fdc11032ac2040894d08c6a0a4"
24
24
  }
25
25
  ],
26
26
  "files": [
@@ -31,13 +31,13 @@
31
31
  },
32
32
  {
33
33
  "file": "shards/ashcombe-hall.jsonl.gz",
34
- "bytes": 963,
35
- "sha256": "d44e229d8f30dc43e8801443d426c315b43a6758adb11ad332c84e7108268746"
34
+ "bytes": 1020,
35
+ "sha256": "1255ea52149c3941fafd4ee462239bae8e10175c8e7db2e54c2c2dc1a2e0d924"
36
36
  },
37
37
  {
38
38
  "file": "shards/spider-fly.jsonl.gz",
39
- "bytes": 2829,
40
- "sha256": "dd9f1ac66b26da2cea350fdeab17e1fe0f27d583823356e3bc189c131e3adb86"
39
+ "bytes": 3156,
40
+ "sha256": "3e55614801471186b06a9514d5534ac14050663ec00e27dc0618eb97319d516f"
41
41
  }
42
42
  ]
43
43
  }
@@ -51,6 +51,9 @@
51
51
  {"world":"ashcombe-hall","kind":"fact","subject":"cook","predicate":"mgx:works-in","object":"kitchen"}
52
52
  {"world":"ashcombe-hall","kind":"fact","subject":"butler","predicate":"mgx:works-in","object":"drawing-room"}
53
53
  {"world":"ashcombe-hall","kind":"fact","subject":"gardener","predicate":"mgx:works-in","object":"garden"}
54
+ {"world":"ashcombe-hall","kind":"fact","subject":"ashcombe hall","predicate":"mgx:hasA","object":"6 rooms"}
55
+ {"world":"ashcombe-hall","kind":"fact","subject":"player","predicate":"mgx:hasA","object":"satchel"}
56
+ {"world":"ashcombe-hall","kind":"fact","subject":"satchel","predicate":"mgx:usedFor","object":"carrying what the player takes"}
54
57
  {"world":"ashcombe-hall","kind":"rule","name":"go","ruleKind":"action-signature","slots":{"subjectClass":"adventurer","targetClass":"room"}}
55
58
  {"world":"ashcombe-hall","kind":"rule","name":"go","ruleKind":"action-signature","slots":{"subjectClass":"person","targetClass":"room"}}
56
59
  {"world":"ashcombe-hall","kind":"rule","name":"go","ruleKind":"action-effect","slots":{"predicate":"currently-in","subjectRole":"subject","objectRole":"target"}}
@@ -475,5 +475,22 @@
475
475
  {"world":"spider-fly","kind":"fact","subject":"arachnid","predicate":"rdfs:subClassOf","object":"animal"}
476
476
  {"world":"spider-fly","kind":"fact","subject":"fly","predicate":"rdfs:subClassOf","object":"insect"}
477
477
  {"world":"spider-fly","kind":"fact","subject":"insect","predicate":"rdfs:subClassOf","object":"animal"}
478
+ {"world":"spider-fly","kind":"fact","subject":"board","predicate":"rdf:type","object":"grid"}
479
+ {"world":"spider-fly","kind":"fact","subject":"board","predicate":"mgx:hasA","object":"10 rows"}
480
+ {"world":"spider-fly","kind":"fact","subject":"board","predicate":"mgx:hasA","object":"10 columns"}
481
+ {"world":"spider-fly","kind":"fact","subject":"board","predicate":"mgx:hasA","object":"100 cells"}
482
+ {"world":"spider-fly","kind":"fact","subject":"web","predicate":"mgx:cover","object":"9 cells around cell-2-2"}
483
+ {"world":"spider-fly","kind":"fact","subject":"web","predicate":"mgx:last-for","object":"10 turns by default when a spider builds one"}
484
+ {"world":"spider-fly","kind":"fact","subject":"spider","predicate":"mgx:hasA","object":"vision radius of 4 cells by default"}
485
+ {"world":"spider-fly","kind":"fact","subject":"spider","predicate":"mgx:start-with","object":"mass 15 by default"}
486
+ {"world":"spider-fly","kind":"fact","subject":"spider","predicate":"mgx:lose","object":"0.5 mass per turn by default"}
487
+ {"world":"spider-fly","kind":"fact","subject":"spider","predicate":"mgx:lay","object":"one egg at mass 25 by default"}
488
+ {"world":"spider-fly","kind":"fact","subject":"fly","predicate":"mgx:hasA","object":"vision radius of 4 cells by default"}
489
+ {"world":"spider-fly","kind":"fact","subject":"fly","predicate":"mgx:start-with","object":"mass 10 by default"}
490
+ {"world":"spider-fly","kind":"fact","subject":"fly","predicate":"mgx:lose","object":"1 mass per turn by default"}
491
+ {"world":"spider-fly","kind":"fact","subject":"fly","predicate":"mgx:arrive","object":"every 3 turns at the edge of the board by default"}
492
+ {"world":"spider-fly","kind":"fact","subject":"egg","predicate":"mgx:hatch-after","object":"3 turns by default"}
493
+ {"world":"spider-fly","kind":"fact","subject":"egg","predicate":"mgx:hatch-into","object":"2 spiders by default"}
494
+ {"world":"spider-fly","kind":"fact","subject":"vision radius","predicate":"mgx:hasProperty","object":"4 cells for both the spider and the fly by default"}
478
495
  {"world":"spider-fly","kind":"rule","name":"go","ruleKind":"action-signature","slots":{"subjectClass":"spider","targetClass":"cell"}}
479
496
  {"world":"spider-fly","kind":"rule","name":"go","ruleKind":"action-effect","slots":{"predicate":"currently-in","subjectRole":"subject","objectRole":"target"}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "2.9.0",
3
+ "version": "2.9.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
@@ -81,11 +81,11 @@
81
81
  "wink-nlp": "^2.4.0"
82
82
  },
83
83
  "scripts": {
84
- "help": "node bin/tmct.mjs --help",
85
- "test": "node --test --test-concurrency=8 \"test/**/*.test.mjs\"",
86
- "test:smoke": "node --test test/smoke/*.test.mjs",
87
- "test:fast": "node --test --test-concurrency=8 test/smoke/*.test.mjs test/fast/*.test.mjs test/tools/ask.test.mjs test/tools/server.test.mjs test/estate/import-layers.test.mjs",
88
- "test:e2e": "node --test \"e2e/**/*.test.mjs\"",
84
+ "help": "node --disable-warning=ExperimentalWarning bin/tmct.mjs --help",
85
+ "test": "node --disable-warning=ExperimentalWarning --test --test-concurrency=8 \"test/**/*.test.mjs\"",
86
+ "test:smoke": "node --disable-warning=ExperimentalWarning --test test/smoke/*.test.mjs",
87
+ "test:fast": "node --disable-warning=ExperimentalWarning --test --test-concurrency=8 test/smoke/*.test.mjs test/fast/*.test.mjs test/tools/ask.test.mjs test/tools/server.test.mjs test/estate/import-layers.test.mjs",
88
+ "test:e2e": "node --disable-warning=ExperimentalWarning --test \"e2e/**/*.test.mjs\"",
89
89
  "e2e:browsers": "playwright install chromium",
90
90
  "check:links": "node scripts/check-links.mjs",
91
91
  "check:pii": "node scripts/pii-lint.mjs",
@@ -97,32 +97,32 @@
97
97
  "check:publish": "node scripts/check-publish.mjs",
98
98
  "check:all": "npm run check:links && npm run check:pii && npm run check:licences && npm run check:publint && npm run check:tool-docs && npm run check:budgets",
99
99
  "smoke:deploy": "node scripts/post-deploy-smoke.mjs",
100
- "chat": "node bin/tmct.mjs",
101
- "chat:repo": "node bin/tmct.mjs chat --repo",
102
- "chat:plain": "node bin/tmct.mjs chat --plain",
103
- "chat:narrate": "node bin/tmct.mjs chat --narrate",
104
- "init": "node bin/tmct.mjs init",
105
- "init:sqlite": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --memory-backend sqlite",
106
- "init:persona:human": "node bin/tmct.mjs init --with-persona human",
107
- "init:persona:empty": "node bin/tmct.mjs init --with-persona empty",
108
- "init:large": "node bin/tmct.mjs init && node bin/tmct.mjs import --corpus seon && node bin/tmct.mjs import --corpus conceptnet && node bin/tmct.mjs import --corpus aws && node bin/tmct.mjs import --corpus python && node bin/tmct.mjs import --corpus java",
109
- "init:xl": "node bin/tmct.mjs init --persona-size large && node bin/tmct.mjs import --corpus seon && node bin/tmct.mjs import --corpus conceptnet && node bin/tmct.mjs import --corpus aws && node bin/tmct.mjs import --corpus python && node bin/tmct.mjs import --corpus java && node bin/tmct.mjs import --corpus wordnet-xl",
110
- "init:xxl": "node bin/tmct.mjs init --persona-size large && node bin/tmct.mjs import --corpus seon && node bin/tmct.mjs import --corpus conceptnet && node bin/tmct.mjs import --corpus aws && node bin/tmct.mjs import --corpus python && node bin/tmct.mjs import --corpus java && node bin/tmct.mjs import --corpus wordnet-full && node bin/tmct.mjs import --corpus namenet",
111
- "memory": "node bin/tmct.mjs memory",
112
- "memory:verbose": "node bin/tmct.mjs memory --verbose",
113
- "syllogise": "node bin/tmct.mjs syllogise",
114
- "viz": "node bin/tmct.mjs viz",
115
- "viz:term": "node bin/tmct.mjs viz --term",
116
- "plan": "node bin/tmct.mjs plan",
117
- "plan:json": "node bin/tmct.mjs plan --json",
118
- "cli": "node bin/tmct.mjs cli",
119
- "cli:digest": "node bin/tmct.mjs cli digest",
120
- "import": "node bin/tmct.mjs import",
121
- "extend": "node bin/tmct.mjs extend --validate",
122
- "serve": "node bin/tmct.mjs serve",
123
- "serve:public": "node bin/tmct.mjs serve --host 0.0.0.0 --port 8787",
124
- "example:mini": "node bin/tmct.mjs chat --repo examples/mini-webapp --ephemeral",
125
- "example:polyglot": "node bin/tmct.mjs chat --repo examples/polyglot --ephemeral",
100
+ "chat": "node --disable-warning=ExperimentalWarning bin/tmct.mjs",
101
+ "chat:repo": "node --disable-warning=ExperimentalWarning bin/tmct.mjs chat --repo",
102
+ "chat:plain": "node --disable-warning=ExperimentalWarning bin/tmct.mjs chat --plain",
103
+ "chat:narrate": "node --disable-warning=ExperimentalWarning bin/tmct.mjs chat --narrate",
104
+ "init": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init",
105
+ "init:sqlite": "npm run init",
106
+ "init:persona:human": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --with-persona human",
107
+ "init:persona:empty": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --with-persona empty",
108
+ "init:large": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus seon && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus conceptnet && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus aws && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus python && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus java",
109
+ "init:xl": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --persona-size large && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus seon && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus conceptnet && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus aws && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus python && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus java && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus wordnet-xl",
110
+ "init:xxl": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --persona-size large && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus seon && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus conceptnet && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus aws && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus python && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus java && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus wordnet-full && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus namenet",
111
+ "memory": "node --disable-warning=ExperimentalWarning bin/tmct.mjs memory",
112
+ "memory:verbose": "node --disable-warning=ExperimentalWarning bin/tmct.mjs memory --verbose",
113
+ "syllogise": "node --disable-warning=ExperimentalWarning bin/tmct.mjs syllogise",
114
+ "viz": "node --disable-warning=ExperimentalWarning bin/tmct.mjs viz",
115
+ "viz:term": "node --disable-warning=ExperimentalWarning bin/tmct.mjs viz --term",
116
+ "plan": "node --disable-warning=ExperimentalWarning bin/tmct.mjs plan",
117
+ "plan:json": "node --disable-warning=ExperimentalWarning bin/tmct.mjs plan --json",
118
+ "cli": "node --disable-warning=ExperimentalWarning bin/tmct.mjs cli",
119
+ "cli:digest": "node --disable-warning=ExperimentalWarning bin/tmct.mjs cli digest",
120
+ "import": "node --disable-warning=ExperimentalWarning bin/tmct.mjs import",
121
+ "extend": "node --disable-warning=ExperimentalWarning bin/tmct.mjs extend --validate",
122
+ "serve": "node --disable-warning=ExperimentalWarning bin/tmct.mjs serve",
123
+ "serve:public": "node --disable-warning=ExperimentalWarning bin/tmct.mjs serve --host 0.0.0.0 --port 8787",
124
+ "example:mini": "node --disable-warning=ExperimentalWarning bin/tmct.mjs chat --repo examples/mini-webapp --ephemeral",
125
+ "example:polyglot": "node --disable-warning=ExperimentalWarning bin/tmct.mjs chat --repo examples/polyglot --ephemeral",
126
126
  "chatbench:run": "node chatbench/run.mjs",
127
127
  "chatbench:judge": "node chatbench/judge.mjs",
128
128
  "agentbench:run": "node agentbench/run.mjs",
@@ -139,11 +139,14 @@
139
139
  "build:spider-fly-bundle": "node scripts/build-spider-fly-bundle.mjs",
140
140
  "build:plan-bundle": "node scripts/build-plan-bundle.mjs",
141
141
  "build:ledger-bundle": "node scripts/build-ledger-bundle.mjs",
142
+ "build:sprites-bundle": "node scripts/build-sprites-bundle.mjs",
143
+ "build:wink-vendor": "node scripts/build-wink-vendor.mjs",
142
144
  "build:chat-seed": "node scripts/build-chat-seed.mjs",
143
145
  "build:demo-graph": "node scripts/build-demo-graph.mjs",
144
146
  "build:demo-pack": "node scripts/build-demo-pack.mjs",
145
147
  "build:demo-sprites-pack": "node scripts/build-demo-sprites-pack.mjs",
146
148
  "build:demo-memory": "node scripts/build-demo-memory.mjs",
149
+ "gen:sprite-facts": "node scripts/gen-sprite-facts.mjs",
147
150
  "gen:tool-docs": "node scripts/generate-tool-docs.mjs",
148
151
  "gen:collisions": "node scripts/generate-real-word-collisions.mjs",
149
152
  "gen:prose-corpus": "node scripts/fetch-prose-corpus.mjs",
@@ -152,8 +155,8 @@
152
155
  "measure:child-corpus": "node scripts/measure-child-corpus.mjs",
153
156
  "gen:variants": "node scripts/generate-template-variants.mjs",
154
157
  "gen:worlds-pack": "node scripts/build-worlds-pack.mjs",
155
- "extract": "node bin/tmct.mjs extract",
156
- "extract:facts": "node bin/tmct.mjs extract"
158
+ "extract": "node --disable-warning=ExperimentalWarning bin/tmct.mjs extract",
159
+ "extract:facts": "node --disable-warning=ExperimentalWarning bin/tmct.mjs extract"
157
160
  },
158
161
  "devDependencies": {
159
162
  "esbuild": "0.28.1",
@@ -0,0 +1,145 @@
1
+ // corpus/wikipedia-live.mjs — the OPT-IN live Wikipedia lookup behind the
2
+ // clean-miss gate: two small GET round trips against en.wikipedia.org (an
3
+ // opensearch title match, then the REST page summary), mapped into the same
4
+ // article-row shape the shipped reference pack uses. No node builtins — this
5
+ // module ships in the browser bundle unchanged; the only I/O is fetch.
6
+ //
7
+ // Courtesy is structural, not advisory: one in-flight lookup at a time, a
8
+ // minimum interval between network round trips, a 429 cool-off honouring
9
+ // Retry-After, an abort timeout on every fetch, and a cache that keeps hits
10
+ // AND misses so a term is never asked twice. Every failure of any kind reads
11
+ // as null — the caller's honest miss stands byte-identical.
12
+ //
13
+ // The provider seam mirrors reference-pack.mjs's: registerLiveReferenceProvider
14
+ // swaps the whole lookup behind one async { lookup(normTerm) } contract (tests
15
+ // and the demo page stub it); null restores the default provider below.
16
+
17
+ import { normFactTerm } from "../../domain/hash.mjs";
18
+ import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
19
+ import { isReferenceArticleRow, sentencesUpTo, isaOf, SUMMARY_CHAR_CAP } from "../../domain/reference-pack.mjs";
20
+
21
+ export const WIKIPEDIA_LIVE_ORIGIN = "https://en.wikipedia.org";
22
+
23
+ const DEFAULT_TIMEOUT_MS = 4000;
24
+ const DEFAULT_MIN_INTERVAL_MS = 2000;
25
+ const RETRY_AFTER_FLOOR_MS = 5000;
26
+
27
+ /**
28
+ * A live-lookup provider: { lookup(normTerm) -> article row | null }.
29
+ *
30
+ * `fetchImpl` defaults to the global fetch; `origin` to en.wikipedia.org;
31
+ * `lexicon` (optional) feeds the isa extraction. The row shape is the shipped
32
+ * pack's own ({ term, title, text, summary, url, revid, isa? }), validated by
33
+ * isReferenceArticleRow before it is ever returned.
34
+ */
35
+ export function createWikipediaLiveProvider({
36
+ fetchImpl,
37
+ origin = WIKIPEDIA_LIVE_ORIGIN,
38
+ timeoutMs = DEFAULT_TIMEOUT_MS,
39
+ minIntervalMs = DEFAULT_MIN_INTERVAL_MS,
40
+ lexicon = null,
41
+ } = {}) {
42
+ const doFetch = fetchImpl ?? ((...args) => globalThis.fetch(...args));
43
+ const cache = new Map(); // key -> row | null (hits AND settled misses)
44
+ let lastLookupAt = 0;
45
+ let coolOffUntil = 0;
46
+ let inFlight = false;
47
+
48
+ async function fetchJson(url) {
49
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
50
+ const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
51
+ try {
52
+ const res = await doFetch(url, controller ? { signal: controller.signal } : {});
53
+ if (res.status === 429) {
54
+ const retryAfterMs = Number(res.headers?.get?.("retry-after")) * 1000;
55
+ coolOffUntil = Date.now() + Math.max(retryAfterMs || 0, RETRY_AFTER_FLOOR_MS);
56
+ return null;
57
+ }
58
+ if (!res.ok) return null;
59
+ return await res.json();
60
+ } catch {
61
+ return null;
62
+ } finally {
63
+ if (timer !== null) clearTimeout(timer);
64
+ }
65
+ }
66
+
67
+ /** The opensearch title whose normFactTerm fold equals or extends the key —
68
+ * the topic-drift guard: "quasar" may resolve to "Quasar" or "Quasars",
69
+ * never to a first suggestion about something else. */
70
+ function matchingTitle(key, searchResult) {
71
+ const titles = Array.isArray(searchResult?.[1]) ? searchResult[1] : [];
72
+ for (const title of titles) {
73
+ const folded = normFactTerm(title);
74
+ if (folded === key || folded.startsWith(key)) return String(title);
75
+ }
76
+ return null;
77
+ }
78
+
79
+ async function roundTrips(key) {
80
+ const search = await fetchJson(
81
+ `${origin}/w/api.php?action=opensearch&format=json&origin=*&search=${encodeURIComponent(key)}&limit=3`,
82
+ );
83
+ const title = search ? matchingTitle(key, search) : null;
84
+ if (!title) return null;
85
+ const summary = await fetchJson(
86
+ `${origin}/api/rest_v1/page/summary/${encodeURIComponent(title.replace(/ /g, "_"))}`,
87
+ );
88
+ if (!summary) return null;
89
+ const extract = String(summary.extract ?? "");
90
+ const revid = Number(summary.revision);
91
+ const row = {
92
+ term: key,
93
+ title: String(summary.title ?? title),
94
+ text: extract,
95
+ summary: sentencesUpTo(extract, SUMMARY_CHAR_CAP),
96
+ url: String(summary.content_urls?.desktop?.page ?? `${origin}/wiki/${encodeURIComponent(title.replace(/ /g, "_"))}`),
97
+ revid,
98
+ };
99
+ let lex = lexicon;
100
+ if (!lex) { try { lex = loadLexicon(); } catch { lex = null; } }
101
+ const isa = lex ? isaOf(extract, lex) : null;
102
+ if (isa) row.isa = isa;
103
+ return isReferenceArticleRow(row) ? row : null;
104
+ }
105
+
106
+ return {
107
+ async lookup(normTerm) {
108
+ const key = String(normTerm ?? "");
109
+ if (!key) return null;
110
+ if (cache.has(key)) return cache.get(key);
111
+ const now = Date.now();
112
+ if (inFlight || now < coolOffUntil || now - lastLookupAt < minIntervalMs) return null;
113
+ lastLookupAt = now;
114
+ inFlight = true;
115
+ let row = null;
116
+ try {
117
+ row = await roundTrips(key);
118
+ } catch {
119
+ row = null;
120
+ } finally {
121
+ inFlight = false;
122
+ }
123
+ cache.set(key, row);
124
+ return row;
125
+ },
126
+ };
127
+ }
128
+
129
+ let defaultProvider = null;
130
+ let registeredProvider = null;
131
+
132
+ /** Swap the live lookup: provider = { lookup: async (normTerm) => row|null }.
133
+ * Pass null to restore the default en.wikipedia.org provider. */
134
+ export function registerLiveReferenceProvider(provider) {
135
+ registeredProvider = provider && typeof provider.lookup === "function" ? provider : null;
136
+ }
137
+
138
+ /** The active provider — the registered one, else one lazily created default
139
+ * provider shared by every caller (its cache and throttle are what make the
140
+ * lookup polite, so it must be a singleton). */
141
+ export function getLiveReferenceProvider() {
142
+ if (registeredProvider) return registeredProvider;
143
+ if (!defaultProvider) defaultProvider = createWikipediaLiveProvider();
144
+ return defaultProvider;
145
+ }
@@ -1,6 +1,8 @@
1
1
  // memory/core.mjs — tmct's OWN conversational memory graph: a dedicated
2
- // OWL-labelled store at <repo>/.tmct/memory/graph.json, distinct from any
3
- // provider-supplied code graph. Utterances, Facts (reified RDF triples via
2
+ // OWL-labelled store (routed default: the SQLite file at
3
+ // <repo>/.tmct/memory/graph.sqlite; "memory" keeps it in-process; the
4
+ // flat-JSON read/write path survives for callers holding a plain dir),
5
+ // distinct from any provider-supplied code graph. Utterances, Facts (reified RDF triples via
4
6
  // appendFact), and Sessions are all typed twice — payload `class` and an
5
7
  // `rdf:type` attribute. Every append is crash-safe and idempotent (utterance
6
8
  // ids are deterministic, fact ids hash the triple).
@@ -12,7 +14,7 @@
12
14
  // systems toward triple terms) but does not deprecate. See
13
15
  // docs/references/schemas/rdf-reification-and-rdf-star.md.
14
16
 
15
- import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
17
+ import { access, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
16
18
  import { dirname, join } from "node:path";
17
19
  import { proseTokensFor, buildProseIndex } from "../../domain/prose.mjs";
18
20
  import { fnv1aHex, normText, normFactTerm, normFactPredicate, factIdFor, factIdForTriple } from "../../domain/hash.mjs";
@@ -187,11 +189,29 @@ CREATE INDEX IF NOT EXISTS edges_by_prop ON edges(prop);
187
189
  // Edge keys with dedicated columns; any other key round-trips via `extra`.
188
190
  const STD_EDGE_KEYS = new Set(["subject", "object", "subjectLabel", "objectLabel"]);
189
191
 
192
+ // node:sqlite still emits an ExperimentalWarning on first import. Using it is
193
+ // this module's deliberate choice, so the warning is pure noise for every
194
+ // consumer (library embedders, examples, bare `node` scripts) — swallow that
195
+ // one warning here, once, and re-emit everything else untouched.
196
+ let sqliteWarningFilterInstalled = false;
197
+ function installSqliteWarningFilter() {
198
+ if (sqliteWarningFilterInstalled) return;
199
+ sqliteWarningFilterInstalled = true;
200
+ const prior = process.listeners("warning").slice();
201
+ process.removeAllListeners("warning");
202
+ process.on("warning", (warning) => {
203
+ if (warning?.name === "ExperimentalWarning" && /sqlite/i.test(String(warning?.message))) return;
204
+ if (prior.length) for (const l of prior) l(warning);
205
+ else console.error(warning.stack || String(warning));
206
+ });
207
+ }
208
+
190
209
  /** Open (creating if absent) a resident node:sqlite connection: a Backend C
191
210
  * handle `{ backend: "sqlite", db, dbPath }`. `node:sqlite` is imported
192
211
  * lazily — only opting into this backend ever loads it. Meant to be opened
193
212
  * once per session; close via closeSqliteMemoryStore at session end. */
194
213
  export async function createSqliteMemoryStore(dbPath) {
214
+ installSqliteWarningFilter();
195
215
  const { DatabaseSync } = await import("node:sqlite");
196
216
  const db = new DatabaseSync(dbPath);
197
217
  db.exec("PRAGMA journal_mode = WAL");
@@ -210,18 +230,37 @@ export function closeSqliteMemoryStore(handle) {
210
230
  /** Resolve a backend token ("memory" | "sqlite" | anything else) into
211
231
  * `{ dir, close }` — the ONE shared resolver, so every entry point (init's
212
232
  * corpus seed, bin/tmct.mjs, chat) picks the same backend for a repo rather
213
- * than silently splitting its memory across two stores. */
233
+ * than silently splitting its memory across two stores. The empty/"default"
234
+ * token routes to the sqlite store: the flat-file Backend A is retired from
235
+ * routing (loadMemory/persistMemory still honour a plain dir string for
236
+ * callers that hold one directly). */
214
237
  export async function openMemoryBackend(repoRoot, backendChoice) {
215
238
  if (backendChoice === BACKEND_MEMORY) {
216
239
  return { dir: createInMemoryStore(), close: async () => {} };
217
240
  }
218
- if (backendChoice === BACKEND_SQLITE) {
219
- const dbPath = join(repoRoot, ".tmct", "memory", "graph.sqlite");
220
- await mkdir(dirname(dbPath), { recursive: true });
221
- const handle = await createSqliteMemoryStore(dbPath);
222
- return { dir: handle, close: async () => closeSqliteMemoryStore(handle) };
241
+ const dbPath = join(repoRoot, ".tmct", "memory", "graph.sqlite");
242
+ const exists = (p) => access(p).then(() => true, () => false);
243
+ if (!(await exists(dbPath)) && (await exists(join(repoRoot, MEMORY_GRAPH_REL)))) {
244
+ process.stderr.write("found .tmct/memory/graph.json the flat-file memory backend is retired; starting a fresh sqlite store (the old file is left untouched)\n");
245
+ }
246
+ await mkdir(dirname(dbPath), { recursive: true });
247
+ const handle = await createSqliteMemoryStore(dbPath);
248
+ return { dir: handle, close: async () => closeSqliteMemoryStore(handle) };
249
+ }
250
+
251
+ /** openMemoryBackend for an entry point that holds only a repo path and has no
252
+ * CLI-flag tier (the fold's idle pass, `tmct import --file`): resolve the
253
+ * backend token the way the chat path does minus the flag —
254
+ * TMCT_MEMORY_BACKEND env > tmct.toml's [memory] backend > the default. */
255
+ export async function openConfiguredMemoryBackend(repoRoot, env = process.env) {
256
+ let backend = String(env?.TMCT_MEMORY_BACKEND || "").trim().toLowerCase();
257
+ if (!backend) {
258
+ try {
259
+ const { loadTomlConfig } = await import("../toml-config.mjs");
260
+ backend = String((await loadTomlConfig(repoRoot))?.memory?.backend || "").trim().toLowerCase();
261
+ } catch { backend = ""; }
223
262
  }
224
- return { dir: repoRoot, close: async () => {} };
263
+ return openMemoryBackend(repoRoot, backend);
225
264
  }
226
265
 
227
266
  /** Deep-clone a JSON-safe value — keeps every cache read/write from aliasing
@@ -230,9 +269,20 @@ const cloneJson = (v) => (v === undefined ? v : structuredClone(v));
230
269
 
231
270
  /** The loadMemory-equivalent read for Backend C: reconstructs from SQL once
232
271
  * per handle (or after a failed write invalidates the cache), then returns a
233
- * clone of `handle.cachedPayload` with zero SQL. */
272
+ * clone of `handle.cachedPayload` with zero SQL.
273
+ *
274
+ * PRAGMA data_version guards the cache across CONNECTIONS: it ticks when any
275
+ * other connection commits to the same file (never for this connection's own
276
+ * writes, which the cache already mirrors in lockstep). Without the check, a
277
+ * second writer's rows would be invisible here — and this handle's next
278
+ * persist would delete them as absent-from-payload. */
234
279
  function readSqlitePayload(handle) {
235
- if (!handle.cachedPayload) handle.cachedPayload = buildSqlitePayloadFromRows(handle);
280
+ const dataVersion = handle.db.prepare("PRAGMA data_version").get()?.data_version;
281
+ if (handle.cachedPayload && handle.cachedDataVersion !== dataVersion) handle.cachedPayload = null;
282
+ if (!handle.cachedPayload) {
283
+ handle.cachedPayload = buildSqlitePayloadFromRows(handle);
284
+ handle.cachedDataVersion = dataVersion;
285
+ }
236
286
  return cloneJson(handle.cachedPayload);
237
287
  }
238
288
 
@@ -1081,6 +1131,21 @@ export async function appendUtterances(dir, utterances) {
1081
1131
  return { ids };
1082
1132
  }
1083
1133
 
1134
+ /** Append mgx:canonicalisedFrom edges (canonical Fact → as-spoken Utterance),
1135
+ * deduped by (subject, object) via upsertEdge — the fold's canonise-link
1136
+ * write, routed through the same backend-dispatched mutate path as every
1137
+ * other append so it lands in whichever store the repo actually uses. */
1138
+ export async function appendCanonicalisedFromEdges(dir, links) {
1139
+ if (!links?.length) return;
1140
+ await mutateMemory(dir, (payload) => {
1141
+ for (const l of links) {
1142
+ upsertEdge(payload, { predicate: "canonicalisedFrom", prop: CANONICALISED_FROM_PROP }, {
1143
+ subject: l.factId, object: l.uttId, subjectLabel: l.factLabel, objectLabel: l.uttLabel,
1144
+ });
1145
+ }
1146
+ });
1147
+ }
1148
+
1084
1149
  /** Append one grammar-derived OWL triple, RDF-reified as a `Fact` individual.
1085
1150
  * Same (s,p,o) -> same id -> upsert, never a duplicate. `premiseTrusts`/
1086
1151
  * `ruleConfidence` optionally engage trust.mjs's entailed hook. Validated
@@ -169,11 +169,12 @@ export async function normalizeConfig(raw, { configDir } = {}) {
169
169
  // (snapshotMemory), not injected here.
170
170
  //
171
171
  // [memory] backend — the storage-backend seam (createSession's
172
- // `memoryBackend` option / TMCT_MEMORY_BACKEND env): "default" (the flat
173
- // OWL-labelled JSON file under .tmct/memory/), "memory" (in-process only,
174
- // nothing on disk), or "sqlite" (a local SQLite file). Written by `tmct init
175
- // --memory-backend <...>`; read by chat.mjs's createSession at CLI-flag > env >
176
- // tmct.toml > default precedence, same order as everything else in this file.
172
+ // `memoryBackend` option / TMCT_MEMORY_BACKEND env): "sqlite" (a local
173
+ // SQLite file at .tmct/memory/graph.sqlite — also what "default"/unset
174
+ // resolves to), or "memory" (in-process only, nothing on disk). Written by
175
+ // `tmct init --memory-backend <...>`; read by chat.mjs's createSession at
176
+ // CLI-flag > env > tmct.toml > default precedence, same order as everything
177
+ // else in this file.
177
178
  const mem = src.memory || {};
178
179
  const memory = {};
179
180
  if (mem.retention_versions !== undefined) memory.retentionVersions = mem.retention_versions;
@@ -31,10 +31,12 @@ export const CLI_VERBS = [
31
31
  { flag: "[--ephemeral]", prose: ["read the graph but write nothing back (demo/read-only)"] },
32
32
  { flag: "[--prompt \"<text>\"]", prose: ["one-shot: run the prompt's sentences as turns and print", "the final answer (teach state first, trigger last)"] },
33
33
  { flag: "[--render blocks]", prose: ["with --prompt: when the final turn produced a plan,", "write it as a self-contained animated page"] },
34
- { flag: "[--output <path>]", prose: ["the rendered page's path (default plan.html)"] },
34
+ { flag: "[--render spider-fly|adventure|sprites]", prose: ["write that demo view as one self-contained", "page (no --prompt; the game pages inline their engine)"] },
35
+ { flag: "[--output <path>]", prose: ["the rendered page's path (default plan.html for", "blocks, <archetype>.html for the views)"] },
35
36
  { flag: "[--narrate]", prose: ["start with narrate mode on — a verbose, developer-facing", "trace of decision points/matched pattern/results/goal per", "turn, appended under a \"--- narrate ---\" marker (also", "TMCT_NARRATE=1; toggle mid-session with /narrate on|off)"] },
37
+ { flag: "[--live-wikipedia]", prose: ["start with the live Wikipedia supplement on — a question", "nothing local can answer also tries en.wikipedia.org,", "cited (network; also TMCT_LIVE_WIKIPEDIA=1 or tmct.toml", "corpus tier3; toggle mid-session with /wiki on|off)"] },
36
38
  { flag: "[--plain]", prose: ["force the plain readline shell (the default when", "stdin/stdout is not a terminal)"] },
37
- { flag: "[--memory-backend <default|memory|sqlite>]", prose: ["storage backend for taught facts this", "session (CLI flag > TMCT_MEMORY_BACKEND env > tmct.toml's", "[memory] backend > \"default\", the flat .tmct/ JSON file)"] },
39
+ { flag: "[--memory-backend <default|memory|sqlite>]", prose: ["storage backend for taught facts this", "session (CLI flag > TMCT_MEMORY_BACKEND env > tmct.toml's", "[memory] backend > sqlite, .tmct/memory/graph.sqlite)"] },
38
40
  ],
39
41
  },
40
42
  {
@@ -17,6 +17,14 @@
17
17
  // relation is stored as given and never chased transitively (see the
18
18
  // "scale" variation documented in hanoi-3.txt itself), so a partial pairing
19
19
  // would leave some disks with no legal move.
20
+ //
21
+ // On top of hanoi-3.txt's own content, the lesson teaches two puzzle-
22
+ // inventory facts ("puzzle has N disks." / "puzzle has 3 pegs.") so the
23
+ // page's dock can answer "what is the puzzle?" / "does the puzzle have N
24
+ // disks?" about the very control the page displays. The subject is bare
25
+ // ("puzzle", no article) because that is the shape the has-teach frame
26
+ // stores; the disk fact regenerates with the disk-count control, so it
27
+ // tracks N instead of going stale.
20
28
 
21
29
  /** The taught lesson for an N-disk puzzle: the class/individual/ordering
22
30
  * facts, the action rule, the render hints, the starting stack (largest at
@@ -33,6 +41,8 @@ export function hanoiLessonSentences(diskCount = 3, { goalPeg = "peg-c" } = {})
33
41
  sentences.push("a peg is a kind of place.");
34
42
  for (const d of disks) sentences.push(`${d} is a disk.`);
35
43
  for (const p of pegs) sentences.push(`${p} is a peg.`);
44
+ sentences.push(`puzzle has ${n} disk${n === 1 ? "" : "s"}.`);
45
+ sentences.push("puzzle has 3 pegs.");
36
46
  for (let i = 0; i < n; i += 1) {
37
47
  for (let j = i + 1; j < n; j += 1) sentences.push(`${disks[i]} is smaller than ${disks[j]}.`);
38
48
  }