@polycode-projects/the-mechanical-code-talker 3.0.2 → 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.
@@ -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 };