@polycode-projects/the-mechanical-code-talker 3.3.0 → 4.0.1

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 (84) hide show
  1. package/README.md +176 -8
  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 +52 -0
  6. package/corpus/worlds/index.json.gz +0 -0
  7. package/corpus/worlds/manifest.json +53 -13
  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-garden.jsonl.gz +0 -0
  11. package/corpus/worlds/shards/mud-hollow.jsonl.gz +0 -0
  12. package/corpus/worlds/shards/mud-warren.jsonl.gz +0 -0
  13. package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
  14. package/corpus/worlds/src/greyvale-museum.jsonl +136 -0
  15. package/corpus/worlds/src/lantern-cottage.jsonl +60 -0
  16. package/corpus/worlds/src/mud-garden.jsonl +34 -1
  17. package/corpus/worlds/src/mud-hollow.jsonl +82 -0
  18. package/corpus/worlds/src/mud-warren.jsonl +124 -0
  19. package/corpus/worlds/src/spider-fly.jsonl +1 -1
  20. package/data/sprites/animal-icon.toml +11 -5
  21. package/data/sprites/book-icon.toml +9 -5
  22. package/data/sprites/cabinet-icon.toml +10 -5
  23. package/data/sprites/cellar-icon.toml +16 -6
  24. package/data/sprites/container-icon.toml +7 -3
  25. package/data/sprites/desk-icon.toml +8 -5
  26. package/data/sprites/dog-icon.toml +8 -5
  27. package/data/sprites/dog-with-colour-icon.toml +13 -12
  28. package/data/sprites/drawing-room-icon.toml +14 -5
  29. package/data/sprites/egg-icon.toml +6 -3
  30. package/data/sprites/fly-icon.toml +10 -5
  31. package/data/sprites/furniture-icon.toml +5 -3
  32. package/data/sprites/garden-icon.toml +10 -3
  33. package/data/sprites/key-icon.toml +3 -1
  34. package/data/sprites/kitchen-icon.toml +15 -6
  35. package/data/sprites/lamp-icon.toml +9 -4
  36. package/data/sprites/letter-icon.toml +7 -4
  37. package/data/sprites/library-icon.toml +14 -3
  38. package/data/sprites/pan-icon.toml +7 -3
  39. package/data/sprites/person-icon.toml +6 -2
  40. package/data/sprites/poodle-icon.toml +1 -1
  41. package/data/sprites/portable-icon.toml +6 -4
  42. package/data/sprites/portrait-icon.toml +7 -4
  43. package/data/sprites/room-icon.toml +11 -2
  44. package/data/sprites/spider-icon.toml +9 -3
  45. package/data/sprites/study-icon.toml +10 -2
  46. package/package.json +5 -4
  47. package/src/adapters/p2p/webrtc-transport.mjs +146 -0
  48. package/src/domain/game-config.mjs +26 -5
  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 +31 -0
  52. package/src/domain/memory/trust.mjs +6 -4
  53. package/src/domain/p2p/facts.mjs +81 -0
  54. package/src/domain/p2p/peer-id.mjs +32 -0
  55. package/src/domain/p2p/provenance-relabel.mjs +26 -0
  56. package/src/domain/p2p/sync-filter.mjs +31 -0
  57. package/src/domain/p2p/wire.mjs +123 -0
  58. package/src/domain/reference-pack.mjs +31 -7
  59. package/src/domain/spider-fly-world.mjs +1 -1
  60. package/src/domain/sprite-map.mjs +10 -2
  61. package/src/domain/sprite-templates.mjs +8 -6
  62. package/src/services/adventure-editor.mjs +10 -2
  63. package/src/services/adventure-viz.mjs +309 -73
  64. package/src/services/adventure.mjs +1034 -330
  65. package/src/services/chat-page-viz.mjs +1097 -16
  66. package/src/services/chat.mjs +99 -18
  67. package/src/services/code-explorer-viz.mjs +52 -14
  68. package/src/services/extract-facts.mjs +4 -7
  69. package/src/services/ingest-viz.mjs +54 -26
  70. package/src/services/ledger-viz.mjs +105 -56
  71. package/src/services/memory-panel-viz.mjs +24 -0
  72. package/src/services/mud-editor.mjs +313 -0
  73. package/src/services/mud-turn.mjs +264 -87
  74. package/src/services/mud-viz.mjs +2606 -384
  75. package/src/services/p2p-room.mjs +559 -0
  76. package/src/services/plan-viz.mjs +119 -61
  77. package/src/services/research-viz.mjs +151 -57
  78. package/src/services/spider-fly-turn.mjs +1 -1
  79. package/src/services/spider-fly-viz.mjs +67 -43
  80. package/src/services/sprite-catalog-viz.mjs +175 -51
  81. package/src/services/viz-theme.mjs +15 -0
  82. package/src/surfaces/web/memory-ask-browser.bundle.js +109 -109
  83. package/src/surfaces/web/mud-browser-entry.mjs +226 -18
  84. package/src/surfaces/web/p2p-browser-entry.mjs +39 -0
@@ -19,12 +19,32 @@
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";
22
+ import { THEME_TOKENS_CSS, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
23
23
  import { fetchWithProgress } from "./memory-panel-viz.mjs";
24
24
  import { createTicker, prefersReducedMotion } from "./viz-ticker.mjs";
25
25
 
26
26
  const DEFAULT_TITLE = "the-mechanical-code-talker — research";
27
27
 
28
+ // research.html reads as ledger.html's dashboard sibling (dense stat/control
29
+ // panels, monospace metrics), so it runs the same system sans + forced dark
30
+ // chrome — see ledger-viz.mjs's own DASH_SANS_STACK/DASH_DARK_CHROME_CSS for
31
+ // the fuller rationale. Kept as a separate copy rather than a shared import:
32
+ // each generated-page builder is a standalone, self-contained module (no
33
+ // cross-page runtime dependency), matching every sibling page builder here.
34
+ const DASH_SANS_STACK = `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`;
35
+ const DASH_DARK_CHROME_CSS = `
36
+ :root { color-scheme: dark; }
37
+ body {
38
+ --bg: #15181C; --ink: #E7E5DF; --muted: #9A9E95; --line: #2B3036; --card: #1C2126;
39
+ --taught: #5FBE8B; --corpus: #6C93BF; --entail: #D9A554; --alert: #D08070;
40
+ --taught-t1: rgba(95,190,139,.35); --taught-t2: rgba(95,190,139,.65); --taught-t3: rgba(95,190,139,1);
41
+ --corpus-t1: rgba(108,147,191,.35); --corpus-t2: rgba(108,147,191,.65); --corpus-t3: rgba(108,147,191,1);
42
+ --entail-t1: rgba(217,165,84,.35); --entail-t2: rgba(217,165,84,.65); --entail-t3: rgba(217,165,84,1);
43
+ --taught-soft: rgba(95,190,139,.14); --corpus-soft: rgba(108,147,191,.14);
44
+ --entail-soft: rgba(217,165,84,.16); --alert-soft: rgba(208,128,112,.16);
45
+ }
46
+ `;
47
+
28
48
  /** The human label + short colour key for one source snapshot entry
29
49
  * ({ key, band }). A seed band folds to a readable corpus name; the three
30
50
  * growth sources get their own words. Pure and `.toString()`-splice safe. */
@@ -79,7 +99,7 @@ export function loadProgressLine(parts) {
79
99
  * page can digest a term client-side over its grown store (no TOML parser in
80
100
  * the browser); an empty list leaves the digest panel degrading to an honest
81
101
  * "no digest available" the same way the node stub does. */
82
- export function renderResearchHtml({ title = DEFAULT_TITLE, digestStructures = [] } = {}) {
102
+ export function renderResearchHtml({ title = DEFAULT_TITLE, digestStructures = [], seedStamp = "" } = {}) {
83
103
  const digestStructuresJson = JSON.stringify(Array.isArray(digestStructures) ? digestStructures : []);
84
104
  return `<!doctype html>
85
105
  <html lang="en">
@@ -99,34 +119,52 @@ export function renderResearchHtml({ title = DEFAULT_TITLE, digestStructures = [
99
119
  -->
100
120
  <style>
101
121
  ${THEME_TOKENS_CSS}
122
+ ${DASH_DARK_CHROME_CSS}
102
123
  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; }
124
+ body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${DASH_SANS_STACK}; font-size: 15px; line-height: 1.5; }
104
125
  .mono { font-family: ${MONO_STACK}; }
105
126
  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; }
127
+ button:focus-visible, textarea:focus-visible, input:focus-visible, summary:focus-visible { outline: 2px solid var(--corpus); outline-offset: 2px; }
107
128
  a { color: var(--corpus); }
108
129
 
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); }
130
+ .wrap { max-width: 1200px; margin: 0 auto; padding: 1.1rem 1.1rem 3rem; }
131
+
132
+ /* A brand block on the left and a small stat-readout panel on the right —
133
+ the readout fills the row instead of one status line stranded beside a
134
+ wide gap, and it doubles as the boot-progress display. */
135
+ 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; }
136
+ .brand { display: flex; flex-direction: column; gap: .3rem; max-width: 640px; }
137
+ .eyebrow { font-family: ${MONO_STACK}; font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
138
+ .subtitle { font-size: .92rem; color: var(--ink); opacity: .82; max-width: 58ch; }
139
+ .statuspanel { display: flex; gap: 1.1rem; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: .5rem .9rem; }
140
+ .statuspanel .stat { display: flex; flex-direction: column; gap: .14rem; min-width: 7rem; }
141
+ .statuspanel .stat-label { font-family: ${MONO_STACK}; font-size: .6rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
142
+ .statuspanel .stat-value { font-family: ${MONO_STACK}; font-size: .82rem; font-variant-numeric: tabular-nums; color: var(--ink); }
143
+ /* the live total is what this page is actually growing, so it carries the
144
+ panel's emphasis — the seed count beside it never moves after boot. */
145
+ .statuspanel .stat-facts { padding-right: 1.1rem; border-right: 1px solid var(--line); }
146
+ .statuspanel .stat-facts .stat-value { font-size: 1.15rem; font-weight: 600; }
147
+ .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; }
148
+ .enginenote[hidden] { display: none; }
116
149
 
117
150
  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
151
 
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; }
152
+ /* the three ways to grow the graph, as one control-bar row of panels: a
153
+ hairline grid gap (painted by the shared background) separates them
154
+ instead of individual card borders, and each carries a top accent in
155
+ its own tone color. Stacks on a narrow screen. */
156
+ .grow { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
157
+ .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; }
158
+ .grow .card.card-research { border-top-color: var(--corpus); }
159
+ .grow .card.card-taught { border-top-color: var(--taught); }
160
+ .grow .card.card-ingest { border-top-color: var(--entail); }
161
+ .grow .card h3 { margin: 0; font-size: .95rem; font-weight: 600; }
124
162
  .grow .card .hint { font-size: .76rem; color: var(--muted); margin: 0; }
125
163
  .grow .card .tone { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: .35rem; vertical-align: baseline; }
126
164
  .tone-taught { background: var(--taught); } .tone-ingest { background: var(--entail); }
127
165
  .tone-research { background: var(--corpus); } .tone-seed { background: var(--muted); }
128
166
 
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; }
167
+ .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
168
  .grow textarea { resize: vertical; min-height: 5.5rem; line-height: 1.5; }
131
169
  .grow input::placeholder, .grow textarea::placeholder { color: var(--muted); }
132
170
  .row { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
@@ -138,35 +176,45 @@ ${THEME_TOKENS_CSS}
138
176
  .optionToggle input { margin: 0; accent-color: var(--corpus); }
139
177
  .knobs { display: flex; gap: .9rem; align-items: center; flex-wrap: wrap; }
140
178
  .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; }
179
+ .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
180
 
143
181
  /* highlights + ask, two columns */
144
182
  .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 1.4rem; align-items: start; }
145
183
 
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; }
184
+ .panel { border: 1px solid var(--line); border-radius: 6px; padding: .9rem 1rem 1rem; background: var(--card); }
185
+ .panel h3 { margin: 0 0 .6rem; font-size: .9rem; font-weight: 600; }
148
186
  .panel .empty { color: var(--muted); font-size: .8rem; margin: .3rem 0; }
149
187
 
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; }
188
+ /* recently-learned: a dense feed of the facts just grounded. */
189
+ .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
190
  .fact:last-child { border-bottom: none; }
191
+ .fact:hover { background: var(--line); }
152
192
  .fact .dot { width: 7px; height: 7px; border-radius: 50%; align-self: center; }
153
193
  .fact .subj { color: var(--ink); word-break: break-word; }
154
194
  .fact .pred { color: var(--corpus); white-space: nowrap; }
155
195
  .fact .obj { color: var(--ink); word-break: break-word; }
156
196
 
157
- .chips { display: flex; flex-wrap: wrap; gap: .4rem; }
197
+ /* best-connected terms: a top-N ranked list (a mini bar chart), styled as
198
+ ledger.html's own predicate.top panel so the two pages read as siblings.
199
+ Each row keeps the "chip tapchip" contract the page's click handling and
200
+ its own e2e coverage rely on; the ranked-bar look rides inside it. */
201
+ .chips { display: flex; flex-direction: column; gap: .2rem; }
158
202
  .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; }
203
+ .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; }
204
+ .chip.tapchip:hover { background: var(--line); }
205
+ .chip .rank { color: var(--muted); font-variant-numeric: tabular-nums; }
206
+ .chip .term { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
207
+ .chip .track { background: var(--line); border-radius: 99px; height: 5px; overflow: hidden; }
208
+ .chip .fill { display: block; height: 100%; background: var(--ink); opacity: .6; border-radius: 99px; }
209
+ .chip .deg { color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
162
210
 
163
211
  /* the term digest: a narrative card, its sources, and the flat fact list one
164
212
  click away behind "show the facts". */
165
213
  .digestpanel .hint { font-size: .78rem; color: var(--muted); margin: 0 0 .55rem; }
166
214
  .digestout { min-height: 1.4rem; }
167
215
  .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); }
216
+ .digestout .miss { color: var(--muted); font-size: .88rem; border: 1px dashed var(--line); border-radius: 6px; padding: .55rem .7rem; }
217
+ .dgcard { border: 1px solid var(--line); border-radius: 6px; padding: .7rem .85rem .8rem; background: var(--bg); }
170
218
  .dgterm { font-family: ${MONO_STACK}; font-size: .72rem; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); margin-bottom: .4rem; }
171
219
  .dgcard p { margin: 0 0 .5rem; font-size: .95rem; }
172
220
  .dgcard p:last-of-type { margin-bottom: 0; }
@@ -178,15 +226,19 @@ ${THEME_TOKENS_CSS}
178
226
 
179
227
  /* ask, scoped by source */
180
228
  .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; }
229
+ .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; }
230
+ #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
231
  #answer.miss { color: var(--muted); border-style: dashed; }
184
232
  .sourcesHead { display: flex; align-items: baseline; justify-content: space-between; gap: .6rem; margin: .2rem 0 .5rem; }
185
233
  .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
234
 
235
+ /* scope by source: a filterable table — a header row names the columns,
236
+ then one row per source with its own checkbox, count, and history. */
237
+ .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
238
  details.source { border-bottom: 1px solid var(--line); }
188
239
  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; }
240
+ 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; }
241
+ details.source > summary:hover { background: var(--line); }
190
242
  details.source > summary::-webkit-details-marker { display: none; }
191
243
  details.source > summary .srcLabel { flex: 1; display: inline-flex; align-items: center; gap: .4rem; }
192
244
  details.source > summary input { margin: 0; accent-color: var(--corpus); }
@@ -201,23 +253,33 @@ ${THEME_TOKENS_CSS}
201
253
  @media (max-width: 820px) {
202
254
  .grow { grid-template-columns: 1fr; }
203
255
  .cols { grid-template-columns: 1fr; }
256
+ .chip.tapchip { grid-template-columns: 1.5rem minmax(0,1fr) 3.2rem; }
257
+ .chip.tapchip .track { display: none; }
204
258
  }
205
259
  @media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; } }
260
+ @media (prefers-reduced-motion: no-preference) {
261
+ .fact, .chip.tapchip, details.source > summary { transition: background-color .1s ease; }
262
+ }
206
263
  </style>
207
264
  </head>
208
265
  <body>
209
266
  <div class="wrap">
210
267
  <header class="topbar">
211
268
  <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>
269
+ <span class="eyebrow">the-mechanical-code-talker &middot; research</span>
270
+ <span class="subtitle">Grow one graph three ways. Watch what it learns, then ask a question scoped to the sources you trust.</span>
271
+ </div>
272
+ <div class="statuspanel" id="statusPanel" aria-live="polite">
273
+ <div class="stat stat-facts"><span class="stat-label">facts in the graph</span><span class="stat-value" id="statFacts">&mdash;</span></div>
274
+ <div class="stat"><span class="stat-label">seed</span><span class="stat-value" id="statSeed">&mdash;</span></div>
275
+ <div class="stat"><span class="stat-label">engine</span><span class="stat-value" id="statEngine">booting&hellip;</span></div>
214
276
  </div>
215
- <span class="status" id="status">loading the engine&hellip;</span>
216
277
  </header>
278
+ <div class="enginenote" id="engineNote" hidden></div>
217
279
 
218
280
  <h2 class="band">grow the graph</h2>
219
281
  <section class="grow">
220
- <div class="card">
282
+ <div class="card card-research">
221
283
  <h3><span class="tone tone-research"></span>research a term</h3>
222
284
  <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
285
  <div class="row">
@@ -236,24 +298,24 @@ ${THEME_TOKENS_CSS}
236
298
  </div>
237
299
  <p class="note" id="researchNote"></p>
238
300
  </div>
239
- <div class="card">
301
+ <div class="card card-taught">
240
302
  <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>
303
+ <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
304
  <div class="row">
243
305
  <input id="teachInput" type="text" autocomplete="off" spellcheck="false" placeholder="a beagle is a kind of dog" aria-label="A fact to teach">
244
306
  <button type="button" class="btn primary" id="teachGo" disabled>teach</button>
245
307
  </div>
246
308
  <p class="note" id="teachNote"></p>
247
309
  </div>
248
- <div class="card">
310
+ <div class="card card-ingest">
249
311
  <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>
312
+ <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
313
  <textarea id="ingestText" spellcheck="false" placeholder="Paste a paragraph or drop a .txt/.md file here." aria-label="Text to ingest"></textarea>
252
314
  <div class="row">
253
315
  <button type="button" class="btn primary" id="ingestGo" disabled>ingest</button>
254
316
  <button type="button" class="btn" id="ingestBrowse">browse&hellip;</button>
255
317
  <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.">
318
+ <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
319
  <input type="checkbox" id="fuzzyToggle"> fuzzy tier
258
320
  </label>
259
321
  </div>
@@ -275,7 +337,7 @@ ${THEME_TOKENS_CSS}
275
337
 
276
338
  <h2 class="band">read a term back</h2>
277
339
  <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>
340
+ <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
341
  <div class="askRow">
280
342
  <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
343
  <button type="button" class="btn primary" id="digestGo" disabled>digest</button>
@@ -298,6 +360,7 @@ ${THEME_TOKENS_CSS}
298
360
  <h3 style="margin:0">scope by source</h3>
299
361
  <button type="button" class="toggleAll" id="toggleAll">all / none</button>
300
362
  </div>
363
+ <div class="sourceTableHead" aria-hidden="true"><span>source</span><span>facts</span></div>
301
364
  <div id="sourcesList"><p class="empty">loading sources&hellip;</p></div>
302
365
  </div>
303
366
  </div>
@@ -326,7 +389,21 @@ ${THEME_TOKENS_CSS}
326
389
 
327
390
  if ("serviceWorker" in navigator) navigator.serviceWorker.register("./tmct-sw.js").catch(() => {});
328
391
 
329
- const statusEl = el("status");
392
+ // The header's stat panel replaces one status line: "seed" carries the
393
+ // fact-count readout (boot progress, then the final count), "engine"
394
+ // carries the wink-nlp load result. engineNote is a separate banner for
395
+ // the one case that needs more room than a stat value: the whole engine
396
+ // bundle failing to load.
397
+ // The build's own content hash for the seed. It rides in the URL this page
398
+ // fetches the seed by, so the service worker's cache-first read can only
399
+ // ever return the copy this page asked for. Empty in a build with no seed.
400
+ const SEED_STAMP = ${JSON.stringify(seedStamp)};
401
+ const SEED_QUERY = SEED_STAMP ? "?b=" + SEED_STAMP : "";
402
+
403
+ const statFactsEl = el("statFacts");
404
+ const statSeedEl = el("statSeed");
405
+ const statEngineEl = el("statEngine");
406
+ const engineNoteEl = el("engineNote");
330
407
  let session = null;
331
408
  let researchQueue = null; // the engine's latest research snapshot, null when no run stands
332
409
  const checkedSources = new Set(); // source keys currently checked for the ask
@@ -337,7 +414,7 @@ ${THEME_TOKENS_CSS}
337
414
  let progressActive = true;
338
415
  function noteProgress(key, loaded, total) {
339
416
  progressParts[key] = { loaded: loaded, total: total };
340
- if (progressActive) statusEl.textContent = loadProgressLine(Object.values(progressParts));
417
+ if (progressActive) statEngineEl.textContent = loadProgressLine(Object.values(progressParts));
341
418
  }
342
419
 
343
420
  async function tryLoadWink() {
@@ -359,7 +436,7 @@ ${THEME_TOKENS_CSS}
359
436
  let seedFacts = 0;
360
437
  async function fetchSeed() {
361
438
  try {
362
- const blob = await fetchWithProgress("./chat-seed.json", (loaded, total) => noteProgress("seed", loaded, total));
439
+ const blob = await fetchWithProgress("./chat-seed.json" + SEED_QUERY, (loaded, total) => noteProgress("seed", loaded, total));
363
440
  seedPayload = JSON.parse(await blob.text());
364
441
  seedFacts = (seedPayload.individuals || []).filter((i) => i.class === "Fact").length;
365
442
  } catch (err) {
@@ -398,15 +475,19 @@ ${THEME_TOKENS_CSS}
398
475
 
399
476
  async function boot() {
400
477
  if (!window.tmctResearch) {
401
- statusEl.textContent = "the research engine didn't load — this page needs its build step (npm run demo:build)";
478
+ engineNoteEl.hidden = false;
479
+ engineNoteEl.textContent = "The research engine did not load. Run npm run demo:build, then reload this page.";
480
+ statSeedEl.textContent = "—";
481
+ statEngineEl.textContent = "unavailable";
402
482
  return;
403
483
  }
404
484
  const [winkStatus] = await Promise.all([tryLoadWink(), fetchSeed()]);
405
485
  progressActive = false;
406
486
  window.tmctResearch.registerReferencePackProvider(fetchPackProvider);
407
487
  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.";
488
+ const winkPart = winkStatus === "loaded" ? "wink-nlp loaded" : "wink-nlp unavailable, curated tiers only";
489
+ statSeedEl.textContent = seedPayload ? seedFacts.toLocaleString() + " facts" : "no seed";
490
+ statEngineEl.textContent = winkPart + " · ready";
410
491
  enableInputs();
411
492
  await refresh();
412
493
  }
@@ -428,6 +509,8 @@ ${THEME_TOKENS_CSS}
428
509
  let snap;
429
510
  try { snap = await window.tmctResearch.researchSnapshot(session.memoryDir, session.sessionIds); }
430
511
  catch (err) { console.warn("tmct research: snapshot failed", err); return; }
512
+ const total = (snap.sources || []).reduce((sum, source) => sum + (Number(source.count) || 0), 0);
513
+ statFactsEl.textContent = total.toLocaleString();
431
514
  renderRecent(snap.recent);
432
515
  renderHubs(snap.hubs);
433
516
  renderSources(snap.sources, snap.history);
@@ -459,6 +542,11 @@ ${THEME_TOKENS_CSS}
459
542
  for (const fact of recent) box.appendChild(factRow(fact));
460
543
  }
461
544
 
545
+ // A top-N ranked bar list, styled after ledger.html's own predicate.top
546
+ // panel so the two pages' widgets read as siblings. Each row keeps the
547
+ // page's "chip tapchip" contract (its click-to-digest behaviour and its
548
+ // own e2e coverage key off those two classes); the rank/track/fill bar
549
+ // rides inside it.
462
550
  function renderHubs(hubs) {
463
551
  const box = el("hubsList");
464
552
  box.textContent = "";
@@ -468,19 +556,24 @@ ${THEME_TOKENS_CSS}
468
556
  box.appendChild(p);
469
557
  return;
470
558
  }
471
- for (const hub of hubs) {
559
+ const max = hubs.reduce((m, h) => Math.max(m, h.degree), 0) || 1;
560
+ hubs.forEach((hub, i) => {
472
561
  const chip = document.createElement("button");
473
562
  chip.type = "button";
474
563
  chip.className = "chip tapchip";
475
564
  chip.setAttribute("aria-label", "read a digest of " + hub.term);
476
- chip.appendChild(document.createTextNode(hub.term));
565
+ const rank = document.createElement("span"); rank.className = "rank"; rank.textContent = String(i + 1).padStart(2, "0");
566
+ const term = document.createElement("span"); term.className = "term"; term.textContent = hub.term;
567
+ const track = document.createElement("span"); track.className = "track";
568
+ const fill = document.createElement("span"); fill.className = "fill"; fill.style.width = ((hub.degree / max) * 100).toFixed(1) + "%";
569
+ track.appendChild(fill);
477
570
  const deg = document.createElement("span");
478
571
  deg.className = "deg";
479
572
  deg.textContent = hub.degree + (hub.degree === 1 ? " fact" : " facts");
480
- chip.appendChild(deg);
573
+ chip.appendChild(rank); chip.appendChild(term); chip.appendChild(track); chip.appendChild(deg);
481
574
  chip.addEventListener("click", () => { el("digestInput").value = hub.term; digest(); });
482
575
  box.appendChild(chip);
483
- }
576
+ });
484
577
  }
485
578
 
486
579
  function renderSources(sources, history) {
@@ -499,7 +592,7 @@ ${THEME_TOKENS_CSS}
499
592
  }
500
593
  if (!sources || !sources.length) {
501
594
  const p = document.createElement("p"); p.className = "empty";
502
- p.textContent = "No facts yet grow the graph above, then scope your question here.";
595
+ p.textContent = "No facts yet. Grow the graph above, then scope your question here.";
503
596
  box.appendChild(p);
504
597
  return;
505
598
  }
@@ -569,7 +662,7 @@ ${THEME_TOKENS_CSS}
569
662
  const answerEl = el("answer");
570
663
  if (boxes.length && checked.length === 0) {
571
664
  answerEl.className = "miss";
572
- answerEl.textContent = "No source is checked check at least one on the right to ask against it.";
665
+ answerEl.textContent = "No source is checked. Check at least one on the right to ask against it.";
573
666
  return;
574
667
  }
575
668
  answerEl.className = "";
@@ -605,7 +698,7 @@ ${THEME_TOKENS_CSS}
605
698
  box.textContent = "";
606
699
  if (!view) {
607
700
  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.';
701
+ p.textContent = 'No grounded digest for "' + term + '". Nothing is stored about it, or nothing the digest could compose. It abstains rather than guess.';
609
702
  box.appendChild(p);
610
703
  return;
611
704
  }
@@ -659,7 +752,7 @@ ${THEME_TOKENS_CSS}
659
752
  note.textContent = "stored: " + (res.answer || "remembered.");
660
753
  el("teachInput").value = "";
661
754
  } else {
662
- note.textContent = res && res.answer ? res.answer : "not a recognized fact shape nothing stored.";
755
+ note.textContent = res && res.answer ? res.answer : "not a recognized fact shape, nothing stored.";
663
756
  }
664
757
  await refresh();
665
758
  }
@@ -711,9 +804,9 @@ ${THEME_TOKENS_CSS}
711
804
  const depth = researchQueue.maxDepth || 1;
712
805
  const budget = researchQueue.maxTopics || 0;
713
806
  const knobs = " (depth " + depth + (budget ? ", budget " + budget : "") + ")";
714
- const capped = researchQueue.nodeCapReached ? " node budget reached" : "";
807
+ const capped = researchQueue.nodeCapReached ? " · node budget reached" : "";
715
808
  if (researchQueue.complete) {
716
- note.textContent = 'research "' + researchQueue.topic + '" complete '
809
+ note.textContent = 'research "' + researchQueue.topic + '" complete · '
717
810
  + researchQueue.done.length + " topic" + (researchQueue.done.length === 1 ? "" : "s") + " grounded" + knobs + capped + ".";
718
811
  } else {
719
812
  note.textContent = 'research "' + researchQueue.topic + '": '
@@ -793,7 +886,8 @@ ${THEME_TOKENS_CSS}
793
886
 
794
887
  window.tmctResearchReady = boot().catch((err) => {
795
888
  console.error("tmct research failed to boot", err);
796
- statusEl.textContent = "the research page failed to start (" + (err && err.message ? err.message : err) + ")";
889
+ engineNoteEl.hidden = false;
890
+ engineNoteEl.textContent = "The research page failed to start (" + (err && err.message ? err.message : err) + ").";
797
891
  });
798
892
  })();
799
893
  </script>
@@ -73,7 +73,7 @@ const SPIDER_FLY_TOLD_RE = new RegExp(
73
73
  const SPIDER_FLY_SEE_RE = /^what (?:does|can) the (spider|fly)(?:-(\d+))?\s+see[.!?\s]*$/i;
74
74
 
75
75
  const WORLD_OPENING_FALLBACK =
76
- "a spider waits in its web; a fly drifts in from the edge of the board. Neither is yours to move — watch, or address one by name in chat.";
76
+ "a spider waits in its web; a fly drifts in from the edge of the board. Neither is yours to move. Watch, or address one by name in chat.";
77
77
 
78
78
  // ---- the opening turn: load the shipped board through the worlds pack -------
79
79