@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
@@ -30,6 +30,33 @@
30
30
  // `bear-facing-left.toml` for mgx:faces = left) — the `[match]` table,
31
31
  // never the name, is what selects it.
32
32
  //
33
+ // A variant can require MORE THAN ONE fact at once, written as repeated
34
+ // `[[match]]` tables (the array-of-tables idiom data/templates/
35
+ // grammar-rules.toml's own `[[rule]]` already uses) instead of a single
36
+ // `[match]` one. Every entry must hold for the variant to be selected, so
37
+ // `bear-facing-left-moving.toml` asks for mgx:faces = left AND
38
+ // mgx:pose = moving and draws the left profile mid-stride. The two spellings
39
+ // are one constraint or many, never a different meaning: a lone `[match]`
40
+ // table is exactly a one-entry list, which is why every file authored before
41
+ // the plural spelling existed still resolves byte-for-byte as it did.
42
+ //
43
+ // Where two satisfied variants overlap, the one requiring MORE facts wins —
44
+ // an instance taught both the facing and the pose gets the combined art, and
45
+ // the same instance taught only the facing gets the plain profile, because
46
+ // the combined variant's second constraint no longer holds. Specificity is
47
+ // counted from the constraints, never read off the filename, so adding a
48
+ // file can't silently reorder what an existing instance resolves to.
49
+ //
50
+ // The two kinds compose rather than exclude each other: a VARIANT may also
51
+ // declare its own `[parameters.*]` tables, and once its `[match]` selects
52
+ // it those parameters fill from the instance's OTHER facts (the facing pair
53
+ // files each carry `[face]` + `[parameters.emotion]`, so a bear taught both
54
+ // mgx:faces = left and mgx:feels = happy renders the left profile wearing
55
+ // the happy face). A satisfied `[match]` is the instance naming this art
56
+ // directly, so a variant is never allowed to fall through the way an
57
+ // unfilled parameterized template is — any placeholder no fact filled is
58
+ // dropped instead, which keeps the returned markup complete.
59
+ //
33
60
  // A parameterized template's `[parameters.<name>]` table comes in two
34
61
  // shapes, picked by which of `placeholder`/`placeholders` it declares:
35
62
  // - single-placeholder (the shape above): one `placeholder` token, and
@@ -59,9 +86,10 @@
59
86
  //
60
87
  // Specificity order, checked at EACH term of the class's ancestor chain
61
88
  // (nearest first, sprite-map.mjs's own classAncestorChain) before moving to
62
- // the next ancestor: an exact fully-specific variant whose [match] is
63
- // satisfied > a parameterized template filled with an observed matching
64
- // value > a plain class template > (repeat at the next ancestor) > the
89
+ // the next ancestor: the satisfied fully-specific variant requiring the most
90
+ // facts (filled from its own [parameters.*] if it declares any) > a
91
+ // parameterized template filled with an observed matching value > a plain
92
+ // class template > (repeat at the next ancestor) > the
65
93
  // existing flat spriteRegistry entry for that same term (so a class not yet
66
94
  // migrated to its own template keeps resolving exactly as it did before this
67
95
  // module existed) > once the chain is exhausted, the same three-step check
@@ -70,14 +98,77 @@
70
98
  import { classAncestorChain } from "./sprite-map.mjs";
71
99
  import { namespaceSvgIds } from "./svg-instance-ids.mjs";
72
100
 
101
+ /** The predicate a variant matches on to pick a facing angle. */
102
+ export const FACING_PROPERTY = "mgx:faces";
103
+
104
+ /** The predicate a variant matches on to pick a pose. */
105
+ export const POSE_PROPERTY = "mgx:pose";
106
+
107
+ /** The facing angles a variant's [match] may require of mgx:faces — a
108
+ * turntable read as five steps, of which four are named. Centre is the
109
+ * ABSENT fact, never a word here: a figure with nothing on record about
110
+ * which way it faces already renders its own front-facing art, so spending
111
+ * a value on that would give one picture two names and let an instance ask
112
+ * for the front view two different ways. */
113
+ export const FACING_VALUES = Object.freeze(["left", "half-left", "half-right", "right"]);
114
+
115
+ /** The poses a variant's [match] may require of mgx:pose. At rest is the
116
+ * ABSENT fact, the same reasoning centre-facing gets above: a figure with no
117
+ * pose on record is standing still, and that is what every plain template
118
+ * already draws. "moving" is the one intermediate frame between two rests —
119
+ * the read a walk cycle needs and the only pose that has to exist before
120
+ * movement can be animated at all. */
121
+ export const POSE_VALUES = Object.freeze(["moving"]);
122
+
73
123
  /** Every template in `templates` whose `classes` list names `term`. */
74
124
  function templatesForClass(term, templates) {
75
125
  return (templates || []).filter((t) => Array.isArray(t?.classes) && t.classes.includes(term));
76
126
  }
77
127
 
78
- function matchSatisfied(match, propertyFacts) {
79
- if (!match || !match.property || match.value === undefined) return false;
80
- return (propertyFacts || []).some((f) => f.predicate === match.property && f.object === match.value);
128
+ /** One template's `[match]`/`[[match]]` tables as a flat list of raw entries,
129
+ * whichever of the two spellings it used the shape every reader wants
130
+ * before it decides anything, so no caller re-answers "did I get a table or
131
+ * a list of them" for itself. */
132
+ function matchEntries(template) {
133
+ const match = template?.match;
134
+ if (!match) return [];
135
+ return Array.isArray(match) ? match : [match];
136
+ }
137
+
138
+ /**
139
+ * Every `{property, value}` fact one template's `[match]` requires, all of
140
+ * which must hold for the variant to apply. A single `[match]` table yields
141
+ * one constraint, repeated `[[match]]` tables one each, and a template with
142
+ * no `[match]` at all yields none. An entry missing either half is dropped
143
+ * rather than treated as a wildcard — a half-written constraint must never
144
+ * widen what a variant claims to match (spriteTemplateProblems reports it as
145
+ * the authoring mistake it is). Pure.
146
+ */
147
+ export function matchConstraints(template) {
148
+ return matchEntries(template).filter((c) => c?.property && c.value !== undefined);
149
+ }
150
+
151
+ function matchSatisfied(constraints, propertyFacts) {
152
+ if (!constraints.length) return false;
153
+ return constraints.every((c) => (propertyFacts || []).some((f) => f.predicate === c.property && f.object === c.value));
154
+ }
155
+
156
+ /** The satisfied `[match]` variant among `candidates` requiring the MOST
157
+ * facts, or null when none is satisfied — so a combined facing-and-pose
158
+ * variant outranks the facing-only one it shares an angle with, and drops
159
+ * back to it the moment the instance stops carrying the pose. A tie keeps
160
+ * the earliest candidate, which is the load order the caller handed in. */
161
+ function bestSatisfiedVariant(candidates, propertyFacts) {
162
+ let best = null;
163
+ let bestCount = 0;
164
+ for (const t of candidates) {
165
+ const constraints = matchConstraints(t);
166
+ if (constraints.length <= bestCount) continue;
167
+ if (!matchSatisfied(constraints, propertyFacts)) continue;
168
+ best = t;
169
+ bestCount = constraints.length;
170
+ }
171
+ return best;
81
172
  }
82
173
 
83
174
  /** Substitute one matched `[parameters.*.values]` entry into `svg`: a plain
@@ -125,15 +216,47 @@ function parameterizedFillAll(template, propertyFacts) {
125
216
  return filledCount > 0 ? svg : null;
126
217
  }
127
218
 
219
+ /** Every placeholder token a template's own `[parameters.*]` tables name,
220
+ * across both shapes — the single `placeholder` and every token in a
221
+ * `placeholders` table. */
222
+ function declaredPlaceholderTokens(template) {
223
+ const tokens = [];
224
+ for (const param of Object.values(template.parameters || {})) {
225
+ if (param?.placeholder) tokens.push(param.placeholder);
226
+ for (const token of Object.values(param?.placeholders || {})) tokens.push(token);
227
+ }
228
+ return tokens;
229
+ }
230
+
231
+ /** `svg` with every token `template` declares that no observed fact filled
232
+ * dropped outright. Only the [match]-selected path wants this: a satisfied
233
+ * [match] means the instance asked for THIS art by name, so the variant
234
+ * can't fall through to a less specific template the way an unfilled
235
+ * parameterized template does, and dropping the leftover token is what
236
+ * keeps the returned markup complete rather than shipping a literal
237
+ * "{{FACE}}" into the page. */
238
+ function withUnfilledPlaceholdersDropped(template, svg) {
239
+ let out = svg;
240
+ for (const token of declaredPlaceholderTokens(template)) out = out.split(token).join("");
241
+ return out;
242
+ }
243
+
128
244
  /** Resolve ONE class term (no ancestor walk here — the caller repeats this
129
245
  * at every level of the chain) against the template set, in specificity
130
246
  * order: fully-specific match variant > parameterized template filled with
131
- * an observed value > plain class template. Returns the SVG string, or null
132
- * when nothing at this level matches. */
247
+ * an observed value > plain class template. Among satisfied match variants
248
+ * the most demanding one wins (bestSatisfiedVariant). A match variant that
249
+ * declares its own `[parameters.*]` is filled from them first, so a facing
250
+ * profile also carrying `[face]`/`[parameters.emotion]` renders the mood the
251
+ * instance's own mgx:feels fact names. Returns the SVG string, or null when
252
+ * nothing at this level matches. */
133
253
  function resolveAtTerm(term, propertyFacts, templates) {
134
254
  const candidates = templatesForClass(term, templates);
135
- const matched = candidates.find((t) => t.match && matchSatisfied(t.match, propertyFacts));
136
- if (matched) return matched.svg;
255
+ const matched = bestSatisfiedVariant(candidates, propertyFacts);
256
+ if (matched && !matched.parameters) return matched.svg;
257
+ if (matched) {
258
+ return withUnfilledPlaceholdersDropped(matched, parameterizedFillAll(matched, propertyFacts) || matched.svg);
259
+ }
137
260
  for (const t of candidates) {
138
261
  if (t.match || !t.parameters) continue;
139
262
  const filled = parameterizedFillAll(t, propertyFacts);
@@ -187,12 +310,22 @@ export function resolveSpriteAsset(className, factRows, propertyFacts, templates
187
310
  * names a `property` and exactly one of `placeholder`/`placeholders` (every
188
311
  * token named appears in `svg`), its `values` map is non-empty and every
189
312
  * entry matches the shape its own `placeholder`/`placeholders` choice
190
- * expects, a `[match]` table names both `property` and `value`, and
313
+ * expects, every `[match]`/`[[match]]` entry names both `property` and
314
+ * `value`, no two entries demand different values of the SAME property (a
315
+ * constraint set no instance can ever satisfy is dead art, not a stricter
316
+ * variant), a constraint on one of the two CLOSED axes names a value that
317
+ * axis actually has (mgx:faces one of FACING_VALUES, mgx:pose one of
318
+ * POSE_VALUES — every other predicate stays open, since a variant may match
319
+ * on any fact at all, mgx:hasProperty included), and
191
320
  * `[face]`/`[parameters.emotion]` are always declared TOGETHER — a face
192
321
  * anchor with nothing to select it, or an emotion parameter with nowhere to
193
322
  * position its face fragment, is a real authoring mistake either way
194
323
  * (sprite-expressions.mjs's own header explains why the face fragment
195
- * needs the pairing). */
324
+ * needs the pairing). Every check reads the tables a template actually
325
+ * declares and none of them cares whether a `[match]` sits alongside, so a
326
+ * facing variant carrying `[match]` + `[face]` + `[parameters.emotion]` is
327
+ * held to exactly the same pairing and placeholder rules as a plain
328
+ * `*-with-emotion.toml` file. */
196
329
  export function spriteTemplateProblems(template) {
197
330
  const problems = [];
198
331
  const t = template || {};
@@ -233,8 +366,22 @@ export function spriteTemplateProblems(template) {
233
366
  }
234
367
  }
235
368
  }
236
- if (t.match && (!t.match.property || t.match.value === undefined)) {
237
- problems.push("match is missing property or value");
369
+ const entries = matchEntries(t);
370
+ for (const entry of entries) {
371
+ if (!entry?.property || entry?.value === undefined) problems.push("match is missing property or value");
372
+ }
373
+ const requiredBy = new Map();
374
+ for (const { property, value } of matchConstraints(t)) {
375
+ if (requiredBy.has(property) && requiredBy.get(property) !== value) {
376
+ problems.push(`match requires ${property} to be both "${requiredBy.get(property)}" and "${value}" — no instance can satisfy that`);
377
+ }
378
+ requiredBy.set(property, value);
379
+ if (property === FACING_PROPERTY && !FACING_VALUES.includes(value)) {
380
+ problems.push(`match requires ${FACING_PROPERTY} "${value}", which is not one of the turntable's angles (${FACING_VALUES.join(", ")}; the centre view is the absent fact)`);
381
+ }
382
+ if (property === POSE_PROPERTY && !POSE_VALUES.includes(value)) {
383
+ problems.push(`match requires ${POSE_PROPERTY} "${value}", which is not one of the known poses (${POSE_VALUES.join(", ")}; at rest is the absent fact)`);
384
+ }
238
385
  }
239
386
  if (t.face && !t.parameters?.emotion) {
240
387
  problems.push("face is declared without parameters.emotion — a face anchor with nothing to select it is dead data");
@@ -14,10 +14,13 @@
14
14
  // mgx:acts-toward, mgx:is-objective) — an editor has to show and change
15
15
  // exactly the facts a player is never told.
16
16
  //
17
- // No imports: every export here is .toString()-splice-safe, the same
18
- // discipline adventure-viz.mjs's own render-glue functions hold (see that
19
- // module's header) — this module's functions get spliced directly into the
20
- // adventure page's inline script the same way.
17
+ // No imports of its own logic: every export here is .toString()-splice-safe,
18
+ // the same discipline adventure-viz.mjs's own render-glue functions hold (see
19
+ // that module's header) — this module's functions get spliced directly into
20
+ // the adventure page's inline script the same way. The one exception,
21
+ // wordBeforeCursor, re-exports viz-theme.mjs's own shared copy (byte-identical
22
+ // to what used to live here, and to mud-editor.mjs's own copy) rather than
23
+ // keep a third copy of the same regex.
21
24
  //
22
25
  // Two predicate families get different sync strategies, on purpose:
23
26
  // - PLACEMENT/OPENNESS (mgx:currently-in/located-in/fixed-in/stands-
@@ -357,13 +360,4 @@ export function planWorldEditorSync(rows, state, triples) {
357
360
 
358
361
  // ---- cursor-driven suggestions ---------------------------------------------
359
362
 
360
- /** The word immediately before `cursorPos` in `text` — a run of letters/
361
- * digits/hyphens, the same token shape this vocabulary's own terms use
362
- * (kebab-case room names like "drawing-room"). Empty string when the
363
- * cursor sits after whitespace/punctuation with no word directly behind
364
- * it. Pure. */
365
- export function wordBeforeCursor(text, cursorPos) {
366
- const head = String(text || "").slice(0, cursorPos);
367
- const m = head.match(/[A-Za-z][A-Za-z0-9-]*$/);
368
- return m ? m[0].toLowerCase() : "";
369
- }
363
+ export { wordBeforeCursor } from "./viz-theme.mjs";