@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
@@ -35,10 +35,13 @@
35
35
  // game-shaped falls through to the ordinary conversational layer, exactly
36
36
  // like a real CLI session.
37
37
  import { createTurnSession } from "./turn-session.mjs";
38
+ import { publishTmctSurface } from "./tmct-surface.mjs";
39
+ import { graphAsk, enginePlan } from "./engine-surface.mjs";
38
40
  import {
39
41
  createInMemoryStore, appendFacts, appendRule, loadMemory, readFactRows, removeFacts,
40
42
  } from "../../adapters/memory/core.mjs";
41
43
  import { parseEntities } from "../../domain/codegraph.mjs";
44
+ import { memoryFactGraphPayload } from "../../domain/memory-facts.mjs";
42
45
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
43
46
  import { foldWorldState, worldDigestRows, roomAffordances, worldActionRows } from "../../services/adventure.mjs";
44
47
  import { runAdventureAutoplayTick, exposedFacts } from "../../services/adventure-autoplay.mjs";
@@ -101,7 +104,21 @@ export async function createAdventureSession(worldPayload, { restoredPayload = n
101
104
  const openingHere = foldWorldState(worldActionRows(openingRows)).placements.get("player")?.object ?? null;
102
105
  if (openingHere) visitedRoomIds.add(openingHere);
103
106
 
104
- const graph = parseEntities({ individuals: [], objectProperties: [] });
107
+ // A known-empty code graph: code-structure questions get the same honest
108
+ // no-code-graph answer an un-pointed CLI session gives, never a crash — the
109
+ // turn engine keeps reading THIS one for its own in-turn code lane.
110
+ const codeGraph = parseEntities({ individuals: [], objectProperties: [] });
111
+
112
+ // What `tmct.ask()` traverses is a different graph: this world's own memory
113
+ // store, projected through memoryFactGraphPayload. Rebuilt on demand rather
114
+ // than once at open, because every taught fact and every played turn grows
115
+ // the store.
116
+ let memoryGraph = parseEntities({ individuals: [], objectProperties: [] });
117
+ async function refreshGraph() {
118
+ memoryGraph = parseEntities(memoryFactGraphPayload(readFactRows(await loadMemory(memoryDir))));
119
+ return memoryGraph;
120
+ }
121
+
105
122
  const lexicon = loadLexicon();
106
123
 
107
124
  // createTurnSession owns focus/last and the catch fallback; planHolder
@@ -115,9 +132,9 @@ export async function createAdventureSession(worldPayload, { restoredPayload = n
115
132
  // when the command didn't move anyone) — the manual-play half of the
116
133
  // merged exposure set.
117
134
  const turnSession = createTurnSession({
118
- memoryDir, graph, lexicon, sessionId,
135
+ memoryDir, graph: codeGraph, lexicon, sessionId,
119
136
  vocabHint: 'Try a world question ("where is the key"), or teach me: "remember: the moat is a ditch".',
120
- buildExtraOptions: () => ({ uiContext: "browser", planState: planHolder.state }),
137
+ buildExtraOptions: () => ({ planState: planHolder.state }),
121
138
  captureExtraState: async (result) => {
122
139
  if ("planState" in result) planHolder.state = result.planState;
123
140
  const here = foldWorldState(worldActionRows(readFactRows(await loadMemory(memoryDir)))).placements.get("player")?.object ?? null;
@@ -127,6 +144,9 @@ export async function createAdventureSession(worldPayload, { restoredPayload = n
127
144
 
128
145
  return {
129
146
  memoryDir,
147
+ codeGraph,
148
+ get graph() { return memoryGraph; },
149
+ refreshGraph,
130
150
 
131
151
  /** One auto-play tick: infer the goal, execute exactly one move through
132
152
  * adventureTurn (adventure-autoplay.mjs's own contract), thread the
@@ -192,25 +212,26 @@ export async function createAdventureSession(worldPayload, { restoredPayload = n
192
212
  };
193
213
  }
194
214
 
195
- // Re-exported so the page's own rendering script (adventure-viz.mjs) never
196
- // has to duplicate sprite resolution, the digest reader, the room
197
- // affordances the chat dock's own pills read from, or (foldWorldState,
198
- // exposedFacts) the exposure-filtered fold the goal-status panel mirrors
199
- // the same posture spider-fly-browser-entry.mjs's own
200
- // globalThis.tmctSpiderFly re-export takes. `relatedForTerm`/
201
- // `classAncestorChain` back the edit mode's own cursor-suggestion pills
202
- // (adventure-viz.mjs's suggestionsForTerm mirrors this same pairing against
203
- // the global, the same reach-through-the-global pattern captionFor/pillsFor
204
- // already use for their own adventure.mjs calls). `directedGridLayout`/
205
- // `roomGraphSvg` (viz-room-graph.mjs's shared BFS-grid room-map layout and
206
- // SVG renderer) back the manor-map panel, its lightbox and its edit-mode
207
- // whole-map twin the same way — neither is `.toString()`-splice-safe
208
- // (roomGraphSvg needs escapeHtml, directedGridLayout needs its own
209
- // module-level exit-delta table), so the page reaches them here instead of
210
- // re-implementing the layout a second time.
211
- globalThis.tmctAdventure = {
212
- createAdventureSession, resolveSpriteForClass, SPRITE_REGISTRY, resolveSpriteAsset,
213
- worldDigestRows, roomAffordances, foldWorldState, exposedFacts,
214
- relatedForTerm, classAncestorChain, openPersistedStore,
215
- directedGridLayout, roomGraphSvg,
216
- };
215
+ // `tmct.page` keeps what the page draws with and the engine has no
216
+ // plain-English form for: sprite resolution, the digest reader, the room
217
+ // affordances the chat pills read from, the exposure-filtered fold the
218
+ // goal-status panel mirrors, the SKOS neighbourhood and is-a chain behind the
219
+ // edit mode's cursor pills, the persisted-store wrapper, and the manor map's
220
+ // own BFS-grid layout and SVG renderer (neither is `.toString()`-splice-safe:
221
+ // roomGraphSvg needs escapeHtml, directedGridLayout its own exit-delta table).
222
+ publishTmctSurface({
223
+ open: createAdventureSession,
224
+ // The memory projection is rebuilt first, so a direct tmct.ask() sees every
225
+ // fact taught or played into this world since the last one.
226
+ ask: async (request, options, session) => {
227
+ await session.refreshGraph();
228
+ return graphAsk(request, options, session);
229
+ },
230
+ plan: enginePlan,
231
+ page: {
232
+ resolveSpriteForClass, SPRITE_REGISTRY, resolveSpriteAsset,
233
+ worldDigestRows, roomAffordances, foldWorldState, exposedFacts,
234
+ relatedForTerm, classAncestorChain, openPersistedStore,
235
+ directedGridLayout, roomGraphSvg,
236
+ },
237
+ });
@@ -30,6 +30,7 @@ import { vocabExampleHint } from "../../services/chat.mjs";
30
30
  import { createInMemoryStore, normFactTerm, loadMemory, readFactRows, applySeedPayload } from "../../adapters/memory/core.mjs";
31
31
  import { splitSentencesPreservingPaths } from "../../services/sentences.mjs";
32
32
  import { parseEntities } from "../../domain/codegraph.mjs";
33
+ import { memoryFactGraphPayload } from "../../domain/memory-facts.mjs";
33
34
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
34
35
  import { registerWinkModel } from "../../adapters/wink-model.mjs";
35
36
  import { setDigestStructures } from "../../adapters/corpus/digest-bank.mjs";
@@ -51,6 +52,8 @@ import { registerLiveReferenceProvider, registerResearchProvider } from "../../a
51
52
  // decides when to save/load/clear; this entry only carries the wrapper
52
53
  // across the bundle boundary.
53
54
  import { openPersistedStore } from "./idb-persist.mjs";
55
+ import { publishTmctSurface } from "./tmct-surface.mjs";
56
+ import { graphAsk, enginePlan } from "./engine-surface.mjs";
54
57
 
55
58
  /**
56
59
  * A browser chat session over the real turn engine.
@@ -70,9 +73,14 @@ import { openPersistedStore } from "./idb-persist.mjs";
70
73
  * is module-scope, last write wins, and every session created after this one
71
74
  * shares it.
72
75
  *
73
- * Returns { memoryDir, sessionId, turn }. `turn(line)` resolves to
74
- * { answer, end, record, plan } and threads focus/last/planState between
75
- * calls exactly as the CLI session does.
76
+ * Returns { memoryDir, sessionId, graph, codeGraph, refreshGraph, turn }.
77
+ * `turn(line)` resolves to { answer, end, record, plan } and threads
78
+ * focus/last/planState between calls exactly as the CLI session does.
79
+ *
80
+ * The two graphs are separate on purpose. `codeGraph` is the known-empty index
81
+ * the turn engine reads, and `graph` is this session's memory store projected
82
+ * for `ask()` — a question about taught facts has a real graph to traverse
83
+ * while a code-structure question keeps its honest no-code-graph refusal.
76
84
  */
77
85
  export function createChatSession({ seedPayload = null, vocabSeeded = false, liveReference = false, onLiveLookup = null, synthesisBudget = 12, digestStructures = null } = {}) {
78
86
  setDigestStructures(digestStructures || []);
@@ -80,10 +88,24 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false, liv
80
88
  applySeedPayload(memoryDir, seedPayload);
81
89
 
82
90
  // A known-empty code graph: code-structure questions get the same honest
83
- // no-code-graph answer an un-pointed CLI session gives, never a crash.
84
- const graph = parseEntities({ individuals: [], objectProperties: [] });
91
+ // no-code-graph answer an un-pointed CLI session gives, never a crash. The
92
+ // turn engine keeps reading THIS one, so the identity-led greeting, the teach
93
+ // pointer and the zero module counts a page earns from an empty index all
94
+ // stay exactly as they were.
95
+ const codeGraph = parseEntities({ individuals: [], objectProperties: [] });
96
+
97
+ // What `tmct.ask()` traverses is a different graph: this session's own memory
98
+ // store, projected through memoryFactGraphPayload. Rebuilt on demand rather
99
+ // than once at open, because every teach, ingest and research turn grows the
100
+ // store — the same reason spider-fly rebuilds its board before each turn.
101
+ let memoryGraph = parseEntities({ individuals: [], objectProperties: [] });
102
+ async function refreshGraph() {
103
+ memoryGraph = parseEntities(memoryFactGraphPayload(readFactRows(await loadMemory(memoryDir))));
104
+ return memoryGraph;
105
+ }
106
+
85
107
  const lexicon = loadLexicon();
86
- const vocabHint = vocabExampleHint(vocabSeeded);
108
+ const vocabHint = vocabExampleHint(vocabSeeded, "browser");
87
109
  const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
88
110
 
89
111
  // Four-state, like the CLI: false (off), true (rescue on a miss),
@@ -97,10 +119,10 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false, liv
97
119
  let synthesisBudgetOn = Number.isFinite(synthesisBudget) ? synthesisBudget : 12;
98
120
 
99
121
  const session = createTurnSession({
100
- memoryDir, graph, lexicon, sessionId, vocabHint,
122
+ memoryDir, graph: codeGraph, lexicon, sessionId, vocabHint,
101
123
  buildExtraOptions: () => ({
102
124
  liveReference: liveReferenceOn, onLiveLookup,
103
- uiContext: "browser", synthesisBudget: synthesisBudgetOn,
125
+ synthesisBudget: synthesisBudgetOn,
104
126
  }),
105
127
  // `result.liveReference` mirrors a `/wiki on|off|supplement|always`
106
128
  // command back into this session's own toggle state.
@@ -112,6 +134,9 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false, liv
112
134
  return {
113
135
  memoryDir,
114
136
  sessionId,
137
+ get graph() { return memoryGraph; },
138
+ codeGraph,
139
+ refreshGraph,
115
140
  get liveReference() { return liveReferenceOn; },
116
141
  /** The page's toggle seam: set the live Wikipedia mode for every later turn
117
142
  * (the `/wiki on|off|supplement|always` command sets the same state). */
@@ -144,4 +169,25 @@ export async function researchedFactRows(memoryDir) {
144
169
  .map((row) => ({ subject: row.subject, predicate: row.predicate, object: row.object }));
145
170
  }
146
171
 
147
- globalThis.tmctChat = { createChatSession, registerWinkModel, registerReferencePackProvider, registerLiveReferenceProvider, registerResearchProvider, normFactTerm, vocabExampleHint, memoryStats, openPersistedStore, exportFactsJsonl, researchedFactRows, splitSentences: splitSentencesPreservingPaths };
172
+ // The page reaches the engine through the one shared surface: `tmct.open()`
173
+ // opens this session, `tmct.turn()` runs the dock, `tmct.ask()` puts a
174
+ // question to the session's own graph. What stays on `tmct.page` is what has
175
+ // no plain-English form — the vendor/provider seams the page registers before
176
+ // the first turn, its IndexedDB wrapper, and the two serializers its export
177
+ // and paste-ingest controls run.
178
+ publishTmctSurface({
179
+ open: createChatSession,
180
+ // The memory projection is rebuilt first, so a direct tmct.ask() sees every
181
+ // fact taught, pasted or researched since the last one.
182
+ ask: async (request, options, session) => {
183
+ await session.refreshGraph();
184
+ return graphAsk(request, options, session);
185
+ },
186
+ plan: enginePlan,
187
+ page: {
188
+ registerWinkModel, registerReferencePackProvider, registerLiveReferenceProvider,
189
+ registerResearchProvider, normFactTerm, vocabExampleHint, memoryStats,
190
+ openPersistedStore, exportFactsJsonl, researchedFactRows,
191
+ splitSentences: splitSentencesPreservingPaths,
192
+ },
193
+ });
@@ -22,6 +22,8 @@ import * as source from "../../adapters/source.mjs";
22
22
  import { computeCodeExplorerData, computeCodeLedger, edgePhrase } from "../../services/code-explorer-viz.mjs";
23
23
  import { generateCodeHints } from "../../domain/code-explorer-hints.mjs";
24
24
  import { createTurnSession } from "./turn-session.mjs";
25
+ import { publishTmctSurface } from "./tmct-surface.mjs";
26
+ import { graphAsk, enginePlan } from "./engine-surface.mjs";
25
27
  import { tmct_ask } from "../../tools/handlers/tmct-ask.mjs";
26
28
  import { RELATIONS } from "../../domain/ask-vocab.mjs";
27
29
 
@@ -37,7 +39,7 @@ import { RELATIONS } from "../../domain/ask-vocab.mjs";
37
39
  * payload carries the starter vocabulary. Teaches land in the same in-memory
38
40
  * store, so a taught fact never touches disk.
39
41
  *
40
- * Returns { memoryDir, sessionId, turn }, the createChatSession shape the
42
+ * Returns { memoryDir, sessionId, graph, turn }, the createChatSession shape the
41
43
  * page's chat expects.
42
44
  */
43
45
  export function createCodeExplorerSession({ graphPayload, seedPayload = null, vocabSeeded = false } = {}) {
@@ -51,7 +53,7 @@ export function createCodeExplorerSession({ graphPayload, seedPayload = null, vo
51
53
  // the write path recounts — teach turns must work on any seed.
52
54
  if (seedPayload) memoryDir.payload = { ...memoryDir.payload, ...seedPayload };
53
55
  const lexicon = loadLexicon();
54
- const vocabHint = vocabExampleHint(vocabSeeded);
56
+ const vocabHint = vocabExampleHint(vocabSeeded, "browser");
55
57
  const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
56
58
  // A stable virtual path: the provider answers every fetch, so no file is
57
59
  // ever read, but the code lanes still do path math (join/dirname) on it.
@@ -69,6 +71,7 @@ export function createCodeExplorerSession({ graphPayload, seedPayload = null, vo
69
71
  return {
70
72
  memoryDir,
71
73
  sessionId,
74
+ graph,
72
75
  turn: turnSession.turn,
73
76
  };
74
77
  }
@@ -223,17 +226,28 @@ export function askRelatedFacts(graphPayload, term) {
223
226
  return { term, rows, asked, grounded };
224
227
  }
225
228
 
226
- // Exposed for the page's inline client: the re-derivation helpers so a graph
227
- // swapped through the desktop picker re-renders without duplicating logic, the
228
- // engine's own answer to "what relates to the focus", plus the wink loader hook
229
- // registerWinkModel and normFactTerm the chat shares with the ledger page.
230
- globalThis.tmctCodeExplorer = {
231
- createCodeExplorerSession,
232
- askRelatedFacts,
233
- computeCodeExplorerData,
234
- computeCodeLedger,
235
- generateCodeHints,
236
- parseEntities,
237
- normFactTerm,
238
- registerWinkModel,
239
- };
229
+ // This page holds a REAL code graph, so `tmct.ask` is the engine answering
230
+ // over it the same round trip askRelatedFacts already makes twice per
231
+ // relation kind to build the sidebar's rows.
232
+ //
233
+ // `tmct.page` keeps the re-derivation helpers a graph swapped through the
234
+ // desktop picker re-renders with, plus the wink and term-normalizing seams the
235
+ // chat shares with the ledger page. `createCodeExplorerSession` sits there
236
+ // too: the page keeps its own session slot, created lazily on the first turn
237
+ // once the general-knowledge seed has landed, rather than the one `tmct.open`
238
+ // would install eagerly — so it calls the factory straight from the bag.
239
+ publishTmctSurface({
240
+ open: createCodeExplorerSession,
241
+ ask: graphAsk,
242
+ plan: enginePlan,
243
+ page: {
244
+ createCodeExplorerSession,
245
+ askRelatedFacts,
246
+ computeCodeExplorerData,
247
+ computeCodeLedger,
248
+ generateCodeHints,
249
+ parseEntities,
250
+ normFactTerm,
251
+ registerWinkModel,
252
+ },
253
+ });
@@ -0,0 +1,82 @@
1
+ // engine-surface.mjs — the standard `ask` and `plan` routes a browser page
2
+ // wires into publishTmctSurface (tmct-surface.mjs).
3
+ //
4
+ // Both run the SAME machinery the CLI runs, over the session's own in-memory
5
+ // state rather than a repo on disk: `graphAsk` dispatches `tmct_ask` through
6
+ // the tool layer and hands back the structured envelope beside the prose
7
+ // (dispatchToolStructured's `{ content, data }`); `enginePlan` builds the
8
+ // capability planner's context from whatever the session holds and runs one
9
+ // request through it.
10
+ //
11
+ // Kept out of tmct-surface.mjs on purpose. That module is pure wiring and
12
+ // imports nothing, so memory-ask-browser-entry.mjs — the one bundle that is
13
+ // committed and published, and is deliberately small — can publish the same
14
+ // `globalThis.tmct` without pulling the tool layer and the router into its
15
+ // bundle.
16
+ import { dispatchToolStructured } from "../../tools/server.mjs";
17
+ import { capabilityPlanDeps, noCodeGraph } from "../../services/chat.mjs";
18
+
19
+ /**
20
+ * One plain-English question, answered against the graph this session holds,
21
+ * through the same `tmct_ask` tool the CLI dispatches. Returns
22
+ * `{ answer, data, miss }` — `data` is ask()'s own envelope (the parse, the
23
+ * matches it found, the traversal it walked), which is what a panel renders
24
+ * rows from and what makes an answer auditable.
25
+ *
26
+ * A session whose graph is empty answers with an honest miss. That is the real
27
+ * state of a memory-only page: its rows are facts in a store, and ask() reads
28
+ * a graph. spider-fly is the page that closed that gap for its own world, by
29
+ * projecting its live board into a graph (worldRelationGraphPayload) before
30
+ * every turn — the same route is open to any page whose rows have a shape
31
+ * worth asking over.
32
+ */
33
+ export async function graphAsk(request, options, session) {
34
+ const { content, data } = await dispatchToolStructured("tmct_ask", { query: request }, {
35
+ graph: session.graph,
36
+ memoryBackend: session.memoryDir,
37
+ });
38
+ return { answer: content, data, miss: Boolean(data?.miss) };
39
+ }
40
+
41
+ /**
42
+ * One compound or maintenance-goal request, planned and executed over whatever
43
+ * this session holds — the same capability router `/plan` reaches, with the
44
+ * same memory-only mode a page with no code graph needs (a code-graph
45
+ * capability there refuses by naming the graph it hasn't got).
46
+ *
47
+ * Returns the planner's own result: `{ refused, why, driver, calls, composed,
48
+ * observed }`. No prose is composed here — a page that wants the written
49
+ * answer asks for it in words through `tmct.turn("/plan …")`, which is the
50
+ * same split `dispatchTool` and `dispatchToolStructured` already draw.
51
+ */
52
+ export async function enginePlan(request, options, session) {
53
+ const graph = session.graph && !noCodeGraph(session.graph) ? session.graph : null;
54
+ const memoryDir = session.memoryDir ?? null;
55
+ if (!graph && !memoryDir) {
56
+ return {
57
+ refused: true,
58
+ why: "nothing to plan over — this session holds neither a code graph nor a memory store",
59
+ driver: null, calls: [], composed: null, observed: null,
60
+ };
61
+ }
62
+ const { buildCapabilityPlanCtx, runCapabilityPlan, declaredCapabilityNames } =
63
+ await import("../../domain/router/drive.mjs");
64
+ const ctx = await buildCapabilityPlanCtx({
65
+ ...capabilityPlanDeps(), config: null, source: null, graph, memoryDir,
66
+ });
67
+ try {
68
+ const result = await runCapabilityPlan(request, declaredCapabilityNames(), ctx);
69
+ return {
70
+ refused: Boolean(result.refused),
71
+ why: Array.isArray(result.why) ? result.why.join("; ") : (result.why ?? null),
72
+ driver: result.driver ?? null,
73
+ calls: result.calls ?? [],
74
+ composed: result.composed ?? null,
75
+ observed: result.observed ?? null,
76
+ };
77
+ } finally {
78
+ // The taught registrations are per-ctx: dispose them so the next call
79
+ // re-reads the store instead of meeting a stale name collision.
80
+ for (const dispose of ctx.disposers || []) dispose();
81
+ }
82
+ }
@@ -29,15 +29,24 @@ import { splitSentencesPreservingPaths, stripCitationResidue } from "../../servi
29
29
  import { clauseCandidates, optimisticTriples } from "../../services/extract-facts.mjs";
30
30
  import { touchedFactRows } from "../../domain/memory/touched-facts.mjs";
31
31
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
32
+ import { parseEntities } from "../../domain/codegraph.mjs";
33
+ import { ingestFactGraphPayload, ingestedFactRows } from "../../domain/ingest-facts.mjs";
32
34
  import { registerWinkModel, winkInstance } from "../../adapters/wink-model.mjs";
33
35
  import { memoryStats, exportFactsJsonl } from "./memory-stats.mjs";
34
36
  import { openPersistedStore } from "./idb-persist.mjs";
37
+ import { createTurnSession } from "./turn-session.mjs";
38
+ import { publishTmctSurface } from "./tmct-surface.mjs";
39
+ import { graphAsk, enginePlan } from "./engine-surface.mjs";
35
40
 
36
41
  // The pronoun subjects a bounded carry substitutes with the last unique
37
42
  // grounded subject in the SAME paragraph. Reset at every blank line, so a
38
43
  // fresh paragraph never resolves against a stale antecedent.
39
44
  const PRONOUN_LEAD_RE = /^(?:they|it|these|those|this)\b\s*/i;
40
45
 
46
+ // The turn kinds that move the fact store, so the projected graph a later
47
+ // question traverses has to be rebuilt before it answers.
48
+ const STORE_WRITING_TURNS = new Set(["assert", "retract"]);
49
+
41
50
  /**
42
51
  * The single recognizer seam. Splits `text` into paragraphs (blank-line
43
52
  * separated, so the pronoun carry never bridges a topic break) and each
@@ -170,29 +179,90 @@ export async function groundTextToFacts(text, { memoryDir, sessionId, lexicon, v
170
179
  * the page can then export. `seedPayload` (optional) pre-loads a graph the
171
180
  * recognizer can recall and link against.
172
181
  *
173
- * Returns { memoryDir, sessionId, ingest }. `ingest(text, { onFact,
174
- * optimistic })` is the one call the page makes; it drives groundTextToFacts
175
- * against this session's store and returns its { sentences, recognized,
176
- * skipped, facts } summary.
182
+ * Returns { memoryDir, sessionId, graph, refreshGraph, askableClasses, ingest,
183
+ * turn }. `ingest(text, { onFact, optimistic })` is the call the ingest panes
184
+ * make; it drives groundTextToFacts against this session's store and returns
185
+ * its { sentences, recognized, skipped, facts } summary. `turn(line)` is the
186
+ * ask dock's own entry point — the FULL chat turn engine (the exact dispatch
187
+ * the CLI runs) over the same store, so a question it can't ground gets the
188
+ * same refusal the CLI gives.
189
+ *
190
+ * `graph` is what a question actually traverses: the ingested rows projected
191
+ * through ingest-facts.mjs, rebuilt by `refreshGraph()` whenever the store has
192
+ * moved since the last one. The rebuild is gated on a dirty flag rather than
193
+ * run per call because a seeded store holds tens of thousands of rows, and
194
+ * re-reading all of them to answer a second question about the same two
195
+ * sentences is the one cost this page has to avoid.
177
196
  */
178
197
  export function createIngestSession({ seedPayload = null, vocabSeeded = false } = {}) {
179
198
  const memoryDir = createInMemoryStore();
180
199
  applySeedPayload(memoryDir, seedPayload);
181
200
 
182
201
  const lexicon = loadLexicon();
183
- const vocabHint = vocabExampleHint(vocabSeeded);
202
+ const vocabHint = vocabExampleHint(vocabSeeded, "browser");
184
203
  const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
185
204
 
205
+ let graph = parseEntities({ individuals: [], objectProperties: [] });
206
+ let storeMovedSinceGraph = true;
207
+ async function refreshGraph() {
208
+ if (!storeMovedSinceGraph) return graph;
209
+ const rows = ingestedFactRows(readFactRows(await loadMemory(memoryDir)));
210
+ graph = parseEntities(ingestFactGraphPayload(rows));
211
+ storeMovedSinceGraph = false;
212
+ return graph;
213
+ }
214
+
215
+ // An empty graph is not the same as no graph to runTurn: a non-null one
216
+ // makes it read an ordinary "the number of X" phrase as a graph count query
217
+ // instead of running the teach cascade, so before anything is ingested the
218
+ // dock keeps the plain conversational reading.
219
+ const graphForTurn = () => (graph.individuals.length ? graph : null);
220
+
221
+ const turnSession = createTurnSession({
222
+ memoryDir, lexicon, sessionId, vocabHint,
223
+ buildExtraOptions: () => ({ graph: graphForTurn() }),
224
+ captureExtraState: (result) => {
225
+ if (STORE_WRITING_TURNS.has(result?.record?.via) && !result.record.miss) storeMovedSinceGraph = true;
226
+ },
227
+ });
228
+
186
229
  return {
187
230
  memoryDir,
188
231
  sessionId,
189
- ingest(text, { onFact = null, optimistic = false } = {}) {
190
- return groundTextToFacts(text, { memoryDir, sessionId, lexicon, vocabHint, onFact, optimistic });
232
+ get graph() { return graph; },
233
+ refreshGraph,
234
+ /** The class nouns this graph can actually list, for a page building its
235
+ * own "try asking" hint out of real data rather than invented examples. */
236
+ askableClasses() {
237
+ return [...new Set(graph.individuals.map((i) => i.class).filter(Boolean))].sort();
238
+ },
239
+ async ingest(text, { onFact = null, optimistic = false } = {}) {
240
+ const summary = await groundTextToFacts(text, { memoryDir, sessionId, lexicon, vocabHint, onFact, optimistic });
241
+ if (summary.recognized) storeMovedSinceGraph = true;
242
+ return summary;
243
+ },
244
+ async turn(line) {
245
+ await refreshGraph();
246
+ return turnSession.turn(line);
191
247
  },
192
248
  };
193
249
  }
194
250
 
195
- globalThis.tmctIngest = {
196
- createIngestSession, groundTextToFacts, exportFactsJsonl, registerWinkModel, normFactTerm,
197
- memoryStats, openPersistedStore,
198
- };
251
+ // The page's own two calls are `tmct.session.ingest(text)` and, from the ask
252
+ // dock, `tmct.turn(line)`. `tmct.ask` puts one question straight to the graph
253
+ // this session projects from what it has ingested, rebuilding it first so a
254
+ // fact grounded a moment ago is already visible. `tmct.page` keeps the
255
+ // recognizer entry point the page also drives directly, the wink seam, the
256
+ // stats fold behind its counters, its IndexedDB wrapper and its JSONL export.
257
+ publishTmctSurface({
258
+ open: createIngestSession,
259
+ ask: async (request, options, session) => {
260
+ await session.refreshGraph();
261
+ return graphAsk(request, options, session);
262
+ },
263
+ plan: enginePlan,
264
+ page: {
265
+ groundTextToFacts, exportFactsJsonl, registerWinkModel, normFactTerm,
266
+ memoryStats, openPersistedStore,
267
+ },
268
+ });
@@ -17,9 +17,10 @@
17
17
  // never published — only the hosted demo site's public/ledger.html links to
18
18
  // it, as an optional sibling script the page degrades honestly without.
19
19
  import { vocabExampleHint } from "../../services/chat.mjs";
20
- import { createInMemoryStore, normFactTerm, applySeedPayload } from "../../adapters/memory/core.mjs";
20
+ import { createInMemoryStore, normFactTerm, applySeedPayload, loadMemory, readFactRows } from "../../adapters/memory/core.mjs";
21
21
  import { splitSentencesPreservingPaths } from "../../services/sentences.mjs";
22
22
  import { parseEntities } from "../../domain/codegraph.mjs";
23
+ import { memoryFactGraphPayload } from "../../domain/memory-facts.mjs";
23
24
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
24
25
  import { registerWinkModel } from "../../adapters/wink-model.mjs";
25
26
  import { registerResearchProvider } from "../../adapters/corpus/wikipedia-live.mjs";
@@ -27,6 +28,8 @@ import { computeLedgerDataFromPayload } from "../../services/ledger-viz.mjs";
27
28
  import { digestTermFromPayloadBrowser } from "./digest-client.mjs";
28
29
  import { createTurnSession } from "./turn-session.mjs";
29
30
  import { exportFactsJsonl } from "./memory-stats.mjs";
31
+ import { publishTmctSurface } from "./tmct-surface.mjs";
32
+ import { graphAsk, enginePlan } from "./engine-surface.mjs";
30
33
 
31
34
  /**
32
35
  * A browser ledger-dock session over the real turn engine — createChatSession's
@@ -35,7 +38,12 @@ import { exportFactsJsonl } from "./memory-stats.mjs";
35
38
  * a fact taught through the dock extends the SAME graph the page renders
36
39
  * from, never a disconnected one.
37
40
  *
38
- * Returns { memoryDir, sessionId, turn }, identical to createChatSession.
41
+ * Returns { memoryDir, sessionId, graph, codeGraph, refreshGraph, turn },
42
+ * identical to createChatSession — including the two-graph split: `codeGraph`
43
+ * is the known-empty index the turn engine reads, and `graph` is the ledger's
44
+ * own store projected for `ask()`, so a question about the facts on the page
45
+ * traverses those facts while a code-structure question still refuses honestly.
46
+ *
39
47
  * ledger-viz.mjs's own inline script calls computeLedgerDataFromPayload
40
48
  * (re-exported below) on `memoryDir.payload` after a turn whose record shows
41
49
  * a successful write (`via: "assert"` or `via: "retract"`, `miss: false`) to
@@ -46,20 +54,45 @@ export function createLedgerSession({ seedPayload = null, vocabSeeded = false }
46
54
  const memoryDir = createInMemoryStore();
47
55
  applySeedPayload(memoryDir, seedPayload);
48
56
 
49
- const graph = parseEntities({ individuals: [], objectProperties: [] });
57
+ const codeGraph = parseEntities({ individuals: [], objectProperties: [] });
58
+
59
+ let memoryGraph = parseEntities({ individuals: [], objectProperties: [] });
60
+ async function refreshGraph() {
61
+ memoryGraph = parseEntities(memoryFactGraphPayload(readFactRows(await loadMemory(memoryDir))));
62
+ return memoryGraph;
63
+ }
64
+
50
65
  const lexicon = loadLexicon();
51
- const vocabHint = vocabExampleHint(vocabSeeded);
66
+ const vocabHint = vocabExampleHint(vocabSeeded, "browser");
52
67
  const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
53
68
 
54
- const session = createTurnSession({ memoryDir, graph, lexicon, sessionId, vocabHint });
55
- return { memoryDir, sessionId, turn: session.turn };
69
+ const session = createTurnSession({ memoryDir, graph: codeGraph, lexicon, sessionId, vocabHint });
70
+ return {
71
+ memoryDir,
72
+ sessionId,
73
+ get graph() { return memoryGraph; },
74
+ codeGraph,
75
+ refreshGraph,
76
+ turn: session.turn,
77
+ };
56
78
  }
57
79
 
58
- // Re-exported so ledger-viz.mjs's own inline script never has to duplicate
59
- // the derivation logic that builds rows/terms/edges/contradictions/
60
- // worthALook/stats from a payload the same posture chat-browser-entry.mjs
61
- // takes re-exporting registerWinkModel for its own page's CDN wink load.
62
- // splitSentences + exportFactsJsonl carry the dock's paste-and-drop ingest and
63
- // its JSONL export across the bundle boundary, the same one-serializer posture
64
- // chat-browser-entry.mjs holds for its own page.
65
- globalThis.tmctLedger = { createLedgerSession, computeLedgerDataFromPayload, normFactTerm, registerWinkModel, registerResearchProvider, splitSentences: splitSentencesPreservingPaths, exportFactsJsonl, digestTermFromPayloadBrowser };
80
+ // `tmct.page` keeps what the page's own script renders with and the engine has
81
+ // no plain-English form for: the payload-to-rows/terms/edges/stats derivation
82
+ // the ledger view re-mounts from after a successful teach, the digest reader,
83
+ // the wink and research seams, and the two serializers behind the dock's
84
+ // paste-ingest and JSONL export.
85
+ publishTmctSurface({
86
+ open: createLedgerSession,
87
+ // The memory projection is rebuilt first, so a direct tmct.ask() sees every
88
+ // fact the dock or the ingest panel has 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: {
95
+ computeLedgerDataFromPayload, normFactTerm, registerWinkModel, registerResearchProvider,
96
+ splitSentences: splitSentencesPreservingPaths, exportFactsJsonl, digestTermFromPayloadBrowser,
97
+ },
98
+ });