@polycode-projects/the-mechanical-code-talker 2.10.5 → 2.11.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 (40) hide show
  1. package/README.md +2 -2
  2. package/corpus/sprites/src/sprite-facts.jsonl +18 -0
  3. package/corpus/worlds/manifest.json +5 -5
  4. package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
  5. package/corpus/worlds/src/ashcombe-hall.jsonl +27 -0
  6. package/data/sprites/book-icon.toml +12 -0
  7. package/data/sprites/cellar-icon.toml +12 -0
  8. package/data/sprites/drawing-room-icon.toml +13 -0
  9. package/data/sprites/garden-icon.toml +12 -0
  10. package/data/sprites/kitchen-icon.toml +13 -0
  11. package/data/sprites/library-icon.toml +12 -0
  12. package/data/sprites/pan-icon.toml +11 -0
  13. package/data/sprites/study-icon.toml +12 -0
  14. package/package.json +5 -2
  15. package/src/adapters/corpus/wikipedia-live.mjs +182 -26
  16. package/src/adapters/corpus/worlds-pack.mjs +8 -2
  17. package/src/adapters/toml-config.mjs +6 -0
  18. package/src/domain/memory/trust.mjs +11 -0
  19. package/src/domain/worlds-pack.mjs +50 -0
  20. package/src/services/adventure-autoplay.mjs +5 -2
  21. package/src/services/adventure-viz.mjs +301 -33
  22. package/src/services/adventure.mjs +162 -14
  23. package/src/services/chat-page-viz.mjs +265 -189
  24. package/src/services/chat-session.mjs +15 -5
  25. package/src/services/chat.mjs +286 -43
  26. package/src/services/code-explorer-viz.mjs +183 -75
  27. package/src/services/extract-facts.mjs +118 -28
  28. package/src/services/ingest-viz.mjs +328 -79
  29. package/src/services/ledger-viz.mjs +99 -0
  30. package/src/services/memory-panel-viz.mjs +159 -0
  31. package/src/services/research.mjs +266 -0
  32. package/src/services/sentences.mjs +19 -0
  33. package/src/services/spider-fly-viz.mjs +21 -5
  34. package/src/surfaces/web/adventure-browser-entry.mjs +9 -5
  35. package/src/surfaces/web/chat-browser-entry.mjs +28 -11
  36. package/src/surfaces/web/code-explorer-browser-entry.mjs +27 -11
  37. package/src/surfaces/web/ingest-browser-entry.mjs +123 -41
  38. package/src/surfaces/web/ledger-browser-entry.mjs +10 -4
  39. package/src/surfaces/web/memory-ask-browser.bundle.js +112 -112
  40. package/src/surfaces/web/memory-stats.mjs +53 -0
package/README.md CHANGED
@@ -142,7 +142,7 @@ offline before it deploys. The example graph, the chat seed, the pages, and
142
142
  the engine copy land in `public/`. The in-page chat's query bundle is rebuilt
143
143
  from the same `src/` the CLI runs:
144
144
 
145
- ```bash e2e cwd=repo
145
+ ```bash e2e heavy cwd=repo
146
146
  npm run demo:build # public/: demo graph, memory, ledger page, engine copy
147
147
  npm run build:ask-bundle # the browser query bundle the in-page chat runs
148
148
  ```
@@ -869,7 +869,7 @@ the wordnet-xl corpus (~72,000 facts); `init:xxl` swaps wordnet-xl for the
869
869
  full WordNet slice plus namenet (~239,000 facts, the biggest committed
870
870
  vocabulary — expect its imports to take a minute). The xl chain, spelled out:
871
871
 
872
- ```bash e2e
872
+ ```bash e2e heavy
873
873
  npx tmct init --persona-size large # npm run init:xl runs this whole chain from a clone
874
874
  npx tmct import --corpus seon
875
875
  npx tmct import --corpus conceptnet
@@ -19,6 +19,7 @@
19
19
  {"subject":"birthday sprite","predicate":"rdf:type","object":"sprite class"}
20
20
  {"subject":"boat sprite","predicate":"rdf:type","object":"sprite class"}
21
21
  {"subject":"body of water sprite","predicate":"rdf:type","object":"sprite class"}
22
+ {"subject":"book sprite","predicate":"rdf:type","object":"sprite class"}
22
23
  {"subject":"boss sprite","predicate":"rdf:type","object":"sprite class"}
23
24
  {"subject":"boy sprite","predicate":"rdf:type","object":"sprite class"}
24
25
  {"subject":"bread sprite","predicate":"rdf:type","object":"sprite class"}
@@ -31,6 +32,7 @@
31
32
  {"subject":"car sprite","predicate":"rdf:type","object":"sprite class"}
32
33
  {"subject":"castle sprite","predicate":"rdf:type","object":"sprite class"}
33
34
  {"subject":"cat sprite","predicate":"rdf:type","object":"sprite class"}
35
+ {"subject":"cellar sprite","predicate":"rdf:type","object":"sprite class"}
34
36
  {"subject":"chair sprite","predicate":"rdf:type","object":"sprite class"}
35
37
  {"subject":"champion sprite","predicate":"rdf:type","object":"sprite class"}
36
38
  {"subject":"cheese sprite","predicate":"rdf:type","object":"sprite class"}
@@ -52,6 +54,7 @@
52
54
  {"subject":"dinner sprite","predicate":"rdf:type","object":"sprite class"}
53
55
  {"subject":"doctor sprite","predicate":"rdf:type","object":"sprite class"}
54
56
  {"subject":"dog sprite","predicate":"rdf:type","object":"sprite class"}
57
+ {"subject":"drawing-room sprite","predicate":"rdf:type","object":"sprite class"}
55
58
  {"subject":"dress sprite","predicate":"rdf:type","object":"sprite class"}
56
59
  {"subject":"drink sprite","predicate":"rdf:type","object":"sprite class"}
57
60
  {"subject":"driver sprite","predicate":"rdf:type","object":"sprite class"}
@@ -128,6 +131,7 @@
128
131
  {"subject":"office sprite","predicate":"rdf:type","object":"sprite class"}
129
132
  {"subject":"officer sprite","predicate":"rdf:type","object":"sprite class"}
130
133
  {"subject":"owl sprite","predicate":"rdf:type","object":"sprite class"}
134
+ {"subject":"pan sprite","predicate":"rdf:type","object":"sprite class"}
131
135
  {"subject":"parent sprite","predicate":"rdf:type","object":"sprite class"}
132
136
  {"subject":"park sprite","predicate":"rdf:type","object":"sprite class"}
133
137
  {"subject":"person sprite","predicate":"rdf:type","object":"sprite class"}
@@ -168,6 +172,7 @@
168
172
  {"subject":"stranger sprite","predicate":"rdf:type","object":"sprite class"}
169
173
  {"subject":"street sprite","predicate":"rdf:type","object":"sprite class"}
170
174
  {"subject":"student sprite","predicate":"rdf:type","object":"sprite class"}
175
+ {"subject":"study sprite","predicate":"rdf:type","object":"sprite class"}
171
176
  {"subject":"sugar sprite","predicate":"rdf:type","object":"sprite class"}
172
177
  {"subject":"summer sprite","predicate":"rdf:type","object":"sprite class"}
173
178
  {"subject":"sun sprite","predicate":"rdf:type","object":"sprite class"}
@@ -198,26 +203,34 @@
198
203
  {"subject":"writer sprite","predicate":"rdf:type","object":"sprite class"}
199
204
  {"subject":"adventurer sprite","predicate":"mgx:render-at","object":"icon tier"}
200
205
  {"subject":"animal sprite","predicate":"mgx:render-at","object":"icon tier"}
206
+ {"subject":"book sprite","predicate":"mgx:render-at","object":"icon tier"}
201
207
  {"subject":"butler sprite","predicate":"mgx:render-at","object":"icon tier"}
202
208
  {"subject":"cabinet sprite","predicate":"mgx:render-at","object":"icon tier"}
209
+ {"subject":"cellar sprite","predicate":"mgx:render-at","object":"icon tier"}
203
210
  {"subject":"container sprite","predicate":"mgx:render-at","object":"icon tier"}
204
211
  {"subject":"cook sprite","predicate":"mgx:render-at","object":"icon tier"}
205
212
  {"subject":"desk sprite","predicate":"mgx:render-at","object":"icon tier"}
206
213
  {"subject":"dog sprite","predicate":"mgx:render-at","object":"icon tier"}
214
+ {"subject":"drawing-room sprite","predicate":"mgx:render-at","object":"icon tier"}
207
215
  {"subject":"egg sprite","predicate":"mgx:render-at","object":"icon tier"}
208
216
  {"subject":"fly sprite","predicate":"mgx:render-at","object":"icon tier"}
209
217
  {"subject":"furniture sprite","predicate":"mgx:render-at","object":"icon tier"}
218
+ {"subject":"garden sprite","predicate":"mgx:render-at","object":"icon tier"}
210
219
  {"subject":"gardener sprite","predicate":"mgx:render-at","object":"icon tier"}
211
220
  {"subject":"housekeeper sprite","predicate":"mgx:render-at","object":"icon tier"}
212
221
  {"subject":"key sprite","predicate":"mgx:render-at","object":"icon tier"}
222
+ {"subject":"kitchen sprite","predicate":"mgx:render-at","object":"icon tier"}
213
223
  {"subject":"lamp sprite","predicate":"mgx:render-at","object":"icon tier"}
214
224
  {"subject":"letter sprite","predicate":"mgx:render-at","object":"icon tier"}
225
+ {"subject":"library sprite","predicate":"mgx:render-at","object":"icon tier"}
226
+ {"subject":"pan sprite","predicate":"mgx:render-at","object":"icon tier"}
215
227
  {"subject":"person sprite","predicate":"mgx:render-at","object":"icon tier"}
216
228
  {"subject":"poodle sprite","predicate":"mgx:render-at","object":"icon tier"}
217
229
  {"subject":"portable sprite","predicate":"mgx:render-at","object":"icon tier"}
218
230
  {"subject":"portrait sprite","predicate":"mgx:render-at","object":"icon tier"}
219
231
  {"subject":"room sprite","predicate":"mgx:render-at","object":"icon tier"}
220
232
  {"subject":"spider sprite","predicate":"mgx:render-at","object":"icon tier"}
233
+ {"subject":"study sprite","predicate":"mgx:render-at","object":"icon tier"}
221
234
  {"subject":"adult sprite","predicate":"mgx:render-at","object":"sprite tier"}
222
235
  {"subject":"adventurer sprite","predicate":"mgx:render-at","object":"sprite tier"}
223
236
  {"subject":"airport sprite","predicate":"mgx:render-at","object":"sprite tier"}
@@ -239,6 +252,7 @@
239
252
  {"subject":"birthday sprite","predicate":"mgx:render-at","object":"sprite tier"}
240
253
  {"subject":"boat sprite","predicate":"mgx:render-at","object":"sprite tier"}
241
254
  {"subject":"body of water sprite","predicate":"mgx:render-at","object":"sprite tier"}
255
+ {"subject":"book sprite","predicate":"mgx:render-at","object":"sprite tier"}
242
256
  {"subject":"boss sprite","predicate":"mgx:render-at","object":"sprite tier"}
243
257
  {"subject":"boy sprite","predicate":"mgx:render-at","object":"sprite tier"}
244
258
  {"subject":"bread sprite","predicate":"mgx:render-at","object":"sprite tier"}
@@ -251,6 +265,7 @@
251
265
  {"subject":"car sprite","predicate":"mgx:render-at","object":"sprite tier"}
252
266
  {"subject":"castle sprite","predicate":"mgx:render-at","object":"sprite tier"}
253
267
  {"subject":"cat sprite","predicate":"mgx:render-at","object":"sprite tier"}
268
+ {"subject":"cellar sprite","predicate":"mgx:render-at","object":"sprite tier"}
254
269
  {"subject":"chair sprite","predicate":"mgx:render-at","object":"sprite tier"}
255
270
  {"subject":"champion sprite","predicate":"mgx:render-at","object":"sprite tier"}
256
271
  {"subject":"cheese sprite","predicate":"mgx:render-at","object":"sprite tier"}
@@ -272,6 +287,7 @@
272
287
  {"subject":"dinner sprite","predicate":"mgx:render-at","object":"sprite tier"}
273
288
  {"subject":"doctor sprite","predicate":"mgx:render-at","object":"sprite tier"}
274
289
  {"subject":"dog sprite","predicate":"mgx:render-at","object":"sprite tier"}
290
+ {"subject":"drawing-room sprite","predicate":"mgx:render-at","object":"sprite tier"}
275
291
  {"subject":"dress sprite","predicate":"mgx:render-at","object":"sprite tier"}
276
292
  {"subject":"drink sprite","predicate":"mgx:render-at","object":"sprite tier"}
277
293
  {"subject":"driver sprite","predicate":"mgx:render-at","object":"sprite tier"}
@@ -348,6 +364,7 @@
348
364
  {"subject":"office sprite","predicate":"mgx:render-at","object":"sprite tier"}
349
365
  {"subject":"officer sprite","predicate":"mgx:render-at","object":"sprite tier"}
350
366
  {"subject":"owl sprite","predicate":"mgx:render-at","object":"sprite tier"}
367
+ {"subject":"pan sprite","predicate":"mgx:render-at","object":"sprite tier"}
351
368
  {"subject":"parent sprite","predicate":"mgx:render-at","object":"sprite tier"}
352
369
  {"subject":"park sprite","predicate":"mgx:render-at","object":"sprite tier"}
353
370
  {"subject":"person sprite","predicate":"mgx:render-at","object":"sprite tier"}
@@ -388,6 +405,7 @@
388
405
  {"subject":"stranger sprite","predicate":"mgx:render-at","object":"sprite tier"}
389
406
  {"subject":"street sprite","predicate":"mgx:render-at","object":"sprite tier"}
390
407
  {"subject":"student sprite","predicate":"mgx:render-at","object":"sprite tier"}
408
+ {"subject":"study sprite","predicate":"mgx:render-at","object":"sprite tier"}
391
409
  {"subject":"sugar sprite","predicate":"mgx:render-at","object":"sprite tier"}
392
410
  {"subject":"summer sprite","predicate":"mgx:render-at","object":"sprite tier"}
393
411
  {"subject":"sun sprite","predicate":"mgx:render-at","object":"sprite tier"}
@@ -3,7 +3,7 @@
3
3
  "generated": "by scripts/build-worlds-pack.mjs from corpus/worlds/src/",
4
4
  "counts": {
5
5
  "worlds": 2,
6
- "facts": 548,
6
+ "facts": 575,
7
7
  "rules": 19
8
8
  },
9
9
  "budgets": {
@@ -14,8 +14,8 @@
14
14
  "sources": [
15
15
  {
16
16
  "file": "src/ashcombe-hall.jsonl",
17
- "bytes": 8795,
18
- "sha256": "771ed4e7a5e9046235e8dc193b2696f83e6ef92a556ca1ca126dd070927912ce"
17
+ "bytes": 11754,
18
+ "sha256": "18e58700a59ae3876dd4f949ede7fa17e19f3a8c2dc8ae4644dbddf98c138524"
19
19
  },
20
20
  {
21
21
  "file": "src/spider-fly.jsonl",
@@ -31,8 +31,8 @@
31
31
  },
32
32
  {
33
33
  "file": "shards/ashcombe-hall.jsonl.gz",
34
- "bytes": 1020,
35
- "sha256": "1255ea52149c3941fafd4ee462239bae8e10175c8e7db2e54c2c2dc1a2e0d924"
34
+ "bytes": 1292,
35
+ "sha256": "a8e61a93ebf992a8b87221c5a9a9f53bf92b7798cee86fb9d6915a1844d960dd"
36
36
  },
37
37
  {
38
38
  "file": "shards/spider-fly.jsonl.gz",
@@ -54,6 +54,33 @@
54
54
  {"world":"ashcombe-hall","kind":"fact","subject":"ashcombe hall","predicate":"mgx:hasA","object":"6 rooms"}
55
55
  {"world":"ashcombe-hall","kind":"fact","subject":"player","predicate":"mgx:hasA","object":"satchel"}
56
56
  {"world":"ashcombe-hall","kind":"fact","subject":"satchel","predicate":"mgx:usedFor","object":"carrying what the player takes"}
57
+ {"world":"ashcombe-hall","kind":"fact","subject":"room","predicate":"rdfs:subClassOf","object":"space"}
58
+ {"world":"ashcombe-hall","kind":"fact","subject":"outdoor-space","predicate":"rdfs:subClassOf","object":"space"}
59
+ {"world":"ashcombe-hall","kind":"fact","subject":"underground-space","predicate":"rdfs:subClassOf","object":"space"}
60
+ {"world":"ashcombe-hall","kind":"fact","subject":"garden","predicate":"rdf:type","object":"outdoor-space"}
61
+ {"world":"ashcombe-hall","kind":"fact","subject":"cellar","predicate":"rdf:type","object":"underground-space"}
62
+ {"world":"ashcombe-hall","kind":"fact","subject":"portrait","predicate":"rdfs:subClassOf","object":"painting"}
63
+ {"world":"ashcombe-hall","kind":"fact","subject":"painting","predicate":"mgx:default-plane","object":"wall"}
64
+ {"world":"ashcombe-hall","kind":"fact","subject":"furniture","predicate":"mgx:default-plane","object":"floor"}
65
+ {"world":"ashcombe-hall","kind":"fact","subject":"portable","predicate":"mgx:default-plane","object":"floor"}
66
+ {"world":"ashcombe-hall","kind":"fact","subject":"lamp","predicate":"mgx:on-top-of","object":"desk"}
67
+ {"world":"ashcombe-hall","kind":"fact","subject":"book","predicate":"rdfs:subClassOf","object":"portable"}
68
+ {"world":"ashcombe-hall","kind":"fact","subject":"pan","predicate":"rdfs:subClassOf","object":"portable"}
69
+ {"world":"ashcombe-hall","kind":"fact","subject":"library","predicate":"mgx:default-contains","object":"book"}
70
+ {"world":"ashcombe-hall","kind":"fact","subject":"kitchen","predicate":"mgx:default-contains","object":"pan"}
71
+ {"world":"ashcombe-hall","kind":"fact","subject":"gardener","predicate":"mgx:knows-where","object":"letter"}
72
+ {"world":"ashcombe-hall","kind":"fact","subject":"housekeeper","predicate":"mgx:knows-where","object":"key"}
73
+ {"world":"ashcombe-hall","kind":"fact","subject":"butler","predicate":"mgx:knows-objective","object":"letter"}
74
+ {"world":"ashcombe-hall","kind":"fact","subject":"butler","predicate":"mgx:knows-where","object":"housekeeper"}
75
+ {"world":"ashcombe-hall","kind":"fact","subject":"butler","predicate":"mgx:knows-where","object":"cook"}
76
+ {"world":"ashcombe-hall","kind":"fact","subject":"butler","predicate":"mgx:knows-where","object":"gardener"}
77
+ {"world":"ashcombe-hall","kind":"fact","subject":"cook","predicate":"mgx:knows-where","object":"gardener"}
78
+ {"world":"ashcombe-hall","kind":"fact","subject":"gardener","predicate":"mgx:knows-about","object":"garden"}
79
+ {"world":"ashcombe-hall","kind":"fact","subject":"housekeeper","predicate":"mgx:knows-about","object":"study"}
80
+ {"world":"ashcombe-hall","kind":"fact","subject":"butler","predicate":"mgx:knows-about","object":"drawing-room"}
81
+ {"world":"ashcombe-hall","kind":"fact","subject":"cook","predicate":"mgx:knows-about","object":"kitchen"}
82
+ {"world":"ashcombe-hall","kind":"fact","subject":"garden","predicate":"mgx:hasA","object":"old apple tree"}
83
+ {"world":"ashcombe-hall","kind":"fact","subject":"kitchen","predicate":"mgx:usedFor","object":"cooking the hall's meals"}
57
84
  {"world":"ashcombe-hall","kind":"rule","name":"go","ruleKind":"action-signature","slots":{"subjectClass":"adventurer","targetClass":"room"}}
58
85
  {"world":"ashcombe-hall","kind":"rule","name":"go","ruleKind":"action-signature","slots":{"subjectClass":"person","targetClass":"room"}}
59
86
  {"world":"ashcombe-hall","kind":"rule","name":"go","ruleKind":"action-effect","slots":{"predicate":"currently-in","subjectRole":"subject","objectRole":"target"}}
@@ -0,0 +1,12 @@
1
+ # book — the library's default portable: a closed hardback seen spine-on, the
2
+ # cover boards and a page block, so a taken book reads as its own thing rather
3
+ # than the generic portable box.
4
+ classes = ["book"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <path d="M6 3 H18 A1 1 0 0 1 19 4 V20 A1 1 0 0 1 18 21 H6 Z" fill="currentColor"/>
8
+ <rect x="6" y="3" width="1.8" height="18" fill="currentColor" opacity="0.4"/>
9
+ <path d="M9.4 3 V21" stroke="currentColor" stroke-width="0.5" opacity="0.35"/>
10
+ <path d="M11 7.5 H17 M11 10 H17 M11 12.5 H15" stroke="#000000" stroke-width="0.5" opacity="0.3"/>
11
+ </svg>
12
+ """
@@ -0,0 +1,12 @@
1
+ # cellar — an underground space: a room box with a stair stepping down and
2
+ # stone coursing, so the room-kind icon reads as below ground rather than a
3
+ # plain room, matching the underground border.
4
+ classes = ["cellar"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <rect x="3" y="3" width="18" height="18" rx="1" fill="currentColor" opacity="0.14"/>
8
+ <rect x="3" y="3" width="18" height="18" rx="1" fill="none" stroke="currentColor" stroke-width="1.6"/>
9
+ <path d="M6 8 H10 V11 H14 V14 H18 V18 H6 Z" fill="currentColor" opacity="0.55"/>
10
+ <path d="M3 12 H21 M3 16.5 H21" stroke="currentColor" stroke-width="0.5" opacity="0.4"/>
11
+ </svg>
12
+ """
@@ -0,0 +1,13 @@
1
+ # drawing-room — a room floor plan marked by a framed picture on the wall (the
2
+ # portrait hangs here), so the room-kind icon reads as the drawing room rather
3
+ # than a plain room.
4
+ classes = ["drawing-room"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <rect x="3" y="3" width="18" height="18" rx="1" fill="currentColor" opacity="0.14"/>
8
+ <rect x="3" y="3" width="18" height="18" rx="1" fill="none" stroke="currentColor" stroke-width="1.6"/>
9
+ <rect x="8.5" y="7" width="7" height="6" rx="0.4" fill="none" stroke="currentColor" stroke-width="1.3"/>
10
+ <circle cx="12" cy="9.4" r="1.1" fill="currentColor" opacity="0.7"/>
11
+ <path d="M9.6 12.4 C9.6 10.9 10.6 10 12 10 C13.4 10 14.4 10.9 14.4 12.4 Z" fill="currentColor" opacity="0.7"/>
12
+ </svg>
13
+ """
@@ -0,0 +1,12 @@
1
+ # garden — an outdoor space, so no room box: a tree over a ground line reads
2
+ # as open air rather than an interior, matching the outdoor room-kind border.
3
+ classes = ["garden"]
4
+ svg = """
5
+ <svg viewBox="0 0 24 24" aria-hidden="true">
6
+ <path d="M3 20 H21" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
7
+ <path d="M12 20 V12" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
8
+ <circle cx="12" cy="8.5" r="5" fill="currentColor" opacity="0.7"/>
9
+ <circle cx="7.5" cy="17" r="1" fill="currentColor" opacity="0.5"/>
10
+ <circle cx="16.5" cy="17" r="1" fill="currentColor" opacity="0.5"/>
11
+ </svg>
12
+ """
@@ -0,0 +1,13 @@
1
+ # kitchen — a room floor plan marked by a stove with two burners, the same
2
+ # distinguishing cue the large tier uses, so the room-kind icon reads as a
3
+ # kitchen rather than a plain room.
4
+ classes = ["kitchen"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <rect x="3" y="3" width="18" height="18" rx="1" fill="currentColor" opacity="0.14"/>
8
+ <rect x="3" y="3" width="18" height="18" rx="1" fill="none" stroke="currentColor" stroke-width="1.6"/>
9
+ <rect x="8" y="10" width="8" height="7" rx="0.6" fill="none" stroke="currentColor" stroke-width="1.2"/>
10
+ <circle cx="10.5" cy="12.6" r="1" fill="currentColor" opacity="0.7"/>
11
+ <circle cx="13.5" cy="12.6" r="1" fill="currentColor" opacity="0.7"/>
12
+ </svg>
13
+ """
@@ -0,0 +1,12 @@
1
+ # library — a room floor plan marked by an open book, so the room-kind icon
2
+ # reads as a library rather than a plain room: the washed square is room.toml's
3
+ # own shape, the two-page V is the same open-book cue the large tier uses.
4
+ classes = ["library"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <rect x="3" y="3" width="18" height="18" rx="1" fill="currentColor" opacity="0.14"/>
8
+ <rect x="3" y="3" width="18" height="18" rx="1" fill="none" stroke="currentColor" stroke-width="1.6"/>
9
+ <path d="M12 9 C10.4 7.9 8.2 7.6 6.4 8.1 V15.6 C8.2 15.1 10.4 15.4 12 16.5 C13.6 15.4 15.8 15.1 17.6 15.6 V8.1 C15.8 7.6 13.6 7.9 12 9 Z" fill="currentColor" opacity="0.75"/>
10
+ <path d="M12 9 V16.5" stroke="currentColor" stroke-width="0.5" opacity="0.5"/>
11
+ </svg>
12
+ """
@@ -0,0 +1,11 @@
1
+ # pan — the kitchen's default portable: a frying pan seen from above with a
2
+ # long handle, so a taken pan reads as its own thing rather than the generic
3
+ # portable box.
4
+ classes = ["pan"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <circle cx="10" cy="14" r="6" fill="currentColor"/>
8
+ <circle cx="10" cy="14" r="3.6" fill="currentColor" opacity="0.35"/>
9
+ <rect x="15" y="13" width="7" height="2" rx="1" fill="currentColor"/>
10
+ </svg>
11
+ """
@@ -0,0 +1,12 @@
1
+ # study — a room floor plan marked by a writing desk against one wall, so the
2
+ # room-kind icon reads as the study you start in rather than a plain room.
3
+ classes = ["study"]
4
+ svg = """
5
+ <svg viewBox="0 0 24 24" aria-hidden="true">
6
+ <rect x="3" y="3" width="18" height="18" rx="1" fill="currentColor" opacity="0.14"/>
7
+ <rect x="3" y="3" width="18" height="18" rx="1" fill="none" stroke="currentColor" stroke-width="1.6"/>
8
+ <rect x="7" y="11" width="10" height="2.4" fill="currentColor" opacity="0.7"/>
9
+ <path d="M8 13.4 V17 M16 13.4 V17" stroke="currentColor" stroke-width="1.2"/>
10
+ <rect x="9.6" y="7.6" width="4.8" height="3" fill="none" stroke="currentColor" stroke-width="1"/>
11
+ </svg>
12
+ """
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "2.10.5",
3
+ "version": "2.11.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
@@ -86,8 +86,11 @@
86
86
  "test": "node --disable-warning=ExperimentalWarning --test --test-concurrency=8 \"test/**/*.test.mjs\"",
87
87
  "test:smoke": "node --disable-warning=ExperimentalWarning --test test/smoke/*.test.mjs",
88
88
  "test:fast": "node --disable-warning=ExperimentalWarning --test --test-concurrency=8 test/smoke/*.test.mjs test/fast/*.test.mjs test/tools/ask.test.mjs test/tools/server.test.mjs test/estate/import-layers.test.mjs",
89
- "test:e2e": "node --disable-warning=ExperimentalWarning --test \"e2e/**/*.test.mjs\"",
89
+ "test:e2e": "node --disable-warning=ExperimentalWarning --test \"e2e/*.test.mjs\"",
90
+ "test:e2e:heavy": "node --disable-warning=ExperimentalWarning --test \"e2e/heavy/*.test.mjs\"",
91
+ "check:readme": "TMCT_CHECK_README=1 node --disable-warning=ExperimentalWarning --test e2e/readme-examples.test.mjs",
90
92
  "e2e:browsers": "playwright install chromium",
93
+ "gen:screenshots": "node scripts/gen-screenshots.mjs",
91
94
  "check:links": "node scripts/check-links.mjs",
92
95
  "check:pii": "node scripts/pii-lint.mjs",
93
96
  "check:pack": "node scripts/check-pack-manifest.mjs",
@@ -13,24 +13,55 @@
13
13
  // The provider seam mirrors reference-pack.mjs's: registerLiveReferenceProvider
14
14
  // swaps the whole lookup behind one async { lookup(normTerm) } contract (tests
15
15
  // and the demo page stub it); null restores the default provider below.
16
+ //
17
+ // The research lane rides the same provider factory against
18
+ // simple.wikipedia.org (registerResearchProvider/getResearchProvider below),
19
+ // adding two fan-out reads: pageByTitle (an exact linked title costs ONE
20
+ // round trip, no opensearch) and linkedTitles (the lead section's
21
+ // namespace-0 links, document-ordered). Requests identify themselves per
22
+ // Wikimedia's robot etiquette (WIKIMEDIA_USER_AGENT) and carry maxlag so an
23
+ // overloaded replica set is backed off from, not hammered.
16
24
 
17
25
  import { normFactTerm } from "../../domain/hash.mjs";
18
26
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
19
27
  import { isReferenceArticleRow, sentencesUpTo, isaOf, SUMMARY_CHAR_CAP } from "../../domain/reference-pack.mjs";
20
28
 
21
29
  export const WIKIPEDIA_LIVE_ORIGIN = "https://en.wikipedia.org";
30
+ export const SIMPLE_WIKIPEDIA_ORIGIN = "https://simple.wikipedia.org";
31
+
32
+ /** The identification string Wikimedia's robot policy asks API clients to
33
+ * carry, pointing at this project's public site as the contact. Browsers
34
+ * refuse to override the User-Agent request header, so the API-recognised
35
+ * Api-User-Agent header carries the same string there; under Node both are
36
+ * sent. */
37
+ export const WIKIMEDIA_USER_AGENT = "the-mechanical-code-talker (+https://polycode-projects.gitlab.io/the-mechanical-code-talker/)";
22
38
 
23
39
  const DEFAULT_TIMEOUT_MS = 4000;
24
40
  const DEFAULT_MIN_INTERVAL_MS = 2000;
25
41
  const RETRY_AFTER_FLOOR_MS = 5000;
42
+ // Action-API requests carry maxlag so an overloaded replica set answers with
43
+ // an error we back off from instead of adding to its load (Wikimedia's own
44
+ // recommended default for non-interactive clients).
45
+ const MAXLAG_SECONDS = 5;
26
46
 
27
47
  /**
28
- * A live-lookup provider: { lookup(normTerm) -> article row | null }.
48
+ * A live-lookup provider: { lookup(normTerm) -> article row | null }, plus
49
+ * the research fan-out surface: pageByTitle(title) fetches an exact title's
50
+ * summary in ONE round trip (no opensearch — a linked title is already
51
+ * exact), and linkedTitles(title) lists the namespace-0 articles the page's
52
+ * LEAD section links to, in document order.
29
53
  *
30
54
  * `fetchImpl` defaults to the global fetch; `origin` to en.wikipedia.org;
31
55
  * `lexicon` (optional) feeds the isa extraction. The row shape is the shipped
32
56
  * pack's own ({ term, title, text, summary, url, revid, isa? }), validated by
33
57
  * isReferenceArticleRow before it is ever returned.
58
+ *
59
+ * Throttle posture: every public method takes one "slot" gated by the
60
+ * minimum interval, the single-flight guard and any open cool-off. By
61
+ * default a caller that asks too soon gets null (the chat's clean-miss hook
62
+ * must never block a turn); with `waitForSlot: true` the method WAITS for
63
+ * the slot instead — the research queue's posture, where a false miss would
64
+ * be dishonest and the caller is already paced turn-by-turn.
34
65
  */
35
66
  export function createWikipediaLiveProvider({
36
67
  fetchImpl,
@@ -38,6 +69,8 @@ export function createWikipediaLiveProvider({
38
69
  timeoutMs = DEFAULT_TIMEOUT_MS,
39
70
  minIntervalMs = DEFAULT_MIN_INTERVAL_MS,
40
71
  lexicon = null,
72
+ userAgent = WIKIMEDIA_USER_AGENT,
73
+ waitForSlot = false,
41
74
  } = {}) {
42
75
  const doFetch = fetchImpl ?? ((...args) => globalThis.fetch(...args));
43
76
  const cache = new Map(); // key -> row | null (hits AND settled misses)
@@ -45,18 +78,43 @@ export function createWikipediaLiveProvider({
45
78
  let coolOffUntil = 0;
46
79
  let inFlight = false;
47
80
 
81
+ const identifyingHeaders = () => {
82
+ if (!userAgent) return null;
83
+ const headers = { "Api-User-Agent": userAgent };
84
+ // A browser strips User-Agent as a forbidden header; only set it where
85
+ // no DOM says we are one (Node ships a global `navigator` these days, so
86
+ // `document` is the discriminating global).
87
+ if (typeof document === "undefined") headers["User-Agent"] = userAgent;
88
+ return headers;
89
+ };
90
+
91
+ function openCoolOff(retryAfterSeconds) {
92
+ const retryAfterMs = Number(retryAfterSeconds) * 1000;
93
+ coolOffUntil = Date.now() + Math.max(retryAfterMs || 0, RETRY_AFTER_FLOOR_MS);
94
+ }
95
+
48
96
  async function fetchJson(url) {
49
97
  const controller = typeof AbortController === "function" ? new AbortController() : null;
50
98
  const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
51
99
  try {
52
- const res = await doFetch(url, controller ? { signal: controller.signal } : {});
100
+ const opts = {};
101
+ if (controller) opts.signal = controller.signal;
102
+ const headers = identifyingHeaders();
103
+ if (headers) opts.headers = headers;
104
+ const res = await doFetch(url, opts);
53
105
  if (res.status === 429) {
54
- const retryAfterMs = Number(res.headers?.get?.("retry-after")) * 1000;
55
- coolOffUntil = Date.now() + Math.max(retryAfterMs || 0, RETRY_AFTER_FLOOR_MS);
106
+ openCoolOff(res.headers?.get?.("retry-after"));
56
107
  return null;
57
108
  }
58
109
  if (!res.ok) return null;
59
- return await res.json();
110
+ const body = await res.json();
111
+ // A maxlag rejection arrives as HTTP 200 with an error body (and a
112
+ // Retry-After header) — back off exactly as a 429 asks.
113
+ if (body?.error?.code === "maxlag") {
114
+ openCoolOff(res.headers?.get?.("retry-after"));
115
+ return null;
116
+ }
117
+ return body;
60
118
  } catch {
61
119
  return null;
62
120
  } finally {
@@ -64,6 +122,26 @@ export function createWikipediaLiveProvider({
64
122
  }
65
123
  }
66
124
 
125
+ /** When the next network slot opens: past the cool-off, past the minimum
126
+ * interval since the last taken slot. */
127
+ const slotOpensAt = () => Math.max(coolOffUntil, lastLookupAt + minIntervalMs);
128
+
129
+ /** Take the one network slot, or report it unavailable. Default posture
130
+ * returns false immediately (the clean-miss hook's "null, never block");
131
+ * `waitForSlot` sleeps until the slot opens instead. */
132
+ async function takeSlot() {
133
+ for (;;) {
134
+ const now = Date.now();
135
+ if (!inFlight && now >= slotOpensAt()) {
136
+ lastLookupAt = now;
137
+ inFlight = true;
138
+ return true;
139
+ }
140
+ if (!waitForSlot) return false;
141
+ await new Promise((resolve) => setTimeout(resolve, Math.max(slotOpensAt() - now, 25)));
142
+ }
143
+ }
144
+
67
145
  /** The opensearch title whose normFactTerm fold equals or extends the key —
68
146
  * the topic-drift guard: "quasar" may resolve to "Quasar" or "Quasars",
69
147
  * never to a first suggestion about something else. */
@@ -76,12 +154,7 @@ export function createWikipediaLiveProvider({
76
154
  return null;
77
155
  }
78
156
 
79
- async function roundTrips(key) {
80
- const search = await fetchJson(
81
- `${origin}/w/api.php?action=opensearch&format=json&origin=*&search=${encodeURIComponent(key)}&limit=3`,
82
- );
83
- const title = search ? matchingTitle(key, search) : null;
84
- if (!title) return null;
157
+ async function summaryRow(key, title) {
85
158
  const summary = await fetchJson(
86
159
  `${origin}/api/rest_v1/page/summary/${encodeURIComponent(title.replace(/ /g, "_"))}`,
87
160
  );
@@ -103,25 +176,77 @@ export function createWikipediaLiveProvider({
103
176
  return isReferenceArticleRow(row) ? row : null;
104
177
  }
105
178
 
179
+ async function roundTrips(key) {
180
+ const search = await fetchJson(
181
+ `${origin}/w/api.php?action=opensearch&format=json&origin=*&maxlag=${MAXLAG_SECONDS}&search=${encodeURIComponent(key)}&limit=3`,
182
+ );
183
+ const title = search ? matchingTitle(key, search) : null;
184
+ if (!title) return null;
185
+ return summaryRow(key, title);
186
+ }
187
+
188
+ /** One slot-gated, cached operation: cache first (a settled hit or miss is
189
+ * never refetched), then the slot, then `work()`, with every failure
190
+ * cached as null so it never costs a second round trip. */
191
+ async function cachedFetch(cacheKey, work) {
192
+ if (cache.has(cacheKey)) return cache.get(cacheKey);
193
+ if (!(await takeSlot())) return null;
194
+ let value = null;
195
+ try {
196
+ value = await work();
197
+ } catch {
198
+ value = null;
199
+ } finally {
200
+ inFlight = false;
201
+ }
202
+ cache.set(cacheKey, value);
203
+ return value;
204
+ }
205
+
106
206
  return {
107
207
  async lookup(normTerm) {
108
208
  const key = String(normTerm ?? "");
109
209
  if (!key) return null;
110
- if (cache.has(key)) return cache.get(key);
111
- const now = Date.now();
112
- if (inFlight || now < coolOffUntil || now - lastLookupAt < minIntervalMs) return null;
113
- lastLookupAt = now;
114
- inFlight = true;
115
- let row = null;
116
- try {
117
- row = await roundTrips(key);
118
- } catch {
119
- row = null;
120
- } finally {
121
- inFlight = false;
122
- }
123
- cache.set(key, row);
124
- return row;
210
+ return cachedFetch(key, () => roundTrips(key));
211
+ },
212
+
213
+ /** The summary for an EXACT title — one round trip, no opensearch. The
214
+ * research queue's depth-1 fetch: a linked title came from the wiki
215
+ * itself, so the fuzzy title match would only waste a request. */
216
+ async pageByTitle(title) {
217
+ const t = String(title ?? "").trim();
218
+ if (!t) return null;
219
+ return cachedFetch(`title\0${normFactTerm(t)}`, () => summaryRow(normFactTerm(t), t));
220
+ },
221
+
222
+ /** The namespace-0 articles the page's LEAD section links to, in document
223
+ * order, capped at `limit`. One action-API round trip
224
+ * (action=parse&prop=links&section=0): the lead is the smallest payload
225
+ * that still orders links by how the article introduces its topic —
226
+ * a full-page prop=links listing is alphabetical, which would make the
227
+ * fan-out pick by spelling instead of relevance. Null on any failure. */
228
+ async linkedTitles(title, { limit = 25 } = {}) {
229
+ const t = String(title ?? "").trim();
230
+ if (!t) return null;
231
+ const listed = await cachedFetch(`links\0${normFactTerm(t)}`, async () => {
232
+ const parsed = await fetchJson(
233
+ `${origin}/w/api.php?action=parse&format=json&formatversion=2&origin=*&maxlag=${MAXLAG_SECONDS}&prop=links&redirects=1&page=${encodeURIComponent(t)}&section=0`,
234
+ );
235
+ const links = parsed?.parse?.links;
236
+ if (!Array.isArray(links)) return null;
237
+ const seen = new Set();
238
+ const out = [];
239
+ for (const link of links) {
240
+ if (!link || link.ns !== 0 || link.exists === false) continue;
241
+ const linkTitle = String(link.title ?? link["*"] ?? "").trim();
242
+ const folded = normFactTerm(linkTitle);
243
+ if (!linkTitle || !folded || seen.has(folded)) continue;
244
+ seen.add(folded);
245
+ out.push(linkTitle);
246
+ }
247
+ return out;
248
+ });
249
+ return Array.isArray(listed) ? listed.slice(0, Math.max(0, limit)) : null;
125
250
  },
126
251
  };
127
252
  }
@@ -143,3 +268,34 @@ export function getLiveReferenceProvider() {
143
268
  if (!defaultProvider) defaultProvider = createWikipediaLiveProvider();
144
269
  return defaultProvider;
145
270
  }
271
+
272
+ // ---- the research lane's provider: Simple English Wikipedia, waiting slots --
273
+
274
+ let defaultResearchProvider = null;
275
+ let registeredResearchProvider = null;
276
+
277
+ /** Swap the research lane's lookup: provider = { lookup, pageByTitle,
278
+ * linkedTitles } (tests and the demo pages stub it, same seam shape as
279
+ * registerLiveReferenceProvider). Pass null to restore the default
280
+ * simple.wikipedia.org provider. */
281
+ export function registerResearchProvider(provider) {
282
+ registeredResearchProvider = provider && typeof provider.lookup === "function" ? provider : null;
283
+ }
284
+
285
+ /** The research lane's active provider — the registered one, else one lazily
286
+ * created singleton against simple.wikipedia.org with `waitForSlot` on: the
287
+ * queue is paced turn-by-turn, so a throttled step WAITS for its polite slot
288
+ * rather than reporting a false miss. `minIntervalMs` (first call only —
289
+ * the singleton keeps its throttle clock) can only ever RAISE the interval;
290
+ * the shipped minimum stays the floor. */
291
+ export function getResearchProvider({ minIntervalMs } = {}) {
292
+ if (registeredResearchProvider) return registeredResearchProvider;
293
+ if (!defaultResearchProvider) {
294
+ defaultResearchProvider = createWikipediaLiveProvider({
295
+ origin: SIMPLE_WIKIPEDIA_ORIGIN,
296
+ waitForSlot: true,
297
+ minIntervalMs: Math.max(DEFAULT_MIN_INTERVAL_MS, Number(minIntervalMs) || 0),
298
+ });
299
+ }
300
+ return defaultResearchProvider;
301
+ }