@polycode-projects/the-mechanical-code-talker 4.0.1 → 4.1.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.
- package/README.md +2 -1
- package/corpus/sprites/src/sprite-facts.jsonl +375 -8
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +20 -0
- package/src/domain/ask-vocab.mjs +71 -0
- package/src/domain/ask.mjs +168 -0
- package/src/domain/game-config.mjs +11 -0
- package/src/domain/mud-facts.mjs +15 -0
- package/src/domain/router/drive.mjs +35 -9
- package/src/domain/router/registry.mjs +24 -4
- package/src/domain/router/resolver.mjs +102 -40
- package/src/domain/scene-compose.mjs +117 -0
- package/src/domain/spider-fly-world.mjs +36 -0
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/domain/sprite-request.mjs +156 -0
- package/src/domain/sprite-templates.mjs +161 -14
- package/src/services/adventure-editor.mjs +8 -14
- package/src/services/adventure-viz.mjs +119 -150
- package/src/services/adventure.mjs +97 -35
- package/src/services/chat-page-viz.mjs +64 -48
- package/src/services/chat.mjs +102 -34
- package/src/services/code-explorer-viz.mjs +52 -50
- package/src/services/ingest-viz.mjs +32 -74
- package/src/services/ledger-viz.mjs +87 -70
- package/src/services/memory-panel-viz.mjs +38 -0
- package/src/services/mud-editor.mjs +10 -15
- package/src/services/mud-turn.mjs +6 -6
- package/src/services/mud-viz.mjs +119 -225
- package/src/services/p2p-room.mjs +90 -23
- package/src/services/plan-pddl.mjs +3 -1
- package/src/services/plan-viz.mjs +13 -12
- package/src/services/research-viz.mjs +25 -67
- package/src/services/spider-fly-turn.mjs +14 -22
- package/src/services/spider-fly-viz.mjs +97 -136
- package/src/services/spider-fly.mjs +69 -11
- package/src/services/sprite-catalog-viz.mjs +274 -224
- package/src/services/viz-boot.mjs +71 -0
- package/src/services/viz-room-graph.mjs +203 -0
- package/src/services/viz-theme.mjs +75 -1
- package/src/services/viz-ticker.mjs +22 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +62 -47
- package/src/surfaces/web/chat-browser-entry.mjs +51 -107
- package/src/surfaces/web/code-explorer-browser-entry.mjs +192 -35
- package/src/surfaces/web/engine-surface.mjs +82 -0
- package/src/surfaces/web/ingest-browser-entry.mjs +16 -17
- package/src/surfaces/web/ledger-browser-entry.mjs +24 -56
- package/src/surfaces/web/memory-ask-browser-entry.mjs +55 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +128 -125
- package/src/surfaces/web/memory-stats.mjs +11 -0
- package/src/surfaces/web/mud-browser-entry.mjs +70 -49
- package/src/surfaces/web/plan-browser-entry.mjs +39 -50
- package/src/surfaces/web/research-browser-entry.mjs +48 -46
- package/src/surfaces/web/spider-fly-browser-entry.mjs +76 -40
- package/src/surfaces/web/sprites-browser-entry.mjs +28 -32
- package/src/surfaces/web/tmct-surface.mjs +147 -0
- package/src/surfaces/web/turn-session.mjs +124 -0
- package/src/tools/definitions.mjs +30 -0
- package/src/tools/handlers/index.mjs +6 -3
- package/src/tools/handlers/kit.mjs +19 -2
- package/src/tools/handlers/tmct-ask.mjs +11 -6
- package/src/tools/handlers/tmct-ingest.mjs +5 -1
- package/src/tools/handlers/tmct-related.mjs +4 -4
- package/src/tools/handlers/tmct-sprite.mjs +147 -0
- package/src/tools/memory-fallthrough.mjs +9 -2
- package/src/tools/server.mjs +37 -6
|
@@ -164,6 +164,19 @@ export function createP2pRoom({
|
|
|
164
164
|
const seenProvenanceById = new Map();
|
|
165
165
|
let cachedRows = [];
|
|
166
166
|
|
|
167
|
+
// Store-touching work runs one job at a time, in arrival order. Every path
|
|
168
|
+
// that reads or writes memoryDir/seenProvenanceById/cachedRows crosses at
|
|
169
|
+
// least one await, so without this a rebind could swap the store while a
|
|
170
|
+
// merge is half-written into the old one — the merged rows would then
|
|
171
|
+
// baseline against the wrong store, or vanish. Jobs queued behind a rebind
|
|
172
|
+
// land on the store the rebind installed.
|
|
173
|
+
let storeChain = Promise.resolve();
|
|
174
|
+
function withStore(job) {
|
|
175
|
+
const run = storeChain.then(job);
|
|
176
|
+
storeChain = run.then(() => {}, () => {});
|
|
177
|
+
return run;
|
|
178
|
+
}
|
|
179
|
+
|
|
167
180
|
const factsListeners = new Set();
|
|
168
181
|
const stateListeners = new Set();
|
|
169
182
|
const peersListeners = new Set();
|
|
@@ -246,8 +259,8 @@ export function createP2pRoom({
|
|
|
246
259
|
|
|
247
260
|
/** Diff the store against what this room last saw, relabel each changed
|
|
248
261
|
* fact's provenance for the wire, and broadcast the batch. The page calls
|
|
249
|
-
* this once after every local turn or action. */
|
|
250
|
-
async function
|
|
262
|
+
* this (as `afterLocalChange`) once after every local turn or action. */
|
|
263
|
+
async function flushLocalChange() {
|
|
251
264
|
await ensureStarted();
|
|
252
265
|
await refreshRows();
|
|
253
266
|
const changed = [];
|
|
@@ -270,7 +283,7 @@ export function createP2pRoom({
|
|
|
270
283
|
if (!accepted.length) return { merged: 0 };
|
|
271
284
|
// Flush first: a local fact still waiting to be diffed would otherwise be
|
|
272
285
|
// recorded as merged below and never leave this browser.
|
|
273
|
-
await
|
|
286
|
+
await flushLocalChange();
|
|
274
287
|
const { ids } = await appendFacts(memoryDir, accepted.map((f) => ({
|
|
275
288
|
subject: f.subject,
|
|
276
289
|
predicate: f.predicate,
|
|
@@ -357,9 +370,11 @@ export function createP2pRoom({
|
|
|
357
370
|
return;
|
|
358
371
|
}
|
|
359
372
|
case "sync-request": {
|
|
360
|
-
await
|
|
361
|
-
|
|
362
|
-
|
|
373
|
+
const facts = await withStore(async () => {
|
|
374
|
+
await refreshRows();
|
|
375
|
+
const timestamp = now();
|
|
376
|
+
return syncableFacts(cachedRows).flatMap((row) => toWireFacts(row, displayName, timestamp));
|
|
377
|
+
});
|
|
363
378
|
send(transport, syncResponseMessage({ facts }));
|
|
364
379
|
return;
|
|
365
380
|
}
|
|
@@ -368,7 +383,7 @@ export function createP2pRoom({
|
|
|
368
383
|
// between them needs no sequencing.
|
|
369
384
|
case "sync-response":
|
|
370
385
|
case "op":
|
|
371
|
-
await mergeIncomingFacts(message.facts);
|
|
386
|
+
await withStore(() => mergeIncomingFacts(message.facts));
|
|
372
387
|
}
|
|
373
388
|
}
|
|
374
389
|
|
|
@@ -413,7 +428,7 @@ export function createP2pRoom({
|
|
|
413
428
|
/** Mint a fresh invite blob. One blob completes exactly one connection, so
|
|
414
429
|
* each call replaces whatever earlier invite was still waiting for a reply. */
|
|
415
430
|
async function startSharing() {
|
|
416
|
-
await ensureStarted
|
|
431
|
+
await withStore(ensureStarted);
|
|
417
432
|
const transport = attachTransport(transportFactory());
|
|
418
433
|
pendingShare = transport;
|
|
419
434
|
lastError = null;
|
|
@@ -425,7 +440,7 @@ export function createP2pRoom({
|
|
|
425
440
|
/** Decode someone's invite and answer it. Returns the reply blob to send
|
|
426
441
|
* back, or a named problem the page can show beside the box it came from. */
|
|
427
442
|
async function acceptInvite(blobString) {
|
|
428
|
-
await ensureStarted
|
|
443
|
+
await withStore(ensureStarted);
|
|
429
444
|
const decoded = decodeInviteBlob(blobString);
|
|
430
445
|
if (decoded.error) {
|
|
431
446
|
lastError = problem("invite", decoded.error);
|
|
@@ -453,7 +468,7 @@ export function createP2pRoom({
|
|
|
453
468
|
/** Feed the joiner's reply back into the invite it answers, completing the
|
|
454
469
|
* connection. */
|
|
455
470
|
async function completeInvite(replyBlob) {
|
|
456
|
-
await ensureStarted
|
|
471
|
+
await withStore(ensureStarted);
|
|
457
472
|
if (!pendingShare) {
|
|
458
473
|
lastError = problem("reply", "no-pending-invite");
|
|
459
474
|
return lastError;
|
|
@@ -482,20 +497,24 @@ export function createP2pRoom({
|
|
|
482
497
|
}
|
|
483
498
|
|
|
484
499
|
async function setMyDisplayName(name) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
500
|
+
return withStore(async () => {
|
|
501
|
+
await ensureStarted();
|
|
502
|
+
displayName = name;
|
|
503
|
+
await appendFacts(memoryDir, [nodeNameFact(myPeerId, name, now())]);
|
|
504
|
+
await sortFactIndividualsById();
|
|
505
|
+
return flushLocalChange();
|
|
506
|
+
});
|
|
490
507
|
}
|
|
491
508
|
|
|
492
509
|
/** Wave as `subjectId`, in `roomId` when there is one. chat.html's presence
|
|
493
510
|
* wave passes null and lands on PRESENCE_SCOPE instead. */
|
|
494
511
|
async function wave(subjectId, roomId = null) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
512
|
+
return withStore(async () => {
|
|
513
|
+
await ensureStarted();
|
|
514
|
+
await appendFacts(memoryDir, [waveFact(subjectId, roomId || PRESENCE_SCOPE, now())]);
|
|
515
|
+
await sortFactIndividualsById();
|
|
516
|
+
return flushLocalChange();
|
|
517
|
+
});
|
|
499
518
|
}
|
|
500
519
|
|
|
501
520
|
/** Whether `subjectId` is waving right now, read from the cached rows so a
|
|
@@ -517,6 +536,53 @@ export function createP2pRoom({
|
|
|
517
536
|
return peers.get(peerId)?.displayName || String(peerId).slice(0, 8);
|
|
518
537
|
}
|
|
519
538
|
|
|
539
|
+
/** Re-bind this room to a fresh store — the recast path. The peer
|
|
540
|
+
* connections are the point of keeping the room alive, so nothing about
|
|
541
|
+
* the transports or the peer map is touched. In order: flush whatever the
|
|
542
|
+
* OLD store still had undiffed (a turn landed just before the recast must
|
|
543
|
+
* not vanish silently), swap the store reference, rebuild the diff
|
|
544
|
+
* baseline against the new store, write the identity facts into it, then
|
|
545
|
+
* push the new store's syncable facts to every open channel as an
|
|
546
|
+
* ordinary op and ask each peer for its own view with an ordinary
|
|
547
|
+
* sync-request — the exact machinery a freshly opened channel uses, no
|
|
548
|
+
* new message type. Runs on the store chain, so a merge in flight when
|
|
549
|
+
* the recast happens finishes against the store it started on, and
|
|
550
|
+
* everything behind it lands on the new one. */
|
|
551
|
+
async function rebind({ memoryDir: nextMemoryDir, worldName: nextWorldName, myDisplayName: nextDisplayName } = {}) {
|
|
552
|
+
if (!nextMemoryDir) throw new Error("rebind needs the store to bind to");
|
|
553
|
+
return withStore(async () => {
|
|
554
|
+
if (closed) throw new Error("this room is closed");
|
|
555
|
+
if (started) await flushLocalChange();
|
|
556
|
+
memoryDir = nextMemoryDir;
|
|
557
|
+
if (nextWorldName) worldName = nextWorldName;
|
|
558
|
+
if (nextDisplayName) displayName = nextDisplayName;
|
|
559
|
+
started = true;
|
|
560
|
+
seenProvenanceById.clear();
|
|
561
|
+
cachedRows = [];
|
|
562
|
+
const timestamp = now();
|
|
563
|
+
const identity = [];
|
|
564
|
+
if (worldId && worldName) identity.push(worldNameFact(worldId, worldName, timestamp));
|
|
565
|
+
if (myPeerId && displayName) identity.push(nodeNameFact(myPeerId, displayName, timestamp));
|
|
566
|
+
if (identity.length) {
|
|
567
|
+
await appendFacts(memoryDir, identity);
|
|
568
|
+
await sortFactIndividualsById();
|
|
569
|
+
}
|
|
570
|
+
await refreshRows();
|
|
571
|
+
for (const row of cachedRows) seenProvenanceById.set(row.id, row.provenance);
|
|
572
|
+
const targets = connectedPeers();
|
|
573
|
+
let pushed = 0;
|
|
574
|
+
if (targets.length) {
|
|
575
|
+
const wireTimestamp = now();
|
|
576
|
+
const facts = syncableFacts(cachedRows).flatMap((row) => toWireFacts(row, displayName, wireTimestamp));
|
|
577
|
+
pushed = facts.length;
|
|
578
|
+
if (facts.length) broadcast(opMessage({ from: myPeerId, facts }));
|
|
579
|
+
broadcast(syncRequestMessage());
|
|
580
|
+
}
|
|
581
|
+
emit(factsListeners, { merged: 0, rows: cachedRows });
|
|
582
|
+
return { pushed, peers: targets.length };
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
520
586
|
function close() {
|
|
521
587
|
closed = true;
|
|
522
588
|
for (const peer of peers.values()) peer.transport.close();
|
|
@@ -534,23 +600,24 @@ export function createP2pRoom({
|
|
|
534
600
|
return {
|
|
535
601
|
peerId: myPeerId,
|
|
536
602
|
worldId,
|
|
537
|
-
worldName,
|
|
603
|
+
get worldName() { return worldName; },
|
|
538
604
|
get displayName() { return displayName; },
|
|
539
605
|
get state() { return state; },
|
|
540
606
|
get lastError() { return lastError; },
|
|
541
607
|
get droppedMessages() { return droppedMessages; },
|
|
542
608
|
peers: peerList,
|
|
543
609
|
displayNameFor,
|
|
544
|
-
start: ensureStarted,
|
|
610
|
+
start: () => withStore(ensureStarted),
|
|
545
611
|
startSharing,
|
|
546
612
|
acceptInvite,
|
|
547
613
|
completeInvite,
|
|
548
|
-
afterLocalChange,
|
|
614
|
+
afterLocalChange: () => withStore(flushLocalChange),
|
|
549
615
|
setMyDisplayName,
|
|
550
616
|
wave,
|
|
551
617
|
isWaving,
|
|
618
|
+
rebind,
|
|
552
619
|
factRows: () => cachedRows,
|
|
553
|
-
refresh: refreshRows,
|
|
620
|
+
refresh: () => withStore(refreshRows),
|
|
554
621
|
onFactsChanged: subscribe(factsListeners),
|
|
555
622
|
onStateChanged: subscribe(stateListeners),
|
|
556
623
|
onPeersChanged: subscribe(peersListeners),
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
// is itself a declared class name is a class-to-class edge; anything else is
|
|
26
26
|
// an individual-to-class edge), not a guess.
|
|
27
27
|
|
|
28
|
+
import { countLabel } from "./viz-theme.mjs";
|
|
29
|
+
|
|
28
30
|
const attachPrefix = (predicate) => {
|
|
29
31
|
const p = String(predicate ?? "").trim();
|
|
30
32
|
return p.includes(":") ? p : `mgx:${p}`;
|
|
@@ -208,7 +210,7 @@ export function planToPddl(plan, { problemName = "tmct-plan", domainName = "tmct
|
|
|
208
210
|
|
|
209
211
|
if (actions.length) {
|
|
210
212
|
lines.push("");
|
|
211
|
-
lines.push(`;; action sequence — findActionPath's own shortest path (${actions.length
|
|
213
|
+
lines.push(`;; action sequence — findActionPath's own shortest path (${countLabel(actions.length, "move")})`);
|
|
212
214
|
actions.forEach((action, i) => {
|
|
213
215
|
const before = states[i] || [];
|
|
214
216
|
const after = states[i + 1] || [];
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// inline script degrades honestly when that sibling script is absent or
|
|
19
19
|
// fails to load — the live controls disable themselves rather than pretend
|
|
20
20
|
// to work.
|
|
21
|
-
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson } from "./viz-theme.mjs";
|
|
21
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, countLabel } from "./viz-theme.mjs";
|
|
22
22
|
import { planToPddl } from "./plan-pddl.mjs";
|
|
23
23
|
|
|
24
24
|
const BOARD_W = 640;
|
|
@@ -285,7 +285,7 @@ export function renderPlanHtml({ plan, rendersAs = {}, sizeOrder = [], title } =
|
|
|
285
285
|
const pageData = planToPageData({ plan, rendersAs, sizeOrder });
|
|
286
286
|
const embedded = embedJson(pageData);
|
|
287
287
|
const pddlText = planToPddl(plan);
|
|
288
|
-
const pageTitle = title || `tmct plan — ${actions.length
|
|
288
|
+
const pageTitle = title || `tmct plan — ${countLabel(actions.length, "move")}`;
|
|
289
289
|
|
|
290
290
|
return `<!doctype html>
|
|
291
291
|
<!-- data-theme is pinned to dark on purpose: this page reads as a track/
|
|
@@ -483,6 +483,7 @@ const PLAN = ${embedded};
|
|
|
483
483
|
(function () {
|
|
484
484
|
"use strict";
|
|
485
485
|
const escapeHtml = ${escapeHtml.toString()};
|
|
486
|
+
const countLabel = ${countLabel.toString()};
|
|
486
487
|
// Best-effort: a copy of this page opened without the sibling worker file
|
|
487
488
|
// (a tmct --render plan --output file, a file:// open) just swallows the
|
|
488
489
|
// registration failure and works exactly as before.
|
|
@@ -664,7 +665,7 @@ const PLAN = ${embedded};
|
|
|
664
665
|
mountPlan(PLAN);
|
|
665
666
|
|
|
666
667
|
// ---- live re-solve: disk-count/max-depth controls + the chat-assert dock
|
|
667
|
-
// over the sibling plan-browser.bundle.js (window.
|
|
668
|
+
// over the sibling plan-browser.bundle.js (window.tmct). Degrades
|
|
668
669
|
// honestly when the bundle failed to load or wasn't built alongside this
|
|
669
670
|
// page (e.g. a plain renderPlanHtml() call with no bundle nearby) — the
|
|
670
671
|
// baked-in replay above already stands on its own either way.
|
|
@@ -677,7 +678,7 @@ const PLAN = ${embedded};
|
|
|
677
678
|
const chatqEl = document.getElementById("chatq");
|
|
678
679
|
const chatpillsEl = document.getElementById("chatpills");
|
|
679
680
|
|
|
680
|
-
const liveAvailable = typeof
|
|
681
|
+
const liveAvailable = typeof tmct !== "undefined" && typeof tmct.open === "function";
|
|
681
682
|
if (!liveAvailable) {
|
|
682
683
|
liveStatusEl.textContent = "live re-solve unavailable here — showing the baked-in replay only.";
|
|
683
684
|
liveStatusEl.classList.add("isError");
|
|
@@ -711,7 +712,7 @@ const PLAN = ${embedded};
|
|
|
711
712
|
import("./vendor/wink.js"),
|
|
712
713
|
winkTimeout(WINK_LOAD_TIMEOUT_MS, "wink vendor asset load timed out"),
|
|
713
714
|
]);
|
|
714
|
-
|
|
715
|
+
tmct.page.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
|
|
715
716
|
} catch (err) {
|
|
716
717
|
// eslint-disable-next-line no-console
|
|
717
718
|
console.warn("tmct plan: the wink vendor asset failed to load, continuing without the lemma/POS tier", err);
|
|
@@ -728,9 +729,9 @@ const PLAN = ${embedded};
|
|
|
728
729
|
}
|
|
729
730
|
function applyPlan(freshPlan) {
|
|
730
731
|
if (!freshPlan) return;
|
|
731
|
-
const { rendersAs, sizeOrder } =
|
|
732
|
-
mountPlan(
|
|
733
|
-
if (pddlEl) pddlEl.textContent =
|
|
732
|
+
const { rendersAs, sizeOrder } = tmct.page.renderInputsFromPlan(freshPlan);
|
|
733
|
+
mountPlan(tmct.page.planToPageData({ plan: freshPlan, rendersAs, sizeOrder }));
|
|
734
|
+
if (pddlEl) pddlEl.textContent = tmct.page.planToPddl(freshPlan);
|
|
734
735
|
// The <title>/<h1> were baked from the INITIAL plan's own goal text —
|
|
735
736
|
// a live re-solve toward a different goal (a fresh puzzle, or a
|
|
736
737
|
// taught goal revision) must not leave them stating the old one.
|
|
@@ -741,7 +742,7 @@ const PLAN = ${embedded};
|
|
|
741
742
|
async function ensureSession() {
|
|
742
743
|
if (session) return session;
|
|
743
744
|
await tryLoadWink();
|
|
744
|
-
session = await
|
|
745
|
+
session = await tmct.open({
|
|
745
746
|
diskCount: Math.max(1, Math.min(7, parseInt(diskCountEl.value, 10) || 3)),
|
|
746
747
|
maxDepth: Math.max(1, parseInt(maxDepthEl.value, 10) || 300),
|
|
747
748
|
});
|
|
@@ -756,10 +757,10 @@ const PLAN = ${embedded};
|
|
|
756
757
|
liveStatusEl.textContent = "solving a " + n + "-disk puzzle…";
|
|
757
758
|
liveStatusEl.classList.remove("isError");
|
|
758
759
|
await tryLoadWink();
|
|
759
|
-
session = await
|
|
760
|
+
session = await tmct.open({ diskCount: n, maxDepth: d });
|
|
760
761
|
if (session.plan) {
|
|
761
762
|
applyPlan(session.plan);
|
|
762
|
-
liveStatusEl.textContent = "live — " + n
|
|
763
|
+
liveStatusEl.textContent = "live — " + countLabel(n, "disk", "disks") + ", max depth " + d + ".";
|
|
763
764
|
} else {
|
|
764
765
|
liveStatusEl.textContent = "no plan found within " + d + " moves — raise max search depth and try again.";
|
|
765
766
|
liveStatusEl.classList.add("isError");
|
|
@@ -776,7 +777,7 @@ const PLAN = ${embedded};
|
|
|
776
777
|
withLock(async () => {
|
|
777
778
|
const s = await ensureSession();
|
|
778
779
|
const maxDepth = Math.max(1, parseInt(maxDepthEl.value, 10) || 300);
|
|
779
|
-
const result = await
|
|
780
|
+
const result = await tmct.turn(q, { maxDepth });
|
|
780
781
|
addChatLine("a", result.answer);
|
|
781
782
|
if (result.plan) applyPlan(result.plan);
|
|
782
783
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// document shaped exactly like ingest-viz.mjs/chat-page-viz.mjs's own
|
|
3
3
|
// page-builders — one inlined <style> importing viz-theme.mjs's shared tokens,
|
|
4
4
|
// behaviour as an inlined IIFE — running the research engine
|
|
5
|
-
// (research-browser.bundle.js's globalThis.
|
|
5
|
+
// (research-browser.bundle.js's globalThis.tmct) by same-origin
|
|
6
6
|
// relative paths.
|
|
7
7
|
//
|
|
8
8
|
// One in-memory graph grows three ways, each visible on the page:
|
|
@@ -20,8 +20,10 @@
|
|
|
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
22
|
import { THEME_TOKENS_CSS, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
|
|
23
|
-
import { fetchWithProgress } from "./memory-panel-viz.mjs";
|
|
23
|
+
import { fetchWithProgress, loadProgressLine, factTripleParts } from "./memory-panel-viz.mjs";
|
|
24
24
|
import { createTicker, prefersReducedMotion } from "./viz-ticker.mjs";
|
|
25
|
+
import { loadWinkVendor } from "./viz-boot.mjs";
|
|
26
|
+
import { cloneMemoryPayload } from "../adapters/memory/core.mjs";
|
|
25
27
|
|
|
26
28
|
const DEFAULT_TITLE = "the-mechanical-code-talker — research";
|
|
27
29
|
|
|
@@ -63,35 +65,6 @@ export function sourceLabelFor(source) {
|
|
|
63
65
|
return { label: (BANDS[band] || band || "seed") + " (seed corpus)", tone: "seed" };
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
/** One learned fact as its three canonical cells plus a source tone, for the
|
|
67
|
-
* highlights and history lists. Pure, `.toString()`-splice safe. */
|
|
68
|
-
export function factTripleParts(fact) {
|
|
69
|
-
return {
|
|
70
|
-
subject: String((fact && fact.subject) || ""),
|
|
71
|
-
predicate: String((fact && fact.predicate) || ""),
|
|
72
|
-
object: String((fact && fact.object) || ""),
|
|
73
|
-
source: String((fact && fact.source) || ""),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** The boot statusline while the big assets stream in — the same aggregator
|
|
78
|
-
* ingest-viz.mjs's own loadProgressLine is. `parts` is an array of
|
|
79
|
-
* { loaded, total } byte counts. Self-contained, `.toString()`-splice safe. */
|
|
80
|
-
export function loadProgressLine(parts) {
|
|
81
|
-
const mb = (n) => (n / 1048576).toFixed(1);
|
|
82
|
-
let loaded = 0;
|
|
83
|
-
let total = 0;
|
|
84
|
-
let totalKnown = true;
|
|
85
|
-
for (const p of parts || []) {
|
|
86
|
-
loaded += (p && p.loaded) || 0;
|
|
87
|
-
if (p && p.total > 0) total += p.total;
|
|
88
|
-
else totalKnown = false;
|
|
89
|
-
}
|
|
90
|
-
return totalKnown && total > 0
|
|
91
|
-
? "loading the engine… " + mb(loaded) + " MB / " + mb(total) + " MB"
|
|
92
|
-
: "loading the engine… " + mb(loaded) + " MB";
|
|
93
|
-
}
|
|
94
|
-
|
|
95
68
|
/** The self-contained research page. Pure — the same output for the same
|
|
96
69
|
* input every time; every piece of state is computed live in the browser once
|
|
97
70
|
* the sibling research bundle loads. `digestStructures` are the pre-parsed
|
|
@@ -136,7 +109,7 @@ ${DASH_DARK_CHROME_CSS}
|
|
|
136
109
|
.brand { display: flex; flex-direction: column; gap: .3rem; max-width: 640px; }
|
|
137
110
|
.eyebrow { font-family: ${MONO_STACK}; font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
|
138
111
|
.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; }
|
|
112
|
+
.statuspanel { display: flex; flex-wrap: wrap; gap: .5rem 1.1rem; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: .5rem .9rem; }
|
|
140
113
|
.statuspanel .stat { display: flex; flex-direction: column; gap: .14rem; min-width: 7rem; }
|
|
141
114
|
.statuspanel .stat-label { font-family: ${MONO_STACK}; font-size: .6rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
|
142
115
|
.statuspanel .stat-value { font-family: ${MONO_STACK}; font-size: .82rem; font-variant-numeric: tabular-nums; color: var(--ink); }
|
|
@@ -255,6 +228,7 @@ ${DASH_DARK_CHROME_CSS}
|
|
|
255
228
|
.cols { grid-template-columns: 1fr; }
|
|
256
229
|
.chip.tapchip { grid-template-columns: 1.5rem minmax(0,1fr) 3.2rem; }
|
|
257
230
|
.chip.tapchip .track { display: none; }
|
|
231
|
+
.statuspanel .stat { min-width: 0; }
|
|
258
232
|
}
|
|
259
233
|
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; } }
|
|
260
234
|
@media (prefers-reduced-motion: no-preference) {
|
|
@@ -381,6 +355,8 @@ ${DASH_DARK_CHROME_CSS}
|
|
|
381
355
|
const fetchWithProgress = ${fetchWithProgress.toString()};
|
|
382
356
|
const createTicker = ${createTicker.toString()};
|
|
383
357
|
const prefersReducedMotion = ${prefersReducedMotion.toString()};
|
|
358
|
+
const loadWinkVendor = ${loadWinkVendor.toString()};
|
|
359
|
+
const cloneMemoryPayload = ${cloneMemoryPayload.toString()};
|
|
384
360
|
const el = (id) => document.getElementById(id);
|
|
385
361
|
// The digest sentence-structure bank, pre-parsed at build time — the browser
|
|
386
362
|
// has no TOML parser, so the page carries the table the client-side digest
|
|
@@ -409,7 +385,6 @@ ${DASH_DARK_CHROME_CSS}
|
|
|
409
385
|
const checkedSources = new Set(); // source keys currently checked for the ask
|
|
410
386
|
|
|
411
387
|
// ---- boot --------------------------------------------------------------
|
|
412
|
-
const WINK_LOAD_TIMEOUT_MS = 8000;
|
|
413
388
|
const progressParts = {};
|
|
414
389
|
let progressActive = true;
|
|
415
390
|
function noteProgress(key, loaded, total) {
|
|
@@ -417,20 +392,7 @@ ${DASH_DARK_CHROME_CSS}
|
|
|
417
392
|
if (progressActive) statEngineEl.textContent = loadProgressLine(Object.values(progressParts));
|
|
418
393
|
}
|
|
419
394
|
|
|
420
|
-
|
|
421
|
-
let settled = false;
|
|
422
|
-
const timeout = new Promise((_, reject) => setTimeout(() => { if (!settled) reject(new Error("wink load stalled")); }, WINK_LOAD_TIMEOUT_MS));
|
|
423
|
-
try {
|
|
424
|
-
const mod = await Promise.race([import("./vendor/wink.js"), timeout]);
|
|
425
|
-
settled = true;
|
|
426
|
-
window.tmctResearch.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
|
|
427
|
-
return "loaded";
|
|
428
|
-
} catch (err) {
|
|
429
|
-
settled = true;
|
|
430
|
-
console.warn("tmct research: the wink vendor asset failed to load", err);
|
|
431
|
-
return "unavailable";
|
|
432
|
-
}
|
|
433
|
-
}
|
|
395
|
+
const loadWink = loadWinkVendor({ register: (factory) => window.tmct.page.registerWinkModel(factory) });
|
|
434
396
|
|
|
435
397
|
let seedPayload = null;
|
|
436
398
|
let seedFacts = 0;
|
|
@@ -444,12 +406,8 @@ ${DASH_DARK_CHROME_CSS}
|
|
|
444
406
|
console.warn("tmct research: chat-seed.json unavailable — starting unseeded", err);
|
|
445
407
|
}
|
|
446
408
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
try { return structuredClone(seedPayload); } catch { return JSON.parse(JSON.stringify(seedPayload)); }
|
|
450
|
-
};
|
|
451
|
-
function newSession() {
|
|
452
|
-
return window.tmctResearch.createResearchSession({ seedPayload: cloneSeed(), vocabSeeded: Boolean(seedPayload), digestStructures: DIGEST_STRUCTURES });
|
|
409
|
+
async function newSession() {
|
|
410
|
+
return window.tmct.open({ seedPayload: cloneMemoryPayload(seedPayload), vocabSeeded: Boolean(seedPayload), digestStructures: DIGEST_STRUCTURES });
|
|
453
411
|
}
|
|
454
412
|
|
|
455
413
|
// The curated reference pack provider, same fetch seam chat.html registers,
|
|
@@ -474,17 +432,17 @@ ${DASH_DARK_CHROME_CSS}
|
|
|
474
432
|
};
|
|
475
433
|
|
|
476
434
|
async function boot() {
|
|
477
|
-
if (!window.
|
|
435
|
+
if (!window.tmct) {
|
|
478
436
|
engineNoteEl.hidden = false;
|
|
479
437
|
engineNoteEl.textContent = "The research engine did not load. Run npm run demo:build, then reload this page.";
|
|
480
438
|
statSeedEl.textContent = "—";
|
|
481
439
|
statEngineEl.textContent = "unavailable";
|
|
482
440
|
return;
|
|
483
441
|
}
|
|
484
|
-
const [winkStatus] = await Promise.all([
|
|
442
|
+
const [winkStatus] = await Promise.all([loadWink(), fetchSeed()]);
|
|
485
443
|
progressActive = false;
|
|
486
|
-
window.
|
|
487
|
-
session = newSession();
|
|
444
|
+
window.tmct.page.registerReferencePackProvider(fetchPackProvider);
|
|
445
|
+
session = await newSession();
|
|
488
446
|
const winkPart = winkStatus === "loaded" ? "wink-nlp loaded" : "wink-nlp unavailable, curated tiers only";
|
|
489
447
|
statSeedEl.textContent = seedPayload ? seedFacts.toLocaleString() + " facts" : "no seed";
|
|
490
448
|
statEngineEl.textContent = winkPart + " · ready";
|
|
@@ -507,7 +465,7 @@ ${DASH_DARK_CHROME_CSS}
|
|
|
507
465
|
async function refresh() {
|
|
508
466
|
if (!session) return;
|
|
509
467
|
let snap;
|
|
510
|
-
try { snap = await window.
|
|
468
|
+
try { snap = await window.tmct.page.researchSnapshot(session.memoryDir, session.sessionIds); }
|
|
511
469
|
catch (err) { console.warn("tmct research: snapshot failed", err); return; }
|
|
512
470
|
const total = (snap.sources || []).reduce((sum, source) => sum + (Number(source.count) || 0), 0);
|
|
513
471
|
statFactsEl.textContent = total.toLocaleString();
|
|
@@ -668,15 +626,15 @@ ${DASH_DARK_CHROME_CSS}
|
|
|
668
626
|
answerEl.className = "";
|
|
669
627
|
answerEl.textContent = "thinking…";
|
|
670
628
|
let res;
|
|
671
|
-
try { res = await
|
|
672
|
-
catch (err) { res = {
|
|
673
|
-
if (res.miss || !res.
|
|
629
|
+
try { res = await tmct.ask(q, { sources: allChecked ? null : checked }); }
|
|
630
|
+
catch (err) { res = { answer: "", miss: true }; }
|
|
631
|
+
if (res.miss || !res.answer) {
|
|
674
632
|
answerEl.className = "miss";
|
|
675
633
|
const scope = allChecked ? "any checked source" : "the " + checked.length + " checked source" + (checked.length === 1 ? "" : "s");
|
|
676
634
|
answerEl.textContent = "No grounded answer from " + scope + ". It abstains rather than guess.";
|
|
677
635
|
} else {
|
|
678
636
|
answerEl.className = "";
|
|
679
|
-
answerEl.textContent = res.
|
|
637
|
+
answerEl.textContent = res.answer;
|
|
680
638
|
}
|
|
681
639
|
}
|
|
682
640
|
el("askGo").addEventListener("click", ask);
|
|
@@ -747,7 +705,7 @@ ${DASH_DARK_CHROME_CSS}
|
|
|
747
705
|
const note = el("teachNote");
|
|
748
706
|
note.textContent = "…";
|
|
749
707
|
let res;
|
|
750
|
-
try { res = await
|
|
708
|
+
try { res = await tmct.turn(q); } catch { res = null; }
|
|
751
709
|
if (res && res.record && res.record.via === "assert" && !res.record.miss) {
|
|
752
710
|
note.textContent = "stored: " + (res.answer || "remembered.");
|
|
753
711
|
el("teachInput").value = "";
|
|
@@ -828,7 +786,7 @@ ${DASH_DARK_CHROME_CSS}
|
|
|
828
786
|
async function researchStep(line) {
|
|
829
787
|
if (!session) return;
|
|
830
788
|
let res;
|
|
831
|
-
try { res = await
|
|
789
|
+
try { res = await tmct.turn(line); } catch { res = null; }
|
|
832
790
|
if (res && res.research !== undefined) {
|
|
833
791
|
researchQueue = res.research;
|
|
834
792
|
renderResearchControls();
|
|
@@ -859,9 +817,9 @@ ${DASH_DARK_CHROME_CSS}
|
|
|
859
817
|
|
|
860
818
|
// ---- tools --------------------------------------------------------------
|
|
861
819
|
el("exportFacts").addEventListener("click", async () => {
|
|
862
|
-
if (!session || !window.
|
|
820
|
+
if (!session || !window.tmct.page.exportFactsJsonl) return;
|
|
863
821
|
let jsonl;
|
|
864
|
-
try { jsonl = await window.
|
|
822
|
+
try { jsonl = await window.tmct.page.exportFactsJsonl(session.memoryDir); }
|
|
865
823
|
catch { return; }
|
|
866
824
|
const blob = new Blob([jsonl], { type: "application/x-ndjson" });
|
|
867
825
|
const url = URL.createObjectURL(blob);
|
|
@@ -874,7 +832,7 @@ ${DASH_DARK_CHROME_CSS}
|
|
|
874
832
|
researchTicker.pause();
|
|
875
833
|
researchQueue = null;
|
|
876
834
|
checkedSources.clear();
|
|
877
|
-
session = newSession();
|
|
835
|
+
session = await newSession();
|
|
878
836
|
el("teachNote").textContent = "";
|
|
879
837
|
el("ingestNote").textContent = "";
|
|
880
838
|
el("researchNote").textContent = "";
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
19
|
DIRECTION_DELTA, WORLD_NAME, cellId, parseCellId, inBounds, chebyshevDistance, oneStepDirectionBetween,
|
|
20
|
+
agentKindOf, liveIdsOfKind,
|
|
20
21
|
} from "../domain/spider-fly-world.mjs";
|
|
21
22
|
import { foldSpiderFlyState, runSpiderFlyTick, startSpiderFlyGame, beliefSnapshotFor } from "./spider-fly.mjs";
|
|
22
23
|
import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
|
|
@@ -121,11 +122,6 @@ async function openSpiderFlyGame({ planHolder, memoryDir, env, cache, gameConfig
|
|
|
121
122
|
|
|
122
123
|
// ---- resolving an addressed agent / belief target ----------------------------
|
|
123
124
|
|
|
124
|
-
const liveIdsOfKind = (kind, state) => {
|
|
125
|
-
const re = new RegExp(`^${kind}-\\d+$`);
|
|
126
|
-
return [...state.placements.keys()].filter((id) => re.test(id) && !state.removed.has(id)).sort();
|
|
127
|
-
};
|
|
128
|
-
|
|
129
125
|
/** An exact "kind-num" reference, or (no number given) the first live
|
|
130
126
|
* individual of that kind — null when nothing live matches. */
|
|
131
127
|
function resolveAgentId(kind, num, state) {
|
|
@@ -133,7 +129,7 @@ function resolveAgentId(kind, num, state) {
|
|
|
133
129
|
const id = `${kind}-${num}`;
|
|
134
130
|
return state.placements.has(id) && !state.removed.has(id) ? id : null;
|
|
135
131
|
}
|
|
136
|
-
const live = liveIdsOfKind(kind, state);
|
|
132
|
+
const live = liveIdsOfKind(state.placements, kind, state.removed);
|
|
137
133
|
return live[0] ?? null;
|
|
138
134
|
}
|
|
139
135
|
|
|
@@ -143,7 +139,7 @@ function resolveAgentId(kind, num, state) {
|
|
|
143
139
|
* than one spider or fly. */
|
|
144
140
|
function resolveNearestAgentId(kind, num, state, nearCell) {
|
|
145
141
|
if (num) return resolveAgentId(kind, num, state);
|
|
146
|
-
const live = liveIdsOfKind(kind, state);
|
|
142
|
+
const live = liveIdsOfKind(state.placements, kind, state.removed);
|
|
147
143
|
if (!live.length || !nearCell) return live[0] ?? null;
|
|
148
144
|
let best = live[0];
|
|
149
145
|
let bestDist = Infinity;
|
|
@@ -193,11 +189,6 @@ export { oneStepDirectionBetween };
|
|
|
193
189
|
// claim, true or false alike. A pill's `truth` tag is for the human eye
|
|
194
190
|
// only — it never rides along in the submitted text itself.
|
|
195
191
|
|
|
196
|
-
const liveIdsOfKindFromAgents = (kind, agents) => {
|
|
197
|
-
const re = new RegExp(`^${kind}-\\d+$`);
|
|
198
|
-
return Object.keys(agents || {}).filter((id) => re.test(id)).sort();
|
|
199
|
-
};
|
|
200
|
-
|
|
201
192
|
/** "spider"/"fly" bare when exactly one individual of that kind is live
|
|
202
193
|
* (nothing to disambiguate), else the individual's own numbered id — a
|
|
203
194
|
* pill-set legibility choice, not a grammar restriction (the addressed
|
|
@@ -246,8 +237,8 @@ function reflectedCell(cell) {
|
|
|
246
237
|
*/
|
|
247
238
|
export function pillsForSpiderFly(agents, explicitAddresseeId, opts = {}) {
|
|
248
239
|
const { defaultKind = "spider" } = opts;
|
|
249
|
-
const liveSpiders =
|
|
250
|
-
const liveFlies =
|
|
240
|
+
const liveSpiders = liveIdsOfKind(agents, "spider");
|
|
241
|
+
const liveFlies = liveIdsOfKind(agents, "fly");
|
|
251
242
|
|
|
252
243
|
const addressPills = [
|
|
253
244
|
...liveSpiders.map((id) => ({ id, kind: "spider", label: `@${agentPillLabel("spider", id, liveSpiders)}` })),
|
|
@@ -258,7 +249,7 @@ export function pillsForSpiderFly(agents, explicitAddresseeId, opts = {}) {
|
|
|
258
249
|
const addresseeId = (explicitAddresseeId && agents[explicitAddresseeId]) ? explicitAddresseeId : fallbackAddresseeId;
|
|
259
250
|
if (!addresseeId) return { addressPills, claimPills: [], addresseeId: null };
|
|
260
251
|
|
|
261
|
-
const addresseeKind =
|
|
252
|
+
const addresseeKind = agentKindOf(addresseeId);
|
|
262
253
|
const addresseeLabel = agentPillLabel(addresseeKind, addresseeId, addresseeKind === "spider" ? liveSpiders : liveFlies);
|
|
263
254
|
const addresseeCell = parseCellId(agents[addresseeId].cell);
|
|
264
255
|
|
|
@@ -352,10 +343,11 @@ async function runTickAndRender({ planHolder, memoryDir, cache, toldFacts = [],
|
|
|
352
343
|
|
|
353
344
|
/** One `[id, cellId | null]` belief entry as a sentence: `"spider-1 is at
|
|
354
345
|
* cell-3-4."` when observed/told, `"fly-2 has not been observed."`
|
|
355
|
-
* otherwise —
|
|
356
|
-
* (observedFactsHtml) renders
|
|
357
|
-
*
|
|
358
|
-
|
|
346
|
+
* otherwise — exported so spider-fly-viz.mjs's own click-expand panel
|
|
347
|
+
* (observedFactsHtml) renders from the SAME function instead of a
|
|
348
|
+
* hand-typed copy, so the chat phrasing and the browser panel can never
|
|
349
|
+
* drift apart. Pure, self-contained, `.toString()`-splice safe. */
|
|
350
|
+
export function believedFactSentence(id, believedCell) {
|
|
359
351
|
return believedCell ? `${id} is at ${believedCell}.` : `${id} has not been observed.`;
|
|
360
352
|
}
|
|
361
353
|
|
|
@@ -374,14 +366,14 @@ async function spiderFlyBeliefAnswer(match, { memoryDir, gameConfig = DEFAULT_GA
|
|
|
374
366
|
const observerId = resolveAgentId(kind, num, state);
|
|
375
367
|
if (!observerId) return noSuchAgentAnswer(kind, "addressee");
|
|
376
368
|
const observerCell = parseCellId(state.placements.get(observerId).cell);
|
|
377
|
-
const candidateIds = [...liveIdsOfKind("spider", state), ...liveIdsOfKind("fly", state)];
|
|
369
|
+
const candidateIds = [...liveIdsOfKind(state.placements, "spider", state.removed), ...liveIdsOfKind(state.placements, "fly", state.removed)];
|
|
378
370
|
const visionRadius = kind === "spider"
|
|
379
371
|
? gameConfig?.spiderFly?.spiderVisionRadius
|
|
380
372
|
: gameConfig?.spiderFly?.flyVisionRadius;
|
|
381
373
|
const belief = beliefSnapshotFor(observerId, observerCell, candidateIds, state, { visionRadius });
|
|
382
374
|
const entries = Object.entries(belief);
|
|
383
375
|
const text = entries.length
|
|
384
|
-
? `${observerId} sees: ${entries.map(([id, cell]) =>
|
|
376
|
+
? `${observerId} sees: ${entries.map(([id, cell]) => believedFactSentence(id, cell)).join(" ")}`
|
|
385
377
|
: `${observerId} is alone on the board — nothing else to see.`;
|
|
386
378
|
return {
|
|
387
379
|
text,
|
|
@@ -446,7 +438,7 @@ const SF_GOAL_RE = /^(?:what(?:'s|\s+is)\s+(?:the\s+|my\s+)?(?:goal|objective|po
|
|
|
446
438
|
const WATCHER_STANCE = 'you have no piece here — both agents move on their own. Watch, say "tick" to advance, or address one, e.g. "@spider the fly is east".';
|
|
447
439
|
|
|
448
440
|
const positionsOfKind = (kind, state) =>
|
|
449
|
-
liveIdsOfKind(kind, state).map((id) => `${id} at ${state.placements.get(id).cell}`);
|
|
441
|
+
liveIdsOfKind(state.placements, kind, state.removed).map((id) => `${id} at ${state.placements.get(id).cell}`);
|
|
450
442
|
|
|
451
443
|
async function spiderFlyContextAnswer(line, { memoryDir }) {
|
|
452
444
|
const l = String(line).trim();
|