@polycode-projects/the-mechanical-code-talker 4.0.1 → 4.1.1

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 (65) hide show
  1. package/README.md +2 -1
  2. package/corpus/sprites/src/sprite-facts.jsonl +375 -8
  3. package/package.json +1 -1
  4. package/src/adapters/memory/core.mjs +20 -0
  5. package/src/domain/ask-vocab.mjs +71 -0
  6. package/src/domain/ask.mjs +168 -0
  7. package/src/domain/game-config.mjs +11 -0
  8. package/src/domain/mud-facts.mjs +15 -0
  9. package/src/domain/router/drive.mjs +35 -9
  10. package/src/domain/router/registry.mjs +24 -4
  11. package/src/domain/router/resolver.mjs +102 -40
  12. package/src/domain/scene-compose.mjs +117 -0
  13. package/src/domain/spider-fly-world.mjs +36 -0
  14. package/src/domain/sprite-facts.mjs +0 -0
  15. package/src/domain/sprite-request.mjs +156 -0
  16. package/src/domain/sprite-templates.mjs +161 -14
  17. package/src/services/adventure-editor.mjs +8 -14
  18. package/src/services/adventure-viz.mjs +119 -150
  19. package/src/services/adventure.mjs +97 -35
  20. package/src/services/chat-page-viz.mjs +64 -48
  21. package/src/services/chat.mjs +102 -34
  22. package/src/services/code-explorer-viz.mjs +52 -50
  23. package/src/services/ingest-viz.mjs +32 -74
  24. package/src/services/ledger-viz.mjs +87 -70
  25. package/src/services/memory-panel-viz.mjs +38 -0
  26. package/src/services/mud-editor.mjs +10 -15
  27. package/src/services/mud-turn.mjs +6 -6
  28. package/src/services/mud-viz.mjs +119 -225
  29. package/src/services/p2p-room.mjs +90 -23
  30. package/src/services/plan-pddl.mjs +3 -1
  31. package/src/services/plan-viz.mjs +13 -12
  32. package/src/services/research-viz.mjs +25 -67
  33. package/src/services/spider-fly-turn.mjs +14 -22
  34. package/src/services/spider-fly-viz.mjs +97 -136
  35. package/src/services/spider-fly.mjs +69 -11
  36. package/src/services/sprite-catalog-viz.mjs +274 -224
  37. package/src/services/viz-boot.mjs +71 -0
  38. package/src/services/viz-room-graph.mjs +203 -0
  39. package/src/services/viz-theme.mjs +75 -1
  40. package/src/services/viz-ticker.mjs +22 -0
  41. package/src/surfaces/web/adventure-browser-entry.mjs +62 -47
  42. package/src/surfaces/web/chat-browser-entry.mjs +51 -107
  43. package/src/surfaces/web/code-explorer-browser-entry.mjs +192 -35
  44. package/src/surfaces/web/engine-surface.mjs +82 -0
  45. package/src/surfaces/web/ingest-browser-entry.mjs +16 -17
  46. package/src/surfaces/web/ledger-browser-entry.mjs +24 -56
  47. package/src/surfaces/web/memory-ask-browser-entry.mjs +55 -13
  48. package/src/surfaces/web/memory-ask-browser.bundle.js +128 -125
  49. package/src/surfaces/web/memory-stats.mjs +11 -0
  50. package/src/surfaces/web/mud-browser-entry.mjs +70 -49
  51. package/src/surfaces/web/plan-browser-entry.mjs +39 -50
  52. package/src/surfaces/web/research-browser-entry.mjs +48 -46
  53. package/src/surfaces/web/spider-fly-browser-entry.mjs +76 -40
  54. package/src/surfaces/web/sprites-browser-entry.mjs +28 -32
  55. package/src/surfaces/web/tmct-surface.mjs +147 -0
  56. package/src/surfaces/web/turn-session.mjs +124 -0
  57. package/src/tools/definitions.mjs +30 -0
  58. package/src/tools/handlers/index.mjs +6 -3
  59. package/src/tools/handlers/kit.mjs +19 -2
  60. package/src/tools/handlers/tmct-ask.mjs +11 -6
  61. package/src/tools/handlers/tmct-ingest.mjs +5 -1
  62. package/src/tools/handlers/tmct-related.mjs +4 -4
  63. package/src/tools/handlers/tmct-sprite.mjs +147 -0
  64. package/src/tools/memory-fallthrough.mjs +9 -2
  65. package/src/tools/server.mjs +37 -6
@@ -15,8 +15,10 @@
15
15
  // doesn't do: it reads the checked-in chat-dock engine bundle.
16
16
 
17
17
  import { loadMemory, readFactRows, findContradictions, normFactTerm } from "../adapters/memory/core.mjs";
18
- import { THEME_TOKENS_CSS, MONO_STACK, escapeHtml, embedJson } from "./viz-theme.mjs";
18
+ import { THEME_TOKENS_CSS, MONO_STACK, escapeHtml, embedJson, countLabel, TOKENS } from "./viz-theme.mjs";
19
19
  import { createTicker, prefersReducedMotion } from "./viz-ticker.mjs";
20
+ import { bfsLevels } from "../domain/planning.mjs";
21
+ import { pluralOf } from "../domain/inflect.mjs";
20
22
  import { readFile } from "node:fs/promises";
21
23
  import { fileURLToPath } from "node:url";
22
24
  import { dirname, join } from "node:path";
@@ -33,23 +35,33 @@ const DASH_SANS_STACK = `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
33
35
  // themes — a deliberate departure from THEME_TOKENS_CSS's light/dark switch,
34
36
  // which every other generated page still follows. --taught/--corpus/--entail/
35
37
  // --alert stay the shared semantic provenance tokens; here they're pinned to
36
- // their dark-theme-friendly values (viz-theme.mjs's own dark token table) so
37
- // the palette that ships in dark mode elsewhere is what this page always
38
- // shows. viz-theme.mjs exports only the assembled THEME_TOKENS_CSS string,
39
- // not its per-mode token table, so the dark set is restated here from that
40
- // table rather than recomputed.
41
- const DASH_DARK_CHROME_CSS = `
38
+ // their dark-theme-friendly values, read straight off viz-theme.mjs's own
39
+ // exported TOKENS.dark table so this page's palette can never drift from the
40
+ // one every other generated page's dark mode uses. The tier alphas (.35/.65/1)
41
+ // match TOKENS's own tier scale; the four "soft" tints use higher alphas than
42
+ // THEME_TOKENS_CSS's shared soft tokens on purpose — this dashboard chrome
43
+ // wants a more visible tint against its always-dark background than the
44
+ // light/dark-switching pages need, so those four alphas stay page-specific.
45
+ function hexRgb(hex) {
46
+ const n = parseInt(hex.slice(1), 16);
47
+ return `${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}`;
48
+ }
49
+ const DASH_DARK_CHROME_CSS = (() => {
50
+ const t = TOKENS.dark;
51
+ const [taught, corpus, entail] = [hexRgb(t.taught), hexRgb(t.corpus), hexRgb(t.entail)];
52
+ return `
42
53
  :root { color-scheme: dark; }
43
54
  body {
44
- --bg: #15181C; --ink: #E7E5DF; --muted: #9A9E95; --line: #2B3036; --card: #1C2126;
45
- --taught: #5FBE8B; --corpus: #6C93BF; --entail: #D9A554; --alert: #D08070;
46
- --taught-t1: rgba(95,190,139,.35); --taught-t2: rgba(95,190,139,.65); --taught-t3: rgba(95,190,139,1);
47
- --corpus-t1: rgba(108,147,191,.35); --corpus-t2: rgba(108,147,191,.65); --corpus-t3: rgba(108,147,191,1);
48
- --entail-t1: rgba(217,165,84,.35); --entail-t2: rgba(217,165,84,.65); --entail-t3: rgba(217,165,84,1);
49
- --taught-soft: rgba(95,190,139,.14); --corpus-soft: rgba(108,147,191,.14);
50
- --entail-soft: rgba(217,165,84,.16); --alert-soft: rgba(208,128,112,.16);
55
+ --bg: ${t.bg}; --ink: ${t.ink}; --muted: ${t.muted}; --line: ${t.line}; --card: ${t.card};
56
+ --taught: ${t.taught}; --corpus: ${t.corpus}; --entail: ${t.entail}; --alert: ${t.alert};
57
+ --taught-t1: rgba(${taught},.35); --taught-t2: rgba(${taught},.65); --taught-t3: rgba(${taught},1);
58
+ --corpus-t1: rgba(${corpus},.35); --corpus-t2: rgba(${corpus},.65); --corpus-t3: rgba(${corpus},1);
59
+ --entail-t1: rgba(${entail},.35); --entail-t2: rgba(${entail},.65); --entail-t3: rgba(${entail},1);
60
+ --taught-soft: rgba(${taught},.14); --corpus-soft: rgba(${corpus},.14);
61
+ --entail-soft: rgba(${entail},.16); --alert-soft: rgba(${hexRgb(t.alert)},.16);
51
62
  }
52
63
  `;
64
+ })();
53
65
 
54
66
  /** Read the checked-in browser memory-ask-engine bundle
55
67
  * (`src/surfaces/web/memory-ask-browser.bundle.js`) — the real memory-graph answer engine
@@ -453,7 +465,7 @@ function dashboardHtml(stats, { fresh = false } = {}) {
453
465
  <div class="tile">
454
466
  <span class="tile-label">facts.total</span>
455
467
  <span class="tile-value">${total}</span>
456
- <span class="tile-sub">${terms} term${terms === 1 ? "" : "s"} tracked</span>
468
+ <span class="tile-sub">${countLabel(terms, "term")} tracked</span>
457
469
  </div>
458
470
  <div class="tile tile-tier">
459
471
  <span class="tile-label">facts.by-tier</span>
@@ -467,7 +479,7 @@ function dashboardHtml(stats, { fresh = false } = {}) {
467
479
  <div class="tile${qualityCls}">
468
480
  <span class="tile-label">data.quality</span>
469
481
  <span class="tile-value">${s.contradictionCount || 0}</span>
470
- <span class="tile-sub">term${s.contradictionCount === 1 ? "" : "s"} with more than one answer</span>
482
+ <span class="tile-sub">${s.contradictionCount === 1 ? "term" : pluralOf("term")} with more than one answer</span>
471
483
  </div>
472
484
  </div>
473
485
  <div class="barpanels">
@@ -537,7 +549,7 @@ function sparkCaptionHtml(stats) {
537
549
  * self-contained document with no external requests. Only
538
550
  * scripts/build-demo-site.mjs, which builds the sibling bundle itself
539
551
  * first, passes `true`. The dock's own runtime code below ALSO gates on
540
- * `typeof tmctLedger !== "undefined"` regardless — the two checks answer
552
+ * `typeof tmct !== "undefined"` regardless — the two checks answer
541
553
  * different questions (did this render even offer the reference; did the
542
554
  * browser actually manage to load it), and both must hold for the live
543
555
  * path to run. */
@@ -545,14 +557,14 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
545
557
  const ledgerJson = embedJson({ rows: rows || [], terms: terms || [], edges: edges || [], focus: focus || null, contradictions: contradictions || [], worthALook: worthALook || null, meta: meta || { shown: 0, total: 0, truncated: false }, focusDigest: focusDigest || null, digestStructures: Array.isArray(digestStructures) ? digestStructures : [] });
546
558
  const payloadJson = embedJson(payload || { individuals: [], objectProperties: [] });
547
559
  const shown = meta?.shown ?? (rows || []).length;
548
- const title = `tmct ledger — ${shown} fact${shown === 1 ? "" : "s"}${focus ? ` (focus: ${escapeHtml(focus)})` : ""}`;
560
+ const title = `tmct ledger — ${countLabel(shown, "fact")}${focus ? ` (focus: ${escapeHtml(focus)})` : ""}`;
549
561
  const bundleStr = typeof memoryAskBundle === "string" ? memoryAskBundle : "";
550
562
  const hasMemChat = bundleStr.length > 0;
551
563
  // The placeholder is honest: the canonical exchange only when its terms are
552
564
  // really in this payload, otherwise a real term from this graph. Left as
553
565
  // the query-only wording even when the live bundle is offered — the dock's
554
566
  // own script swaps it for a teach-aware placeholder the moment it confirms
555
- // tmctLedger actually loaded (never claimed ahead of that confirmation).
567
+ // the live engine actually loaded (never claimed ahead of that confirmation).
556
568
  // The example term skips anything under 3 characters — the SAME floor the
557
569
  // dock's own miss-tips apply below — so a graph whose highest-degree term
558
570
  // is a short stopword-shaped fragment (init:large corpora carry plenty:
@@ -563,7 +575,7 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
563
575
  ? "who is the grandfather of ishmael"
564
576
  : (exampleTerm ? `ask the graph… e.g. what is ${exampleTerm.term}` : "ask the graph…");
565
577
  // The paste-and-drop ingest panel plus the JSONL export ride the LIVE
566
- // engine (window.tmctLedger): both teach into and read the dock's own
578
+ // engine (window.tmct): both teach into and read the dock's own
567
579
  // session store, so they appear only where that bundle is offered — the
568
580
  // demo site, never the CLI's self-contained tmct viz page.
569
581
  const ingestHtml = ledgerBundleAvailable
@@ -833,6 +845,11 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
833
845
  // escapeHtml/pct by their real names) resolve without a second copy.
834
846
  const escapeHtml = esc;
835
847
  const pct = ${pct.toString()};
848
+ // pluralOf underlies countLabel's own default-parameter lookup — spliced
849
+ // ahead of it so that lookup resolves the same way it does node-side.
850
+ const pluralOf = ${pluralOf.toString()};
851
+ const countLabel = ${countLabel.toString()};
852
+ const bfsLevels = ${bfsLevels.toString()};
836
853
  const tierTileHtml = ${tierTileHtml.toString()};
837
854
  const microbarsHtml = ${microbarsHtml.toString()};
838
855
  const dashboardHtml = ${dashboardHtml.toString()};
@@ -871,14 +888,13 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
871
888
  // exists it points at that session's store, which teach/research grow in
872
889
  // place, so a digest of a just-taught term reads the fresh facts.
873
890
  let getLivePayload = () => PAYLOAD;
874
- // Whichever loaded bundle carries the browser digest helper — the demo
875
- // ledger's own live engine (tmctLedger) or the committed memory-ask engine
876
- // the CLI viz page inlines (tmctMemoryAsk). Null before either loads, and the
877
- // focus card then keeps whatever server-computed digest it shipped.
891
+ // The browser digest helper, from whichever engine this page loaded — the
892
+ // demo ledger's live one or the committed query-only one the CLI viz page
893
+ // inlines. Both publish it in the same place now, so there is one lookup
894
+ // rather than two. Null before either loads, and the focus card then keeps
895
+ // whatever server-computed digest it shipped.
878
896
  const digestHelper = () =>
879
- (typeof tmctLedger !== "undefined" && tmctLedger && tmctLedger.digestTermFromPayloadBrowser)
880
- || (typeof tmctMemoryAsk !== "undefined" && tmctMemoryAsk && tmctMemoryAsk.digestTermFromPayloadBrowser)
881
- || null;
897
+ (typeof tmct !== "undefined" && tmct.page && tmct.page.digestTermFromPayloadBrowser) || null;
882
898
  // The digest for one term, computed live in the browser from the embedded
883
899
  // structure table, or null when no structures were embedded, no engine has
884
900
  // loaded, or the term holds nothing to compose.
@@ -965,7 +981,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
965
981
  : "";
966
982
  let html = '<div class="focuscard"><div class="term">' + esc(focus) + "</div>" +
967
983
  '<div class="klass">' + (klass ? esc(klass.phrase + " " + klass.o) : "no class recorded") + "</div>" +
968
- '<div class="stats">' + all.length + " facts &middot; " + srcs.size + " source" + (srcs.size === 1 ? "" : "s") +
984
+ '<div class="stats">' + countLabel(all.length, "fact") + " &middot; " + countLabel(srcs.size, "source") +
969
985
  (dates.length ? " &middot; first " + esc(dates[0].slice(0, 10)) + " &middot; last " + esc(dates[dates.length - 1].slice(0, 10)) : "") + "</div>" + digestHtml + "</div>";
970
986
  const bracketed = new Set();
971
987
  for (const fam of FAMS) {
@@ -1028,13 +1044,16 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1028
1044
  if (!focus) return;
1029
1045
  const color = { taught: cssVar("--taught"), corpus: cssVar("--corpus"), entailed: cssVar("--entail"), entail: cssVar("--entail") };
1030
1046
  const lineC = cssVar("--line"), inkC = cssVar("--ink");
1031
- const hop1 = new Set(), hop2 = new Set();
1032
- for (const e of LEDGER.edges) { if (e.s === focus) hop1.add(e.o); if (e.o === focus) hop1.add(e.s); }
1033
- hop1.delete(focus);
1034
- for (const e of LEDGER.edges) {
1035
- if (hop1.has(e.s) && e.o !== focus && !hop1.has(e.o)) hop2.add(e.o);
1036
- if (hop1.has(e.o) && e.s !== focus && !hop1.has(e.s)) hop2.add(e.s);
1037
- }
1047
+ const neighborsOf = (term) => {
1048
+ const out = [];
1049
+ for (const e of LEDGER.edges) {
1050
+ if (e.s === term) out.push(e.o);
1051
+ else if (e.o === term) out.push(e.s);
1052
+ }
1053
+ return out;
1054
+ };
1055
+ const [hop1Level, hop2Level] = bfsLevels(focus, neighborsOf, { maxDepth: 2 });
1056
+ const hop1 = new Set(hop1Level || []), hop2 = new Set(hop2Level || []);
1038
1057
  const cx = w / 2, cy = h / 2, r1 = 42, r2 = 68;
1039
1058
  const pos = { [focus]: { x: cx, y: cy } };
1040
1059
  const place = (set, r) => { const a = [...set].sort(); a.forEach((t, i) => { const ang = (2 * Math.PI * i) / a.length - Math.PI / 2; pos[t] = { x: cx + r * Math.cos(ang), y: cy + r * Math.sin(ang) }; }); };
@@ -1118,9 +1137,9 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1118
1137
  }
1119
1138
 
1120
1139
  // ---- the chat dock: LIVE teach+ask over the sibling ledger-browser.bundle.js
1121
- // (window.tmctLedger) when the demo build offered it AND it actually
1122
- // loaded; falls back to the read-only tmctMemoryAsk engine below
1123
- // unchanged from before this page could teach at all — otherwise.
1140
+ // (window.tmct) when the demo build offered it AND it actually
1141
+ // loaded; falls back to the read-only engine below, which publishes the
1142
+ // same surface as a stand-in and says so through tmct.fallback.
1124
1143
  // bin/tmct.mjs's own \`tmct viz\` output never offers the live bundle
1125
1144
  // (renderLedgerHtml's own ledgerBundleAvailable defaults false there), so
1126
1145
  // this branch is simply never reachable on a CLI-generated page.
@@ -1128,7 +1147,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1128
1147
  const createTicker = ${createTicker.toString()};
1129
1148
  const prefersReducedMotion = ${prefersReducedMotion.toString()};
1130
1149
  const chatForm = el("chatform");
1131
- if (chatForm && typeof tmctLedger !== "undefined" && typeof tmctLedger.createLedgerSession === "function") {
1150
+ if (chatForm && typeof tmct !== "undefined" && !tmct.fallback) {
1132
1151
  const log = el("chatlog");
1133
1152
  const chatqEl = el("chatq");
1134
1153
  chatqEl.placeholder = 'ask or teach the graph\\u2026 e.g. "blue is a peg"';
@@ -1163,7 +1182,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1163
1182
  import("./vendor/wink.js"),
1164
1183
  winkTimeout(WINK_LOAD_TIMEOUT_MS, "wink vendor asset load timed out"),
1165
1184
  ]);
1166
- tmctLedger.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
1185
+ tmct.page.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
1167
1186
  } catch (err) {
1168
1187
  // eslint-disable-next-line no-console
1169
1188
  console.warn("tmct ledger: the wink vendor asset failed to load, continuing without the lemma/POS tier", err);
@@ -1176,7 +1195,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1176
1195
  async function ensureSession() {
1177
1196
  if (session) return session;
1178
1197
  await tryLoadWink();
1179
- session = await tmctLedger.createLedgerSession({ seedPayload: PAYLOAD });
1198
+ session = await tmct.open({ seedPayload: PAYLOAD });
1180
1199
  // From here the live store is the source of truth for the digest, so a
1181
1200
  // digest of a term taught this session reads its fresh facts — the store
1182
1201
  // grows in place, so this one closure stays current.
@@ -1194,15 +1213,15 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1194
1213
  chatqEl.disabled = true;
1195
1214
  withLock(async () => {
1196
1215
  try {
1197
- const s = await ensureSession();
1198
- const result = await s.turn(q);
1216
+ const live = await ensureSession();
1217
+ const result = await tmct.turn(q);
1199
1218
  const record = result.record;
1200
1219
  const taught = !!record && record.miss === false && (record.via === "assert" || record.via === "retract");
1201
1220
  const body = esc(result.answer).replace(/\\n/g, "<br>");
1202
1221
  pending.className = "a" + (taught ? " taught" : (record && record.miss ? " miss" : ""));
1203
1222
  pending.innerHTML = taught ? '<span class="tag">taught</span>' + body : body;
1204
1223
  if (taught) {
1205
- const fresh = tmctLedger.computeLedgerDataFromPayload(s.memoryDir.payload, {});
1224
+ const fresh = tmct.page.computeLedgerDataFromPayload(live.memoryDir.payload, {});
1206
1225
  applyLedgerData(fresh);
1207
1226
  } else if (!(record && record.miss)) {
1208
1227
  // Only a genuine answer (never a miss) tries to resolve a
@@ -1211,7 +1230,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1211
1230
  // ordinary English word (e.g. "run", if the graph happens to
1212
1231
  // hold it), and resolveAnsweredTerm has no way to tell that
1213
1232
  // apart from the term genuinely being discussed.
1214
- const hit = resolveAnsweredTerm(result.answer, q, LEDGER.terms, tmctLedger.normFactTerm);
1233
+ const hit = resolveAnsweredTerm(result.answer, q, LEDGER.terms, tmct.page.normFactTerm);
1215
1234
  if (hit) refocusWithLabel(hit, q);
1216
1235
  }
1217
1236
  } catch {
@@ -1226,7 +1245,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1226
1245
 
1227
1246
  // ---- ingest + export: bring your own text, take the graph away ---------
1228
1247
  // The ingest panel runs pasted/dropped/browsed text through the SAME dock
1229
- // session, one sentence at a time (tmctLedger.splitSentences, then
1248
+ // session, one sentence at a time (tmct.page.splitSentences, then
1230
1249
  // s.turn), keeping only the sentences the recognizer grounds — then
1231
1250
  // re-derives the whole ledger so the new facts can be examined in place.
1232
1251
  // Export serializes that same session store to canonical JSONL.
@@ -1262,16 +1281,16 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1262
1281
  withLock(async () => {
1263
1282
  try {
1264
1283
  const s = await ensureSession();
1265
- const sentences = tmctLedger.splitSentences(text);
1284
+ const sentences = tmct.page.splitSentences(text);
1266
1285
  let grounded = 0;
1267
1286
  for (const sentence of sentences) {
1268
- const r = await s.turn(sentence);
1287
+ const r = await tmct.turn(sentence);
1269
1288
  if (r.record && r.record.miss === false && r.record.via === "assert") grounded += 1;
1270
1289
  }
1271
- const fresh = tmctLedger.computeLedgerDataFromPayload(s.memoryDir.payload, {});
1290
+ const fresh = tmct.page.computeLedgerDataFromPayload(s.memoryDir.payload, {});
1272
1291
  applyLedgerData(fresh);
1273
1292
  const skipped = sentences.length - grounded;
1274
- statusEl.textContent = sentences.length + " sentence" + (sentences.length === 1 ? "" : "s")
1293
+ statusEl.textContent = countLabel(sentences.length, "sentence")
1275
1294
  + ", " + grounded + " added" + (skipped ? ", " + skipped + " skipped" : "");
1276
1295
  } catch {
1277
1296
  statusEl.textContent = "something went wrong ingesting that";
@@ -1288,7 +1307,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1288
1307
  withLock(async () => {
1289
1308
  try {
1290
1309
  const s = await ensureSession();
1291
- const jsonl = await tmctLedger.exportFactsJsonl(s.memoryDir);
1310
+ const jsonl = await tmct.page.exportFactsJsonl(s.memoryDir);
1292
1311
  const blob = new Blob([jsonl], { type: "application/x-ndjson" });
1293
1312
  const url = URL.createObjectURL(blob);
1294
1313
  const link = document.createElement("a");
@@ -1323,7 +1342,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1323
1342
  researchPlayBtn.setAttribute("aria-pressed", String(st.playing));
1324
1343
  researchStatusEl.textContent = !researchQueue ? ""
1325
1344
  : researchQueue.complete
1326
- ? 'research "' + researchQueue.topic + '" complete \\u2014 ' + researchQueue.done.length + " topic" + (researchQueue.done.length === 1 ? "" : "s")
1345
+ ? 'research "' + researchQueue.topic + '" complete \\u2014 ' + countLabel(researchQueue.done.length, "topic")
1327
1346
  : researchQueue.done.length + " done \\u00b7 " + researchQueue.pending.length + " queued";
1328
1347
  }
1329
1348
 
@@ -1334,8 +1353,8 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1334
1353
  const pending = addLine("a pending", "researching\\u2026");
1335
1354
  return withLock(async () => {
1336
1355
  try {
1337
- const s = await ensureSession();
1338
- const result = await s.turn(q);
1356
+ const live = await ensureSession();
1357
+ const result = await tmct.turn(q);
1339
1358
  const missed = !result.record || Boolean(result.record.miss);
1340
1359
  pending.className = "a" + (missed ? " miss" : "");
1341
1360
  pending.innerHTML = esc(result.answer).replace(/\\n/g, "<br>");
@@ -1343,7 +1362,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1343
1362
  researchQueue = result.research;
1344
1363
  renderResearchControls();
1345
1364
  if (!missed) {
1346
- const fresh = tmctLedger.computeLedgerDataFromPayload(s.memoryDir.payload, {});
1365
+ const fresh = tmct.page.computeLedgerDataFromPayload(live.memoryDir.payload, {});
1347
1366
  applyLedgerData(fresh);
1348
1367
  }
1349
1368
  }
@@ -1386,9 +1405,11 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1386
1405
  else researchTicker.play();
1387
1406
  });
1388
1407
  }
1389
- } else if (chatForm && typeof tmctMemoryAsk !== "undefined") {
1390
- const memHandle = tmctMemoryAsk.createInMemoryStore();
1391
- memHandle.payload = PAYLOAD;
1408
+ } else if (chatForm && typeof tmct !== "undefined") {
1409
+ // The query-only dock. One session over the page's own embedded payload,
1410
+ // and every line goes to tmct.ask the cascade that used to be chained
1411
+ // here by hand now lives behind that one call.
1412
+ const opened = tmct.open({ payload: PAYLOAD });
1392
1413
  const log = el("chatlog");
1393
1414
  const addLine = (cls, html) => {
1394
1415
  const d = document.createElement("div");
@@ -1403,20 +1424,16 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1403
1424
  input.value = "";
1404
1425
  addLine("u", esc(q));
1405
1426
  (async () => {
1406
- let fact = null;
1407
- try { fact = await tmctMemoryAsk.factAnswer(memHandle, q, null, true, {}); } catch { fact = null; }
1408
- // runAsk's own cascade is factAnswer ?? factReadBack; chain the same
1409
- // way when the bundle exposes the second reader (relation chases
1410
- // "who is the grandfather of ishmael" — live there, not in factAnswer).
1411
- if (!(fact && fact.text) && typeof tmctMemoryAsk.factReadBack === "function") {
1412
- try { fact = await tmctMemoryAsk.factReadBack(memHandle, q, null, true, null); } catch { fact = null; }
1413
- }
1414
- if (fact && fact.text) {
1415
- addLine("a", esc(fact.text).replace(/\\n/g, "<br>"));
1427
+ await opened;
1428
+ let answer = null;
1429
+ let data = null;
1430
+ try { ({ answer, data } = await tmct.ask(q)); } catch { answer = null; data = null; }
1431
+ if (answer) {
1432
+ addLine("a", esc(answer).replace(/\\n/g, "<br>"));
1416
1433
  // Same formatting contract as chat's withGoalLine: capitalized,
1417
1434
  // full-stop-terminated, rendered only when the engine deduced one.
1418
- if (fact.goal) addLine("a goal", "Goal (inferred): " + esc(fact.goal.charAt(0).toUpperCase() + fact.goal.slice(1)) + ".");
1419
- const hit = resolveAnsweredTerm(fact.text, q, LEDGER.terms, tmctMemoryAsk.normFactTerm);
1435
+ if (data && data.goal) addLine("a goal", "Goal (inferred): " + esc(data.goal.charAt(0).toUpperCase() + data.goal.slice(1)) + ".");
1436
+ const hit = resolveAnsweredTerm(answer, q, LEDGER.terms, tmct.page.normFactTerm);
1420
1437
  if (hit) refocusWithLabel(hit, q);
1421
1438
  } else {
1422
1439
  const tips = LEDGER.terms.filter((t) => t.term.length >= 3).slice(0, 2)
@@ -181,3 +181,41 @@ export function renderStatsPanelInto(panelEl, stats, opts) {
181
181
  }
182
182
  }
183
183
  }
184
+
185
+ /** The boot statusline while the big engine assets stream in — byte-identical
186
+ * across research-viz.mjs and ingest-viz.mjs, each commenting that it uses
187
+ * "the same aggregator" as the other. `parts` is an array of { loaded, total }
188
+ * byte counts (total 0 when a response carried no Content-Length); with no
189
+ * usable total the line shows loaded bytes alone rather than inventing a
190
+ * denominator. Self-contained, `.toString()`-splice safe. */
191
+ export function loadProgressLine(parts) {
192
+ const mb = (n) => (n / 1048576).toFixed(1);
193
+ let loaded = 0;
194
+ let total = 0;
195
+ let totalKnown = true;
196
+ for (const p of parts || []) {
197
+ loaded += (p && p.loaded) || 0;
198
+ if (p && p.total > 0) total += p.total;
199
+ else totalKnown = false;
200
+ }
201
+ return totalKnown && total > 0
202
+ ? "loading the engine… " + mb(loaded) + " MB / " + mb(total) + " MB"
203
+ : "loading the engine… " + mb(loaded) + " MB";
204
+ }
205
+
206
+ /** One learned/grounded fact as its three canonical cells, plus whichever of
207
+ * the two per-page provenance fields the caller's fact objects carry: `source`
208
+ * (research.html's source key, e.g. "taught"/"seed:conceptnet" — used for the
209
+ * tone dot) and `provenance` (ingest.html's raw ' | '-joined tag string —
210
+ * shown verbatim). Both default to "" when the fact object doesn't carry
211
+ * that field, so either page reads back only the one it populated. Pure,
212
+ * `.toString()`-splice safe. */
213
+ export function factTripleParts(fact) {
214
+ return {
215
+ subject: String((fact && fact.subject) || ""),
216
+ predicate: String((fact && fact.predicate) || ""),
217
+ object: String((fact && fact.object) || ""),
218
+ source: String((fact && fact.source) || ""),
219
+ provenance: String((fact && fact.provenance) || ""),
220
+ };
221
+ }
@@ -12,11 +12,14 @@
12
12
  // point is "change what this world is made of" has to speak the vocabulary the
13
13
  // world is actually written in, and neither table is a superset of the other.
14
14
  //
15
- // No imports: every export here is `.toString()`-splice-safe, the same discipline
16
- // adventure-editor.mjs and the *-viz render-glue functions hold — mud-viz.mjs
17
- // splices these straight into mud.html's inline script, which captures a
18
- // function's own source text and nothing it closes over. A splice-safe function
19
- // carries its whole dependency graph inside its own body.
15
+ // Every export here but wordBeforeCursor is written locally, and is
16
+ // `.toString()`-splice-safe, the same discipline adventure-editor.mjs and the
17
+ // *-viz render-glue functions hold mud-viz.mjs splices these straight into
18
+ // mud.html's inline script, which captures a function's own source text and
19
+ // nothing it closes over. A splice-safe function carries its whole dependency
20
+ // graph inside its own body. wordBeforeCursor is a re-export of viz-theme.mjs's
21
+ // shared implementation, itself self-contained and splice-safe the same way —
22
+ // this module no longer keeps its own copy of it.
20
23
  //
21
24
  // Two predicate families sync differently, for the reason adventure-editor.mjs's
22
25
  // own header sets out:
@@ -34,6 +37,8 @@
34
37
  // that fails to parse this keystroke must never be read as "this fact is
35
38
  // gone".
36
39
 
40
+ export { wordBeforeCursor } from "./viz-theme.mjs";
41
+
37
42
  const PLACEMENT_KIND = "placement";
38
43
  const OPENNESS_KIND = "openness";
39
44
  const MASS_KIND = "mass";
@@ -301,13 +306,3 @@ export function planMudEditorSync(rows, state, triples) {
301
306
  }
302
307
  return { toAppend, toRemoveIds };
303
308
  }
304
-
305
- /** The word immediately before `cursorPos` in `text` — a run of letters, digits
306
- * and hyphens, the shape this vocabulary's own terms take ("underground-space",
307
- * "mole-1"). Empty when the cursor sits after whitespace or punctuation with no
308
- * word directly behind it. Pure. */
309
- export function wordBeforeCursor(text, cursorPos) {
310
- const head = String(text || "").slice(0, cursorPos);
311
- const m = head.match(/[A-Za-z][A-Za-z0-9-]*$/);
312
- return m ? m[0].toLowerCase() : "";
313
- }
@@ -50,10 +50,12 @@ import { fnv1a32 } from "../domain/hash.mjs";
50
50
  import { bfsLevels } from "../domain/planning.mjs";
51
51
  import { loadMemory, readFactRows } from "../adapters/memory/core.mjs";
52
52
  import { DEFAULT_GAME_CONFIG, mudMassDrainPerTurn } from "../domain/game-config.mjs";
53
+ import { predatorSubjects } from "../domain/mud-facts.mjs";
53
54
  import {
54
55
  foldWorldState, worldActionRows, runWorldCommand, recordTold, recordExamined,
55
56
  recordMassDrain, personKnowledgeLines, objectClassChain, diggableDirections,
56
57
  isOutOfPlay, outOfPlayReasonOf, outOfPlayPhrase, massDrainPerTurnOf,
58
+ parseSnapshotSubject,
57
59
  } from "./adventure.mjs";
58
60
 
59
61
  const FOOD_CLASS = "food";
@@ -61,7 +63,6 @@ const LATERAL_DIRECTIONS = ["north", "south", "east", "west"];
61
63
  // Deep enough to cross a whole burrow without letting one character's
62
64
  // pathfinder walk a whole grown world every tick.
63
65
  const WALK_SEARCH_DEPTH = 8;
64
- const SNAPSHOT_RE = /^(.+)@turn(\d+)$/;
65
66
 
66
67
  const EXIT_TOWARD_FOOD_CHANCE = 0.5;
67
68
  const EDGE_FOLLOW_DIG_CHANCE = 0.25;
@@ -208,8 +209,8 @@ function roomsStoodIn(rows, character) {
208
209
  const visited = new Set();
209
210
  for (const row of worldActionRows(rows)) {
210
211
  if (row.predicate !== "mgx:currently-in") continue;
211
- const snapshot = SNAPSHOT_RE.exec(row.subject);
212
- if ((snapshot ? snapshot[1] : row.subject) !== character) continue;
212
+ const snapshot = parseSnapshotSubject(row.subject);
213
+ if ((snapshot ? snapshot.base : row.subject) !== character) continue;
213
214
  visited.add(row.object);
214
215
  }
215
216
  return visited;
@@ -220,9 +221,8 @@ function roomsStoodIn(rows, character) {
220
221
  * funnel straight to the den, since the unvisited room nearest the burrow is
221
222
  * exactly where the fox lives. The den stays reachable by the food gamble
222
223
  * below, which is a gamble and is meant to be. */
223
- const predatorRooms = (rows, state) => new Set((rows || [])
224
- .filter((r) => r.predicate === "mgx:is-predator" && r.object === "true")
225
- .map((r) => state.placements.get(r.subject)?.object)
224
+ const predatorRooms = (rows, state) => new Set(predatorSubjects(rows)
225
+ .map((subject) => state.placements.get(subject)?.object)
226
226
  .filter(Boolean));
227
227
 
228
228
  /**