@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.
- package/README.md +2 -2
- package/corpus/sprites/src/sprite-facts.jsonl +18 -0
- package/corpus/worlds/manifest.json +5 -5
- package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
- package/corpus/worlds/src/ashcombe-hall.jsonl +27 -0
- package/data/sprites/book-icon.toml +12 -0
- package/data/sprites/cellar-icon.toml +12 -0
- package/data/sprites/drawing-room-icon.toml +13 -0
- package/data/sprites/garden-icon.toml +12 -0
- package/data/sprites/kitchen-icon.toml +13 -0
- package/data/sprites/library-icon.toml +12 -0
- package/data/sprites/pan-icon.toml +11 -0
- package/data/sprites/study-icon.toml +12 -0
- package/package.json +5 -2
- package/src/adapters/corpus/wikipedia-live.mjs +182 -26
- package/src/adapters/corpus/worlds-pack.mjs +8 -2
- package/src/adapters/toml-config.mjs +6 -0
- package/src/domain/memory/trust.mjs +11 -0
- package/src/domain/worlds-pack.mjs +50 -0
- package/src/services/adventure-autoplay.mjs +5 -2
- package/src/services/adventure-viz.mjs +301 -33
- package/src/services/adventure.mjs +162 -14
- package/src/services/chat-page-viz.mjs +265 -189
- package/src/services/chat-session.mjs +15 -5
- package/src/services/chat.mjs +286 -43
- package/src/services/code-explorer-viz.mjs +183 -75
- package/src/services/extract-facts.mjs +118 -28
- package/src/services/ingest-viz.mjs +328 -79
- package/src/services/ledger-viz.mjs +99 -0
- package/src/services/memory-panel-viz.mjs +159 -0
- package/src/services/research.mjs +266 -0
- package/src/services/sentences.mjs +19 -0
- package/src/services/spider-fly-viz.mjs +21 -5
- package/src/surfaces/web/adventure-browser-entry.mjs +9 -5
- package/src/surfaces/web/chat-browser-entry.mjs +28 -11
- package/src/surfaces/web/code-explorer-browser-entry.mjs +27 -11
- package/src/surfaces/web/ingest-browser-entry.mjs +123 -41
- package/src/surfaces/web/ledger-browser-entry.mjs +10 -4
- package/src/surfaces/web/memory-ask-browser.bundle.js +112 -112
- 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":
|
|
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":
|
|
18
|
-
"sha256": "
|
|
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":
|
|
35
|
-
"sha256": "
|
|
34
|
+
"bytes": 1292,
|
|
35
|
+
"sha256": "a8e61a93ebf992a8b87221c5a9a9f53bf92b7798cee86fb9d6915a1844d960dd"
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
"file": "shards/spider-fly.jsonl.gz",
|
|
Binary file
|
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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§ion=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)}§ion=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
|
+
}
|