@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.
Files changed (65) hide show
  1. package/README.md +2 -1
  2. package/corpus/sprites/src/sprite-facts.jsonl +375 -8
  3. package/package.json +1 -1
  4. package/src/adapters/memory/core.mjs +20 -0
  5. package/src/domain/ask-vocab.mjs +71 -0
  6. package/src/domain/ask.mjs +168 -0
  7. package/src/domain/game-config.mjs +11 -0
  8. package/src/domain/mud-facts.mjs +15 -0
  9. package/src/domain/router/drive.mjs +35 -9
  10. package/src/domain/router/registry.mjs +24 -4
  11. package/src/domain/router/resolver.mjs +102 -40
  12. package/src/domain/scene-compose.mjs +117 -0
  13. package/src/domain/spider-fly-world.mjs +36 -0
  14. package/src/domain/sprite-facts.mjs +0 -0
  15. package/src/domain/sprite-request.mjs +156 -0
  16. package/src/domain/sprite-templates.mjs +161 -14
  17. package/src/services/adventure-editor.mjs +8 -14
  18. package/src/services/adventure-viz.mjs +119 -150
  19. package/src/services/adventure.mjs +97 -35
  20. package/src/services/chat-page-viz.mjs +64 -48
  21. package/src/services/chat.mjs +102 -34
  22. package/src/services/code-explorer-viz.mjs +52 -50
  23. package/src/services/ingest-viz.mjs +32 -74
  24. package/src/services/ledger-viz.mjs +87 -70
  25. package/src/services/memory-panel-viz.mjs +38 -0
  26. package/src/services/mud-editor.mjs +10 -15
  27. package/src/services/mud-turn.mjs +6 -6
  28. package/src/services/mud-viz.mjs +119 -225
  29. package/src/services/p2p-room.mjs +90 -23
  30. package/src/services/plan-pddl.mjs +3 -1
  31. package/src/services/plan-viz.mjs +13 -12
  32. package/src/services/research-viz.mjs +25 -67
  33. package/src/services/spider-fly-turn.mjs +14 -22
  34. package/src/services/spider-fly-viz.mjs +97 -136
  35. package/src/services/spider-fly.mjs +69 -11
  36. package/src/services/sprite-catalog-viz.mjs +274 -224
  37. package/src/services/viz-boot.mjs +71 -0
  38. package/src/services/viz-room-graph.mjs +203 -0
  39. package/src/services/viz-theme.mjs +75 -1
  40. package/src/services/viz-ticker.mjs +22 -0
  41. package/src/surfaces/web/adventure-browser-entry.mjs +62 -47
  42. package/src/surfaces/web/chat-browser-entry.mjs +51 -107
  43. package/src/surfaces/web/code-explorer-browser-entry.mjs +192 -35
  44. package/src/surfaces/web/engine-surface.mjs +82 -0
  45. package/src/surfaces/web/ingest-browser-entry.mjs +16 -17
  46. package/src/surfaces/web/ledger-browser-entry.mjs +24 -56
  47. package/src/surfaces/web/memory-ask-browser-entry.mjs +55 -13
  48. package/src/surfaces/web/memory-ask-browser.bundle.js +128 -125
  49. package/src/surfaces/web/memory-stats.mjs +11 -0
  50. package/src/surfaces/web/mud-browser-entry.mjs +70 -49
  51. package/src/surfaces/web/plan-browser-entry.mjs +39 -50
  52. package/src/surfaces/web/research-browser-entry.mjs +48 -46
  53. package/src/surfaces/web/spider-fly-browser-entry.mjs +76 -40
  54. package/src/surfaces/web/sprites-browser-entry.mjs +28 -32
  55. package/src/surfaces/web/tmct-surface.mjs +147 -0
  56. package/src/surfaces/web/turn-session.mjs +124 -0
  57. package/src/tools/definitions.mjs +30 -0
  58. package/src/tools/handlers/index.mjs +6 -3
  59. package/src/tools/handlers/kit.mjs +19 -2
  60. package/src/tools/handlers/tmct-ask.mjs +11 -6
  61. package/src/tools/handlers/tmct-ingest.mjs +5 -1
  62. package/src/tools/handlers/tmct-related.mjs +4 -4
  63. package/src/tools/handlers/tmct-sprite.mjs +147 -0
  64. package/src/tools/memory-fallthrough.mjs +9 -2
  65. package/src/tools/server.mjs +37 -6
@@ -45,7 +45,7 @@
45
45
  // why the mechanism exists, just not exercised by any class shown here).
46
46
 
47
47
  import { classAncestorChain, SPRITE_REGISTRY } from "../domain/sprite-map.mjs";
48
- import { resolveSpriteAsset } from "../domain/sprite-templates.mjs";
48
+ import { resolveSpriteAsset, matchConstraints } from "../domain/sprite-templates.mjs";
49
49
  import { MATERIAL_PALETTE } from "../domain/sprite-materials.mjs";
50
50
  import { spriteFactRows } from "../domain/sprite-facts.mjs";
51
51
  import { SEED_TAXONOMY } from "../domain/spider-fly-world.mjs";
@@ -247,7 +247,16 @@ export function parameterVariantsFor(template) {
247
247
  * generic root-fallback shape, not its own silhouette (this module's own
248
248
  * header names the 19 large-tier classes this applies to). Every
249
249
  * [parameters.*] value and every [match] variant renders through the real
250
- * resolveSpriteAsset with the exact property fact it declares. A class
250
+ * resolveSpriteAsset with the exact property facts it declares and a
251
+ * parameter sitting ON a [match] variant is rendered with BOTH facts, so a
252
+ * facing profile's mood swatch shows the profile wearing that mood and is
253
+ * labeled for the pair ("left + happy"). Rendering it on the parameter fact
254
+ * alone would resolve a different template altogether (the front-facing
255
+ * one) and label it as if it belonged to the profile. A value that a
256
+ * match-free template of the same class already offers is listed once, from
257
+ * that template: the facing pair and the `*-with-emotion.toml` file both
258
+ * carry all six moods, and one swatch per mood is the catalog's unit. A
259
+ * class
251
260
  * with templates but no plain among them also gets one extra swatch,
252
261
  * labeled `fallback: true`, showing exactly what the real resolver returns
253
262
  * for that class with NO taught fact — honest engine behaviour, never
@@ -264,19 +273,33 @@ export function tierSwatchesFor(cls, templates, registry, tier) {
264
273
  svg: resolveSpriteAsset(cls, [], [], templates, registry, { instanceKey: `${cls}-${tier}-plain` }),
265
274
  });
266
275
  }
276
+ const valueKey = (v) => `${v.paramName}:${v.rawValue}`;
277
+ const shownByUnmatchedTemplate = new Set(
278
+ forClass.filter((t) => !t.match).flatMap((t) => parameterVariantsFor(t).map(valueKey)),
279
+ );
267
280
  for (const t of forClass) {
268
- if (t.match) {
269
- const propertyFacts = [{ predicate: t.match.property, object: t.match.value }];
281
+ // A variant may require several facts at once (sprite-templates.mjs's own
282
+ // matchConstraints), so the swatch is rendered with EVERY fact its
283
+ // [match] asks for and labeled for the whole set — "left + moving" for a
284
+ // combined facing-and-pose file. A one-constraint variant reads exactly
285
+ // as it did before the plural spelling existed.
286
+ const constraints = matchConstraints(t);
287
+ const matchFact = constraints.map((c) => ({ predicate: c.property, object: c.value }));
288
+ const variantLabel = constraints.map((c) => c.value).join(" + ");
289
+ const variantKey = constraints.map((c) => c.value).join("-");
290
+ const matchPrefix = constraints.length ? `${variantLabel} + ` : "";
291
+ if (constraints.length) {
270
292
  swatches.push({
271
- tier, label: t.match.value, kind: "variant", property: t.match.property,
272
- svg: resolveSpriteAsset(cls, [], propertyFacts, templates, registry, { instanceKey: `${cls}-${tier}-match-${t.match.value}` }),
293
+ tier, label: variantLabel, kind: "variant", property: constraints.map((c) => c.property).join(" + "),
294
+ svg: resolveSpriteAsset(cls, [], matchFact, templates, registry, { instanceKey: `${cls}-${tier}-match-${variantKey}` }),
273
295
  });
274
296
  }
275
297
  for (const v of parameterVariantsFor(t)) {
276
- const propertyFacts = [{ predicate: v.property, object: v.rawValue }];
298
+ if (constraints.length && shownByUnmatchedTemplate.has(valueKey(v))) continue;
299
+ const propertyFacts = [...matchFact, { predicate: v.property, object: v.rawValue }];
277
300
  swatches.push({
278
- tier, label: v.rawValue, kind: "material", property: v.property, treatment: v.treatment,
279
- svg: resolveSpriteAsset(cls, [], propertyFacts, templates, registry, { instanceKey: `${cls}-${tier}-${v.paramName}-${v.rawValue}` }),
301
+ tier, label: `${matchPrefix}${v.rawValue}`, kind: "material", property: v.property, treatment: v.treatment,
302
+ svg: resolveSpriteAsset(cls, [], propertyFacts, templates, registry, { instanceKey: `${cls}-${tier}-${matchPrefix ? `${variantKey}-` : ""}${v.paramName}-${v.rawValue}` }),
280
303
  });
281
304
  }
282
305
  }
@@ -311,121 +334,95 @@ export function buildSpriteCatalogEntries({ iconTemplates = [], largeTemplates =
311
334
  });
312
335
  }
313
336
 
314
- // ---- scene composer (pure) ----
337
+ // ---- scene composer ----
315
338
  //
316
- // The free-text "there is a..." box (PLAN_GAMES_UPLIFT_V3.md's own precedent:
317
- // adventure-viz.mjs's roomSceneObjects/room-frame) over THIS page's own
318
- // already-real classes never a general NLU pass. extractSceneItems is the
319
- // one pure, unit-testable piece; the DOM index it's matched against
320
- // (className -> real swatch labels, read straight off this page's own
339
+ // The free-text "there is a..." box (adventure-viz.mjs's roomSceneObjects/
340
+ // room-frame is the same shape) over THIS page's own already-real classes.
341
+ // The parser itself is scene-compose.mjs, which resolves a typed class name
342
+ // through the real resolver; this page owns only the class index it is matched
343
+ // against (className -> real swatch labels, read straight off this page's own
321
344
  // already-rendered `.card`/`.swatch-label` markup at load time — see this
322
345
  // module's own header for why that beats re-embedding the same SVG data a
323
- // second time) is built client-side in the inline script below, since
324
- // walking rendered DOM has no meaning in this pure module.
325
-
326
- /** `text`'s lowercase word runs with their token index, the unit
327
- * extractSceneItems matches class names against — punctuation never fuses
328
- * two real words into one token nor splits one real word into two. */
329
- function tokenizeSceneText(text) {
330
- const tokens = [];
331
- const re = /[A-Za-z]+/g;
332
- let m;
333
- while ((m = re.exec(String(text ?? "")))) tokens.push({ word: m[0].toLowerCase() });
334
- return tokens;
346
+ // second time), which is built client-side, since walking rendered DOM has no
347
+ // meaning in this pure module.
348
+
349
+ export { extractSceneItems } from "../domain/scene-compose.mjs";
350
+
351
+ // ---- the catalog question lane ----
352
+ //
353
+ // There isn't one. Every catalog-shaped question the dock takes — "how many
354
+ // sprite classes are there", "list the sprite classes", "what parameters does
355
+ // a person sprite take", "what emotions does a person sprite accept" — is
356
+ // answered by the ordinary chat engine reading the same sprite-facts rows this
357
+ // page embeds, through the membership, count and property lanes it already runs
358
+ // for every other caller. The predicate spellings sprite-facts.mjs mints
359
+ // (mgx:take-parameter, mgx:accept-emotion, mgx:offer-variant) are what those
360
+ // lanes read, so the page hands the line over and renders what comes back.
361
+
362
+ // ---- the animated swatches (pure) ----
363
+ //
364
+ // Each card's large tier carries three animated swatches, one per axis the
365
+ // sprite tier actually varies on: mood (mgx:feels), facing (mgx:faces) and
366
+ // pose (mgx:pose). The page builds their frames client-side out of its own
367
+ // already-rendered swatches (see the inline script below — the same
368
+ // read-the-DOM posture the scene composer's class index takes), but the
369
+ // frame ORDER and the tempo are real logic worth pinning on their own, so
370
+ // they live here as pure functions the page splices in by `.toString()`.
371
+ // One delay constant serves all three, so the three can't drift apart into
372
+ // three tempos.
373
+ //
374
+ // A `frame` is `{svg, label}`. An empty return means there is nothing worth
375
+ // animating for that class, and the page leaves its static swatch alone.
376
+
377
+ /** The step between animated swatch frames, in milliseconds — the mood
378
+ * cycle's own established tempo, shared by the facing sweep and the pose
379
+ * toggle. */
380
+ export const CYCLE_FRAME_DELAY_MS = 800;
381
+
382
+ /** The turntable order the facing sweep steps through: one continuous
383
+ * rotation from the sprite's own left profile round to its right. `null`
384
+ * is the centre pose — the plain sprite, no mgx:faces fact at all — which
385
+ * sits in the middle of the sweep exactly where the geometry puts it, so
386
+ * the turn reads as a rotation rather than a jump back through front. */
387
+ export const FACING_TURN_ORDER = Object.freeze(["left", "half-left", null, "half-right", "right"]);
388
+
389
+ /** The mood cycle's frames: the class's plain sprite, then every mood it
390
+ * has a rendered swatch for, in the order the templates declare them. Two
391
+ * moods is the floor — one mood alternating with plain is a blink, not a
392
+ * cycle. Pure; self-contained (no outer refs), `.toString()`-splice safe. */
393
+ export function moodFrameSequence(plainFrame, moodFrames) {
394
+ const moods = (moodFrames || []).filter(Boolean);
395
+ if (moods.length < 2) return [];
396
+ return plainFrame ? [plainFrame, ...moods] : moods;
335
397
  }
336
398
 
337
- /** Every real catalog class the free-typed `text` names, in the order each
338
- * first appears, paired with the real material label (one of that SAME
339
- * class's own swatch labels never another class's, never a fabricated
340
- * one) immediately preceding it, or `null`. `classIndex` is
341
- * `{className: {materials}}` with `materials` keyed by lowercase label
342
- * (sprite-catalog-viz's own client-side `buildClassIndexFromDom` output, or
343
- * an equivalent test fixture) — a class name absent from `classIndex` can
344
- * never match, and a modifier word that isn't one of ITS matched class's
345
- * own material labels is silently dropped rather than guessed at, the same
346
- * honest-miss posture an unrecognized class name gets (an unmatched word,
347
- * e.g. "red" before a lamp with no red material, is never an error, just
348
- * silently not drawn). A multi-word class name (e.g. "body of water") is
349
- * checked, at every token position, before any shorter class that would
350
- * otherwise claim part of it — candidates are tried longest-word-count
351
- * first, so the longer name always wins the position it starts at. Pure. */
352
- export function extractSceneItems(text, classIndex) {
353
- const index = classIndex || {};
354
- const candidates = Object.keys(index)
355
- .map((name) => ({ name, words: name.toLowerCase().split(/\s+/).filter(Boolean) }))
356
- .filter((c) => c.words.length)
357
- .sort((a, b) => b.words.length - a.words.length || b.name.length - a.name.length);
358
- const tokens = tokenizeSceneText(text);
359
- const used = new Array(tokens.length).fill(false);
360
- const items = [];
361
- for (let i = 0; i < tokens.length; i += 1) {
362
- if (used[i]) continue;
363
- const hit = candidates.find(({ words }) => {
364
- if (i + words.length > tokens.length) return false;
365
- for (let k = 0; k < words.length; k += 1) {
366
- if (used[i + k] || tokens[i + k].word !== words[k]) return false;
367
- }
368
- return true;
369
- });
370
- if (!hit) continue;
371
- let materialLabel = null;
372
- if (i > 0 && !used[i - 1]) {
373
- const materials = index[hit.name]?.materials || {};
374
- const prevWord = tokens[i - 1].word;
375
- if (Object.prototype.hasOwnProperty.call(materials, prevWord)) {
376
- materialLabel = prevWord;
377
- used[i - 1] = true;
378
- }
399
+ /** The facing sweep's frames: `order` (FACING_TURN_ORDER) filtered down to
400
+ * the facings this class actually has a rendered swatch for, with
401
+ * `centreFrame` the plain, undirected sprite dropped into the centre
402
+ * slot. `facingFrames` is keyed by each swatch's own label, which is its
403
+ * real mgx:faces value. A class with one facing or none has no sweep.
404
+ * Pure; self-contained, `.toString()`-splice safe. */
405
+ export function turnFrameSequence(order, centreFrame, facingFrames) {
406
+ const byFacing = facingFrames || {};
407
+ const frames = [];
408
+ for (const facing of order || []) {
409
+ if (facing === null) {
410
+ if (centreFrame) frames.push({ svg: centreFrame.svg, label: "centre" });
411
+ } else if (byFacing[facing]) {
412
+ frames.push({ svg: byFacing[facing].svg, label: facing });
379
413
  }
380
- for (let k = 0; k < hit.words.length; k += 1) used[i + k] = true;
381
- items.push({ className: hit.name, materialLabel });
382
414
  }
383
- return items;
415
+ return frames.length >= 2 ? frames : [];
384
416
  }
385
417
 
386
- // ---- catalog question lane (pure) ----
387
-
388
- /** The closed set of catalog-shaped questions the chat dock answers straight
389
- * off THIS page's own embedded sprite-facts rows (src/domain/
390
- * sprite-facts.mjs's output), before a line ever reaches the full engine —
391
- * the same closed-template posture extractSceneItems takes for the scene
392
- * composer. Returns { answer, grounding } (grounding = how many real rows
393
- * the answer was read from) or null for ANY question this lane doesn't own,
394
- * including a catalog-shaped question about a class with no rows on record —
395
- * the caller falls through to the engine, whose refusal is the honest miss.
396
- * Self-contained (no outer refs), `.toString()`-splice safe. Pure. */
397
- export function answerSpriteQuestion(text, rows) {
398
- const all = Array.isArray(rows) ? rows : [];
399
- const q = String(text ?? "").trim().toLowerCase().replace(/[?.!\s]+$/, "");
400
-
401
- if (/^what (?:classes|sprites|sprite classes) (?:can you|do you|are there)(?:\s+(?:render|draw|show))?$/.test(q)) {
402
- const classes = all
403
- .filter((r) => r.predicate === "rdf:type" && r.object === "sprite class")
404
- .map((r) => String(r.subject).replace(/ sprite$/, ""));
405
- if (!classes.length) return null;
406
- const sample = classes.slice(0, 10).join(", ");
407
- return {
408
- answer: `${classes.length} sprite classes are on record — ${sample}, … (the catalog below lists every one).`,
409
- grounding: classes.length,
410
- };
411
- }
412
-
413
- const m = q.match(/^(?:what|which) parameters? (?:does|do|can|will) (?:a |an |the )?(.+?)(?: sprite)? (?:take|accept|use)s?$/);
414
- if (m) {
415
- const subject = `${m[1]} sprite`;
416
- const params = all.filter((r) => r.subject === subject && r.predicate === "mgx:take-parameter").map((r) => r.object);
417
- if (!params.length) return null;
418
- let grounding = params.length;
419
- const parts = params.map((p) => {
420
- const values = all.filter((r) => r.subject === subject && r.predicate === `mgx:accept-${p}`).map((r) => r.object);
421
- grounding += values.length;
422
- return values.length ? `${p} (${values.join(", ")})` : p;
423
- });
424
- const lead = params.length === 1 ? "one parameter" : `${params.length} parameters`;
425
- return { answer: `a ${subject} takes ${lead}: ${parts.join("; ")}.`, grounding };
426
- }
427
-
428
- return null;
418
+ /** The pose toggle's frames: the class's plain idle sprite alternating with
419
+ * its mgx:pose = "moving" one. Both have to be there — a class with no
420
+ * moving sprite has no second state to toggle into, and gets nothing
421
+ * rather than a still frame pretending to animate. Pure; self-contained,
422
+ * `.toString()`-splice safe. */
423
+ export function movingFrameSequence(idleFrame, movingFrame) {
424
+ if (!idleFrame || !movingFrame) return [];
425
+ return [{ svg: idleFrame.svg, label: "idle" }, { svg: movingFrame.svg, label: "moving" }];
429
426
  }
430
427
 
431
428
  // ---- rendering ----
@@ -505,14 +502,16 @@ function sectionHtml(group, entries, { clusterBy = null } = {}) {
505
502
  * ancestor chains.
506
503
  *
507
504
  * `spritesBundleAvailable: true` (ledger-viz.mjs's own ledgerBundleAvailable
508
- * idiom) is what adds the chat dock at all: the page then embeds the
509
- * sprite-facts rows (src/domain/sprite-facts.mjs, derived purely from the
505
+ * idiom) is what adds the two interactive panels at all: the page then embeds
506
+ * the sprite-facts rows (src/domain/sprite-facts.mjs, derived purely from the
510
507
  * same two template sets) and references the sibling
511
508
  * ./sprites-browser.bundle.js scripts/build-demo-site.mjs builds alongside
512
- * it. Left false, the page renders exactly as before no dock, no bundle
513
- * reference, nothing extra to 404 including the favicon links, since the
514
- * CLI's standalone export (also `spritesBundleAvailable: false`) can't carry
515
- * a dangling relative ./favicon.svg either. */
509
+ * it. Both panels need that bundle the dock for its chat session, the scene
510
+ * composer for the resolver its parser asks. Left false, the page is the
511
+ * catalog alone: no dock, no composer, no bundle reference, nothing extra to
512
+ * 404 including the favicon links, since the CLI's standalone export (also
513
+ * `spritesBundleAvailable: false`) can't carry a dangling relative
514
+ * ./favicon.svg either. */
516
515
  export function renderSpriteCatalogHtml({ title = DEFAULT_TITLE, iconTemplates = [], largeTemplates = [], factRows = [], spritesBundleAvailable = false } = {}) {
517
516
  const entries = buildSpriteCatalogEntries({ iconTemplates, largeTemplates, factRows });
518
517
  const totalSwatches = entries.reduce((n, e) => n + e.iconSwatches.length + e.largeSwatches.length, 0);
@@ -562,22 +561,56 @@ export function renderSpriteCatalogHtml({ title = DEFAULT_TITLE, iconTemplates =
562
561
  </form>
563
562
  <div class="pills" id="dockPills" role="group" aria-label="quick questions to ask">
564
563
  <button type="button" class="pill" data-q="what parameters does a person sprite take?">person parameters</button>
565
- <button type="button" class="pill" data-q="what parameters does a cabinet sprite take?">cabinet parameters</button>
566
- <button type="button" class="pill" data-q="what classes can you render?">classes on record</button>
564
+ <button type="button" class="pill" data-q="what emotions does a person sprite accept?">person emotions</button>
565
+ <button type="button" class="pill" data-q="what materials does a cabinet sprite accept?">cabinet materials</button>
566
+ <button type="button" class="pill" data-q="how many sprite classes are there?">classes on record</button>
567
567
  <button type="button" class="pill" data-q="what is a portrait sprite?">about the portrait sprite</button>
568
568
  </div>
569
569
  <div class="dock-status mono" id="dockStatus">loading the engine&hellip;</div>
570
570
  </section>
571
571
  </div>`;
572
572
 
573
+ // The bundle carries the scene composer's parser as well as the dock's
574
+ // session, and the catalog script below calls it directly, so it loads ahead
575
+ // of that script rather than beside the dock's own wiring at the end.
576
+ const spriteBundleScript = !spritesBundleAvailable ? "" : `<script src="./sprites-browser.bundle.js"></script>`;
577
+
578
+ // The composer resolves a typed class name through the real resolver, which
579
+ // reaches the page in that bundle. Without it there is nothing to type into,
580
+ // so the panel stays out rather than rendering a box that can't answer.
581
+ const composerHtml = !spritesBundleAvailable ? "" : `<div class="composer">
582
+ <section class="panel compose-panel" aria-label="Describe a scene">
583
+ <h2>describe a scene</h2>
584
+ <form class="composeform" id="composeForm">
585
+ <span class="prompt mono">there is a</span>
586
+ <input id="composeq" type="text" autocomplete="off"
587
+ placeholder="red lamp, a doctor with a hat, and a cabinet"
588
+ aria-label="Continue the sentence: there is a&hellip;">
589
+ </form>
590
+ <div class="pills" id="composePills" role="group" aria-label="quick words to add">
591
+ <button type="button" class="pill" data-fill="doctor">doctor</button>
592
+ <button type="button" class="pill" data-fill="hat">hat</button>
593
+ <button type="button" class="pill" data-fill="wood cabinet">wood cabinet</button>
594
+ <button type="button" class="pill" data-fill="glass lamp">glass lamp</button>
595
+ <button type="button" class="pill" data-fill="cat">cat</button>
596
+ <button type="button" class="pill" data-fill="garden">garden</button>
597
+ </div>
598
+ </section>
599
+ <section class="panel viewer-panel" aria-label="The composed scene">
600
+ <h2>the scene</h2>
601
+ <div class="scene-frame" id="sceneFrame">
602
+ <div class="scene-row" id="sceneRow" aria-live="polite"></div>
603
+ <span class="empty-note" id="sceneEmpty">nothing recognized yet. Try &ldquo;a doctor with a hat, and a cabinet&rdquo;.</span>
604
+ </div>
605
+ </section>
606
+ </div>`;
607
+
573
608
  const dockScripts = !spritesBundleAvailable ? "" : `<script>
574
609
  const SPRITE_CHAT = ${embedJson({ rows: dockRows })};
575
610
  </script>
576
- <script src="./sprites-browser.bundle.js"></script>
577
611
  <script>
578
612
  (function () {
579
613
  "use strict";
580
- const answerSpriteQuestion = ${answerSpriteQuestion.toString()};
581
614
  const dockLogEl = document.getElementById("dockLog");
582
615
  const dockFormEl = document.getElementById("dockForm");
583
616
  const dockqEl = document.getElementById("dockq");
@@ -598,7 +631,7 @@ const SPRITE_CHAT = ${embedJson({ rows: dockRows })};
598
631
  import("./vendor/wink.js"),
599
632
  winkTimeout(WINK_LOAD_TIMEOUT_MS, "wink vendor asset load timed out"),
600
633
  ]);
601
- tmctSprites.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
634
+ tmct.page.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
602
635
  } catch (err) {
603
636
  console.warn("tmct sprites: the wink vendor asset failed to load, continuing without the lemma/POS tier", err);
604
637
  }
@@ -626,12 +659,11 @@ const SPRITE_CHAT = ${embedJson({ rows: dockRows })};
626
659
  if (!q) return;
627
660
  dockqEl.value = "";
628
661
  addDockLine("u", q);
629
- const local = answerSpriteQuestion(q, SPRITE_CHAT.rows);
630
- if (local) { addDockLine("a grounded", local.answer); return; }
631
662
  if (!session) { addDockLine("a miss", "the engine is still loading. Try again in a moment."); return; }
632
663
  withLock(async () => {
633
- const result = await session.turn(q);
634
- addDockLine(result.record && result.record.miss ? "a miss" : "a", result.answer);
664
+ const result = await tmct.turn(q);
665
+ const missed = !!(result.record && result.record.miss);
666
+ addDockLine(missed ? "a miss" : "a grounded", result.answer);
635
667
  });
636
668
  });
637
669
 
@@ -645,7 +677,7 @@ const SPRITE_CHAT = ${embedJson({ rows: dockRows })};
645
677
  (async () => {
646
678
  try {
647
679
  await tryLoadWink();
648
- session = await tmctSprites.createSpriteCatalogSession({ factRows: SPRITE_CHAT.rows });
680
+ session = await tmct.open({ factRows: SPRITE_CHAT.rows });
649
681
  dockqEl.disabled = false;
650
682
  dockStatusEl.textContent = SPRITE_CHAT.rows.length + " sprite facts on record. Ask away, or use a quick question.";
651
683
  } catch (err) {
@@ -757,32 +789,7 @@ ${dockCss}</style>
757
789
  <h1>Sprites</h1>
758
790
  <span class="doc-sub">tmct &middot; the sprite library</span>
759
791
  </header>
760
- <div class="composer">
761
- <section class="panel compose-panel" aria-label="Describe a scene">
762
- <h2>describe a scene</h2>
763
- <form class="composeform" id="composeForm">
764
- <span class="prompt mono">there is a</span>
765
- <input id="composeq" type="text" autocomplete="off"
766
- placeholder="red lamp, a doctor with a hat, and a cabinet"
767
- aria-label="Continue the sentence: there is a&hellip;">
768
- </form>
769
- <div class="pills" id="composePills" role="group" aria-label="quick words to add">
770
- <button type="button" class="pill" data-fill="doctor">doctor</button>
771
- <button type="button" class="pill" data-fill="hat">hat</button>
772
- <button type="button" class="pill" data-fill="wood cabinet">wood cabinet</button>
773
- <button type="button" class="pill" data-fill="glass lamp">glass lamp</button>
774
- <button type="button" class="pill" data-fill="cat">cat</button>
775
- <button type="button" class="pill" data-fill="garden">garden</button>
776
- </div>
777
- </section>
778
- <section class="panel viewer-panel" aria-label="The composed scene">
779
- <h2>the scene</h2>
780
- <div class="scene-frame" id="sceneFrame">
781
- <div class="scene-row" id="sceneRow" aria-live="polite"></div>
782
- <span class="empty-note" id="sceneEmpty">nothing recognized yet. Try &ldquo;a doctor with a hat, and a cabinet&rdquo;.</span>
783
- </div>
784
- </section>
785
- </div>
792
+ ${composerHtml}
786
793
  ${dockHtml}
787
794
  <div class="topbar">
788
795
  <nav aria-label="Jump to group">${navHtml}</nav>
@@ -797,6 +804,7 @@ ${dockCss}</style>
797
804
  <script>
798
805
  const SPRITE_CATALOG = ${pageData};
799
806
  </script>
807
+ ${spriteBundleScript}
800
808
  <script>
801
809
  (function () {
802
810
  "use strict";
@@ -824,10 +832,10 @@ const SPRITE_CATALOG = ${pageData};
824
832
  // THIS page's own already-rendered card and swatch-label markup (this
825
833
  // module's own header explains why: never a second embedded copy of the
826
834
  // same swatch data), so the composed scene below only ever shows a sprite
827
- // this same page already proved the resolver draws.
835
+ // this same page already proved the resolver draws. Which class a typed
836
+ // word names is the bundle's extractSceneItems, which asks the real
837
+ // resolver — the page never matches a class name itself.
828
838
  const esc = ${escapeHtml.toString()};
829
- const tokenizeSceneText = ${tokenizeSceneText.toString()};
830
- const extractSceneItems = ${extractSceneItems.toString()};
831
839
 
832
840
  function buildClassIndexFromDom() {
833
841
  const index = {};
@@ -854,72 +862,83 @@ const SPRITE_CATALOG = ${pageData};
854
862
  return index;
855
863
  }
856
864
 
857
- const classIndex = buildClassIndexFromDom();
858
- const composeqEl = document.getElementById("composeq");
859
- const composeFormEl = document.getElementById("composeForm");
860
- const composePillsEl = document.getElementById("composePills");
861
- const sceneRowEl = document.getElementById("sceneRow");
862
- const sceneEmptyEl = document.getElementById("sceneEmpty");
863
-
864
- function renderScene(text) {
865
- const items = extractSceneItems(text, classIndex);
866
- if (!items.length) {
867
- sceneRowEl.innerHTML = "";
868
- sceneEmptyEl.hidden = false;
869
- return;
865
+ function wireSceneComposer() {
866
+ const composeqEl = document.getElementById("composeq");
867
+ if (!composeqEl) return;
868
+ const extractSceneItems = tmct.page.extractSceneItems;
869
+ const composeFormEl = document.getElementById("composeForm");
870
+ const composePillsEl = document.getElementById("composePills");
871
+ const sceneRowEl = document.getElementById("sceneRow");
872
+ const sceneEmptyEl = document.getElementById("sceneEmpty");
873
+ const classIndex = buildClassIndexFromDom();
874
+
875
+ function renderScene(text) {
876
+ const items = extractSceneItems(text, classIndex);
877
+ if (!items.length) {
878
+ sceneRowEl.innerHTML = "";
879
+ sceneEmptyEl.hidden = false;
880
+ return;
881
+ }
882
+ sceneEmptyEl.hidden = true;
883
+ sceneRowEl.innerHTML = items.map((item) => {
884
+ const entry = classIndex[item.className];
885
+ if (!entry) return "";
886
+ const svg = (item.materialLabel && entry.materials[item.materialLabel]) || entry.defaultSvg || "";
887
+ const label = item.materialLabel ? item.materialLabel + " " + item.className : item.className;
888
+ return '<div class="scene-card"><div class="scene-sprite">' + svg + '</div><div class="scene-label">' + esc(label) + "</div></div>";
889
+ }).join("");
870
890
  }
871
- sceneEmptyEl.hidden = true;
872
- sceneRowEl.innerHTML = items.map((item) => {
873
- const entry = classIndex[item.className];
874
- if (!entry) return "";
875
- const svg = (item.materialLabel && entry.materials[item.materialLabel]) || entry.defaultSvg || "";
876
- const label = item.materialLabel ? item.materialLabel + " " + item.className : item.className;
877
- return '<div class="scene-card"><div class="scene-sprite">' + svg + '</div><div class="scene-label">' + esc(label) + "</div></div>";
878
- }).join("");
879
- }
880
891
 
881
- composeqEl.addEventListener("input", () => renderScene(composeqEl.value));
882
- composeFormEl.addEventListener("submit", (e) => { e.preventDefault(); renderScene(composeqEl.value); });
883
- composePillsEl.addEventListener("click", (e) => {
884
- const btn = e.target.closest(".pill");
885
- if (!btn) return;
886
- const phrase = btn.dataset.fill || "";
887
- const current = composeqEl.value.trim();
888
- composeqEl.value = current ? current + ", a " + phrase : phrase;
889
- composeqEl.focus();
890
- renderScene(composeqEl.value);
891
- });
892
- renderScene("");
893
-
894
- // ---- the variant cycle — a card whose large tier carries expression
895
- // (mgx:feels) or direction (mgx:faces) variants gains one extra swatch
896
- // that steps through them: plain, then each direction, then each face.
897
- // Frames are the card's own already-rendered swatches (the same
898
- // read-the-DOM posture classIndex takes above never a second embedded
899
- // copy), and one shared interval steps every cycle in sync. Reduced
900
- // motion disables the auto-step only; the frame itself is a button, so a
901
- // click or Enter always advances one frame by hand. Injected AFTER
902
- // buildClassIndexFromDom ran, so a cycle frame never leaks into the
903
- // composer's material index.
904
- const CYCLE_PROPERTIES = ["mgx:faces", "mgx:feels"];
892
+ composeqEl.addEventListener("input", () => renderScene(composeqEl.value));
893
+ composeFormEl.addEventListener("submit", (e) => { e.preventDefault(); renderScene(composeqEl.value); });
894
+ composePillsEl.addEventListener("click", (e) => {
895
+ const btn = e.target.closest(".pill");
896
+ if (!btn) return;
897
+ const phrase = btn.dataset.fill || "";
898
+ const current = composeqEl.value.trim();
899
+ composeqEl.value = current ? current + ", a " + phrase : phrase;
900
+ composeqEl.focus();
901
+ renderScene(composeqEl.value);
902
+ });
903
+ renderScene("");
904
+ }
905
+ wireSceneComposer();
906
+
907
+ // ---- the animated swatches a card's large tier gains one moving
908
+ // swatch per axis its own templates actually vary on: mood (mgx:feels),
909
+ // facing (mgx:faces) and pose (mgx:pose). Frames are the card's own
910
+ // already-rendered swatches (the same read-the-DOM posture classIndex
911
+ // takes above never a second embedded copy), ordered by the pure
912
+ // *FrameSequence functions spliced in below, and one shared interval
913
+ // steps every cycle in sync at one shared delay. The mood cycle leads the
914
+ // row; the facing sweep and the pose toggle take over the static plain
915
+ // and happy swatches' own places rather than adding cells, so the row
916
+ // keeps its width. Reduced motion disables the auto-step only; each frame
917
+ // 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
+ const CYCLE_FRAME_DELAY_MS = ${CYCLE_FRAME_DELAY_MS};
921
+ const FACING_TURN_ORDER = ${embedJson(FACING_TURN_ORDER)};
922
+ const MOOD_PROPERTY = "mgx:feels";
923
+ const FACING_PROPERTY = "mgx:faces";
924
+ const POSE_PROPERTY = "mgx:pose";
925
+ const moodFrameSequence = ${moodFrameSequence.toString()};
926
+ const turnFrameSequence = ${turnFrameSequence.toString()};
927
+ const movingFrameSequence = ${movingFrameSequence.toString()};
905
928
  const cycleSteppers = [];
906
- for (const card of cards) {
907
- const swatchRow = card.querySelector('.tier-row[data-tier="large"] .swatches');
908
- if (!swatchRow) continue;
909
- const frames = [];
910
- for (const swatch of swatchRow.querySelectorAll(".swatch")) {
911
- if (CYCLE_PROPERTIES.indexOf(swatch.dataset.property || "") === -1) continue;
912
- const img = swatch.querySelector(".swatch-img");
913
- const label = swatch.querySelector(".swatch-label");
914
- if (img && label) frames.push({ svg: img.innerHTML, label: label.textContent.trim() });
915
- }
916
- if (frames.length < 2) continue;
917
- const baseImg = swatchRow.querySelector(".swatch.plain .swatch-img, .swatch.fallback .swatch-img");
918
- if (baseImg) frames.unshift({ svg: baseImg.innerHTML, label: card.dataset.cls });
929
+
930
+ function frameFromSwatch(swatch) {
931
+ const img = swatch && swatch.querySelector(".swatch-img");
932
+ const label = swatch && swatch.querySelector(".swatch-label");
933
+ if (!img || !label) return null;
934
+ return { svg: img.innerHTML, label: label.textContent.trim() };
935
+ }
936
+
937
+ function makeCycleSwatch(frames, ariaLabel, kind) {
919
938
  const holder = document.createElement("div");
920
- holder.className = "swatch large cycle";
921
- holder.innerHTML = '<button type="button" class="swatch-img" aria-label="step through the '
922
- + esc(card.dataset.cls) + ' variants"></button><div class="swatch-caption"><span class="swatch-label"></span></div>';
939
+ holder.className = "swatch large cycle cycle-" + kind;
940
+ holder.innerHTML = '<button type="button" class="swatch-img" aria-label="' + esc(ariaLabel)
941
+ + '"></button><div class="swatch-caption"><span class="swatch-label"></span></div>';
923
942
  const frameImgEl = holder.querySelector(".swatch-img");
924
943
  const frameLabelEl = holder.querySelector(".swatch-label");
925
944
  let frameIndex = 0;
@@ -930,12 +949,43 @@ const SPRITE_CATALOG = ${pageData};
930
949
  const stepFrame = () => { frameIndex = (frameIndex + 1) % frames.length; showFrame(); };
931
950
  frameImgEl.addEventListener("click", stepFrame);
932
951
  showFrame();
933
- swatchRow.insertBefore(holder, swatchRow.firstChild);
934
952
  cycleSteppers.push(stepFrame);
953
+ return holder;
954
+ }
955
+
956
+ for (const card of cards) {
957
+ const swatchRow = card.querySelector('.tier-row[data-tier="large"] .swatches');
958
+ if (!swatchRow) continue;
959
+ const cls = card.dataset.cls;
960
+ const rows = Array.from(swatchRow.querySelectorAll(".swatch"))
961
+ .map((el) => ({ el, property: el.dataset.property || "", frame: frameFromSwatch(el) }))
962
+ .filter((r) => r.frame);
963
+ const baseSwatch = swatchRow.querySelector(".swatch.plain, .swatch.fallback");
964
+ const baseFrame = frameFromSwatch(baseSwatch);
965
+
966
+ const moodRows = rows.filter((r) => r.property === MOOD_PROPERTY);
967
+ const moodFrames = moodFrameSequence(baseFrame && { svg: baseFrame.svg, label: cls }, moodRows.map((r) => r.frame));
968
+ if (moodFrames.length) {
969
+ swatchRow.insertBefore(makeCycleSwatch(moodFrames, "step through the " + cls + " expressions", "mood"), swatchRow.firstChild);
970
+ }
971
+
972
+ const facingFrames = {};
973
+ for (const r of rows) { if (r.property === FACING_PROPERTY) facingFrames[r.frame.label] = r.frame; }
974
+ const turnFrames = turnFrameSequence(FACING_TURN_ORDER, baseFrame, facingFrames);
975
+ if (turnFrames.length && baseSwatch) {
976
+ swatchRow.replaceChild(makeCycleSwatch(turnFrames, "watch the " + cls + " turn through every direction", "turn"), baseSwatch);
977
+ }
978
+
979
+ const movingRow = rows.find((r) => r.property === POSE_PROPERTY && r.frame.label === "moving");
980
+ const movingFrames = movingFrameSequence(baseFrame, movingRow && movingRow.frame);
981
+ if (movingFrames.length) {
982
+ const happyRow = moodRows.find((r) => r.frame.label === "happy");
983
+ swatchRow.replaceChild(makeCycleSwatch(movingFrames, "watch the " + cls + " move", "moving"), (happyRow || movingRow).el);
984
+ }
935
985
  }
936
986
  const reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
937
987
  if (cycleSteppers.length && !reducedMotion) {
938
- setInterval(() => { for (const stepFrame of cycleSteppers) stepFrame(); }, 800);
988
+ setInterval(() => { for (const stepFrame of cycleSteppers) stepFrame(); }, CYCLE_FRAME_DELAY_MS);
939
989
  }
940
990
  })();
941
991
  </script>