@polycode-projects/the-mechanical-code-talker 4.0.0 → 4.1.0

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 (108) hide show
  1. package/README.md +178 -9
  2. package/corpus/reference/index.json.gz +0 -0
  3. package/corpus/reference/manifest.json +8 -8
  4. package/corpus/reference/shards/ref-00.jsonl.gz +0 -0
  5. package/corpus/sprites/src/sprite-facts.jsonl +401 -0
  6. package/corpus/worlds/index.json.gz +0 -0
  7. package/corpus/worlds/manifest.json +49 -9
  8. package/corpus/worlds/shards/greyvale-museum.jsonl.gz +0 -0
  9. package/corpus/worlds/shards/lantern-cottage.jsonl.gz +0 -0
  10. package/corpus/worlds/shards/mud-hollow.jsonl.gz +0 -0
  11. package/corpus/worlds/shards/mud-warren.jsonl.gz +0 -0
  12. package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
  13. package/corpus/worlds/src/greyvale-museum.jsonl +136 -0
  14. package/corpus/worlds/src/lantern-cottage.jsonl +60 -0
  15. package/corpus/worlds/src/mud-hollow.jsonl +82 -0
  16. package/corpus/worlds/src/mud-warren.jsonl +124 -0
  17. package/corpus/worlds/src/spider-fly.jsonl +1 -1
  18. package/data/sprites/animal-icon.toml +11 -5
  19. package/data/sprites/book-icon.toml +9 -5
  20. package/data/sprites/cabinet-icon.toml +10 -5
  21. package/data/sprites/cellar-icon.toml +16 -6
  22. package/data/sprites/container-icon.toml +7 -3
  23. package/data/sprites/desk-icon.toml +8 -5
  24. package/data/sprites/dog-icon.toml +8 -5
  25. package/data/sprites/dog-with-colour-icon.toml +13 -12
  26. package/data/sprites/drawing-room-icon.toml +14 -5
  27. package/data/sprites/egg-icon.toml +6 -3
  28. package/data/sprites/fly-icon.toml +10 -5
  29. package/data/sprites/furniture-icon.toml +5 -3
  30. package/data/sprites/garden-icon.toml +10 -3
  31. package/data/sprites/key-icon.toml +3 -1
  32. package/data/sprites/kitchen-icon.toml +15 -6
  33. package/data/sprites/lamp-icon.toml +9 -4
  34. package/data/sprites/letter-icon.toml +7 -4
  35. package/data/sprites/library-icon.toml +14 -3
  36. package/data/sprites/pan-icon.toml +7 -3
  37. package/data/sprites/person-icon.toml +6 -2
  38. package/data/sprites/poodle-icon.toml +1 -1
  39. package/data/sprites/portable-icon.toml +6 -4
  40. package/data/sprites/portrait-icon.toml +7 -4
  41. package/data/sprites/room-icon.toml +11 -2
  42. package/data/sprites/spider-icon.toml +9 -3
  43. package/data/sprites/study-icon.toml +10 -2
  44. package/package.json +5 -4
  45. package/src/adapters/memory/core.mjs +20 -0
  46. package/src/domain/ask-vocab.mjs +71 -0
  47. package/src/domain/ask.mjs +168 -0
  48. package/src/domain/game-config.mjs +11 -0
  49. package/src/domain/grammar/ace.mjs +40 -4
  50. package/src/domain/grammar/lexicon-core.json +2 -1
  51. package/src/domain/grammar/lexicon.mjs +18 -0
  52. package/src/domain/mud-facts.mjs +15 -0
  53. package/src/domain/reference-pack.mjs +31 -7
  54. package/src/domain/router/drive.mjs +35 -9
  55. package/src/domain/router/registry.mjs +24 -4
  56. package/src/domain/router/resolver.mjs +102 -40
  57. package/src/domain/scene-compose.mjs +117 -0
  58. package/src/domain/spider-fly-world.mjs +37 -1
  59. package/src/domain/sprite-facts.mjs +0 -0
  60. package/src/domain/sprite-request.mjs +156 -0
  61. package/src/domain/sprite-templates.mjs +169 -20
  62. package/src/services/adventure-editor.mjs +8 -14
  63. package/src/services/adventure-viz.mjs +209 -157
  64. package/src/services/adventure.mjs +526 -391
  65. package/src/services/chat-page-viz.mjs +69 -25
  66. package/src/services/chat.mjs +200 -51
  67. package/src/services/code-explorer-viz.mjs +102 -62
  68. package/src/services/extract-facts.mjs +4 -7
  69. package/src/services/ingest-viz.mjs +68 -82
  70. package/src/services/ledger-viz.mjs +136 -67
  71. package/src/services/memory-panel-viz.mjs +62 -0
  72. package/src/services/mud-editor.mjs +10 -15
  73. package/src/services/mud-turn.mjs +6 -6
  74. package/src/services/mud-viz.mjs +1016 -208
  75. package/src/services/p2p-room.mjs +90 -23
  76. package/src/services/plan-pddl.mjs +3 -1
  77. package/src/services/plan-viz.mjs +123 -64
  78. package/src/services/research-viz.mjs +160 -108
  79. package/src/services/spider-fly-turn.mjs +15 -23
  80. package/src/services/spider-fly-viz.mjs +146 -161
  81. package/src/services/spider-fly.mjs +69 -11
  82. package/src/services/sprite-catalog-viz.mjs +414 -240
  83. package/src/services/viz-boot.mjs +71 -0
  84. package/src/services/viz-room-graph.mjs +203 -0
  85. package/src/services/viz-theme.mjs +90 -1
  86. package/src/services/viz-ticker.mjs +22 -0
  87. package/src/surfaces/web/adventure-browser-entry.mjs +49 -33
  88. package/src/surfaces/web/chat-browser-entry.mjs +30 -105
  89. package/src/surfaces/web/code-explorer-browser-entry.mjs +168 -24
  90. package/src/surfaces/web/ingest-browser-entry.mjs +3 -13
  91. package/src/surfaces/web/ledger-browser-entry.mjs +7 -47
  92. package/src/surfaces/web/memory-ask-browser.bundle.js +127 -124
  93. package/src/surfaces/web/memory-stats.mjs +11 -0
  94. package/src/surfaces/web/mud-browser-entry.mjs +71 -29
  95. package/src/surfaces/web/plan-browser-entry.mjs +22 -40
  96. package/src/surfaces/web/research-browser-entry.mjs +26 -41
  97. package/src/surfaces/web/spider-fly-browser-entry.mjs +45 -28
  98. package/src/surfaces/web/sprites-browser-entry.mjs +14 -27
  99. package/src/surfaces/web/turn-session.mjs +120 -0
  100. package/src/tools/definitions.mjs +30 -0
  101. package/src/tools/handlers/index.mjs +6 -3
  102. package/src/tools/handlers/kit.mjs +19 -2
  103. package/src/tools/handlers/tmct-ask.mjs +11 -6
  104. package/src/tools/handlers/tmct-ingest.mjs +5 -1
  105. package/src/tools/handlers/tmct-related.mjs +4 -4
  106. package/src/tools/handlers/tmct-sprite.mjs +147 -0
  107. package/src/tools/memory-fallthrough.mjs +9 -2
  108. package/src/tools/server.mjs +25 -1
@@ -28,7 +28,7 @@
28
28
 
29
29
  import {
30
30
  loadLexicon, lookupNoun, lookupVerb, lookupAdjective, lookupProperName,
31
- predicateOf, numberOf, classify,
31
+ predicateOf, numberOf, classify, OF_CLASSIFIER_HEADS, OF_PARTITIVE_HEADS,
32
32
  } from "./lexicon.mjs";
33
33
  import { fuzzyMatchInSet } from "../interpret/fuzzy.mjs";
34
34
 
@@ -85,7 +85,15 @@ function local(lexicon, term) {
85
85
  const stripDet = (tokens) =>
86
86
  tokens.length > 1 && DET.has(tokens[0].toLowerCase()) ? tokens.slice(1) : tokens;
87
87
 
88
- /** Resolve a 1–2 word noun phrase: PROPERNAME | code-ref | NOUN | ADJ NOUN.
88
+ /** A token a compound noun phrase may be built out of: a plain word (never a
89
+ * code-shaped ref, a number or a CURIE) that is not a DECLARED proper name —
90
+ * a name in a compound slot ("GitLab pipeline") is a structural miss, not a
91
+ * compound. Shared by both compound arms of resolveNP below. */
92
+ const compoundableWord = (lexicon, token) =>
93
+ /^[a-z][a-z'-]*$/i.test(token) && !lookupProperName(lexicon, token);
94
+
95
+ /** Resolve a noun phrase: PROPERNAME | code-ref | NOUN | ADJ NOUN | NOUN NOUN
96
+ * | NOUN of NOUN (the last two opt-in, see `allowCompound`).
89
97
  * Returns { term, individual, extras, unknown } — `term` null on a miss with
90
98
  * the undeclared tokens in `unknown` (empty `unknown` = structurally
91
99
  * unparseable phrase → the caller returns a hard null). `extras` carries the
@@ -149,8 +157,7 @@ function resolveNP(lexicon, tokensIn, { allowCompound = false } = {}) {
149
157
  // DECLARED proper name in either slot ("GitLab pipeline") keeps the
150
158
  // structural miss below — a name in the wrong slot, not a compound.
151
159
  if (allowCompound
152
- && !lookupProperName(lexicon, tokens[0]) && !lookupProperName(lexicon, tokens[1])
153
- && /^[a-z][a-z'-]*$/i.test(tokens[0]) && /^[a-z][a-z'-]*$/i.test(tokens[1])) {
160
+ && compoundableWord(lexicon, tokens[0]) && compoundableWord(lexicon, tokens[1])) {
154
161
  const n0 = lookupNoun(lexicon, tokens[0], { singularOnly: false });
155
162
  const n1 = lookupNoun(lexicon, tokens[1], { singularOnly });
156
163
  if (n0 && n1) {
@@ -162,6 +169,35 @@ function resolveNP(lexicon, tokensIn, { allowCompound = false } = {}) {
162
169
  const unknown = tokens.filter((t) => !classify(t, lexicon));
163
170
  return { term: null, individual: false, extras: [], unknown };
164
171
  }
172
+ // "NOUN of NOUN" is ONE compound noun ("unit of work", "chain of command",
173
+ // "conflict of interest"), joined with its own "of" the same way two plain
174
+ // nouns are joined with a space, so the taught fact and the query side
175
+ // unify on one term. Opt-in through the same `allowCompound` gate and the
176
+ // same compoundableWord shape test as the two-noun arm above; the head noun
177
+ // leads, so an "a"/"an" agrees with IT.
178
+ //
179
+ // Which "of" this claims is decided by the head, against the two closed
180
+ // of-frame vocabularies in lexicon.mjs — the same ones the fact extractor
181
+ // reads. A CLASSIFIER head is a rewrite of the inner noun rather than a
182
+ // compound ("a kind of dog" is a dog), and a PARTITIVE head states quantity
183
+ // or composition rather than a class ("a piece of cake", "a lot of dogs");
184
+ // both decline here and land on the miss below. The relational reading is
185
+ // pattern 7's and is claimed before any of this: parseAce routes "the N of N
186
+ // is VALUE" to parseOfForm, so "the version of tmct is 0.2.0" never reaches
187
+ // a copula split at all. An inner determiner is likewise out of the shape —
188
+ // "a piece of the cake" is four tokens and no compound noun carries one.
189
+ if (tokens.length === 3 && allowCompound && tokens[1].toLowerCase() === "of"
190
+ && compoundableWord(lexicon, tokens[0]) && compoundableWord(lexicon, tokens[2])) {
191
+ const head = tokens[0].toLowerCase();
192
+ const inner = tokens[2].toLowerCase();
193
+ if (!OF_CLASSIFIER_HEADS.has(head) && !OF_PARTITIVE_HEADS.has(head)) {
194
+ const headNoun = lookupNoun(lexicon, head, { singularOnly });
195
+ const innerNoun = lookupNoun(lexicon, inner, { singularOnly: false });
196
+ if (headNoun && innerNoun) {
197
+ return { term: `${ns}${head} of ${inner}`, individual: false, noun: headNoun, extras: [], unknown: [] };
198
+ }
199
+ }
200
+ }
165
201
  // 0 or 3+ tokens: not a fragment NP. Name the undeclared words if any.
166
202
  return { term: null, individual: false, extras: [], unknown: tokens.filter((t) => !classify(t, lexicon)) };
167
203
  }
@@ -9448,7 +9448,8 @@
9448
9448
  "danger": {},
9449
9449
  "push": {},
9450
9450
  "refusal": {},
9451
- "tooling": {}
9451
+ "tooling": {},
9452
+ "trelvox": {}
9452
9453
  },
9453
9454
  "verbs": {
9454
9455
  "import": {},
@@ -43,6 +43,24 @@ export const QUANTIFIERS = Object.freeze({
43
43
  exactly: "owl:cardinality",
44
44
  });
45
45
 
46
+ // The two closed head vocabularies that say what an "of" between two nouns is
47
+ // DOING, so every reader of the of-frame decides it the same way instead of
48
+ // each growing its own list. A CLASSIFIER head reads THROUGH to the inner noun
49
+ // — "a kind of dog" is a dog, "a type of mammal" is a mammal — so the phrase
50
+ // is a rewrite of the inner noun, never a term of its own. A PARTITIVE head
51
+ // states quantity or composition — "a piece of cake", "a lot of dogs", "a body
52
+ // of ice" — so the phrase names no class at all. Anything else ("unit of
53
+ // work", "chain of command") is a compound noun the of belongs to.
54
+ /** Of-frame heads that read through to the inner noun. */
55
+ export const OF_CLASSIFIER_HEADS = Object.freeze(new Set([
56
+ "type", "kind", "sort", "form", "class", "variety", "species", "breed", "genus",
57
+ ]));
58
+ /** Of-frame heads that state quantity or composition, not a class. */
59
+ export const OF_PARTITIVE_HEADS = Object.freeze(new Set([
60
+ "body", "mass", "group", "collection", "set", "series", "number", "amount",
61
+ "piece", "part", "lot", "pair", "bunch", "pile",
62
+ ]));
63
+
46
64
  const NUMBER_WORDS = Object.freeze({
47
65
  one: 1, two: 2, three: 3, four: 4, five: 5,
48
66
  six: 6, seven: 7, eight: 8, nine: 9, ten: 10,
@@ -0,0 +1,15 @@
1
+ // mud-facts.mjs — the one `mgx:is-predator` reader shared across the
2
+ // client/server boundary: mud-turn.mjs (the server-side turn engine, deciding
3
+ // which rooms to route an animal away from) and mud-viz.mjs (the client-facing
4
+ // view, deciding which room to draw a pounce against) each asked "which
5
+ // subject does the world mark dangerous" their own way, over the exact same
6
+ // filter. One reader here, so the two never drift apart on what counts as a
7
+ // predator.
8
+
9
+ /** Every subject the world marks `mgx:is-predator` true, in fact-row order.
10
+ * Pure. */
11
+ export function predatorSubjects(rows) {
12
+ return (rows || [])
13
+ .filter((r) => r.predicate === "mgx:is-predator" && r.object === "true")
14
+ .map((r) => r.subject);
15
+ }
@@ -35,25 +35,49 @@ export function isReferenceIndexEntry(e) {
35
35
  && Number.isInteger(e.r) && e.r > 0;
36
36
  }
37
37
 
38
- /** A shard row: {term, title, text, summary, url, revid, isa?}. */
38
+ /** A shard row: {term, title, text, summary, url, revid, isa?, source?,
39
+ * licence?}. `source`/`licence` are optional per-entry overrides of the
40
+ * citation's source name and licence line (renderReferenceAnswer, below) —
41
+ * absent on every real Simple English Wikipedia row the build pipeline
42
+ * emits, present only on a hand-added synthetic entry (e.g. a demo/test
43
+ * term) whose citation must name its OWN real source, not Wikipedia's. */
39
44
  export function isReferenceArticleRow(row) {
40
45
  if (!row || typeof row !== "object") return false;
41
46
  for (const field of ["term", "title", "text", "summary", "url"]) {
42
47
  if (typeof row[field] !== "string" || !row[field]) return false;
43
48
  }
44
49
  if (!Number.isInteger(row.revid) || row.revid <= 0) return false;
45
- if (row.isa !== undefined && (typeof row.isa !== "string" || !row.isa)) return false;
50
+ for (const field of ["isa", "source", "licence"]) {
51
+ if (row[field] !== undefined && (typeof row[field] !== "string" || !row[field])) return false;
52
+ }
46
53
  return true;
47
54
  }
48
55
 
56
+ /** The default citation source/licence — every article the build pipeline
57
+ * (scripts/fetch-reference-pack.mjs) emits from the pinned Simple English
58
+ * Wikipedia dump carries neither field, so this is what renderReferenceAnswer
59
+ * falls back to for the overwhelming majority of rows. */
60
+ export const DEFAULT_REFERENCE_SOURCE = "Simple English Wikipedia";
61
+ export const DEFAULT_REFERENCE_LICENCE = "CC BY-SA 4.0";
62
+
49
63
  /** The cited answer for a clean miss the pack could ground: the article's
50
- * summary with its title, licence and revision-pinned URL always visible,
51
- * then the grain cue. Without the cue a reader has to recognize the source
52
- * name to tell this apart from a fact read out of their own graph — both
53
- * answer the same "what is X" question in the same voice. */
64
+ * summary with its title, licence and source always visible, then the grain
65
+ * cue. Without the cue a reader has to recognize the source name to tell
66
+ * this apart from a fact read out of their own graph — both answer the same
67
+ * "what is X" question in the same voice.
68
+ *
69
+ * The source/licence line reads the article's OWN `source`/`licence` fields
70
+ * when present, falling back to the Wikipedia defaults otherwise — every
71
+ * real pack row renders byte-identically to before this existed. Wikipedia's
72
+ * revision-pinned `?oldid=` query is a convention of that source alone, so it
73
+ * is only appended when the article is citing the default source; a
74
+ * non-Wikipedia entry's own `url` is shown bare. */
54
75
  export function renderReferenceAnswer(term, article) {
76
+ const source = article.source ?? DEFAULT_REFERENCE_SOURCE;
77
+ const licence = article.licence ?? DEFAULT_REFERENCE_LICENCE;
78
+ const url = source === DEFAULT_REFERENCE_SOURCE ? `${article.url}?oldid=${article.revid}` : article.url;
55
79
  return `${term} — ${article.summary} (source: reference article "${article.title}", `
56
- + `Simple English Wikipedia, CC BY-SA 4.0 — ${article.url}?oldid=${article.revid})`
80
+ + `${source}, ${licence} — ${url})`
57
81
  + ` ${GENERAL_VOCABULARY_CUE}`;
58
82
  }
59
83
 
@@ -31,7 +31,7 @@
31
31
  import { resolveOne, backwardChainWorld, resolveMemoryTerm } from "./resolver.mjs";
32
32
  import { plan, isMultiStep, decompose, MAX_STEPS } from "./planner.mjs";
33
33
  import { goalReason } from "./goal-reasoner.mjs";
34
- import { capabilities } from "./registry.mjs";
34
+ import { capabilities, preconditionsOf, PRECOND } from "./registry.mjs";
35
35
  import { registerTaughtActions } from "./taught.mjs";
36
36
  import { intersect, fallbackIfEmpty, guardIfEmpty, memberIndividuals, membersReaching, resultSetOf } from "./results.mjs";
37
37
  import { resolveObject } from "../ask.mjs";
@@ -288,23 +288,49 @@ export async function runCapabilityPlan(request, tools, ctx) {
288
288
  * same store, re-reading it per request via ctx.readTaughtStore. The new
289
289
  * registrations' unregister disposers ride the ctx as `ctx.disposers`; the
290
290
  * caller runs them when the ctx is done. The same `memoryDir` also opens
291
- * `ctx.resolveMemoryTerm` — resolveOne's binding oracle for a memoryTerm slot
292
- * (tmct_related's `term`), re-reading the store's fact rows per request
291
+ * `ctx.resolveMemoryTerm` — resolveOne's binding oracle for a KINDS.MemoryTerm
292
+ * slot (tmct_related's `term`), re-reading the store's fact rows per request
293
293
  * through resolveMemoryTerm (resolver.mjs), the memory-graph sibling of
294
- * `resolve` above. */
294
+ * `resolve` above.
295
+ *
296
+ * MEMORY-ONLY MODE: pass a `memoryDir` with no `graph` and no `source`, and
297
+ * the ctx builds with no code graph at all — the shape of a page that lives on
298
+ * the memory graph alone. `resolve` then misses every term (a code-graph slot
299
+ * refuses honestly at binding), dispatch refuses any capability whose
300
+ * preconditions need a loaded code graph, and the memory lanes — MemoryTerm
301
+ * binding, the taught world-goal simulation — carry the whole ctx. `config`
302
+ * (if any) is still handed to dispatchTool so a memory-store tool can derive
303
+ * its backend. A call with no graph, no source and no memoryDir is a wiring
304
+ * error and throws. */
295
305
  export async function buildCapabilityPlanCtx({
296
- config, source, tel = null, graph = null, memoryDir = null,
306
+ config, source = null, tel = null, graph = null, memoryDir = null,
297
307
  dispatchTool, isToolError = () => false, selectTool = null,
298
308
  loadMemory = null, readFactRows = null, readRuleRows = null,
299
309
  } = {}) {
300
- const g = graph || parseEntities(await source.fetchEntities(config));
301
- const resolve = (term) => resolveObject(g, term);
310
+ const g = graph || (source ? parseEntities(await source.fetchEntities(config)) : null);
311
+ if (!g && !(memoryDir && loadMemory)) {
312
+ throw new Error("buildCapabilityPlanCtx: pass a graph, a source to load one, or a memoryDir (memory-only mode)");
313
+ }
314
+ const resolve = g ? (term) => resolveObject(g, term) : () => ({ match: null, ambiguous: false, candidates: [] });
315
+ const needsCodeGraph = (name) => preconditionsOf(name).some((p) => p.pred === PRECOND.graphLoaded);
302
316
  const dispatch = async (name, input) => {
317
+ if (!g && needsCodeGraph(name)) {
318
+ return { ok: false, error: `${name} queries the code graph, and this memory-only context has none loaded` };
319
+ }
303
320
  try {
304
- const text = await dispatchTool(name, input, { config, source, tel });
321
+ // `source ?? undefined` so a memory-only caller's explicit null never
322
+ // suppresses dispatchTool's own default source for a tool that loads.
323
+ // `memoryBackend` hands a memory-reading tool the SAME open store this
324
+ // ctx binds its MemoryTerm slots against, so binding and dispatch can
325
+ // never answer from two different stores — and so a store that was never
326
+ // derived from a config (a browser page's in-memory one) is reachable at
327
+ // all.
328
+ const text = await dispatchTool(name, input, {
329
+ config, source: source ?? undefined, tel, memoryBackend: memoryDir ?? undefined,
330
+ });
305
331
  const primary = input && (input.symbol ?? input.module ?? input.class ?? input.query);
306
332
  const resolved = primary ? resolve(String(primary)).match : null;
307
- const result = resultSetOf(g, name, input, resolved);
333
+ const result = g ? resultSetOf(g, name, input, resolved) : [];
308
334
  return { ok: true, text, resolved, result };
309
335
  } catch (e) {
310
336
  if (isToolError(e)) return { ok: false, error: e.message };
@@ -26,16 +26,25 @@ export const VOCAB = Object.freeze({
26
26
 
27
27
  // Parameter entity-KINDS — the seon/mgx classes a slot ranges over. `Query` and
28
28
  // `Kind`/`Package` are free-text / enum slots (no graph resolution); the rest
29
- // name a graph entity the resolver must prove RESOLVES before the call fires.
29
+ // name a graph entity the resolver must prove RESOLVES before the call fires
30
+ // code-graph kinds through resolveObject (ask.mjs), MemoryTerm through
31
+ // resolveMemoryTerm (resolver.mjs), the conversational-memory sibling oracle.
30
32
  export const KINDS = Object.freeze({
31
33
  Symbol: "seon:CodeEntity", // any code symbol: function/method/class/module/attribute
32
34
  Module: "mgx:Module", // SEON has no JS-module class (its nearest are Namespace/main:File); owned
33
35
  Class: "seon:ClassType", // SEON's real class for a class definition
36
+ MemoryTerm: "mgx:MemoryTerm", // owned: a conversational-memory term (a minted skos:Concept or a world-fact subject/object) — binds in the memory graph, never the code graph
34
37
  Query: "cap:FreeText", // lexical search string — no resolution precondition
35
38
  Kind: "cap:KindFilter", // enum: function|class|method|… (search filter)
36
39
  Package: "cap:PackageName", // optional architecture-scope filter
37
40
  });
38
41
 
42
+ // The kinds that bind in the conversational-memory graph (resolveMemoryTerm)
43
+ // rather than the code graph (resolveObject). A consumer that walks KINDS for
44
+ // code-graph-resolvable classes (e.g. the synthbench enumerator's focus
45
+ // classes) must exclude these — a memory term is never a code-graph focus.
46
+ export const MEMORY_KINDS = Object.freeze([KINDS.MemoryTerm]);
47
+
39
48
  // Precondition PREDICATE tags (the small closed vocabulary a precondition uses).
40
49
  export const PRECOND = Object.freeze({
41
50
  graphLoaded: "cap:graph-loaded", // a graph artifact is present + parseable
@@ -87,7 +96,8 @@ function capability({ name, label, question, params = [], preconditions = [], ad
87
96
  // callees/tests/history/… take `symbol`; impact/exports take `module`; members/
88
97
  // subclasses take `class`; search takes `query` (+ optional kind/name/decorator);
89
98
  // architecture takes an optional `package`; untested takes nothing; related
90
- // takes `term` (a memory-graph concept term).
99
+ // takes `term` (a memory-graph concept term); sprite takes `class` (a memory-graph
100
+ // term) plus optional `expression`/`size`.
91
101
 
92
102
  const CAPABILITIES = Object.freeze([
93
103
  capability({
@@ -187,10 +197,20 @@ const CAPABILITIES = Object.freeze([
187
197
  }),
188
198
  capability({
189
199
  name: "tmct_related", label: "related", question: "a term's synonyms and related concepts (the SKOS view over the conversational-memory graph)",
190
- params: [param("term", KINDS.Query, { note: "a concept term, matched against memory relation facts rather than resolved in the code graph" })],
191
- preconditions: [memoryFacts()],
200
+ params: [param("term", KINDS.MemoryTerm, { note: "a concept term, matched against memory relation facts rather than resolved in the code graph" })],
201
+ preconditions: [memoryFacts(), resolves("term", KINDS.MemoryTerm)],
192
202
  add: [knows("related", "term")],
193
203
  }),
204
+ capability({
205
+ name: "tmct_sprite", label: "sprite", question: "the sprite for a class, with the expression and size asked for",
206
+ params: [
207
+ param("class", KINDS.MemoryTerm, { arg: "class", note: "a world class, matched against the memory graph's own fact rows rather than resolved in the code graph" }),
208
+ param("expression", KINDS.Kind, { required: false, note: "one of sprite-expressions.mjs's palette words, carried as an mgx:feels fact" }),
209
+ param("size", KINDS.Kind, { required: false, note: "a taught mgx:hasProperty size word, resolved to a render scale — not a template tier" }),
210
+ ],
211
+ preconditions: [memoryFacts(), resolves("class", KINDS.MemoryTerm)],
212
+ add: [knows("sprite", "class")],
213
+ }),
194
214
  ]);
195
215
 
196
216
  // The live capability set: the built-in frozen array is the seed; registration
@@ -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); an ambiguous or no-match term
15
- // is an honest refuse, never a guess.
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". `arg: "term"` is the one memory-graph-bound slot in this
90
- // table (see resolveMemoryTerm below); every other frame's arg binds against
91
- // the code graph via ctx.resolve.
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
- export function extractEntity(request) {
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 pool = tokens.filter((t) => !STOP.has(t.toLowerCase()));
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
- const term = f.arg === "query" ? searchQuery(request) : extractEntity(request);
191
- // memoryTerm: this slot binds against the memory graph's SKOS concept view
192
- // (resolveMemoryTerm below), not the code graph resolveObject resolves
193
- // every other frame's arg against "term" is the one param kind this is
194
- // true for (tmct_related's own memory-facts-gated param).
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", memoryTerm: f.arg === "term",
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's SKOS concept view
219
- * (skos-view.mjs's buildSkosConceptView) the memory-graph sibling of
220
- * resolveObject, for a param whose precondition is memory-facts rather than
221
- * a code-graph `resolves`. Same `{ match, ambiguous }` shape resolveObject
222
- * returns, so resolveOne's generic binding step treats both the same way:
223
- * `match.label` is the RAW queried term (not the concept's canonicalised
224
- * prefLabel), because tmct_related's own lookup (relatedForTerm) re-derives
225
- * the concept from whatever term it's given the bound call should carry
226
- * what the user actually asked about. No code-graph fallback: a term the
227
- * store holds no synonym/related facts for is an honest miss, never a guess.
228
- * `rows` is a loadMemory+readFactRows payload the same trust-bearing rows
229
- * skosRelatedAnswer (chat.mjs) and tmct_related (the tool handler) read. */
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 (!view.conceptIdForTerm(t)) return { match: null };
235
- return { match: { label: t, class: "skos:Concept" }, ambiguous: false, tier: "memory-concept" };
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 memoryTerm slot, resolveMemoryTerm
332
- // (the memory graph's SKOS concept view) the two binding oracles never mix on one pick.
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 r = pick.memoryTerm
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(pick.memoryTerm
343
- ? `"${term}" has no synonym/related facts in the memory graph (honest miss)`
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 SKOS concept view has no code-graph tests-edge notion, so the
350
- // sibling tie-check only applies on the code-graph resolution path.
351
- const sibling = (!pick.memoryTerm && !r.ambiguous)
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
- input = { [pick.arg]: r.match.label };
364
- why = [...why, pick.memoryTerm
365
- ? `resolveMemoryTerm: "${term}" mints a memory-graph SKOS concept (tier ${r.tier})`
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 };