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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +178 -9
  2. package/corpus/reference/index.json.gz +0 -0
  3. package/corpus/reference/manifest.json +8 -8
  4. package/corpus/reference/shards/ref-00.jsonl.gz +0 -0
  5. package/corpus/sprites/src/sprite-facts.jsonl +401 -0
  6. package/corpus/worlds/index.json.gz +0 -0
  7. package/corpus/worlds/manifest.json +49 -9
  8. package/corpus/worlds/shards/greyvale-museum.jsonl.gz +0 -0
  9. package/corpus/worlds/shards/lantern-cottage.jsonl.gz +0 -0
  10. package/corpus/worlds/shards/mud-hollow.jsonl.gz +0 -0
  11. package/corpus/worlds/shards/mud-warren.jsonl.gz +0 -0
  12. package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
  13. package/corpus/worlds/src/greyvale-museum.jsonl +136 -0
  14. package/corpus/worlds/src/lantern-cottage.jsonl +60 -0
  15. package/corpus/worlds/src/mud-hollow.jsonl +82 -0
  16. package/corpus/worlds/src/mud-warren.jsonl +124 -0
  17. package/corpus/worlds/src/spider-fly.jsonl +1 -1
  18. package/data/sprites/animal-icon.toml +11 -5
  19. package/data/sprites/book-icon.toml +9 -5
  20. package/data/sprites/cabinet-icon.toml +10 -5
  21. package/data/sprites/cellar-icon.toml +16 -6
  22. package/data/sprites/container-icon.toml +7 -3
  23. package/data/sprites/desk-icon.toml +8 -5
  24. package/data/sprites/dog-icon.toml +8 -5
  25. package/data/sprites/dog-with-colour-icon.toml +13 -12
  26. package/data/sprites/drawing-room-icon.toml +14 -5
  27. package/data/sprites/egg-icon.toml +6 -3
  28. package/data/sprites/fly-icon.toml +10 -5
  29. package/data/sprites/furniture-icon.toml +5 -3
  30. package/data/sprites/garden-icon.toml +10 -3
  31. package/data/sprites/key-icon.toml +3 -1
  32. package/data/sprites/kitchen-icon.toml +15 -6
  33. package/data/sprites/lamp-icon.toml +9 -4
  34. package/data/sprites/letter-icon.toml +7 -4
  35. package/data/sprites/library-icon.toml +14 -3
  36. package/data/sprites/pan-icon.toml +7 -3
  37. package/data/sprites/person-icon.toml +6 -2
  38. package/data/sprites/poodle-icon.toml +1 -1
  39. package/data/sprites/portable-icon.toml +6 -4
  40. package/data/sprites/portrait-icon.toml +7 -4
  41. package/data/sprites/room-icon.toml +11 -2
  42. package/data/sprites/spider-icon.toml +9 -3
  43. package/data/sprites/study-icon.toml +10 -2
  44. package/package.json +5 -4
  45. package/src/adapters/memory/core.mjs +20 -0
  46. package/src/domain/ask-vocab.mjs +71 -0
  47. package/src/domain/ask.mjs +168 -0
  48. package/src/domain/game-config.mjs +11 -0
  49. package/src/domain/grammar/ace.mjs +40 -4
  50. package/src/domain/grammar/lexicon-core.json +2 -1
  51. package/src/domain/grammar/lexicon.mjs +18 -0
  52. package/src/domain/mud-facts.mjs +15 -0
  53. package/src/domain/reference-pack.mjs +31 -7
  54. package/src/domain/router/drive.mjs +35 -9
  55. package/src/domain/router/registry.mjs +24 -4
  56. package/src/domain/router/resolver.mjs +102 -40
  57. package/src/domain/scene-compose.mjs +117 -0
  58. package/src/domain/spider-fly-world.mjs +37 -1
  59. package/src/domain/sprite-facts.mjs +0 -0
  60. package/src/domain/sprite-request.mjs +156 -0
  61. package/src/domain/sprite-templates.mjs +169 -20
  62. package/src/services/adventure-editor.mjs +8 -14
  63. package/src/services/adventure-viz.mjs +209 -157
  64. package/src/services/adventure.mjs +526 -391
  65. package/src/services/chat-page-viz.mjs +69 -25
  66. package/src/services/chat.mjs +200 -51
  67. package/src/services/code-explorer-viz.mjs +102 -62
  68. package/src/services/extract-facts.mjs +4 -7
  69. package/src/services/ingest-viz.mjs +68 -82
  70. package/src/services/ledger-viz.mjs +136 -67
  71. package/src/services/memory-panel-viz.mjs +62 -0
  72. package/src/services/mud-editor.mjs +10 -15
  73. package/src/services/mud-turn.mjs +6 -6
  74. package/src/services/mud-viz.mjs +1016 -208
  75. package/src/services/p2p-room.mjs +90 -23
  76. package/src/services/plan-pddl.mjs +3 -1
  77. package/src/services/plan-viz.mjs +123 -64
  78. package/src/services/research-viz.mjs +160 -108
  79. package/src/services/spider-fly-turn.mjs +15 -23
  80. package/src/services/spider-fly-viz.mjs +146 -161
  81. package/src/services/spider-fly.mjs +69 -11
  82. package/src/services/sprite-catalog-viz.mjs +414 -240
  83. package/src/services/viz-boot.mjs +71 -0
  84. package/src/services/viz-room-graph.mjs +203 -0
  85. package/src/services/viz-theme.mjs +90 -1
  86. package/src/services/viz-ticker.mjs +22 -0
  87. package/src/surfaces/web/adventure-browser-entry.mjs +49 -33
  88. package/src/surfaces/web/chat-browser-entry.mjs +30 -105
  89. package/src/surfaces/web/code-explorer-browser-entry.mjs +168 -24
  90. package/src/surfaces/web/ingest-browser-entry.mjs +3 -13
  91. package/src/surfaces/web/ledger-browser-entry.mjs +7 -47
  92. package/src/surfaces/web/memory-ask-browser.bundle.js +127 -124
  93. package/src/surfaces/web/memory-stats.mjs +11 -0
  94. package/src/surfaces/web/mud-browser-entry.mjs +71 -29
  95. package/src/surfaces/web/plan-browser-entry.mjs +22 -40
  96. package/src/surfaces/web/research-browser-entry.mjs +26 -41
  97. package/src/surfaces/web/spider-fly-browser-entry.mjs +45 -28
  98. package/src/surfaces/web/sprites-browser-entry.mjs +14 -27
  99. package/src/surfaces/web/turn-session.mjs +120 -0
  100. package/src/tools/definitions.mjs +30 -0
  101. package/src/tools/handlers/index.mjs +6 -3
  102. package/src/tools/handlers/kit.mjs +19 -2
  103. package/src/tools/handlers/tmct-ask.mjs +11 -6
  104. package/src/tools/handlers/tmct-ingest.mjs +5 -1
  105. package/src/tools/handlers/tmct-related.mjs +4 -4
  106. package/src/tools/handlers/tmct-sprite.mjs +147 -0
  107. package/src/tools/memory-fallthrough.mjs +9 -2
  108. package/src/tools/server.mjs +25 -1
@@ -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
@@ -76,10 +77,7 @@ import { openPersistedStore } from "./idb-persist.mjs";
76
77
  export function createChatSession({ seedPayload = null, vocabSeeded = false, liveReference = false, onLiveLookup = null, synthesisBudget = 12, digestStructures = null } = {}) {
77
78
  setDigestStructures(digestStructures || []);
78
79
  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 };
80
+ applySeedPayload(memoryDir, seedPayload);
83
81
 
84
82
  // A known-empty code graph: code-structure questions get the same honest
85
83
  // no-code-graph answer an un-pointed CLI session gives, never a crash.
@@ -88,10 +86,6 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false, liv
88
86
  const vocabHint = vocabExampleHint(vocabSeeded);
89
87
  const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
90
88
 
91
- let focus = null;
92
- let last = null;
93
- let planState = null;
94
- let researchState = null;
95
89
  // Four-state, like the CLI: false (off), true (rescue on a miss),
96
90
  // "supplement" (also append a cited read-out under every grounded vocabulary
97
91
  // answer), or "always" (widen that to every grounded answer). The two string
@@ -102,6 +96,19 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false, liv
102
96
  // page's slider sets it; 0 stores article facts without any entailed rows.
103
97
  let synthesisBudgetOn = Number.isFinite(synthesisBudget) ? synthesisBudget : 12;
104
98
 
99
+ const session = createTurnSession({
100
+ memoryDir, graph, lexicon, sessionId, vocabHint,
101
+ buildExtraOptions: () => ({
102
+ liveReference: liveReferenceOn, onLiveLookup,
103
+ uiContext: "browser", synthesisBudget: synthesisBudgetOn,
104
+ }),
105
+ // `result.liveReference` mirrors a `/wiki on|off|supplement|always`
106
+ // command back into this session's own toggle state.
107
+ captureExtraState: (result) => {
108
+ if (typeof result.liveReference === "boolean" || result.liveReference === "supplement" || result.liveReference === "always") liveReferenceOn = result.liveReference;
109
+ },
110
+ });
111
+
105
112
  return {
106
113
  memoryDir,
107
114
  sessionId,
@@ -113,92 +120,10 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false, liv
113
120
  /** The page's slider seam: set the auto-synthesis budget for every later
114
121
  * learn-on-miss load. Clamped to a non-negative integer; 0 disables it. */
115
122
  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
- },
123
+ turn: session.turn,
142
124
  };
143
125
  }
144
126
 
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
127
  /**
203
128
  * Every fact a "research <topic>" run has stored so far, in storage order —
204
129
  * the exposure the "researched this session" panel needs and runTurn's
@@ -13,14 +13,17 @@
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 { tmct_ask } from "../../tools/handlers/tmct-ask.mjs";
26
+ import { RELATIONS } from "../../domain/ask-vocab.mjs";
24
27
 
25
28
  /**
26
29
  * A browser code-explorer session over the real turn engine. Registers the
@@ -54,38 +57,179 @@ export function createCodeExplorerSession({ graphPayload, seedPayload = null, vo
54
57
  // ever read, but the code lanes still do path math (join/dirname) on it.
55
58
  const config = { graphFile: "graph.json" };
56
59
 
57
- let focus = null;
58
- let last = null;
59
- let planState = null;
60
+ const turnSession = createTurnSession({
61
+ memoryDir, graph, lexicon, sessionId, vocabHint,
62
+ // The code explorer is the one page whose turns run against a real
63
+ // provider seam (source.mjs, registered above) and a virtual graph file
64
+ // path, so both override createTurnSession's config:null/source:null
65
+ // defaults on every call.
66
+ buildExtraOptions: () => ({ config, source }),
67
+ });
60
68
 
61
69
  return {
62
70
  memoryDir,
63
71
  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
- },
72
+ turn: turnSession.turn,
80
73
  };
81
74
  }
82
75
 
76
+ // ---- "what relates to X", asked ------------------------------------------
77
+
78
+ // A symbol-grain predicate asks as its coarse sibling: ask()'s relation
79
+ // vocabulary carries one question per grain pair, not two.
80
+ const COARSE_RELATION = { callsSymbol: "calls", touchesSymbol: "touches" };
81
+
82
+ /** The two questions ask() understands about one relation kind, both spelled
83
+ * from its own vocabulary: `verbs[0]` reads as the reverse direction ("what
84
+ * couples to X"), `bare` as the forward one ("what does X import"). A relation
85
+ * taught to ask-vocab is asked here with no second table to keep in step. */
86
+ function relationQuestions(kind, term) {
87
+ const { bare, verbs } = RELATIONS[kind];
88
+ return [
89
+ { shape: "reverse", query: `what ${verbs[0]} ${term}` },
90
+ { shape: "forward", query: `what does ${term} ${bare}` },
91
+ ];
92
+ }
93
+
94
+ /** The kinds worth asking about: the ones this graph actually stores, folded
95
+ * onto ask()'s vocabulary keys. A graph carrying only imports asks two
96
+ * questions, not twenty. */
97
+ function askableRelationKinds(graph) {
98
+ const kinds = new Set();
99
+ for (const relation of graph.relations) {
100
+ if (!relation.edges.length) continue;
101
+ const kind = COARSE_RELATION[relation.predicate] || relation.predicate;
102
+ if (Object.hasOwn(RELATIONS, kind)) kinds.add(kind);
103
+ }
104
+ return [...kinds];
105
+ }
106
+
107
+ /** Every stored edge as `<subjectId> <objectId>`, keyed by the coarse relation
108
+ * kind so both grains of a pair land in one set, beside a label-to-ids index.
109
+ * An answer names the individuals it found; these two decide whether the row
110
+ * built from that answer is an edge the graph actually holds. */
111
+ function indexEdges(graph) {
112
+ const edgesByKind = new Map();
113
+ for (const relation of graph.relations) {
114
+ const kind = COARSE_RELATION[relation.predicate] || relation.predicate;
115
+ let pairs = edgesByKind.get(kind);
116
+ if (!pairs) edgesByKind.set(kind, (pairs = new Set()));
117
+ for (const edge of relation.edges) pairs.add(`${edge.subject} ${edge.object}`);
118
+ }
119
+ const idsByLabel = new Map();
120
+ for (const individual of graph.individuals) {
121
+ if (!individual?.label || !individual.id) continue;
122
+ const ids = idsByLabel.get(individual.label);
123
+ if (ids) ids.push(individual.id);
124
+ else idsByLabel.set(individual.label, [individual.id]);
125
+ }
126
+ return { edgesByKind, idsByLabel };
127
+ }
128
+
129
+ // parseEntities walks the whole payload, and a focus click re-asks the same
130
+ // graph, so one parse and one edge index per payload serve every question put
131
+ // to it.
132
+ const readGraphs = new WeakMap();
133
+ function readGraph(payload) {
134
+ let read = readGraphs.get(payload);
135
+ if (!read) {
136
+ const graph = parseEntities(payload);
137
+ read = { graph, ...indexEdges(graph) };
138
+ readGraphs.set(payload, read);
139
+ }
140
+ return read;
141
+ }
142
+
143
+ const rowKey = (row) => JSON.stringify([row.s, row.kind, row.o]);
144
+
145
+ /**
146
+ * "What relates to <term>", asked instead of computed. Every relation kind the
147
+ * loaded graph carries becomes two real tmct_ask round trips over that graph —
148
+ * the same ask() every chat turn on this page already reaches — and the
149
+ * answers' typed `matches` become the explorer sidebar's rows. The page stops
150
+ * deciding what relates to what, so the panel and the conversation cannot
151
+ * scope or phrase one question two ways.
152
+ *
153
+ * Two things have to hold before an answer becomes a row. ask() must have
154
+ * parsed the question as the question asked: an identifier that is itself a
155
+ * relation verb makes it read "what tests run" as a question about testing, and
156
+ * rows off that answer would describe the wrong term. And the row must be an
157
+ * edge this graph holds between the term and what the answer named — ask()
158
+ * resolves a term against the asked relation's own range, so "what contains
159
+ * Task" legitimately answers about `Task.title`'s container, which is a true
160
+ * sentence about a different pair than the one the sidebar is drawing. A row
161
+ * the graph cannot confirm is dropped rather than shown; the engine's own prose
162
+ * is where an inexact resolution belongs, because there it can say so.
163
+ *
164
+ * `grounded` is false when no question was understood as asked, which is the
165
+ * caller's signal to fall back rather than show an empty neighbourhood for a
166
+ * term the graph plainly has edges for.
167
+ *
168
+ * Returns { term, rows, asked, grounded }: `rows` in computeCodeLedger's own
169
+ * row shape so one renderer draws both, `asked` every question put to the
170
+ * engine beside what came back.
171
+ */
172
+ export function askRelatedFacts(graphPayload, term) {
173
+ const rows = [];
174
+ const asked = [];
175
+ let grounded = false;
176
+ if (!graphPayload || !term) return { term: term || null, rows, asked, grounded };
177
+
178
+ const { graph, edgesByKind, idsByLabel } = readGraph(graphPayload);
179
+ // An edge group's objectLabel is denormalized and can be shorter than the
180
+ // individual's own ("assignTo" for `Task.assignTo`), so the row list offers
181
+ // clickable terms this graph holds no individual for. There is nothing to ask
182
+ // about one, and saying so leaves the caller its row-list fallback instead of
183
+ // an answered-and-empty neighbourhood.
184
+ const termIds = idsByLabel.get(term) || [];
185
+ if (!termIds.length) return { term, rows, asked, grounded };
186
+
187
+ const seen = new Set();
188
+ for (const kind of askableRelationKinds(graph)) {
189
+ const storedEdges = edgesByKind.get(kind) || new Set();
190
+ const holdsEdge = (subjectId, objectId) => storedEdges.has(`${subjectId} ${objectId}`);
191
+ for (const { shape, query } of relationQuestions(kind, term)) {
192
+ const envelope = tmct_ask({ query }, { graph }).data;
193
+ const parsed = envelope?.parsed;
194
+ const answeredAsAsked = parsed?.shape === shape
195
+ && parsed?.kind === kind
196
+ && parsed?.object === term
197
+ && !envelope.ambiguous;
198
+ if (!answeredAsAsked) {
199
+ asked.push({ query, kind, shape, used: false, parsedAs: parsed?.canonical?.machine ?? null });
200
+ continue;
201
+ }
202
+ grounded = true;
203
+ const matches = Array.isArray(envelope.matches) ? envelope.matches : [];
204
+ let confirmed = 0;
205
+ for (const match of matches) {
206
+ if (!match?.label || !match.id) continue;
207
+ const aboutTerm = termIds.some((termId) => (shape === "reverse"
208
+ ? holdsEdge(match.id, termId)
209
+ : holdsEdge(termId, match.id)));
210
+ if (!aboutTerm) continue;
211
+ confirmed += 1;
212
+ const row = shape === "reverse"
213
+ ? { s: match.label, kind, phrase: edgePhrase(kind), o: term, sClass: match.type || "", oClass: "" }
214
+ : { s: term, kind, phrase: edgePhrase(kind), o: match.label, sClass: "", oClass: match.type || "" };
215
+ const key = rowKey(row);
216
+ if (seen.has(key)) continue;
217
+ seen.add(key);
218
+ rows.push(row);
219
+ }
220
+ asked.push({ query, kind, shape, used: true, miss: Boolean(envelope.miss), matched: matches.length, confirmed, traversal: envelope.traversal ?? null });
221
+ }
222
+ }
223
+ return { term, rows, asked, grounded };
224
+ }
225
+
83
226
  // 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.
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.
87
230
  globalThis.tmctCodeExplorer = {
88
231
  createCodeExplorerSession,
232
+ askRelatedFacts,
89
233
  computeCodeExplorerData,
90
234
  computeCodeLedger,
91
235
  generateCodeHints,
@@ -24,14 +24,13 @@
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";
36
35
 
37
36
  // The pronoun subjects a bounded carry substitutes with the last unique
@@ -178,7 +177,7 @@ export async function groundTextToFacts(text, { memoryDir, sessionId, lexicon, v
178
177
  */
179
178
  export function createIngestSession({ seedPayload = null, vocabSeeded = false } = {}) {
180
179
  const memoryDir = createInMemoryStore();
181
- if (seedPayload) memoryDir.payload = { ...memoryDir.payload, ...seedPayload };
180
+ applySeedPayload(memoryDir, seedPayload);
182
181
 
183
182
  const lexicon = loadLexicon();
184
183
  const vocabHint = vocabExampleHint(vocabSeeded);
@@ -193,15 +192,6 @@ export function createIngestSession({ seedPayload = null, vocabSeeded = false }
193
192
  };
194
193
  }
195
194
 
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
195
  globalThis.tmctIngest = {
206
196
  createIngestSession, groundTextToFacts, exportFactsJsonl, registerWinkModel, normFactTerm,
207
197
  memoryStats, openPersistedStore,
@@ -16,9 +16,8 @@
16
16
  // class as chat/spider-fly/adventure/plan's own browser bundles) and is
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
- import { runTurn, vocabExampleHint } from "../../services/chat.mjs";
20
- import { createInMemoryStore, normFactTerm, loadMemory } from "../../adapters/memory/core.mjs";
21
- import { serializeFactsJsonl } from "../../adapters/memory/export-jsonl.mjs";
19
+ import { vocabExampleHint } from "../../services/chat.mjs";
20
+ import { createInMemoryStore, normFactTerm, applySeedPayload } from "../../adapters/memory/core.mjs";
22
21
  import { splitSentencesPreservingPaths } from "../../services/sentences.mjs";
23
22
  import { parseEntities } from "../../domain/codegraph.mjs";
24
23
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
@@ -26,6 +25,8 @@ import { registerWinkModel } from "../../adapters/wink-model.mjs";
26
25
  import { registerResearchProvider } from "../../adapters/corpus/wikipedia-live.mjs";
27
26
  import { computeLedgerDataFromPayload } from "../../services/ledger-viz.mjs";
28
27
  import { digestTermFromPayloadBrowser } from "./digest-client.mjs";
28
+ import { createTurnSession } from "./turn-session.mjs";
29
+ import { exportFactsJsonl } from "./memory-stats.mjs";
29
30
 
30
31
  /**
31
32
  * A browser ledger-dock session over the real turn engine — createChatSession's
@@ -43,56 +44,15 @@ import { digestTermFromPayloadBrowser } from "./digest-client.mjs";
43
44
  */
44
45
  export function createLedgerSession({ seedPayload = null, vocabSeeded = false } = {}) {
45
46
  const memoryDir = createInMemoryStore();
46
- // Spread onto the store's own empty payload so a partial seed still
47
- // carries the classes/prefixes scaffolding the write path recounts —
48
- // teach turns must work regardless of what the seed payload carries.
49
- if (seedPayload) memoryDir.payload = { ...memoryDir.payload, ...seedPayload };
47
+ applySeedPayload(memoryDir, seedPayload);
50
48
 
51
49
  const graph = parseEntities({ individuals: [], objectProperties: [] });
52
50
  const lexicon = loadLexicon();
53
51
  const vocabHint = vocabExampleHint(vocabSeeded);
54
52
  const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
55
53
 
56
- let focus = null;
57
- let last = null;
58
- let planState = null;
59
- let researchState = null;
60
-
61
- return {
62
- memoryDir,
63
- sessionId,
64
-
65
- /** One dispatched turn. A throwing runTurn must never kill the session —
66
- * the page has no other chance to show this turn's answer. */
67
- async turn(line) {
68
- let result;
69
- try {
70
- result = await runTurn(line, {
71
- config: null, source: null, graph, focus, last, memoryDir, sessionId,
72
- env: {}, lexicon, vocabHint, planState, researchState,
73
- });
74
- } catch (e) {
75
- const message = e instanceof Error ? e.message : String(e);
76
- return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, record: null, research: null };
77
- }
78
- focus = result.focus;
79
- last = result.last;
80
- if ("planState" in result) planState = result.planState;
81
- if ("researchState" in result) researchState = result.researchState;
82
- // `research` distinguishes a queue snapshot (research turn) from null
83
- // (a research turn that ended the run) from undefined (not a research
84
- // turn) — the dock's controls only react to the first two.
85
- return { answer: result.answer, end: Boolean(result.end), record: result.record ?? null, research: result.research };
86
- },
87
- };
88
- }
89
-
90
- /** The session's whole triple store as JSONL — the same
91
- * { subject, predicate, object, provenance } shape `tmct extract` and
92
- * `tmct memory --export` emit. The ledger dock's "export facts" control reads
93
- * this and offers it as a download. */
94
- export async function exportFactsJsonl(memoryDir) {
95
- return serializeFactsJsonl(await loadMemory(memoryDir));
54
+ const session = createTurnSession({ memoryDir, graph, lexicon, sessionId, vocabHint });
55
+ return { memoryDir, sessionId, turn: session.turn };
96
56
  }
97
57
 
98
58
  // Re-exported so ledger-viz.mjs's own inline script never has to duplicate