@polycode-projects/the-mechanical-code-talker 4.0.0 → 4.1.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 (108) hide show
  1. package/README.md +178 -9
  2. package/corpus/reference/index.json.gz +0 -0
  3. package/corpus/reference/manifest.json +8 -8
  4. package/corpus/reference/shards/ref-00.jsonl.gz +0 -0
  5. package/corpus/sprites/src/sprite-facts.jsonl +401 -0
  6. package/corpus/worlds/index.json.gz +0 -0
  7. package/corpus/worlds/manifest.json +49 -9
  8. package/corpus/worlds/shards/greyvale-museum.jsonl.gz +0 -0
  9. package/corpus/worlds/shards/lantern-cottage.jsonl.gz +0 -0
  10. package/corpus/worlds/shards/mud-hollow.jsonl.gz +0 -0
  11. package/corpus/worlds/shards/mud-warren.jsonl.gz +0 -0
  12. package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
  13. package/corpus/worlds/src/greyvale-museum.jsonl +136 -0
  14. package/corpus/worlds/src/lantern-cottage.jsonl +60 -0
  15. package/corpus/worlds/src/mud-hollow.jsonl +82 -0
  16. package/corpus/worlds/src/mud-warren.jsonl +124 -0
  17. package/corpus/worlds/src/spider-fly.jsonl +1 -1
  18. package/data/sprites/animal-icon.toml +11 -5
  19. package/data/sprites/book-icon.toml +9 -5
  20. package/data/sprites/cabinet-icon.toml +10 -5
  21. package/data/sprites/cellar-icon.toml +16 -6
  22. package/data/sprites/container-icon.toml +7 -3
  23. package/data/sprites/desk-icon.toml +8 -5
  24. package/data/sprites/dog-icon.toml +8 -5
  25. package/data/sprites/dog-with-colour-icon.toml +13 -12
  26. package/data/sprites/drawing-room-icon.toml +14 -5
  27. package/data/sprites/egg-icon.toml +6 -3
  28. package/data/sprites/fly-icon.toml +10 -5
  29. package/data/sprites/furniture-icon.toml +5 -3
  30. package/data/sprites/garden-icon.toml +10 -3
  31. package/data/sprites/key-icon.toml +3 -1
  32. package/data/sprites/kitchen-icon.toml +15 -6
  33. package/data/sprites/lamp-icon.toml +9 -4
  34. package/data/sprites/letter-icon.toml +7 -4
  35. package/data/sprites/library-icon.toml +14 -3
  36. package/data/sprites/pan-icon.toml +7 -3
  37. package/data/sprites/person-icon.toml +6 -2
  38. package/data/sprites/poodle-icon.toml +1 -1
  39. package/data/sprites/portable-icon.toml +6 -4
  40. package/data/sprites/portrait-icon.toml +7 -4
  41. package/data/sprites/room-icon.toml +11 -2
  42. package/data/sprites/spider-icon.toml +9 -3
  43. package/data/sprites/study-icon.toml +10 -2
  44. package/package.json +5 -4
  45. package/src/adapters/memory/core.mjs +20 -0
  46. package/src/domain/ask-vocab.mjs +71 -0
  47. package/src/domain/ask.mjs +168 -0
  48. package/src/domain/game-config.mjs +11 -0
  49. package/src/domain/grammar/ace.mjs +40 -4
  50. package/src/domain/grammar/lexicon-core.json +2 -1
  51. package/src/domain/grammar/lexicon.mjs +18 -0
  52. package/src/domain/mud-facts.mjs +15 -0
  53. package/src/domain/reference-pack.mjs +31 -7
  54. package/src/domain/router/drive.mjs +35 -9
  55. package/src/domain/router/registry.mjs +24 -4
  56. package/src/domain/router/resolver.mjs +102 -40
  57. package/src/domain/scene-compose.mjs +117 -0
  58. package/src/domain/spider-fly-world.mjs +37 -1
  59. package/src/domain/sprite-facts.mjs +0 -0
  60. package/src/domain/sprite-request.mjs +156 -0
  61. package/src/domain/sprite-templates.mjs +169 -20
  62. package/src/services/adventure-editor.mjs +8 -14
  63. package/src/services/adventure-viz.mjs +209 -157
  64. package/src/services/adventure.mjs +526 -391
  65. package/src/services/chat-page-viz.mjs +69 -25
  66. package/src/services/chat.mjs +200 -51
  67. package/src/services/code-explorer-viz.mjs +102 -62
  68. package/src/services/extract-facts.mjs +4 -7
  69. package/src/services/ingest-viz.mjs +68 -82
  70. package/src/services/ledger-viz.mjs +136 -67
  71. package/src/services/memory-panel-viz.mjs +62 -0
  72. package/src/services/mud-editor.mjs +10 -15
  73. package/src/services/mud-turn.mjs +6 -6
  74. package/src/services/mud-viz.mjs +1016 -208
  75. package/src/services/p2p-room.mjs +90 -23
  76. package/src/services/plan-pddl.mjs +3 -1
  77. package/src/services/plan-viz.mjs +123 -64
  78. package/src/services/research-viz.mjs +160 -108
  79. package/src/services/spider-fly-turn.mjs +15 -23
  80. package/src/services/spider-fly-viz.mjs +146 -161
  81. package/src/services/spider-fly.mjs +69 -11
  82. package/src/services/sprite-catalog-viz.mjs +414 -240
  83. package/src/services/viz-boot.mjs +71 -0
  84. package/src/services/viz-room-graph.mjs +203 -0
  85. package/src/services/viz-theme.mjs +90 -1
  86. package/src/services/viz-ticker.mjs +22 -0
  87. package/src/surfaces/web/adventure-browser-entry.mjs +49 -33
  88. package/src/surfaces/web/chat-browser-entry.mjs +30 -105
  89. package/src/surfaces/web/code-explorer-browser-entry.mjs +168 -24
  90. package/src/surfaces/web/ingest-browser-entry.mjs +3 -13
  91. package/src/surfaces/web/ledger-browser-entry.mjs +7 -47
  92. package/src/surfaces/web/memory-ask-browser.bundle.js +127 -124
  93. package/src/surfaces/web/memory-stats.mjs +11 -0
  94. package/src/surfaces/web/mud-browser-entry.mjs +71 -29
  95. package/src/surfaces/web/plan-browser-entry.mjs +22 -40
  96. package/src/surfaces/web/research-browser-entry.mjs +26 -41
  97. package/src/surfaces/web/spider-fly-browser-entry.mjs +45 -28
  98. package/src/surfaces/web/sprites-browser-entry.mjs +14 -27
  99. package/src/surfaces/web/turn-session.mjs +120 -0
  100. package/src/tools/definitions.mjs +30 -0
  101. package/src/tools/handlers/index.mjs +6 -3
  102. package/src/tools/handlers/kit.mjs +19 -2
  103. package/src/tools/handlers/tmct-ask.mjs +11 -6
  104. package/src/tools/handlers/tmct-ingest.mjs +5 -1
  105. package/src/tools/handlers/tmct-related.mjs +4 -4
  106. package/src/tools/handlers/tmct-sprite.mjs +147 -0
  107. package/src/tools/memory-fallthrough.mjs +9 -2
  108. package/src/tools/server.mjs +25 -1
@@ -19,12 +19,34 @@
19
19
  // renderResearchHtml() is pure: no I/O, deterministic output for identical
20
20
  // input. scripts/build-demo-site.mjs calls it directly and writes the result to
21
21
  // public/research.html, after research-browser.bundle.js already exists.
22
- import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
23
- import { fetchWithProgress } from "./memory-panel-viz.mjs";
22
+ import { THEME_TOKENS_CSS, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
23
+ import { fetchWithProgress, loadProgressLine, factTripleParts } from "./memory-panel-viz.mjs";
24
24
  import { createTicker, prefersReducedMotion } from "./viz-ticker.mjs";
25
+ import { loadWinkVendor } from "./viz-boot.mjs";
26
+ import { cloneMemoryPayload } from "../adapters/memory/core.mjs";
25
27
 
26
28
  const DEFAULT_TITLE = "the-mechanical-code-talker — research";
27
29
 
30
+ // research.html reads as ledger.html's dashboard sibling (dense stat/control
31
+ // panels, monospace metrics), so it runs the same system sans + forced dark
32
+ // chrome — see ledger-viz.mjs's own DASH_SANS_STACK/DASH_DARK_CHROME_CSS for
33
+ // the fuller rationale. Kept as a separate copy rather than a shared import:
34
+ // each generated-page builder is a standalone, self-contained module (no
35
+ // cross-page runtime dependency), matching every sibling page builder here.
36
+ const DASH_SANS_STACK = `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`;
37
+ const DASH_DARK_CHROME_CSS = `
38
+ :root { color-scheme: dark; }
39
+ body {
40
+ --bg: #15181C; --ink: #E7E5DF; --muted: #9A9E95; --line: #2B3036; --card: #1C2126;
41
+ --taught: #5FBE8B; --corpus: #6C93BF; --entail: #D9A554; --alert: #D08070;
42
+ --taught-t1: rgba(95,190,139,.35); --taught-t2: rgba(95,190,139,.65); --taught-t3: rgba(95,190,139,1);
43
+ --corpus-t1: rgba(108,147,191,.35); --corpus-t2: rgba(108,147,191,.65); --corpus-t3: rgba(108,147,191,1);
44
+ --entail-t1: rgba(217,165,84,.35); --entail-t2: rgba(217,165,84,.65); --entail-t3: rgba(217,165,84,1);
45
+ --taught-soft: rgba(95,190,139,.14); --corpus-soft: rgba(108,147,191,.14);
46
+ --entail-soft: rgba(217,165,84,.16); --alert-soft: rgba(208,128,112,.16);
47
+ }
48
+ `;
49
+
28
50
  /** The human label + short colour key for one source snapshot entry
29
51
  * ({ key, band }). A seed band folds to a readable corpus name; the three
30
52
  * growth sources get their own words. Pure and `.toString()`-splice safe. */
@@ -43,35 +65,6 @@ export function sourceLabelFor(source) {
43
65
  return { label: (BANDS[band] || band || "seed") + " (seed corpus)", tone: "seed" };
44
66
  }
45
67
 
46
- /** One learned fact as its three canonical cells plus a source tone, for the
47
- * highlights and history lists. Pure, `.toString()`-splice safe. */
48
- export function factTripleParts(fact) {
49
- return {
50
- subject: String((fact && fact.subject) || ""),
51
- predicate: String((fact && fact.predicate) || ""),
52
- object: String((fact && fact.object) || ""),
53
- source: String((fact && fact.source) || ""),
54
- };
55
- }
56
-
57
- /** The boot statusline while the big assets stream in — the same aggregator
58
- * ingest-viz.mjs's own loadProgressLine is. `parts` is an array of
59
- * { loaded, total } byte counts. Self-contained, `.toString()`-splice safe. */
60
- export function loadProgressLine(parts) {
61
- const mb = (n) => (n / 1048576).toFixed(1);
62
- let loaded = 0;
63
- let total = 0;
64
- let totalKnown = true;
65
- for (const p of parts || []) {
66
- loaded += (p && p.loaded) || 0;
67
- if (p && p.total > 0) total += p.total;
68
- else totalKnown = false;
69
- }
70
- return totalKnown && total > 0
71
- ? "loading the engine… " + mb(loaded) + " MB / " + mb(total) + " MB"
72
- : "loading the engine… " + mb(loaded) + " MB";
73
- }
74
-
75
68
  /** The self-contained research page. Pure — the same output for the same
76
69
  * input every time; every piece of state is computed live in the browser once
77
70
  * the sibling research bundle loads. `digestStructures` are the pre-parsed
@@ -79,7 +72,7 @@ export function loadProgressLine(parts) {
79
72
  * page can digest a term client-side over its grown store (no TOML parser in
80
73
  * the browser); an empty list leaves the digest panel degrading to an honest
81
74
  * "no digest available" the same way the node stub does. */
82
- export function renderResearchHtml({ title = DEFAULT_TITLE, digestStructures = [] } = {}) {
75
+ export function renderResearchHtml({ title = DEFAULT_TITLE, digestStructures = [], seedStamp = "" } = {}) {
83
76
  const digestStructuresJson = JSON.stringify(Array.isArray(digestStructures) ? digestStructures : []);
84
77
  return `<!doctype html>
85
78
  <html lang="en">
@@ -99,34 +92,52 @@ export function renderResearchHtml({ title = DEFAULT_TITLE, digestStructures = [
99
92
  -->
100
93
  <style>
101
94
  ${THEME_TOKENS_CSS}
95
+ ${DASH_DARK_CHROME_CSS}
102
96
  html, body { min-height: 100%; }
103
- body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; }
97
+ body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${DASH_SANS_STACK}; font-size: 15px; line-height: 1.5; }
104
98
  .mono { font-family: ${MONO_STACK}; }
105
99
  button { font: inherit; color: inherit; background: none; cursor: pointer; border: none; }
106
- button:focus-visible, textarea:focus-visible, input:focus-visible, summary:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
100
+ button:focus-visible, textarea:focus-visible, input:focus-visible, summary:focus-visible { outline: 2px solid var(--corpus); outline-offset: 2px; }
107
101
  a { color: var(--corpus); }
108
102
 
109
- .wrap { max-width: 1100px; margin: 0 auto; padding: 1.1rem 1.1rem 3rem; }
110
-
111
- header.topbar { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; }
112
- .brand { display: flex; flex-direction: column; gap: .12rem; }
113
- .eyebrow { font-family: ${MONO_STACK}; font-size: .78rem; letter-spacing: .08em; color: var(--muted); }
114
- .subtitle { font-size: .86rem; color: var(--muted); max-width: 46ch; }
115
- .status { font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); }
103
+ .wrap { max-width: 1200px; margin: 0 auto; padding: 1.1rem 1.1rem 3rem; }
104
+
105
+ /* A brand block on the left and a small stat-readout panel on the right —
106
+ the readout fills the row instead of one status line stranded beside a
107
+ wide gap, and it doubles as the boot-progress display. */
108
+ header.topbar { display: flex; align-items: flex-start; justify-content: space-between; gap: 1.4rem; flex-wrap: wrap; padding: .6rem 0 1rem; border-bottom: 1px solid var(--line); margin-bottom: 1.3rem; }
109
+ .brand { display: flex; flex-direction: column; gap: .3rem; max-width: 640px; }
110
+ .eyebrow { font-family: ${MONO_STACK}; font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
111
+ .subtitle { font-size: .92rem; color: var(--ink); opacity: .82; max-width: 58ch; }
112
+ .statuspanel { display: flex; flex-wrap: wrap; gap: .5rem 1.1rem; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: .5rem .9rem; }
113
+ .statuspanel .stat { display: flex; flex-direction: column; gap: .14rem; min-width: 7rem; }
114
+ .statuspanel .stat-label { font-family: ${MONO_STACK}; font-size: .6rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
115
+ .statuspanel .stat-value { font-family: ${MONO_STACK}; font-size: .82rem; font-variant-numeric: tabular-nums; color: var(--ink); }
116
+ /* the live total is what this page is actually growing, so it carries the
117
+ panel's emphasis — the seed count beside it never moves after boot. */
118
+ .statuspanel .stat-facts { padding-right: 1.1rem; border-right: 1px solid var(--line); }
119
+ .statuspanel .stat-facts .stat-value { font-size: 1.15rem; font-weight: 600; }
120
+ .enginenote { margin: -.5rem 0 1.2rem; padding: .55rem .8rem; border: 1px solid var(--alert); border-radius: 6px; background: var(--alert-soft); color: var(--alert); font-family: ${MONO_STACK}; font-size: .78rem; }
121
+ .enginenote[hidden] { display: none; }
116
122
 
117
123
  h2.band { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .09em; text-transform: uppercase; color: var(--muted); margin: 1.6rem 0 .7rem; border-bottom: 1px solid var(--line); padding-bottom: .35rem; }
118
124
 
119
- /* the three ways to grow the graph three cards in one row, stacking on a
120
- narrow screen. */
121
- .grow { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
122
- .grow .card { background: var(--bg); padding: .9rem 1rem 1.1rem; display: flex; flex-direction: column; gap: .55rem; min-width: 0; }
123
- .grow .card h3 { margin: 0; font-size: .95rem; }
125
+ /* the three ways to grow the graph, as one control-bar row of panels: a
126
+ hairline grid gap (painted by the shared background) separates them
127
+ instead of individual card borders, and each carries a top accent in
128
+ its own tone color. Stacks on a narrow screen. */
129
+ .grow { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
130
+ .grow .card { background: var(--card); border-top: 2px solid var(--line); padding: .9rem 1rem 1.1rem; display: flex; flex-direction: column; gap: .55rem; min-width: 0; }
131
+ .grow .card.card-research { border-top-color: var(--corpus); }
132
+ .grow .card.card-taught { border-top-color: var(--taught); }
133
+ .grow .card.card-ingest { border-top-color: var(--entail); }
134
+ .grow .card h3 { margin: 0; font-size: .95rem; font-weight: 600; }
124
135
  .grow .card .hint { font-size: .76rem; color: var(--muted); margin: 0; }
125
136
  .grow .card .tone { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: .35rem; vertical-align: baseline; }
126
137
  .tone-taught { background: var(--taught); } .tone-ingest { background: var(--entail); }
127
138
  .tone-research { background: var(--corpus); } .tone-seed { background: var(--muted); }
128
139
 
129
- .grow input[type="text"], .grow textarea { width: 100%; box-sizing: border-box; font-family: ${MONO_STACK}; font-size: .8rem; background: var(--card); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .4rem .55rem; }
140
+ .grow input[type="text"], .grow textarea { width: 100%; box-sizing: border-box; font-family: ${MONO_STACK}; font-size: .8rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .4rem .55rem; }
130
141
  .grow textarea { resize: vertical; min-height: 5.5rem; line-height: 1.5; }
131
142
  .grow input::placeholder, .grow textarea::placeholder { color: var(--muted); }
132
143
  .row { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
@@ -138,35 +149,45 @@ ${THEME_TOKENS_CSS}
138
149
  .optionToggle input { margin: 0; accent-color: var(--corpus); }
139
150
  .knobs { display: flex; gap: .9rem; align-items: center; flex-wrap: wrap; }
140
151
  .knob { display: inline-flex; align-items: center; gap: .4rem; font-family: ${MONO_STACK}; font-size: .68rem; color: var(--muted); }
141
- .knob input[type="number"] { width: 3.4rem; font-family: ${MONO_STACK}; font-size: .74rem; background: var(--card); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .25rem .35rem; text-align: right; }
152
+ .knob input[type="number"] { width: 3.4rem; font-family: ${MONO_STACK}; font-size: .74rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .25rem .35rem; text-align: right; }
142
153
 
143
154
  /* highlights + ask, two columns */
144
155
  .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 1.4rem; align-items: start; }
145
156
 
146
- .panel { border: 1px solid var(--line); border-radius: 10px; padding: .9rem 1rem 1rem; background: var(--card); }
147
- .panel h3 { margin: 0 0 .6rem; font-size: .9rem; }
157
+ .panel { border: 1px solid var(--line); border-radius: 6px; padding: .9rem 1rem 1rem; background: var(--card); }
158
+ .panel h3 { margin: 0 0 .6rem; font-size: .9rem; font-weight: 600; }
148
159
  .panel .empty { color: var(--muted); font-size: .8rem; margin: .3rem 0; }
149
160
 
150
- .fact { display: grid; grid-template-columns: auto 1fr auto 1fr; gap: .45rem; align-items: baseline; padding: .28rem 0; border-bottom: 1px solid var(--line); font-family: ${MONO_STACK}; font-size: .76rem; }
161
+ /* recently-learned: a dense feed of the facts just grounded. */
162
+ .fact { display: grid; grid-template-columns: auto 1fr auto 1fr; gap: .45rem; align-items: baseline; padding: .3rem .3rem; margin: 0 -.3rem; border-radius: 4px; border-bottom: 1px solid var(--line); font-family: ${MONO_STACK}; font-size: .76rem; }
151
163
  .fact:last-child { border-bottom: none; }
164
+ .fact:hover { background: var(--line); }
152
165
  .fact .dot { width: 7px; height: 7px; border-radius: 50%; align-self: center; }
153
166
  .fact .subj { color: var(--ink); word-break: break-word; }
154
167
  .fact .pred { color: var(--corpus); white-space: nowrap; }
155
168
  .fact .obj { color: var(--ink); word-break: break-word; }
156
169
 
157
- .chips { display: flex; flex-wrap: wrap; gap: .4rem; }
170
+ /* best-connected terms: a top-N ranked list (a mini bar chart), styled as
171
+ ledger.html's own predicate.top panel so the two pages read as siblings.
172
+ Each row keeps the "chip tapchip" contract the page's click handling and
173
+ its own e2e coverage rely on; the ranked-bar look rides inside it. */
174
+ .chips { display: flex; flex-direction: column; gap: .2rem; }
158
175
  .chip { font-family: ${MONO_STACK}; font-size: .74rem; border: 1px solid var(--line); border-radius: 99px; padding: .2rem .6rem; background: var(--bg); }
159
- .chip.tapchip { cursor: pointer; }
160
- .chip.tapchip:hover { border-color: var(--ink); }
161
- .chip .deg { color: var(--muted); margin-left: .35rem; }
176
+ .chip.tapchip { cursor: pointer; display: grid; grid-template-columns: 1.5rem minmax(0,1fr) 4.6rem 3.6rem; align-items: center; gap: .5rem; width: 100%; text-align: left; border: none; border-radius: 4px; padding: .3rem .4rem; background: none; }
177
+ .chip.tapchip:hover { background: var(--line); }
178
+ .chip .rank { color: var(--muted); font-variant-numeric: tabular-nums; }
179
+ .chip .term { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
180
+ .chip .track { background: var(--line); border-radius: 99px; height: 5px; overflow: hidden; }
181
+ .chip .fill { display: block; height: 100%; background: var(--ink); opacity: .6; border-radius: 99px; }
182
+ .chip .deg { color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
162
183
 
163
184
  /* the term digest: a narrative card, its sources, and the flat fact list one
164
185
  click away behind "show the facts". */
165
186
  .digestpanel .hint { font-size: .78rem; color: var(--muted); margin: 0 0 .55rem; }
166
187
  .digestout { min-height: 1.4rem; }
167
188
  .digestout .empty { color: var(--muted); font-size: .82rem; margin: .2rem 0; }
168
- .digestout .miss { color: var(--muted); font-size: .88rem; border: 1px dashed var(--line); border-radius: 8px; padding: .55rem .7rem; }
169
- .dgcard { border: 1px solid var(--line); border-radius: 8px; padding: .7rem .85rem .8rem; background: var(--bg); }
189
+ .digestout .miss { color: var(--muted); font-size: .88rem; border: 1px dashed var(--line); border-radius: 6px; padding: .55rem .7rem; }
190
+ .dgcard { border: 1px solid var(--line); border-radius: 6px; padding: .7rem .85rem .8rem; background: var(--bg); }
170
191
  .dgterm { font-family: ${MONO_STACK}; font-size: .72rem; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); margin-bottom: .4rem; }
171
192
  .dgcard p { margin: 0 0 .5rem; font-size: .95rem; }
172
193
  .dgcard p:last-of-type { margin-bottom: 0; }
@@ -178,15 +199,19 @@ ${THEME_TOKENS_CSS}
178
199
 
179
200
  /* ask, scoped by source */
180
201
  .askRow { display: flex; gap: .5rem; margin: .2rem 0 .7rem; }
181
- .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; }
182
- #answer { font-size: .9rem; white-space: pre-wrap; word-break: break-word; padding: .55rem .7rem; border-radius: 8px; background: var(--bg); border: 1px solid var(--line); min-height: 1.4rem; }
202
+ .askRow input { flex: 1; min-width: 0; font-family: inherit; font-size: .92rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); border-radius: 6px; padding: .45rem .7rem; }
203
+ #answer { font-size: .9rem; white-space: pre-wrap; word-break: break-word; padding: .55rem .7rem; border-radius: 6px; background: var(--bg); border: 1px solid var(--line); min-height: 1.4rem; }
183
204
  #answer.miss { color: var(--muted); border-style: dashed; }
184
205
  .sourcesHead { display: flex; align-items: baseline; justify-content: space-between; gap: .6rem; margin: .2rem 0 .5rem; }
185
206
  .sourcesHead .toggleAll { font-family: ${MONO_STACK}; font-size: .66rem; color: var(--muted); border: 1px solid var(--line); border-radius: 4px; padding: .12rem .5rem; background: var(--bg); }
186
207
 
208
+ /* scope by source: a filterable table — a header row names the columns,
209
+ then one row per source with its own checkbox, count, and history. */
210
+ .sourceTableHead { display: flex; justify-content: space-between; padding: 0 .3rem .3rem; font-family: ${MONO_STACK}; font-size: .6rem; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); border-bottom: 1px solid var(--line); margin-bottom: .2rem; }
187
211
  details.source { border-bottom: 1px solid var(--line); }
188
212
  details.source:last-of-type { border-bottom: none; }
189
- details.source > summary { list-style: none; display: flex; align-items: center; gap: .5rem; padding: .35rem 0; cursor: pointer; font-family: ${MONO_STACK}; font-size: .78rem; }
213
+ details.source > summary { list-style: none; display: flex; align-items: center; gap: .5rem; padding: .35rem .3rem; margin: 0 -.3rem; border-radius: 4px; cursor: pointer; font-family: ${MONO_STACK}; font-size: .78rem; }
214
+ details.source > summary:hover { background: var(--line); }
190
215
  details.source > summary::-webkit-details-marker { display: none; }
191
216
  details.source > summary .srcLabel { flex: 1; display: inline-flex; align-items: center; gap: .4rem; }
192
217
  details.source > summary input { margin: 0; accent-color: var(--corpus); }
@@ -201,23 +226,34 @@ ${THEME_TOKENS_CSS}
201
226
  @media (max-width: 820px) {
202
227
  .grow { grid-template-columns: 1fr; }
203
228
  .cols { grid-template-columns: 1fr; }
229
+ .chip.tapchip { grid-template-columns: 1.5rem minmax(0,1fr) 3.2rem; }
230
+ .chip.tapchip .track { display: none; }
231
+ .statuspanel .stat { min-width: 0; }
204
232
  }
205
233
  @media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; } }
234
+ @media (prefers-reduced-motion: no-preference) {
235
+ .fact, .chip.tapchip, details.source > summary { transition: background-color .1s ease; }
236
+ }
206
237
  </style>
207
238
  </head>
208
239
  <body>
209
240
  <div class="wrap">
210
241
  <header class="topbar">
211
242
  <div class="brand">
212
- <span class="eyebrow">the-mechanical-code-talker</span>
213
- <span class="subtitle">research &mdash; grow one graph three ways, watch what it learns, and ask it a question scoped to the sources you trust.</span>
243
+ <span class="eyebrow">the-mechanical-code-talker &middot; research</span>
244
+ <span class="subtitle">Grow one graph three ways. Watch what it learns, then ask a question scoped to the sources you trust.</span>
245
+ </div>
246
+ <div class="statuspanel" id="statusPanel" aria-live="polite">
247
+ <div class="stat stat-facts"><span class="stat-label">facts in the graph</span><span class="stat-value" id="statFacts">&mdash;</span></div>
248
+ <div class="stat"><span class="stat-label">seed</span><span class="stat-value" id="statSeed">&mdash;</span></div>
249
+ <div class="stat"><span class="stat-label">engine</span><span class="stat-value" id="statEngine">booting&hellip;</span></div>
214
250
  </div>
215
- <span class="status" id="status">loading the engine&hellip;</span>
216
251
  </header>
252
+ <div class="enginenote" id="engineNote" hidden></div>
217
253
 
218
254
  <h2 class="band">grow the graph</h2>
219
255
  <section class="grow">
220
- <div class="card">
256
+ <div class="card card-research">
221
257
  <h3><span class="tone tone-research"></span>research a term</h3>
222
258
  <p class="hint">Fetches the topic from Simple English Wikipedia and stores the facts it grounds, then queues the topics its lead section links to. Asking is the consent for these fetches.</p>
223
259
  <div class="row">
@@ -236,24 +272,24 @@ ${THEME_TOKENS_CSS}
236
272
  </div>
237
273
  <p class="note" id="researchNote"></p>
238
274
  </div>
239
- <div class="card">
275
+ <div class="card card-taught">
240
276
  <h3><span class="tone tone-taught"></span>teach by telling</h3>
241
- <p class="hint">Type a plain fact and it is stored if the recognizer can ground it &mdash; &ldquo;a beagle is a kind of dog&rdquo;, &ldquo;a dog has a tail&rdquo;. No guessing: an unrecognized sentence is skipped, honestly.</p>
277
+ <p class="hint">Type a plain fact and it's stored if the recognizer can ground it, for example &ldquo;a beagle is a kind of dog&rdquo; or &ldquo;a dog has a tail&rdquo;. An unrecognized sentence is skipped.</p>
242
278
  <div class="row">
243
279
  <input id="teachInput" type="text" autocomplete="off" spellcheck="false" placeholder="a beagle is a kind of dog" aria-label="A fact to teach">
244
280
  <button type="button" class="btn primary" id="teachGo" disabled>teach</button>
245
281
  </div>
246
282
  <p class="note" id="teachNote"></p>
247
283
  </div>
248
- <div class="card">
284
+ <div class="card card-ingest">
249
285
  <h3><span class="tone tone-ingest"></span>ingest documents</h3>
250
- <p class="hint">Paste or drop text; it keeps only the sentences it can ground as facts and skips the rest. The same recognizer the ingest page runs.</p>
286
+ <p class="hint">Paste or drop text. It keeps only the sentences it can ground as facts and skips the rest, using the same recognizer the ingest page runs.</p>
251
287
  <textarea id="ingestText" spellcheck="false" placeholder="Paste a paragraph or drop a .txt/.md file here." aria-label="Text to ingest"></textarea>
252
288
  <div class="row">
253
289
  <button type="button" class="btn primary" id="ingestGo" disabled>ingest</button>
254
290
  <button type="button" class="btn" id="ingestBrowse">browse&hellip;</button>
255
291
  <input type="file" id="ingestFile" accept=".txt,.md,text/plain,text/markdown" hidden>
256
- <label class="optionToggle" title="On a miss, also tries a copula or known relation verb flanked by two resolvable entities as a low-trust candidate, tagged optimistic-extract below every curated source.">
292
+ <label class="optionToggle" title="On a miss, also tries a copula or known relation verb flanked by two resolvable entities as a low-trust candidate, tagged optimistic-extract, below every curated source.">
257
293
  <input type="checkbox" id="fuzzyToggle"> fuzzy tier
258
294
  </label>
259
295
  </div>
@@ -275,7 +311,7 @@ ${THEME_TOKENS_CSS}
275
311
 
276
312
  <h2 class="band">read a term back</h2>
277
313
  <div class="panel digestpanel">
278
- <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>
314
+ <p class="hint">Pick a term the graph knows. It reads back a short narrative built from the facts it holds, with each source named. The narrative is deterministic and never a guess. The full fact list stays one click away behind &ldquo;show the facts&rdquo;.</p>
279
315
  <div class="askRow">
280
316
  <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>
281
317
  <button type="button" class="btn primary" id="digestGo" disabled>digest</button>
@@ -298,6 +334,7 @@ ${THEME_TOKENS_CSS}
298
334
  <h3 style="margin:0">scope by source</h3>
299
335
  <button type="button" class="toggleAll" id="toggleAll">all / none</button>
300
336
  </div>
337
+ <div class="sourceTableHead" aria-hidden="true"><span>source</span><span>facts</span></div>
301
338
  <div id="sourcesList"><p class="empty">loading sources&hellip;</p></div>
302
339
  </div>
303
340
  </div>
@@ -318,6 +355,8 @@ ${THEME_TOKENS_CSS}
318
355
  const fetchWithProgress = ${fetchWithProgress.toString()};
319
356
  const createTicker = ${createTicker.toString()};
320
357
  const prefersReducedMotion = ${prefersReducedMotion.toString()};
358
+ const loadWinkVendor = ${loadWinkVendor.toString()};
359
+ const cloneMemoryPayload = ${cloneMemoryPayload.toString()};
321
360
  const el = (id) => document.getElementById(id);
322
361
  // The digest sentence-structure bank, pre-parsed at build time — the browser
323
362
  // has no TOML parser, so the page carries the table the client-side digest
@@ -326,40 +365,40 @@ ${THEME_TOKENS_CSS}
326
365
 
327
366
  if ("serviceWorker" in navigator) navigator.serviceWorker.register("./tmct-sw.js").catch(() => {});
328
367
 
329
- const statusEl = el("status");
368
+ // The header's stat panel replaces one status line: "seed" carries the
369
+ // fact-count readout (boot progress, then the final count), "engine"
370
+ // carries the wink-nlp load result. engineNote is a separate banner for
371
+ // the one case that needs more room than a stat value: the whole engine
372
+ // bundle failing to load.
373
+ // The build's own content hash for the seed. It rides in the URL this page
374
+ // fetches the seed by, so the service worker's cache-first read can only
375
+ // ever return the copy this page asked for. Empty in a build with no seed.
376
+ const SEED_STAMP = ${JSON.stringify(seedStamp)};
377
+ const SEED_QUERY = SEED_STAMP ? "?b=" + SEED_STAMP : "";
378
+
379
+ const statFactsEl = el("statFacts");
380
+ const statSeedEl = el("statSeed");
381
+ const statEngineEl = el("statEngine");
382
+ const engineNoteEl = el("engineNote");
330
383
  let session = null;
331
384
  let researchQueue = null; // the engine's latest research snapshot, null when no run stands
332
385
  const checkedSources = new Set(); // source keys currently checked for the ask
333
386
 
334
387
  // ---- boot --------------------------------------------------------------
335
- const WINK_LOAD_TIMEOUT_MS = 8000;
336
388
  const progressParts = {};
337
389
  let progressActive = true;
338
390
  function noteProgress(key, loaded, total) {
339
391
  progressParts[key] = { loaded: loaded, total: total };
340
- if (progressActive) statusEl.textContent = loadProgressLine(Object.values(progressParts));
392
+ if (progressActive) statEngineEl.textContent = loadProgressLine(Object.values(progressParts));
341
393
  }
342
394
 
343
- async function tryLoadWink() {
344
- let settled = false;
345
- const timeout = new Promise((_, reject) => setTimeout(() => { if (!settled) reject(new Error("wink load stalled")); }, WINK_LOAD_TIMEOUT_MS));
346
- try {
347
- const mod = await Promise.race([import("./vendor/wink.js"), timeout]);
348
- settled = true;
349
- window.tmctResearch.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
350
- return "loaded";
351
- } catch (err) {
352
- settled = true;
353
- console.warn("tmct research: the wink vendor asset failed to load", err);
354
- return "unavailable";
355
- }
356
- }
395
+ const loadWink = loadWinkVendor({ register: (factory) => window.tmctResearch.registerWinkModel(factory) });
357
396
 
358
397
  let seedPayload = null;
359
398
  let seedFacts = 0;
360
399
  async function fetchSeed() {
361
400
  try {
362
- const blob = await fetchWithProgress("./chat-seed.json", (loaded, total) => noteProgress("seed", loaded, total));
401
+ const blob = await fetchWithProgress("./chat-seed.json" + SEED_QUERY, (loaded, total) => noteProgress("seed", loaded, total));
363
402
  seedPayload = JSON.parse(await blob.text());
364
403
  seedFacts = (seedPayload.individuals || []).filter((i) => i.class === "Fact").length;
365
404
  } catch (err) {
@@ -367,12 +406,8 @@ ${THEME_TOKENS_CSS}
367
406
  console.warn("tmct research: chat-seed.json unavailable — starting unseeded", err);
368
407
  }
369
408
  }
370
- const cloneSeed = () => {
371
- if (!seedPayload) return null;
372
- try { return structuredClone(seedPayload); } catch { return JSON.parse(JSON.stringify(seedPayload)); }
373
- };
374
409
  function newSession() {
375
- return window.tmctResearch.createResearchSession({ seedPayload: cloneSeed(), vocabSeeded: Boolean(seedPayload), digestStructures: DIGEST_STRUCTURES });
410
+ return window.tmctResearch.createResearchSession({ seedPayload: cloneMemoryPayload(seedPayload), vocabSeeded: Boolean(seedPayload), digestStructures: DIGEST_STRUCTURES });
376
411
  }
377
412
 
378
413
  // The curated reference pack provider, same fetch seam chat.html registers,
@@ -398,15 +433,19 @@ ${THEME_TOKENS_CSS}
398
433
 
399
434
  async function boot() {
400
435
  if (!window.tmctResearch) {
401
- statusEl.textContent = "the research engine didn't load — this page needs its build step (npm run demo:build)";
436
+ engineNoteEl.hidden = false;
437
+ engineNoteEl.textContent = "The research engine did not load. Run npm run demo:build, then reload this page.";
438
+ statSeedEl.textContent = "—";
439
+ statEngineEl.textContent = "unavailable";
402
440
  return;
403
441
  }
404
- const [winkStatus] = await Promise.all([tryLoadWink(), fetchSeed()]);
442
+ const [winkStatus] = await Promise.all([loadWink(), fetchSeed()]);
405
443
  progressActive = false;
406
444
  window.tmctResearch.registerReferencePackProvider(fetchPackProvider);
407
445
  session = newSession();
408
- const winkPart = winkStatus === "loaded" ? "wink-nlp: loaded" : "wink-nlp unavailable curated tiers only";
409
- statusEl.textContent = (seedPayload ? seedFacts + " seed facts" : "no seed") + " · " + winkPart + " — ready.";
446
+ const winkPart = winkStatus === "loaded" ? "wink-nlp loaded" : "wink-nlp unavailable, curated tiers only";
447
+ statSeedEl.textContent = seedPayload ? seedFacts.toLocaleString() + " facts" : "no seed";
448
+ statEngineEl.textContent = winkPart + " · ready";
410
449
  enableInputs();
411
450
  await refresh();
412
451
  }
@@ -428,6 +467,8 @@ ${THEME_TOKENS_CSS}
428
467
  let snap;
429
468
  try { snap = await window.tmctResearch.researchSnapshot(session.memoryDir, session.sessionIds); }
430
469
  catch (err) { console.warn("tmct research: snapshot failed", err); return; }
470
+ const total = (snap.sources || []).reduce((sum, source) => sum + (Number(source.count) || 0), 0);
471
+ statFactsEl.textContent = total.toLocaleString();
431
472
  renderRecent(snap.recent);
432
473
  renderHubs(snap.hubs);
433
474
  renderSources(snap.sources, snap.history);
@@ -459,6 +500,11 @@ ${THEME_TOKENS_CSS}
459
500
  for (const fact of recent) box.appendChild(factRow(fact));
460
501
  }
461
502
 
503
+ // A top-N ranked bar list, styled after ledger.html's own predicate.top
504
+ // panel so the two pages' widgets read as siblings. Each row keeps the
505
+ // page's "chip tapchip" contract (its click-to-digest behaviour and its
506
+ // own e2e coverage key off those two classes); the rank/track/fill bar
507
+ // rides inside it.
462
508
  function renderHubs(hubs) {
463
509
  const box = el("hubsList");
464
510
  box.textContent = "";
@@ -468,19 +514,24 @@ ${THEME_TOKENS_CSS}
468
514
  box.appendChild(p);
469
515
  return;
470
516
  }
471
- for (const hub of hubs) {
517
+ const max = hubs.reduce((m, h) => Math.max(m, h.degree), 0) || 1;
518
+ hubs.forEach((hub, i) => {
472
519
  const chip = document.createElement("button");
473
520
  chip.type = "button";
474
521
  chip.className = "chip tapchip";
475
522
  chip.setAttribute("aria-label", "read a digest of " + hub.term);
476
- chip.appendChild(document.createTextNode(hub.term));
523
+ const rank = document.createElement("span"); rank.className = "rank"; rank.textContent = String(i + 1).padStart(2, "0");
524
+ const term = document.createElement("span"); term.className = "term"; term.textContent = hub.term;
525
+ const track = document.createElement("span"); track.className = "track";
526
+ const fill = document.createElement("span"); fill.className = "fill"; fill.style.width = ((hub.degree / max) * 100).toFixed(1) + "%";
527
+ track.appendChild(fill);
477
528
  const deg = document.createElement("span");
478
529
  deg.className = "deg";
479
530
  deg.textContent = hub.degree + (hub.degree === 1 ? " fact" : " facts");
480
- chip.appendChild(deg);
531
+ chip.appendChild(rank); chip.appendChild(term); chip.appendChild(track); chip.appendChild(deg);
481
532
  chip.addEventListener("click", () => { el("digestInput").value = hub.term; digest(); });
482
533
  box.appendChild(chip);
483
- }
534
+ });
484
535
  }
485
536
 
486
537
  function renderSources(sources, history) {
@@ -499,7 +550,7 @@ ${THEME_TOKENS_CSS}
499
550
  }
500
551
  if (!sources || !sources.length) {
501
552
  const p = document.createElement("p"); p.className = "empty";
502
- p.textContent = "No facts yet grow the graph above, then scope your question here.";
553
+ p.textContent = "No facts yet. Grow the graph above, then scope your question here.";
503
554
  box.appendChild(p);
504
555
  return;
505
556
  }
@@ -569,7 +620,7 @@ ${THEME_TOKENS_CSS}
569
620
  const answerEl = el("answer");
570
621
  if (boxes.length && checked.length === 0) {
571
622
  answerEl.className = "miss";
572
- answerEl.textContent = "No source is checked check at least one on the right to ask against it.";
623
+ answerEl.textContent = "No source is checked. Check at least one on the right to ask against it.";
573
624
  return;
574
625
  }
575
626
  answerEl.className = "";
@@ -605,7 +656,7 @@ ${THEME_TOKENS_CSS}
605
656
  box.textContent = "";
606
657
  if (!view) {
607
658
  const p = document.createElement("p"); p.className = "miss";
608
- p.textContent = 'No grounded digest for "' + term + '" nothing is stored about it, or nothing the digest could compose. It abstains rather than guess.';
659
+ p.textContent = 'No grounded digest for "' + term + '". Nothing is stored about it, or nothing the digest could compose. It abstains rather than guess.';
609
660
  box.appendChild(p);
610
661
  return;
611
662
  }
@@ -659,7 +710,7 @@ ${THEME_TOKENS_CSS}
659
710
  note.textContent = "stored: " + (res.answer || "remembered.");
660
711
  el("teachInput").value = "";
661
712
  } else {
662
- note.textContent = res && res.answer ? res.answer : "not a recognized fact shape nothing stored.";
713
+ note.textContent = res && res.answer ? res.answer : "not a recognized fact shape, nothing stored.";
663
714
  }
664
715
  await refresh();
665
716
  }
@@ -711,9 +762,9 @@ ${THEME_TOKENS_CSS}
711
762
  const depth = researchQueue.maxDepth || 1;
712
763
  const budget = researchQueue.maxTopics || 0;
713
764
  const knobs = " (depth " + depth + (budget ? ", budget " + budget : "") + ")";
714
- const capped = researchQueue.nodeCapReached ? " node budget reached" : "";
765
+ const capped = researchQueue.nodeCapReached ? " · node budget reached" : "";
715
766
  if (researchQueue.complete) {
716
- note.textContent = 'research "' + researchQueue.topic + '" complete '
767
+ note.textContent = 'research "' + researchQueue.topic + '" complete · '
717
768
  + researchQueue.done.length + " topic" + (researchQueue.done.length === 1 ? "" : "s") + " grounded" + knobs + capped + ".";
718
769
  } else {
719
770
  note.textContent = 'research "' + researchQueue.topic + '": '
@@ -793,7 +844,8 @@ ${THEME_TOKENS_CSS}
793
844
 
794
845
  window.tmctResearchReady = boot().catch((err) => {
795
846
  console.error("tmct research failed to boot", err);
796
- statusEl.textContent = "the research page failed to start (" + (err && err.message ? err.message : err) + ")";
847
+ engineNoteEl.hidden = false;
848
+ engineNoteEl.textContent = "The research page failed to start (" + (err && err.message ? err.message : err) + ").";
797
849
  });
798
850
  })();
799
851
  </script>