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

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 (60) hide show
  1. package/README.md +31 -18
  2. package/bin/tmct.mjs +3 -0
  3. package/data/templates/responses.jsonl +3 -0
  4. package/package.json +2 -1
  5. package/src/adapters/memory/core.mjs +1358 -196
  6. package/src/adapters/memory/inspect.mjs +11 -0
  7. package/src/adapters/memory/shacl.mjs +38 -0
  8. package/src/adapters/p2p/webrtc-transport.mjs +28 -5
  9. package/src/domain/ask-vocab.mjs +39 -0
  10. package/src/domain/ask.mjs +183 -34
  11. package/src/domain/grammar/assert.mjs +8 -2
  12. package/src/domain/hanoi-board.mjs +232 -0
  13. package/src/domain/ingest-facts.mjs +120 -0
  14. package/src/domain/interpret/normalize.mjs +49 -0
  15. package/src/domain/memory/compaction.mjs +284 -0
  16. package/src/domain/memory/resolution.mjs +171 -0
  17. package/src/domain/memory/trust.mjs +175 -5
  18. package/src/domain/memory-facts.mjs +139 -0
  19. package/src/domain/p2p/facts.mjs +21 -0
  20. package/src/domain/p2p/peer-id.mjs +15 -0
  21. package/src/domain/p2p/provenance-relabel.mjs +13 -2
  22. package/src/domain/p2p/sync-filter.mjs +5 -1
  23. package/src/domain/p2p/wire.mjs +7 -4
  24. package/src/domain/scene-compose.mjs +2 -2
  25. package/src/domain/sprite-facts.mjs +0 -0
  26. package/src/domain/sprite-request.mjs +1 -1
  27. package/src/services/adventure-viz.mjs +43 -39
  28. package/src/services/adventure.mjs +70 -44
  29. package/src/services/chat-page-viz.mjs +403 -332
  30. package/src/services/chat.mjs +274 -156
  31. package/src/services/code-explorer-viz.mjs +142 -55
  32. package/src/services/index.mjs +1 -1
  33. package/src/services/ingest-viz.mjs +153 -28
  34. package/src/services/ledger-viz.mjs +47 -47
  35. package/src/services/memory-panel-viz.mjs +8 -3
  36. package/src/services/mud-turn.mjs +11 -8
  37. package/src/services/mud-viz.mjs +474 -234
  38. package/src/services/p2p-room.mjs +110 -23
  39. package/src/services/plan-viz.mjs +72 -13
  40. package/src/services/research-viz.mjs +35 -24
  41. package/src/services/share-overlay-viz.mjs +623 -0
  42. package/src/services/spider-fly-viz.mjs +24 -24
  43. package/src/services/sprite-catalog-viz.mjs +307 -82
  44. package/src/surfaces/web/adventure-browser-entry.mjs +46 -25
  45. package/src/surfaces/web/chat-browser-entry.mjs +55 -9
  46. package/src/surfaces/web/code-explorer-browser-entry.mjs +30 -16
  47. package/src/surfaces/web/engine-surface.mjs +82 -0
  48. package/src/surfaces/web/ingest-browser-entry.mjs +81 -11
  49. package/src/surfaces/web/ledger-browser-entry.mjs +47 -14
  50. package/src/surfaces/web/memory-ask-browser-entry.mjs +55 -13
  51. package/src/surfaces/web/memory-ask-browser.bundle.js +149 -116
  52. package/src/surfaces/web/mud-browser-entry.mjs +77 -25
  53. package/src/surfaces/web/p2p-browser-entry.mjs +1 -1
  54. package/src/surfaces/web/plan-browser-entry.mjs +49 -11
  55. package/src/surfaces/web/research-browser-entry.mjs +33 -24
  56. package/src/surfaces/web/spider-fly-browser-entry.mjs +32 -13
  57. package/src/surfaces/web/sprites-browser-entry.mjs +51 -11
  58. package/src/surfaces/web/tmct-surface.mjs +159 -0
  59. package/src/surfaces/web/turn-session.mjs +16 -5
  60. package/src/tools/server.mjs +13 -6
@@ -32,6 +32,7 @@ import {
32
32
  createInMemoryStore, appendFacts, appendRule, loadMemory, readFactRows, removeFacts,
33
33
  } from "../../adapters/memory/core.mjs";
34
34
  import { parseEntities } from "../../domain/codegraph.mjs";
35
+ import { memoryFactGraphPayload } from "../../domain/memory-facts.mjs";
35
36
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
36
37
  import {
37
38
  foldWorldState, worldActionRows, worldDigestRows, roomAffordances,
@@ -48,6 +49,8 @@ import { worldProvenanceTag } from "../../domain/worlds-pack.mjs";
48
49
  import { resolveSpriteForClass, SPRITE_REGISTRY, classAncestorChain } from "../../domain/sprite-map.mjs";
49
50
  import { resolveSpriteAsset } from "../../domain/sprite-templates.mjs";
50
51
  import { createTurnSession } from "./turn-session.mjs";
52
+ import { publishTmctSurface } from "./tmct-surface.mjs";
53
+ import { graphAsk, enginePlan } from "./engine-surface.mjs";
51
54
 
52
55
  /** A live, shared mud world several characters can each act in. `worldPayload`
53
56
  * is `{ name, facts, rules, opening }` — the same shape adventure-browser-
@@ -60,7 +63,10 @@ import { createTurnSession } from "./turn-session.mjs";
60
63
  * are minted, fewer leaves the ones nobody is playing out of the world
61
64
  * altogether.
62
65
  *
63
- * Returns `{ memoryDir, windows, snapshot }`. `windows` is a plain object
66
+ * Returns `{ memoryDir, codeGraph, graph, refreshGraph, windows, snapshot }`.
67
+ * `codeGraph` is the known-empty index the turn engine and a scripted
68
+ * autoplayTick read; `graph` is this world's own memory store projected for
69
+ * `ask()` (refreshGraph rebuilds it on demand). `windows` is a plain object
64
70
  * keyed by character id, each value `{ character, turn, autoplayTick,
65
71
  * visitedRoomIds, turnsTaken, isOutOfPlay, outOfPlayReason }`. `snapshot()` is
66
72
  * the one OMNISCIENT read this module exposes — the central world map's own
@@ -87,7 +93,11 @@ export async function createMudSession(worldPayload, { characters = [], epoch =
87
93
  // way rather than through the "play <world>" opener (which needs a
88
94
  // shipped "player" individual mud-garden deliberately has none of).
89
95
  const planHolder = { state: { adventure: { world: worldPayload.name } } };
90
- const graph = parseEntities({ individuals: [], objectProperties: [] });
96
+ // A known-empty code graph: code-structure questions get the same honest
97
+ // no-code-graph answer an un-pointed CLI session gives, never a crash — the
98
+ // turn engine (and a scripted autoplayTick) keep reading THIS one for their
99
+ // own in-turn code lane.
100
+ const codeGraph = parseEntities({ individuals: [], objectProperties: [] });
91
101
  // The world's own minted ids ("groundhog-1", "carrot-2") are declared as
92
102
  // vocabulary inside the adventure lane itself, for the length of one world
93
103
  // command — see adventure.mjs's own worldLexicon. This page hands over the
@@ -99,6 +109,17 @@ export async function createMudSession(worldPayload, { characters = [], epoch =
99
109
  return { rows, state: foldWorldState(worldActionRows(rows)) };
100
110
  }
101
111
 
112
+ // What `tmct.ask()` traverses is a different graph: this shared world's own
113
+ // memory store, projected through memoryFactGraphPayload. Rebuilt on demand
114
+ // rather than once at open, because every character's turn grows the store.
115
+ // Built off readWorld()'s own rows rather than a second readFactRows call.
116
+ let memoryGraph = parseEntities({ individuals: [], objectProperties: [] });
117
+ async function refreshGraph() {
118
+ const { rows } = await readWorld();
119
+ memoryGraph = parseEntities(memoryFactGraphPayload(rows));
120
+ return memoryGraph;
121
+ }
122
+
102
123
  async function roomOf(character) {
103
124
  const { state } = await readWorld();
104
125
  return state.placements.get(character)?.object ?? null;
@@ -124,10 +145,10 @@ export async function createMudSession(worldPayload, { characters = [], epoch =
124
145
  if (startRoom) visitedRoomIds.add(startRoom);
125
146
 
126
147
  const turnSession = createTurnSession({
127
- memoryDir, graph, lexicon, sessionId: character,
148
+ memoryDir, graph: codeGraph, lexicon, sessionId: character,
128
149
  vocabHint: 'Try a world command ("dig north", "eat the carrot-1"), or ask "what food do you know about".',
129
150
  buildExtraOptions: () => ({
130
- uiContext: "browser", actingSubject: character, planState: planHolder.state,
151
+ actingSubject: character, planState: planHolder.state,
131
152
  }),
132
153
  captureExtraState: async (result, state) => {
133
154
  if ("planState" in result) planHolder.state = state.planState;
@@ -156,7 +177,7 @@ export async function createMudSession(worldPayload, { characters = [], epoch =
156
177
  * note }` unmodified, so the caller can render the speech-bubble/
157
178
  * dig-flourish triggers straight off `actions`. */
158
179
  async autoplayTick(k) {
159
- const result = await runMudTurn(character, { world: worldPayload.name, memoryDir, env: {}, graph, k });
180
+ const result = await runMudTurn(character, { world: worldPayload.name, memoryDir, env: {}, graph: codeGraph, k });
160
181
  // A turn that ended in starvation still happened, and still counts —
161
182
  // only a DECLINED turn (no room to act in) leaves the tally alone.
162
183
  if (result.room) turnsTaken += 1;
@@ -270,7 +291,13 @@ export async function createMudSession(worldPayload, { characters = [], epoch =
270
291
  return claims.length;
271
292
  }
272
293
 
273
- return { memoryDir, windows, snapshot, applyEdit, wave, claimCharacters };
294
+ return {
295
+ memoryDir,
296
+ codeGraph,
297
+ get graph() { return memoryGraph; },
298
+ refreshGraph,
299
+ windows, snapshot, applyEdit, wave, claimCharacters,
300
+ };
274
301
  }
275
302
 
276
303
  /** `count` entries drawn at random from `roster`, in random order, without
@@ -351,22 +378,47 @@ export function worldFactsForCast(facts, characters) {
351
378
  return rows.filter((f) => !uncast.has(f.subject)).concat(mintedCharacterFacts(rows, characters));
352
379
  }
353
380
 
354
- // Re-exported so mud-viz.mjs's own inlined script never duplicates sprite
355
- // resolution or the digest/affordance/knowledge readers its room view and
356
- // chat pills already need the same reach-through-the-global posture
357
- // adventure-browser-entry.mjs's own globalThis.tmctAdventure takes.
358
- globalThis.tmctMud = {
359
- createMudSession, pickMudRoster, expandMudRoster, mintedCharacterFacts, worldFactsForCast,
360
- resolveSpriteForClass, SPRITE_REGISTRY, classAncestorChain, resolveSpriteAsset,
361
- foldWorldState, worldActionRows, worldDigestRows, roomAffordances,
362
- personKnowledgeLines, personKnownFoodLines,
363
- diggableDirections, castInRoom, displayNameOf, isOutOfPlay, outOfPlayReasonOf, outOfPlayPhrase,
364
- roomKindOf,
365
- // The shared-world reach-throughs: which predicates carry live world state,
366
- // and the P2P layer's own four. mud.html hands both to `mudSyncableFacts`,
367
- // which is written to take the check rather than import the engine itself.
368
- isMudStatePredicate, P2P_PREDICATES,
369
- // The edit mode's own reach-throughs: the SKOS neighbourhood and the is-a
370
- // chain its cursor-suggestion pills read, neither of which is splice-safe.
371
- relatedForTerm,
372
- };
381
+ // Several characters, one world, so a line has to say who is speaking:
382
+ // `tmct.turn(line, { as: "mole-1" })` routes to that character's own window,
383
+ // which owns its private focus/last and its own fog of war. Everything else a
384
+ // window does a scripted autoplayTick, its visited rooms, whether it is
385
+ // still in play — stays on `tmct.session.windows[id]`, because those are
386
+ // per-character state rather than questions anyone could ask in words.
387
+ //
388
+ // `tmct.page` keeps the sprite resolution and the digest/affordance/knowledge
389
+ // readers the room view and chat pills render from, the roster helpers that
390
+ // decide which animals this visit is played with, the two predicate lists the
391
+ // P2P layer checks against, and the SKOS neighbourhood behind the edit mode's
392
+ // cursor pills none of which is `.toString()`-splice-safe.
393
+ publishTmctSurface({
394
+ open: createMudSession,
395
+ turn: (line, options, session) => {
396
+ const character = options?.as;
397
+ const characterWindow = character ? session.windows[character] : null;
398
+ if (!characterWindow) {
399
+ return {
400
+ answer: `say which character is speaking — tmct.turn(line, { as: "${Object.keys(session.windows)[0] || "mole-1"}" })`,
401
+ end: false, record: null, plan: null, research: null,
402
+ };
403
+ }
404
+ return characterWindow.turn(line);
405
+ },
406
+ // The memory projection is rebuilt first, so a direct tmct.ask() sees every
407
+ // fact any character's turn has written into this shared world since the
408
+ // last one.
409
+ ask: async (request, options, session) => {
410
+ await session.refreshGraph();
411
+ return graphAsk(request, options, session);
412
+ },
413
+ plan: enginePlan,
414
+ page: {
415
+ pickMudRoster, expandMudRoster, mintedCharacterFacts, worldFactsForCast,
416
+ resolveSpriteForClass, SPRITE_REGISTRY, classAncestorChain, resolveSpriteAsset,
417
+ foldWorldState, worldActionRows, worldDigestRows, roomAffordances,
418
+ personKnowledgeLines, personKnownFoodLines,
419
+ diggableDirections, castInRoom, displayNameOf, isOutOfPlay, outOfPlayReasonOf, outOfPlayPhrase,
420
+ roomKindOf,
421
+ isMudStatePredicate, P2P_PREDICATES,
422
+ relatedForTerm,
423
+ },
424
+ });
@@ -26,7 +26,7 @@ export {
26
26
  ROOM_FAILED,
27
27
  } from "../../services/p2p-room.mjs";
28
28
  export { createTransport } from "../../adapters/p2p/webrtc-transport.mjs";
29
- export { generatePeerId, generateWorldId, generateDisplayName } from "../../domain/p2p/peer-id.mjs";
29
+ export { generatePeerId, generateWorldId, generateDisplayName, generateNodeId } from "../../domain/p2p/peer-id.mjs";
30
30
  export { chatSyncableFacts, mudSyncableFacts } from "../../domain/p2p/sync-filter.mjs";
31
31
  export { decodeInviteBlob, encodeInviteBlob } from "../../domain/p2p/wire.mjs";
32
32
  export {
@@ -29,12 +29,13 @@ import { parseEntities } from "../../domain/codegraph.mjs";
29
29
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
30
30
  import { DEFAULT_GAME_CONFIG } from "../../domain/game-config.mjs";
31
31
  import { hanoiLessonSentences } from "../../domain/hanoi-lesson.mjs";
32
+ import { hanoiBoardRows, hanoiBoardGraphPayload } from "../../domain/hanoi-board.mjs";
32
33
  import { computeBlocksLayout, planToPageData, renderInputsFromPlan } from "../../services/plan-viz.mjs";
33
34
  import { planToPddl } from "../../services/plan-pddl.mjs";
34
35
  import { createTurnSession } from "./turn-session.mjs";
35
36
  // Re-exported so the page can register a CDN-loaded wink-nlp pair before the
36
37
  // first teach, the same seam chat-browser-entry.mjs exposes as
37
- // tmctChat.registerWinkModel — see wink-model.mjs's own header. The hanoi
38
+ // tmct.page.registerWinkModel — see wink-model.mjs's own header. The hanoi
38
39
  // lesson's own "moving a disk onto a target makes the disk rest on the
39
40
  // target" sentence needs a REAL lemmatiser (verbLemma reduces "moving" to
40
41
  // "move" to match the taught "move onto" action family) — without it, that
@@ -44,10 +45,12 @@ import { createTurnSession } from "./turn-session.mjs";
44
45
  // model because their own gameplay never asks a taught rule to reduce a
45
46
  // verb; the hanoi lesson is the first live session here that does.
46
47
  import { registerWinkModel } from "../../adapters/wink-model.mjs";
48
+ import { publishTmctSurface } from "./tmct-surface.mjs";
49
+ import { graphAsk, enginePlan } from "./engine-surface.mjs";
47
50
 
48
51
  /** A live in-memory towers-of-hanoi session this page's live controls AND
49
- * chat dock can both drive. Returns `{ memoryDir, sessionId, diskCount,
50
- * maxDepth, plan, turn }`. `plan` is the puzzle's freshly solved plan (the
52
+ * chat dock can both drive. Returns `{ memoryDir, sessionId, graph,
53
+ * diskCount, maxDepth, plan, turn }`. `plan` is the puzzle's freshly solved plan (the
51
54
  * same shape chat.mjs's planLaneAnswer returns, enriched with
52
55
  * `becauseText` — see `turn()` below), or null when `maxDepth` was too low
53
56
  * to find one (an honest miss, not an error: `turn()`'s own answer text
@@ -84,13 +87,48 @@ export async function createPlanSession({ diskCount = 3, maxDepth = DEFAULT_GAME
84
87
  if (r.plan) plan = r.plan;
85
88
  }
86
89
 
87
- return { memoryDir, sessionId, diskCount, maxDepth, plan, turn: session.turn };
90
+ // The board `tmct.ask()` traverses. The plan lane's own states are rows, not
91
+ // a graph, so an ask over this session used to meet an empty one and miss
92
+ // every question about the puzzle in front of the visitor. hanoi-board.mjs
93
+ // projects one position into `{individuals, objectProperties}`; the page
94
+ // calls `showBoard` whenever it mounts a fresh plan or the transport moves
95
+ // the step, so what ask() reads is what the board shows.
96
+ let boardPlan = plan;
97
+ let boardStep = 0;
98
+ let boardGraph = null;
99
+ function showBoard({ plan: nextPlan = boardPlan, step = boardStep } = {}) {
100
+ boardPlan = nextPlan;
101
+ boardStep = Math.max(0, Math.floor(Number(step) || 0));
102
+ boardGraph = parseEntities(hanoiBoardGraphPayload(hanoiBoardRows({ plan: boardPlan, step: boardStep })));
103
+ return boardGraph;
104
+ }
105
+ showBoard();
106
+
107
+ return {
108
+ memoryDir, sessionId, graph, diskCount, maxDepth, plan, turn: session.turn,
109
+ showBoard,
110
+ get boardGraph() { return boardGraph; },
111
+ get boardStep() { return boardStep; },
112
+ };
88
113
  }
89
114
 
90
- // Re-exported so the page's own rendering script (plan-viz.mjs's inlined
91
- // script) never has to duplicate board layout or PDDL/OWL-RDF formatting
92
- // the same posture adventure-browser-entry.mjs/spider-fly-browser-entry.mjs
93
- // take re-exporting their own engines' pure helpers.
94
- globalThis.tmctPlan = {
95
- createPlanSession, computeBlocksLayout, planToPageData, renderInputsFromPlan, planToPddl, registerWinkModel,
96
- };
115
+ // `tmct.page` keeps the board layout and the PDDL/OWL-RDF formatting the
116
+ // page's own script draws with, plus the wink seam the hanoi lesson needs
117
+ // registered before its first teach. Note that `tmct.plan(...)` here is the
118
+ // CAPABILITY planner, not the puzzle solver: a typed "solve it" is a
119
+ // conversational turn like any other, so the page reaches the hanoi plan
120
+ // through `tmct.turn("solve it", { maxDepth })` and reads `.plan` off it.
121
+ //
122
+ // `tmct.ask(q, { step })` answers over the projected BOARD rather than the
123
+ // session's own (empty) code graph, so a question about the puzzle is grounded
124
+ // in the position on screen. Passing `step` moves the board first, which is
125
+ // how the page keeps the two in step while the transport scrubs.
126
+ publishTmctSurface({
127
+ open: createPlanSession,
128
+ ask: (request, options, session) => {
129
+ if (options?.step != null) session.showBoard({ step: options.step });
130
+ return graphAsk(request, options, { graph: session.boardGraph, memoryDir: session.memoryDir });
131
+ },
132
+ plan: enginePlan,
133
+ page: { computeBlocksLayout, planToPageData, renderInputsFromPlan, planToPddl, registerWinkModel },
134
+ });
@@ -39,15 +39,10 @@ import { groundTextToFacts } from "./ingest-browser-entry.mjs";
39
39
  import { openPersistedStore } from "./idb-persist.mjs";
40
40
  import { digestTermFromPayloadBrowser } from "./digest-client.mjs";
41
41
  import { createTurnSession } from "./turn-session.mjs";
42
+ import { publishTmctSurface } from "./tmct-surface.mjs";
43
+ import { enginePlan } from "./engine-surface.mjs";
42
44
  import { exportFactsJsonl } from "./memory-stats.mjs";
43
45
 
44
- // The Fact individual's first-write-wins timestamp, read straight off the
45
- // stored attribute (mgx:createdAt) so a "recently learned" ordering never
46
- // has to re-parse a provenance tag — the research lane's own tags carry a
47
- // depth, not a timestamp, so the attribute is the one field every source
48
- // shares.
49
- const CREATED_AT_ATTR = "mgx:createdAt";
50
-
51
46
  /**
52
47
  * The source key ONE provenance tag folds to, for the page's per-source
53
48
  * checkboxes/history. `sessionIds` tells a teach tag apart: a teach:chat tag
@@ -100,18 +95,17 @@ const clonePayload = (payload) => {
100
95
  try { return structuredClone(payload); } catch { return JSON.parse(JSON.stringify(payload)); }
101
96
  };
102
97
 
103
- /** Fact rows plus the createdAt attribute readFactRows drops, in one pass over
104
- * the loaded memory — the "recently learned" panels want the timestamp, the
105
- * ask filter wants the id, both want the provenance. */
98
+ /** Fact rows plus the createdAt readFactRows keeps per assertion rather than per
99
+ * row — the "recently learned" panels want the timestamp, the ask filter wants
100
+ * the id, both want the provenance. A triple asserted by several sources was
101
+ * first learned when the EARLIEST of them said it, which is the moment those
102
+ * panels are ordering by. */
106
103
  async function factRowsWithCreatedAt(memoryDir) {
107
104
  const memory = await loadMemory(memoryDir);
108
- const createdById = new Map();
109
- for (const ind of memory?.individuals || []) {
110
- if (ind?.class !== "Fact") continue;
111
- const at = (ind.attributes || []).find((a) => a?.prop === CREATED_AT_ATTR || a?.key === CREATED_AT_ATTR)?.value || "";
112
- createdById.set(ind.id, at);
113
- }
114
- return readFactRows(memory).map((row) => ({ ...row, createdAt: createdById.get(row.id) || "" }));
105
+ return readFactRows(memory).map((row) => {
106
+ const stamps = (row.assertions || []).map((a) => a.createdAt).filter(Boolean).sort();
107
+ return { ...row, createdAt: stamps[0] || "" };
108
+ });
115
109
  }
116
110
 
117
111
  const SOURCE_ORDER = ["taught", "ingest", "research"];
@@ -210,7 +204,7 @@ export function createResearchSession({ seedPayload = null, vocabSeeded = false,
210
204
 
211
205
  const graph = parseEntities({ individuals: [], objectProperties: [] });
212
206
  const lexicon = loadLexicon();
213
- const vocabHint = vocabExampleHint(vocabSeeded);
207
+ const vocabHint = vocabExampleHint(vocabSeeded, "browser");
214
208
  // A DISTINCT id so an ingested fact's teach tag is told apart from a typed
215
209
  // teach turn's by session id alone — the whole reason the two growth paths
216
210
  // stay separable in the source panel.
@@ -234,7 +228,7 @@ export function createResearchSession({ seedPayload = null, vocabSeeded = false,
234
228
  sessionId: globalThis.crypto?.randomUUID?.() ?? String(Date.now()),
235
229
  buildExtraOptions: () => ({
236
230
  researchConfig, liveReference: liveReferenceOn, onLiveLookup,
237
- uiContext: "browser", synthesisBudget: synthesisBudgetOn,
231
+ synthesisBudget: synthesisBudgetOn,
238
232
  }),
239
233
  captureExtraState: (result) => {
240
234
  if (typeof result.liveReference === "boolean" || result.liveReference === "supplement" || result.liveReference === "always") {
@@ -247,6 +241,7 @@ export function createResearchSession({ seedPayload = null, vocabSeeded = false,
247
241
 
248
242
  return {
249
243
  memoryDir,
244
+ graph,
250
245
  chatSessionId,
251
246
  ingestSessionId,
252
247
  sessionIds,
@@ -327,8 +322,22 @@ export function createResearchSession({ seedPayload = null, vocabSeeded = false,
327
322
  };
328
323
  }
329
324
 
330
- globalThis.tmctResearch = {
331
- createResearchSession, researchSnapshot, exportFactsJsonl,
332
- registerWinkModel, registerReferencePackProvider, registerLiveReferenceProvider, registerResearchProvider,
333
- normFactTerm, vocabExampleHint, openPersistedStore,
334
- };
325
+ // This page's ask is already source-scoped: `tmct.ask(q, { sources })` keeps
326
+ // the checked source keys and runs the same factAnswer/factReadBack cascade
327
+ // the ledger dock runs, so a visitor can see the answer change as they tick
328
+ // sources off. `tmct.page` keeps the provider and wink seams the page
329
+ // registers, the run snapshot its timeline reads, its persisted store and its
330
+ // JSONL export.
331
+ publishTmctSurface({
332
+ open: createResearchSession,
333
+ ask: async (request, options, session) => {
334
+ const { text, miss } = await session.ask(request, options);
335
+ return { answer: text, data: null, miss };
336
+ },
337
+ plan: enginePlan,
338
+ page: {
339
+ researchSnapshot, exportFactsJsonl,
340
+ registerWinkModel, registerReferencePackProvider, registerLiveReferenceProvider, registerResearchProvider,
341
+ normFactTerm, vocabExampleHint, openPersistedStore,
342
+ },
343
+ });
@@ -44,6 +44,8 @@
44
44
  // them back (hand-written pathfinding over has-exit-* facts, not the taught
45
45
  // action-rule DSL), so nothing here depends on them being loaded.
46
46
  import { createTurnSession } from "./turn-session.mjs";
47
+ import { publishTmctSurface } from "./tmct-surface.mjs";
48
+ import { graphAsk, enginePlan } from "./engine-surface.mjs";
47
49
  import { registerWinkModel } from "../../adapters/wink-model.mjs";
48
50
  import {
49
51
  createInMemoryStore, normFactTerm, appendFacts, loadMemory, readFactRows,
@@ -137,6 +139,16 @@ export async function createSpiderFlySession({ flyCount = 1 } = {}) {
137
139
  initial: { turn: 0, agents: initialAgents, activeWebs: [] },
138
140
  taxonomyRows,
139
141
 
142
+ /** The board as a graph, as of the last refresh. Every tick moves the
143
+ * pieces, so a caller putting a question to it calls `refreshGraph()`
144
+ * first — which is exactly what `turn()` below already does. */
145
+ get graph() { return graph; },
146
+
147
+ /** Rebuild the board graph from the store's current rows. Exposed so a
148
+ * question asked outside the chat dock (tmct.ask) reads this turn's
149
+ * positions and not last turn's, the same way a typed one does. */
150
+ refreshGraph: refreshWorldGraph,
151
+
140
152
  /** Run one real engine turn directly. Returns spider-fly.mjs's own
141
153
  * { turn, agents, ecology } shape unmodified. */
142
154
  async tick() {
@@ -189,16 +201,23 @@ export async function createSpiderFlySession({ flyCount = 1 } = {}) {
189
201
  };
190
202
  }
191
203
 
192
- // cellId/parseCellId/DIRECTION_DELTA/visibleCells/DEFAULT_VISION_RADIUS are
193
- // re-exported so the page's own rendering script (spider-fly-viz.mjs) never
194
- // has to duplicate grid geometry or the vision-radius default: reconstructing
195
- // a spider's remaining silk-thread path from its returned direction list, and
196
- // computing the POV overlay's visible-cell mask, both need them.
197
- // pillsForSpiderFly/oneStepDirectionBetween are re-exported so the same page
198
- // can build its own dynamic deception-pill container without duplicating
199
- // spider-fly-turn.mjs's own pill logic.
200
- globalThis.tmctSpiderFly = {
201
- createSpiderFlySession, normFactTerm, resolveSpriteForClass, SPRITE_REGISTRY, resolveSpriteAsset,
202
- cellId, parseCellId, DIRECTION_DELTA, visibleCells, DEFAULT_VISION_RADIUS,
203
- pillsForSpiderFly, oneStepDirectionBetween, DEFAULT_GAME_CONFIG, registerWinkModel,
204
- };
204
+ // `tmct.page` is grid geometry and sprite resolution — the two things on this
205
+ // page the engine has no plain-English form for. Reconstructing a spider's
206
+ // remaining silk-thread path from its returned direction list and masking the
207
+ // POV overlay's visible cells both need the raw cell math; the deception pills
208
+ // need spider-fly-turn.mjs's own pill logic.
209
+ publishTmctSurface({
210
+ open: createSpiderFlySession,
211
+ // The board moves every tick, so a question rebuilds the graph first —
212
+ // the same refresh a typed turn does before it dispatches.
213
+ ask: async (request, options, session) => {
214
+ await session.refreshGraph();
215
+ return graphAsk(request, options, session);
216
+ },
217
+ plan: enginePlan,
218
+ page: {
219
+ normFactTerm, resolveSpriteForClass, SPRITE_REGISTRY, resolveSpriteAsset,
220
+ cellId, parseCellId, DIRECTION_DELTA, visibleCells, DEFAULT_VISION_RADIUS,
221
+ pillsForSpiderFly, oneStepDirectionBetween, DEFAULT_GAME_CONFIG, registerWinkModel,
222
+ },
223
+ });
@@ -18,38 +18,78 @@
18
18
  // a typed class name through ask.mjs's resolveObject, so it needs the real
19
19
  // resolver in the page rather than a self-contained function the page could
20
20
  // splice in as text.
21
- import { createInMemoryStore, appendFacts, normFactTerm } from "../../adapters/memory/core.mjs";
21
+ //
22
+ // `tmct.ask()` needs a real graph to traverse (engine-surface.mjs's graphAsk
23
+ // dispatches tmct_ask over `session.graph`), so this session projects its own
24
+ // store into one via spriteFactGraphPayload — sprite-facts.mjs's own
25
+ // counterpart to spider-fly-browser-entry.mjs's worldRelationGraphPayload
26
+ // use. `refreshGraph()` rebuilds it from the store's CURRENT rows (not just
27
+ // the embedded factRows), so a fact taught mid-session is visible to a later
28
+ // ask() the same way spider-fly's board rebuild is; `turn()` and the
29
+ // published `ask` route both call it first, mirroring spider-fly's exact
30
+ // wiring points.
31
+ import { createInMemoryStore, appendFacts, normFactTerm, loadMemory, readFactRows } from "../../adapters/memory/core.mjs";
22
32
  import { extractSceneItems } from "../../domain/scene-compose.mjs";
23
33
  import { parseEntities } from "../../domain/codegraph.mjs";
24
34
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
25
- import { SPRITE_FACTS_PROVENANCE } from "../../domain/sprite-facts.mjs";
35
+ import { SPRITE_FACTS_PROVENANCE, spriteFactGraphPayload } from "../../domain/sprite-facts.mjs";
26
36
  import { registerWinkModel } from "../../adapters/wink-model.mjs";
27
37
  import { createTurnSession } from "./turn-session.mjs";
38
+ import { publishTmctSurface } from "./tmct-surface.mjs";
39
+ import { graphAsk, enginePlan } from "./engine-surface.mjs";
28
40
 
29
41
  /** A live in-memory chat session seeded with the embedded sprite-facts rows.
30
- * Returns { memoryDir, sessionId, factCount, turn }. */
42
+ * Returns { memoryDir, sessionId, graph, refreshGraph, factCount, turn }. */
31
43
  export async function createSpriteCatalogSession({ factRows = [] } = {}) {
32
44
  const memoryDir = createInMemoryStore();
33
45
  await appendFacts(memoryDir, factRows.map((f) => ({
34
46
  subject: f.subject, predicate: f.predicate, object: f.object, provenance: SPRITE_FACTS_PROVENANCE,
35
47
  })));
36
48
 
37
- const graph = parseEntities({ individuals: [], objectProperties: [] });
49
+ let graph = parseEntities({ individuals: [], objectProperties: [] });
50
+ async function refreshGraph() {
51
+ const rows = readFactRows(await loadMemory(memoryDir));
52
+ graph = parseEntities(spriteFactGraphPayload(rows));
53
+ }
54
+ await refreshGraph();
55
+
38
56
  const lexicon = loadLexicon();
39
57
  const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
40
58
 
41
- const session = createTurnSession({ memoryDir, graph, lexicon, sessionId, vocabHint: "" });
59
+ // `graph` is re-read here, not captured once: createTurnSession binds its
60
+ // own copy at creation, and refreshGraph() reassigns this closure's own
61
+ // variable on every call.
62
+ const turnSession = createTurnSession({
63
+ memoryDir, graph, lexicon, sessionId, vocabHint: "",
64
+ buildExtraOptions: () => ({ graph }),
65
+ });
42
66
 
43
67
  return {
44
68
  memoryDir,
45
69
  sessionId,
70
+ get graph() { return graph; },
71
+ refreshGraph,
46
72
  factCount: factRows.length,
47
- turn: session.turn,
73
+ async turn(line) {
74
+ await refreshGraph();
75
+ return turnSession.turn(line);
76
+ },
48
77
  };
49
78
  }
50
79
 
51
- // registerWinkModel is re-exported so the page's own inline script can hand in
52
- // the self-hosted wink pair (./vendor/wink.js) exactly the way chat.html/
53
- // ledger.html/plan.html register theirs — the bundle itself never imports
54
- // wink-nlp (wink-model.mjs's own header explains why).
55
- globalThis.tmctSprites = { createSpriteCatalogSession, registerWinkModel, normFactTerm, extractSceneItems };
80
+ // `tmct.page` keeps the wink seam (the page hands in the self-hosted pair from
81
+ // ./vendor/wink.js, exactly the way chat.html/ledger.html/plan.html register
82
+ // theirs — the bundle itself never imports wink-nlp), the term normalizer, and
83
+ // the scene composer's parser, which reads a typed line into drawable items
84
+ // rather than answering anything.
85
+ publishTmctSurface({
86
+ open: createSpriteCatalogSession,
87
+ // The graph is rebuilt first, so a direct tmct.ask() call (not just a
88
+ // typed chat turn) sees any fact taught since the last one.
89
+ ask: async (request, options, session) => {
90
+ await session.refreshGraph();
91
+ return graphAsk(request, options, session);
92
+ },
93
+ plan: enginePlan,
94
+ page: { registerWinkModel, normFactTerm, extractSceneItems },
95
+ });