@polycode-projects/the-mechanical-code-talker 4.1.1 → 4.1.3
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 +31 -18
- package/bin/tmct.mjs +3 -0
- package/data/templates/responses.jsonl +3 -0
- package/package.json +2 -1
- package/src/adapters/memory/core.mjs +1358 -196
- package/src/adapters/memory/inspect.mjs +11 -0
- package/src/adapters/memory/shacl.mjs +38 -0
- package/src/adapters/p2p/webrtc-transport.mjs +28 -5
- package/src/domain/ask-vocab.mjs +39 -0
- package/src/domain/ask.mjs +183 -34
- package/src/domain/grammar/assert.mjs +8 -2
- package/src/domain/hanoi-board.mjs +232 -0
- package/src/domain/ingest-facts.mjs +120 -0
- package/src/domain/interpret/normalize.mjs +49 -0
- package/src/domain/memory/compaction.mjs +284 -0
- package/src/domain/memory/resolution.mjs +171 -0
- package/src/domain/memory/trust.mjs +175 -5
- package/src/domain/memory-facts.mjs +139 -0
- package/src/domain/p2p/facts.mjs +21 -0
- package/src/domain/p2p/peer-id.mjs +15 -0
- package/src/domain/p2p/provenance-relabel.mjs +13 -2
- package/src/domain/p2p/sync-filter.mjs +5 -1
- package/src/domain/p2p/wire.mjs +7 -4
- package/src/domain/scene-compose.mjs +2 -2
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/services/adventure-viz.mjs +5 -1
- package/src/services/adventure.mjs +70 -44
- package/src/services/chat-page-viz.mjs +381 -310
- package/src/services/chat.mjs +273 -155
- package/src/services/code-explorer-viz.mjs +141 -54
- package/src/services/index.mjs +1 -1
- package/src/services/ingest-viz.mjs +134 -9
- package/src/services/ledger-viz.mjs +7 -4
- package/src/services/memory-panel-viz.mjs +8 -3
- package/src/services/mud-turn.mjs +11 -8
- package/src/services/mud-viz.mjs +441 -206
- package/src/services/p2p-room.mjs +110 -23
- package/src/services/plan-viz.mjs +63 -4
- package/src/services/research-viz.mjs +18 -7
- package/src/services/share-overlay-viz.mjs +623 -0
- package/src/services/spider-fly-viz.mjs +2 -2
- package/src/services/sprite-catalog-viz.mjs +303 -78
- package/src/surfaces/web/adventure-browser-entry.mjs +27 -5
- package/src/surfaces/web/chat-browser-entry.mjs +37 -10
- package/src/surfaces/web/code-explorer-browser-entry.mjs +4 -3
- package/src/surfaces/web/ingest-browser-entry.mjs +73 -12
- package/src/surfaces/web/ledger-browser-entry.mjs +32 -7
- package/src/surfaces/web/memory-ask-browser.bundle.js +149 -116
- package/src/surfaces/web/mud-browser-entry.mjs +38 -7
- package/src/surfaces/web/p2p-browser-entry.mjs +1 -1
- package/src/surfaces/web/plan-browser-entry.mjs +33 -2
- package/src/surfaces/web/research-browser-entry.mjs +11 -19
- package/src/surfaces/web/sprites-browser-entry.mjs +39 -8
- package/src/surfaces/web/tmct-surface.mjs +12 -0
- package/src/surfaces/web/turn-session.mjs +10 -3
|
@@ -51,7 +51,7 @@ import { spriteFactRows } from "../domain/sprite-facts.mjs";
|
|
|
51
51
|
import { SEED_TAXONOMY } from "../domain/spider-fly-world.mjs";
|
|
52
52
|
import { loadSlice, loadMap, toFacts, WORDNET_DIR } from "../adapters/corpus/conceptnet.mjs";
|
|
53
53
|
import { join } from "node:path";
|
|
54
|
-
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson } from "./viz-theme.mjs";
|
|
54
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText } from "./viz-theme.mjs";
|
|
55
55
|
|
|
56
56
|
const DEFAULT_TITLE = "tmct — the sprite library";
|
|
57
57
|
const MAX_CHAIN_DISPLAY = 6;
|
|
@@ -164,13 +164,28 @@ export const GROUP_PERSON = "person";
|
|
|
164
164
|
export const GROUP_OBJECT = "object";
|
|
165
165
|
export const GROUP_EMOJI = "emoji";
|
|
166
166
|
|
|
167
|
+
// Each group's own full-gallery page filename — the demo site builds one
|
|
168
|
+
// page per group (scripts/build-demo-site.mjs) plus the sprites.html landing
|
|
169
|
+
// page's "view all" links, both read straight off this field so neither can
|
|
170
|
+
// name a group's page differently from the other.
|
|
167
171
|
export const CATALOG_GROUPS = Object.freeze([
|
|
168
|
-
Object.freeze({ id: GROUP_ADVENTURE, label: "Ashcombe Hall's own adventure props", note: "the icon tier's named cast and furniture. Each has its own 44px sprite." }),
|
|
169
|
-
Object.freeze({ id: GROUP_PERSON, label: "Person roles" }),
|
|
170
|
-
Object.freeze({ id: GROUP_OBJECT, label: "Physical objects, creatures & places" }),
|
|
171
|
-
Object.freeze({ id: GROUP_EMOJI, label: "Emotions & events", note: "abstract concepts with no single physical picture, drawn as the familiar emoji instead" }),
|
|
172
|
+
Object.freeze({ id: GROUP_ADVENTURE, label: "Ashcombe Hall's own adventure props", note: "the icon tier's named cast and furniture. Each has its own 44px sprite.", page: "sprites-adventure-props.html" }),
|
|
173
|
+
Object.freeze({ id: GROUP_PERSON, label: "Person roles", page: "sprites-person-roles.html" }),
|
|
174
|
+
Object.freeze({ id: GROUP_OBJECT, label: "Physical objects, creatures & places", page: "sprites-objects.html" }),
|
|
175
|
+
Object.freeze({ id: GROUP_EMOJI, label: "Emotions & events", note: "abstract concepts with no single physical picture, drawn as the familiar emoji instead", page: "sprites-emotions.html" }),
|
|
172
176
|
]);
|
|
173
177
|
|
|
178
|
+
// The two sections large enough to need ancestor clustering rather than one
|
|
179
|
+
// flat card grid — same two groups sectionHtml's own caller already singled
|
|
180
|
+
// out before this was a named function.
|
|
181
|
+
const CLUSTERED_GROUPS = Object.freeze([GROUP_PERSON, GROUP_OBJECT]);
|
|
182
|
+
|
|
183
|
+
/** Whether `groupId`'s own full gallery clusters its cards under ancestor
|
|
184
|
+
* headings rather than rendering one flat grid. Pure. */
|
|
185
|
+
export function groupIsClustered(groupId) {
|
|
186
|
+
return CLUSTERED_GROUPS.includes(groupId);
|
|
187
|
+
}
|
|
188
|
+
|
|
174
189
|
/** Which catalog section `cls` belongs in. Pure. `isIconTierClass`/`isEmoji`
|
|
175
190
|
* are handed in rather than recomputed here so this stays a one-line
|
|
176
191
|
* decision over already-known facts about the class. */
|
|
@@ -208,6 +223,65 @@ export function clusterEntriesByAncestor(entries, spritedClasses) {
|
|
|
208
223
|
return clusters;
|
|
209
224
|
}
|
|
210
225
|
|
|
226
|
+
/** Every section this catalog's own pages break into, in on-page reading
|
|
227
|
+
* order — the four top-level groups, each expanded into its own ancestor
|
|
228
|
+
* clusters when groupIsClustered says so (clusterEntriesByAncestor's own
|
|
229
|
+
* cluster order — big clusters first, "everything else" trailing), or left
|
|
230
|
+
* as one section when a group carries no clustering at all (adventure,
|
|
231
|
+
* emoji). This is the landing page's own granularity: one example card per
|
|
232
|
+
* section, not per top-level group, since a group as broad as "physical
|
|
233
|
+
* objects, creatures & places" reads as dozens of unrelated sub-themes a
|
|
234
|
+
* single example could never stand in for. Each item is `{group, label,
|
|
235
|
+
* entries}` — `label` is the ancestor's own name, "everything else" for a
|
|
236
|
+
* clustered group's trailing bucket, or the group's own label when the
|
|
237
|
+
* group isn't clustered at all. Pure. */
|
|
238
|
+
export function catalogSections(entries, spritedClasses) {
|
|
239
|
+
const sections = [];
|
|
240
|
+
for (const g of CATALOG_GROUPS) {
|
|
241
|
+
const rows = (entries || []).filter((e) => e.group === g.id);
|
|
242
|
+
if (!rows.length) continue;
|
|
243
|
+
if (!groupIsClustered(g.id)) {
|
|
244
|
+
sections.push({ group: g, label: g.label, entries: rows });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
for (const c of clusterEntriesByAncestor(rows, spritedClasses)) {
|
|
248
|
+
sections.push({ group: g, label: c.ancestor || "everything else", entries: c.entries });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return sections;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The operator's own curated example sprites, one per landing-page section,
|
|
255
|
+
* in reading order — a closed, hand-picked list in the same idiom as
|
|
256
|
+
* PERSON_ROLE_CLASSES/CATALOG_TAXONOMY_GAPFILL above, not derived from any
|
|
257
|
+
* ordering rule. Checked directly against catalogSections' real output: each
|
|
258
|
+
* name here really is a member of exactly one of this catalog's 28 current
|
|
259
|
+
* sections (the whole adventure and emoji groups, the 9 person-role
|
|
260
|
+
* clusters, the 17 physical-object clusters), one per section. A future
|
|
261
|
+
* catalog change that drops one of these classes, or adds/removes a section,
|
|
262
|
+
* is exactly what landingExampleFor's own fallback below is for. */
|
|
263
|
+
export const LANDING_EXAMPLE_CLASSES = Object.freeze([
|
|
264
|
+
"adventurer", "engineer", "king", "driver", "boss", "wife", "crowd", "worker",
|
|
265
|
+
"person", "family", "bear", "house", "tomato", "fly", "stadium", "spider",
|
|
266
|
+
"ocean", "planet", "metal", "boat", "breakfast", "town", "plant", "tiger",
|
|
267
|
+
"rain", "bedroom", "frog", "autumn",
|
|
268
|
+
]);
|
|
269
|
+
|
|
270
|
+
/** The landing page's one example entry for a section (`sectionEntries`,
|
|
271
|
+
* already the exact class list that section's own full page shows): the
|
|
272
|
+
* first of LANDING_EXAMPLE_CLASSES that is really a member of this section,
|
|
273
|
+
* so the operator's own curated favourite wins whenever this catalog really
|
|
274
|
+
* carries it there. Falls back to the section's own first entry (its
|
|
275
|
+
* existing catalog order) when none of the 28 named favourites is a member —
|
|
276
|
+
* an honest real example rather than nothing, for a section the curated
|
|
277
|
+
* list doesn't happen to name. Pure. */
|
|
278
|
+
const LANDING_EXAMPLE_CLASS_SET = new Set(LANDING_EXAMPLE_CLASSES);
|
|
279
|
+
|
|
280
|
+
export function landingExampleFor(sectionEntries) {
|
|
281
|
+
if (!sectionEntries?.length) return null;
|
|
282
|
+
return sectionEntries.find((e) => LANDING_EXAMPLE_CLASS_SET.has(e.className)) || sectionEntries[0];
|
|
283
|
+
}
|
|
284
|
+
|
|
211
285
|
function templatesForClass(cls, templates) {
|
|
212
286
|
return (templates || []).filter((t) => Array.isArray(t?.classes) && t.classes.includes(cls));
|
|
213
287
|
}
|
|
@@ -337,17 +411,47 @@ export function buildSpriteCatalogEntries({ iconTemplates = [], largeTemplates =
|
|
|
337
411
|
// ---- scene composer ----
|
|
338
412
|
//
|
|
339
413
|
// The free-text "there is a..." box (adventure-viz.mjs's roomSceneObjects/
|
|
340
|
-
// room-frame is the same shape) over
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
414
|
+
// room-frame is the same shape) over the WHOLE catalog's real classes, never
|
|
415
|
+
// just whichever group's cards the current page happens to render — the
|
|
416
|
+
// sprites.html landing page shows one card per group, and each per-group
|
|
417
|
+
// page shows only its own group, but a visitor composing "a doctor with a
|
|
418
|
+
// hat, and a cabinet" needs all three classes to resolve regardless of which
|
|
419
|
+
// page they typed it into. The parser itself is scene-compose.mjs, which
|
|
420
|
+
// resolves a typed class name through the real resolver; this page owns only
|
|
421
|
+
// the class index it is matched against (className -> real swatch labels),
|
|
422
|
+
// computed HERE, in Node, over the full catalog (sceneComposerClassIndex)
|
|
423
|
+
// and embedded once per page — the only way the index can stay whole-catalog
|
|
424
|
+
// once no single page renders every card any more.
|
|
348
425
|
|
|
349
426
|
export { extractSceneItems } from "../domain/scene-compose.mjs";
|
|
350
427
|
|
|
428
|
+
/** The scene composer's class index, `{className: {defaultSvg, materials}}`
|
|
429
|
+
* with `materials` keyed by lowercase label — computed over the WHOLE
|
|
430
|
+
* catalog's `entries` (every group), never just the classes a particular
|
|
431
|
+
* page's cards happen to show. Pure. Mirrors exactly what the page's own
|
|
432
|
+
* large-tier card markup carries: a plain swatch is the default sprite, a
|
|
433
|
+
* fallback swatch stands in when there's no plain one, and every other
|
|
434
|
+
* swatch's own label becomes a material key. A class with no large-tier
|
|
435
|
+
* swatch at all (icon-only) contributes nothing — there is no sprite for
|
|
436
|
+
* the composer to draw for it either way. */
|
|
437
|
+
export function sceneComposerClassIndex(entries) {
|
|
438
|
+
const index = {};
|
|
439
|
+
for (const entry of entries || []) {
|
|
440
|
+
const large = entry.largeSwatches || [];
|
|
441
|
+
if (!large.length) continue;
|
|
442
|
+
let defaultSvg = null;
|
|
443
|
+
const materials = {};
|
|
444
|
+
for (const s of large) {
|
|
445
|
+
if (s.kind === "plain") defaultSvg = s.svg;
|
|
446
|
+
else if (s.kind === "fallback") { if (!defaultSvg) defaultSvg = s.svg; }
|
|
447
|
+
else materials[String(s.label).trim().toLowerCase()] = s.svg;
|
|
448
|
+
}
|
|
449
|
+
if (!defaultSvg) defaultSvg = large[0].svg;
|
|
450
|
+
index[entry.className] = { defaultSvg, materials };
|
|
451
|
+
}
|
|
452
|
+
return index;
|
|
453
|
+
}
|
|
454
|
+
|
|
351
455
|
// ---- the catalog question lane ----
|
|
352
456
|
//
|
|
353
457
|
// There isn't one. Every catalog-shaped question the dock takes — "how many
|
|
@@ -427,6 +531,15 @@ export function movingFrameSequence(idleFrame, movingFrame) {
|
|
|
427
531
|
|
|
428
532
|
// ---- rendering ----
|
|
429
533
|
|
|
534
|
+
/** The DOM id a class's own card carries on whichever full-gallery page
|
|
535
|
+
* renders it — what a landing-page "view all" link anchors to
|
|
536
|
+
* (`${group.page}#${classAnchorId(className)}`), so the visitor lands
|
|
537
|
+
* exactly on the class they clicked through for rather than the top of a
|
|
538
|
+
* page with dozens or hundreds of other cards above it. Pure. */
|
|
539
|
+
export function classAnchorId(className) {
|
|
540
|
+
return `card-${String(className).trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")}`;
|
|
541
|
+
}
|
|
542
|
+
|
|
430
543
|
function chainHtml(chain) {
|
|
431
544
|
const shown = chain.slice(0, MAX_CHAIN_DISPLAY);
|
|
432
545
|
const rest = chain.length - shown.length;
|
|
@@ -455,7 +568,7 @@ function tierRowHtml(tierName, swatches) {
|
|
|
455
568
|
}
|
|
456
569
|
|
|
457
570
|
function cardHtml(entry) {
|
|
458
|
-
return `<article class="card" data-cls="${escapeHtml(entry.className)}" data-group="${escapeHtml(entry.group)}">
|
|
571
|
+
return `<article class="card" id="${classAnchorId(entry.className)}" data-cls="${escapeHtml(entry.className)}" data-group="${escapeHtml(entry.group)}">
|
|
459
572
|
<h3 class="card-name">${escapeHtml(entry.className)}</h3>
|
|
460
573
|
${chainHtml(entry.chain)}
|
|
461
574
|
${tierRowHtml("icon", entry.iconSwatches)}
|
|
@@ -492,43 +605,108 @@ function sectionHtml(group, entries, { clusterBy = null } = {}) {
|
|
|
492
605
|
</section>`;
|
|
493
606
|
}
|
|
494
607
|
|
|
495
|
-
/** The
|
|
496
|
-
*
|
|
497
|
-
* (
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
608
|
+
/** The sprite-catalog page. Pure given `iconTemplates` (readSpriteTemplateFiles'
|
|
609
|
+
* own output), `largeTemplates` (readSpriteLargeTemplateFiles' own output)
|
|
610
|
+
* and `factRows` (loadSpriteOntologyFactRows' own output) — the same
|
|
611
|
+
* "byte-identical for identical input" invariant every other viz page in
|
|
612
|
+
* this project holds. All three default to `[]` so a caller mid-migration
|
|
613
|
+
* (no ontology facts loaded yet, say) still gets a page that renders, just
|
|
614
|
+
* with plainer ancestor chains.
|
|
615
|
+
*
|
|
616
|
+
* With no `groupId`, this is the FULL catalog on one page, every group's
|
|
617
|
+
* full gallery, anchor-nav between its own sections — the CLI's `--render
|
|
618
|
+
* sprites` standalone export, and this module's own test fixture. Given a
|
|
619
|
+
* real `groupId` (one of CATALOG_GROUPS' own ids), this instead renders
|
|
620
|
+
* ONLY that group's full gallery — the demo site's own sprites-<group>.html
|
|
621
|
+
* pages (scripts/build-demo-site.mjs) — with the topbar switched to
|
|
622
|
+
* cross-page links (crossPageNavHtml), since a `#g-<id>` anchor to a
|
|
623
|
+
* section that isn't on this page any more would go nowhere. Either way the
|
|
624
|
+
* footer counts describe only what THIS page actually shows.
|
|
503
625
|
*
|
|
504
626
|
* `spritesBundleAvailable: true` (ledger-viz.mjs's own ledgerBundleAvailable
|
|
505
627
|
* idiom) is what adds the two interactive panels at all: the page then embeds
|
|
506
628
|
* the sprite-facts rows (src/domain/sprite-facts.mjs, derived purely from the
|
|
507
|
-
* same two template sets) and references the
|
|
508
|
-
*
|
|
509
|
-
* it. Both panels need that bundle — the dock for its chat session, the scene
|
|
629
|
+
* same two template sets) and references the sprites-browser bundle. Both
|
|
630
|
+
* panels need that bundle — the dock for its chat session, the scene
|
|
510
631
|
* composer for the resolver its parser asks. Left false, the page is the
|
|
511
632
|
* catalog alone: no dock, no composer, no bundle reference, nothing extra to
|
|
512
|
-
* 404 — including the favicon links
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
|
|
633
|
+
* 404 — including the favicon links.
|
|
634
|
+
*
|
|
635
|
+
* `engineBundleJs` (the built sprites-browser bundle's own text, spider-fly-
|
|
636
|
+
* viz.mjs's own idiom) inlines that bundle into the page instead of
|
|
637
|
+
* referencing it as the sibling `./sprites-browser.bundle.js`
|
|
638
|
+
* scripts/build-demo-site.mjs writes alongside the deployed pages — for the
|
|
639
|
+
* CLI's standalone export, one downloadable file that runs from file://
|
|
640
|
+
* with no sibling assets. Default empty keeps the site build's sibling-file
|
|
641
|
+
* arrangement byte-identical, favicon links included; the standalone export
|
|
642
|
+
* drops those too, since a relative ./favicon.svg would be a dangling
|
|
643
|
+
* external reference the "no sibling assets" export can't carry.
|
|
644
|
+
*
|
|
645
|
+
* Both the dock's fact rows and the composer's class index are always built
|
|
646
|
+
* from the WHOLE catalog, never just `groupId`'s own slice — a visitor on
|
|
647
|
+
* one group's page can still ask about, or compose, any real class from any
|
|
648
|
+
* other group. */
|
|
649
|
+
export function renderSpriteCatalogHtml({ title = DEFAULT_TITLE, iconTemplates = [], largeTemplates = [], factRows = [], spritesBundleAvailable = false, groupId = null, engineBundleJs = "" } = {}) {
|
|
516
650
|
const entries = buildSpriteCatalogEntries({ iconTemplates, largeTemplates, factRows });
|
|
517
|
-
const totalSwatches = entries.reduce((n, e) => n + e.iconSwatches.length + e.largeSwatches.length, 0);
|
|
518
|
-
const pageData = embedJson({ classCount: entries.length, swatchCount: totalSwatches });
|
|
519
|
-
// Ancestor clustering for the two big sections: the heading chip is the
|
|
520
|
-
// ancestor's OWN resolved sprite — visible proof that a new subclass
|
|
521
|
-
// attached there already has fallback art waiting for it.
|
|
522
651
|
const spritedClasses = new Set([...iconTemplates, ...largeTemplates].flatMap((t) => t?.classes || []));
|
|
523
652
|
const clusterBy = {
|
|
524
653
|
spritedClasses,
|
|
525
654
|
resolveChip: (ancestor) =>
|
|
526
655
|
resolveSpriteAsset(ancestor, [], [], largeTemplates, SPRITE_REGISTRY, { instanceKey: `cluster-${ancestor}` }),
|
|
527
656
|
};
|
|
528
|
-
const
|
|
529
|
-
const
|
|
530
|
-
.map((g) =>
|
|
657
|
+
const groupsToRender = groupId ? CATALOG_GROUPS.filter((g) => g.id === groupId) : CATALOG_GROUPS;
|
|
658
|
+
const bodyHtml = groupsToRender
|
|
659
|
+
.map((g) => sectionHtml(g, entries, { clusterBy: groupIsClustered(g.id) ? clusterBy : null }))
|
|
531
660
|
.join("");
|
|
661
|
+
const navHtml = groupId
|
|
662
|
+
? crossPageNavHtml(entries, { currentGroupId: groupId, includeOverviewLink: true })
|
|
663
|
+
: CATALOG_GROUPS
|
|
664
|
+
.map((g) => `<a class="jump" href="#g-${g.id}">${escapeHtml(g.label)} <span class="count">${entries.filter((e) => e.group === g.id).length}</span></a>`)
|
|
665
|
+
.join("");
|
|
666
|
+
const footerEntries = groupId ? entries.filter((e) => e.group === groupId) : entries;
|
|
667
|
+
const footerSwatches = footerEntries.reduce((n, e) => n + e.iconSwatches.length + e.largeSwatches.length, 0);
|
|
668
|
+
return renderSpriteCatalogPage({
|
|
669
|
+
title, entries, bodyHtml, navHtml, iconTemplates, largeTemplates, spritesBundleAvailable, engineBundleJs,
|
|
670
|
+
footerClassCount: footerEntries.length, footerSwatchCount: footerSwatches,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/** The topbar's cross-page nav for a per-group or landing site page: real
|
|
675
|
+
* links to each group's own full-gallery page (CATALOG_GROUPS' own `page`
|
|
676
|
+
* field), each carrying that group's real class count. Used wherever a page
|
|
677
|
+
* no longer holds every group's own section, so an in-page `#g-<id>` anchor
|
|
678
|
+
* would name a section that isn't there any more — the full single-page
|
|
679
|
+
* render (no `groupId`, CLI standalone) keeps the anchor nav instead, since
|
|
680
|
+
* every section really is on that one page. `currentGroupId` marks its own
|
|
681
|
+
* page's link `aria-current="page"` rather than dropping it, and
|
|
682
|
+
* `includeOverviewLink` adds a link back to the sprites.html landing page
|
|
683
|
+
* (every per-group page wants one; the landing page itself doesn't). */
|
|
684
|
+
function crossPageNavHtml(entries, { currentGroupId = null, includeOverviewLink = false } = {}) {
|
|
685
|
+
const groupLinks = CATALOG_GROUPS.map((g) => {
|
|
686
|
+
const count = entries.filter((e) => e.group === g.id).length;
|
|
687
|
+
const current = g.id === currentGroupId ? ' aria-current="page"' : "";
|
|
688
|
+
return `<a class="jump" href="./${g.page}"${current}>${escapeHtml(g.label)} <span class="count">${count}</span></a>`;
|
|
689
|
+
}).join("");
|
|
690
|
+
const overview = includeOverviewLink ? `<a class="jump jump-overview" href="./sprites.html">overview</a>` : "";
|
|
691
|
+
return groupLinks + overview;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** The shared page scaffold every sprite-catalog page renders through: the
|
|
695
|
+
* chrome (appbar, composer, ask dock, topbar+filter, footer) and every
|
|
696
|
+
* script, identical on the CLI's standalone full-catalog export, the
|
|
697
|
+
* sprites.html landing page, and each of the four per-group pages — only
|
|
698
|
+
* `bodyHtml`/`navHtml`/the footer counts differ per caller. `entries` is
|
|
699
|
+
* always the WHOLE catalog (never filtered to one group), because the
|
|
700
|
+
* composer's class index and the dock's fact rows must resolve any real
|
|
701
|
+
* catalog class regardless of which cards this particular page shows. */
|
|
702
|
+
function renderSpriteCatalogPage({ title, entries, bodyHtml, navHtml, iconTemplates, largeTemplates, spritesBundleAvailable, engineBundleJs = "", footerClassCount, footerSwatchCount }) {
|
|
703
|
+
const totalSwatches = entries.reduce((n, e) => n + e.iconSwatches.length + e.largeSwatches.length, 0);
|
|
704
|
+
const pageData = embedJson({ classCount: entries.length, swatchCount: totalSwatches });
|
|
705
|
+
const dockRows = spritesBundleAvailable ? spriteFactRows({ iconTemplates, largeTemplates }) : [];
|
|
706
|
+
// The composer's class index, computed here over the WHOLE catalog and
|
|
707
|
+
// embedded once — see sceneComposerClassIndex's own header for why this
|
|
708
|
+
// replaced reading it back off the page's own rendered card markup.
|
|
709
|
+
const classIndexJs = !spritesBundleAvailable ? "{}" : embedJson(sceneComposerClassIndex(entries));
|
|
532
710
|
|
|
533
711
|
const dockCss = !spritesBundleAvailable ? "" : `
|
|
534
712
|
.dockwrap { margin: .2rem 0 1.3rem; }
|
|
@@ -573,7 +751,15 @@ export function renderSpriteCatalogHtml({ title = DEFAULT_TITLE, iconTemplates =
|
|
|
573
751
|
// The bundle carries the scene composer's parser as well as the dock's
|
|
574
752
|
// session, and the catalog script below calls it directly, so it loads ahead
|
|
575
753
|
// of that script rather than beside the dock's own wiring at the end.
|
|
576
|
-
|
|
754
|
+
// `engineBundleJs` present means the CLI's standalone export: inline the
|
|
755
|
+
// bundle text instead of the sibling `<script src>` the deployed site uses,
|
|
756
|
+
// the same choice spider-fly-viz.mjs/adventure-viz.mjs make for their own
|
|
757
|
+
// engine bundles.
|
|
758
|
+
const spriteBundleScript = !spritesBundleAvailable
|
|
759
|
+
? ""
|
|
760
|
+
: engineBundleJs
|
|
761
|
+
? `<script>\n${embedScriptText(engineBundleJs)}\n</script>`
|
|
762
|
+
: `<script src="./sprites-browser.bundle.js"></script>`;
|
|
577
763
|
|
|
578
764
|
// The composer resolves a typed class name through the real resolver, which
|
|
579
765
|
// reaches the page in that bundle. Without it there is nothing to type into,
|
|
@@ -694,7 +880,7 @@ const SPRITE_CHAT = ${embedJson({ rows: dockRows })};
|
|
|
694
880
|
<meta charset="utf-8">
|
|
695
881
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
696
882
|
<title>${escapeHtml(title)}</title>
|
|
697
|
-
${spritesBundleAvailable ? `<link rel="icon" href="./favicon.svg" type="image/svg+xml">
|
|
883
|
+
${spritesBundleAvailable && !engineBundleJs ? `<link rel="icon" href="./favicon.svg" type="image/svg+xml">
|
|
698
884
|
<link rel="icon" href="./favicon.ico" sizes="any">
|
|
699
885
|
<link rel="apple-touch-icon" href="./apple-touch-icon.png">` : ""}
|
|
700
886
|
<style>
|
|
@@ -723,6 +909,8 @@ ${THEME_TOKENS_CSS}
|
|
|
723
909
|
.jump { font-family: ${MONO_STACK}; font-size: .7rem; padding: .2rem .6rem; border: 1px solid var(--ai-edge); border-radius: 3px; background: transparent; color: var(--ink); text-decoration: none; }
|
|
724
910
|
.jump:hover { border-color: var(--corpus); color: var(--corpus); }
|
|
725
911
|
.jump .count { color: var(--muted); }
|
|
912
|
+
.jump[aria-current="page"] { border-color: var(--taught); color: var(--taught); }
|
|
913
|
+
.jump-overview { margin-left: .3rem; opacity: .8; }
|
|
726
914
|
.filter { margin-left: auto; display: flex; align-items: center; gap: .4rem; }
|
|
727
915
|
.filter input { font-family: ${MONO_STACK}; font-size: .78rem; background: var(--ai-panel-hi); color: var(--ink); border: 1px solid var(--ai-edge); border-radius: 3px; padding: .3rem .6rem; width: 200px; }
|
|
728
916
|
.filter .n { font-family: ${MONO_STACK}; font-size: .7rem; color: var(--muted); white-space: nowrap; }
|
|
@@ -748,6 +936,13 @@ ${THEME_TOKENS_CSS}
|
|
|
748
936
|
.group h2 { font-family: ${MONO_STACK}; font-size: .78rem; text-transform: uppercase; letter-spacing: .09em; margin: 0 0 .2rem; display: flex; align-items: baseline; gap: .5rem; }
|
|
749
937
|
.group h2 .count { font-size: .7rem; color: var(--muted); font-weight: 400; }
|
|
750
938
|
.section-note { color: var(--muted); font-size: .82rem; margin: 0 0 .8rem; max-width: 68ch; }
|
|
939
|
+
.landing-sections { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 1.1rem 1rem; }
|
|
940
|
+
.landing-section { min-width: 0; }
|
|
941
|
+
.landing-section-name { font-family: ${MONO_STACK}; font-size: .68rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); margin: 0 0 .4rem; display: flex; align-items: baseline; gap: .4rem; }
|
|
942
|
+
.landing-section-name .count { font-weight: 400; opacity: .75; }
|
|
943
|
+
.landing-section .cards { grid-template-columns: 1fr; }
|
|
944
|
+
.viewall { display: inline-block; font-family: ${MONO_STACK}; font-size: .7rem; margin-top: .4rem; color: var(--taught); text-decoration: none; }
|
|
945
|
+
.viewall:hover { color: var(--corpus); text-decoration: underline; }
|
|
751
946
|
.cluster { margin: 1rem 0 1.5rem; }
|
|
752
947
|
.cluster-head { display: flex; align-items: center; gap: .45rem; font-family: ${MONO_STACK}; font-size: .7rem; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; color: var(--muted); margin: 0 0 .55rem; }
|
|
753
948
|
.cluster-head .count { font-weight: 400; opacity: .75; }
|
|
@@ -798,11 +993,12 @@ ${dockCss}</style>
|
|
|
798
993
|
<span class="n mono" id="qcount"></span>
|
|
799
994
|
</div>
|
|
800
995
|
</div>
|
|
801
|
-
${
|
|
802
|
-
<footer class="page">${
|
|
996
|
+
${bodyHtml}
|
|
997
|
+
<footer class="page">${footerClassCount} classes · ${footerSwatchCount} swatches · icon tier 44px, sprite tier 400px</footer>
|
|
803
998
|
</main>
|
|
804
999
|
<script>
|
|
805
1000
|
const SPRITE_CATALOG = ${pageData};
|
|
1001
|
+
const SPRITE_CLASS_INDEX = ${classIndexJs};
|
|
806
1002
|
</script>
|
|
807
1003
|
${spriteBundleScript}
|
|
808
1004
|
<script>
|
|
@@ -828,40 +1024,16 @@ ${spriteBundleScript}
|
|
|
828
1024
|
q.addEventListener("input", apply);
|
|
829
1025
|
apply();
|
|
830
1026
|
|
|
831
|
-
// ---- the scene composer — reads the class/material index
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
// word names is the bundle's
|
|
837
|
-
// resolver — the page never
|
|
1027
|
+
// ---- the scene composer — reads the class/material index from
|
|
1028
|
+
// SPRITE_CLASS_INDEX, computed server-side over the WHOLE catalog
|
|
1029
|
+
// (sceneComposerClassIndex) and embedded above, never scoped to whichever
|
|
1030
|
+
// cards THIS page happens to render — a landing page showing one card per
|
|
1031
|
+
// section, or a per-group page showing only one group, still composes any
|
|
1032
|
+
// real catalog class. Which class a typed word names is the bundle's
|
|
1033
|
+
// extractSceneItems, which asks the real resolver — the page never
|
|
1034
|
+
// matches a class name itself.
|
|
838
1035
|
const esc = ${escapeHtml.toString()};
|
|
839
1036
|
|
|
840
|
-
function buildClassIndexFromDom() {
|
|
841
|
-
const index = {};
|
|
842
|
-
for (const card of cards) {
|
|
843
|
-
const largeRow = card.querySelector('.tier-row[data-tier="large"]');
|
|
844
|
-
if (!largeRow) continue;
|
|
845
|
-
let defaultSvg = null;
|
|
846
|
-
const materials = {};
|
|
847
|
-
for (const swatch of largeRow.querySelectorAll(".swatch")) {
|
|
848
|
-
const labelEl = swatch.querySelector(".swatch-label");
|
|
849
|
-
const svgEl = swatch.querySelector(".swatch-img");
|
|
850
|
-
if (!labelEl || !svgEl) continue;
|
|
851
|
-
const svg = svgEl.innerHTML;
|
|
852
|
-
if (swatch.classList.contains("plain")) defaultSvg = svg;
|
|
853
|
-
else if (swatch.classList.contains("fallback")) { if (!defaultSvg) defaultSvg = svg; }
|
|
854
|
-
else materials[labelEl.textContent.trim().toLowerCase()] = svg;
|
|
855
|
-
}
|
|
856
|
-
if (!defaultSvg) {
|
|
857
|
-
const firstSvg = largeRow.querySelector(".swatch-img");
|
|
858
|
-
if (firstSvg) defaultSvg = firstSvg.innerHTML;
|
|
859
|
-
}
|
|
860
|
-
index[card.dataset.cls] = { defaultSvg, materials };
|
|
861
|
-
}
|
|
862
|
-
return index;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
1037
|
function wireSceneComposer() {
|
|
866
1038
|
const composeqEl = document.getElementById("composeq");
|
|
867
1039
|
if (!composeqEl) return;
|
|
@@ -870,7 +1042,7 @@ ${spriteBundleScript}
|
|
|
870
1042
|
const composePillsEl = document.getElementById("composePills");
|
|
871
1043
|
const sceneRowEl = document.getElementById("sceneRow");
|
|
872
1044
|
const sceneEmptyEl = document.getElementById("sceneEmpty");
|
|
873
|
-
const classIndex =
|
|
1045
|
+
const classIndex = SPRITE_CLASS_INDEX;
|
|
874
1046
|
|
|
875
1047
|
function renderScene(text) {
|
|
876
1048
|
const items = extractSceneItems(text, classIndex);
|
|
@@ -907,16 +1079,15 @@ ${spriteBundleScript}
|
|
|
907
1079
|
// ---- the animated swatches — a card's large tier gains one moving
|
|
908
1080
|
// swatch per axis its own templates actually vary on: mood (mgx:feels),
|
|
909
1081
|
// facing (mgx:faces) and pose (mgx:pose). Frames are the card's own
|
|
910
|
-
// already-rendered swatches (
|
|
911
|
-
//
|
|
1082
|
+
// already-rendered swatches (read straight off the DOM here — unlike the
|
|
1083
|
+
// composer's classIndex above, this only ever needs whatever cards THIS
|
|
1084
|
+
// page actually renders, never the whole catalog), ordered by the pure
|
|
912
1085
|
// *FrameSequence functions spliced in below, and one shared interval
|
|
913
1086
|
// steps every cycle in sync at one shared delay. The mood cycle leads the
|
|
914
1087
|
// row; the facing sweep and the pose toggle take over the static plain
|
|
915
1088
|
// and happy swatches' own places rather than adding cells, so the row
|
|
916
1089
|
// keeps its width. Reduced motion disables the auto-step only; each frame
|
|
917
1090
|
// is a button, so a click or Enter always advances one frame by hand.
|
|
918
|
-
// Injected AFTER buildClassIndexFromDom ran, so a moving frame never
|
|
919
|
-
// leaks into the composer's material index.
|
|
920
1091
|
const CYCLE_FRAME_DELAY_MS = ${CYCLE_FRAME_DELAY_MS};
|
|
921
1092
|
const FACING_TURN_ORDER = ${embedJson(FACING_TURN_ORDER)};
|
|
922
1093
|
const MOOD_PROPERTY = "mgx:feels";
|
|
@@ -994,3 +1165,57 @@ ${dockScripts}
|
|
|
994
1165
|
</html>
|
|
995
1166
|
`;
|
|
996
1167
|
}
|
|
1168
|
+
|
|
1169
|
+
/** `sprites.html` itself: a lighter landing page over the WHOLE catalog —
|
|
1170
|
+
* one real example card per section (catalogSections/landingExampleFor;
|
|
1171
|
+
* a section is a top-level group when it isn't clustered, adventure and
|
|
1172
|
+
* emoji, or one of person/object's own ancestor clusters otherwise), each
|
|
1173
|
+
* linking straight to that class's own card on its group's full-gallery
|
|
1174
|
+
* page (classAnchorId). A section this small never earns a page of its
|
|
1175
|
+
* own — every section, big or small, already lives together with the rest
|
|
1176
|
+
* of its group on that one shared full-gallery page, which is exactly
|
|
1177
|
+
* where a landing card's link lands.
|
|
1178
|
+
*
|
|
1179
|
+
* The composer and the ask dock still answer over the WHOLE catalog from
|
|
1180
|
+
* here (sceneComposerClassIndex/spriteFactRows both read every class, never
|
|
1181
|
+
* just the ~28 shown), so typing "a doctor with a hat, and a cabinet"
|
|
1182
|
+
* composes correctly even though none of those three classes has a card on
|
|
1183
|
+
* this page. Same chrome, same styling, same scripts as every per-group
|
|
1184
|
+
* page — renderSpriteCatalogPage owns all of that; this function only
|
|
1185
|
+
* picks which body and nav go in it. */
|
|
1186
|
+
export function renderSpriteCatalogLandingHtml({ title = DEFAULT_TITLE, iconTemplates = [], largeTemplates = [], factRows = [], spritesBundleAvailable = false } = {}) {
|
|
1187
|
+
const entries = buildSpriteCatalogEntries({ iconTemplates, largeTemplates, factRows });
|
|
1188
|
+
const spritedClasses = new Set([...iconTemplates, ...largeTemplates].flatMap((t) => t?.classes || []));
|
|
1189
|
+
const sections = catalogSections(entries, spritedClasses);
|
|
1190
|
+
const bodyHtml = CATALOG_GROUPS.map((g) => {
|
|
1191
|
+
const groupEntries = entries.filter((e) => e.group === g.id);
|
|
1192
|
+
if (!groupEntries.length) return "";
|
|
1193
|
+
const groupSections = sections.filter((s) => s.group.id === g.id);
|
|
1194
|
+
const note = g.note ? `<p class="section-note">${escapeHtml(g.note)}</p>` : "";
|
|
1195
|
+
const sectionsHtml = groupSections.map((section) => {
|
|
1196
|
+
const example = landingExampleFor(section.entries);
|
|
1197
|
+
if (!example) return "";
|
|
1198
|
+
// A group with only one section (adventure, emoji) needs no
|
|
1199
|
+
// sub-heading of its own — the group heading above already names it.
|
|
1200
|
+
const heading = groupSections.length > 1
|
|
1201
|
+
? `<h3 class="landing-section-name">${escapeHtml(section.label)} <span class="count">${section.entries.length}</span></h3>`
|
|
1202
|
+
: "";
|
|
1203
|
+
return `<div class="landing-section">
|
|
1204
|
+
${heading}
|
|
1205
|
+
<div class="cards">${cardHtml(example)}</div>
|
|
1206
|
+
<a class="viewall" href="./${g.page}#${classAnchorId(example.className)}">view all ${section.entries.length} ›</a>
|
|
1207
|
+
</div>`;
|
|
1208
|
+
}).join("");
|
|
1209
|
+
return `<section class="group landing-group" id="g-${g.id}" aria-label="${escapeHtml(g.label)}">
|
|
1210
|
+
<h2>${escapeHtml(g.label)} <span class="count">${groupEntries.length}</span></h2>
|
|
1211
|
+
${note}
|
|
1212
|
+
<div class="landing-sections">${sectionsHtml}</div>
|
|
1213
|
+
</section>`;
|
|
1214
|
+
}).join("");
|
|
1215
|
+
const navHtml = crossPageNavHtml(entries, {});
|
|
1216
|
+
const totalSwatches = entries.reduce((n, e) => n + e.iconSwatches.length + e.largeSwatches.length, 0);
|
|
1217
|
+
return renderSpriteCatalogPage({
|
|
1218
|
+
title, entries, bodyHtml, navHtml, iconTemplates, largeTemplates, spritesBundleAvailable,
|
|
1219
|
+
footerClassCount: entries.length, footerSwatchCount: totalSwatches,
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
createInMemoryStore, appendFacts, appendRule, loadMemory, readFactRows, removeFacts,
|
|
42
42
|
} from "../../adapters/memory/core.mjs";
|
|
43
43
|
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
44
|
+
import { memoryFactGraphPayload } from "../../domain/memory-facts.mjs";
|
|
44
45
|
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
45
46
|
import { foldWorldState, worldDigestRows, roomAffordances, worldActionRows } from "../../services/adventure.mjs";
|
|
46
47
|
import { runAdventureAutoplayTick, exposedFacts } from "../../services/adventure-autoplay.mjs";
|
|
@@ -103,7 +104,21 @@ export async function createAdventureSession(worldPayload, { restoredPayload = n
|
|
|
103
104
|
const openingHere = foldWorldState(worldActionRows(openingRows)).placements.get("player")?.object ?? null;
|
|
104
105
|
if (openingHere) visitedRoomIds.add(openingHere);
|
|
105
106
|
|
|
106
|
-
|
|
107
|
+
// A known-empty code graph: code-structure questions get the same honest
|
|
108
|
+
// no-code-graph answer an un-pointed CLI session gives, never a crash — the
|
|
109
|
+
// turn engine keeps reading THIS one for its own in-turn code lane.
|
|
110
|
+
const codeGraph = parseEntities({ individuals: [], objectProperties: [] });
|
|
111
|
+
|
|
112
|
+
// What `tmct.ask()` traverses is a different graph: this world's own memory
|
|
113
|
+
// store, projected through memoryFactGraphPayload. Rebuilt on demand rather
|
|
114
|
+
// than once at open, because every taught fact and every played turn grows
|
|
115
|
+
// the store.
|
|
116
|
+
let memoryGraph = parseEntities({ individuals: [], objectProperties: [] });
|
|
117
|
+
async function refreshGraph() {
|
|
118
|
+
memoryGraph = parseEntities(memoryFactGraphPayload(readFactRows(await loadMemory(memoryDir))));
|
|
119
|
+
return memoryGraph;
|
|
120
|
+
}
|
|
121
|
+
|
|
107
122
|
const lexicon = loadLexicon();
|
|
108
123
|
|
|
109
124
|
// createTurnSession owns focus/last and the catch fallback; planHolder
|
|
@@ -117,9 +132,9 @@ export async function createAdventureSession(worldPayload, { restoredPayload = n
|
|
|
117
132
|
// when the command didn't move anyone) — the manual-play half of the
|
|
118
133
|
// merged exposure set.
|
|
119
134
|
const turnSession = createTurnSession({
|
|
120
|
-
memoryDir, graph, lexicon, sessionId,
|
|
135
|
+
memoryDir, graph: codeGraph, lexicon, sessionId,
|
|
121
136
|
vocabHint: 'Try a world question ("where is the key"), or teach me: "remember: the moat is a ditch".',
|
|
122
|
-
buildExtraOptions: () => ({
|
|
137
|
+
buildExtraOptions: () => ({ planState: planHolder.state }),
|
|
123
138
|
captureExtraState: async (result) => {
|
|
124
139
|
if ("planState" in result) planHolder.state = result.planState;
|
|
125
140
|
const here = foldWorldState(worldActionRows(readFactRows(await loadMemory(memoryDir)))).placements.get("player")?.object ?? null;
|
|
@@ -129,7 +144,9 @@ export async function createAdventureSession(worldPayload, { restoredPayload = n
|
|
|
129
144
|
|
|
130
145
|
return {
|
|
131
146
|
memoryDir,
|
|
132
|
-
|
|
147
|
+
codeGraph,
|
|
148
|
+
get graph() { return memoryGraph; },
|
|
149
|
+
refreshGraph,
|
|
133
150
|
|
|
134
151
|
/** One auto-play tick: infer the goal, execute exactly one move through
|
|
135
152
|
* adventureTurn (adventure-autoplay.mjs's own contract), thread the
|
|
@@ -204,7 +221,12 @@ export async function createAdventureSession(worldPayload, { restoredPayload = n
|
|
|
204
221
|
// roomGraphSvg needs escapeHtml, directedGridLayout its own exit-delta table).
|
|
205
222
|
publishTmctSurface({
|
|
206
223
|
open: createAdventureSession,
|
|
207
|
-
ask
|
|
224
|
+
// The memory projection is rebuilt first, so a direct tmct.ask() sees every
|
|
225
|
+
// fact taught or played into this world since the last one.
|
|
226
|
+
ask: async (request, options, session) => {
|
|
227
|
+
await session.refreshGraph();
|
|
228
|
+
return graphAsk(request, options, session);
|
|
229
|
+
},
|
|
208
230
|
plan: enginePlan,
|
|
209
231
|
page: {
|
|
210
232
|
resolveSpriteForClass, SPRITE_REGISTRY, resolveSpriteAsset,
|