@polycode-projects/the-mechanical-code-talker 3.0.1 → 3.0.3

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.
@@ -504,8 +504,8 @@ function sparkCaptionHtml(stats) {
504
504
  * different questions (did this render even offer the reference; did the
505
505
  * browser actually manage to load it), and both must hold for the live
506
506
  * path to run. */
507
- export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, worthALook, payload, meta, memoryAskBundle, stats, ledgerBundleAvailable = false } = {}) {
508
- const ledgerJson = embedJson({ rows: rows || [], terms: terms || [], edges: edges || [], focus: focus || null, contradictions: contradictions || [], worthALook: worthALook || null, meta: meta || { shown: 0, total: 0, truncated: false } });
507
+ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, worthALook, payload, meta, memoryAskBundle, stats, ledgerBundleAvailable = false, focusDigest = null, digestStructures = [] } = {}) {
508
+ const ledgerJson = embedJson({ rows: rows || [], terms: terms || [], edges: edges || [], focus: focus || null, contradictions: contradictions || [], worthALook: worthALook || null, meta: meta || { shown: 0, total: 0, truncated: false }, focusDigest: focusDigest || null, digestStructures: Array.isArray(digestStructures) ? digestStructures : [] });
509
509
  const payloadJson = embedJson(payload || { individuals: [], objectProperties: [] });
510
510
  const shown = meta?.shown ?? (rows || []).length;
511
511
  const title = `tmct ledger — ${shown} fact${shown === 1 ? "" : "s"}${focus ? ` (focus: ${escapeHtml(focus)})` : ""}`;
@@ -641,6 +641,15 @@ ${THEME_TOKENS_CSS}
641
641
  .focuscard { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: .8rem 1rem; margin-bottom: 1rem; }
642
642
  .focuscard .term { font-size: 1.35rem; font-weight: 700; }
643
643
  .focuscard .klass, .focuscard .stats { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--muted); margin-top: .3rem; font-variant-numeric: tabular-nums; }
644
+ .focuscard .focusdigest { margin-top: .7rem; font-size: .92rem; line-height: 1.5; }
645
+ .focuscard .focusdigest p { margin: 0 0 .4rem; }
646
+ .focuscard .focusdigest .dgsrc { font-family: ${MONO_STACK}; font-size: .7rem; color: var(--muted); margin-top: .2rem; }
647
+ .focuscard .focusdigest .dgfacts { margin-top: .5rem; }
648
+ .focuscard .focusdigest .dgfacts > summary { font-family: ${MONO_STACK}; font-size: .7rem; color: var(--corpus); cursor: pointer; }
649
+ .focuscard .focusdigest .dgfacts[open] > summary { margin-bottom: .35rem; }
650
+ .focuscard .focusdigest .dgfactlist { border-top: 1px solid var(--line); }
651
+ .focuscard .focusdigest .dgfact { font-size: .72rem; padding: .22rem 0; border-bottom: 1px solid var(--line); word-break: break-word; }
652
+ .focuscard .focusdigest .dgfact:last-child { border-bottom: none; }
644
653
  .group { margin: 1.1rem 0; }
645
654
  .group h3 { font-family: ${MONO_STACK}; font-size: .68rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); font-weight: 400; margin: 0 0 .35rem; display: flex; align-items: baseline; gap: .5rem; }
646
655
  .group h3::after { content: ""; flex: 1; border-top: 1px solid var(--line); transform: translateY(-.2em); }
@@ -805,6 +814,30 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
805
814
  let trail = focus ? [{ term: focus, label: null }] : [];
806
815
  const sel = { prov: new Set(), fam: new Set(), rec: new Set() };
807
816
 
817
+ // The store the client-side digest reads. Defaults to the page's embedded
818
+ // PAYLOAD (a static viz page never changes it); once a live dock session
819
+ // exists it points at that session's store, which teach/research grow in
820
+ // place, so a digest of a just-taught term reads the fresh facts.
821
+ let getLivePayload = () => PAYLOAD;
822
+ // Whichever loaded bundle carries the browser digest helper — the demo
823
+ // ledger's own live engine (tmctLedger) or the committed memory-ask engine
824
+ // the CLI viz page inlines (tmctMemoryAsk). Null before either loads, and the
825
+ // focus card then keeps whatever server-computed digest it shipped.
826
+ const digestHelper = () =>
827
+ (typeof tmctLedger !== "undefined" && tmctLedger && tmctLedger.digestTermFromPayloadBrowser)
828
+ || (typeof tmctMemoryAsk !== "undefined" && tmctMemoryAsk && tmctMemoryAsk.digestTermFromPayloadBrowser)
829
+ || null;
830
+ // The digest for one term, computed live in the browser from the embedded
831
+ // structure table, or null when no structures were embedded, no engine has
832
+ // loaded, or the term holds nothing to compose.
833
+ function clientDigest(term) {
834
+ const structures = LEDGER.digestStructures;
835
+ const helper = digestHelper();
836
+ if (!term || !helper || !structures || !structures.length) return null;
837
+ try { return helper(getLivePayload(), term, structures, { budget: 8 }); }
838
+ catch { return null; }
839
+ }
840
+
808
841
  const recOf = (r) => {
809
842
  const t = Date.parse(r.createdAt);
810
843
  if (!Number.isFinite(t)) return "older";
@@ -860,10 +893,28 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
860
893
  const srcs = new Set(all.map((r) => r.src.split(" | ")[0]));
861
894
  const dates = all.map((r) => r.createdAt).filter(Boolean).sort();
862
895
  const klass = all.find((r) => r.s === focus && r.family === "is-a");
896
+ // The digest paragraph recomputes client-side on every focus, from the
897
+ // embedded structure table over the current store — so a refocus reads the
898
+ // new term back rather than losing the card. The server-computed focus
899
+ // digest is the fallback for the initial focus alone, kept for a page whose
900
+ // engine bundle never loads (its client digest would come back null).
901
+ const dg = clientDigest(focus)
902
+ || ((focus === LEDGER.focus && LEDGER.focusDigest && LEDGER.focusDigest.paragraphs && LEDGER.focusDigest.paragraphs.length) ? LEDGER.focusDigest : null);
903
+ const dgFacts = (dg && Array.isArray(dg.facts)) ? dg.facts : [];
904
+ const factsEscapeHtml = dgFacts.length
905
+ ? '<details class="dgfacts"><summary>show the facts (' + dgFacts.length + ')</summary><div class="dgfactlist">' +
906
+ dgFacts.map((f) => '<div class="dgfact"><span class="mono">' + esc(String(f.subject || "")) + " " + esc(String(f.predicate || "")) + " " + esc(String(f.object || "")) + "</span></div>").join("") +
907
+ "</div></details>"
908
+ : "";
909
+ const digestHtml = dg
910
+ ? '<div class="focusdigest">' + dg.paragraphs.map((p) => "<p>" + esc(p) + "</p>").join("") +
911
+ (dg.sources && dg.sources.length ? '<p class="dgsrc">(sources: ' + esc(dg.sources.join("; ")) + ")</p>" : "") +
912
+ factsEscapeHtml + "</div>"
913
+ : "";
863
914
  let html = '<div class="focuscard"><div class="term">' + esc(focus) + "</div>" +
864
915
  '<div class="klass">' + (klass ? esc(klass.phrase + " " + klass.o) : "no class recorded") + "</div>" +
865
916
  '<div class="stats">' + all.length + " facts &middot; " + srcs.size + " source" + (srcs.size === 1 ? "" : "s") +
866
- (dates.length ? " &middot; first " + esc(dates[0].slice(0, 10)) + " &middot; last " + esc(dates[dates.length - 1].slice(0, 10)) : "") + "</div></div>";
917
+ (dates.length ? " &middot; first " + esc(dates[0].slice(0, 10)) + " &middot; last " + esc(dates[dates.length - 1].slice(0, 10)) : "") + "</div>" + digestHtml + "</div>";
867
918
  const bracketed = new Set();
868
919
  for (const fam of FAMS) {
869
920
  const rows = mine.filter((r) => r.family === fam && !bracketed.has(r.id));
@@ -998,6 +1049,11 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
998
1049
  rows: freshData.rows, terms: freshData.terms, edges: freshData.edges,
999
1050
  focus: freshData.focus, contradictions: freshData.contradictions,
1000
1051
  worthALook: freshData.worthALook, meta: freshData.meta,
1052
+ // The embedded structure table is build-time data a re-derivation never
1053
+ // recomputes; carry it across so a digest of the just-taught term still
1054
+ // has a table to compose from. The stale server focusDigest is dropped —
1055
+ // the client recompute over the grown store supersedes it.
1056
+ digestStructures: LEDGER.digestStructures,
1001
1057
  };
1002
1058
  rebuildIndexes();
1003
1059
  el("dash").outerHTML = dashboardHtml(freshData.stats, { fresh: true });
@@ -1069,6 +1125,10 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
1069
1125
  if (session) return session;
1070
1126
  await tryLoadWink();
1071
1127
  session = await tmctLedger.createLedgerSession({ seedPayload: PAYLOAD });
1128
+ // From here the live store is the source of truth for the digest, so a
1129
+ // digest of a term taught this session reads its fresh facts — the store
1130
+ // grows in place, so this one closure stays current.
1131
+ getLivePayload = () => session.memoryDir.payload;
1072
1132
  return session;
1073
1133
  }
1074
1134
 
@@ -73,9 +73,14 @@ export function loadProgressLine(parts) {
73
73
  }
74
74
 
75
75
  /** The self-contained research page. Pure — the same output for the same
76
- * `title` every time; every piece of state is computed live in the browser
77
- * once the sibling research bundle loads. */
78
- export function renderResearchHtml({ title = DEFAULT_TITLE } = {}) {
76
+ * input every time; every piece of state is computed live in the browser once
77
+ * the sibling research bundle loads. `digestStructures` are the pre-parsed
78
+ * [[structure]] rows of the digest sentence-structure bank, embedded so the
79
+ * page can digest a term client-side over its grown store (no TOML parser in
80
+ * the browser); an empty list leaves the digest panel degrading to an honest
81
+ * "no digest available" the same way the node stub does. */
82
+ export function renderResearchHtml({ title = DEFAULT_TITLE, digestStructures = [] } = {}) {
83
+ const digestStructuresJson = JSON.stringify(Array.isArray(digestStructures) ? digestStructures : []);
79
84
  return `<!doctype html>
80
85
  <html lang="en">
81
86
  <head>
@@ -148,8 +153,26 @@ ${THEME_TOKENS_CSS}
148
153
 
149
154
  .chips { display: flex; flex-wrap: wrap; gap: .4rem; }
150
155
  .chip { font-family: ${MONO_STACK}; font-size: .74rem; border: 1px solid var(--line); border-radius: 99px; padding: .2rem .6rem; background: var(--bg); }
156
+ .chip.tapchip { cursor: pointer; }
157
+ .chip.tapchip:hover { border-color: var(--ink); }
151
158
  .chip .deg { color: var(--muted); margin-left: .35rem; }
152
159
 
160
+ /* the term digest: a narrative card, its sources, and the flat fact list one
161
+ click away behind "show the facts". */
162
+ .digestpanel .hint { font-size: .78rem; color: var(--muted); margin: 0 0 .55rem; }
163
+ .digestout { min-height: 1.4rem; }
164
+ .digestout .empty { color: var(--muted); font-size: .82rem; margin: .2rem 0; }
165
+ .digestout .miss { color: var(--muted); font-size: .88rem; border: 1px dashed var(--line); border-radius: 8px; padding: .55rem .7rem; }
166
+ .dgcard { border: 1px solid var(--line); border-radius: 8px; padding: .7rem .85rem .8rem; background: var(--bg); }
167
+ .dgterm { font-family: ${MONO_STACK}; font-size: .72rem; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); margin-bottom: .4rem; }
168
+ .dgcard p { margin: 0 0 .5rem; font-size: .95rem; }
169
+ .dgcard p:last-of-type { margin-bottom: 0; }
170
+ .dgsrc { font-family: ${MONO_STACK}; font-size: .7rem; color: var(--muted); word-break: break-word; }
171
+ .dgfacts { margin-top: .6rem; }
172
+ .dgfacts > summary { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--corpus); cursor: pointer; list-style: revert; }
173
+ .dgfacts[open] > summary { margin-bottom: .35rem; }
174
+ .dgfacts .factlist { border-top: 1px solid var(--line); }
175
+
153
176
  /* ask, scoped by source */
154
177
  .askRow { display: flex; gap: .5rem; margin: .2rem 0 .7rem; }
155
178
  .askRow input { flex: 1; min-width: 0; font-family: ${SERIF_STACK}; font-size: .92rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 8px; padding: .45rem .7rem; }
@@ -247,6 +270,16 @@ ${THEME_TOKENS_CSS}
247
270
  </div>
248
271
  </div>
249
272
 
273
+ <h2 class="band">read a term back</h2>
274
+ <div class="panel digestpanel">
275
+ <p class="hint">Pick a term the graph knows and it reads back a short narrative &mdash; the facts it holds, composed into sentences with the sources named, deterministic and never a guess. The full fact list stays one click away behind &ldquo;show the facts&rdquo;.</p>
276
+ <div class="askRow">
277
+ <input id="digestInput" type="text" autocomplete="off" spellcheck="false" placeholder="a term the graph knows, e.g. dog" aria-label="A term to read back as a digest" disabled>
278
+ <button type="button" class="btn primary" id="digestGo" disabled>digest</button>
279
+ </div>
280
+ <div id="digestOut" class="digestout" aria-live="polite"><p class="empty">Ask for a digest, or click a term under &ldquo;best-connected terms&rdquo; above.</p></div>
281
+ </div>
282
+
250
283
  <h2 class="band">ask the graph</h2>
251
284
  <div class="cols">
252
285
  <div class="panel">
@@ -283,6 +316,10 @@ ${THEME_TOKENS_CSS}
283
316
  const createTicker = ${createTicker.toString()};
284
317
  const prefersReducedMotion = ${prefersReducedMotion.toString()};
285
318
  const el = (id) => document.getElementById(id);
319
+ // The digest sentence-structure bank, pre-parsed at build time — the browser
320
+ // has no TOML parser, so the page carries the table the client-side digest
321
+ // composes from.
322
+ const DIGEST_STRUCTURES = ${digestStructuresJson};
286
323
 
287
324
  if ("serviceWorker" in navigator) navigator.serviceWorker.register("./tmct-sw.js").catch(() => {});
288
325
 
@@ -332,7 +369,7 @@ ${THEME_TOKENS_CSS}
332
369
  try { return structuredClone(seedPayload); } catch { return JSON.parse(JSON.stringify(seedPayload)); }
333
370
  };
334
371
  function newSession() {
335
- return window.tmctResearch.createResearchSession({ seedPayload: cloneSeed(), vocabSeeded: Boolean(seedPayload) });
372
+ return window.tmctResearch.createResearchSession({ seedPayload: cloneSeed(), vocabSeeded: Boolean(seedPayload), digestStructures: DIGEST_STRUCTURES });
336
373
  }
337
374
 
338
375
  // The curated reference pack provider, same fetch seam chat.html registers,
@@ -377,6 +414,8 @@ ${THEME_TOKENS_CSS}
377
414
  el("ingestGo").disabled = false;
378
415
  el("askInput").disabled = false;
379
416
  el("askGo").disabled = false;
417
+ el("digestInput").disabled = false;
418
+ el("digestGo").disabled = false;
380
419
  el("exportFacts").disabled = false;
381
420
  }
382
421
 
@@ -427,13 +466,16 @@ ${THEME_TOKENS_CSS}
427
466
  return;
428
467
  }
429
468
  for (const hub of hubs) {
430
- const chip = document.createElement("span");
431
- chip.className = "chip";
469
+ const chip = document.createElement("button");
470
+ chip.type = "button";
471
+ chip.className = "chip tapchip";
472
+ chip.setAttribute("aria-label", "read a digest of " + hub.term);
432
473
  chip.appendChild(document.createTextNode(hub.term));
433
474
  const deg = document.createElement("span");
434
475
  deg.className = "deg";
435
476
  deg.textContent = hub.degree + (hub.degree === 1 ? " fact" : " facts");
436
477
  chip.appendChild(deg);
478
+ chip.addEventListener("click", () => { el("digestInput").value = hub.term; digest(); });
437
479
  box.appendChild(chip);
438
480
  }
439
481
  }
@@ -544,6 +586,64 @@ ${THEME_TOKENS_CSS}
544
586
  el("askGo").addEventListener("click", ask);
545
587
  el("askInput").addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); ask(); } });
546
588
 
589
+ // ---- digest a term: the narrative read-back over the grown store ---------
590
+ function renderDigestFact(fact) {
591
+ const row = document.createElement("div");
592
+ row.className = "fact";
593
+ const dot = document.createElement("span"); dot.className = "dot tone-seed";
594
+ const subj = document.createElement("span"); subj.className = "subj"; subj.textContent = String(fact.subject || "");
595
+ const pred = document.createElement("span"); pred.className = "pred"; pred.textContent = String(fact.predicate || "");
596
+ const obj = document.createElement("span"); obj.className = "obj"; obj.textContent = String(fact.object || "");
597
+ row.appendChild(dot); row.appendChild(subj); row.appendChild(pred); row.appendChild(obj);
598
+ return row;
599
+ }
600
+ function renderDigest(term, view) {
601
+ const box = el("digestOut");
602
+ box.textContent = "";
603
+ if (!view) {
604
+ const p = document.createElement("p"); p.className = "miss";
605
+ p.textContent = 'No grounded digest for "' + term + '" — nothing is stored about it, or nothing the digest could compose. It abstains rather than guess.';
606
+ box.appendChild(p);
607
+ return;
608
+ }
609
+ const card = document.createElement("div"); card.className = "dgcard";
610
+ const head = document.createElement("div"); head.className = "dgterm mono"; head.textContent = view.term || term;
611
+ card.appendChild(head);
612
+ for (const para of view.paragraphs) {
613
+ const p = document.createElement("p"); p.textContent = para; card.appendChild(p);
614
+ }
615
+ if (view.sources && view.sources.length) {
616
+ const src = document.createElement("p"); src.className = "dgsrc";
617
+ src.textContent = "(sources: " + view.sources.join("; ") + ")";
618
+ card.appendChild(src);
619
+ }
620
+ const facts = view.facts || [];
621
+ if (facts.length) {
622
+ const det = document.createElement("details"); det.className = "dgfacts";
623
+ const sum = document.createElement("summary");
624
+ sum.textContent = "show the facts (" + view.factCount + ")";
625
+ det.appendChild(sum);
626
+ const list = document.createElement("div"); list.className = "factlist";
627
+ for (const f of facts) list.appendChild(renderDigestFact(f));
628
+ det.appendChild(list);
629
+ card.appendChild(det);
630
+ }
631
+ box.appendChild(card);
632
+ }
633
+ async function digest() {
634
+ const term = el("digestInput").value.trim();
635
+ if (!term || !session) return;
636
+ const box = el("digestOut");
637
+ box.textContent = "";
638
+ const wait = document.createElement("p"); wait.className = "empty"; wait.textContent = "reading it back…";
639
+ box.appendChild(wait);
640
+ let view = null;
641
+ try { view = await session.digest(term); } catch { view = null; }
642
+ renderDigest(term, view);
643
+ }
644
+ el("digestGo").addEventListener("click", digest);
645
+ el("digestInput").addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); digest(); } });
646
+
547
647
  // ---- grow: teach by telling --------------------------------------------
548
648
  async function teach() {
549
649
  const q = el("teachInput").value.trim();
@@ -0,0 +1,73 @@
1
+ // digest-client.mjs — the browser-native side of the digest layer's stage-5
2
+ // wiring. The node surfaces (chat, `tmct digest`, the page generators) reach
3
+ // the pure pipeline through adapters/corpus/digest-bank.mjs, which reads the
4
+ // sentence-structure bank off disk with a TOML parser. A browser can't do that,
5
+ // so the bundle scripts stub digest-bank out — and the pages that grow or hold
6
+ // a store IN the browser (research, the ledger dock, the CLI `tmct viz` page)
7
+ // digest from a structure table embedded in the page as JSON at build time.
8
+ //
9
+ // This module is that browser path: it feeds the embedded structures straight
10
+ // into the SAME pure layer (src/domain/digest — buildStructureTable, digestTerm,
11
+ // the store-stats scan) the node seam uses, so the two produce the same article
12
+ // from the same rows. It ships no TOML parser; the page hands it pre-parsed
13
+ // [[structure]] rows.
14
+
15
+ import { buildStructureTable, digestTerm } from "../../domain/digest/index.mjs";
16
+ import { digestStoreStats, chainsForObjects, isaObjectsOf } from "../../domain/digest/store-stats.mjs";
17
+ import { readFactRows } from "../../adapters/memory/core.mjs";
18
+
19
+ /**
20
+ * Digest one term end to end from fact rows and a pre-parsed structure table.
21
+ * The browser twin of digestTermFromRows: identical pipeline, but the structure
22
+ * rows arrive as data (embedded JSON) rather than from a filesystem TOML read.
23
+ *
24
+ * `structures` is the array of [[structure]] rows the bank parses to. Returns
25
+ * the term-article shape, or null when no structures were supplied (the page
26
+ * then falls back to its flat fact list, the same degradation the node stub
27
+ * gives).
28
+ */
29
+ export function digestTermFromRowsBrowser(term, termRows, allRows, structures, opts = {}) {
30
+ const table = buildStructureTable(structures || []);
31
+ if (!table || table.size === 0) return null;
32
+ const rows = termRows || [];
33
+ const store = digestStoreStats(allRows || rows);
34
+ const chains = chainsForObjects(store.subClassEdges, isaObjectsOf(rows));
35
+ return digestTerm(term, rows, store, table, { ...opts, chains });
36
+ }
37
+
38
+ /**
39
+ * Digest one term straight from a loadMemory()-shaped payload — the shape every
40
+ * in-browser surface already holds (the ledger's embedded PAYLOAD, the ledger
41
+ * dock's live memoryDir.payload, the research session's store). Scans the
42
+ * payload once for the term's own rows and the whole-store statistics, so a
43
+ * caller never has to run readFactRows itself.
44
+ *
45
+ * Returns the render-ready view (see digestViewFromArticle) or null when the
46
+ * term has no rows, no structures were supplied, or the selector kept nothing.
47
+ */
48
+ export function digestTermFromPayloadBrowser(payload, term, structures, opts = {}) {
49
+ const rows = readFactRows(payload || { individuals: [], objectProperties: [] });
50
+ const termRows = rows.filter((r) => r.subject === term);
51
+ if (!termRows.length) return null;
52
+ const article = digestTermFromRowsBrowser(term, termRows, rows, structures, opts);
53
+ return digestViewFromArticle(article);
54
+ }
55
+
56
+ /**
57
+ * The flat, render-ready view a page's DOM code wants from a term article:
58
+ * the narrative paragraphs, the distinct source strings (the "(sources: …)"
59
+ * line the chat and CLI surfaces render), and the full fact list behind the
60
+ * "show the facts" escape. Null when the article carried no narrative, so the
61
+ * caller falls back to its flat rendering rather than showing an empty card.
62
+ */
63
+ export function digestViewFromArticle(article) {
64
+ if (!article || !Array.isArray(article.paragraphs) || !article.paragraphs.length) return null;
65
+ const facts = (article.detail && Array.isArray(article.detail.facts)) ? article.detail.facts : [];
66
+ return {
67
+ term: article.term || "",
68
+ paragraphs: article.paragraphs.slice(),
69
+ sources: [...new Set((article.sources || []).map((s) => s.provenance).filter(Boolean))],
70
+ facts,
71
+ factCount: (article.detail && Number.isInteger(article.detail.factCount)) ? article.detail.factCount : facts.length,
72
+ };
73
+ }
@@ -25,6 +25,7 @@ import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
25
25
  import { registerWinkModel } from "../../adapters/wink-model.mjs";
26
26
  import { registerResearchProvider } from "../../adapters/corpus/wikipedia-live.mjs";
27
27
  import { computeLedgerDataFromPayload } from "../../services/ledger-viz.mjs";
28
+ import { digestTermFromPayloadBrowser } from "./digest-client.mjs";
28
29
 
29
30
  /**
30
31
  * A browser ledger-dock session over the real turn engine — createChatSession's
@@ -101,4 +102,4 @@ export async function exportFactsJsonl(memoryDir) {
101
102
  // splitSentences + exportFactsJsonl carry the dock's paste-and-drop ingest and
102
103
  // its JSONL export across the bundle boundary, the same one-serializer posture
103
104
  // chat-browser-entry.mjs holds for its own page.
104
- globalThis.tmctLedger = { createLedgerSession, computeLedgerDataFromPayload, normFactTerm, registerWinkModel, registerResearchProvider, splitSentences: splitSentencesPreservingPaths, exportFactsJsonl };
105
+ globalThis.tmctLedger = { createLedgerSession, computeLedgerDataFromPayload, normFactTerm, registerWinkModel, registerResearchProvider, splitSentences: splitSentencesPreservingPaths, exportFactsJsonl, digestTermFromPayloadBrowser };
@@ -8,8 +8,13 @@
8
8
  // bypassing the structural-graph parse pipeline this dock has no use for.
9
9
  import { factAnswer, factReadBack } from "../../services/chat.mjs";
10
10
  import { createInMemoryStore, normFactTerm } from "../../adapters/memory/core.mjs";
11
+ import { digestTermFromPayloadBrowser } from "./digest-client.mjs";
11
12
 
12
13
  // normFactTerm is re-exported too, for the page's client-side term normalization.
13
14
  // factReadBack carries the taught-relation chases (grandfather-style questions)
14
15
  // that factAnswer's own lanes don't reach.
15
- globalThis.tmctMemoryAsk = { factAnswer, factReadBack, createInMemoryStore, normFactTerm };
16
+ // digestTermFromPayloadBrowser lets the CLI `tmct viz` page read a refocused
17
+ // term back as a digest client-side, from the structure table the page embeds —
18
+ // the same narrative the demo ledger's live bundle produces, over the same
19
+ // static store this page already carries.
20
+ globalThis.tmctMemoryAsk = { factAnswer, factReadBack, createInMemoryStore, normFactTerm, digestTermFromPayloadBrowser };