@polycode-projects/the-mechanical-code-talker 5.0.5 → 5.0.7
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 +78 -19
- package/bin/tmct.mjs +63 -2
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +23 -0
- package/src/domain/ask-vocab.mjs +19 -0
- package/src/domain/ask.mjs +10 -5
- package/src/domain/codegraph.mjs +23 -9
- package/src/domain/game-config.mjs +12 -0
- package/src/domain/interpret/strategies/keywords.mjs +30 -1
- package/src/domain/memory/capability.mjs +15 -11
- package/src/domain/router/drive.mjs +36 -17
- package/src/domain/router/resolver.mjs +63 -17
- package/src/domain/spider-fly-world.mjs +2 -2
- package/src/domain/sprite-templates.mjs +19 -7
- package/src/domain/syllogise.mjs +16 -6
- package/src/domain/town-square-world.mjs +1 -1
- package/src/services/adventure-viz.mjs +5 -2
- package/src/services/adventure.mjs +8 -1
- package/src/services/chat-page-viz.mjs +123 -25
- package/src/services/chat-session.mjs +60 -10
- package/src/services/chat.mjs +328 -36
- package/src/services/code-explorer-viz.mjs +3 -2
- package/src/services/extract-facts.mjs +47 -7
- package/src/services/ingest-viz.mjs +113 -29
- package/src/services/ledger-viz.mjs +9 -4
- package/src/services/memory-panel-viz.mjs +44 -0
- package/src/services/mud-viz.mjs +21 -3
- package/src/services/mudiii-scene.mjs +407 -36
- package/src/services/mudiii-turn.mjs +65 -9
- package/src/services/mudiii-viz.mjs +810 -157
- package/src/services/p2p-room.mjs +1 -1
- package/src/services/plan-viz.mjs +26 -4
- package/src/services/predator-prey.mjs +141 -37
- package/src/services/research-viz.mjs +17 -23
- package/src/services/spider-fly-turn.mjs +7 -1
- package/src/services/spider-fly-viz.mjs +13 -5
- package/src/services/sprite-catalog-viz.mjs +3 -2
- package/src/services/viz-theme.mjs +20 -0
- package/src/services/viz-ticker.mjs +15 -2
- package/src/surfaces/http/server-http.mjs +90 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +125 -125
- package/src/surfaces/web/mud-browser-entry.mjs +33 -1
- package/src/surfaces/web/mudiii-browser-entry.mjs +70 -34
- package/src/surfaces/web/tmct-surface.mjs +18 -6
- package/src/tools/handlers/tmct-ask.mjs +15 -2
- package/src/tools/server.mjs +31 -2
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// sidebar/centre grid, so a later panel is one more <section class="panel">,
|
|
20
20
|
// not a re-architecture.
|
|
21
21
|
|
|
22
|
-
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText } from "./viz-theme.mjs";
|
|
22
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, demoEyebrowHtml, EYEBROW_LINKS_CSS } from "./viz-theme.mjs";
|
|
23
23
|
import { generateCodeHints } from "../domain/code-explorer-hints.mjs";
|
|
24
24
|
import { phraseForRelation } from "../domain/ask-vocab.mjs";
|
|
25
25
|
import { fetchWithProgress } from "./memory-panel-viz.mjs";
|
|
@@ -483,6 +483,7 @@ body { margin: 0; overflow: hidden; background: var(--bg); color: var(--ink); fo
|
|
|
483
483
|
.titlebar { display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap; padding: 0.5rem 0.9rem; background: var(--card); border-bottom: 1px solid var(--line); box-shadow: inset 0 -2px 0 var(--entail-soft); }
|
|
484
484
|
.titlebar .mark { width: 15px; height: 15px; color: var(--entail); flex: none; }
|
|
485
485
|
.titlebar h1 { font-size: 0.95rem; margin: 0; font-weight: 600; letter-spacing: 0.02em; }
|
|
486
|
+
${EYEBROW_LINKS_CSS}
|
|
486
487
|
.titlebar .sub { color: var(--muted); font-size: 0.76rem; font-family: ${MONO_STACK}; }
|
|
487
488
|
.titlebar .sub a { color: var(--corpus); }
|
|
488
489
|
/* the fact count sits in the titlebar, not the footer status bar: it is what
|
|
@@ -544,7 +545,7 @@ ul.rows { list-style: none; margin: 0; padding: 0; }
|
|
|
544
545
|
<div class="shell">
|
|
545
546
|
<header class="titlebar">
|
|
546
547
|
<svg class="mark" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M8 0l1 2.3a5.8 5.8 0 0 1 1.9.8L13.3 2l.7.7-1.1 2.4c.4.6.6 1.2.8 1.9L16 8l-2.3 1a5.8 5.8 0 0 1-.8 1.9l1.1 2.4-.7.7-2.4-1.1a5.8 5.8 0 0 1-1.9.8L8 16l-1-2.3a5.8 5.8 0 0 1-1.9-.8L2.7 14l-.7-.7 1.1-2.4a5.8 5.8 0 0 1-.8-1.9L0 8l2.3-1c.2-.7.4-1.3.8-1.9L2 2.7l.7-.7 2.4 1.1A5.8 5.8 0 0 1 7 2.3L8 0zm0 5.2A2.8 2.8 0 1 0 8 10.8 2.8 2.8 0 0 0 8 5.2z"/></svg>
|
|
547
|
-
<h1
|
|
548
|
+
<h1>${demoEyebrowHtml("code", "code explorer")}</h1>
|
|
548
549
|
<span class="sub">source: <span id="source-name">${escapeHtml(sourceName)}</span></span>
|
|
549
550
|
<span class="factpill" id="fact-total" aria-live="polite" title="every fact this page's chat can draw on — the code graph's own edges plus the general-knowledge seed once it lands">
|
|
550
551
|
<span class="factpill-value" id="fact-total-value">—</span> facts loaded
|
|
@@ -92,12 +92,23 @@ export function parseArgs(argv) {
|
|
|
92
92
|
*/
|
|
93
93
|
async function runSentence(sentence, { config, memoryDir }) {
|
|
94
94
|
const before = readFactRows(await loadMemory(memoryDir));
|
|
95
|
-
const { record } = await runTurn(sentence, { config, memoryDir, sessionId: uuidv7() });
|
|
96
|
-
if (record?.via !== "assert" || record?.miss) return { recognized: false, rows: [] };
|
|
95
|
+
const { record, answer } = await runTurn(sentence, { config, memoryDir, sessionId: uuidv7() });
|
|
96
|
+
if (record?.via !== "assert" || record?.miss) return { recognized: false, rows: [], decline: String(answer || "") };
|
|
97
97
|
const after = readFactRows(await loadMemory(memoryDir));
|
|
98
98
|
return { recognized: true, rows: touchedFactRows(before, after) };
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/** The recognizer's own words for why it turned a sentence down, when it named
|
|
102
|
+
* an ungrounded term rather than the sentence's shape. "A wombat is a
|
|
103
|
+
* marsupial." is the same shape as "A kestrel is a bird.", which IS recognized;
|
|
104
|
+
* the difference is that "bird" is in the vocabulary and "marsupial" is not, so
|
|
105
|
+
* reporting every skip as an unrecognized shape blames the wrong thing. */
|
|
106
|
+
const UNGROUNDED_TERM_DECLINE_RE = /I don't recognize ((?:"[^"]+"(?:\s+(?:and|or)\s+)?)+) as (?:a )?words? I know/i;
|
|
107
|
+
const ungroundedTermsIn = (decline) => {
|
|
108
|
+
const m = String(decline || "").match(UNGROUNDED_TERM_DECLINE_RE);
|
|
109
|
+
return m ? m[1].match(/"([^"]+)"/g).map((q) => q.slice(1, -1)) : null;
|
|
110
|
+
};
|
|
111
|
+
|
|
101
112
|
// The copula lemmas that read as class membership; a following noun phrase is
|
|
102
113
|
// the class the subject is-a. "has/have" and other verbs are relations, not isa.
|
|
103
114
|
const OPTIMISTIC_COPULAS = new Set(["is", "are", "was", "were", "be", "been", "being", "am"]);
|
|
@@ -541,11 +552,17 @@ export async function ingestText(text, {
|
|
|
541
552
|
let sentenceCount = 0;
|
|
542
553
|
let recognizedSentences = 0;
|
|
543
554
|
let optimisticSentences = 0;
|
|
555
|
+
// The terms a skipped sentence named that nothing grounds yet. Reported with
|
|
556
|
+
// the skip count so the summary names the real obstacle instead of blaming
|
|
557
|
+
// the sentence's shape for a shape it actually recognizes.
|
|
558
|
+
const ungroundedTerms = new Set();
|
|
544
559
|
|
|
545
560
|
// One recognized read of some text form: null when the strict recognizer
|
|
546
561
|
// grounds nothing, else the Fact rows it touched.
|
|
562
|
+
let lastDecline = "";
|
|
547
563
|
const strictRows = async (form) => {
|
|
548
|
-
const { recognized, rows } = await runSentence(form, { config: cfg, memoryDir: dir });
|
|
564
|
+
const { recognized, rows, decline } = await runSentence(form, { config: cfg, memoryDir: dir });
|
|
565
|
+
if (!recognized) lastDecline = decline || lastDecline;
|
|
549
566
|
return recognized && rows.length ? rows : null;
|
|
550
567
|
};
|
|
551
568
|
|
|
@@ -557,6 +574,7 @@ export async function ingestText(text, {
|
|
|
557
574
|
let carrySubject = null;
|
|
558
575
|
for (const sentence of splitSentencesPreservingPaths(paragraph)) {
|
|
559
576
|
sentenceCount += 1;
|
|
577
|
+
lastDecline = "";
|
|
560
578
|
const cleaned = stripCitationResidue(sentence);
|
|
561
579
|
// Whole sentence first, then each closed-marker clause as a fallback.
|
|
562
580
|
let rows = null;
|
|
@@ -592,6 +610,8 @@ export async function ingestText(text, {
|
|
|
592
610
|
}
|
|
593
611
|
continue;
|
|
594
612
|
}
|
|
613
|
+
const ungrounded = ungroundedTermsIn(lastDecline);
|
|
614
|
+
if (ungrounded) for (const term of ungrounded) ungroundedTerms.add(term);
|
|
595
615
|
if (!optimistic) continue;
|
|
596
616
|
const candidates = optimisticTriples(cleaned, { lexicon: lex, nlp });
|
|
597
617
|
if (!candidates.length) continue;
|
|
@@ -612,6 +632,7 @@ export async function ingestText(text, {
|
|
|
612
632
|
extracted,
|
|
613
633
|
optimistic: optimisticFacts,
|
|
614
634
|
skipped: sentenceCount - recognizedSentences - optimisticSentences,
|
|
635
|
+
ungroundedTerms: [...ungroundedTerms],
|
|
615
636
|
};
|
|
616
637
|
if (canonical) {
|
|
617
638
|
result.canonical = canonicalLines([...extracted, ...optimisticFacts], readFactRows(await loadMemory(dir)));
|
|
@@ -641,9 +662,23 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
641
662
|
const filePath = resolve(process.cwd(), file);
|
|
642
663
|
const text = await readFile(filePath, "utf8");
|
|
643
664
|
const sourceTag = basename(filePath);
|
|
644
|
-
|
|
665
|
+
// A repo path is not a store handle. Resolve the SAME backend every other verb
|
|
666
|
+
// reads back through, or the facts land in the retired flat file and the
|
|
667
|
+
// "facts written into …" line below reports a write chat can never see.
|
|
668
|
+
const repoRoot = repo ? resolve(process.cwd(), repo) : null;
|
|
669
|
+
const { openConfiguredMemoryBackend } = await import("../adapters/memory/core.mjs");
|
|
670
|
+
const store = repoRoot ? await openConfiguredMemoryBackend(repoRoot) : null;
|
|
645
671
|
|
|
646
|
-
|
|
672
|
+
let result;
|
|
673
|
+
try {
|
|
674
|
+
result = await ingestText(text, {
|
|
675
|
+
memoryDir: store ? store.dir : null,
|
|
676
|
+
config: repoRoot ? loadConfig(process.env, repoRoot) : null,
|
|
677
|
+
sourceTag, optimistic, canonical,
|
|
678
|
+
});
|
|
679
|
+
} finally {
|
|
680
|
+
if (store) await store.close();
|
|
681
|
+
}
|
|
647
682
|
const emitted = optimistic ? [...result.extracted, ...result.optimistic] : result.extracted;
|
|
648
683
|
|
|
649
684
|
if (out) {
|
|
@@ -663,9 +698,14 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
663
698
|
+ `(${result.extracted.length} fact row${result.extracted.length === 1 ? "" : "s"})`
|
|
664
699
|
+ (optimistic ? `, ${optimisticCount} optimistic candidate${optimisticCount === 1 ? "" : "s"}` : "")
|
|
665
700
|
+ `, ${result.skipped} skipped — not a recognized declarative shape (an honest, expected gap; this is `
|
|
666
|
-
+ `an attempt, not full NLU)
|
|
701
|
+
+ `an attempt, not full NLU).`
|
|
702
|
+
+ (result.ungroundedTerms?.length
|
|
703
|
+
? `\nSome of those skips were shapes I do read, held up by terms nothing grounds yet: `
|
|
704
|
+
+ `${result.ungroundedTerms.map((t) => `"${t}"`).join(", ")}. `
|
|
705
|
+
+ `Ground one side first (e.g. "every ${result.ungroundedTerms[0]} is a thing") and re-run.`
|
|
706
|
+
: ""),
|
|
667
707
|
);
|
|
668
|
-
if (repo) console.error(`facts written into ${
|
|
708
|
+
if (repo) console.error(`facts written into ${repoRoot}'s tmct memory, tagged ${sourceTag}`);
|
|
669
709
|
if (out) console.error(`facts written to ${out}`);
|
|
670
710
|
return { sentences, recognized, extracted: result.extracted, optimistic: result.optimistic, skipped: result.skipped };
|
|
671
711
|
}
|
|
@@ -25,12 +25,13 @@
|
|
|
25
25
|
// renderIngestHtml() is pure: no I/O, deterministic output for identical
|
|
26
26
|
// input. scripts/build-demo-site.mjs calls it directly and writes the result
|
|
27
27
|
// to public/ingest.html, after ingest-browser.bundle.js already exists.
|
|
28
|
-
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
|
|
28
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, demoEyebrowHtml, EYEBROW_LINKS_CSS } from "./viz-theme.mjs";
|
|
29
29
|
import {
|
|
30
30
|
bandLabelFor,
|
|
31
31
|
statsSummaryLine,
|
|
32
32
|
clearSiteAssetCaches,
|
|
33
33
|
fetchWithProgress,
|
|
34
|
+
loadSeedPayload,
|
|
34
35
|
renderStatsPanelInto,
|
|
35
36
|
loadProgressLine,
|
|
36
37
|
factTripleParts,
|
|
@@ -43,7 +44,7 @@ const DEFAULT_TITLE = "the-mechanical-code-talker — ingest";
|
|
|
43
44
|
/** The self-contained ingest page. Pure — the same output for the same
|
|
44
45
|
* `title` every time; every piece of state (the session, each grounded fact)
|
|
45
46
|
* is computed live in the browser once the sibling ingest bundle loads. */
|
|
46
|
-
export function renderIngestHtml({ title = DEFAULT_TITLE, seedStamp = "" } = {}) {
|
|
47
|
+
export function renderIngestHtml({ title = DEFAULT_TITLE, seedStamp = "", seedBytes = 0 } = {}) {
|
|
47
48
|
return `<!doctype html>
|
|
48
49
|
<html lang="en">
|
|
49
50
|
<head>
|
|
@@ -75,7 +76,8 @@ ${THEME_TOKENS_CSS}
|
|
|
75
76
|
header.topbar { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: .7rem 1.1rem; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
|
|
76
77
|
.brand { display: flex; flex-direction: column; gap: .1rem; }
|
|
77
78
|
.eyebrow { font-family: ${MONO_STACK}; font-size: .78rem; letter-spacing: .08em; color: var(--muted); }
|
|
78
|
-
|
|
79
|
+
${EYEBROW_LINKS_CSS}
|
|
80
|
+
.subtitle { margin: 0; font-size: .82rem; font-weight: 400; color: var(--muted); }
|
|
79
81
|
|
|
80
82
|
/* the live memory count, in the topbar rather than the status line: it is
|
|
81
83
|
the one number that says what this page's memory actually holds, and the
|
|
@@ -83,6 +85,14 @@ ${THEME_TOKENS_CSS}
|
|
|
83
85
|
.topbar-right { display: flex; align-items: center; gap: .7rem; }
|
|
84
86
|
.fact-pill { display: inline-flex; align-items: baseline; gap: .34rem; font-family: ${MONO_STACK}; font-size: .66rem; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); border: 1px solid var(--corpus-t1); border-radius: 99px; padding: .2rem .8rem; background: var(--corpus-soft); white-space: nowrap; }
|
|
85
87
|
.fact-pill .fact-pill-value { font-size: .94rem; letter-spacing: 0; font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ink); }
|
|
88
|
+
/* While the starter memory streams in, the pill says so and breathes — the
|
|
89
|
+
same three-state pill chat.html carries, for the same reason: what this
|
|
90
|
+
page knows grows while you watch it. */
|
|
91
|
+
.fact-pill[data-state="loading"] { animation: pill-breathe 1.6s ease-in-out infinite; }
|
|
92
|
+
.fact-pill[data-state="loading"] .fact-pill-value { font-size: .66rem; letter-spacing: .06em; text-transform: uppercase; font-weight: 600; }
|
|
93
|
+
.fact-pill[data-state="failed"] { border-color: var(--miss-t1, var(--corpus-t1)); }
|
|
94
|
+
@keyframes pill-breathe { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
|
|
95
|
+
@media (prefers-reduced-motion: reduce) { .fact-pill[data-state="loading"] { animation: none; } }
|
|
86
96
|
|
|
87
97
|
/* mode pills — Text | Document — the translate-tool idiom: two segments in
|
|
88
98
|
one rounded track, the active one filled. */
|
|
@@ -181,13 +191,13 @@ ${THEME_TOKENS_CSS}
|
|
|
181
191
|
<div class="ingestCol">
|
|
182
192
|
<header class="topbar">
|
|
183
193
|
<div class="brand">
|
|
184
|
-
<span class="eyebrow"
|
|
185
|
-
<
|
|
194
|
+
<span class="eyebrow">${demoEyebrowHtml("ingest", "ingest")}</span>
|
|
195
|
+
<h1 class="subtitle">ingest — paste or drop text. It keeps the facts it can ground and skips the rest.</h1>
|
|
186
196
|
</div>
|
|
187
197
|
<div class="topbar-right">
|
|
188
|
-
<span class="fact-pill" id="factPill" aria-live="polite"
|
|
198
|
+
<span class="fact-pill" id="factPill" aria-live="polite" data-state="loading"
|
|
189
199
|
title="every fact this page's memory holds right now — the starter memory it booted with plus everything it has grounded since">
|
|
190
|
-
<span class="fact-pill-value" id="factPillValue"
|
|
200
|
+
<span class="fact-pill-value" id="factPillValue">loading</span><span id="factPillUnit"> starter memory…</span>
|
|
191
201
|
</span>
|
|
192
202
|
<div class="pills" role="group" aria-label="Input mode">
|
|
193
203
|
<button type="button" id="modeText" aria-pressed="true">Text</button>
|
|
@@ -253,6 +263,7 @@ ${THEME_TOKENS_CSS}
|
|
|
253
263
|
const statsSummaryLine = ${statsSummaryLine.toString()};
|
|
254
264
|
const clearSiteAssetCaches = ${clearSiteAssetCaches.toString()};
|
|
255
265
|
const fetchWithProgress = ${fetchWithProgress.toString()};
|
|
266
|
+
const loadSeedPayload = ${loadSeedPayload.toString()};
|
|
256
267
|
const renderStatsPanelInto = ${renderStatsPanelInto.toString()};
|
|
257
268
|
const el = (id) => document.getElementById(id);
|
|
258
269
|
|
|
@@ -274,7 +285,9 @@ ${THEME_TOKENS_CSS}
|
|
|
274
285
|
const seedToggleEl = el("seedToggle");
|
|
275
286
|
const fuzzyToggleEl = el("fuzzyToggle");
|
|
276
287
|
const statsPanelEl = el("statsPanel");
|
|
288
|
+
const factPillEl = el("factPill");
|
|
277
289
|
const factPillValueEl = el("factPillValue");
|
|
290
|
+
const factPillUnitEl = el("factPillUnit");
|
|
278
291
|
const askFormEl = el("askForm");
|
|
279
292
|
const askInputEl = el("askq");
|
|
280
293
|
const askGoBtn = el("askGo");
|
|
@@ -469,7 +482,8 @@ ${THEME_TOKENS_CSS}
|
|
|
469
482
|
try { stats = await window.tmct.page.memoryStats(session.memoryDir); }
|
|
470
483
|
catch { return; }
|
|
471
484
|
}
|
|
472
|
-
|
|
485
|
+
lastStatsTotal = Number(stats.total || 0);
|
|
486
|
+
renderFactPill();
|
|
473
487
|
renderStatsPanelInto(statsPanelEl, stats, {
|
|
474
488
|
bandLabel: bandLabelFor,
|
|
475
489
|
taughtHint: "nothing yet. Ingest some text and its grounded facts land here, with their source.",
|
|
@@ -495,6 +509,12 @@ ${THEME_TOKENS_CSS}
|
|
|
495
509
|
// ever return the copy this page asked for. Empty in a build with no seed.
|
|
496
510
|
const SEED_STAMP = ${JSON.stringify(seedStamp)};
|
|
497
511
|
const SEED_QUERY = SEED_STAMP ? "?b=" + SEED_STAMP : "";
|
|
512
|
+
// The builder's own measurement of chat-seed.json, in decompressed bytes.
|
|
513
|
+
// The response cannot supply this: over a compressed transfer Content-Length
|
|
514
|
+
// is the wire size while the stream yields decompressed bytes, so the two
|
|
515
|
+
// are not the same quantity. Both numbers in the percentage below are
|
|
516
|
+
// decompressed bytes, which is what makes it a real fraction.
|
|
517
|
+
const SEED_BYTES = ${Number(seedBytes) || 0};
|
|
498
518
|
|
|
499
519
|
let seedPayload = null;
|
|
500
520
|
let seedFacts = 0;
|
|
@@ -508,29 +528,89 @@ ${THEME_TOKENS_CSS}
|
|
|
508
528
|
// The one branch this page's seed choice makes: checked, fetch and parse
|
|
509
529
|
// the same chat-seed.json chat.html embeds; unchecked, skip the request
|
|
510
530
|
// outright and stay on the previous empty-store fast path.
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
531
|
+
let seedLoadedBytes = 0;
|
|
532
|
+
let seedTotalBytes = SEED_BYTES;
|
|
533
|
+
let seedPercentShown = -1;
|
|
534
|
+
let lastStatsTotal = null;
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* What the starter memory is doing right now, published as tmct.seed —
|
|
538
|
+
* loading, indexing, ready, failed, or skipped when the seed toggle is off.
|
|
539
|
+
* The same five phases chat.html publishes, for the same reasons: the pill
|
|
540
|
+
* can say the memory is still growing, and a test that asserts on seeded
|
|
541
|
+
* content can wait for "ready" instead of guessing.
|
|
542
|
+
*/
|
|
543
|
+
function setSeedPhase(phase, extra) {
|
|
544
|
+
window.tmct.seed = Object.assign({ state: phase, facts: seedFacts }, extra || {});
|
|
545
|
+
renderFactPill();
|
|
546
|
+
}
|
|
547
|
+
setSeedPhase("loading");
|
|
548
|
+
|
|
549
|
+
function noteSeedBytes(loaded, total) {
|
|
550
|
+
seedLoadedBytes = loaded;
|
|
551
|
+
seedTotalBytes = total;
|
|
552
|
+
const percent = seedProgressPercent();
|
|
553
|
+
if (percent === seedPercentShown) return;
|
|
554
|
+
seedPercentShown = percent;
|
|
555
|
+
renderFactPill();
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Real progress, 0-100, or null when no true total is known — the pill then
|
|
559
|
+
* shows that it is loading and no number, never a figure inferred from how
|
|
560
|
+
* long it has been going. */
|
|
561
|
+
function seedProgressPercent() {
|
|
562
|
+
if (!(seedTotalBytes > 0)) return null;
|
|
563
|
+
return Math.min(100, Math.floor((seedLoadedBytes / seedTotalBytes) * 100));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function renderFactPill() {
|
|
567
|
+
const seedState = window.tmct.seed ? window.tmct.seed.state : "loading";
|
|
568
|
+
if (seedState === "loading") {
|
|
569
|
+
const percent = seedProgressPercent();
|
|
570
|
+
factPillEl.dataset.state = "loading";
|
|
571
|
+
factPillValueEl.textContent = percent === null ? "loading" : percent + "%";
|
|
572
|
+
factPillUnitEl.textContent = percent === null ? " starter memory\\u2026" : " of starter memory";
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
// "ready" lands a moment before the first memoryStats read returns, and
|
|
576
|
+
// there is no count to show until it does. Holding the indexing words over
|
|
577
|
+
// that gap keeps the pill from flashing a zero it does not mean.
|
|
578
|
+
if (seedState === "indexing" || (seedState === "ready" && lastStatsTotal === null)) {
|
|
579
|
+
factPillEl.dataset.state = "loading";
|
|
580
|
+
factPillValueEl.textContent = "indexing";
|
|
581
|
+
factPillUnitEl.textContent = " " + seedFacts.toLocaleString() + " facts\\u2026";
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (seedState === "failed" && lastStatsTotal === null) {
|
|
585
|
+
factPillEl.dataset.state = "failed";
|
|
586
|
+
factPillValueEl.textContent = "no";
|
|
587
|
+
factPillUnitEl.textContent = " starter memory";
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
factPillEl.dataset.state = seedState === "failed" ? "failed" : "ready";
|
|
591
|
+
factPillValueEl.textContent = Number(lastStatsTotal || 0).toLocaleString();
|
|
592
|
+
factPillUnitEl.textContent = " facts in memory";
|
|
593
|
+
}
|
|
594
|
+
|
|
517
595
|
async function fetchSeedIfWanted() {
|
|
518
|
-
if (!seedToggleEl.checked) {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
596
|
+
if (!seedToggleEl.checked) {
|
|
597
|
+
seedPayload = null;
|
|
598
|
+
seedFacts = 0;
|
|
599
|
+
setSeedPhase("skipped");
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const outcome = await loadSeedPayload(fetchWithProgress, "./chat-seed.json", SEED_QUERY, (loaded, total) => {
|
|
603
|
+
noteProgress("seed", loaded, total || SEED_BYTES);
|
|
604
|
+
noteSeedBytes(loaded, total || SEED_BYTES);
|
|
605
|
+
});
|
|
606
|
+
seedPayload = outcome.payload;
|
|
607
|
+
seedFacts = outcome.status.facts;
|
|
608
|
+
if (outcome.status.state === "failed") {
|
|
609
|
+
console.error("tmct ingest: chat-seed.json unavailable — starting unseeded (" + outcome.status.error + ")");
|
|
610
|
+
setSeedPhase("failed", { error: outcome.status.error, attempts: outcome.status.attempts });
|
|
611
|
+
return;
|
|
533
612
|
}
|
|
613
|
+
setSeedPhase("indexing", { attempts: outcome.status.attempts });
|
|
534
614
|
}
|
|
535
615
|
const cloneMemoryPayload = ${cloneMemoryPayload.toString()};
|
|
536
616
|
async function newSession() {
|
|
@@ -609,6 +689,7 @@ ${THEME_TOKENS_CSS}
|
|
|
609
689
|
session = await newSession();
|
|
610
690
|
clearFactsPane();
|
|
611
691
|
askLogEl.textContent = "";
|
|
692
|
+
if (window.tmct.seed.state === "indexing") setSeedPhase("ready");
|
|
612
693
|
const stats = await window.tmct.page.memoryStats(session.memoryDir);
|
|
613
694
|
statusEl.textContent = statsSummaryLine(stats, bandLabelFor) + ". Ready.";
|
|
614
695
|
await renderStatsPanel(stats);
|
|
@@ -639,6 +720,9 @@ ${THEME_TOKENS_CSS}
|
|
|
639
720
|
setMode(false);
|
|
640
721
|
updateIngestEnabled();
|
|
641
722
|
updateAskEnabled();
|
|
723
|
+
// The session holds the seed now, so the phase settles here rather than
|
|
724
|
+
// when the bytes landed: "ready" means queryable.
|
|
725
|
+
if (window.tmct.seed.state === "indexing") setSeedPhase("ready");
|
|
642
726
|
const stats = await window.tmct.page.memoryStats(session.memoryDir);
|
|
643
727
|
const winkPart = winkStatus === "loaded"
|
|
644
728
|
? "wink-nlp: loaded"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// doesn't do: it reads the checked-in chat-dock engine bundle.
|
|
16
16
|
|
|
17
17
|
import { loadMemory, readFactRows, findContradictions, normFactTerm } from "../adapters/memory/core.mjs";
|
|
18
|
-
import { THEME_TOKENS_CSS, MONO_STACK, escapeHtml, embedJson, countLabel, TOKENS } from "./viz-theme.mjs";
|
|
18
|
+
import { THEME_TOKENS_CSS, MONO_STACK, escapeHtml, embedJson, countLabel, TOKENS, demoEyebrowHtml, EYEBROW_LINKS_CSS } from "./viz-theme.mjs";
|
|
19
19
|
import { createTicker, prefersReducedMotion } from "./viz-ticker.mjs";
|
|
20
20
|
import { bfsLevels } from "../domain/planning.mjs";
|
|
21
21
|
import { pluralOf } from "../domain/inflect.mjs";
|
|
@@ -644,7 +644,9 @@ ${DASH_DARK_CHROME_CSS}
|
|
|
644
644
|
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${DASH_SANS_STACK}; font-size: 15px; line-height: 1.5; }
|
|
645
645
|
.mono { font-family: ${MONO_STACK}; }
|
|
646
646
|
main { max-width: 1200px; margin: 0 auto; padding: 1.4rem 1.2rem 3rem; }
|
|
647
|
+
.visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
|
647
648
|
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); display: flex; flex-wrap: wrap; gap: .4em 1.2em; margin-bottom: .9rem; }
|
|
649
|
+
${EYEBROW_LINKS_CSS}
|
|
648
650
|
button { font: inherit; color: inherit; background: none; border: none; padding: 0; cursor: pointer; }
|
|
649
651
|
button:focus-visible, input:focus-visible { outline: 2px solid var(--corpus); outline-offset: 2px; border-radius: 4px; }
|
|
650
652
|
.topbar { display: flex; flex-wrap: wrap; align-items: center; gap: .8rem; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: .5rem .7rem; margin-bottom: 1.1rem; }
|
|
@@ -789,7 +791,8 @@ ${DASH_DARK_CHROME_CSS}
|
|
|
789
791
|
</head>
|
|
790
792
|
<body>
|
|
791
793
|
<main>
|
|
792
|
-
<
|
|
794
|
+
<h1 class="visually-hidden">memory ledger</h1>
|
|
795
|
+
<div class="eyebrow"><span>${demoEyebrowHtml("ledger", "memory ledger")}</span><span id="counts"></span></div>
|
|
793
796
|
${dashboardHtml(stats)}
|
|
794
797
|
<div class="topbar">
|
|
795
798
|
<nav class="crumbs" id="crumbs" aria-label="Focus trail"></nav>
|
|
@@ -1234,7 +1237,9 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
|
|
|
1234
1237
|
// hold it), and resolveAnsweredTerm has no way to tell that
|
|
1235
1238
|
// apart from the term genuinely being discussed.
|
|
1236
1239
|
const hit = resolveAnsweredTerm(result.answer, q, LEDGER.terms, tmct.page.normFactTerm);
|
|
1237
|
-
|
|
1240
|
+
// No label: the breadcrumb falls back to the term itself
|
|
1241
|
+
// (c.label || c.term) rather than showing the whole question.
|
|
1242
|
+
if (hit) refocusWithLabel(hit);
|
|
1238
1243
|
}
|
|
1239
1244
|
} catch {
|
|
1240
1245
|
pending.className = "a miss";
|
|
@@ -1437,7 +1442,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
|
|
|
1437
1442
|
// full-stop-terminated, rendered only when the engine deduced one.
|
|
1438
1443
|
if (data && data.goal) addLine("a goal", "Goal (inferred): " + esc(data.goal.charAt(0).toUpperCase() + data.goal.slice(1)) + ".");
|
|
1439
1444
|
const hit = resolveAnsweredTerm(answer, q, LEDGER.terms, tmct.page.normFactTerm);
|
|
1440
|
-
if (hit) refocusWithLabel(hit
|
|
1445
|
+
if (hit) refocusWithLabel(hit);
|
|
1441
1446
|
} else {
|
|
1442
1447
|
const tips = LEDGER.terms.filter((t) => t.term.length >= 3).slice(0, 2)
|
|
1443
1448
|
.map((t) => '"what is ' + esc(t.term) + '"').join(" \\u00b7 ");
|
|
@@ -104,6 +104,50 @@ export async function fetchWithProgress(url, onProgress) {
|
|
|
104
104
|
return new Blob(chunks);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Fetch and parse the starter-memory seed, and say what actually happened.
|
|
109
|
+
*
|
|
110
|
+
* Resolves to `{ payload, status }` and never rejects. `payload` is the parsed
|
|
111
|
+
* seed, or null. `status` is the small, payload-free record a page publishes
|
|
112
|
+
* as `tmct.seed` and a test reads back:
|
|
113
|
+
*
|
|
114
|
+
* { state: "loaded", facts, attempts }
|
|
115
|
+
* { state: "failed", facts: 0, attempts, error }
|
|
116
|
+
*
|
|
117
|
+
* The record exists because a seed that failed to download and a store that is
|
|
118
|
+
* empty on purpose both leave the fact count at zero, and until now nothing
|
|
119
|
+
* downstream could tell them apart — not the page, which went on answering as
|
|
120
|
+
* a normal empty chat, and not a test, which could only report the zero. A
|
|
121
|
+
* page that cannot ground an answer must be able to say WHY it cannot, and
|
|
122
|
+
* that starts with knowing.
|
|
123
|
+
*
|
|
124
|
+
* It resolves rather than throwing because a failed seed is not a failed boot:
|
|
125
|
+
* you can still teach the page facts of your own. Boot carries on, and the
|
|
126
|
+
* caller decides what to show.
|
|
127
|
+
*
|
|
128
|
+
* Two attempts, the second under a cache-busting query param: a CDN edge can
|
|
129
|
+
* serve a corrupt, truncated or wrongly-encoded response, and a throw out of
|
|
130
|
+
* the read or the parse is the only signal of that. `seedQuery` already
|
|
131
|
+
* carries the build's own content-hash query string, if any, so the bust param
|
|
132
|
+
* joins with `&` or `?` accordingly.
|
|
133
|
+
*/
|
|
134
|
+
export async function loadSeedPayload(fetcher, url, seedQuery, onProgress) {
|
|
135
|
+
let lastError = null;
|
|
136
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
137
|
+
try {
|
|
138
|
+
const bust = attempt === 1 ? "" : (seedQuery ? "&" : "?") + "retry=1";
|
|
139
|
+
const blob = await fetcher(url + seedQuery + bust, onProgress);
|
|
140
|
+
const payload = JSON.parse(await blob.text());
|
|
141
|
+
const facts = (payload.individuals || []).filter((i) => i.class === "Fact").length;
|
|
142
|
+
return { payload, status: { state: "loaded", facts, attempts: attempt } };
|
|
143
|
+
} catch (err) {
|
|
144
|
+
lastError = err;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const reason = lastError && lastError.message ? lastError.message : String(lastError);
|
|
148
|
+
return { payload: null, status: { state: "failed", facts: 0, attempts: 2, error: reason } };
|
|
149
|
+
}
|
|
150
|
+
|
|
107
151
|
/**
|
|
108
152
|
* (Re)render the docked "this session's memory" panel into `panelEl` from a
|
|
109
153
|
* memoryStats() result: a total-facts row, one row per seed band the session
|
package/src/services/mud-viz.mjs
CHANGED
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
// actually executed, never a race between panes.
|
|
77
77
|
import {
|
|
78
78
|
THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, scenarioLabel,
|
|
79
|
-
wordBeforeCursor, rowsForWorld, appendLogLine,
|
|
79
|
+
wordBeforeCursor, rowsForWorld, appendLogLine, demoEyebrowHtml, EYEBROW_LINKS_CSS,
|
|
80
80
|
} from "./viz-theme.mjs";
|
|
81
81
|
import { createTicker, createSerialQueue } from "./viz-ticker.mjs";
|
|
82
82
|
import { directedGridLayout, roomGraphSvg, levelsOf, EXIT_DELTA } from "./viz-room-graph.mjs";
|
|
@@ -392,7 +392,7 @@ ${MUD_SHARE_SKIN}
|
|
|
392
392
|
<body>
|
|
393
393
|
<main>
|
|
394
394
|
<header class="mud-topbar">
|
|
395
|
-
<h1 class="eyebrow"
|
|
395
|
+
<h1 class="eyebrow">${demoEyebrowHtml("mud", "mud")}</h1>
|
|
396
396
|
<div class="mud-topbar-actions">
|
|
397
397
|
<button type="button" class="state-pill" id="statePill" aria-expanded="false" aria-controls="netPanel"
|
|
398
398
|
title="whether this burrow is shared with anyone"><i class="state-dot"></i><span id="statePillWord">not shared</span></button>
|
|
@@ -406,12 +406,13 @@ ${MUD_SHARE_SKIN}
|
|
|
406
406
|
<section class="deck" aria-label="simulation controls">
|
|
407
407
|
<div class="deck-controls">
|
|
408
408
|
<button type="button" class="deck-play" id="autoToggle" aria-pressed="false">▶ play</button>
|
|
409
|
+
<button type="button" id="stepBtn">step</button>
|
|
409
410
|
<button type="button" id="resetBtn">reset</button>
|
|
410
411
|
${scenarioList.length > 1 ? ` <select id="scenarioSelect" class="deck-select" aria-label="which burrow to play">
|
|
411
412
|
${scenarioList.map((s, i) => ` <option value="${i}"${i === 0 ? " selected" : ""}>${escapeHtml(s.label || scenarioLabel(s.worldPayload?.name))}</option>`).join("\n")}
|
|
412
413
|
</select>` : ""}
|
|
413
414
|
<button type="button" id="editModeBtn" aria-pressed="false">edit</button>
|
|
414
|
-
<label class="deck-teach" title="With this on, a sentence like "
|
|
415
|
+
<label class="deck-teach" title="With this on, a sentence like "Pebble lies in the garden." writes a fact into the world instead of running as a command.">
|
|
415
416
|
<input type="checkbox" id="teachToggle">
|
|
416
417
|
teach
|
|
417
418
|
</label>
|
|
@@ -590,6 +591,7 @@ const MUD_STYLE = `
|
|
|
590
591
|
.mono { font-family: ${MONO_STACK}; }
|
|
591
592
|
main { max-width: 1280px; margin: 0 auto; padding: 1.1rem 1.2rem 2.4rem; }
|
|
592
593
|
.eyebrow { font-family: ${MONO_STACK}; font-weight: 500; font-size: .72rem; letter-spacing: .16em; text-transform: uppercase; color: var(--parchment); opacity: .9; margin: 0 0 .8rem; }
|
|
594
|
+
${EYEBROW_LINKS_CSS}
|
|
593
595
|
|
|
594
596
|
/* ---- the header: brand on the left, the sharing chrome on the right ----
|
|
595
597
|
The same arrangement chat.html's topbar holds — sharing is page chrome,
|
|
@@ -2091,6 +2093,7 @@ function pageScript() {
|
|
|
2091
2093
|
// that could start one is disabled for as long as the editor is open.
|
|
2092
2094
|
function setPlayControlsEnabled(on) {
|
|
2093
2095
|
el("autoToggle").disabled = !on;
|
|
2096
|
+
el("stepBtn").disabled = !on || autoOn;
|
|
2094
2097
|
el("playerCountSlider").disabled = !on;
|
|
2095
2098
|
el("resetBtn").disabled = !on;
|
|
2096
2099
|
for (const character of cast) {
|
|
@@ -2888,6 +2891,7 @@ function pageScript() {
|
|
|
2888
2891
|
|
|
2889
2892
|
function wireDeck() {
|
|
2890
2893
|
const playBtn = el("autoToggle");
|
|
2894
|
+
const stepBtn = el("stepBtn");
|
|
2891
2895
|
const delaySlider = el("delaySlider");
|
|
2892
2896
|
const maxTurnsSlider = el("maxTurnsSlider");
|
|
2893
2897
|
const playerCountSlider = el("playerCountSlider");
|
|
@@ -2897,6 +2901,7 @@ function pageScript() {
|
|
|
2897
2901
|
autoOn = !autoOn;
|
|
2898
2902
|
playBtn.setAttribute("aria-pressed", autoOn ? "true" : "false");
|
|
2899
2903
|
playBtn.textContent = autoOn ? "\\u23F8 pause" : "\\u25B6 play";
|
|
2904
|
+
stepBtn.disabled = autoOn;
|
|
2900
2905
|
// Every animal in the world, pane or none: the deck's play control is the
|
|
2901
2906
|
// world running, and an npc has no other control that could start it.
|
|
2902
2907
|
for (const character of everyone()) {
|
|
@@ -2905,6 +2910,19 @@ function pageScript() {
|
|
|
2905
2910
|
if (autoOn) ticker.play(); else ticker.pause();
|
|
2906
2911
|
}
|
|
2907
2912
|
});
|
|
2913
|
+
// One whole turn: every animal in the world (pane or none, same "everyone"
|
|
2914
|
+
// the play toggle above drives) takes exactly one tick, so every pane's own
|
|
2915
|
+
// "turn N" reading is one higher once every stepOnce() below has settled.
|
|
2916
|
+
// Each character keeps its own independent tick count (mud has no single
|
|
2917
|
+
// shared "round" the way adventure/spider-fly's one-ticker world does), so
|
|
2918
|
+
// stepping the whole deck is stepping every one of those counters once,
|
|
2919
|
+
// not advancing one shared counter by one.
|
|
2920
|
+
stepBtn.addEventListener("click", function () {
|
|
2921
|
+
if (!session || autoOn) return;
|
|
2922
|
+
stepBtn.disabled = true;
|
|
2923
|
+
const steps = everyone().map(function (character) { return ensureTicker(character).stepOnce(); });
|
|
2924
|
+
Promise.all(steps).finally(function () { stepBtn.disabled = autoOn; });
|
|
2925
|
+
});
|
|
2908
2926
|
delaySlider.addEventListener("input", function () {
|
|
2909
2927
|
delayMs = Number(delaySlider.value);
|
|
2910
2928
|
el("delayValue").textContent = delayMs + "ms";
|