@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
@@ -107,10 +107,111 @@ export function computeCodeExplorerData(payload, opts = {}) {
107
107
  return { payload, ledger, hints, focus: ledger.focus || focus, meta: { title: opts.title || "code graph" } };
108
108
  }
109
109
 
110
+ // ---- client script pieces, spliced into CLIENT_JS below -------------------
111
+ // Named module functions so the page's own source reads as a list of calls
112
+ // into real, unit-tested code rather than an inlined block. Each is written
113
+ // to survive a `.toString()` round trip into a browser `<script>` tag: no
114
+ // closure over this module's own scope, only its parameters and whichever
115
+ // sibling below it is also spliced into the same page script.
116
+
117
+ /** One line naming the loaded graph's own size: individual/edge counts, then
118
+ * up to four of its most common classes. */
119
+ export function statsSummaryLine(ledgerStats) {
120
+ var parts = [ledgerStats.individuals + " individuals", ledgerStats.edges + " edges"];
121
+ var cls = ledgerStats.classes.slice(0, 4).map(function (c) { return c[1] + " " + c[0]; });
122
+ return parts.concat(cls).join(" · ");
123
+ }
124
+
125
+ /** Every fact the chat can draw on: the graph's own edges plus whatever the
126
+ * general-knowledge seed has counted so far. */
127
+ export function factTotalText(edges, seedFacts) {
128
+ return (edges + seedFacts).toLocaleString();
129
+ }
130
+
131
+ export function rowKey(row) { return JSON.stringify([row.s, row.kind, row.o]); }
132
+
133
+ /** The sidebar's rows: the engine's own answer about the focus when it
134
+ * grounded one, the row list's plain filter otherwise, then whatever
135
+ * neither named — a fold over the rest of the graph. */
136
+ export function focusRowsHtml(data, related) {
137
+ var focus = data.focus;
138
+ var rows = data.ledger.rows;
139
+ var grounded = Boolean(related && related.grounded);
140
+ var near = grounded ? related.rows : rows.filter(function (r) { return r.s === focus || r.o === focus; });
141
+ var nearKeys = {};
142
+ near.forEach(function (r) { nearKeys[rowKey(r)] = true; });
143
+ var rest = rows.filter(function (r) { return !nearKeys[rowKey(r)]; });
144
+ var ordered = near.concat(rest);
145
+ return ordered.map(function (r) {
146
+ var hot = (r.s === focus || r.o === focus) ? " row-focus" : "";
147
+ return '<li class="row' + hot + '">'
148
+ + '<button class="term" data-term="' + escapeHtml(r.s) + '">' + escapeHtml(r.s) + '</button> '
149
+ + '<span class="verb">' + escapeHtml(r.phrase) + '</span> '
150
+ + '<button class="term" data-term="' + escapeHtml(r.o) + '">' + escapeHtml(r.o) + '</button>'
151
+ + '</li>';
152
+ }).join("") || (grounded
153
+ ? '<li class="row muted">nothing in this graph relates to ' + escapeHtml(focus) + '.</li>'
154
+ : '<li class="row muted">no edges in this graph.</li>');
155
+ }
156
+
157
+ export function hintsHtml(hints) {
158
+ return hints.map(function (h) {
159
+ return '<button class="hint" data-q="' + escapeHtml(h.text) + '" title="' + escapeHtml(h.rationale) + '">'
160
+ + escapeHtml(h.text) + '</button>';
161
+ }).join("") || '<span class="muted">nothing to suggest for this graph.</span>';
162
+ }
163
+
164
+ export function turnHtml(role, text) {
165
+ return '<span class="who">' + (role === "you" ? "you" : "tmct") + '</span><span class="said">' + escapeHtml(text) + '</span>';
166
+ }
167
+
168
+ export function chatEmptyStateHtml(hasEngine) {
169
+ return hasEngine
170
+ ? '<p class="chat-empty-head">Ask the graph, or ask it anything</p>'
171
+ + '<p>Ask about the code graph on the left, or about anything its general knowledge covers, like “what is a queue”.</p>'
172
+ + '<p class="chat-empty-hint">Try one of the questions below, or type your own.</p>'
173
+ : '<p class="chat-empty-head">Static view</p>'
174
+ + '<p>This page shows a fixed snapshot of the graph. The live chat is not available here.</p>';
175
+ }
176
+
177
+ export function mbText(n) { return (n / 1048576).toFixed(1); }
178
+
179
+ /** How many of the seed's own individuals are facts — the general-knowledge
180
+ * half of the fact-total pill. */
181
+ export function seedFactsFromPayload(payload) {
182
+ return (payload.individuals || []).filter(function (i) { return i.class === "Fact"; }).length;
183
+ }
184
+
185
+ export function seedLoadingNote(loadedBytes, totalBytes) {
186
+ return "loading general knowledge… " + mbText(loadedBytes) + (totalBytes ? " of " + mbText(totalBytes) : "") + " MB";
187
+ }
188
+
189
+ /** One retry with a cache-busting query param, fetch side only: a CDN edge
190
+ * can serve a corrupted or truncated precompressed response (a transient
191
+ * bad cache entry, not a code defect — real bytes decompress fine, and the
192
+ * same URL fetched moments later is clean), and JSON.parse throwing is the
193
+ * only signal of that. `fetcher` is `fetchWithProgress` (or a stand-in for
194
+ * a test); `baseUrl` already carries the build's own content-hash query
195
+ * string, if any, so the bust param joins with `&` or `?` accordingly. */
196
+ export async function fetchSeedPayloadWithRetry(fetcher, baseUrl, seedQuery, onProgress) {
197
+ var lastErr = null;
198
+ for (var attempt = 1; attempt <= 2; attempt++) {
199
+ try {
200
+ var bust = attempt === 1 ? "" : (seedQuery ? "&" : "?") + "retry=1";
201
+ var seedBlob = await fetcher(baseUrl + seedQuery + bust, onProgress);
202
+ var text = await seedBlob.text();
203
+ return JSON.parse(text); // throwing here is the corrupted/truncated-response retry signal
204
+ } catch (e) {
205
+ lastErr = e;
206
+ }
207
+ }
208
+ throw lastErr;
209
+ }
210
+
110
211
  const CLIENT_JS = String.raw`
111
212
  (function () {
112
213
  var DATA = window.__CODE_EXPLORER__;
113
- var api = window.tmctCodeExplorer || null;
214
+ var api = window.tmct ? window.tmct.page : null;
114
215
  var els = {
115
216
  focus: document.getElementById("focus-name"),
116
217
  ledger: document.getElementById("ledger"),
@@ -126,13 +227,17 @@ const CLIENT_JS = String.raw`
126
227
  };
127
228
  var session = null;
128
229
 
129
- var esc = ${escapeHtml.toString()};
230
+ var escapeHtml = ${escapeHtml.toString()};
231
+ var rowKey = ${rowKey.toString()};
232
+ var statsSummaryLine = ${statsSummaryLine.toString()};
233
+ var factTotalText = ${factTotalText.toString()};
234
+ var focusRowsHtml = ${focusRowsHtml.toString()};
235
+ var hintsHtml = ${hintsHtml.toString()};
236
+ var turnHtml = ${turnHtml.toString()};
237
+ var chatEmptyStateHtml = ${chatEmptyStateHtml.toString()};
130
238
 
131
239
  function renderStats(data) {
132
- var s = data.ledger.stats;
133
- var parts = [s.individuals + " individuals", s.edges + " edges"];
134
- var cls = s.classes.slice(0, 4).map(function (c) { return c[1] + " " + c[0]; });
135
- els.stats.textContent = parts.concat(cls).join(" · ");
240
+ els.stats.textContent = statsSummaryLine(data.ledger.stats);
136
241
  renderFactTotal(data);
137
242
  }
138
243
 
@@ -141,7 +246,7 @@ const CLIENT_JS = String.raw`
141
246
  function renderFactTotal(data) {
142
247
  if (!els.factTotal) return;
143
248
  var edges = (data && data.ledger && data.ledger.stats && data.ledger.stats.edges) || 0;
144
- els.factTotal.textContent = (edges + seedState.facts).toLocaleString();
249
+ els.factTotal.textContent = factTotalText(edges, seedState.facts);
145
250
  }
146
251
 
147
252
  // "What relates to the focus", put to the engine. askRelatedFacts asks
@@ -159,41 +264,19 @@ const CLIENT_JS = String.raw`
159
264
  }
160
265
  }
161
266
 
162
- function rowKey(r) { return JSON.stringify([r.s, r.kind, r.o]); }
163
-
267
+ // The engine's answer whenever it grounded one. The row list's own split is
268
+ // what is left when there is no engine (the static page) or when every
269
+ // question came back parsed as something else — an identifier that is
270
+ // itself a relation verb reads as a question about the verb. Whatever the
271
+ // neighbourhood did not already name follows, in degree order: a bulk view
272
+ // of the rest of the graph, which is a fold and not a question.
164
273
  function renderFocusRows(data, related) {
165
- var focus = data.focus;
166
- var rows = data.ledger.rows;
167
- // The engine's answer whenever it grounded one. The row list's own split is
168
- // what is left when there is no engine (the static page) or when every
169
- // question came back parsed as something else — an identifier that is
170
- // itself a relation verb reads as a question about the verb.
171
- var grounded = Boolean(related && related.grounded);
172
- var near = grounded ? related.rows : rows.filter(function (r) { return r.s === focus || r.o === focus; });
173
- var nearKeys = {};
174
- near.forEach(function (r) { nearKeys[rowKey(r)] = true; });
175
- // Whatever the neighbourhood did not already name, in degree order: a bulk
176
- // view of the rest of the graph, which is a fold and not a question.
177
- var rest = rows.filter(function (r) { return !nearKeys[rowKey(r)]; });
178
- var ordered = near.concat(rest);
179
- els.ledger.innerHTML = ordered.map(function (r) {
180
- var hot = (r.s === focus || r.o === focus) ? " row-focus" : "";
181
- return '<li class="row' + hot + '">'
182
- + '<button class="term" data-term="' + esc(r.s) + '">' + esc(r.s) + '</button> '
183
- + '<span class="verb">' + esc(r.phrase) + '</span> '
184
- + '<button class="term" data-term="' + esc(r.o) + '">' + esc(r.o) + '</button>'
185
- + '</li>';
186
- }).join("") || (grounded
187
- ? '<li class="row muted">nothing in this graph relates to ' + esc(focus) + '.</li>'
188
- : '<li class="row muted">no edges in this graph.</li>');
189
- els.focus.textContent = focus || "—";
274
+ els.ledger.innerHTML = focusRowsHtml(data, related);
275
+ els.focus.textContent = data.focus || "—";
190
276
  }
191
277
 
192
278
  function renderHints(data) {
193
- els.hints.innerHTML = data.hints.map(function (h) {
194
- return '<button class="hint" data-q="' + esc(h.text) + '" title="' + esc(h.rationale) + '">'
195
- + esc(h.text) + '</button>';
196
- }).join("") || '<span class="muted">nothing to suggest for this graph.</span>';
279
+ els.hints.innerHTML = hintsHtml(data.hints);
197
280
  }
198
281
 
199
282
  function focusOn(term) {
@@ -218,7 +301,7 @@ const CLIENT_JS = String.raw`
218
301
  clearEmptyState();
219
302
  var div = document.createElement("div");
220
303
  div.className = "turn turn-" + role;
221
- div.innerHTML = '<span class="who">' + (role === "you" ? "you" : "tmct") + '</span><span class="said">' + esc(text) + '</span>';
304
+ div.innerHTML = turnHtml(role, text);
222
305
  els.log.appendChild(div);
223
306
  els.log.scrollTop = els.log.scrollHeight;
224
307
  }
@@ -227,12 +310,7 @@ const CLIENT_JS = String.raw`
227
310
  var div = document.createElement("div");
228
311
  div.className = "chat-empty";
229
312
  div.id = "chat-empty";
230
- div.innerHTML = api
231
- ? '<p class="chat-empty-head">Ask the graph, or ask it anything</p>'
232
- + '<p>Ask about the code graph on the left, or about anything its general knowledge covers, like “what is a queue”.</p>'
233
- + '<p class="chat-empty-hint">Try one of the questions below, or type your own.</p>'
234
- : '<p class="chat-empty-head">Static view</p>'
235
- + '<p>This page shows a fixed snapshot of the graph. The live chat is not available here.</p>';
313
+ div.innerHTML = chatEmptyStateHtml(Boolean(api));
236
314
  els.log.appendChild(div);
237
315
  }
238
316
 
@@ -248,25 +326,34 @@ const CLIENT_JS = String.raw`
248
326
  var SEED_QUERY = DATA.seedStamp ? "?b=" + DATA.seedStamp : "";
249
327
  var seedState = { status: api ? "loading" : "absent", payload: null, facts: 0 };
250
328
  function seedNote(text) { if (els.seedStatus) els.seedStatus.textContent = text; }
251
- function mbText(n) { return (n / 1048576).toFixed(1); }
252
329
 
330
+ var mbText = ${mbText.toString()};
253
331
  var fetchWithProgress = ${fetchWithProgress.toString()};
332
+ var seedFactsFromPayload = ${seedFactsFromPayload.toString()};
333
+ var seedLoadingNote = ${seedLoadingNote.toString()};
334
+ var fetchSeedPayloadWithRetry = ${fetchSeedPayloadWithRetry.toString()};
335
+
336
+ // The desktop shell's readSeed() reads off local disk, not a CDN, so it
337
+ // keeps its single attempt in loadSeed() below; only this fetch path retries.
338
+ function fetchSeedPayload() {
339
+ return fetchSeedPayloadWithRetry(fetchWithProgress, "./chat-seed.json", SEED_QUERY, function (loaded, total) {
340
+ seedNote(seedLoadingNote(loaded, total));
341
+ });
342
+ }
254
343
 
255
344
  async function loadSeed() {
256
345
  try {
257
- var text = null;
346
+ var payload = null;
258
347
  if (window.tmctDesktop && typeof window.tmctDesktop.readSeed === "function") {
259
348
  seedNote("loading general knowledge…");
260
- text = await window.tmctDesktop.readSeed();
349
+ var text = await window.tmctDesktop.readSeed();
350
+ if (text) payload = JSON.parse(text);
261
351
  } else {
262
- var seedBlob = await fetchWithProgress("./chat-seed.json" + SEED_QUERY, function (loaded, total) {
263
- seedNote("loading general knowledge… " + mbText(loaded) + (total ? " of " + mbText(total) : "") + " MB");
264
- });
265
- text = await seedBlob.text();
352
+ payload = await fetchSeedPayload();
266
353
  }
267
- if (text) {
268
- seedState.payload = JSON.parse(text);
269
- seedState.facts = (seedState.payload.individuals || []).filter(function (i) { return i.class === "Fact"; }).length;
354
+ if (payload) {
355
+ seedState.payload = payload;
356
+ seedState.facts = seedFactsFromPayload(seedState.payload);
270
357
  seedState.status = "ready";
271
358
  seedNote("general knowledge: " + seedState.facts + " facts");
272
359
  renderFactTotal(DATA);
@@ -39,7 +39,7 @@ export { relationKind, impactClosure } from "../domain/codegraph.mjs";
39
39
  export { createGraphService } from "../adapters/providers/graph-service.mjs";
40
40
 
41
41
  // Tool dispatch (slash-commands and CLI tool calls route through here).
42
- export { dispatchTool } from "../tools/server.mjs";
42
+ export { dispatchTool, dispatchToolStructured } from "../tools/server.mjs";
43
43
 
44
44
  // Conversational memory — tmct's OWN OWL-labelled graph under
45
45
  // .tmct/memory/, distinct from any provider-supplied code graph.
@@ -2,7 +2,7 @@
2
2
  // self-contained document shaped exactly like chat-page-viz.mjs's own
3
3
  // page-builder — one inlined <style> importing viz-theme.mjs's shared tokens,
4
4
  // behaviour as an inlined IIFE — running the ingest engine
5
- // (ingest-browser.bundle.js's globalThis.tmctIngest) by same-origin relative
5
+ // (ingest-browser.bundle.js's globalThis.tmct) by same-origin relative
6
6
  // paths.
7
7
  //
8
8
  // The page's own chrome is a two-pane translate-tool layout: mode pills
@@ -113,7 +113,8 @@ ${THEME_TOKENS_CSS}
113
113
  .browse { font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); border: 1px solid var(--line); border-radius: 4px; padding: .16rem .55rem; background: var(--card); }
114
114
  .browse:hover { color: var(--ink); }
115
115
 
116
- /* right: the canonical facts, on the soft panel background. */
116
+ /* right: the canonical facts, on the soft panel background, with the ask
117
+ dock pinned under them — you read what landed, then question it in place. */
117
118
  .outPane { background: var(--card); }
118
119
  #facts { flex: 1 1 auto; overflow-y: auto; padding: .6rem .9rem 1rem; font-family: ${MONO_STACK}; font-size: .8rem; }
119
120
  #facts .fact { display: grid; grid-template-columns: 1fr auto 1fr; gap: .5rem; align-items: baseline; padding: .3rem 0; border-bottom: 1px solid var(--line); }
@@ -123,6 +124,24 @@ ${THEME_TOKENS_CSS}
123
124
  #facts .fact .prov { grid-column: 1 / -1; color: var(--muted); font-size: .66rem; }
124
125
  #facts .empty { color: var(--muted); text-align: center; max-width: 24rem; line-height: 1.6; margin: 3rem auto 0; }
125
126
 
127
+ /* the ask dock: one line in, one answer out, against the graph this session
128
+ projects from what it has ingested. */
129
+ .askDock { flex: 0 0 auto; border-top: 1px solid var(--line); display: flex; flex-direction: column; }
130
+ #askLog { max-height: 11rem; overflow-y: auto; padding: .5rem .9rem 0; font-family: ${MONO_STACK}; font-size: .78rem; }
131
+ #askLog:empty { display: none; }
132
+ .askLine { margin: 0 0 .35rem; white-space: pre-wrap; word-break: break-word; }
133
+ .askLine.q { color: var(--muted); }
134
+ .askLine.q::before { content: "> "; }
135
+ .askLine.a { color: var(--ink); }
136
+ .askLine.detail { color: var(--muted); font-size: .68rem; }
137
+ .askLine.miss { color: var(--muted); font-style: italic; }
138
+ .askRow { display: flex; align-items: center; gap: .5rem; padding: .5rem .9rem .6rem; }
139
+ #askq { flex: 1 1 auto; min-width: 0; border: 1px solid var(--line); border-radius: 6px; background: var(--bg); color: var(--ink); font-family: ${MONO_STACK}; font-size: .78rem; padding: .35rem .6rem; }
140
+ #askq::placeholder { color: var(--muted); }
141
+ #askq:disabled { opacity: .55; }
142
+ .askGo { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .32rem .85rem; background: var(--card); }
143
+ .askGo:disabled { opacity: .45; cursor: default; }
144
+
126
145
  .actions { flex: 0 0 auto; display: flex; align-items: center; gap: .6rem; padding: .6rem 1.1rem; border-top: 1px solid var(--line); flex-wrap: wrap; }
127
146
  .actions .btn { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .35rem .9rem; background: var(--card); }
128
147
  .actions .btn.primary { background: var(--ink); color: var(--bg); border-color: var(--ink); }
@@ -133,7 +152,7 @@ ${THEME_TOKENS_CSS}
133
152
  the right of the ingest column (a real layout column, not an overlay) —
134
153
  the same class names and breakpoint chat-page-viz.mjs's own docked panel
135
154
  uses, re-rendered after boot and after every ingest from
136
- window.tmctIngest's own memoryStats(). */
155
+ window.tmct's own memoryStats(). */
137
156
  .statsPanel { flex: 0 0 300px; max-width: 300px; overflow-y: auto; border-left: 1px solid var(--line); padding: 1.1rem 1.2rem 1.6rem; font-family: ${MONO_STACK}; font-size: .74rem; line-height: 1.55; display: flex; flex-direction: column; }
138
157
  .statsPanel h2 { font-size: .66rem; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); margin: 1.3rem 0 .5rem; }
139
158
  .statsPanel h2:first-child { margin-top: 0; }
@@ -203,6 +222,14 @@ ${THEME_TOKENS_CSS}
203
222
  <span id="factCount" class="mono"></span>
204
223
  </div>
205
224
  <div id="facts"><p class="empty">Nothing ingested yet. The facts it grounds will appear here as it reads.</p></div>
225
+ <form class="askDock" id="askForm" autocomplete="off">
226
+ <div id="askLog" aria-live="polite"></div>
227
+ <div class="askRow">
228
+ <input type="text" id="askq" aria-label="Ask about what you have ingested"
229
+ placeholder="ask about what you&rsquo;ve ingested&hellip;" disabled>
230
+ <button type="submit" class="askGo" id="askGo" disabled>ask</button>
231
+ </div>
232
+ </form>
206
233
  </section>
207
234
  </main>
208
235
  <div class="actions">
@@ -248,6 +275,10 @@ ${THEME_TOKENS_CSS}
248
275
  const fuzzyToggleEl = el("fuzzyToggle");
249
276
  const statsPanelEl = el("statsPanel");
250
277
  const factPillValueEl = el("factPillValue");
278
+ const askFormEl = el("askForm");
279
+ const askInputEl = el("askq");
280
+ const askGoBtn = el("askGo");
281
+ const askLogEl = el("askLog");
251
282
 
252
283
  let session = null;
253
284
  let grounded = 0; // facts on show in the right pane, from the CURRENT ingest only
@@ -356,11 +387,86 @@ ${THEME_TOKENS_CSS}
356
387
  factsEl.scrollTop = factsEl.scrollHeight;
357
388
  }
358
389
 
390
+ // ---- the ask dock: one question, put to what this session has ingested ---
391
+ // Two real routes, tried in order, and neither one guesses. window.tmct.ask
392
+ // puts the question to the graph this session projects from its OWN grounded
393
+ // rows (ingest-facts.mjs), which is what answers a listing — "list dogs"
394
+ // reads the beagles actually on record. On a miss, window.tmct.turn runs the
395
+ // full chat turn engine over the same store, which reads a term's own facts
396
+ // back ("what is a beagle") and takes a taught line ("remember: …") the
397
+ // graph shape has no lane for. Both missing is the honest wall, and the note
398
+ // there names the kinds this memory really holds instead of inventing an
399
+ // example question.
400
+ let asking = false;
401
+
402
+ function updateAskEnabled() {
403
+ const ready = Boolean(session) && !asking;
404
+ askInputEl.disabled = !ready;
405
+ askGoBtn.disabled = !ready || !askInputEl.value.trim();
406
+ }
407
+
408
+ function addAskLine(cls, text) {
409
+ const line = document.createElement("div");
410
+ line.className = "askLine " + cls;
411
+ line.textContent = text;
412
+ askLogEl.appendChild(line);
413
+ askLogEl.scrollTop = askLogEl.scrollHeight;
414
+ }
415
+
416
+ // The engine writes its answer first and its reasoning trailers ("Goal
417
+ // (inferred): …", "Canonical: …") after a blank line. The answer leads; the
418
+ // trailers stay, quieter, because they are how a reader audits it.
419
+ function addAskAnswer(answer) {
420
+ const blocks = String(answer).split(/\\n{2,}/).map(function (b) { return b.trim(); }).filter(Boolean);
421
+ if (!blocks.length) return;
422
+ addAskLine("a", blocks[0]);
423
+ for (const block of blocks.slice(1)) addAskLine("detail", block);
424
+ }
425
+
426
+ function askMissNote() {
427
+ const kinds = session && session.askableClasses ? session.askableClasses() : [];
428
+ if (!kinds.length) return "Nothing of your own is in this session's memory yet. Ingest some text first.";
429
+ return "I can't ground that in what you've ingested. The kinds it holds: " + kinds.slice(0, 8).join(", ") + ".";
430
+ }
431
+
432
+ askInputEl.addEventListener("input", updateAskEnabled);
433
+ askFormEl.addEventListener("submit", async (e) => {
434
+ e.preventDefault();
435
+ const q = askInputEl.value.trim();
436
+ if (!q || asking || !session) return;
437
+ asking = true;
438
+ askInputEl.value = "";
439
+ updateAskEnabled();
440
+ addAskLine("q", q);
441
+ try {
442
+ let asked = null;
443
+ try { asked = await window.tmct.ask(q); } catch { asked = null; }
444
+ if (asked && asked.answer && !asked.miss) { addAskAnswer(asked.answer); return; }
445
+ let turned = null;
446
+ try { turned = await window.tmct.turn(q); } catch { turned = null; }
447
+ if (turned && turned.answer && !(turned.record && turned.record.miss)) {
448
+ addAskAnswer(turned.answer);
449
+ // A taught line writes to the same store an ingest does, so it earns
450
+ // the same debounced save and the same panel refresh.
451
+ if (turned.record && turned.record.via === "assert") {
452
+ scheduleSave();
453
+ await renderStatsPanel();
454
+ }
455
+ return;
456
+ }
457
+ addAskLine("miss", askMissNote());
458
+ } finally {
459
+ asking = false;
460
+ updateAskEnabled();
461
+ askInputEl.focus();
462
+ }
463
+ });
464
+
359
465
  // ---- memory stats: the docked panel, same convention as chat.html --------
360
466
  async function renderStatsPanel(stats) {
361
467
  if (!stats) {
362
- if (!session || !window.tmctIngest.memoryStats) return;
363
- try { stats = await window.tmctIngest.memoryStats(session.memoryDir); }
468
+ if (!session || !window.tmct.page.memoryStats) return;
469
+ try { stats = await window.tmct.page.memoryStats(session.memoryDir); }
364
470
  catch { return; }
365
471
  }
366
472
  factPillValueEl.textContent = Number(stats.total || 0).toLocaleString();
@@ -402,26 +508,38 @@ ${THEME_TOKENS_CSS}
402
508
  // The one branch this page's seed choice makes: checked, fetch and parse
403
509
  // the same chat-seed.json chat.html embeds; unchecked, skip the request
404
510
  // outright and stay on the previous empty-store fast path.
511
+ //
512
+ // One retry with a cache-busting query param: a CDN edge can serve a
513
+ // corrupted or truncated precompressed response (a transient bad cache
514
+ // entry, not a code defect — real bytes decompress fine, and the same URL
515
+ // fetched moments later is clean), and JSON.parse throwing is the only
516
+ // signal of that. The bust param forces a fresh fetch past that one entry.
405
517
  async function fetchSeedIfWanted() {
406
518
  if (!seedToggleEl.checked) { seedPayload = null; seedFacts = 0; return; }
407
- try {
408
- const blob = await fetchWithProgress("./chat-seed.json" + SEED_QUERY, (loaded, total) => noteProgress("seed", loaded, total));
409
- seedPayload = JSON.parse(await blob.text());
410
- seedFacts = (seedPayload.individuals || []).filter((i) => i.class === "Fact").length;
411
- } catch (err) {
412
- seedPayload = null;
413
- seedFacts = 0;
414
- console.warn("tmct ingest: chat-seed.json unavailable — starting unseeded", err);
519
+ for (let attempt = 1; attempt <= 2; attempt++) {
520
+ try {
521
+ const bust = attempt === 1 ? "" : (SEED_QUERY ? "&" : "?") + "retry=1";
522
+ const blob = await fetchWithProgress("./chat-seed.json" + SEED_QUERY + bust, (loaded, total) => noteProgress("seed", loaded, total));
523
+ seedPayload = JSON.parse(await blob.text());
524
+ seedFacts = (seedPayload.individuals || []).filter((i) => i.class === "Fact").length;
525
+ return;
526
+ } catch (err) {
527
+ if (attempt === 2) {
528
+ seedPayload = null;
529
+ seedFacts = 0;
530
+ console.warn("tmct ingest: chat-seed.json unavailable — starting unseeded", err);
531
+ }
532
+ }
415
533
  }
416
534
  }
417
535
  const cloneMemoryPayload = ${cloneMemoryPayload.toString()};
418
- function newSession() {
419
- return window.tmctIngest.createIngestSession({ seedPayload: cloneMemoryPayload(seedPayload), vocabSeeded: Boolean(seedPayload) });
536
+ async function newSession() {
537
+ return window.tmct.open({ seedPayload: cloneMemoryPayload(seedPayload), vocabSeeded: Boolean(seedPayload) });
420
538
  }
421
539
 
422
540
  // ---- engine boot ---------------------------------------------------------
423
541
  const loadWinkVendor = ${loadWinkVendor.toString()};
424
- const tryLoadWink = loadWinkVendor({ register: (factory) => window.tmctIngest.registerWinkModel(factory) });
542
+ const tryLoadWink = loadWinkVendor({ register: (factory) => window.tmct.page.registerWinkModel(factory) });
425
543
 
426
544
  // The deploy's own version, read off the service worker file the build
427
545
  // already stamps — the only same-origin place the number exists at runtime
@@ -467,10 +585,12 @@ ${THEME_TOKENS_CSS}
467
585
  clearTimeout(saveTimer);
468
586
  saveTimer = null;
469
587
  if (persist) await persist.clear();
470
- session = newSession();
588
+ session = await newSession();
471
589
  clearFactsPane();
590
+ askLogEl.textContent = "";
472
591
  updateIngestEnabled();
473
- const stats = await window.tmctIngest.memoryStats(session.memoryDir);
592
+ updateAskEnabled();
593
+ const stats = await window.tmct.page.memoryStats(session.memoryDir);
474
594
  statusEl.textContent = "forgot everything taught on this device. Back to the fresh seed (" + statsSummaryLine(stats, bandLabelFor) + ").";
475
595
  await renderStatsPanel(stats);
476
596
  }
@@ -486,17 +606,19 @@ ${THEME_TOKENS_CSS}
486
606
  await fetchSeedIfWanted();
487
607
  clearTimeout(saveTimer);
488
608
  saveTimer = null;
489
- session = newSession();
609
+ session = await newSession();
490
610
  clearFactsPane();
491
- const stats = await window.tmctIngest.memoryStats(session.memoryDir);
611
+ askLogEl.textContent = "";
612
+ const stats = await window.tmct.page.memoryStats(session.memoryDir);
492
613
  statusEl.textContent = statsSummaryLine(stats, bandLabelFor) + ". Ready.";
493
614
  await renderStatsPanel(stats);
494
615
  updateIngestEnabled();
616
+ updateAskEnabled();
495
617
  sourceEl.focus();
496
618
  });
497
619
 
498
620
  async function boot() {
499
- if (!window.tmctIngest) {
621
+ if (!window.tmct) {
500
622
  statusEl.textContent = "the ingest engine didn't load. This page needs its build step (npm run demo:build)";
501
623
  return;
502
624
  }
@@ -507,16 +629,17 @@ ${THEME_TOKENS_CSS}
507
629
  fetchSiteVersion().then((v) => { siteVersion = v; }),
508
630
  ]);
509
631
  progressActive = false;
510
- if (window.tmctIngest.openPersistedStore) {
511
- persist = window.tmctIngest.openPersistedStore({ storeKey: "ingest", stamp: siteVersion + ":" + seedFacts + ":" + SEED_STAMP });
632
+ if (window.tmct.page.openPersistedStore) {
633
+ persist = window.tmct.page.openPersistedStore({ storeKey: "ingest", stamp: siteVersion + ":" + seedFacts + ":" + SEED_STAMP });
512
634
  }
513
635
  const savedRecord = persist ? await persist.load() : null;
514
636
  session = savedRecord && savedRecord.payload
515
- ? window.tmctIngest.createIngestSession({ seedPayload: savedRecord.payload, vocabSeeded: true })
516
- : newSession();
637
+ ? await window.tmct.open({ seedPayload: savedRecord.payload, vocabSeeded: true })
638
+ : await newSession();
517
639
  setMode(false);
518
640
  updateIngestEnabled();
519
- const stats = await window.tmctIngest.memoryStats(session.memoryDir);
641
+ updateAskEnabled();
642
+ const stats = await window.tmct.page.memoryStats(session.memoryDir);
520
643
  const winkPart = winkStatus === "loaded"
521
644
  ? "wink-nlp: loaded"
522
645
  : "wink-nlp unavailable. The recognizer can't split sentences without it";
@@ -569,10 +692,10 @@ ${THEME_TOKENS_CSS}
569
692
 
570
693
  // ---- download the canonical facts as JSONL -------------------------------
571
694
  downloadBtn.addEventListener("click", async () => {
572
- if (!session || !window.tmctIngest.exportFactsJsonl) return;
695
+ if (!session || !window.tmct.page.exportFactsJsonl) return;
573
696
  let jsonl;
574
697
  try {
575
- jsonl = await window.tmctIngest.exportFactsJsonl(session.memoryDir);
698
+ jsonl = await window.tmct.page.exportFactsJsonl(session.memoryDir);
576
699
  } catch (err) {
577
700
  statusEl.textContent = "couldn't build the download (" + (err && err.message ? err.message : err) + ")";
578
701
  return;
@@ -609,6 +732,7 @@ ${THEME_TOKENS_CSS}
609
732
  sourceTag = "pasted text";
610
733
  srcLabel.textContent = modeDocBtn.getAttribute("aria-pressed") === "true" ? "drop or browse for a file" : "pasted text";
611
734
  clearFactsPane();
735
+ askLogEl.textContent = "";
612
736
  statusEl.textContent = "cleared";
613
737
  updateIngestEnabled();
614
738
  sourceEl.focus();
@@ -618,6 +742,7 @@ ${THEME_TOKENS_CSS}
618
742
  console.error("tmct ingest failed to boot", err);
619
743
  statusEl.textContent = "the ingest page failed to start (" + (err && err.message ? err.message : err) + ")";
620
744
  });
745
+ window.tmct.ready = window.tmctIngestReady;
621
746
  })();
622
747
  </script>
623
748
  </body>