@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
@@ -2,17 +2,18 @@
2
2
  // (built by scripts/build-chat-bundle.mjs).
3
3
  //
4
4
  // Unlike memory-ask-browser-entry.mjs (factAnswer/factReadBack only), this
5
- // exposes the FULL turn engine: createChatSession wraps chat.mjs's runTurn
6
- // with a session-shaped closure — the focus/last/planState threading
7
- // src/services/chat-session.mjs's createSession.turn does, minus every
8
- // filesystem side effect (no transcript log, no sidecar, no graph upsert).
9
- // Memory is an in-memory Backend-B handle, optionally pre-loaded with a
10
- // built seed payload (scripts/build-chat-seed.mjs), so teach turns, recall,
11
- // proof chains and the honest miss all run client-side with zero I/O.
5
+ // exposes the FULL turn engine: createChatSession wraps the shared
6
+ // createTurnSession (turn-session.mjs) around chat.mjs's runTurn — the
7
+ // focus/last/planState/researchState threading src/services/chat-session.mjs's
8
+ // createSession.turn does, minus every filesystem side effect (no transcript
9
+ // log, no sidecar, no graph upsert). Memory is an in-memory Backend-B handle,
10
+ // optionally pre-loaded with a built seed payload (scripts/build-chat-seed.mjs),
11
+ // so teach turns, recall, proof chains and the honest miss all run
12
+ // client-side with zero I/O.
12
13
  //
13
- // Three browser traps this file owns so no caller can fall into them:
14
- // - runTurn defaults `env` to process.env, and a browser has no `process`
15
- // global every turn here passes `env: {}` explicitly;
14
+ // Two browser traps this file still owns, beyond what createTurnSession
15
+ // already covers (passing `env: {}` explicitly, since a browser has no
16
+ // `process` global; the crash-resistant catch fallback):
16
17
  // - the uuid adapter needs node:crypto — the session id comes from the
17
18
  // Web Crypto API instead, with a Date.now fallback for contexts
18
19
  // without it;
@@ -25,15 +26,15 @@
25
26
  // adapter — its own setDigestStructures is a documented no-op there
26
27
  // (see that module's header), so the call is harmless either way, never
27
28
  // a load error and never a behavior change on the Node side.
28
- import { runTurn, vocabExampleHint } from "../../services/chat.mjs";
29
- import { createInMemoryStore, normFactTerm, loadMemory, readFactRows } from "../../adapters/memory/core.mjs";
30
- import { serializeFactsJsonl } from "../../adapters/memory/export-jsonl.mjs";
29
+ import { vocabExampleHint } from "../../services/chat.mjs";
30
+ import { createInMemoryStore, normFactTerm, loadMemory, readFactRows, applySeedPayload } from "../../adapters/memory/core.mjs";
31
31
  import { splitSentencesPreservingPaths } from "../../services/sentences.mjs";
32
- import { provenanceTagToSource } from "../../domain/memory/trust.mjs";
33
32
  import { parseEntities } from "../../domain/codegraph.mjs";
34
33
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
35
34
  import { registerWinkModel } from "../../adapters/wink-model.mjs";
36
35
  import { setDigestStructures } from "../../adapters/corpus/digest-bank.mjs";
36
+ import { createTurnSession } from "./turn-session.mjs";
37
+ import { memoryStats, exportFactsJsonl } from "./memory-stats.mjs";
37
38
  // The reference-pack provider seam: the page registers a fetch-backed
38
39
  // provider over public/reference-pack/ so the engine's pack lookups work
39
40
  // where the gzipped fs layout cannot (the module's own fs loader degrades to
@@ -50,6 +51,8 @@ import { registerLiveReferenceProvider, registerResearchProvider } from "../../a
50
51
  // decides when to save/load/clear; this entry only carries the wrapper
51
52
  // across the bundle boundary.
52
53
  import { openPersistedStore } from "./idb-persist.mjs";
54
+ import { publishTmctSurface } from "./tmct-surface.mjs";
55
+ import { graphAsk, enginePlan } from "./engine-surface.mjs";
53
56
 
54
57
  /**
55
58
  * A browser chat session over the real turn engine.
@@ -69,17 +72,14 @@ import { openPersistedStore } from "./idb-persist.mjs";
69
72
  * is module-scope, last write wins, and every session created after this one
70
73
  * shares it.
71
74
  *
72
- * Returns { memoryDir, sessionId, turn }. `turn(line)` resolves to
75
+ * Returns { memoryDir, sessionId, graph, turn }. `turn(line)` resolves to
73
76
  * { answer, end, record, plan } and threads focus/last/planState between
74
77
  * calls exactly as the CLI session does.
75
78
  */
76
79
  export function createChatSession({ seedPayload = null, vocabSeeded = false, liveReference = false, onLiveLookup = null, synthesisBudget = 12, digestStructures = null } = {}) {
77
80
  setDigestStructures(digestStructures || []);
78
81
  const memoryDir = createInMemoryStore();
79
- // Spread onto the store's own empty payload so a partial seed (individuals
80
- // and objectProperties only) still carries the classes/prefixes scaffolding
81
- // the write path recounts — teach turns must work on any seed.
82
- if (seedPayload) memoryDir.payload = { ...memoryDir.payload, ...seedPayload };
82
+ applySeedPayload(memoryDir, seedPayload);
83
83
 
84
84
  // A known-empty code graph: code-structure questions get the same honest
85
85
  // no-code-graph answer an un-pointed CLI session gives, never a crash.
@@ -88,10 +88,6 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false, liv
88
88
  const vocabHint = vocabExampleHint(vocabSeeded);
89
89
  const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
90
90
 
91
- let focus = null;
92
- let last = null;
93
- let planState = null;
94
- let researchState = null;
95
91
  // Four-state, like the CLI: false (off), true (rescue on a miss),
96
92
  // "supplement" (also append a cited read-out under every grounded vocabulary
97
93
  // answer), or "always" (widen that to every grounded answer). The two string
@@ -102,9 +98,23 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false, liv
102
98
  // page's slider sets it; 0 stores article facts without any entailed rows.
103
99
  let synthesisBudgetOn = Number.isFinite(synthesisBudget) ? synthesisBudget : 12;
104
100
 
101
+ const session = createTurnSession({
102
+ memoryDir, graph, lexicon, sessionId, vocabHint,
103
+ buildExtraOptions: () => ({
104
+ liveReference: liveReferenceOn, onLiveLookup,
105
+ uiContext: "browser", synthesisBudget: synthesisBudgetOn,
106
+ }),
107
+ // `result.liveReference` mirrors a `/wiki on|off|supplement|always`
108
+ // command back into this session's own toggle state.
109
+ captureExtraState: (result) => {
110
+ if (typeof result.liveReference === "boolean" || result.liveReference === "supplement" || result.liveReference === "always") liveReferenceOn = result.liveReference;
111
+ },
112
+ });
113
+
105
114
  return {
106
115
  memoryDir,
107
116
  sessionId,
117
+ graph,
108
118
  get liveReference() { return liveReferenceOn; },
109
119
  /** The page's toggle seam: set the live Wikipedia mode for every later turn
110
120
  * (the `/wiki on|off|supplement|always` command sets the same state). */
@@ -113,92 +123,10 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false, liv
113
123
  /** The page's slider seam: set the auto-synthesis budget for every later
114
124
  * learn-on-miss load. Clamped to a non-negative integer; 0 disables it. */
115
125
  setSynthesisBudget(n) { synthesisBudgetOn = Number.isFinite(n) && n > 0 ? Math.floor(n) : 0; },
116
-
117
- /** One dispatched turn. A throwing runTurn must never kill the session —
118
- * the page has no other chance to show this turn's answer. */
119
- async turn(line) {
120
- let result;
121
- try {
122
- result = await runTurn(line, {
123
- config: null, source: null, graph, focus, last, memoryDir, sessionId,
124
- env: {}, lexicon, vocabHint, planState, researchState,
125
- liveReference: liveReferenceOn, onLiveLookup,
126
- uiContext: "browser", synthesisBudget: synthesisBudgetOn,
127
- });
128
- } catch (e) {
129
- const message = e instanceof Error ? e.message : String(e);
130
- return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, record: null, plan: null, research: null };
131
- }
132
- focus = result.focus;
133
- last = result.last;
134
- if ("planState" in result) planState = result.planState;
135
- if ("researchState" in result) researchState = result.researchState;
136
- if (typeof result.liveReference === "boolean" || result.liveReference === "supplement" || result.liveReference === "always") liveReferenceOn = result.liveReference;
137
- // `research` distinguishes three cases on purpose: a queue snapshot
138
- // (research turn), null (a research turn that ended the run), and
139
- // undefined (not a research turn — the page leaves its controls alone).
140
- return { answer: result.answer, end: Boolean(result.end), record: result.record ?? null, plan: result.plan ?? null, research: result.research };
141
- },
126
+ turn: session.turn,
142
127
  };
143
128
  }
144
129
 
145
- /**
146
- * The memory a running session holds, broken down by where each fact came
147
- * from: the seed corpus bands it booted with (keyed by the SAME band name
148
- * build-chat-seed.mjs/extensions.mjs seed under — "human", "seon",
149
- * "conceptnet" today, whichever bands a future seed adds tomorrow) plus
150
- * whatever has been taught THIS session over chat. Reuses memory/trust.mjs's
151
- * own `provenanceTagToSource` against each fact's already-stored provenance
152
- * tag(s) (readFactRows' `provenance`, the ' | '-joined compat string) rather
153
- * than inventing a second provenance parse — the same tag chat.mjs's own
154
- * "(source: ...)" citation already carries, so this panel and a turn's
155
- * citation always agree on where a fact came from.
156
- *
157
- * Returns { total, bandCounts, taught }: `bandCounts` maps a band label to
158
- * its fact count ("taught this session" for a teach/operator-sourced fact
159
- * with no corpus band, "other" for anything provenance can't place);
160
- * `taught` lists every session-taught fact (subject/predicate/object + its
161
- * own provenance tag), most-recently-taught last — a stats panel's
162
- * provenance column reads straight off this, no further lookup needed.
163
- */
164
- export async function memoryStats(memoryDir) {
165
- const memory = await loadMemory(memoryDir);
166
- const rows = readFactRows(memory);
167
- const bandCounts = {};
168
- const taught = [];
169
- for (const row of rows) {
170
- const tags = String(row.provenance || "").split(" | ").filter(Boolean);
171
- let band = null;
172
- let isTaught = false;
173
- let taughtTag = "";
174
- for (const tag of tags) {
175
- const src = provenanceTagToSource(tag);
176
- if (!src) continue;
177
- // corpusWeak (a /r/RelatedTo-strength triple, e.g. ConceptNet's or
178
- // SEON's own weaker associations) names the SAME band as corpus (a
179
- // /r/IsA-strength one) — both carry `src.name`, and a band count that
180
- // dropped the weak tier would undercount a corpus by exactly its
181
- // weak-relation facts (SEON: 19 of them, all `corpus-weak:seon`).
182
- if ((src.kind === "corpus" || src.kind === "corpusWeak") && src.name) band = src.name;
183
- if (src.kind === "teach" || src.kind === "operator") { isTaught = true; taughtTag = tag; }
184
- }
185
- const label = band || (isTaught ? "taught this session" : "other");
186
- bandCounts[label] = (bandCounts[label] || 0) + 1;
187
- if (isTaught) taught.push({ subject: row.subject, predicate: row.predicate, object: row.object, tag: taughtTag });
188
- }
189
- return { total: rows.length, bandCounts, taught };
190
- }
191
-
192
- /**
193
- * The session's whole triple store as JSONL — one
194
- * { subject, predicate, object, provenance } object per line, the same shape
195
- * `tmct extract` and `tmct memory --export` emit. Reads the live memory the
196
- * same way memoryStats does; the page offers it as a download.
197
- */
198
- export async function exportFactsJsonl(memoryDir) {
199
- return serializeFactsJsonl(await loadMemory(memoryDir));
200
- }
201
-
202
130
  /**
203
131
  * Every fact a "research <topic>" run has stored so far, in storage order —
204
132
  * the exposure the "researched this session" panel needs and runTurn's
@@ -219,4 +147,20 @@ export async function researchedFactRows(memoryDir) {
219
147
  .map((row) => ({ subject: row.subject, predicate: row.predicate, object: row.object }));
220
148
  }
221
149
 
222
- globalThis.tmctChat = { createChatSession, registerWinkModel, registerReferencePackProvider, registerLiveReferenceProvider, registerResearchProvider, normFactTerm, vocabExampleHint, memoryStats, openPersistedStore, exportFactsJsonl, researchedFactRows, splitSentences: splitSentencesPreservingPaths };
150
+ // The page reaches the engine through the one shared surface: `tmct.open()`
151
+ // opens this session, `tmct.turn()` runs the dock, `tmct.ask()` puts a
152
+ // question to the session's own graph. What stays on `tmct.page` is what has
153
+ // no plain-English form — the vendor/provider seams the page registers before
154
+ // the first turn, its IndexedDB wrapper, and the two serializers its export
155
+ // and paste-ingest controls run.
156
+ publishTmctSurface({
157
+ open: createChatSession,
158
+ ask: graphAsk,
159
+ plan: enginePlan,
160
+ page: {
161
+ registerWinkModel, registerReferencePackProvider, registerLiveReferenceProvider,
162
+ registerResearchProvider, normFactTerm, vocabExampleHint, memoryStats,
163
+ openPersistedStore, exportFactsJsonl, researchedFactRows,
164
+ splitSentences: splitSentencesPreservingPaths,
165
+ },
166
+ });
@@ -13,14 +13,19 @@
13
13
  // Gitignored, built fresh by scripts/build-code-explorer-bundle.mjs; the page
14
14
  // degrades to a static view when it is absent (renderCodeExplorerHtml's own
15
15
  // contract), so nothing here is ever published.
16
- import { runTurn, vocabExampleHint } from "../../services/chat.mjs";
16
+ import { vocabExampleHint } from "../../services/chat.mjs";
17
17
  import { createInMemoryStore, normFactTerm } from "../../adapters/memory/core.mjs";
18
18
  import { parseEntities } from "../../domain/codegraph.mjs";
19
19
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
20
20
  import { registerWinkModel } from "../../adapters/wink-model.mjs";
21
21
  import * as source from "../../adapters/source.mjs";
22
- import { computeCodeExplorerData, computeCodeLedger } from "../../services/code-explorer-viz.mjs";
22
+ import { computeCodeExplorerData, computeCodeLedger, edgePhrase } from "../../services/code-explorer-viz.mjs";
23
23
  import { generateCodeHints } from "../../domain/code-explorer-hints.mjs";
24
+ import { createTurnSession } from "./turn-session.mjs";
25
+ import { publishTmctSurface } from "./tmct-surface.mjs";
26
+ import { graphAsk, enginePlan } from "./engine-surface.mjs";
27
+ import { tmct_ask } from "../../tools/handlers/tmct-ask.mjs";
28
+ import { RELATIONS } from "../../domain/ask-vocab.mjs";
24
29
 
25
30
  /**
26
31
  * A browser code-explorer session over the real turn engine. Registers the
@@ -34,7 +39,7 @@ import { generateCodeHints } from "../../domain/code-explorer-hints.mjs";
34
39
  * payload carries the starter vocabulary. Teaches land in the same in-memory
35
40
  * store, so a taught fact never touches disk.
36
41
  *
37
- * Returns { memoryDir, sessionId, turn }, the createChatSession shape the
42
+ * Returns { memoryDir, sessionId, graph, turn }, the createChatSession shape the
38
43
  * page's chat expects.
39
44
  */
40
45
  export function createCodeExplorerSession({ graphPayload, seedPayload = null, vocabSeeded = false } = {}) {
@@ -54,42 +59,194 @@ export function createCodeExplorerSession({ graphPayload, seedPayload = null, vo
54
59
  // ever read, but the code lanes still do path math (join/dirname) on it.
55
60
  const config = { graphFile: "graph.json" };
56
61
 
57
- let focus = null;
58
- let last = null;
59
- let planState = null;
62
+ const turnSession = createTurnSession({
63
+ memoryDir, graph, lexicon, sessionId, vocabHint,
64
+ // The code explorer is the one page whose turns run against a real
65
+ // provider seam (source.mjs, registered above) and a virtual graph file
66
+ // path, so both override createTurnSession's config:null/source:null
67
+ // defaults on every call.
68
+ buildExtraOptions: () => ({ config, source }),
69
+ });
60
70
 
61
71
  return {
62
72
  memoryDir,
63
73
  sessionId,
64
- async turn(line) {
65
- let result;
66
- try {
67
- result = await runTurn(line, {
68
- config, source, graph, focus, last, memoryDir, sessionId,
69
- env: {}, lexicon, vocabHint, planState,
70
- });
71
- } catch (e) {
72
- const message = e instanceof Error ? e.message : String(e);
73
- return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, record: null };
74
- }
75
- focus = result.focus;
76
- last = result.last;
77
- if ("planState" in result) planState = result.planState;
78
- return { answer: result.answer, end: Boolean(result.end), record: result.record ?? null };
79
- },
74
+ graph,
75
+ turn: turnSession.turn,
80
76
  };
81
77
  }
82
78
 
83
- // Exposed for the page's inline client: the re-derivation helpers so a graph
84
- // swapped through the desktop picker re-renders without duplicating logic, plus
85
- // the wink loader hook registerWinkModel and normFactTerm the chat shares with
86
- // the ledger page.
87
- globalThis.tmctCodeExplorer = {
88
- createCodeExplorerSession,
89
- computeCodeExplorerData,
90
- computeCodeLedger,
91
- generateCodeHints,
92
- parseEntities,
93
- normFactTerm,
94
- registerWinkModel,
95
- };
79
+ // ---- "what relates to X", asked ------------------------------------------
80
+
81
+ // A symbol-grain predicate asks as its coarse sibling: ask()'s relation
82
+ // vocabulary carries one question per grain pair, not two.
83
+ const COARSE_RELATION = { callsSymbol: "calls", touchesSymbol: "touches" };
84
+
85
+ /** The two questions ask() understands about one relation kind, both spelled
86
+ * from its own vocabulary: `verbs[0]` reads as the reverse direction ("what
87
+ * couples to X"), `bare` as the forward one ("what does X import"). A relation
88
+ * taught to ask-vocab is asked here with no second table to keep in step. */
89
+ function relationQuestions(kind, term) {
90
+ const { bare, verbs } = RELATIONS[kind];
91
+ return [
92
+ { shape: "reverse", query: `what ${verbs[0]} ${term}` },
93
+ { shape: "forward", query: `what does ${term} ${bare}` },
94
+ ];
95
+ }
96
+
97
+ /** The kinds worth asking about: the ones this graph actually stores, folded
98
+ * onto ask()'s vocabulary keys. A graph carrying only imports asks two
99
+ * questions, not twenty. */
100
+ function askableRelationKinds(graph) {
101
+ const kinds = new Set();
102
+ for (const relation of graph.relations) {
103
+ if (!relation.edges.length) continue;
104
+ const kind = COARSE_RELATION[relation.predicate] || relation.predicate;
105
+ if (Object.hasOwn(RELATIONS, kind)) kinds.add(kind);
106
+ }
107
+ return [...kinds];
108
+ }
109
+
110
+ /** Every stored edge as `<subjectId> <objectId>`, keyed by the coarse relation
111
+ * kind so both grains of a pair land in one set, beside a label-to-ids index.
112
+ * An answer names the individuals it found; these two decide whether the row
113
+ * built from that answer is an edge the graph actually holds. */
114
+ function indexEdges(graph) {
115
+ const edgesByKind = new Map();
116
+ for (const relation of graph.relations) {
117
+ const kind = COARSE_RELATION[relation.predicate] || relation.predicate;
118
+ let pairs = edgesByKind.get(kind);
119
+ if (!pairs) edgesByKind.set(kind, (pairs = new Set()));
120
+ for (const edge of relation.edges) pairs.add(`${edge.subject} ${edge.object}`);
121
+ }
122
+ const idsByLabel = new Map();
123
+ for (const individual of graph.individuals) {
124
+ if (!individual?.label || !individual.id) continue;
125
+ const ids = idsByLabel.get(individual.label);
126
+ if (ids) ids.push(individual.id);
127
+ else idsByLabel.set(individual.label, [individual.id]);
128
+ }
129
+ return { edgesByKind, idsByLabel };
130
+ }
131
+
132
+ // parseEntities walks the whole payload, and a focus click re-asks the same
133
+ // graph, so one parse and one edge index per payload serve every question put
134
+ // to it.
135
+ const readGraphs = new WeakMap();
136
+ function readGraph(payload) {
137
+ let read = readGraphs.get(payload);
138
+ if (!read) {
139
+ const graph = parseEntities(payload);
140
+ read = { graph, ...indexEdges(graph) };
141
+ readGraphs.set(payload, read);
142
+ }
143
+ return read;
144
+ }
145
+
146
+ const rowKey = (row) => JSON.stringify([row.s, row.kind, row.o]);
147
+
148
+ /**
149
+ * "What relates to <term>", asked instead of computed. Every relation kind the
150
+ * loaded graph carries becomes two real tmct_ask round trips over that graph —
151
+ * the same ask() every chat turn on this page already reaches — and the
152
+ * answers' typed `matches` become the explorer sidebar's rows. The page stops
153
+ * deciding what relates to what, so the panel and the conversation cannot
154
+ * scope or phrase one question two ways.
155
+ *
156
+ * Two things have to hold before an answer becomes a row. ask() must have
157
+ * parsed the question as the question asked: an identifier that is itself a
158
+ * relation verb makes it read "what tests run" as a question about testing, and
159
+ * rows off that answer would describe the wrong term. And the row must be an
160
+ * edge this graph holds between the term and what the answer named — ask()
161
+ * resolves a term against the asked relation's own range, so "what contains
162
+ * Task" legitimately answers about `Task.title`'s container, which is a true
163
+ * sentence about a different pair than the one the sidebar is drawing. A row
164
+ * the graph cannot confirm is dropped rather than shown; the engine's own prose
165
+ * is where an inexact resolution belongs, because there it can say so.
166
+ *
167
+ * `grounded` is false when no question was understood as asked, which is the
168
+ * caller's signal to fall back rather than show an empty neighbourhood for a
169
+ * term the graph plainly has edges for.
170
+ *
171
+ * Returns { term, rows, asked, grounded }: `rows` in computeCodeLedger's own
172
+ * row shape so one renderer draws both, `asked` every question put to the
173
+ * engine beside what came back.
174
+ */
175
+ export function askRelatedFacts(graphPayload, term) {
176
+ const rows = [];
177
+ const asked = [];
178
+ let grounded = false;
179
+ if (!graphPayload || !term) return { term: term || null, rows, asked, grounded };
180
+
181
+ const { graph, edgesByKind, idsByLabel } = readGraph(graphPayload);
182
+ // An edge group's objectLabel is denormalized and can be shorter than the
183
+ // individual's own ("assignTo" for `Task.assignTo`), so the row list offers
184
+ // clickable terms this graph holds no individual for. There is nothing to ask
185
+ // about one, and saying so leaves the caller its row-list fallback instead of
186
+ // an answered-and-empty neighbourhood.
187
+ const termIds = idsByLabel.get(term) || [];
188
+ if (!termIds.length) return { term, rows, asked, grounded };
189
+
190
+ const seen = new Set();
191
+ for (const kind of askableRelationKinds(graph)) {
192
+ const storedEdges = edgesByKind.get(kind) || new Set();
193
+ const holdsEdge = (subjectId, objectId) => storedEdges.has(`${subjectId} ${objectId}`);
194
+ for (const { shape, query } of relationQuestions(kind, term)) {
195
+ const envelope = tmct_ask({ query }, { graph }).data;
196
+ const parsed = envelope?.parsed;
197
+ const answeredAsAsked = parsed?.shape === shape
198
+ && parsed?.kind === kind
199
+ && parsed?.object === term
200
+ && !envelope.ambiguous;
201
+ if (!answeredAsAsked) {
202
+ asked.push({ query, kind, shape, used: false, parsedAs: parsed?.canonical?.machine ?? null });
203
+ continue;
204
+ }
205
+ grounded = true;
206
+ const matches = Array.isArray(envelope.matches) ? envelope.matches : [];
207
+ let confirmed = 0;
208
+ for (const match of matches) {
209
+ if (!match?.label || !match.id) continue;
210
+ const aboutTerm = termIds.some((termId) => (shape === "reverse"
211
+ ? holdsEdge(match.id, termId)
212
+ : holdsEdge(termId, match.id)));
213
+ if (!aboutTerm) continue;
214
+ confirmed += 1;
215
+ const row = shape === "reverse"
216
+ ? { s: match.label, kind, phrase: edgePhrase(kind), o: term, sClass: match.type || "", oClass: "" }
217
+ : { s: term, kind, phrase: edgePhrase(kind), o: match.label, sClass: "", oClass: match.type || "" };
218
+ const key = rowKey(row);
219
+ if (seen.has(key)) continue;
220
+ seen.add(key);
221
+ rows.push(row);
222
+ }
223
+ asked.push({ query, kind, shape, used: true, miss: Boolean(envelope.miss), matched: matches.length, confirmed, traversal: envelope.traversal ?? null });
224
+ }
225
+ }
226
+ return { term, rows, asked, grounded };
227
+ }
228
+
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: this page's client script is still one raw-text block, and it reaches
237
+ // its factory through the bag rather than through `tmct.open()`.
238
+ publishTmctSurface({
239
+ open: createCodeExplorerSession,
240
+ ask: graphAsk,
241
+ plan: enginePlan,
242
+ page: {
243
+ createCodeExplorerSession,
244
+ askRelatedFacts,
245
+ computeCodeExplorerData,
246
+ computeCodeLedger,
247
+ generateCodeHints,
248
+ parseEntities,
249
+ normFactTerm,
250
+ registerWinkModel,
251
+ },
252
+ });
@@ -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
+ }
@@ -24,15 +24,15 @@
24
24
  // runTurn engine, the same weight class as the chat/ledger bundles, and is
25
25
  // never published.
26
26
  import { runTurn, vocabExampleHint } from "../../services/chat.mjs";
27
- import { createInMemoryStore, normFactTerm, loadMemory, readFactRows, appendFact } from "../../adapters/memory/core.mjs";
28
- import { serializeFactsJsonl } from "../../adapters/memory/export-jsonl.mjs";
27
+ import { createInMemoryStore, normFactTerm, loadMemory, readFactRows, appendFact, applySeedPayload } from "../../adapters/memory/core.mjs";
29
28
  import { splitSentencesPreservingPaths, stripCitationResidue } from "../../services/sentences.mjs";
30
29
  import { clauseCandidates, optimisticTriples } from "../../services/extract-facts.mjs";
31
30
  import { touchedFactRows } from "../../domain/memory/touched-facts.mjs";
32
31
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
33
32
  import { registerWinkModel, winkInstance } from "../../adapters/wink-model.mjs";
34
- import { memoryStats } from "./memory-stats.mjs";
33
+ import { memoryStats, exportFactsJsonl } from "./memory-stats.mjs";
35
34
  import { openPersistedStore } from "./idb-persist.mjs";
35
+ import { publishTmctSurface } from "./tmct-surface.mjs";
36
36
 
37
37
  // The pronoun subjects a bounded carry substitutes with the last unique
38
38
  // grounded subject in the SAME paragraph. Reset at every blank line, so a
@@ -178,7 +178,7 @@ export async function groundTextToFacts(text, { memoryDir, sessionId, lexicon, v
178
178
  */
179
179
  export function createIngestSession({ seedPayload = null, vocabSeeded = false } = {}) {
180
180
  const memoryDir = createInMemoryStore();
181
- if (seedPayload) memoryDir.payload = { ...memoryDir.payload, ...seedPayload };
181
+ applySeedPayload(memoryDir, seedPayload);
182
182
 
183
183
  const lexicon = loadLexicon();
184
184
  const vocabHint = vocabExampleHint(vocabSeeded);
@@ -193,16 +193,15 @@ export function createIngestSession({ seedPayload = null, vocabSeeded = false }
193
193
  };
194
194
  }
195
195
 
196
- /**
197
- * The session's whole triple store as JSONL the same
198
- * { subject, predicate, object, provenance } shape `tmct extract` and
199
- * `tmct memory --export` emit, offered to the page as the canonical download.
200
- */
201
- export async function exportFactsJsonl(memoryDir) {
202
- return serializeFactsJsonl(await loadMemory(memoryDir));
203
- }
204
-
205
- globalThis.tmctIngest = {
206
- createIngestSession, groundTextToFacts, exportFactsJsonl, registerWinkModel, normFactTerm,
207
- memoryStats, openPersistedStore,
208
- };
196
+ // This page grounds pasted prose into facts; it holds no conversation, so
197
+ // `tmct.turn` is unwired and says so, and the one call the page makes is
198
+ // `tmct.session.ingest(text)`. `tmct.page` keeps the recognizer entry point
199
+ // the page also drives directly, the wink seam, the stats fold behind its
200
+ // counters, its IndexedDB wrapper and its JSONL export.
201
+ publishTmctSurface({
202
+ open: createIngestSession,
203
+ page: {
204
+ groundTextToFacts, exportFactsJsonl, registerWinkModel, normFactTerm,
205
+ memoryStats, openPersistedStore,
206
+ },
207
+ });