@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
@@ -22,7 +22,8 @@
22
22
  // `.toString()` — exactly ledger-viz.mjs's own `facetCounts`/
23
23
  // `resolveAnsweredTerm` pattern — because they are genuinely UI-only, with no
24
24
  // reason to live inside the engine bundle: `createTicker` (viz-ticker.mjs,
25
- // the shared play/pause/step/reset primitive), `classOfAgentId`,
25
+ // the shared play/pause/step/reset primitive), `agentKindOf` (the shared
26
+ // kind-from-id helper, defined once in spider-fly-world.mjs),
26
27
  // `threadCellsForSpiderPlan` (the silk-thread reconstruction), `nextCorpses`
27
28
  // (the dying-agent bookkeeping behind the dusty-corner corpse pile) and
28
29
  // `facingDegreesFor` (plan-driven sprite orientation) — all kept as real,
@@ -32,11 +33,14 @@
32
33
  // bundle's own real ES exports instead, since (unlike ledger-viz, which
33
34
  // reuses a FIXED shared bundle it can't extend for one page's own needs) this
34
35
  // page ships its own dedicated bundle and can just export what it needs.
35
- import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText } from "./viz-theme.mjs";
36
+ import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, meterBarHtml } from "./viz-theme.mjs";
36
37
  import { createTicker } from "./viz-ticker.mjs";
37
- import { GRID_SIZE, WEB_HOME, WEB_RADIUS, isInWebBlock, cellId } from "../domain/spider-fly-world.mjs";
38
+ import { loadWinkVendor } from "./viz-boot.mjs";
39
+ import { GRID_SIZE, WEB_HOME, WEB_RADIUS, isInWebBlock, cellId, agentKindOf } from "../domain/spider-fly-world.mjs";
38
40
  import { FLY_INITIAL_MASS, EGG_LAY_MASS_THRESHOLD } from "./spider-fly.mjs";
39
- import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
41
+ import { believedFactSentence } from "./spider-fly-turn.mjs";
42
+ import { DEFAULT_GAME_CONFIG, massScaleFor } from "../domain/game-config.mjs";
43
+ import { resolveSpriteRequest } from "../domain/sprite-request.mjs";
40
44
 
41
45
  // A larger cell than this page's first cut (44px) — the board sits in its
42
46
  // own full-width block, so a bigger cell keeps it reading as the page's
@@ -62,13 +66,6 @@ function webCellIds() {
62
66
  return out;
63
67
  }
64
68
 
65
- /** An agent id's class for sprite/HUD purposes — "spider-2" -> "spider",
66
- * "fly-10" -> "fly", "egg-1" -> "egg". Self-contained (no outer refs),
67
- * `.toString()`-splice safe. */
68
- export function classOfAgentId(id) {
69
- return String(id).replace(/-\d+$/, "");
70
- }
71
-
72
69
  /**
73
70
  * Reconstruct the spider's remaining silk-thread path — the sequence of
74
71
  * cell ids from its CURRENT cell (after this tick's one executed step) to
@@ -86,7 +83,7 @@ export function classOfAgentId(id) {
86
83
  * `geometry` is `{ parseCellId, cellId, directionDelta }` — the exact three
87
84
  * grid-geometry primitives spider-fly-world.mjs exports, injected rather
88
85
  * than imported so this function stays `.toString()`-splice safe (the
89
- * inlined page calls it with `window.tmctSpiderFly`'s own re-exports of the
86
+ * inlined page calls it with `window.tmct`'s own re-exports of the
90
87
  * same three). Returns the ordered cell-id array (length > 1) or null.
91
88
  */
92
89
  export function threadCellsForSpiderPlan(agents, geometry) {
@@ -128,54 +125,6 @@ export function facingDegreesFor(plan, previousDegrees) {
128
125
  return previousDegrees ?? 0;
129
126
  }
130
127
 
131
- /** The instance-level `mgx:feels` emotion for one spider-fly agent this
132
- * tick — a PURE derivation from state spider-fly.mjs's own `runSpiderFlyTick`
133
- * already returns on the agent (`goal`/`mass`), never a new persisted fact,
134
- * the same derive-don't-duplicate posture `hasActiveWebAt` already holds for
135
- * web state (PLAN_GAMES_UPLIFT_V3.md §B.2.4). Operator-confirmed mapping: a
136
- * spider that just ate is happy; a spider carrying an uncaught fly or
137
- * mid-chase (chasing, or co-located and about to catch) is angry (predatory
138
- * focus); a spider avoiding another spider is scared; a spider holding
139
- * position or building a web is calm. A fly evading a believed-visible
140
- * spider is scared, and so is the sharper form of the same fear — just
141
- * caught, or already being carried; a fly with no threat visible (wandering,
142
- * or trapped with none in sight) is calm.
143
- *
144
- * `agent.goal` is spider-fly.mjs's own fixed-template text (`goalLineFor`'s
145
- * literal phrasing) — the only reliable per-tick discriminator this
146
- * function's caller has, and every real goal string that engine can ever
147
- * produce is matched below by its own distinguishing prefix. A goal this
148
- * function doesn't recognize (a freshly hatched/spawned agent's "no goal
149
- * yet", or the transient "X is gone — re-evaluating" scrub once a named
150
- * agent dies) falls through to "calm" — the 6-word vocabulary's own
151
- * deliberate no-strong-emotion baseline (sprite-expressions.mjs's B.2.1
152
- * design), not a guess.
153
- *
154
- * `maxMass` is accepted for parity with this page's own per-class mass-bar
155
- * denominators (`SPIDERFLY.maxSpiderMass`/`maxFlyMass`) but unused today —
156
- * no state in the operator's own confirmed table keys off a mass ratio,
157
- * only off the goal text above. Self-contained (no outer refs),
158
- * `.toString()`-splice safe. */
159
- export function emotionFor(agent, kind, maxMass) {
160
- const goal = agent?.goal || "";
161
- if (kind === "spider") {
162
- if (goal.startsWith("just ate")) return "happy";
163
- if (goal.startsWith("carrying") || goal.startsWith("chasing") || goal.startsWith("co-located with")) return "angry";
164
- if (goal.startsWith("avoiding")) return "scared";
165
- return "calm";
166
- }
167
- if (kind === "fly") {
168
- if (
169
- goal.startsWith("evading")
170
- || goal.startsWith("being carried by")
171
- || goal.startsWith("just caught by")
172
- || goal.startsWith("trapped in an active web")
173
- ) return "scared";
174
- return "calm";
175
- }
176
- return "calm";
177
- }
178
-
179
128
  /**
180
129
  * The updated corpse set for one redraw (§A.2.5 — visual-only, entirely
181
130
  * client-side; the actual starve/eat removal already happened in the
@@ -200,7 +149,7 @@ export function nextCorpses(prevCorpses, prevAgents, agents, turn, lingerTurns =
200
149
  if (agents[id] || out[id]) continue;
201
150
  const cell = prevAgents[id]?.cell;
202
151
  if (!cell) continue;
203
- out[id] = { cls: classOfAgentId(id), cell, diedAtTurn: turn };
152
+ out[id] = { cls: agentKindOf(id), cell, diedAtTurn: turn };
204
153
  }
205
154
  return out;
206
155
  }
@@ -409,9 +358,9 @@ ${THEME_TOKENS_CSS}
409
358
  texture (repeating-linear-gradient) instead of a smooth gradient bar —
410
359
  discrete resource units, the same reading-at-a-glance language a 90s
411
360
  strategy game's own gold/mana meters use. */
412
- .mass-track { height: 7px; margin-top: .35rem; background: var(--chrome-well); border: 1px solid var(--chrome-edge-lo); box-shadow: var(--chrome-shadow-inset); border-radius: 1px; overflow: hidden; }
413
- .mass-fill { height: 100%; background: repeating-linear-gradient(90deg, var(--taught) 0 6px, rgba(0, 0, 0, .25) 6px 7px); }
414
- .mass-fill.fly { background: repeating-linear-gradient(90deg, var(--fly) 0 6px, rgba(0, 0, 0, .25) 6px 7px); }
361
+ .meter-track { height: 7px; margin-top: .35rem; background: var(--chrome-well); border: 1px solid var(--chrome-edge-lo); box-shadow: var(--chrome-shadow-inset); border-radius: 1px; overflow: hidden; }
362
+ .meter-fill { height: 100%; background: repeating-linear-gradient(90deg, var(--taught) 0 6px, rgba(0, 0, 0, .25) 6px 7px); }
363
+ .meter-fill.fly { background: repeating-linear-gradient(90deg, var(--fly) 0 6px, rgba(0, 0, 0, .25) 6px 7px); }
415
364
  .hud-empty { color: var(--muted); font-size: .85rem; }
416
365
  .chatlog { display: flex; flex-direction: column; gap: .4rem; max-height: 220px; overflow-y: auto; margin-bottom: .5rem; }
417
366
  .chatlog:empty { display: none; margin-bottom: 0; }
@@ -591,12 +540,25 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
591
540
  (function () {
592
541
  "use strict";
593
542
  const createTicker = ${createTicker.toString()};
594
- const classOfAgentId = ${classOfAgentId.toString()};
543
+ const loadWinkVendor = ${loadWinkVendor.toString()};
544
+ const agentKindOf = ${agentKindOf.toString()};
595
545
  const threadCellsForSpiderPlan = ${threadCellsForSpiderPlan.toString()};
596
546
  const facingDegreesFor = ${facingDegreesFor.toString()};
597
- const emotionFor = ${emotionFor.toString()};
547
+ const resolveSpriteRequest = ${resolveSpriteRequest.toString()};
598
548
  const nextCorpses = ${nextCorpses.toString()};
599
- const esc = ${escapeHtml.toString()};
549
+ const massScaleFor = ${massScaleFor.toString()};
550
+ const believedFactSentence = ${believedFactSentence.toString()};
551
+ const escapeHtml = ${escapeHtml.toString()};
552
+ const meterBarHtml = ${meterBarHtml.toString()};
553
+ const esc = escapeHtml;
554
+ // The lemma/POS tier every sibling dock loads: this game's OWN commands
555
+ // (tick, address, stop, see) are closed regexes that never need it, but a
556
+ // line that doesn't match one of those still falls through to the
557
+ // ordinary chat/ask/teach lanes (spiderFlyTurn returns null), and those
558
+ // lanes read exactly as much better with wink loaded as they do on every
559
+ // other page. Fired eagerly here so it is likely settled well before a
560
+ // visitor's first ordinary (non-game) question.
561
+ loadWinkVendor({ register: (factory) => tmct.page.registerWinkModel(factory) })();
600
562
  const el = (id) => document.getElementById(id);
601
563
  const boardFrame = el("boardFrame");
602
564
  const boardCanvas = el("board");
@@ -660,7 +622,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
660
622
  const goalById = {};
661
623
  const spriteEls = {};
662
624
  const facingByAgent = {};
663
- const emotionByAgent = {};
625
+ const moodByAgent = {};
664
626
  let corpses = {};
665
627
  const corpseEls = {};
666
628
  let selectedAddresseeId = null;
@@ -675,30 +637,37 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
675
637
  function removeStaleSprites(agents) {
676
638
  for (const id of Object.keys(spriteEls)) {
677
639
  if (!agents[id]) {
678
- spriteEls[id].remove(); delete spriteEls[id]; delete goalById[id]; delete facingByAgent[id]; delete emotionByAgent[id];
640
+ spriteEls[id].remove(); delete spriteEls[id]; delete goalById[id]; delete facingByAgent[id]; delete moodByAgent[id];
679
641
  }
680
642
  }
681
643
  }
682
644
 
683
- // maxMassFor/propertyFactsForAgent (emotionFor's own caller-side glue): only
684
- // spider/fly carry the mgx:feels parameter (data/sprites-large/*-with-
685
- // emotion.toml exists for those two classes only) every other class (egg,
686
- // and any class this page has no template for at all) keeps propertyFacts
687
- // empty, resolving through its plain template exactly as before this page
688
- // wired emotion through. maxMassFor mirrors massBarHtml's own per-class
689
- // denominator below so the HUD's mass bar and the sprite's own expression
690
- // read the same "how full" scale.
691
- function maxMassFor(cls) {
692
- return cls === "spider" ? SPIDERFLY.maxSpiderMass : cls === "fly" ? SPIDERFLY.maxFlyMass : null;
693
- }
694
- function propertyFactsForAgent(agent, cls) {
695
- if (cls !== "spider" && cls !== "fly") return [];
696
- return [{ predicate: "mgx:feels", object: emotionFor(agent, cls, maxMassFor(cls)) }];
645
+ // The engine writes each agent's mood as a real mgx:feels fact every turn
646
+ // and hands the same word back on the agent, so this page reads it rather
647
+ // than deriving a mood of its own. Only spider/fly carry the mgx:feels
648
+ // parameter (data/sprites-large/*-with-emotion.toml exists for those two
649
+ // classes only) every other class (egg, and any class this page has no
650
+ // template for at all) asks for no expression at all, resolving through its
651
+ // plain template. A snapshot-sourced agent carries no mood (a chat-driven
652
+ // tick returns text, not the structured tick shape), so the last mood the
653
+ // engine reported stands, exactly as goalById already holds the last goal.
654
+ // The starting board has no reported mood at all, and calm is the mood the
655
+ // engine itself writes for a just-minted agent, so that is the baseline.
656
+ function expressionForAgent(id, agent, cls) {
657
+ if (cls !== "spider" && cls !== "fly") return null;
658
+ return (agent && agent.mood) || moodByAgent[id] || "calm";
697
659
  }
698
- function resolveSpriteFace(cls, propertyFacts) {
699
- return window.tmctSpiderFly
700
- ? tmctSpiderFly.resolveSpriteAsset(cls, (session && session.taxonomyRows) || [], propertyFacts, SPIDERFLY.spriteTemplates, tmctSpiderFly.SPRITE_REGISTRY)
701
- : "";
660
+ // One sprite request, in the same { class, expression } shape tmct_sprite
661
+ // takes, answered by the same pure resolver the tool wraps (sprite-request.mjs,
662
+ // spliced in above) the page holds the state and nothing else.
663
+ function resolveSpriteFace(spriteRequest) {
664
+ if (!window.tmct) return "";
665
+ return resolveSpriteRequest(spriteRequest, {
666
+ factRows: (session && session.taxonomyRows) || [],
667
+ templates: SPIDERFLY.spriteTemplates,
668
+ spriteRegistry: tmct.page.SPRITE_REGISTRY,
669
+ resolveSpriteAsset: tmct.page.resolveSpriteAsset,
670
+ }).svg || "";
702
671
  }
703
672
 
704
673
  function togglePov(id) {
@@ -717,18 +686,18 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
717
686
  node.className = "sprite";
718
687
  node.dataset.cls = cls;
719
688
  // Property-aware resolution (sprite-templates.mjs's resolveSpriteAsset):
720
- // a spider/fly's live mgx:feels emotion (emotionFor, above) resolves it
721
- // through the matching data/sprites-large/*-with-emotion.toml template;
722
- // every other class (egg) keeps propertyFacts empty and resolves through
723
- // its plain class template (or the flat SPRITE_REGISTRY), unchanged.
724
- const propertyFacts = propertyFactsForAgent(agent, cls);
725
- emotionByAgent[id] = propertyFacts[0] ? propertyFacts[0].object : null;
689
+ // a spider/fly's live mgx:feels mood resolves it through the matching
690
+ // data/sprites-large/*-with-emotion.toml template; every other class
691
+ // (egg) asks for no expression and resolves through its plain class
692
+ // template (or the flat SPRITE_REGISTRY), unchanged.
693
+ const mood = expressionForAgent(id, agent, cls);
694
+ moodByAgent[id] = mood;
726
695
  // The sprite SVG lives in its own inner wrapper so plan-driven facing
727
696
  // (a CSS rotate on THIS wrapper) never fights the outer .sprite node's
728
697
  // own translate(-50%,-50%) positioning transform.
729
698
  const face = document.createElement("div");
730
699
  face.className = "sprite-face";
731
- face.innerHTML = resolveSpriteFace(cls, propertyFacts);
700
+ face.innerHTML = resolveSpriteFace({ class: cls, expression: mood });
732
701
  node.appendChild(face);
733
702
  if (!preview) {
734
703
  node.tabIndex = 0;
@@ -747,9 +716,9 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
747
716
  function applyAgents(agents) {
748
717
  removeStaleSprites(agents);
749
718
  for (const [id, a] of Object.entries(agents)) {
750
- const cls = classOfAgentId(id);
719
+ const cls = agentKindOf(id);
751
720
  const node = ensureSpriteEl(id, cls, a);
752
- const parsed = tmctSpiderFly.parseCellId(a.cell);
721
+ const parsed = tmct.page.parseCellId(a.cell);
753
722
  if (!parsed) continue;
754
723
  const pct = cellCenterPct(parsed.x, parsed.y);
755
724
  node.style.left = pct.leftPct + "%";
@@ -763,16 +732,15 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
763
732
  const face = node.querySelector(".sprite-face");
764
733
  if (face) {
765
734
  face.style.transform = "rotate(" + facingByAgent[id] + "deg)";
766
- // Emotion changes tick-to-tick (a fly's fear spikes the instant a
735
+ // Mood changes tick-to-tick (a fly's fear spikes the instant a
767
736
  // spider comes into view, a spider's joy fires the instant it eats),
768
737
  // so this must re-resolve every redraw, not just at sprite creation —
769
- // but only actually replace the DOM when the emotion word itself
738
+ // but only actually replace the DOM when the mood word itself
770
739
  // changed since last render, never on every tick regardless.
771
- const propertyFacts = propertyFactsForAgent(a, cls);
772
- const emotion = propertyFacts[0] ? propertyFacts[0].object : null;
773
- if (emotion !== emotionByAgent[id]) {
774
- emotionByAgent[id] = emotion;
775
- face.innerHTML = resolveSpriteFace(cls, propertyFacts);
740
+ const mood = expressionForAgent(id, a, cls);
741
+ if (mood !== moodByAgent[id]) {
742
+ moodByAgent[id] = mood;
743
+ face.innerHTML = resolveSpriteFace({ class: cls, expression: mood });
776
744
  }
777
745
  }
778
746
  if (a.goal) goalById[id] = a.goal;
@@ -805,12 +773,12 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
805
773
  // never emotion-parameterized at all (a rotting carcass has none),
806
774
  // just its class's own plain template under the grayscale/fade the
807
775
  // .sprite.corpse CSS rule already applies.
808
- face.innerHTML = resolveSpriteFace(corpse.cls, []);
776
+ face.innerHTML = resolveSpriteFace({ class: corpse.cls });
809
777
  node.appendChild(face);
810
778
  spriteLayer.appendChild(node);
811
779
  corpseEls[id] = node;
812
780
  }
813
- const deathCell = tmctSpiderFly.parseCellId(corpse.cell);
781
+ const deathCell = tmct.page.parseCellId(corpse.cell);
814
782
  if (!deathCell) continue;
815
783
  const pct = cellCenterPct(deathCell.x, SPIDERFLY.gridSize);
816
784
  node.style.left = pct.leftPct + "%";
@@ -818,13 +786,6 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
818
786
  }
819
787
  }
820
788
 
821
- function massBarHtml(cls, mass) {
822
- const maxMass = cls === "spider" ? SPIDERFLY.maxSpiderMass : cls === "fly" ? SPIDERFLY.maxFlyMass : null;
823
- if (typeof mass !== "number" || !maxMass) return "";
824
- const pct = Math.max(0, Math.min(100, (mass / maxMass) * 100));
825
- return '<div class="mass-track"><div class="mass-fill ' + esc(cls) + '" style="width:' + pct + '%"></div></div>';
826
- }
827
-
828
789
  // The last-created plan as a plain arrow chain ("west → west"), or
829
790
  // "holding." when the agent's plan this tick is empty — mirrors exactly
830
791
  // what drove this tick's sprite facing (facingDegreesFor reads the same
@@ -850,15 +811,15 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
850
811
 
851
812
  // The full text rendering behind the click-expand panel: one sentence per
852
813
  // other candidate the observer currently has a belief about, or "has not
853
- // been observed" for a candidate it doesn't — the same belief map
854
- // beliefLineHtml already compresses into a single line, spelled out in
855
- // full beside the clicked agent instead of abbreviated above it.
814
+ // been observed" for a candidate it doesn't — believedFactSentence is the
815
+ // SAME function spider-fly-turn.mjs's own chat lane renders "X sees: ..."
816
+ // from, so the chat phrasing and this panel can never drift apart.
817
+ // beliefLineHtml already compresses the same belief map into a single line
818
+ // above; this spells it out in full beside the clicked agent.
856
819
  function observedFactsHtml(observerId, belief) {
857
820
  const entries = Object.entries(belief || {});
858
821
  const lines = entries.length
859
- ? entries.map(([id, cell]) => '<div class="hud-detail-line">'
860
- + (cell ? esc(id) + " is at " + esc(cell) + "." : esc(id) + " has not been observed.")
861
- + "</div>").join("")
822
+ ? entries.map(([id, cell]) => '<div class="hud-detail-line">' + esc(believedFactSentence(id, cell)) + "</div>").join("")
862
823
  : '<div class="hud-detail-line">nothing else is on the board yet.</div>';
863
824
  return '<div class="hud-detail"><div class="hud-detail-title">' + esc(observerId) + " observes</div>" + lines + "</div>";
864
825
  }
@@ -867,13 +828,13 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
867
828
  const ids = Object.keys(lastAgents).sort();
868
829
  if (!ids.length) { hudEl.innerHTML = '<div class="hud-empty">no agents on the board.</div>'; return; }
869
830
  hudEl.innerHTML = ids.map((id) => {
870
- const cls = classOfAgentId(id);
831
+ const cls = agentKindOf(id);
871
832
  const a = lastAgents[id];
872
833
  const clickable = cls === "spider" || cls === "fly";
873
834
  const expanded = clickable && expandedAgents.has(id);
874
835
  const main = '<div class="hud-main"><span class="hud-id ' + esc(cls) + '">' + esc(id) + '</span>'
875
836
  + '<span class="hud-goal">' + esc(goalById[id] || "watching\\u2026") + "</span>"
876
- + massBarHtml(cls, a.mass)
837
+ + meterBarHtml(cls, a.mass, massScaleFor(cls, { maxSpiderMass: SPIDERFLY.maxSpiderMass, maxFlyMass: SPIDERFLY.maxFlyMass }))
877
838
  + planLineHtml(a.plan)
878
839
  + beliefLineHtml(a.belief)
879
840
  + "</div>";
@@ -902,9 +863,9 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
902
863
  });
903
864
 
904
865
  const threadGeometry = {
905
- parseCellId: (id) => tmctSpiderFly.parseCellId(id),
906
- cellId: (x, y) => tmctSpiderFly.cellId(x, y),
907
- directionDelta: tmctSpiderFly.DIRECTION_DELTA,
866
+ parseCellId: (id) => tmct.page.parseCellId(id),
867
+ cellId: (x, y) => tmct.page.cellId(x, y),
868
+ directionDelta: tmct.page.DIRECTION_DELTA,
908
869
  };
909
870
 
910
871
  function drawBoard(agents, activeWebs) {
@@ -912,7 +873,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
912
873
  boardCtx.clearRect(0, 0, w, h);
913
874
  boardCtx.fillStyle = cssVar("--taught-soft") || "rgba(46,125,79,.12)";
914
875
  for (const wc of SPIDERFLY.webCells) {
915
- const p = tmctSpiderFly.parseCellId(wc);
876
+ const p = tmct.page.parseCellId(wc);
916
877
  boardCtx.fillRect((p.x - 1) * cellSize, (p.y - 1) * cellSize, cellSize, cellSize);
917
878
  }
918
879
  // A spider-built dynamic web is a distinct color from the always-on
@@ -923,7 +884,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
923
884
  boardCtx.lineWidth = 1;
924
885
  boardCtx.setLineDash([3, 2]);
925
886
  for (const web of activeWebs || []) {
926
- const p = tmctSpiderFly.parseCellId(web.cell);
887
+ const p = tmct.page.parseCellId(web.cell);
927
888
  if (!p) continue;
928
889
  boardCtx.fillRect((p.x - 1) * cellSize, (p.y - 1) * cellSize, cellSize, cellSize);
929
890
  boardCtx.strokeRect((p.x - 1) * cellSize + 0.5, (p.y - 1) * cellSize + 0.5, cellSize - 1, cellSize - 1);
@@ -943,7 +904,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
943
904
  boardCtx.beginPath();
944
905
  let prev = null;
945
906
  thread.forEach((c, i) => {
946
- const p = tmctSpiderFly.parseCellId(c);
907
+ const p = tmct.page.parseCellId(c);
947
908
  const px = (p.x - 0.5) * cellSize, py = (p.y - 0.5) * cellSize;
948
909
  if (i === 0) boardCtx.moveTo(px, py); else boardCtx.lineTo(px, py);
949
910
  if (prev) threadHits.push({ x: (prev.x + px) / 2, y: (prev.y + py) / 2, step: i });
@@ -958,19 +919,19 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
958
919
  if (!povAgentId) return;
959
920
  const agent = lastAgents[povAgentId];
960
921
  if (!agent) { povAgentId = null; return; }
961
- const p = tmctSpiderFly.parseCellId(agent.cell);
922
+ const p = tmct.page.parseCellId(agent.cell);
962
923
  // The live, slider-adjustable per-class radius (falling back to the
963
924
  // engine's own shipped default before the session has finished
964
925
  // booting) — a POV toggle always reflects whatever vision range this
965
926
  // class currently actually has, not a fixed constant.
966
- const radius = classOfAgentId(povAgentId) === "spider"
967
- ? (liveConfig.spiderVisionRadius ?? tmctSpiderFly.DEFAULT_VISION_RADIUS)
968
- : (liveConfig.flyVisionRadius ?? tmctSpiderFly.DEFAULT_VISION_RADIUS);
969
- const visible = new Set(tmctSpiderFly.visibleCells(p.x, p.y, radius));
927
+ const radius = agentKindOf(povAgentId) === "spider"
928
+ ? (liveConfig.spiderVisionRadius ?? tmct.page.DEFAULT_VISION_RADIUS)
929
+ : (liveConfig.flyVisionRadius ?? tmct.page.DEFAULT_VISION_RADIUS);
930
+ const visible = new Set(tmct.page.visibleCells(p.x, p.y, radius));
970
931
  povCtx.fillStyle = "rgba(0,0,0,.55)";
971
932
  for (let gy = 1; gy <= SPIDERFLY.gridSize; gy += 1) {
972
933
  for (let gx = 1; gx <= SPIDERFLY.gridSize; gx += 1) {
973
- if (visible.has(tmctSpiderFly.cellId(gx, gy))) continue;
934
+ if (visible.has(tmct.page.cellId(gx, gy))) continue;
974
935
  povCtx.fillRect((gx - 1) * cellSize, (gy - 1) * cellSize, cellSize, cellSize);
975
936
  }
976
937
  }
@@ -1022,7 +983,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1022
983
  chatqEl.value = "";
1023
984
  addChatLine("u", esc(q));
1024
985
  withLock(async () => {
1025
- const result = await session.turn(q);
986
+ const result = await tmct.turn(q);
1026
987
  addChatLine("a", esc(result.answer).replace(/\\n/g, "<br>"));
1027
988
  const snap = await session.snapshot();
1028
989
  redraw(snap.agents, snap.turn, snap.activeWebs);
@@ -1066,14 +1027,14 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1066
1027
  refreshPills();
1067
1028
 
1068
1029
  // ---- deception pills (§A.2.4): a SEPARATE dynamic rail, alongside (never
1069
- // replacing) the static one above. tmctSpiderFly.pillsForSpiderFly is the
1030
+ // replacing) the static one above. tmct.page.pillsForSpiderFly is the
1070
1031
  // exact same pure function spider-fly-turn.mjs exports — this page never
1071
1032
  // reimplements the true/false claim logic, only renders its output and
1072
1033
  // fills #chatq on click, same click-to-fill discipline as every other
1073
1034
  // pill on this page (never auto-submits).
1074
1035
  function renderDynamicPills() {
1075
1036
  if (!session || !Object.keys(lastAgents).length) { dynamicPillsEl.innerHTML = ""; return; }
1076
- const result = tmctSpiderFly.pillsForSpiderFly(lastAgents, selectedAddresseeId, {});
1037
+ const result = tmct.page.pillsForSpiderFly(lastAgents, selectedAddresseeId, {});
1077
1038
  selectedAddresseeId = result.addresseeId;
1078
1039
  const addrHtml = result.addressPills.map((p) =>
1079
1040
  '<button type="button" class="pill" data-role="dyn-addr" data-id="' + esc(p.id) + '"'
@@ -1138,7 +1099,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
1138
1099
 
1139
1100
  let tuningInitialized = false;
1140
1101
  async function boot() {
1141
- session = await tmctSpiderFly.createSpiderFlySession();
1102
+ session = await tmct.open();
1142
1103
  // A reset mints a brand-new session (fresh board, fresh config) — the
1143
1104
  // FIRST boot seeds the sliders from the engine's own shipped defaults;
1144
1105
  // every boot after that re-applies whatever the visitor already had the