@polycode-projects/the-mechanical-code-talker 2.10.5 → 2.11.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 (40) hide show
  1. package/README.md +2 -2
  2. package/corpus/sprites/src/sprite-facts.jsonl +18 -0
  3. package/corpus/worlds/manifest.json +5 -5
  4. package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
  5. package/corpus/worlds/src/ashcombe-hall.jsonl +27 -0
  6. package/data/sprites/book-icon.toml +12 -0
  7. package/data/sprites/cellar-icon.toml +12 -0
  8. package/data/sprites/drawing-room-icon.toml +13 -0
  9. package/data/sprites/garden-icon.toml +12 -0
  10. package/data/sprites/kitchen-icon.toml +13 -0
  11. package/data/sprites/library-icon.toml +12 -0
  12. package/data/sprites/pan-icon.toml +11 -0
  13. package/data/sprites/study-icon.toml +12 -0
  14. package/package.json +5 -2
  15. package/src/adapters/corpus/wikipedia-live.mjs +182 -26
  16. package/src/adapters/corpus/worlds-pack.mjs +8 -2
  17. package/src/adapters/toml-config.mjs +6 -0
  18. package/src/domain/memory/trust.mjs +11 -0
  19. package/src/domain/worlds-pack.mjs +50 -0
  20. package/src/services/adventure-autoplay.mjs +5 -2
  21. package/src/services/adventure-viz.mjs +301 -33
  22. package/src/services/adventure.mjs +162 -14
  23. package/src/services/chat-page-viz.mjs +265 -189
  24. package/src/services/chat-session.mjs +15 -5
  25. package/src/services/chat.mjs +286 -43
  26. package/src/services/code-explorer-viz.mjs +183 -75
  27. package/src/services/extract-facts.mjs +118 -28
  28. package/src/services/ingest-viz.mjs +328 -79
  29. package/src/services/ledger-viz.mjs +99 -0
  30. package/src/services/memory-panel-viz.mjs +159 -0
  31. package/src/services/research.mjs +266 -0
  32. package/src/services/sentences.mjs +19 -0
  33. package/src/services/spider-fly-viz.mjs +21 -5
  34. package/src/surfaces/web/adventure-browser-entry.mjs +9 -5
  35. package/src/surfaces/web/chat-browser-entry.mjs +28 -11
  36. package/src/surfaces/web/code-explorer-browser-entry.mjs +27 -11
  37. package/src/surfaces/web/ingest-browser-entry.mjs +123 -41
  38. package/src/surfaces/web/ledger-browser-entry.mjs +10 -4
  39. package/src/surfaces/web/memory-ask-browser.bundle.js +112 -112
  40. package/src/surfaces/web/memory-stats.mjs +53 -0
@@ -1,13 +1,20 @@
1
- // code-explorer-viz.mjs — the code-graph "ledger" the desktop shell renders:
2
- // each import/call/contains edge read back as a plain sentence, a hint rail of
3
- // suggested next queries, and a live chat dock over the same graph. The two
4
- // derivations are pure so the shell, the packaging script, and the unit tests
5
- // all share one code path; renderCodeExplorerHtml builds one self-contained
6
- // document with no external requests.
1
+ // code-explorer-viz.mjs — the code explorer as a full-viewport IDE shell:
2
+ // a title bar (graph source, Open graph…/Open repo…), an explorer sidebar
3
+ // reading each import/call/contains edge back as a plain sentence, a chat
4
+ // centre over the same graph with a rail of suggested questions, and a status
5
+ // bar carrying the graph's own counts. The chat session seeds BOTH the loaded
6
+ // code graph and chat.html's general-knowledge bands (./chat-seed.json,
7
+ // fetched lazily at runtime), so one conversation answers "what is a queue"
8
+ // and "what imports src/core/model.mjs" alike — and degrades to graph-only
9
+ // when the seed asset is unavailable.
7
10
  //
8
- // The channel is deliberately thin: this is the SAME ledger-pattern UI the
9
- // browser ledger page uses, refocused on a code graph, and it stays servable
10
- // as a plain page only the Electron shell around it (electron/) is desktop.
11
+ // The derivations are pure so the shell, the packaging scripts, and the unit
12
+ // tests all share one code path; renderCodeExplorerHtml builds one
13
+ // self-contained document that both packagers ship unchanged the site build
14
+ // (scripts/build-demo-site.mjs) and the Electron desktop shell
15
+ // (scripts/build-electron-app.mjs). Panels are plain sections inside the
16
+ // sidebar/centre grid, so a later panel is one more <section class="panel">,
17
+ // not a re-architecture.
11
18
 
12
19
  import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText } from "./viz-theme.mjs";
13
20
  import { generateCodeHints } from "../domain/code-explorer-hints.mjs";
@@ -121,6 +128,7 @@ const CLIENT_JS = String.raw`
121
128
  input: document.getElementById("chat-input"),
122
129
  dockNote: document.getElementById("dock-note"),
123
130
  source: document.getElementById("source-name"),
131
+ seedStatus: document.getElementById("seed-status"),
124
132
  };
125
133
  var session = null;
126
134
 
@@ -174,7 +182,7 @@ const CLIENT_JS = String.raw`
174
182
  renderHints(data);
175
183
  }
176
184
 
177
- function appendLog(role, text) {
185
+ function appendTurn(role, text) {
178
186
  var div = document.createElement("div");
179
187
  div.className = "turn turn-" + role;
180
188
  div.innerHTML = '<span class="who">' + (role === "you" ? "you" : "tmct") + '</span><span class="said">' + esc(text) + '</span>';
@@ -182,23 +190,93 @@ const CLIENT_JS = String.raw`
182
190
  els.log.scrollTop = els.log.scrollHeight;
183
191
  }
184
192
 
193
+ function appendNote(text) {
194
+ var div = document.createElement("div");
195
+ div.className = "turn turn-note";
196
+ div.textContent = text;
197
+ els.log.appendChild(div);
198
+ }
199
+
200
+ // ---- the general-knowledge seed --------------------------------------
201
+ // The same chat-seed.json chat.html boots from, loaded lazily so the shell
202
+ // paints instantly: same-origin fetch on the site, the preload bridge's
203
+ // readSeed under the desktop shell (a file:// page cannot fetch). The
204
+ // status bar narrates the load; a missing or failing seed leaves the chat
205
+ // graph-only and says so, never broken.
206
+ var seedState = { status: api ? "loading" : "absent", payload: null, facts: 0 };
207
+ function seedNote(text) { if (els.seedStatus) els.seedStatus.textContent = text; }
208
+ function mbText(n) { return (n / 1048576).toFixed(1); }
209
+
210
+ async function fetchTextWithProgress(url, onProgress) {
211
+ var res = await fetch(url);
212
+ if (!res.ok) throw new Error("HTTP " + res.status);
213
+ var total = Number(res.headers.get("content-length")) || 0;
214
+ if (!res.body || !res.body.getReader) return res.text();
215
+ var reader = res.body.getReader();
216
+ var chunks = [];
217
+ var loaded = 0;
218
+ for (;;) {
219
+ var step = await reader.read();
220
+ if (step.done) break;
221
+ chunks.push(step.value);
222
+ loaded += step.value.byteLength;
223
+ onProgress(loaded, total);
224
+ }
225
+ return new Blob(chunks).text();
226
+ }
227
+
228
+ async function loadSeed() {
229
+ try {
230
+ var text = null;
231
+ if (window.tmctDesktop && typeof window.tmctDesktop.readSeed === "function") {
232
+ seedNote("loading general knowledge…");
233
+ text = await window.tmctDesktop.readSeed();
234
+ } else {
235
+ text = await fetchTextWithProgress("./chat-seed.json", function (loaded, total) {
236
+ seedNote("loading general knowledge… " + mbText(loaded) + (total ? " of " + mbText(total) : "") + " MB");
237
+ });
238
+ }
239
+ if (text) {
240
+ seedState.payload = JSON.parse(text);
241
+ seedState.facts = (seedState.payload.individuals || []).filter(function (i) { return i.class === "Fact"; }).length;
242
+ seedState.status = "ready";
243
+ seedNote("general knowledge: " + seedState.facts + " facts");
244
+ return;
245
+ }
246
+ } catch (e) {
247
+ console.warn("tmct code explorer: chat-seed unavailable, continuing graph-only", e);
248
+ }
249
+ seedState.status = "absent";
250
+ seedNote("graph-only — general knowledge unavailable");
251
+ }
252
+ var seedPromise = api ? loadSeed() : Promise.resolve();
253
+
254
+ function cloneSeed() {
255
+ if (!seedState.payload) return null;
256
+ try { return structuredClone(seedState.payload); } catch (e) { return JSON.parse(JSON.stringify(seedState.payload)); }
257
+ }
258
+
185
259
  async function ensureSession() {
186
260
  if (session || !api || !api.createCodeExplorerSession) return session;
187
- var winkLoaded = true;
188
261
  if (api.registerWinkModel && window.__WINK_LOADER__) {
189
262
  try { var mod = await window.__WINK_LOADER__(); api.registerWinkModel(function () { return mod; }); }
190
- catch (e) { winkLoaded = false; }
263
+ catch (e) { /* the lemma/POS tier is optional */ }
191
264
  }
192
- session = api.createCodeExplorerSession({ graphPayload: DATA.payload });
265
+ await seedPromise;
266
+ session = api.createCodeExplorerSession({
267
+ graphPayload: DATA.payload,
268
+ seedPayload: cloneSeed(),
269
+ vocabSeeded: seedState.status === "ready",
270
+ });
193
271
  return session;
194
272
  }
195
273
 
196
274
  async function ask(q) {
197
- appendLog("you", q);
275
+ appendTurn("you", q);
198
276
  var s = await ensureSession();
199
- if (!s) { appendLog("tmct", "the live dock is not loaded on this page."); return; }
277
+ if (!s) { appendTurn("tmct", "the live chat is not loaded on this page."); return; }
200
278
  var res = await s.turn(q);
201
- appendLog("tmct", res.answer);
279
+ appendTurn("tmct", res.answer);
202
280
  }
203
281
 
204
282
  // Delegate term + hint clicks.
@@ -219,7 +297,8 @@ const CLIENT_JS = String.raw`
219
297
  });
220
298
  }
221
299
 
222
- // Desktop pickers, present only under the Electron shell.
300
+ // Desktop pickers, present only under the Electron shell. A swapped graph
301
+ // starts a fresh session; the general-knowledge seed carries over.
223
302
  function wirePicker(id, method, updateSource) {
224
303
  var btn = document.getElementById(id);
225
304
  if (!btn) return;
@@ -245,8 +324,11 @@ const CLIENT_JS = String.raw`
245
324
  wirePicker("open-repo", "openRepo", true);
246
325
 
247
326
  if (!api) {
248
- if (els.dockNote) els.dockNote.textContent = "static view — the live chat dock is unavailable on this page.";
327
+ if (els.dockNote) els.dockNote.textContent = "static view — the live chat is unavailable on this page.";
249
328
  if (els.input) els.input.disabled = true;
329
+ seedNote("static view");
330
+ } else {
331
+ appendNote("Ask about this code graph — or anything its general knowledge covers, like “what is a queue”.");
250
332
  }
251
333
 
252
334
  mountView(DATA);
@@ -254,8 +336,8 @@ const CLIENT_JS = String.raw`
254
336
  `;
255
337
 
256
338
  /**
257
- * One self-contained HTML document for the code explorer. `data` is
258
- * computeCodeExplorerData's output. `bundleInline` inlines the dock engine
339
+ * One self-contained HTML document: the full-viewport IDE shell over
340
+ * computeCodeExplorerData's output. `bundleInline` inlines the chat engine
259
341
  * (for a single-file page / a data: URL); otherwise `bundleAvailable` links
260
342
  * `./code-explorer.bundle.js`. `winkLoaderInline` optionally inlines a wink
261
343
  * model loader as `window.__WINK_LOADER__`. `showDesktopLink` adds a line
@@ -265,7 +347,6 @@ const CLIENT_JS = String.raw`
265
347
  export function renderCodeExplorerHtml(data, { bundleInline = "", bundleAvailable = false, winkLoaderInline = "", sourceName = "demo code graph", showDesktopLink = false } = {}) {
266
348
  const payloadJson = embedJson(data.payload);
267
349
  const dataJson = embedJson({ ledger: data.ledger, hints: data.hints, focus: data.focus, meta: data.meta });
268
- const title = escapeHtml(data.meta?.title || "code explorer");
269
350
 
270
351
  return `<!doctype html>
271
352
  <html lang="en">
@@ -276,75 +357,102 @@ export function renderCodeExplorerHtml(data, { bundleInline = "", bundleAvailabl
276
357
  <style>
277
358
  ${THEME_TOKENS_CSS}
278
359
  * { box-sizing: border-box; }
279
- body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; }
280
- header { display: flex; align-items: baseline; gap: 1rem; flex-wrap: wrap; padding: 0.8rem 1.1rem; border-bottom: 1px solid var(--line); }
281
- header h1 { font-size: 1.05rem; margin: 0; font-weight: 600; }
282
- header .sub { color: var(--muted); font-size: 0.85rem; }
283
- header .pickers { margin-left: auto; display: flex; gap: 0.5rem; }
360
+ html, body { height: 100%; }
361
+ body { margin: 0; overflow: hidden; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; }
362
+ .shell { height: 100%; display: grid; grid-template-rows: auto minmax(0, 1fr) auto; }
363
+
364
+ .titlebar { display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap; padding: 0.5rem 0.9rem; background: var(--card); border-bottom: 1px solid var(--line); box-shadow: inset 0 -2px 0 var(--entail-soft); }
365
+ .titlebar .mark { width: 15px; height: 15px; color: var(--entail); flex: none; }
366
+ .titlebar h1 { font-size: 0.95rem; margin: 0; font-weight: 600; letter-spacing: 0.02em; }
367
+ .titlebar .sub { color: var(--muted); font-size: 0.76rem; font-family: ${MONO_STACK}; }
368
+ .titlebar .sub a { color: var(--corpus); }
369
+ .titlebar .pickers { margin-left: auto; display: flex; gap: 0.45rem; }
284
370
  button { font: inherit; cursor: pointer; }
285
371
  button:disabled { cursor: default; opacity: 0.5; }
286
- .pickers button { background: var(--card); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: 0.35rem 0.7rem; font-size: 0.85rem; }
287
- #stats { padding: 0.4rem 1.1rem; color: var(--muted); font-size: 0.8rem; font-family: ${MONO_STACK}; border-bottom: 1px solid var(--line); }
288
- main { display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(260px, 1fr); gap: 0; align-items: stretch; }
289
- @media (max-width: 720px) { main { grid-template-columns: 1fr; } }
290
- .ledger-pane { padding: 0.6rem 1.1rem 2rem; min-height: 60vh; }
291
- .rail { border-left: 1px solid var(--line); padding: 0.6rem 1rem 2rem; display: flex; flex-direction: column; gap: 1rem; }
292
- h2 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin: 0 0 0.4rem; }
372
+ button:focus-visible, input:focus-visible { outline: 2px solid var(--entail); outline-offset: 1px; }
373
+ .pickers button { background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 4px; padding: 0.3rem 0.65rem; font-size: 0.8rem; }
374
+ .pickers button:hover { border-color: var(--entail); }
375
+
376
+ .workbench { display: grid; grid-template-columns: minmax(230px, 320px) minmax(0, 1fr); min-height: 0; }
377
+ .sidebar { display: flex; flex-direction: column; min-height: 0; border-right: 1px solid var(--line); }
378
+ .panel { display: flex; flex-direction: column; min-height: 0; flex: 1; }
379
+ .panel-head { margin: 0; padding: 0.45rem 0.9rem; flex: none; display: flex; align-items: baseline; gap: 0.45rem; font-family: ${MONO_STACK}; font-size: 0.66rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.09em; color: var(--muted); border-bottom: 1px solid var(--line); }
380
+ .panel-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0.5rem 0.9rem 1rem; }
381
+ .focus-line { font-family: ${MONO_STACK}; font-size: 0.72rem; color: var(--corpus); text-transform: none; letter-spacing: 0; overflow-wrap: anywhere; }
293
382
  ul.rows { list-style: none; margin: 0; padding: 0; }
294
- .row { padding: 0.28rem 0.4rem; border-radius: 5px; font-size: 0.95rem; line-height: 1.5; }
383
+ .row { padding: 0.26rem 0.35rem; border-radius: 4px; font-size: 0.88rem; line-height: 1.45; }
295
384
  .row-focus { background: var(--corpus-soft); }
296
385
  .row.muted, .muted { color: var(--muted); }
297
- .term { background: none; border: none; padding: 0; color: var(--corpus); font-family: ${MONO_STACK}; font-size: 0.85rem; text-decoration: underline; text-decoration-color: var(--line); }
386
+ .term { background: none; border: none; padding: 0; color: var(--corpus); font-family: ${MONO_STACK}; font-size: 0.8rem; text-align: left; text-decoration: underline; text-decoration-color: var(--line); overflow-wrap: anywhere; }
298
387
  .term:hover { text-decoration-color: var(--corpus); }
299
388
  .verb { color: var(--muted); }
300
- .hints { display: flex; flex-direction: column; gap: 0.35rem; }
301
- .hint { text-align: left; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 0.35rem 0.55rem; font-size: 0.85rem; color: var(--ink); }
302
- .hint:hover { border-color: var(--corpus); }
303
- .dock { display: flex; flex-direction: column; gap: 0.4rem; }
304
- #chat-log { display: flex; flex-direction: column; gap: 0.4rem; max-height: 40vh; overflow-y: auto; }
305
- .turn { font-size: 0.9rem; line-height: 1.45; }
306
- .turn .who { display: block; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
307
- .turn-you .said { color: var(--ink); }
308
- .turn-tmct .said { color: var(--taught); white-space: pre-wrap; }
309
- #chat-form { display: flex; gap: 0.4rem; }
310
- #chat-input { flex: 1; font: inherit; padding: 0.4rem 0.5rem; border: 1px solid var(--line); border-radius: 6px; background: var(--card); color: var(--ink); }
311
- #chat-form button { background: var(--corpus); color: #fff; border: none; border-radius: 6px; padding: 0.4rem 0.8rem; }
312
- #dock-note { color: var(--muted); font-size: 0.78rem; }
313
- .focus-line { font-family: ${MONO_STACK}; font-size: 0.85rem; }
389
+
390
+ .editor { display: flex; flex-direction: column; min-height: 0; background: var(--card); }
391
+ #chat-log { flex: 1; min-height: 0; overflow-y: auto; padding: 0.9rem 1.1rem; display: flex; flex-direction: column; gap: 0.7rem; }
392
+ .turn { max-width: 46rem; font-size: 0.95rem; line-height: 1.5; }
393
+ .turn .who { display: block; font-family: ${MONO_STACK}; font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); margin-bottom: 0.15rem; }
394
+ .turn-you .said { font-family: ${MONO_STACK}; font-size: 0.84rem; }
395
+ .turn-tmct { border-left: 2px solid var(--entail); padding-left: 0.7rem; }
396
+ .turn-tmct .said { white-space: pre-wrap; }
397
+ .turn-note { color: var(--muted); font-style: italic; font-size: 0.85rem; }
398
+ .suggest { flex: none; border-top: 1px solid var(--line); padding: 0.45rem 1.1rem 0.1rem; }
399
+ .suggest-label { font-family: ${MONO_STACK}; font-size: 0.66rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.09em; color: var(--muted); }
400
+ .hints { display: flex; flex-wrap: wrap; gap: 0.35rem; max-height: 5.6rem; overflow-y: auto; padding: 0.35rem 0 0.45rem; }
401
+ .hint { background: var(--bg); border: 1px solid var(--line); border-radius: 999px; padding: 0.24rem 0.7rem; font-size: 0.8rem; color: var(--ink); text-align: left; }
402
+ .hint:hover { border-color: var(--entail); }
403
+ #chat-form { flex: none; display: flex; gap: 0.5rem; padding: 0.55rem 1.1rem 0.8rem; border-top: 1px solid var(--line); }
404
+ #chat-input { flex: 1; min-width: 0; font: inherit; font-size: 0.92rem; padding: 0.5rem 0.65rem; border: 1px solid var(--line); border-radius: 4px; background: var(--bg); color: var(--ink); }
405
+ #chat-form button { background: var(--entail); color: var(--card); font-weight: 600; border: none; border-radius: 4px; padding: 0.5rem 1rem; font-size: 0.9rem; }
406
+
407
+ .statusbar { display: flex; align-items: baseline; gap: 1rem; flex-wrap: wrap; padding: 0.32rem 0.9rem; font-family: ${MONO_STACK}; font-size: 0.7rem; color: var(--muted); background: var(--entail-soft); border-top: 1px solid var(--entail); }
408
+ #seed-status { margin-left: auto; text-align: right; }
409
+
410
+ @media (max-width: 760px) {
411
+ .workbench { grid-template-columns: 1fr; grid-template-rows: minmax(0, 34%) minmax(0, 1fr); }
412
+ .sidebar { border-right: none; border-bottom: 1px solid var(--line); }
413
+ }
314
414
  </style>
315
415
  </head>
316
416
  <body>
317
- <header>
318
- <h1>tmct code explorer</h1>
319
- <span class="sub">source: <span id="source-name">${escapeHtml(sourceName)}</span></span>
320
- ${showDesktopLink ? `<span class="sub">Also available as a <a href="${DESKTOP_APP_URL}">desktop app</a>.</span>` : ""}
321
- <div class="pickers">
322
- <button id="open-graph">Open graph…</button>
323
- <button id="open-repo">Open repo…</button>
324
- </div>
325
- </header>
326
- <div id="stats"></div>
327
- <main>
328
- <section class="ledger-pane">
329
- <h2>Facts around <span class="focus-line" id="focus-name">—</span></h2>
330
- <ul class="rows" id="ledger"></ul>
331
- </section>
332
- <aside class="rail">
333
- <div>
334
- <h2>Try asking</h2>
335
- <div class="hints" id="hints"></div>
417
+ <div class="shell">
418
+ <header class="titlebar">
419
+ <svg class="mark" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M8 0l1 2.3a5.8 5.8 0 0 1 1.9.8L13.3 2l.7.7-1.1 2.4c.4.6.6 1.2.8 1.9L16 8l-2.3 1a5.8 5.8 0 0 1-.8 1.9l1.1 2.4-.7.7-2.4-1.1a5.8 5.8 0 0 1-1.9.8L8 16l-1-2.3a5.8 5.8 0 0 1-1.9-.8L2.7 14l-.7-.7 1.1-2.4a5.8 5.8 0 0 1-.8-1.9L0 8l2.3-1c.2-.7.4-1.3.8-1.9L2 2.7l.7-.7 2.4 1.1A5.8 5.8 0 0 1 7 2.3L8 0zm0 5.2A2.8 2.8 0 1 0 8 10.8 2.8 2.8 0 0 0 8 5.2z"/></svg>
420
+ <h1>tmct code explorer</h1>
421
+ <span class="sub">source: <span id="source-name">${escapeHtml(sourceName)}</span></span>
422
+ ${showDesktopLink ? `<span class="sub">Also available as a <a href="${DESKTOP_APP_URL}">desktop app</a>.</span>` : ""}
423
+ <div class="pickers">
424
+ <button id="open-graph">Open graph…</button>
425
+ <button id="open-repo">Open repo…</button>
336
426
  </div>
337
- <div class="dock">
338
- <h2>Chat</h2>
427
+ </header>
428
+ <div class="workbench">
429
+ <aside class="sidebar">
430
+ <section class="panel" data-panel="explorer">
431
+ <h2 class="panel-head">Facts around <span class="focus-line" id="focus-name">—</span></h2>
432
+ <div class="panel-body">
433
+ <ul class="rows" id="ledger"></ul>
434
+ </div>
435
+ </section>
436
+ </aside>
437
+ <section class="editor" data-panel="conversation">
438
+ <h2 class="panel-head">Conversation</h2>
339
439
  <div id="chat-log"></div>
440
+ <div class="suggest">
441
+ <span class="suggest-label">Try asking</span>
442
+ <div class="hints" id="hints"></div>
443
+ </div>
340
444
  <form id="chat-form">
341
- <input id="chat-input" type="text" autocomplete="off" placeholder="ask about this graph…">
445
+ <input id="chat-input" type="text" autocomplete="off" placeholder="ask about this graph, or anything it knows…">
342
446
  <button type="submit">Ask</button>
343
447
  </form>
344
- <div id="dock-note"></div>
345
- </div>
346
- </aside>
347
- </main>
448
+ </section>
449
+ </div>
450
+ <footer class="statusbar">
451
+ <span id="stats"></span>
452
+ <span id="dock-note"></span>
453
+ <span id="seed-status"></span>
454
+ </footer>
455
+ </div>
348
456
  <script>window.__CODE_EXPLORER__ = Object.assign({ payload: ${payloadJson} }, ${dataJson});</script>
349
457
  ${winkLoaderInline ? `<script>\n${embedScriptText(winkLoaderInline)}\n</script>` : ""}
350
458
  ${bundleInline ? `<script>\n${embedScriptText(bundleInline)}\n</script>` : ""}
@@ -54,7 +54,7 @@ import { tmpdir } from "node:os";
54
54
  import { basename, join, resolve } from "node:path";
55
55
 
56
56
  import { runTurn, uuidv7 } from "./chat.mjs";
57
- import { splitSentencesPreservingPaths } from "./sentences.mjs";
57
+ import { splitSentencesPreservingPaths, stripCitationResidue } from "./sentences.mjs";
58
58
  import { loadMemory, readFactRows, appendFact } from "../adapters/memory/core.mjs";
59
59
  import { loadConfig } from "../adapters/config.mjs";
60
60
  import { touchedFactRows } from "../domain/memory/touched-facts.mjs";
@@ -202,6 +202,63 @@ export function optimisticTriples(sentence, { lexicon = loadLexicon(), nlp } = {
202
202
  return engine ? optimisticTriplesPos(sentence, lexicon, engine) : optimisticTriplesLexical(sentence, lexicon);
203
203
  }
204
204
 
205
+ // A closed set of clause markers a compound sentence hinges on. A candidate
206
+ // fragment is only kept when it stands alone as a clause; the marker set is
207
+ // deliberately small so a run-on never shatters into noise.
208
+ const CLAUSE_MARKER_RE = /,?\s+(?:because|since|although|though|whereas|while|so|and|but)\s+/i;
209
+ // The copula/auxiliary/light-verb words that mark a fragment as a would-be
210
+ // clause when no wink POS tagger is on hand — the same closed-list discipline
211
+ // the strict teach frames use in place of a probabilistic tag.
212
+ const CLAUSE_VERBISH = new Set([
213
+ "is", "are", "was", "were", "be", "been", "being", "am",
214
+ "has", "have", "had", "can", "could", "will", "would", "should",
215
+ "do", "does", "did", "want", "wants", "need", "needs", "make", "makes", "made",
216
+ ]);
217
+
218
+ /** Does the fragment carry a verb-ish token — a real VERB/AUX when wink tags
219
+ * it, else one of the closed copula/aux words above? A clause with no verb is
220
+ * a noun pile, never a sentence to ground. */
221
+ function fragmentHasVerb(fragment, nlp) {
222
+ const engine = nlp === undefined ? winkInstance() : nlp;
223
+ if (engine) {
224
+ try {
225
+ const pos = engine.readDoc(String(fragment)).tokens().out(engine.its.pos);
226
+ if (pos.some((p) => p === "VERB" || p === "AUX")) return true;
227
+ } catch { /* fall through to the closed-list check */ }
228
+ }
229
+ return String(fragment).toLowerCase().split(/\s+/).some((w) => CLAUSE_VERBISH.has(w));
230
+ }
231
+
232
+ /**
233
+ * The grounding candidates a sentence offers, WHOLE SENTENCE FIRST: the strict
234
+ * recognizer should always get the full sentence before any fragment, so a
235
+ * clause split only ever adds fallbacks, never pre-empts a clean whole-sentence
236
+ * read. When the sentence hinges on a closed clause marker
237
+ * (because/since/although/though/whereas/while/so/and/but), each side is added
238
+ * as a fallback — but only a fragment of at least 3 tokens that carries a
239
+ * verb-ish token, so a stray connective can't split a sentence into noise.
240
+ * Returns [sentence, ...fragments] with no duplicates.
241
+ */
242
+ export function clauseCandidates(sentence, { nlp } = {}) {
243
+ const whole = String(sentence ?? "").trim();
244
+ const out = [whole];
245
+ if (!whole) return out;
246
+ const parts = whole.split(CLAUSE_MARKER_RE).map((s) => s.trim()).filter(Boolean);
247
+ if (parts.length <= 1) return out;
248
+ for (const part of parts) {
249
+ if (part === whole || out.includes(part)) continue;
250
+ if (part.split(/\s+/).length < 3) continue;
251
+ if (!fragmentHasVerb(part, nlp)) continue;
252
+ out.push(part);
253
+ }
254
+ return out;
255
+ }
256
+
257
+ // The pronoun subjects a bounded carry substitutes with the paragraph's last
258
+ // grounded subject. Ingest only — a chat turn resolves "it"/"they" against the
259
+ // live focus, never a stale paragraph carry.
260
+ const PRONOUN_LEAD_RE = /^(?:they|it|these|those|this)\b\s*/i;
261
+
205
262
  /** A readable predicate for canonical output: the local part of an rdfs:/ace:
206
263
  * CURIE, otherwise the predicate verbatim. */
207
264
  const readablePredicate = (predicate) => String(predicate).replace(/^[a-z]+:/i, "");
@@ -256,7 +313,11 @@ export async function ingestText(text, {
256
313
  memoryDir = null, sourceTag = "text", optimistic = false,
257
314
  canonical = false, config = null, lexicon = null,
258
315
  } = {}) {
259
- const sentences = splitSentencesPreservingPaths(String(text ?? ""));
316
+ // Paragraphs first (blank-line separated), so the pronoun carry never bridges
317
+ // a topic break: a fresh paragraph clears the last-subject it would resolve
318
+ // "they"/"it" against. Each paragraph then splits into sentences the shared
319
+ // path-preserving way.
320
+ const paragraphs = String(text ?? "").split(/\n[ \t]*\n/);
260
321
  const ephemeral = !memoryDir;
261
322
  const dir = memoryDir || await mkdtemp(join(tmpdir(), "tmct-ingest-"));
262
323
  // `dir` may be a backend handle (a sqlite store), not a path — only a real
@@ -268,46 +329,75 @@ export async function ingestText(text, {
268
329
 
269
330
  const extracted = [];
270
331
  const optimisticFacts = [];
332
+ let sentenceCount = 0;
271
333
  let recognizedSentences = 0;
272
334
  let optimisticSentences = 0;
273
335
 
336
+ // One recognized read of some text form: null when the strict recognizer
337
+ // grounds nothing, else the Fact rows it touched.
338
+ const strictRows = async (form) => {
339
+ const { recognized, rows } = await runSentence(form, { config: cfg, memoryDir: dir });
340
+ return recognized && rows.length ? rows : null;
341
+ };
342
+
274
343
  try {
275
- for (const sentence of sentences) {
276
- const { recognized, rows } = await runSentence(sentence, { config: cfg, memoryDir: dir });
277
- if (recognized && rows.length) {
278
- recognizedSentences += 1;
279
- const tag = `extracted:${sourceTag}`;
280
- for (const row of rows) {
344
+ for (const paragraph of paragraphs) {
345
+ // The last unique grounded subject in THIS paragraph, carried onto a
346
+ // later pronoun-led sentence the strict recognizer couldn't ground on
347
+ // its own. Cleared at the paragraph boundary.
348
+ let carrySubject = null;
349
+ for (const sentence of splitSentencesPreservingPaths(paragraph)) {
350
+ sentenceCount += 1;
351
+ const cleaned = stripCitationResidue(sentence);
352
+ // Whole sentence first, then each closed-marker clause as a fallback.
353
+ let rows = null;
354
+ for (const candidate of clauseCandidates(cleaned, { nlp })) {
355
+ rows = await strictRows(candidate);
356
+ if (rows) break;
357
+ }
358
+ // Bounded pronoun carry: a "they/it/these/those/this …" sentence the
359
+ // recognizer skipped is retried once with the paragraph's last grounded
360
+ // subject in the pronoun's place. Never a chat turn — ingest only.
361
+ if (!rows && carrySubject && PRONOUN_LEAD_RE.test(cleaned)) {
362
+ rows = await strictRows(cleaned.replace(PRONOUN_LEAD_RE, `${carrySubject} `));
363
+ }
364
+ if (rows) {
365
+ recognizedSentences += 1;
366
+ const subjects = new Set(rows.map((r) => r.subject));
367
+ if (subjects.size === 1) carrySubject = [...subjects][0];
368
+ const tag = `extracted:${sourceTag}`;
369
+ for (const row of rows) {
370
+ await appendFact(dir, {
371
+ subject: row.subject, predicate: row.predicate, object: row.object,
372
+ provenance: tag, quantifier: row.quantifier || "",
373
+ });
374
+ extracted.push({
375
+ subject: row.subject, predicate: row.predicate, object: row.object,
376
+ provenance: tag, quantifier: row.quantifier || "", sentence,
377
+ });
378
+ }
379
+ continue;
380
+ }
381
+ if (!optimistic) continue;
382
+ const candidates = optimisticTriples(cleaned, { lexicon: lex, nlp });
383
+ if (!candidates.length) continue;
384
+ optimisticSentences += 1;
385
+ const tag = `optimistic-extract:${sourceTag}`;
386
+ for (const t of candidates) {
281
387
  await appendFact(dir, {
282
- subject: row.subject, predicate: row.predicate, object: row.object,
283
- provenance: tag, quantifier: row.quantifier || "",
284
- });
285
- extracted.push({
286
- subject: row.subject, predicate: row.predicate, object: row.object,
287
- provenance: tag, quantifier: row.quantifier || "", sentence,
388
+ subject: t.subject, predicate: t.predicate, object: t.object, provenance: tag,
288
389
  });
390
+ optimisticFacts.push({ ...t, provenance: tag, sentence });
289
391
  }
290
- continue;
291
- }
292
- if (!optimistic) continue;
293
- const candidates = optimisticTriples(sentence, { lexicon: lex, nlp });
294
- if (!candidates.length) continue;
295
- optimisticSentences += 1;
296
- const tag = `optimistic-extract:${sourceTag}`;
297
- for (const t of candidates) {
298
- await appendFact(dir, {
299
- subject: t.subject, predicate: t.predicate, object: t.object, provenance: tag,
300
- });
301
- optimisticFacts.push({ ...t, provenance: tag, sentence });
302
392
  }
303
393
  }
304
394
 
305
395
  const result = {
306
- sentences: sentences.length,
396
+ sentences: sentenceCount,
307
397
  recognized: recognizedSentences,
308
398
  extracted,
309
399
  optimistic: optimisticFacts,
310
- skipped: sentences.length - recognizedSentences - optimisticSentences,
400
+ skipped: sentenceCount - recognizedSentences - optimisticSentences,
311
401
  };
312
402
  if (canonical) {
313
403
  result.canonical = canonicalLines([...extracted, ...optimisticFacts], readFactRows(await loadMemory(dir)));