@polycode-projects/the-mechanical-code-talker 2.8.11 → 2.9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "2.8.11",
3
+ "version": "2.9.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
@@ -9,6 +9,7 @@ import { join } from "node:path";
9
9
  import { splitIdentifierWords, tokenizeProse } from "../../domain/prose.mjs";
10
10
  import { idfWeight } from "../../domain/text-stats.mjs";
11
11
  import { SOURCE_PRIOR } from "../../domain/memory/trust.mjs";
12
+ import { isMemoryOrSqliteHandle } from "./core.mjs";
12
13
 
13
14
  // A block inherits its Source's trust (operator 1.0, corpus 0.7); retrieval
14
15
  // weights relevance × trust via a bounded factor (0.5 + trust, ~[0.5, 1.5]).
@@ -50,8 +51,14 @@ async function atomicWrite(file, text) {
50
51
  }
51
52
 
52
53
  /** Load the block index ({ blocks: { id: { file, tokens[], rank } } }); a
53
- * missing index is the empty bootstrap. */
54
+ * missing index is the empty bootstrap. Session-block folding writes to
55
+ * `.tmct/memory/blocks/` on disk (fold.mjs, session end) — a Backend B/C
56
+ * handle (createInMemoryStore/createSqliteMemoryStore) has no such directory
57
+ * to read, so it gets the SAME empty bootstrap a fresh Backend A repo gets,
58
+ * never a raw `path.join(dir, …)` TypeError from treating the handle object
59
+ * as a path string. */
54
60
  export async function loadBlockIndex(dir) {
61
+ if (isMemoryOrSqliteHandle(dir)) return { blocks: {} };
55
62
  try {
56
63
  return JSON.parse(await readFile(join(blocksDir(dir), INDEX_NAME), "utf8"));
57
64
  } catch (e) {
@@ -142,6 +149,9 @@ function rerank(index) {
142
149
  */
143
150
  export async function saveBlock(dir, { id, text, sourceType = DEFAULT_BLOCK_SOURCE_TYPE, createdAt = "" }) {
144
151
  if (!id) throw new Error("a block needs an id");
152
+ if (isMemoryOrSqliteHandle(dir)) {
153
+ throw new Error("saveBlock: dir is a memory/sqlite handle — session-block folding is Backend A only (no on-disk blocks/ directory to write)");
154
+ }
145
155
  const bdir = blocksDir(dir);
146
156
  await mkdir(bdir, { recursive: true });
147
157
  const file = `${safeName(id)}.txt`;
@@ -161,6 +171,7 @@ export async function saveBlock(dir, { id, text, sourceType = DEFAULT_BLOCK_SOUR
161
171
 
162
172
  /** Remove a block (id unknown → no-op) and re-rank the survivors. */
163
173
  export async function removeBlock(dir, id) {
174
+ if (isMemoryOrSqliteHandle(dir)) return false; // nothing was ever written; removing is a no-op, not an error
164
175
  const index = await loadBlockIndex(dir);
165
176
  const entry = index.blocks[id];
166
177
  if (!entry) return false;
@@ -153,7 +153,13 @@ function isMemoryHandle(dir) {
153
153
  function isSqliteHandle(dir) {
154
154
  return !!dir && typeof dir === "object" && dir.backend === BACKEND_SQLITE;
155
155
  }
156
- function isMemoryOrSqliteHandle(dir) {
156
+ /** True for a Backend B (in-memory) or Backend C (sqlite) handle — anything
157
+ * that is NOT a plain repo-path string. Exported so every OTHER module that
158
+ * takes a `dir` and might reach a raw `node:path`/`node:fs` call (blocks.mjs's
159
+ * session-block index chief among them) can guard the same way this module
160
+ * already does, instead of a bare `join(dir, …)` throwing Node's generic
161
+ * "path argument must be of type string" at a caller with no idea why. */
162
+ export function isMemoryOrSqliteHandle(dir) {
157
163
  return isMemoryHandle(dir) || isSqliteHandle(dir);
158
164
  }
159
165
 
@@ -1,25 +1,22 @@
1
- // chat-page-viz.mjs — "Talk to it"'s full-screen destination
2
- // (PLAN_GAMES_UPLIFT_V3.md Part C.4 item 1): a self-contained document shaped
3
- // exactly like spider-fly-viz.mjs/adventure-viz.mjs's own page-builders — one
4
- // inlined <style> importing viz-theme.mjs's shared tokens, behaviour as an
5
- // inlined IIFE — reusing the SAME chat engine the home page's embedded
6
- // #tmct-chat widget runs (chat-browser.bundle.js's globalThis.tmctChat,
7
- // chat-seed.json, public/reference-pack/), referenced by the same same-origin
8
- // relative paths chat-ui.mjs already uses. This module does not import or
9
- // re-render chat-ui.mjs — it is a second, independent consumer of the same
10
- // engine/bundle, the same relationship spider-fly-viz.mjs's own inlined chat
11
- // dock already has with createSpiderFlySession (both call the shared
12
- // session.turn(line), neither reimplements it).
1
+ // chat-page-viz.mjs — chat.html, the full-screen chat page: a self-contained
2
+ // document shaped exactly like spider-fly-viz.mjs/adventure-viz.mjs's own
3
+ // page-builders one inlined <style> importing viz-theme.mjs's shared
4
+ // tokens, behaviour as an inlined IIFE running the full chat engine
5
+ // (chat-browser.bundle.js's globalThis.tmctChat, chat-seed.json,
6
+ // public/reference-pack/) by same-origin relative paths. The same
7
+ // relationship spider-fly-viz.mjs's own inlined chat dock has with
8
+ // createSpiderFlySession: both call the shared session.turn(line), neither
9
+ // reimplements it.
13
10
  //
14
- // What's actually new here, versus the embedded widget: full-screen
15
- // post-ChatGPT-style chrome (centered message column, bottom-fixed composer,
11
+ // This page's own chrome: full-screen post-ChatGPT-style layout (centered
12
+ // message column, bottom-fixed composer,
16
13
  // message bubbles) and this page's own signature element — a quiet
17
14
  // per-message PROVENANCE CHIP (taught / corpus / entailed) next to every
18
15
  // grounded answer, tmct's actual differentiator versus an LLM chatbot. The
19
16
  // chip is read straight off the SAME "(source: ...)" citation chat.mjs's own
20
17
  // factPhrase/renderFactLine convention already appends to most answers (see
21
18
  // e.g. `dog is a kind of animal (source: corpus:conceptnet ...)` — already
22
- // asserted by e2e/pages-chat.test.mjs against the embedded widget), never a
19
+ // asserted by e2e/pages-chat-fullscreen.test.mjs against this page), never a
23
20
  // second provenance computation against memory internals: `provBucketFor`
24
21
  // (ledger-viz.mjs) is spliced in unmodified and applied to whatever citation
25
22
  // text the answer already carries, so this page's chip and the ledger's own
@@ -34,7 +31,7 @@
34
31
  import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
35
32
  import { provBucketFor } from "./ledger-viz.mjs";
36
33
 
37
- const DEFAULT_TITLE = "tmct — talk to it";
34
+ const DEFAULT_TITLE = "the-mechanical-code-talker — talk to it";
38
35
 
39
36
  // Reads left-to-right as the reader meets each tier: what you taught it
40
37
  // directly, what its bundled corpus already knew, what it worked out itself.
@@ -107,10 +104,36 @@ export function renderChatHtml({ title = DEFAULT_TITLE } = {}) {
107
104
  <meta charset="utf-8">
108
105
  <meta name="viewport" content="width=device-width, initial-scale=1">
109
106
  <title>${escapeHtml(title)}</title>
107
+ <!--
108
+ Import map: resolves the "wink-nlp"/"wink-eng-lite-web-model" bare specifiers the
109
+ sibling chat bundle's own dynamic import() needs (pinned to the exact versions
110
+ package.json depends on) to esm.sh CDN builds — the same seam plan.html and
111
+ ledger.html each wire up for their own live sessions, mirrored here because this
112
+ page can be opened standalone (no import map inherited from a host document). The
113
+ bundle itself never touches wink-nlp directly — wink-model.mjs's own header
114
+ explains why a static import would drag the ~1 MB model into every bundle; only
115
+ the page's own inline script performs this CDN import, the same bounded-race
116
+ tryLoadWink() pattern public/tmct-browser.mjs uses.
117
+ -->
118
+ <script type="importmap">
119
+ {
120
+ "imports": {
121
+ "wink-nlp": "https://esm.sh/wink-nlp@2.4.0",
122
+ "wink-eng-lite-web-model": "https://esm.sh/wink-eng-lite-web-model@1.8.1"
123
+ }
124
+ }
125
+ </script>
110
126
  <style>
111
127
  ${THEME_TOKENS_CSS}
112
128
  html, body { height: 100%; }
113
- body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; display: flex; flex-direction: column; overflow: hidden; }
129
+ /* body is the OUTER row: the chat column plus the stats panel docked to its
130
+ right, each a sibling flex item stretching to the full viewport height
131
+ (flex row's default cross-axis stretch) — .chatCol carries the column
132
+ layout the chat chrome itself needs (topbar/main/composer/statusline
133
+ stacked), so this page keeps working exactly as before, just inside one
134
+ more layer. */
135
+ body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; display: flex; overflow: hidden; }
136
+ .chatCol { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
114
137
  .mono { font-family: ${MONO_STACK}; }
115
138
  button { font: inherit; color: inherit; background: none; cursor: pointer; border: none; }
116
139
  button:focus-visible, input:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
@@ -126,7 +149,14 @@ ${THEME_TOKENS_CSS}
126
149
  .dot-taught { background: var(--taught); } .dot-corpus { background: var(--corpus); } .dot-entail { background: var(--entail); }
127
150
 
128
151
  main.chatMain { flex: 1 1 auto; overflow-y: auto; overscroll-behavior: contain; }
129
- .messages { max-width: 720px; margin: 0 auto; padding: 1.2rem 1rem 1.6rem; display: flex; flex-direction: column; gap: .15rem; min-height: 100%; }
152
+ /* box-sizing: border-box so min-height: 100% below counts the padding IN,
153
+ not on top of it — without this, .messages renders ~45px taller than
154
+ main.chatMain's own visible height (its 1.2rem/1.6rem vertical padding,
155
+ added past a content-box min-height), so the initial scrollToEnd() (the
156
+ very first thing boot() does, right after the boot system line lands)
157
+ scrolls that overflow out of view and clips the boot message half under
158
+ the topbar before anyone reads it. */
159
+ .messages { box-sizing: border-box; max-width: 720px; margin: 0 auto; padding: 1.2rem 1rem 1.6rem; display: flex; flex-direction: column; gap: .15rem; min-height: 100%; }
130
160
 
131
161
  .msg-row { display: flex; flex-direction: column; margin: .35rem 0; max-width: 100%; }
132
162
  .msg-row.user { align-items: flex-end; }
@@ -155,6 +185,22 @@ ${THEME_TOKENS_CSS}
155
185
  .composer-inner button[type="submit"]:disabled { opacity: .4; cursor: default; }
156
186
  .statusline { max-width: 720px; margin: 0 auto; padding: 0 1.1rem .6rem; font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); }
157
187
 
188
+ /* the provenance stats panel: what this session's memory holds, docked to
189
+ the right of the chat column (a real layout column, not an overlay) —
190
+ re-rendered after boot and after every turn from window.tmctChat's own
191
+ memoryStats(), never a second provenance computation. */
192
+ .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; }
193
+ .statsPanel h2 { font-size: .66rem; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); margin: 1.3rem 0 .5rem; }
194
+ .statsPanel h2:first-child { margin-top: 0; }
195
+ .statsPanel .band-row { display: flex; justify-content: space-between; gap: .6rem; margin: 0; padding: .12rem 0; }
196
+ .statsPanel .band-count { color: var(--muted); font-variant-numeric: tabular-nums; }
197
+ .statsPanel .taught-item { margin: 0 0 .7rem; }
198
+ .statsPanel .taught-tag { display: block; color: var(--muted); font-size: .66rem; margin-top: .15rem; word-break: break-word; }
199
+ .statsPanel .empty { color: var(--muted); margin: 0; }
200
+
201
+ @media (max-width: 860px) {
202
+ .statsPanel { display: none; }
203
+ }
158
204
  @media (max-width: 560px) {
159
205
  .legend { display: none; }
160
206
  .bubble { max-width: 92%; }
@@ -165,24 +211,29 @@ ${THEME_TOKENS_CSS}
165
211
  </style>
166
212
  </head>
167
213
  <body>
168
- <header class="topbar">
169
- <div class="brand">
170
- <span class="eyebrow">tmct</span>
171
- <h1>Talk to it</h1>
172
- </div>
173
- <div class="legend" aria-hidden="true">${legendHtml}</div>
174
- </header>
175
- <main class="chatMain">
176
- <div class="messages" id="messages" role="log" aria-live="polite" aria-label="Conversation"></div>
177
- </main>
178
- <form class="composer" id="composer">
179
- <div class="composer-inner">
180
- <input id="composerInput" type="text" autocomplete="off" autocapitalize="off" spellcheck="false"
181
- placeholder="loading the engine…" aria-label="Ask tmct something" disabled>
182
- <button type="submit" id="composerSend" aria-label="Send" disabled>&#8594;</button>
183
- </div>
184
- </form>
185
- <div class="statusline" id="status">loading the engine&hellip;</div>
214
+ <div class="chatCol">
215
+ <header class="topbar">
216
+ <div class="brand">
217
+ <span class="eyebrow">the-mechanical-code-talker</span>
218
+ <h1>Talk to it</h1>
219
+ </div>
220
+ <div class="legend" aria-hidden="true">${legendHtml}</div>
221
+ </header>
222
+ <main class="chatMain">
223
+ <div class="messages" id="messages" role="log" aria-live="polite" aria-label="Conversation"></div>
224
+ </main>
225
+ <form class="composer" id="composer">
226
+ <div class="composer-inner">
227
+ <input id="composerInput" type="text" autocomplete="off" autocapitalize="off" spellcheck="false"
228
+ placeholder="loading the engine…" aria-label="Ask tmct something" disabled>
229
+ <button type="submit" id="composerSend" aria-label="Send" disabled>&#8594;</button>
230
+ </div>
231
+ </form>
232
+ <div class="statusline" id="status">loading the engine&hellip;</div>
233
+ </div>
234
+ <aside class="statsPanel" id="statsPanel" aria-label="This session's memory">
235
+ <p class="empty">loading memory stats&hellip;</p>
236
+ </aside>
186
237
  <script src="./chat-browser.bundle.js"></script>
187
238
  <script>
188
239
  (function () {
@@ -196,6 +247,7 @@ ${THEME_TOKENS_CSS}
196
247
  const inputEl = el("composerInput");
197
248
  const sendBtn = el("composerSend");
198
249
  const statusEl = el("status");
250
+ const statsPanelEl = el("statsPanel");
199
251
 
200
252
  function scrollToEnd() {
201
253
  messagesEl.parentElement.scrollTop = messagesEl.parentElement.scrollHeight;
@@ -265,8 +317,8 @@ ${THEME_TOKENS_CSS}
265
317
  }
266
318
 
267
319
  // ---- engine boot -------------------------------------------------------
268
- // Mirrors chat-ui.mjs's own boot sequence against the SAME bundle/seed/pack
269
- // a second consumer of the shared engine, not a fork of it.
320
+ // The same bounded-race wink load public/tmct-browser.mjs uses, against
321
+ // this page's own bundle/seed/pack.
270
322
  const WINK_LOAD_TIMEOUT_MS = 8000;
271
323
  const timeoutAfter = (ms, reason) => new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms));
272
324
 
@@ -329,6 +381,77 @@ ${THEME_TOKENS_CSS}
329
381
  },
330
382
  };
331
383
 
384
+ // ---- memory stats: the boot message's own numbers, and the docked panel -
385
+ // Both read window.tmctChat.memoryStats(memoryDir) (chat-browser-entry.mjs)
386
+ // — one computation, reused, so the boot line and the panel can never
387
+ // disagree with each other about what this session's memory holds.
388
+ const BAND_LABELS = { human: "human persona", seon: "seon ontology", conceptnet: "ConceptNet" };
389
+ const BAND_ORDER = ["human", "seon", "conceptnet", "taught this session", "other"];
390
+ const bandLabel = (key) => BAND_LABELS[key] || key;
391
+
392
+ /** The boot system line's own memory summary — every seed band this
393
+ * session actually loaded, named with its real count, left-to-right in
394
+ * BAND_ORDER; a session with nothing seeded says so plainly instead of
395
+ * naming zero facts. */
396
+ function statsSummaryLine(stats) {
397
+ if (!stats || !stats.total) return "no starter memory; starting empty";
398
+ const parts = BAND_ORDER.filter((k) => stats.bandCounts[k]).map((k) => stats.bandCounts[k] + " " + bandLabel(k));
399
+ return parts.length
400
+ ? "starter memory: " + parts.join(" + ") + " (" + stats.total + " facts total)"
401
+ : stats.total + " starter facts loaded";
402
+ }
403
+
404
+ function bandRow(label, count) {
405
+ const row = document.createElement("p");
406
+ row.className = "band-row";
407
+ const l = document.createElement("span");
408
+ l.textContent = label;
409
+ const c = document.createElement("span");
410
+ c.className = "band-count";
411
+ c.textContent = String(count);
412
+ row.appendChild(l);
413
+ row.appendChild(c);
414
+ return row;
415
+ }
416
+
417
+ /** (Re)render the docked panel from a memoryStats() result — stats may be
418
+ * passed in already-computed (boot reuses its own call rather than asking
419
+ * twice); omitted, it fetches fresh. Best-effort: a session not ready yet,
420
+ * or a read that throws, leaves the panel showing whatever it last showed
421
+ * rather than blanking it. */
422
+ async function renderStatsPanel(stats) {
423
+ if (!stats) {
424
+ if (!window.tmctChatSession || !window.tmctChat.memoryStats) return;
425
+ try { stats = await window.tmctChat.memoryStats(window.tmctChatSession.memoryDir); }
426
+ catch { return; }
427
+ }
428
+ statsPanelEl.textContent = "";
429
+ statsPanelEl.appendChild(Object.assign(document.createElement("h2"), { textContent: "this session's memory" }));
430
+ statsPanelEl.appendChild(bandRow("total facts", stats.total));
431
+ for (const key of BAND_ORDER) {
432
+ if (stats.bandCounts[key]) statsPanelEl.appendChild(bandRow(bandLabel(key), stats.bandCounts[key]));
433
+ }
434
+
435
+ statsPanelEl.appendChild(Object.assign(document.createElement("h2"), { textContent: "taught this session" }));
436
+ if (!stats.taught.length) {
437
+ const empty = document.createElement("p");
438
+ empty.className = "empty";
439
+ empty.textContent = 'nothing yet \\u2014 teach it something ("a dog is a kind of animal") and it lands here, with its source.';
440
+ statsPanelEl.appendChild(empty);
441
+ } else {
442
+ for (const fact of stats.taught.slice(-8).reverse()) {
443
+ const item = document.createElement("p");
444
+ item.className = "taught-item";
445
+ item.appendChild(document.createTextNode(fact.subject + " " + fact.predicate + " " + fact.object));
446
+ const tag = document.createElement("span");
447
+ tag.className = "taught-tag";
448
+ tag.textContent = fact.tag;
449
+ item.appendChild(tag);
450
+ statsPanelEl.appendChild(item);
451
+ }
452
+ }
453
+ }
454
+
332
455
  function renderStatus() {
333
456
  const seedPart = seedPayload
334
457
  ? "starter memory: " + seedFacts + " facts"
@@ -358,7 +481,10 @@ ${THEME_TOKENS_CSS}
358
481
  const pendingRow = addPendingAssistantBubble();
359
482
  setBusy(true);
360
483
  window.tmctChatSession.turn(q)
361
- .then((result) => settleAssistantBubble(pendingRow, result.answer, result.record))
484
+ .then((result) => {
485
+ settleAssistantBubble(pendingRow, result.answer, result.record);
486
+ return renderStatsPanel(); // a teach turn just grew this session's memory; a plain ask leaves it unchanged either way
487
+ })
362
488
  .catch((err) => settleAssistantBubble(pendingRow,
363
489
  "something went wrong answering that (" + (err && err.message ? err.message : err) + ") \\u2014 try rephrasing",
364
490
  { miss: true }))
@@ -377,9 +503,10 @@ ${THEME_TOKENS_CSS}
377
503
  await Promise.all([fetchSeed(), tryLoadWink()]);
378
504
  window.tmctChat.registerReferencePackProvider(fetchPackProvider);
379
505
  window.tmctChatSession = newSession();
380
- addSystemLine(seedPayload
381
- ? "tmct \\u2014 the real engine, running in this page \\u2014 " + seedFacts + " starter facts loaded"
382
- : "tmct \\u2014 the real engine, running in this page \\u2014 no starter memory; starting empty");
506
+ const stats = await window.tmctChat.memoryStats(window.tmctChatSession.memoryDir);
507
+ addSystemLine("tmct \\u2014 the real engine, running in this page \\u2014 " + statsSummaryLine(stats)
508
+ + ". Ask it something, or teach it a fact of your own.");
509
+ await renderStatsPanel(stats);
383
510
  inputEl.placeholder = seedPayload ? 'try "what is a dog"' : window.tmctChat.vocabExampleHint(false);
384
511
  renderStatus();
385
512
  setBusy(false);
@@ -535,6 +535,29 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
535
535
  <meta charset="utf-8">
536
536
  <meta name="viewport" content="width=device-width, initial-scale=1">
537
537
  <title>${escapeHtml(title)}</title>
538
+ <!--
539
+ Import map: resolves the "wink-nlp"/"wink-eng-lite-web-model" bare specifiers the
540
+ live ask-and-teach dock's own dynamic import() needs (pinned to the exact versions
541
+ package.json depends on) to esm.sh CDN builds — the same seam chat.html and
542
+ plan.html each wire up for their own live sessions, mirrored here because this page
543
+ can be opened standalone (no import map inherited from a host document; this
544
+ includes the self-contained file tmct viz writes to disk — a plain cross-origin
545
+ dynamic import() of a remote https:// URL, not a file:// read, so it works the
546
+ same way offline this page's own try/catch already handles for the deployed site:
547
+ the fetch fails and the wink tier degrades gracefully). Nothing in this file
548
+ touches wink-nlp directly — wink-model.mjs's own header explains why a static
549
+ import would drag the ~1 MB model into every bundle; only the page's own inline
550
+ script performs this CDN import, the same bounded-race tryLoadWink() pattern
551
+ public/tmct-browser.mjs uses.
552
+ -->
553
+ <script type="importmap">
554
+ {
555
+ "imports": {
556
+ "wink-nlp": "https://esm.sh/wink-nlp@2.4.0",
557
+ "wink-eng-lite-web-model": "https://esm.sh/wink-eng-lite-web-model@1.8.1"
558
+ }
559
+ }
560
+ </script>
538
561
  <style>
539
562
  ${THEME_TOKENS_CSS}
540
563
  html { background: var(--bg); }
@@ -296,13 +296,13 @@ export function renderPlanHtml({ plan, rendersAs = {}, sizeOrder = [], title } =
296
296
  <!--
297
297
  Import map: resolves the "wink-nlp"/"wink-eng-lite-web-model" bare specifiers the
298
298
  live re-solve session's own dynamic import() needs (pinned to the exact versions
299
- package.json depends on) to esm.sh CDN builds — the same seam index.html's own
300
- import map wires up for the embedded chat, mirrored here because this page can be
301
- opened standalone (no import map inherited from a host document). The bundle
302
- itself (./plan-browser.bundle.js) never touches wink-nlp directly — wink-model.mjs's
303
- own header explains why a static import would drag the ~1 MB model into every
304
- bundle; only the page's own inline script performs this CDN import, exactly like
305
- public/chat-ui.mjs's own tryLoadWink().
299
+ package.json depends on) to esm.sh CDN builds — the same seam chat.html and
300
+ ledger.html each wire up for their own live sessions, mirrored here because this
301
+ page can be opened standalone (no import map inherited from a host document). The
302
+ bundle itself (./plan-browser.bundle.js) never touches wink-nlp directly —
303
+ wink-model.mjs's own header explains why a static import would drag the ~1 MB
304
+ model into every bundle; only the page's own inline script performs this CDN
305
+ import, the same bounded-race tryLoadWink() pattern public/tmct-browser.mjs uses.
306
306
  -->
307
307
  <script type="importmap">
308
308
  {
@@ -636,9 +636,9 @@ const PLAN = ${embedded};
636
636
  // "moving" to "move" — without it that one teach sentence honestly
637
637
  // declines and every position fact taught after it fails in turn. Load
638
638
  // wink from the CDN and register it, the SAME bounded-race pattern
639
- // public/chat-ui.mjs and public/tmct-browser.mjs both use: a cross-
640
- // origin dynamic import() can neither resolve nor reject on some
641
- // failures, so an unbounded await would leave a resolve stuck forever.
639
+ // public/tmct-browser.mjs uses: a cross-origin dynamic import() can
640
+ // neither resolve nor reject on some failures, so an unbounded await
641
+ // would leave a resolve stuck forever.
642
642
  // Awaited before EVERY session creation below (idempotent — a second
643
643
  // await after the first attempt already settled resolves immediately).
644
644
  const WINK_LOAD_TIMEOUT_MS = 8000;
@@ -1,5 +1,5 @@
1
- // chat-browser-entry.mjs — the esbuild entry for the home page's embedded
2
- // chat (public/chat-ui.mjs, built by scripts/build-chat-bundle.mjs).
1
+ // chat-browser-entry.mjs — the esbuild entry for chat.html's embedded chat
2
+ // (built by scripts/build-chat-bundle.mjs).
3
3
  //
4
4
  // Unlike memory-ask-browser-entry.mjs (factAnswer/factReadBack only), this
5
5
  // exposes the FULL turn engine: createChatSession wraps chat.mjs's runTurn
@@ -17,7 +17,8 @@
17
17
  // Web Crypto API instead, with a Date.now fallback for contexts
18
18
  // without it.
19
19
  import { runTurn, vocabExampleHint } from "../../services/chat.mjs";
20
- import { createInMemoryStore, normFactTerm } from "../../adapters/memory/core.mjs";
20
+ import { createInMemoryStore, normFactTerm, loadMemory, readFactRows } from "../../adapters/memory/core.mjs";
21
+ import { provenanceTagToSource } from "../../domain/memory/trust.mjs";
21
22
  import { parseEntities } from "../../domain/codegraph.mjs";
22
23
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
23
24
  import { registerWinkModel } from "../../adapters/wink-model.mjs";
@@ -84,4 +85,51 @@ export function createChatSession({ seedPayload = null, vocabSeeded = false } =
84
85
  };
85
86
  }
86
87
 
87
- globalThis.tmctChat = { createChatSession, registerWinkModel, registerReferencePackProvider, normFactTerm, vocabExampleHint };
88
+ /**
89
+ * The memory a running session holds, broken down by where each fact came
90
+ * from: the seed corpus bands it booted with (keyed by the SAME band name
91
+ * build-chat-seed.mjs/extensions.mjs seed under — "human", "seon",
92
+ * "conceptnet" today, whichever bands a future seed adds tomorrow) plus
93
+ * whatever has been taught THIS session over chat. Reuses memory/trust.mjs's
94
+ * own `provenanceTagToSource` against each fact's already-stored provenance
95
+ * tag(s) (readFactRows' `provenance`, the ' | '-joined compat string) rather
96
+ * than inventing a second provenance parse — the same tag chat.mjs's own
97
+ * "(source: ...)" citation already carries, so this panel and a turn's
98
+ * citation always agree on where a fact came from.
99
+ *
100
+ * Returns { total, bandCounts, taught }: `bandCounts` maps a band label to
101
+ * its fact count ("taught this session" for a teach/operator-sourced fact
102
+ * with no corpus band, "other" for anything provenance can't place);
103
+ * `taught` lists every session-taught fact (subject/predicate/object + its
104
+ * own provenance tag), most-recently-taught last — a stats panel's
105
+ * provenance column reads straight off this, no further lookup needed.
106
+ */
107
+ export async function memoryStats(memoryDir) {
108
+ const memory = await loadMemory(memoryDir);
109
+ const rows = readFactRows(memory);
110
+ const bandCounts = {};
111
+ const taught = [];
112
+ for (const row of rows) {
113
+ const tags = String(row.provenance || "").split(" | ").filter(Boolean);
114
+ let band = null;
115
+ let isTaught = false;
116
+ let taughtTag = "";
117
+ for (const tag of tags) {
118
+ const src = provenanceTagToSource(tag);
119
+ if (!src) continue;
120
+ // corpusWeak (a /r/RelatedTo-strength triple, e.g. ConceptNet's or
121
+ // SEON's own weaker associations) names the SAME band as corpus (a
122
+ // /r/IsA-strength one) — both carry `src.name`, and a band count that
123
+ // dropped the weak tier would undercount a corpus by exactly its
124
+ // weak-relation facts (SEON: 19 of them, all `corpus-weak:seon`).
125
+ if ((src.kind === "corpus" || src.kind === "corpusWeak") && src.name) band = src.name;
126
+ if (src.kind === "teach" || src.kind === "operator") { isTaught = true; taughtTag = tag; }
127
+ }
128
+ const label = band || (isTaught ? "taught this session" : "other");
129
+ bandCounts[label] = (bandCounts[label] || 0) + 1;
130
+ if (isTaught) taught.push({ subject: row.subject, predicate: row.predicate, object: row.object, tag: taughtTag });
131
+ }
132
+ return { total: rows.length, bandCounts, taught };
133
+ }
134
+
135
+ globalThis.tmctChat = { createChatSession, registerWinkModel, registerReferencePackProvider, normFactTerm, vocabExampleHint, memoryStats };
@@ -9268,6 +9268,7 @@ ${bodyText}` : graphText, tier });
9268
9268
  findContradictions: () => findContradictions,
9269
9269
  findRuleByName: () => findRuleByName,
9270
9270
  findRulesByName: () => findRulesByName,
9271
+ isMemoryOrSqliteHandle: () => isMemoryOrSqliteHandle,
9271
9272
  loadMemory: () => loadMemory,
9272
9273
  loadSyllogiseState: () => loadSyllogiseState,
9273
9274
  normFactPredicate: () => normFactPredicate,
@@ -22059,6 +22060,7 @@ ${codeblock}`, options);
22059
22060
  init_prose();
22060
22061
  init_text_stats();
22061
22062
  init_trust();
22063
+ init_core();
22062
22064
  BLOCKS_DIR_REL = join(".tmct", "memory", "blocks");
22063
22065
  }
22064
22066
  });