@polycode-projects/the-mechanical-code-talker 4.0.1 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/corpus/sprites/src/sprite-facts.jsonl +375 -8
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +20 -0
- package/src/domain/ask-vocab.mjs +71 -0
- package/src/domain/ask.mjs +168 -0
- package/src/domain/game-config.mjs +11 -0
- package/src/domain/mud-facts.mjs +15 -0
- package/src/domain/router/drive.mjs +35 -9
- package/src/domain/router/registry.mjs +24 -4
- package/src/domain/router/resolver.mjs +102 -40
- package/src/domain/scene-compose.mjs +117 -0
- package/src/domain/spider-fly-world.mjs +36 -0
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/domain/sprite-request.mjs +156 -0
- package/src/domain/sprite-templates.mjs +161 -14
- package/src/services/adventure-editor.mjs +8 -14
- package/src/services/adventure-viz.mjs +119 -150
- package/src/services/adventure.mjs +97 -35
- package/src/services/chat-page-viz.mjs +64 -48
- package/src/services/chat.mjs +102 -34
- package/src/services/code-explorer-viz.mjs +52 -50
- package/src/services/ingest-viz.mjs +32 -74
- package/src/services/ledger-viz.mjs +87 -70
- package/src/services/memory-panel-viz.mjs +38 -0
- package/src/services/mud-editor.mjs +10 -15
- package/src/services/mud-turn.mjs +6 -6
- package/src/services/mud-viz.mjs +119 -225
- package/src/services/p2p-room.mjs +90 -23
- package/src/services/plan-pddl.mjs +3 -1
- package/src/services/plan-viz.mjs +13 -12
- package/src/services/research-viz.mjs +25 -67
- package/src/services/spider-fly-turn.mjs +14 -22
- package/src/services/spider-fly-viz.mjs +97 -136
- package/src/services/spider-fly.mjs +69 -11
- package/src/services/sprite-catalog-viz.mjs +274 -224
- package/src/services/viz-boot.mjs +71 -0
- package/src/services/viz-room-graph.mjs +203 -0
- package/src/services/viz-theme.mjs +75 -1
- package/src/services/viz-ticker.mjs +22 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +62 -47
- package/src/surfaces/web/chat-browser-entry.mjs +51 -107
- package/src/surfaces/web/code-explorer-browser-entry.mjs +192 -35
- package/src/surfaces/web/engine-surface.mjs +82 -0
- package/src/surfaces/web/ingest-browser-entry.mjs +16 -17
- package/src/surfaces/web/ledger-browser-entry.mjs +24 -56
- package/src/surfaces/web/memory-ask-browser-entry.mjs +55 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +128 -125
- package/src/surfaces/web/memory-stats.mjs +11 -0
- package/src/surfaces/web/mud-browser-entry.mjs +70 -49
- package/src/surfaces/web/plan-browser-entry.mjs +39 -50
- package/src/surfaces/web/research-browser-entry.mjs +48 -46
- package/src/surfaces/web/spider-fly-browser-entry.mjs +76 -40
- package/src/surfaces/web/sprites-browser-entry.mjs +28 -32
- package/src/surfaces/web/tmct-surface.mjs +147 -0
- package/src/surfaces/web/turn-session.mjs +124 -0
- package/src/tools/definitions.mjs +30 -0
- package/src/tools/handlers/index.mjs +6 -3
- package/src/tools/handlers/kit.mjs +19 -2
- package/src/tools/handlers/tmct-ask.mjs +11 -6
- package/src/tools/handlers/tmct-ingest.mjs +5 -1
- package/src/tools/handlers/tmct-related.mjs +4 -4
- package/src/tools/handlers/tmct-sprite.mjs +147 -0
- package/src/tools/memory-fallthrough.mjs +9 -2
- package/src/tools/server.mjs +37 -6
|
@@ -11,15 +11,20 @@
|
|
|
11
11
|
// grammar doesn't carry, and a rescue path when the NL parse selects an out-of-set
|
|
12
12
|
// capability).
|
|
13
13
|
//
|
|
14
|
-
// Entity binding is delegated to `resolveObject` (ask.mjs)
|
|
15
|
-
//
|
|
14
|
+
// Entity binding is delegated to `resolveObject` (ask.mjs) for code-graph slots, and to
|
|
15
|
+
// `resolveMemoryTerm` (below) for a slot whose registry kind is KINDS.MemoryTerm — the
|
|
16
|
+
// conversational-memory graph's own binding oracle. An ambiguous or no-match term is an
|
|
17
|
+
// honest refuse, never a guess, on both paths.
|
|
16
18
|
|
|
17
19
|
import { parseQuery } from "../ask.mjs";
|
|
18
20
|
import { SUPERLATIVE_EXTREMES } from "../ask-vocab.mjs";
|
|
19
21
|
import { buildSkosConceptView } from "../skos-view.mjs";
|
|
22
|
+
import { normFactTerm } from "../hash.mjs";
|
|
20
23
|
import { edgesOfKind } from "../codegraph.mjs";
|
|
24
|
+
import { EXPRESSION_PALETTE } from "../sprite-expressions.mjs";
|
|
25
|
+
import { sizeScaleFor } from "../sprite-size.mjs";
|
|
21
26
|
import {
|
|
22
|
-
capabilities, capabilityByName, preconditionsOf, effectsOf, PRECOND,
|
|
27
|
+
capabilities, capabilityByName, parametersOf, preconditionsOf, effectsOf, PRECOND, KINDS, MEMORY_KINDS,
|
|
23
28
|
} from "./registry.mjs";
|
|
24
29
|
import { hallucinationsIn } from "./call-validator.mjs";
|
|
25
30
|
|
|
@@ -64,6 +69,25 @@ export const UNMAPPED_KINDS = Object.freeze({
|
|
|
64
69
|
// via resolveMemoryTerm, the memory-graph sibling of resolveObject's code-graph binding.
|
|
65
70
|
export const NOT_NL_REACHABLE = Object.freeze({});
|
|
66
71
|
|
|
72
|
+
/** The property predicate sprite-size.mjs reads a size word off. Used to PROBE
|
|
73
|
+
* its own closed scale table instead of restating it here: a word is a size
|
|
74
|
+
* word exactly when it moves the scale away from 1. */
|
|
75
|
+
const SIZE_FACT_PREDICATE = "mgx:hasProperty";
|
|
76
|
+
|
|
77
|
+
/** The sprite frame's two OPTIONAL slots, read straight out of the request.
|
|
78
|
+
* Both vocabularies are the real ones — sprite-expressions.mjs's own palette
|
|
79
|
+
* and sprite-size.mjs's own scale table (probed, not copied) — so a word
|
|
80
|
+
* either module gains is a word this frame gains. A request naming neither
|
|
81
|
+
* fills neither, and the sprite resolves plain. */
|
|
82
|
+
function spriteSlots(request) {
|
|
83
|
+
const slots = {};
|
|
84
|
+
for (const word of String(request).toLowerCase().match(/[a-z]+/g) || []) {
|
|
85
|
+
if (!slots.expression && Object.hasOwn(EXPRESSION_PALETTE, word)) slots.expression = word;
|
|
86
|
+
if (!slots.size && sizeScaleFor([{ predicate: SIZE_FACT_PREDICATE, object: word }]) !== 1) slots.size = word;
|
|
87
|
+
}
|
|
88
|
+
return slots;
|
|
89
|
+
}
|
|
90
|
+
|
|
67
91
|
// ---- imperative intent FRAMES (fills what the relational grammar and command register
|
|
68
92
|
// both miss). regex -> { topic, arg | noArg }. Ordered: first match wins.
|
|
69
93
|
export const FRAMES = Object.freeze([
|
|
@@ -86,10 +110,19 @@ export const FRAMES = Object.freeze([
|
|
|
86
110
|
{ re: /\bsignature\b/i, topic: "signature", arg: "symbol" },
|
|
87
111
|
// tmct_related: the SKOS synonym/related-concept surface over the memory graph
|
|
88
112
|
// — "another word for X" / "a synonym for X" / "synonyms of X" / "what's
|
|
89
|
-
// related to X". `
|
|
90
|
-
//
|
|
91
|
-
//
|
|
113
|
+
// related to X". Its `term` slot is declared KINDS.MemoryTerm in the registry,
|
|
114
|
+
// so resolveOne binds it through resolveMemoryTerm (below) rather than the
|
|
115
|
+
// code graph's resolveObject — the registry kind, not this table, decides.
|
|
92
116
|
{ re: /\bsynonyms?\b|\banother\s+word\s+for\b|\brelated\s+(?:words?|concepts?|to)\b/i, topic: "related", arg: "term" },
|
|
117
|
+
// tmct_sprite: the sprite surface over the memory graph's own world facts —
|
|
118
|
+
// "the large sprite for a happy spider", "what does a hungry fly look like",
|
|
119
|
+
// "show me the spider icon". Its `class` slot is the second KINDS.MemoryTerm
|
|
120
|
+
// one, so it binds the same way tmct_related's `term` does. `slots` is what
|
|
121
|
+
// makes this frame different: it fills the two OPTIONAL args itself.
|
|
122
|
+
{
|
|
123
|
+
re: /\bsprite\b|\bicon\b|\bavatar\b|\bpicture\s+of\b|\bwhat\s+does\s+.+\s+look\s+like\b/i,
|
|
124
|
+
topic: "sprite", arg: "class", slots: spriteSlots,
|
|
125
|
+
},
|
|
93
126
|
{ re: /\bdescribe\b|\bexplain\b|what\s+is\b|tell\s+me\s+about\b|definition\s+of\b/i, topic: "description", arg: "symbol" },
|
|
94
127
|
{ re: /\bsearch\b|\bfind\b|look\s+for\b/i, topic: "matches", arg: "query" },
|
|
95
128
|
]);
|
|
@@ -132,13 +165,17 @@ const STOP = new Set([
|
|
|
132
165
|
"untested", "blast", "radius", "change", "changes", "changing", "reach", "reaches", "affect", "affects",
|
|
133
166
|
"explain", "edge", "edges", "graph", "outgoing", "site", "sites", "invoke", "invokes", "run", "runs", "execute", "executes",
|
|
134
167
|
"word", "words", "another", "synonym", "synonyms", "related", "relate", "relates", "like", "concept", "concepts",
|
|
168
|
+
"sprite", "sprites", "icon", "icons", "avatar", "avatars", "picture", "pictures", "image", "images", "looks",
|
|
135
169
|
]);
|
|
136
170
|
|
|
137
171
|
/** Pull one entity token from a request (imperative-frame slot-filling). Prefer a
|
|
138
|
-
* path/dotted/CamelCase token, else the last non-stopword identifier. "" if none.
|
|
139
|
-
|
|
172
|
+
* path/dotted/CamelCase token, else the last non-stopword identifier. "" if none.
|
|
173
|
+
* `alreadyBound` holds the words a frame's own slot-filler has already claimed
|
|
174
|
+
* (the sprite frame's expression/size), so they can't also be read as the entity. */
|
|
175
|
+
export function extractEntity(request, alreadyBound = null) {
|
|
140
176
|
const tokens = String(request).match(/[A-Za-z_][A-Za-z0-9_./-]*/g) || [];
|
|
141
|
-
const
|
|
177
|
+
const claimed = alreadyBound || new Set();
|
|
178
|
+
const pool = tokens.filter((t) => !STOP.has(t.toLowerCase()) && !claimed.has(t.toLowerCase()));
|
|
142
179
|
const strong = pool.filter((t) => /[./]/.test(t) || /^[A-Z][a-z]/.test(t) || /\.[a-z]+$/.test(t));
|
|
143
180
|
const pick = strong.length ? strong : pool;
|
|
144
181
|
return pick.length ? pick[pick.length - 1] : "";
|
|
@@ -187,13 +224,14 @@ export function mapFrame(request) {
|
|
|
187
224
|
const cap = backwardChain(f.topic);
|
|
188
225
|
if (!cap) continue;
|
|
189
226
|
if (f.noArg) return { name: cap.name, noArg: true, topic: f.topic, source: "frame", why: [`imperative frame => goal (knows ${f.topic})`, `backward-chain => ${cap.name}`] };
|
|
190
|
-
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
227
|
+
// A frame's own OPTIONAL slots, filled from the request's vocabulary before
|
|
228
|
+
// the entity is read, so a word a slot claimed can't also be read as the entity.
|
|
229
|
+
const slots = f.slots ? f.slots(request) : null;
|
|
230
|
+
const term = f.arg === "query"
|
|
231
|
+
? searchQuery(request)
|
|
232
|
+
: extractEntity(request, slots ? new Set(Object.values(slots)) : null);
|
|
195
233
|
return {
|
|
196
|
-
name: cap.name, arg: f.arg, term, topic: f.topic, source: "frame",
|
|
234
|
+
name: cap.name, arg: f.arg, term, topic: f.topic, source: "frame", slots,
|
|
197
235
|
why: [`imperative frame => goal (knows ${f.topic} ?${f.arg})`, `backward-chain => ${cap.name}`],
|
|
198
236
|
};
|
|
199
237
|
}
|
|
@@ -215,24 +253,41 @@ export function commandCapability(request, declaredNames, selectTool) {
|
|
|
215
253
|
return { name: sel.name, input, source: "command", why: [`command register: "${String(request).trim().split(/\s+/)[0]}" => ${sel.name}`] };
|
|
216
254
|
}
|
|
217
255
|
|
|
218
|
-
/** Resolve a term against the memory graph
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
256
|
+
/** Resolve a term against the conversational-memory graph — the memory-graph
|
|
257
|
+
* sibling of resolveObject, for a slot whose registry kind is KINDS.MemoryTerm.
|
|
258
|
+
* Two tiers, tried in order, both EXACT after normFactTerm (no fuzzy tier — a
|
|
259
|
+
* near-miss is a miss, never a guess):
|
|
260
|
+
* 1. "memory-concept" — the SKOS concept view (skos-view.mjs) mints a
|
|
261
|
+
* concept for the term, i.e. the store holds synonym/related facts for it;
|
|
262
|
+
* 2. "memory-fact-term" — the term appears as the subject or object of any
|
|
263
|
+
* stored fact row (a world-fact term: a room, a game agent, a taught
|
|
264
|
+
* individual).
|
|
265
|
+
* Same `{ match, ambiguous }` shape resolveObject returns, so resolveOne's
|
|
266
|
+
* generic binding step treats both oracles the same way: `match.label` is the
|
|
267
|
+
* RAW queried term (not a canonicalised prefLabel), because a memory tool's
|
|
268
|
+
* own lookup (e.g. relatedForTerm) re-derives its rows from whatever term it's
|
|
269
|
+
* given — the bound call should carry what the user actually asked about. No
|
|
270
|
+
* code-graph fallback: a term the store holds no facts for at all is an honest
|
|
271
|
+
* miss. `rows` is a loadMemory+readFactRows payload — the same trust-bearing
|
|
272
|
+
* rows skosRelatedAnswer (chat.mjs) and the memory tool handlers read. */
|
|
230
273
|
export function resolveMemoryTerm(rows, term) {
|
|
231
274
|
const t = String(term || "").trim();
|
|
232
275
|
if (!t) return { match: null };
|
|
233
276
|
const view = buildSkosConceptView(rows);
|
|
234
|
-
if (
|
|
235
|
-
|
|
277
|
+
if (view.conceptIdForTerm(t)) return { match: { label: t, class: "skos:Concept" }, ambiguous: false, tier: "memory-concept" };
|
|
278
|
+
const n = normFactTerm(t);
|
|
279
|
+
if (n && rows.some((r) => normFactTerm(r.subject) === n || normFactTerm(r.object) === n)) {
|
|
280
|
+
return { match: { label: t, class: KINDS.MemoryTerm }, ambiguous: false, tier: "memory-fact-term" };
|
|
281
|
+
}
|
|
282
|
+
return { match: null };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** True when capability `capName`'s slot with arg key `arg` is declared a
|
|
286
|
+
* memory-graph kind (KINDS.MemoryTerm) — the registry-driven switch between
|
|
287
|
+
* the two binding oracles: resolveMemoryTerm for memory slots, the code
|
|
288
|
+
* graph's resolveObject for everything else. */
|
|
289
|
+
export function isMemoryTermSlot(capName, arg) {
|
|
290
|
+
return parametersOf(capName).some((p) => p.arg === arg && MEMORY_KINDS.includes(p.kind));
|
|
236
291
|
}
|
|
237
292
|
|
|
238
293
|
// ---- the full single-call resolver (async — binds + grounds) -----------------
|
|
@@ -328,27 +383,29 @@ export async function resolveOne(request, declaredNames, ctx, { execute = true }
|
|
|
328
383
|
if (!declared.has(pick.name)) return REFUSE(`selected ${pick.name} but it is not in the declared toolset`);
|
|
329
384
|
|
|
330
385
|
// A command pick already carries a bound input; an NL/frame pick carries a raw term
|
|
331
|
-
// we bind via resolveObject (code graph) or, for a
|
|
332
|
-
//
|
|
386
|
+
// we bind via resolveObject (code graph) or, for a KINDS.MemoryTerm slot (the
|
|
387
|
+
// registry's own declaration, read through isMemoryTermSlot), resolveMemoryTerm —
|
|
388
|
+
// the two binding oracles never mix on one pick.
|
|
333
389
|
let input = pick.input ? { ...pick.input } : {};
|
|
334
390
|
let resolved = null;
|
|
335
391
|
if (!pick.input && !pick.noArg) {
|
|
336
392
|
const term = String(pick.term || "").trim();
|
|
337
393
|
if (!term) return REFUSE(`the ${pick.topic} intent named no entity to bind`);
|
|
338
|
-
const
|
|
394
|
+
const memoryBound = isMemoryTermSlot(pick.name, pick.arg);
|
|
395
|
+
const r = memoryBound
|
|
339
396
|
? (ctx.resolveMemoryTerm ? await ctx.resolveMemoryTerm(term) : { match: null })
|
|
340
397
|
: (ctx.resolve ? ctx.resolve(term) : { match: { label: term }, ambiguous: false });
|
|
341
398
|
if (!r || !r.match) {
|
|
342
|
-
return REFUSE(
|
|
343
|
-
? `
|
|
399
|
+
return REFUSE(memoryBound
|
|
400
|
+
? `the memory graph holds no facts mentioning "${term}" (honest miss)`
|
|
344
401
|
: `"${term}" does not resolve to any graph entity (honest miss)`);
|
|
345
402
|
}
|
|
346
403
|
// A tied read: either resolveObject's own score-tie (r.ambiguous), or a same-tier
|
|
347
404
|
// candidate the graph's own `tests` edge ties to the match (a source module and
|
|
348
405
|
// its test module — a grain neither side's raw score alone reveals as tied).
|
|
349
|
-
// resolveMemoryTerm's
|
|
350
|
-
// sibling tie-check only applies on the code-graph resolution path.
|
|
351
|
-
const sibling = (!
|
|
406
|
+
// resolveMemoryTerm's exact-match tiers have no code-graph tests-edge notion, so
|
|
407
|
+
// the sibling tie-check only applies on the code-graph resolution path.
|
|
408
|
+
const sibling = (!memoryBound && !r.ambiguous)
|
|
352
409
|
? (r.candidates || []).find((c) => testModuleTie(ctx.graph, r.match, c))
|
|
353
410
|
: null;
|
|
354
411
|
if (r.ambiguous || sibling) {
|
|
@@ -360,10 +417,15 @@ export async function resolveOne(request, declaredNames, ctx, { execute = true }
|
|
|
360
417
|
return REFUSE(`"${term}" is ambiguous (${pool.map((m) => m.label).join(", ")}) — narrow it`, extra);
|
|
361
418
|
}
|
|
362
419
|
resolved = r.match;
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
420
|
+
// The frame's own optional slots ride alongside the bound entity — declared
|
|
421
|
+
// params of the same capability, so hallucinationsIn still validates them.
|
|
422
|
+
input = { [pick.arg]: r.match.label, ...(pick.slots || {}) };
|
|
423
|
+
why = [...why, memoryBound
|
|
424
|
+
? `resolveMemoryTerm: "${term}" binds in the memory graph (${r.match.class || "?"}, tier ${r.tier})`
|
|
366
425
|
: `resolveObject: "${term}" => ${r.match.label} (${r.match.class || "?"}, tier ${r.tier})`];
|
|
426
|
+
if (pick.slots && Object.keys(pick.slots).length) {
|
|
427
|
+
why = [...why, `frame slots bound from the request's own vocabulary: ${Object.entries(pick.slots).map(([k, v]) => `${k}=${v}`).join(", ")}`];
|
|
428
|
+
}
|
|
367
429
|
}
|
|
368
430
|
|
|
369
431
|
const call = { name: pick.name, input };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// scene-compose.mjs — the sprite catalog's "there is a…" box: which real
|
|
2
|
+
// catalog classes a free-typed sentence names, and with which of that class's
|
|
3
|
+
// own material labels.
|
|
4
|
+
//
|
|
5
|
+
// Naming a class is the engine's own job. This module segments the sentence
|
|
6
|
+
// into candidate spans and hands each one to ask.mjs's resolveObject — the same
|
|
7
|
+
// resolver the chat lanes use — over a graph whose individuals are the caller's
|
|
8
|
+
// real classes. So the caller owns where a name might start and stop, and
|
|
9
|
+
// nothing else: casing, leading articles and grain words come from the
|
|
10
|
+
// resolver's own tiers, and a span carrying a word the index has no reading for
|
|
11
|
+
// declines there rather than resolving past it.
|
|
12
|
+
//
|
|
13
|
+
// It lives in domain/ rather than beside the page that draws the scene because
|
|
14
|
+
// the page's browser bundle needs it: routing through the real resolver means
|
|
15
|
+
// the parser can't be spliced into the page as self-contained text any more.
|
|
16
|
+
|
|
17
|
+
import { resolveObject } from "./ask.mjs";
|
|
18
|
+
import { parseEntities } from "./codegraph.mjs";
|
|
19
|
+
|
|
20
|
+
/** `text`'s lowercase word runs, the unit class names are matched against —
|
|
21
|
+
* punctuation never fuses two real words into one token nor splits one real
|
|
22
|
+
* word into two. */
|
|
23
|
+
export function tokenizeSceneText(text) {
|
|
24
|
+
const tokens = [];
|
|
25
|
+
const re = /[A-Za-z]+/g;
|
|
26
|
+
let m;
|
|
27
|
+
while ((m = re.exec(String(text ?? "")))) tokens.push({ word: m[0].toLowerCase() });
|
|
28
|
+
return tokens;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const sceneClassGraphCache = new WeakMap();
|
|
32
|
+
|
|
33
|
+
/** The `classIndex`'s own class names as a resolvable graph, one individual per
|
|
34
|
+
* class, plus the longest class name's word count (the widest span worth
|
|
35
|
+
* offering the resolver). Cached per index object: a page builds its index once
|
|
36
|
+
* at load and then composes on every keystroke. */
|
|
37
|
+
function sceneClassGraph(classIndex) {
|
|
38
|
+
const cached = sceneClassGraphCache.get(classIndex);
|
|
39
|
+
if (cached) return cached;
|
|
40
|
+
const names = Object.keys(classIndex).filter((name) => String(name).trim());
|
|
41
|
+
const built = {
|
|
42
|
+
graph: parseEntities({
|
|
43
|
+
individuals: names.map((name) => ({ id: `sprite-class:${name}`, label: name, class: "Class" })),
|
|
44
|
+
objectProperties: [],
|
|
45
|
+
}),
|
|
46
|
+
longestClassWordCount: names.reduce((max, name) => Math.max(max, name.trim().split(/\s+/).length), 0),
|
|
47
|
+
};
|
|
48
|
+
sceneClassGraphCache.set(classIndex, built);
|
|
49
|
+
return built;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The one resolver tier the composer draws on. Composing paints a picture, and
|
|
53
|
+
* painting the wrong sprite is a silent lie the visitor can't audit, so only an
|
|
54
|
+
* exact-grade resolution (resolveObject's tier 1, which its own leading-article
|
|
55
|
+
* and grain-word retries reach too) earns a swatch. The containment and fuzzy
|
|
56
|
+
* tiers below it read "wood" as the food class and "glass" as grass — right for
|
|
57
|
+
* a question the engine answers in words and cites, wrong for a picture drawn
|
|
58
|
+
* without comment. */
|
|
59
|
+
const SCENE_EXACT_TIER = 1;
|
|
60
|
+
|
|
61
|
+
/** The catalog class named by the longest still-unclaimed token span starting at
|
|
62
|
+
* `start`, as `{className, wordCount}`, or null when the resolver grounds none
|
|
63
|
+
* of them. Spans are offered widest-first, so a multi-word class name ("body of
|
|
64
|
+
* water") always wins the position it starts at over a shorter class that would
|
|
65
|
+
* otherwise fragment it. An ambiguous resolution is a miss: a span that reads as
|
|
66
|
+
* several real classes names none of them. */
|
|
67
|
+
function resolveSpanToClass(graph, classIndex, tokens, used, start, longestClassWordCount) {
|
|
68
|
+
const widest = Math.min(longestClassWordCount, tokens.length - start);
|
|
69
|
+
for (let wordCount = widest; wordCount >= 1; wordCount -= 1) {
|
|
70
|
+
let free = true;
|
|
71
|
+
for (let k = 0; k < wordCount && free; k += 1) free = !used[start + k];
|
|
72
|
+
if (!free) continue;
|
|
73
|
+
const span = tokens.slice(start, start + wordCount).map((t) => t.word).join(" ");
|
|
74
|
+
const resolved = resolveObject(graph, span);
|
|
75
|
+
const label = resolved?.match?.label;
|
|
76
|
+
if (!label || resolved.ambiguous || resolved.tier !== SCENE_EXACT_TIER) continue;
|
|
77
|
+
if (Object.prototype.hasOwnProperty.call(classIndex, label)) return { className: label, wordCount };
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Every real catalog class the free-typed `text` names, in the order each first
|
|
83
|
+
* appears, paired with the real material label (one of that SAME class's own
|
|
84
|
+
* swatch labels — never another class's, never a fabricated one) immediately
|
|
85
|
+
* preceding it, or `null`. `classIndex` is `{className: {materials}}` with
|
|
86
|
+
* `materials` keyed by lowercase label (sprites.html's own client-side
|
|
87
|
+
* `buildClassIndexFromDom` output, or an equivalent test fixture) — a class name
|
|
88
|
+
* absent from `classIndex` can never match, and a modifier word that isn't one
|
|
89
|
+
* of ITS matched class's own material labels is silently dropped rather than
|
|
90
|
+
* guessed at, the same honest-miss posture an unrecognized class name gets (an
|
|
91
|
+
* unmatched word, e.g. "red" before a lamp with no red material, is never an
|
|
92
|
+
* error, just silently not drawn). Pure. */
|
|
93
|
+
export function extractSceneItems(text, classIndex) {
|
|
94
|
+
const index = classIndex || {};
|
|
95
|
+
const { graph, longestClassWordCount } = sceneClassGraph(index);
|
|
96
|
+
if (!longestClassWordCount) return [];
|
|
97
|
+
const tokens = tokenizeSceneText(text);
|
|
98
|
+
const used = new Array(tokens.length).fill(false);
|
|
99
|
+
const items = [];
|
|
100
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
101
|
+
if (used[i]) continue;
|
|
102
|
+
const hit = resolveSpanToClass(graph, index, tokens, used, i, longestClassWordCount);
|
|
103
|
+
if (!hit) continue;
|
|
104
|
+
let materialLabel = null;
|
|
105
|
+
if (i > 0 && !used[i - 1]) {
|
|
106
|
+
const materials = index[hit.className]?.materials || {};
|
|
107
|
+
const prevWord = tokens[i - 1].word;
|
|
108
|
+
if (Object.prototype.hasOwnProperty.call(materials, prevWord)) {
|
|
109
|
+
materialLabel = prevWord;
|
|
110
|
+
used[i - 1] = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (let k = 0; k < hit.wordCount; k += 1) used[i + k] = true;
|
|
114
|
+
items.push({ className: hit.className, materialLabel });
|
|
115
|
+
}
|
|
116
|
+
return items;
|
|
117
|
+
}
|
|
@@ -207,6 +207,42 @@ export function worldMetaRow() {
|
|
|
207
207
|
return { world: WORLD_NAME, kind: "meta", opening: WORLD_OPENING };
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
/** The class an agent id names — "spider-2" -> "spider", "fly-10" -> "fly",
|
|
211
|
+
* "egg-1" -> "egg" — the one regex every caller that needs an individual's
|
|
212
|
+
* kind from its id string shares. Self-contained (no outer refs),
|
|
213
|
+
* `.toString()`-splice safe. */
|
|
214
|
+
export function agentKindOf(id) {
|
|
215
|
+
return String(id).replace(/-\d+$/, "");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Every live id of `kind` among `agents`, sorted. `agents` is either a
|
|
219
|
+
* plain `{ id: ... }` roster (runSpiderFlyTick's/foldSpiderFlyState's own
|
|
220
|
+
* agents map, already excluding anything dead) or a `Map` keyed by id
|
|
221
|
+
* (foldSpiderFlyState's own `state.placements`, which still carries a dead
|
|
222
|
+
* individual's last-known cell) — pass the matching `state.removed` Set as
|
|
223
|
+
* `removed` in the Map case to exclude those; omit it for a plain roster
|
|
224
|
+
* that has no such concept. Pure. */
|
|
225
|
+
export function liveIdsOfKind(agents, kind, removed = null) {
|
|
226
|
+
const re = new RegExp(`^${kind}-\\d+$`);
|
|
227
|
+
const ids = agents instanceof Map ? [...agents.keys()] : Object.keys(agents || {});
|
|
228
|
+
return ids.filter((id) => re.test(id) && !(removed && removed.has(id))).sort();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** True for a web individual's own id ("web-3") — a spider-built web is
|
|
232
|
+
* placed via mgx:currently-in exactly like a live agent, but it is never
|
|
233
|
+
* one. */
|
|
234
|
+
export function isWebIndividualId(id) {
|
|
235
|
+
return /^web-\d+$/.test(id);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Whether `id` belongs on a rendered agent roster built from `state`
|
|
239
|
+
* (foldSpiderFlyState's own shape): live (not in `state.removed`) and not a
|
|
240
|
+
* web individual. The one world rule a browser-side snapshot needs to skip
|
|
241
|
+
* the same two things every renderer of `state.placements` must skip. */
|
|
242
|
+
export function isLiveRenderableAgent(id, state) {
|
|
243
|
+
return !state.removed.has(id) && !isWebIndividualId(id);
|
|
244
|
+
}
|
|
245
|
+
|
|
210
246
|
/** A minimal, inert rule-row family, so scripts/build-worlds-pack.mjs's
|
|
211
247
|
* shared validator ("every world needs at least one rule row") passes.
|
|
212
248
|
* src/services/spider-fly.mjs never reads these back: grid movement is
|
|
Binary file
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// sprite-request.mjs — one sprite request ("the large sprite for a happy
|
|
2
|
+
// spider") resolved to markup PLUS the chain that found it. This is the pure
|
|
3
|
+
// core the tmct_sprite tool handler wraps and the spider-and-fly page splices,
|
|
4
|
+
// so the page and the tool answer the same question with the same code instead
|
|
5
|
+
// of two hand-kept call sites drifting apart.
|
|
6
|
+
//
|
|
7
|
+
// Every collaborator is INJECTED rather than imported — sprite-templates.mjs's
|
|
8
|
+
// resolveSpriteAsset, sprite-map.mjs's classAncestorChain, sprite-size.mjs's
|
|
9
|
+
// sizeScaleFor, sprite-expressions.mjs's EXPRESSION_PALETTE. Same reason
|
|
10
|
+
// spider-fly-viz.mjs's threadCellsForSpiderPlan takes its grid geometry as an
|
|
11
|
+
// argument: a function with no module-scope references survives `.toString()`
|
|
12
|
+
// splicing into a page script, and the browser already holds its own copies of
|
|
13
|
+
// those primitives on window.tmct.
|
|
14
|
+
//
|
|
15
|
+
// The resolution CHAIN is derived by OBSERVATION, never by re-implementing
|
|
16
|
+
// sprite-templates.mjs's specificity order. At each term of the ancestor chain
|
|
17
|
+
// the same resolver is asked to resolve that ONE term against an empty
|
|
18
|
+
// registry, so "did this level match" is answered by the real resolver rather
|
|
19
|
+
// than by a copy of its rules that can drift out of step with it. Whether a
|
|
20
|
+
// requested expression was actually honoured is observed the same way: resolve
|
|
21
|
+
// once with the mgx:feels fact and once without, and compare.
|
|
22
|
+
//
|
|
23
|
+
// The two optional slots carry two different senses:
|
|
24
|
+
// - `expression` becomes an `mgx:feels` fact, the parameter every
|
|
25
|
+
// *-with-emotion template selects on.
|
|
26
|
+
// - `size` becomes an `mgx:hasProperty` fact and resolves to a numeric render
|
|
27
|
+
// SCALE (sprite-size.mjs's own closed scale table). It is the taught size of
|
|
28
|
+
// the thing being drawn, not a choice of template tier — the caller picks
|
|
29
|
+
// the tier by which template set it hands in.
|
|
30
|
+
//
|
|
31
|
+
// Nothing here refuses. A caller that needs a miss wall (the tool does) reads
|
|
32
|
+
// `fellBackToRoot` / `expressionApplied` / `sizeKnown` / `expressionKnown` and
|
|
33
|
+
// decides; a caller that wants today's fall-through-to-the-root-sprite
|
|
34
|
+
// behaviour (the page does) just reads `svg`.
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve `request` (`{ class, expression?, size? }`) against a template set.
|
|
38
|
+
*
|
|
39
|
+
* `deps` carries the injected collaborators and the state to resolve against:
|
|
40
|
+
* `resolveSpriteAsset` (required), `templates`, `spriteRegistry`, `factRows`
|
|
41
|
+
* (the taxonomy rows the ancestor walk reads), `rootFallback`, `instanceKey`,
|
|
42
|
+
* and — for the reported chain and vocabulary checks — `classAncestorChain`,
|
|
43
|
+
* `sizeScaleFor` and `expressionPalette`. Omit any of the last three and the
|
|
44
|
+
* fields they feed come back `null` rather than guessed.
|
|
45
|
+
*
|
|
46
|
+
* Returns `{ class, expression, size, svg, scale, sizeKnown, expressionKnown,
|
|
47
|
+
* expressionApplied, rootFallback, fellBackToRoot, chain, matched }`. Pure.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveSpriteRequest(request, deps) {
|
|
50
|
+
const FEELS_PREDICATE = "mgx:feels";
|
|
51
|
+
const PROPERTY_PREDICATE = "mgx:hasProperty";
|
|
52
|
+
|
|
53
|
+
const className = String((request && request.class) || "").trim();
|
|
54
|
+
const expression = String((request && request.expression) || "").trim();
|
|
55
|
+
const size = String((request && request.size) || "").trim();
|
|
56
|
+
|
|
57
|
+
const options = deps || {};
|
|
58
|
+
const resolveAsset = options.resolveSpriteAsset;
|
|
59
|
+
if (typeof resolveAsset !== "function") {
|
|
60
|
+
throw new TypeError("resolveSpriteRequest needs deps.resolveSpriteAsset");
|
|
61
|
+
}
|
|
62
|
+
const templates = options.templates || [];
|
|
63
|
+
const registry = options.spriteRegistry || {};
|
|
64
|
+
const factRows = options.factRows || [];
|
|
65
|
+
const rootFallback = options.rootFallback || "animal";
|
|
66
|
+
const walkAncestors = options.classAncestorChain;
|
|
67
|
+
const scaleFor = options.sizeScaleFor;
|
|
68
|
+
const palette = options.expressionPalette;
|
|
69
|
+
|
|
70
|
+
const propertyFacts = [];
|
|
71
|
+
if (expression) propertyFacts.push({ predicate: FEELS_PREDICATE, object: expression });
|
|
72
|
+
if (size) propertyFacts.push({ predicate: PROPERTY_PREDICATE, object: size });
|
|
73
|
+
|
|
74
|
+
const assetOptions = { rootFallback };
|
|
75
|
+
if (options.instanceKey) assetOptions.instanceKey = options.instanceKey;
|
|
76
|
+
const svg = resolveAsset(className, factRows, propertyFacts, templates, registry, assetOptions);
|
|
77
|
+
|
|
78
|
+
// One resolve of a SINGLE term against an empty registry and its own root:
|
|
79
|
+
// the real resolver's answer to "does this level of the chain match", with no
|
|
80
|
+
// ancestor walk and no fall-through of its own.
|
|
81
|
+
const templateAt = (term) => resolveAsset(term, [], propertyFacts, templates, {}, { rootFallback: term });
|
|
82
|
+
const carriedByRegistry = (term) => Object.prototype.hasOwnProperty.call(registry, term);
|
|
83
|
+
|
|
84
|
+
// The template that produced `hit` at `term`: a fully-specific variant and a
|
|
85
|
+
// plain class template both hand their `svg` through untouched, so equality
|
|
86
|
+
// finds them; a parameterized template's substitutions change the string, and
|
|
87
|
+
// it is the only remaining candidate shape resolveAtTerm can have used.
|
|
88
|
+
const templateBehind = (term, hit) => {
|
|
89
|
+
const candidates = templates.filter((t) => t && Array.isArray(t.classes) && t.classes.indexOf(term) >= 0);
|
|
90
|
+
const authored = candidates.find((t) => t.svg === hit);
|
|
91
|
+
const template = authored || candidates.find((t) => !t.match && t.parameters) || null;
|
|
92
|
+
if (!template) return null;
|
|
93
|
+
return {
|
|
94
|
+
classes: template.classes.slice(),
|
|
95
|
+
parameters: Object.keys(template.parameters || {}).sort(),
|
|
96
|
+
match: template.match || null,
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let chain = null;
|
|
101
|
+
let matched = null;
|
|
102
|
+
if (typeof walkAncestors === "function") {
|
|
103
|
+
chain = [];
|
|
104
|
+
for (const term of walkAncestors(className, factRows)) {
|
|
105
|
+
const hit = templateAt(term);
|
|
106
|
+
chain.push({ term, template: Boolean(hit), registry: carriedByRegistry(term) });
|
|
107
|
+
if (hit) {
|
|
108
|
+
matched = { term, via: "template", hops: chain.length - 1, root: false, template: templateBehind(term, hit) };
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if (carriedByRegistry(term)) {
|
|
112
|
+
matched = { term, via: "registry", hops: chain.length - 1, root: false, template: null };
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!matched) {
|
|
117
|
+
const rootHit = templateAt(rootFallback);
|
|
118
|
+
chain.push({ term: rootFallback, template: Boolean(rootHit), registry: carriedByRegistry(rootFallback), root: true });
|
|
119
|
+
matched = {
|
|
120
|
+
term: rootFallback,
|
|
121
|
+
via: rootHit ? "template" : "registry",
|
|
122
|
+
hops: chain.length - 1,
|
|
123
|
+
root: true,
|
|
124
|
+
template: rootHit ? templateBehind(rootFallback, rootHit) : null,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Compared without the instance-id namespacing, which rewrites gradient ids
|
|
130
|
+
// and would make two otherwise identical resolutions look different.
|
|
131
|
+
const plainSvg = options.instanceKey
|
|
132
|
+
? resolveAsset(className, factRows, propertyFacts, templates, registry, { rootFallback })
|
|
133
|
+
: svg;
|
|
134
|
+
const withoutExpression = expression
|
|
135
|
+
? resolveAsset(className, factRows, propertyFacts.filter((f) => f.predicate !== FEELS_PREDICATE), templates, registry, { rootFallback })
|
|
136
|
+
: null;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
class: className,
|
|
140
|
+
expression: expression || null,
|
|
141
|
+
size: size || null,
|
|
142
|
+
svg,
|
|
143
|
+
scale: typeof scaleFor === "function" ? scaleFor(propertyFacts) : 1,
|
|
144
|
+
sizeKnown: size && typeof scaleFor === "function"
|
|
145
|
+
? scaleFor([{ predicate: PROPERTY_PREDICATE, object: size }]) !== 1
|
|
146
|
+
: null,
|
|
147
|
+
expressionKnown: expression && palette
|
|
148
|
+
? Object.prototype.hasOwnProperty.call(palette, expression)
|
|
149
|
+
: null,
|
|
150
|
+
expressionApplied: expression ? plainSvg !== withoutExpression : null,
|
|
151
|
+
rootFallback,
|
|
152
|
+
fellBackToRoot: matched ? Boolean(matched.root) : null,
|
|
153
|
+
chain,
|
|
154
|
+
matched,
|
|
155
|
+
};
|
|
156
|
+
}
|