@polycode-projects/the-mechanical-code-talker 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +178 -9
  2. package/corpus/reference/index.json.gz +0 -0
  3. package/corpus/reference/manifest.json +8 -8
  4. package/corpus/reference/shards/ref-00.jsonl.gz +0 -0
  5. package/corpus/sprites/src/sprite-facts.jsonl +401 -0
  6. package/corpus/worlds/index.json.gz +0 -0
  7. package/corpus/worlds/manifest.json +49 -9
  8. package/corpus/worlds/shards/greyvale-museum.jsonl.gz +0 -0
  9. package/corpus/worlds/shards/lantern-cottage.jsonl.gz +0 -0
  10. package/corpus/worlds/shards/mud-hollow.jsonl.gz +0 -0
  11. package/corpus/worlds/shards/mud-warren.jsonl.gz +0 -0
  12. package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
  13. package/corpus/worlds/src/greyvale-museum.jsonl +136 -0
  14. package/corpus/worlds/src/lantern-cottage.jsonl +60 -0
  15. package/corpus/worlds/src/mud-hollow.jsonl +82 -0
  16. package/corpus/worlds/src/mud-warren.jsonl +124 -0
  17. package/corpus/worlds/src/spider-fly.jsonl +1 -1
  18. package/data/sprites/animal-icon.toml +11 -5
  19. package/data/sprites/book-icon.toml +9 -5
  20. package/data/sprites/cabinet-icon.toml +10 -5
  21. package/data/sprites/cellar-icon.toml +16 -6
  22. package/data/sprites/container-icon.toml +7 -3
  23. package/data/sprites/desk-icon.toml +8 -5
  24. package/data/sprites/dog-icon.toml +8 -5
  25. package/data/sprites/dog-with-colour-icon.toml +13 -12
  26. package/data/sprites/drawing-room-icon.toml +14 -5
  27. package/data/sprites/egg-icon.toml +6 -3
  28. package/data/sprites/fly-icon.toml +10 -5
  29. package/data/sprites/furniture-icon.toml +5 -3
  30. package/data/sprites/garden-icon.toml +10 -3
  31. package/data/sprites/key-icon.toml +3 -1
  32. package/data/sprites/kitchen-icon.toml +15 -6
  33. package/data/sprites/lamp-icon.toml +9 -4
  34. package/data/sprites/letter-icon.toml +7 -4
  35. package/data/sprites/library-icon.toml +14 -3
  36. package/data/sprites/pan-icon.toml +7 -3
  37. package/data/sprites/person-icon.toml +6 -2
  38. package/data/sprites/poodle-icon.toml +1 -1
  39. package/data/sprites/portable-icon.toml +6 -4
  40. package/data/sprites/portrait-icon.toml +7 -4
  41. package/data/sprites/room-icon.toml +11 -2
  42. package/data/sprites/spider-icon.toml +9 -3
  43. package/data/sprites/study-icon.toml +10 -2
  44. package/package.json +5 -4
  45. package/src/adapters/memory/core.mjs +20 -0
  46. package/src/domain/ask-vocab.mjs +71 -0
  47. package/src/domain/ask.mjs +168 -0
  48. package/src/domain/game-config.mjs +11 -0
  49. package/src/domain/grammar/ace.mjs +40 -4
  50. package/src/domain/grammar/lexicon-core.json +2 -1
  51. package/src/domain/grammar/lexicon.mjs +18 -0
  52. package/src/domain/mud-facts.mjs +15 -0
  53. package/src/domain/reference-pack.mjs +31 -7
  54. package/src/domain/router/drive.mjs +35 -9
  55. package/src/domain/router/registry.mjs +24 -4
  56. package/src/domain/router/resolver.mjs +102 -40
  57. package/src/domain/scene-compose.mjs +117 -0
  58. package/src/domain/spider-fly-world.mjs +37 -1
  59. package/src/domain/sprite-facts.mjs +0 -0
  60. package/src/domain/sprite-request.mjs +156 -0
  61. package/src/domain/sprite-templates.mjs +169 -20
  62. package/src/services/adventure-editor.mjs +8 -14
  63. package/src/services/adventure-viz.mjs +209 -157
  64. package/src/services/adventure.mjs +526 -391
  65. package/src/services/chat-page-viz.mjs +69 -25
  66. package/src/services/chat.mjs +200 -51
  67. package/src/services/code-explorer-viz.mjs +102 -62
  68. package/src/services/extract-facts.mjs +4 -7
  69. package/src/services/ingest-viz.mjs +68 -82
  70. package/src/services/ledger-viz.mjs +136 -67
  71. package/src/services/memory-panel-viz.mjs +62 -0
  72. package/src/services/mud-editor.mjs +10 -15
  73. package/src/services/mud-turn.mjs +6 -6
  74. package/src/services/mud-viz.mjs +1016 -208
  75. package/src/services/p2p-room.mjs +90 -23
  76. package/src/services/plan-pddl.mjs +3 -1
  77. package/src/services/plan-viz.mjs +123 -64
  78. package/src/services/research-viz.mjs +160 -108
  79. package/src/services/spider-fly-turn.mjs +15 -23
  80. package/src/services/spider-fly-viz.mjs +146 -161
  81. package/src/services/spider-fly.mjs +69 -11
  82. package/src/services/sprite-catalog-viz.mjs +414 -240
  83. package/src/services/viz-boot.mjs +71 -0
  84. package/src/services/viz-room-graph.mjs +203 -0
  85. package/src/services/viz-theme.mjs +90 -1
  86. package/src/services/viz-ticker.mjs +22 -0
  87. package/src/surfaces/web/adventure-browser-entry.mjs +49 -33
  88. package/src/surfaces/web/chat-browser-entry.mjs +30 -105
  89. package/src/surfaces/web/code-explorer-browser-entry.mjs +168 -24
  90. package/src/surfaces/web/ingest-browser-entry.mjs +3 -13
  91. package/src/surfaces/web/ledger-browser-entry.mjs +7 -47
  92. package/src/surfaces/web/memory-ask-browser.bundle.js +127 -124
  93. package/src/surfaces/web/memory-stats.mjs +11 -0
  94. package/src/surfaces/web/mud-browser-entry.mjs +71 -29
  95. package/src/surfaces/web/plan-browser-entry.mjs +22 -40
  96. package/src/surfaces/web/research-browser-entry.mjs +26 -41
  97. package/src/surfaces/web/spider-fly-browser-entry.mjs +45 -28
  98. package/src/surfaces/web/sprites-browser-entry.mjs +14 -27
  99. package/src/surfaces/web/turn-session.mjs +120 -0
  100. package/src/tools/definitions.mjs +30 -0
  101. package/src/tools/handlers/index.mjs +6 -3
  102. package/src/tools/handlers/kit.mjs +19 -2
  103. package/src/tools/handlers/tmct-ask.mjs +11 -6
  104. package/src/tools/handlers/tmct-ingest.mjs +5 -1
  105. package/src/tools/handlers/tmct-related.mjs +4 -4
  106. package/src/tools/handlers/tmct-sprite.mjs +147 -0
  107. package/src/tools/memory-fallthrough.mjs +9 -2
  108. package/src/tools/server.mjs +25 -1
@@ -15,14 +15,54 @@
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, SERIF_STACK, 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";
23
25
 
24
26
  const LEDGER_ROW_LIMIT_DEFAULT = 20000;
25
27
 
28
+ // The ledger reads as an observability dashboard (dense stat tiles, bar
29
+ // panels, a log-style fact stream) rather than an editorial page, so its
30
+ // chrome runs the system sans everywhere instead of THEME_TOKENS_CSS's
31
+ // serif body face; MONO_STACK still carries every metric, label, and log row.
32
+ const DASH_SANS_STACK = `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`;
33
+
34
+ // This page pins a dark, New-Relic-style dashboard register in BOTH site
35
+ // themes — a deliberate departure from THEME_TOKENS_CSS's light/dark switch,
36
+ // which every other generated page still follows. --taught/--corpus/--entail/
37
+ // --alert stay the shared semantic provenance tokens; here they're pinned to
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 `
53
+ :root { color-scheme: dark; }
54
+ body {
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);
62
+ }
63
+ `;
64
+ })();
65
+
26
66
  /** Read the checked-in browser memory-ask-engine bundle
27
67
  * (`src/surfaces/web/memory-ask-browser.bundle.js`) — the real memory-graph answer engine
28
68
  * (chat.mjs's factAnswer/factReadBack) the chat dock runs on. Returns `""`,
@@ -407,7 +447,7 @@ function dashboardHtml(stats, { fresh = false } = {}) {
407
447
  const total = s.totalFacts || 0;
408
448
  const freshCls = fresh ? " fresh" : "";
409
449
  if (!total) {
410
- return `<section class="dash${freshCls}" id="dash" aria-label="Ledger metrics"><div class="tile"><span class="tile-label">facts.total</span><span class="tile-value">0</span><span class="tile-sub">nothing taught yet</span></div></section>`;
450
+ return `<section class="dash${freshCls}" id="dash" aria-label="Ledger metrics"><div class="kpirow"><div class="tile"><span class="tile-label">facts.total</span><span class="tile-value">0</span><span class="tile-sub">nothing taught yet</span></div></div></section>`;
411
451
  }
412
452
  const terms = s.totalTerms || 0;
413
453
  const qualityCls = s.contradictionCount > 0 ? " tile-alert" : "";
@@ -415,33 +455,42 @@ function dashboardHtml(stats, { fresh = false } = {}) {
415
455
  const predicatesHtml = s.predicates?.length
416
456
  ? microbarsHtml(s.predicates.map((p) => ({ label: p.phrase || p.predicate, count: p.count })))
417
457
  : `<span class="tile-sub">none yet</span>`;
458
+ // Two explicit-column grids — a 4-up KPI row, then a 2-up row of bar-chart
459
+ // tiles — instead of one auto-fill grid: auto-fill's column count depends
460
+ // on viewport width, so a wide-tile mid-row wrap used to strand empty
461
+ // tracks on the right of both rows. Both stay `.tile` (the dashboard-strip
462
+ // e2e test counts six of them under `.dash`, whatever their sub-grid).
418
463
  return `<section class="dash${freshCls}" id="dash" aria-label="Ledger metrics">
419
- <div class="tile">
420
- <span class="tile-label">facts.total</span>
421
- <span class="tile-value">${total}</span>
422
- <span class="tile-sub">${terms} term${terms === 1 ? "" : "s"} tracked</span>
423
- </div>
424
- <div class="tile tile-wide">
425
- <span class="tile-label">facts.by-tier</span>
426
- ${tierTileHtml(s.byProv || {}, total)}
427
- </div>
428
- <div class="tile">
429
- <span class="tile-label">graph.avg-degree</span>
430
- <span class="tile-value">${(s.density || 0).toFixed(1)}</span>
431
- <span class="tile-sub">facts per term</span>
432
- </div>
433
- <div class="tile${qualityCls}">
434
- <span class="tile-label">data.quality</span>
435
- <span class="tile-value">${s.contradictionCount || 0}</span>
436
- <span class="tile-sub">term${s.contradictionCount === 1 ? "" : "s"} with more than one answer</span>
437
- </div>
438
- <div class="tile tile-wide">
439
- <span class="tile-label">corpus.bundles</span>
440
- <div class="bundlebars">${bundlesHtml}</div>
464
+ <div class="kpirow">
465
+ <div class="tile">
466
+ <span class="tile-label">facts.total</span>
467
+ <span class="tile-value">${total}</span>
468
+ <span class="tile-sub">${countLabel(terms, "term")} tracked</span>
469
+ </div>
470
+ <div class="tile tile-tier">
471
+ <span class="tile-label">facts.by-tier</span>
472
+ ${tierTileHtml(s.byProv || {}, total)}
473
+ </div>
474
+ <div class="tile">
475
+ <span class="tile-label">graph.avg-degree</span>
476
+ <span class="tile-value">${(s.density || 0).toFixed(1)}</span>
477
+ <span class="tile-sub">facts per term</span>
478
+ </div>
479
+ <div class="tile${qualityCls}">
480
+ <span class="tile-label">data.quality</span>
481
+ <span class="tile-value">${s.contradictionCount || 0}</span>
482
+ <span class="tile-sub">${s.contradictionCount === 1 ? "term" : pluralOf("term")} with more than one answer</span>
483
+ </div>
441
484
  </div>
442
- <div class="tile tile-wide">
443
- <span class="tile-label">predicate.top</span>
444
- <div class="bundlebars">${predicatesHtml}</div>
485
+ <div class="barpanels">
486
+ <div class="tile tile-bars">
487
+ <span class="tile-label">corpus.bundles</span>
488
+ <div class="bundlebars">${bundlesHtml}</div>
489
+ </div>
490
+ <div class="tile tile-bars">
491
+ <span class="tile-label">predicate.top</span>
492
+ <div class="bundlebars">${predicatesHtml}</div>
493
+ </div>
445
494
  </div>
446
495
  </section>`;
447
496
  }
@@ -508,7 +557,7 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
508
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 : [] });
509
558
  const payloadJson = embedJson(payload || { individuals: [], objectProperties: [] });
510
559
  const shown = meta?.shown ?? (rows || []).length;
511
- 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)})` : ""}`;
512
561
  const bundleStr = typeof memoryAskBundle === "string" ? memoryAskBundle : "";
513
562
  const hasMemChat = bundleStr.length > 0;
514
563
  // The placeholder is honest: the canonical exchange only when its terms are
@@ -534,7 +583,7 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
534
583
  <button type="button" id="ingestToggle" class="dockbtn">ingest text&hellip;</button>
535
584
  <button type="button" id="exportFacts" class="dockbtn">export facts</button>
536
585
  </div>
537
- <div class="researchrow" title="Fetches the topic from Simple English Wikipedia into this graph, then queues the topics its lead section links to each queued topic runs as its own dock turn, paced politely. Asking is the network consent for these fetches.">
586
+ <div class="researchrow" title="Fetches the topic from Simple English Wikipedia into this graph, then queues the topics its lead section links to. Each queued topic runs as its own dock turn, paced politely. Asking is the network consent for these fetches.">
538
587
  <label for="researchTopic" class="mono">research:</label>
539
588
  <input id="researchTopic" type="text" autocomplete="off" spellcheck="false"
540
589
  placeholder="a topic, e.g. owls" aria-label="Topic to research on Simple English Wikipedia">
@@ -544,7 +593,7 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
544
593
  </div>
545
594
  <div class="ingestpanel" id="ingestPanel" hidden>
546
595
  <textarea id="ingestText" spellcheck="false" aria-label="Text to ingest into the graph"
547
- placeholder="Paste text or drop a .txt/.md file. Each sentence it recognizes as a fact is added to the graph; the rest are skipped honestly."></textarea>
596
+ placeholder="Paste text or drop a .txt/.md file. Each sentence it recognizes as a fact is added to the graph. The rest are skipped."></textarea>
548
597
  <div class="ingestrow">
549
598
  <button type="button" class="dockbtn" id="ingestBrowse">browse&hellip;</button>
550
599
  <input type="file" id="ingestFile" accept=".txt,.md,text/plain,text/markdown" hidden>
@@ -562,7 +611,7 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
562
611
  </form>
563
612
  ${ingestHtml}
564
613
  </div>`
565
- : `<div class="chat chat-off"><p class="chatnote">chat unavailable run <span class="mono">npm run build:ask-bundle</span> to enable the in-page ask engine.</p></div>`;
614
+ : `<div class="chat chat-off"><p class="chatnote">Chat unavailable. Run <span class="mono">npm run build:ask-bundle</span> to enable the in-page ask engine.</p></div>`;
566
615
 
567
616
  return `<!doctype html>
568
617
  <html lang="en">
@@ -587,46 +636,52 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
587
636
  -->
588
637
  <style>
589
638
  ${THEME_TOKENS_CSS}
639
+ ${DASH_DARK_CHROME_CSS}
590
640
  html { background: var(--bg); }
591
- body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; }
641
+ body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${DASH_SANS_STACK}; font-size: 15px; line-height: 1.5; }
592
642
  .mono { font-family: ${MONO_STACK}; }
593
- main { max-width: 1080px; margin: 0 auto; padding: 1.4rem 1.2rem 3rem; }
643
+ main { max-width: 1200px; margin: 0 auto; padding: 1.4rem 1.2rem 3rem; }
594
644
  .eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); display: flex; flex-wrap: wrap; gap: .4em 1.2em; margin-bottom: .9rem; }
595
645
  button { font: inherit; color: inherit; background: none; border: none; padding: 0; cursor: pointer; }
596
- button:focus-visible, input:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; border-radius: 4px; }
597
- .topbar { display: flex; flex-wrap: wrap; align-items: center; gap: .8rem; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); padding: .5rem 0; margin-bottom: 1.1rem; }
646
+ button:focus-visible, input:focus-visible { outline: 2px solid var(--corpus); outline-offset: 2px; border-radius: 4px; }
647
+ .topbar { display: flex; flex-wrap: wrap; align-items: center; gap: .8rem; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: .5rem .7rem; margin-bottom: 1.1rem; }
598
648
  .crumbs { display: flex; align-items: center; flex-wrap: wrap; gap: .35rem; font-size: .78rem; }
599
649
  .crumbs .sep { color: var(--muted); }
600
- .crumb { font-family: ${MONO_STACK}; font-size: .74rem; padding: .12rem .5rem; border: 1px solid var(--line); border-radius: 99px; background: var(--card); }
650
+ .crumb { font-family: ${MONO_STACK}; font-size: .74rem; padding: .12rem .5rem; border: 1px solid var(--line); border-radius: 99px; background: var(--bg); }
601
651
  .crumb[aria-current="true"] { background: var(--ink); color: var(--bg); border-color: var(--ink); }
602
652
  .search { margin-left: auto; display: flex; align-items: center; gap: .5rem; }
603
- .search input { font-family: ${MONO_STACK}; font-size: .78rem; background: var(--card); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .3rem .6rem; width: 170px; }
653
+ .search input { font-family: ${MONO_STACK}; font-size: .78rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .3rem .6rem; width: 170px; }
604
654
  .search .miss { font-size: .72rem; color: var(--alert); font-family: ${MONO_STACK}; }
605
- .dash { display: grid; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); gap: .6rem; margin: 0 0 1.1rem; }
606
- .tile { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: .55rem .7rem .6rem; display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
607
- .tile-wide { grid-column: span 2; }
608
- .tile-alert { border-color: var(--alert); }
655
+ /* Two explicit-column grids instead of one auto-fill grid: a 4-up KPI row,
656
+ then a 2-up row of bar-chart tiles, so both rows fill the container's
657
+ full width at any viewport rather than stranding a leftover track. */
658
+ .dash { display: flex; flex-direction: column; gap: .6rem; margin: 0 0 1.1rem; }
659
+ .kpirow { display: grid; grid-template-columns: repeat(4, 1fr); gap: .6rem; }
660
+ .barpanels { display: grid; grid-template-columns: repeat(2, 1fr); gap: .6rem; }
661
+ @media (max-width: 700px) { .kpirow { grid-template-columns: repeat(2, 1fr); } .barpanels { grid-template-columns: 1fr; } }
662
+ .tile { background: var(--card); border: 1px solid var(--line); border-left: 3px solid var(--line); border-radius: 6px; padding: .6rem .75rem .65rem; display: flex; flex-direction: column; gap: .18rem; min-width: 0; }
663
+ .tile-alert { border-left-color: var(--alert); }
609
664
  .tile-label { font-family: ${MONO_STACK}; font-size: .62rem; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); }
610
- .tile-value { font-family: ${MONO_STACK}; font-size: 1.5rem; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.15; }
665
+ .tile-value { font-family: ${MONO_STACK}; font-size: 1.6rem; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.15; }
611
666
  .tile-alert .tile-value { color: var(--alert); }
612
667
  .tile-sub { font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); }
613
- .tierbar { display: flex; height: 8px; border-radius: 99px; overflow: hidden; background: var(--line); margin-top: .3rem; }
668
+ .tierbar { display: flex; height: 8px; border-radius: 99px; overflow: hidden; background: var(--line); margin-top: .35rem; }
614
669
  .tseg { height: 100%; }
615
670
  .tseg.t-taught { background: var(--taught); } .tseg.t-corpus { background: var(--corpus); } .tseg.t-entail { background: var(--entail); }
616
671
  .tierlegend { display: flex; flex-wrap: wrap; gap: .15rem .7rem; margin-top: .35rem; font-family: ${MONO_STACK}; font-size: .65rem; }
617
672
  .tleg.t-taught { color: var(--taught); } .tleg.t-corpus { color: var(--corpus); } .tleg.t-entail { color: var(--entail); }
618
- .bundlebars { display: flex; flex-direction: column; gap: .22rem; margin-top: .35rem; }
619
- .bbar { display: grid; grid-template-columns: minmax(0,1fr) 3.4rem 1.6rem; align-items: center; gap: .4rem; font-family: ${MONO_STACK}; font-size: .68rem; }
673
+ .bundlebars { display: flex; flex-direction: column; gap: .26rem; margin-top: .4rem; }
674
+ .bbar { display: grid; grid-template-columns: minmax(0,1fr) 3.4rem 1.6rem; align-items: center; gap: .4rem; font-family: ${MONO_STACK}; font-size: .7rem; }
620
675
  .bblabel { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
621
676
  .bbtrack { background: var(--line); border-radius: 99px; height: 5px; overflow: hidden; }
622
- .bbfill { display: block; height: 100%; background: var(--ink); opacity: .55; border-radius: 99px; }
677
+ .bbfill { display: block; height: 100%; background: var(--ink); opacity: .6; border-radius: 99px; }
623
678
  .bbn { color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
624
679
  .sparkwrap { margin-top: .5rem; }
625
680
  .spark { display: block; width: 100%; height: 44px; }
626
681
  .spark .fill { fill: var(--muted); opacity: .18; stroke: none; }
627
682
  .spark .line { fill: none; stroke: var(--ink); stroke-width: 1.4; }
628
683
  .spark .dot { fill: var(--ink); }
629
- .app { display: grid; grid-template-columns: 190px minmax(0,1fr) 230px; gap: 1.3rem; grid-template-areas: "rail ledger aside"; }
684
+ .app { display: grid; grid-template-columns: 190px minmax(0,1fr) 240px; gap: 1.1rem; grid-template-areas: "rail ledger aside"; align-items: start; }
630
685
  .rail { grid-area: rail; } .ledger { grid-area: ledger; } .aside { grid-area: aside; }
631
686
  @media (max-width: 880px) { .app { grid-template-columns: 1fr; grid-template-areas: "ledger" "aside" "rail"; } .search { margin-left: 0; } }
632
687
  .rail h2, .aside h2 { font-family: ${MONO_STACK}; font-size: .66rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); font-weight: 400; margin: 1rem 0 .4rem; }
@@ -641,7 +696,7 @@ ${THEME_TOKENS_CSS}
641
696
  .seg.on.neutral { background: var(--ink); color: var(--bg); } .seg.on.neutral .n { color: var(--bg); }
642
697
  .seg.on.c-taught { background: var(--taught); } .seg.on.c-corpus { background: var(--corpus); } .seg.on.c-entail { background: var(--entail); }
643
698
  .d-taught { background: var(--taught); } .d-corpus { background: var(--corpus); } .d-entail { background: var(--entail); }
644
- .focuscard { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: .8rem 1rem; margin-bottom: 1rem; }
699
+ .focuscard { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: .85rem 1rem; margin-bottom: 1rem; }
645
700
  .focuscard .term { font-size: 1.35rem; font-weight: 700; }
646
701
  .focuscard .klass, .focuscard .stats { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--muted); margin-top: .3rem; font-variant-numeric: tabular-nums; }
647
702
  .focuscard .focusdigest { margin-top: .7rem; font-size: .92rem; line-height: 1.5; }
@@ -656,8 +711,13 @@ ${THEME_TOKENS_CSS}
656
711
  .group { margin: 1.1rem 0; }
657
712
  .group h3 { font-family: ${MONO_STACK}; font-size: .68rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); font-weight: 400; margin: 0 0 .35rem; display: flex; align-items: baseline; gap: .5rem; }
658
713
  .group h3::after { content: ""; flex: 1; border-top: 1px solid var(--line); transform: translateY(-.2em); }
659
- .rows { display: flex; flex-direction: column; gap: .35rem; }
660
- .row { background: var(--card); border: 1px solid var(--line); border-left: 3px solid var(--line); border-radius: 0 8px 8px 0; padding: .45rem .75rem; display: flex; flex-wrap: wrap; align-items: baseline; gap: .3rem .5rem; font-size: .95rem; }
714
+ /* The fact stream reads as a dense query-result log: full-width rows, a
715
+ hairline between each, a provenance-colored left rail, no per-row card
716
+ chrome. */
717
+ .rows { display: flex; flex-direction: column; }
718
+ .row { background: transparent; border: none; border-left: 3px solid var(--line); border-bottom: 1px solid var(--line); border-radius: 0; padding: .42rem .7rem; display: flex; flex-wrap: wrap; align-items: baseline; gap: .3rem .5rem; font-family: ${MONO_STACK}; font-size: .82rem; }
719
+ .rows .row:last-child { border-bottom: none; }
720
+ .row:hover { background: var(--line); }
661
721
  .row.p-taught.t3 { border-left-color: var(--taught-t3); } .row.p-taught.t2 { border-left-color: var(--taught-t2); } .row.p-taught.t1 { border-left-color: var(--taught-t1); }
662
722
  .row.p-corpus.t3 { border-left-color: var(--corpus-t3); } .row.p-corpus.t2 { border-left-color: var(--corpus-t2); } .row.p-corpus.t1 { border-left-color: var(--corpus-t1); }
663
723
  .row.p-entail.t3 { border-left-color: var(--entail-t3); } .row.p-entail.t2 { border-left-color: var(--entail-t2); } .row.p-entail.t1 { border-left-color: var(--entail-t1); }
@@ -668,18 +728,18 @@ ${THEME_TOKENS_CSS}
668
728
  .prov.p-taught { color: var(--taught); background: var(--taught-soft); }
669
729
  .prov.p-corpus { color: var(--corpus); background: var(--corpus-soft); }
670
730
  .prov.p-entail { color: var(--entail); background: var(--entail-soft); }
671
- .contra { border: 1px solid var(--alert); border-radius: 10px; padding: .5rem; margin-top: .35rem; }
731
+ .contra { border: 1px solid var(--alert); border-radius: 6px; padding: .5rem; margin-top: .35rem; }
672
732
  .contra .label { font-family: ${MONO_STACK}; font-size: .66rem; color: var(--alert); margin: 0 0 .35rem .2rem; }
673
- .empty { color: var(--muted); font-size: .9rem; border: 1px dashed var(--line); border-radius: 8px; padding: .8rem 1rem; }
733
+ .empty { color: var(--muted); font-size: .88rem; border: 1px dashed var(--line); border-radius: 6px; padding: .8rem 1rem; }
674
734
  .looks { display: flex; flex-direction: column; gap: .45rem; }
675
- .look { display: block; text-align: left; background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: .5rem .65rem; font-size: .8rem; width: 100%; }
735
+ .look { display: block; text-align: left; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: .5rem .65rem; font-size: .8rem; width: 100%; }
676
736
  .look:hover { border-color: var(--ink); }
677
737
  .look .tag { display: block; font-family: ${MONO_STACK}; font-size: .62rem; letter-spacing: .06em; text-transform: uppercase; margin-bottom: .18rem; }
678
738
  .look.k-taught .tag { color: var(--taught); } .look.k-alert .tag { color: var(--alert); } .look.k-hub .tag { color: var(--corpus); }
679
- .mapwrap { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: .45rem; margin-top: .45rem; }
739
+ .mapwrap { background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: .45rem; margin-top: .45rem; }
680
740
  .mapwrap canvas { display: block; width: 100%; height: 160px; cursor: pointer; }
681
741
  .mapnote { font-family: ${MONO_STACK}; font-size: .62rem; color: var(--muted); margin: .3rem .2rem 0; }
682
- .chat { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: .65rem .85rem; margin-bottom: 1rem; }
742
+ .chat { background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: .65rem .85rem; margin-bottom: 1rem; }
683
743
  .chatlog { display: flex; flex-direction: column; gap: .45rem; max-height: 220px; overflow-y: auto; }
684
744
  .chatlog:empty { display: none; }
685
745
  .chatlog .u { font-family: ${MONO_STACK}; font-size: .76rem; color: var(--muted); }
@@ -712,6 +772,7 @@ ${THEME_TOKENS_CSS}
712
772
  .ingeststatus { font-size: .68rem; color: var(--muted); }
713
773
  @media (prefers-reduced-motion: no-preference) {
714
774
  .seg, .chip, .look { transition: border-color .12s ease, background-color .12s ease; }
775
+ .row { transition: background-color .1s ease; }
715
776
  /* A live teach re-render swaps the dash section wholesale (dashboardHtml
716
777
  is called again in full — see the inline script below), so the "this
717
778
  just changed" signal has to be an animation on the fresh markup itself,
@@ -784,6 +845,11 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
784
845
  // escapeHtml/pct by their real names) resolve without a second copy.
785
846
  const escapeHtml = esc;
786
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()};
787
853
  const tierTileHtml = ${tierTileHtml.toString()};
788
854
  const microbarsHtml = ${microbarsHtml.toString()};
789
855
  const dashboardHtml = ${dashboardHtml.toString()};
@@ -916,7 +982,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
916
982
  : "";
917
983
  let html = '<div class="focuscard"><div class="term">' + esc(focus) + "</div>" +
918
984
  '<div class="klass">' + (klass ? esc(klass.phrase + " " + klass.o) : "no class recorded") + "</div>" +
919
- '<div class="stats">' + all.length + " facts &middot; " + srcs.size + " source" + (srcs.size === 1 ? "" : "s") +
985
+ '<div class="stats">' + countLabel(all.length, "fact") + " &middot; " + countLabel(srcs.size, "source") +
920
986
  (dates.length ? " &middot; first " + esc(dates[0].slice(0, 10)) + " &middot; last " + esc(dates[dates.length - 1].slice(0, 10)) : "") + "</div>" + digestHtml + "</div>";
921
987
  const bracketed = new Set();
922
988
  for (const fam of FAMS) {
@@ -934,7 +1000,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
934
1000
  for (const [, grp] of groupsHere) {
935
1001
  if (grp.length < 2) continue;
936
1002
  grp.forEach((r) => bracketed.add(r.id));
937
- inner += '<div class="contra"><p class="label">more than one answer on record &mdash; shown, never merged</p><div class="rows">' + grp.map(rowHtml).join("") + "</div></div>";
1003
+ inner += '<div class="contra"><p class="label">more than one answer on record, shown, not merged</p><div class="rows">' + grp.map(rowHtml).join("") + "</div></div>";
938
1004
  }
939
1005
  html += '<div class="group"><h3>' + esc(FAM_LABEL[fam]) + '</h3><div class="rows">' + inner + "</div></div>";
940
1006
  }
@@ -952,7 +1018,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
952
1018
  looks.push({ cls: "k-taught", tag: "newest teach", target: w.newestTaught.term, body: r ? esc(r.s + " " + r.phrase + " " + r.o) : esc(w.newestTaught.term) });
953
1019
  }
954
1020
  if (w.contradictions && w.contradictions.count) {
955
- looks.push({ cls: "k-alert", tag: w.contradictions.count + " with more than one answer", target: w.contradictions.firstFocusTerm, body: '<span class="mono">' + esc(w.contradictions.firstFocusTerm) + "</span> has answers that differ &mdash; read both" });
1021
+ looks.push({ cls: "k-alert", tag: w.contradictions.count + " with more than one answer", target: w.contradictions.firstFocusTerm, body: '<span class="mono">' + esc(w.contradictions.firstFocusTerm) + "</span> has answers that differ. Read both." });
956
1022
  }
957
1023
  if (w.biggestHub) {
958
1024
  looks.push({ cls: "k-hub", tag: "biggest hub", target: w.biggestHub.term, body: '<span class="mono">' + esc(w.biggestHub.term) + "</span> touches " + w.biggestHub.degree + " facts" });
@@ -979,13 +1045,16 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
979
1045
  if (!focus) return;
980
1046
  const color = { taught: cssVar("--taught"), corpus: cssVar("--corpus"), entailed: cssVar("--entail"), entail: cssVar("--entail") };
981
1047
  const lineC = cssVar("--line"), inkC = cssVar("--ink");
982
- const hop1 = new Set(), hop2 = new Set();
983
- for (const e of LEDGER.edges) { if (e.s === focus) hop1.add(e.o); if (e.o === focus) hop1.add(e.s); }
984
- hop1.delete(focus);
985
- for (const e of LEDGER.edges) {
986
- if (hop1.has(e.s) && e.o !== focus && !hop1.has(e.o)) hop2.add(e.o);
987
- if (hop1.has(e.o) && e.s !== focus && !hop1.has(e.s)) hop2.add(e.s);
988
- }
1048
+ const neighborsOf = (term) => {
1049
+ const out = [];
1050
+ for (const e of LEDGER.edges) {
1051
+ if (e.s === term) out.push(e.o);
1052
+ else if (e.o === term) out.push(e.s);
1053
+ }
1054
+ return out;
1055
+ };
1056
+ const [hop1Level, hop2Level] = bfsLevels(focus, neighborsOf, { maxDepth: 2 });
1057
+ const hop1 = new Set(hop1Level || []), hop2 = new Set(hop2Level || []);
989
1058
  const cx = w / 2, cy = h / 2, r1 = 42, r2 = 68;
990
1059
  const pos = { [focus]: { x: cx, y: cy } };
991
1060
  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) }; }); };
@@ -1222,7 +1291,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1222
1291
  const fresh = tmctLedger.computeLedgerDataFromPayload(s.memoryDir.payload, {});
1223
1292
  applyLedgerData(fresh);
1224
1293
  const skipped = sentences.length - grounded;
1225
- statusEl.textContent = sentences.length + " sentence" + (sentences.length === 1 ? "" : "s")
1294
+ statusEl.textContent = countLabel(sentences.length, "sentence")
1226
1295
  + ", " + grounded + " added" + (skipped ? ", " + skipped + " skipped" : "");
1227
1296
  } catch {
1228
1297
  statusEl.textContent = "something went wrong ingesting that";
@@ -1274,7 +1343,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1274
1343
  researchPlayBtn.setAttribute("aria-pressed", String(st.playing));
1275
1344
  researchStatusEl.textContent = !researchQueue ? ""
1276
1345
  : researchQueue.complete
1277
- ? 'research "' + researchQueue.topic + '" complete \\u2014 ' + researchQueue.done.length + " topic" + (researchQueue.done.length === 1 ? "" : "s")
1346
+ ? 'research "' + researchQueue.topic + '" complete \\u2014 ' + countLabel(researchQueue.done.length, "topic")
1278
1347
  : researchQueue.done.length + " done \\u00b7 " + researchQueue.pending.length + " queued";
1279
1348
  }
1280
1349
 
@@ -49,6 +49,30 @@ export function statsSummaryLine(stats, bandLabel) {
49
49
  : stats.total + " starter facts loaded";
50
50
  }
51
51
 
52
+ /** Drop every asset the site's service worker has cached, and unregister the
53
+ * worker itself, so the next load reads the shipped files off the network.
54
+ * A page's own taught-facts store lives in IndexedDB, a different store
55
+ * entirely — clearing that alone would still re-seed from whatever copy of
56
+ * chat-seed.json the worker cached, which is the whole reason this exists.
57
+ * Best-effort throughout: a browser with no Cache Storage, no worker, or a
58
+ * storage call that throws just leaves the caller to reload as before.
59
+ * Pure of this module's scope, so it stays `.toString()`-splice safe. */
60
+ export async function clearSiteAssetCaches() {
61
+ try {
62
+ if (globalThis.caches) {
63
+ for (const key of await caches.keys()) {
64
+ if (key.startsWith("tmct-precache-")) await caches.delete(key);
65
+ }
66
+ }
67
+ } catch { /* the reload still re-reads whatever the worker will serve */ }
68
+ try {
69
+ if (navigator.serviceWorker && navigator.serviceWorker.getRegistrations) {
70
+ const registrations = await navigator.serviceWorker.getRegistrations();
71
+ await Promise.all(registrations.map((registration) => registration.unregister()));
72
+ }
73
+ } catch { /* an un-unregisterable worker still has an empty cache now */ }
74
+ }
75
+
52
76
  /** Fetch `url` reading the body as a stream, reporting (loadedBytes,
53
77
  * totalBytes) after every chunk — total is 0 when the response carries no
54
78
  * Content-Length. Resolves to a Blob of the whole body. Falls back to a
@@ -157,3 +181,41 @@ export function renderStatsPanelInto(panelEl, stats, opts) {
157
181
  }
158
182
  }
159
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
  /**