@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
@@ -0,0 +1,120 @@
1
+ // turn-session.mjs — the turn-dispatch wrapper nine near-identical browser
2
+ // sessions each hand-rolled (chat-browser-entry.mjs, code-explorer-browser-
3
+ // entry.mjs, ledger-browser-entry.mjs, adventure-browser-entry.mjs, spider-
4
+ // fly-browser-entry.mjs, plan-browser-entry.mjs, research-browser-entry.mjs,
5
+ // mud-browser-entry.mjs, sprites-browser-entry.mjs): a closure over
6
+ // focus/last/planState/researchState, a `try { runTurn(...) } catch` that
7
+ // turns a throw into a safe answer instead of ending the session (the page
8
+ // has no other chance to show that turn's reply), the post-call state
9
+ // capture, and a return shape covering every field one caller or another
10
+ // reads back.
11
+ //
12
+ // The nine differ in what they hand `runTurn` beyond the common core
13
+ // (`uiContext`, `synthesisBudget`, `gameConfig`, `researchConfig`,
14
+ // `actingSubject`, a per-character `sessionId`...) and in what they do with a
15
+ // turn's result besides the standard focus/last/planState/researchState fold
16
+ // (sync an externally-held plan holder, grow a visited-rooms set, bump a
17
+ // per-character turn tally). `buildExtraOptions`/`captureExtraState` below
18
+ // are exactly those two seams — everything else is common enough to live
19
+ // here once.
20
+ //
21
+ // The catch fallback text converges on the FULLEST of the nine: several
22
+ // callers had already drifted to a shorter one (mud-browser-entry.mjs drops
23
+ // "or /help"; research-browser-entry.mjs drops "or /help" AND "answering
24
+ // that") — proof, not just a risk, that duplicating this string invites
25
+ // exactly this drift.
26
+ import { runTurn as defaultRunTurn } from "../../services/chat.mjs";
27
+
28
+ function turnErrorFallback(message) {
29
+ return `Something went wrong answering that (${message}). Try rephrasing, or /help.`;
30
+ }
31
+
32
+ /**
33
+ * `createTurnSession({ memoryDir, graph, lexicon, sessionId, vocabHint,
34
+ * buildExtraOptions, captureExtraState })` — a live turn-dispatch session
35
+ * over the real engine (`runTurn`), threading focus/last/planState/
36
+ * researchState across calls the way every browser chat dock does.
37
+ *
38
+ * `vocabHint` is the session-lifetime hint every caller computes once at
39
+ * creation (a fixed string, or `vocabExampleHint(vocabSeeded)`'s result) —
40
+ * never recomputed per turn.
41
+ *
42
+ * `buildExtraOptions(state, callArgs)` (optional) returns extra `runTurn`
43
+ * options to merge OVER the defaults below — anything a specific page needs
44
+ * (`uiContext: "browser"`, `synthesisBudget`, `gameConfig`, `researchConfig`,
45
+ * a per-character `actingSubject`/`sessionId`, or a `planState` read from an
46
+ * external holder instead of this closure's own). `state` is
47
+ * `{ focus, last, planState, researchState }` as this turn is about to run;
48
+ * `callArgs` is whatever extra argument `turn(line, callArgs)` was given
49
+ * (plan-browser-entry.mjs's own per-call `{ maxDepth }` override, for one).
50
+ *
51
+ * `captureExtraState(result, state, callArgs)` (optional, may be async) runs
52
+ * once after a SUCCESSFUL turn, with `state` already folded forward
53
+ * (focus/last/planState/researchState updated) — the caller's own chance to
54
+ * do further bookkeeping (sync an external plan holder, reload memory to
55
+ * grow a visited-rooms set, bump a turn tally). Never runs on a throw: none
56
+ * of the nine originals touch their own state when `runTurn` itself failed.
57
+ *
58
+ * Returns `{ memoryDir, sessionId, turn, focus, last, planState,
59
+ * setPlanState, researchState, setResearchState }`. `turn(line, callArgs)`
60
+ * resolves to `{ answer, end, record, plan, research }` — the union of every
61
+ * field any of the nine originals reads; a caller ignores what it doesn't
62
+ * need. `research` is passed through undefined/null/a queue snapshot exactly
63
+ * as `runTurn` returned it (a caller may distinguish "not a research turn"
64
+ * from "a research turn that just ended").
65
+ *
66
+ * `runTurn` (default: the real engine) is injectable, purely so a test can
67
+ * force the catch path deterministically without needing an input that
68
+ * happens to make the real, deliberately crash-resistant engine throw.
69
+ */
70
+ export function createTurnSession({
71
+ memoryDir, graph, lexicon, sessionId, vocabHint = "",
72
+ buildExtraOptions = () => ({}),
73
+ captureExtraState = async () => {},
74
+ runTurn = defaultRunTurn,
75
+ } = {}) {
76
+ let focus = null;
77
+ let last = null;
78
+ let planState = null;
79
+ let researchState = null;
80
+
81
+ async function turn(line, callArgs) {
82
+ const before = { focus, last, planState, researchState };
83
+ const extra = buildExtraOptions(before, callArgs) || {};
84
+ let result;
85
+ try {
86
+ result = await runTurn(line, {
87
+ config: null, source: null, graph, focus, last, memoryDir, sessionId,
88
+ env: {}, lexicon, vocabHint, planState, researchState,
89
+ ...extra,
90
+ });
91
+ } catch (e) {
92
+ const message = e instanceof Error ? e.message : String(e);
93
+ return { answer: turnErrorFallback(message), end: false, record: null, plan: null, research: null };
94
+ }
95
+ focus = result.focus;
96
+ last = result.last;
97
+ if ("planState" in result) planState = result.planState;
98
+ if ("researchState" in result) researchState = result.researchState;
99
+ await captureExtraState(result, { focus, last, planState, researchState }, callArgs);
100
+ return {
101
+ answer: result.answer,
102
+ end: Boolean(result.end),
103
+ record: result.record ?? null,
104
+ plan: result.plan ?? null,
105
+ research: result.research,
106
+ };
107
+ }
108
+
109
+ return {
110
+ memoryDir,
111
+ sessionId,
112
+ get focus() { return focus; },
113
+ get last() { return last; },
114
+ get planState() { return planState; },
115
+ setPlanState(next) { planState = next; },
116
+ get researchState() { return researchState; },
117
+ setResearchState(next) { researchState = next; },
118
+ turn,
119
+ };
120
+ }
@@ -20,12 +20,19 @@
20
20
  // restated here, so a verb the parser learns is a verb the documentation gains.
21
21
 
22
22
  import { RELATIONS, WHERE_MARKERS } from "../domain/ask-vocab.mjs";
23
+ import { EXPRESSION_PALETTE } from "../domain/sprite-expressions.mjs";
23
24
 
24
25
  /** The relation kinds tmct_ask traverses, each with the opening verbs a question can
25
26
  * use for it. Read from the parser's RELATIONS table — never a hand-kept copy. */
26
27
  export const askLexicon = () =>
27
28
  Object.entries(RELATIONS).map(([kind, { verbs }]) => ({ kind, verbs: verbs.slice(0, 4) }));
28
29
 
30
+ /** The expression words tmct_sprite's schema offers. Read from the face-fragment
31
+ * palette sprite-expressions.mjs already ships — the same derivation askLexicon
32
+ * does for the ask verbs, so an expression the palette gains is an expression
33
+ * the schema gains. */
34
+ export const spriteExpressionEnum = () => Object.keys(EXPRESSION_PALETTE).sort();
35
+
29
36
  const symbolArg = (description) => ({
30
37
  type: "object",
31
38
  required: ["symbol"],
@@ -193,6 +200,29 @@ export const TOOL_DEFINITIONS = Object.freeze([
193
200
  },
194
201
  example: { term: "sofa" },
195
202
  },
203
+ {
204
+ name: "tmct_sprite",
205
+ tier: "cold",
206
+ summary:
207
+ "The sprite markup for a class, resolved up the taught rdfs:subClassOf chain — with the expression and size asked for, and the ancestor chain the resolver walked to find it.",
208
+ inputSchema: {
209
+ type: "object",
210
+ required: ["class"],
211
+ properties: {
212
+ class: { type: "string", description: "The class to draw, e.g. spider. Resolved through the memory graph's own rdfs:subClassOf taxonomy." },
213
+ expression: {
214
+ type: "string",
215
+ enum: spriteExpressionEnum(),
216
+ description: "The face the sprite wears, as an mgx:feels value. A class whose template takes no expression parameter misses rather than dropping it.",
217
+ },
218
+ size: {
219
+ type: "string",
220
+ description: "A taught size property word (mgx:hasProperty), resolved to a numeric render SCALE — the size of the thing drawn, not a choice of template tier.",
221
+ },
222
+ },
223
+ },
224
+ example: { class: "spider", expression: "happy", size: "large" },
225
+ },
196
226
  {
197
227
  name: "tmct_architecture",
198
228
  tier: "cold",
@@ -1,9 +1,10 @@
1
1
  // The tool-name → handler registry dispatchTool maps over. One module per tool, so a
2
2
  // tool's behaviour has exactly one home and this file only wires names to it.
3
3
  //
4
- // A handler takes (args, ctx) and returns the caller-facing string. ctx carries the
5
- // already-loaded { graph, svc, config, repoRoot }. A handler marked `ownsGraphLoad`
6
- // gets { config, source, tel } instead and loads whatever it needs itself.
4
+ // A handler takes (args, ctx) and returns the caller-facing string, or kit.mjs's
5
+ // toolResult({ content, data }) when it already holds the structured form of what that
6
+ // string says. ctx carries the already-loaded { graph, svc, config, repoRoot }. A handler
7
+ // marked `ownsGraphLoad` gets { config, source, tel } instead and loads what it needs.
7
8
 
8
9
  import { tmct_context } from "./tmct-context.mjs";
9
10
  import { tmct_context_more } from "./tmct-context-more.mjs";
@@ -15,6 +16,7 @@ import { tmct_search } from "./tmct-search.mjs";
15
16
  import { tmct_members } from "./tmct-members.mjs";
16
17
  import { tmct_subclasses } from "./tmct-subclasses.mjs";
17
18
  import { tmct_related } from "./tmct-related.mjs";
19
+ import { tmct_sprite } from "./tmct-sprite.mjs";
18
20
  import { tmct_architecture } from "./tmct-architecture.mjs";
19
21
  import { tmct_exports } from "./tmct-exports.mjs";
20
22
  import { tmct_untested } from "./tmct-untested.mjs";
@@ -42,6 +44,7 @@ export const HANDLERS = Object.freeze({
42
44
  tmct_members,
43
45
  tmct_subclasses,
44
46
  tmct_related,
47
+ tmct_sprite,
45
48
  tmct_architecture,
46
49
  tmct_exports,
47
50
  tmct_untested,
@@ -1,11 +1,28 @@
1
- // What every per-tool handler module shares: the argument contract, the resolve-or-miss
2
- // contract, and the one shape most cold tools have (take a symbol, resolve it, render it).
1
+ // What every per-tool handler module shares: the argument contract, the result contract,
2
+ // the resolve-or-miss contract, and the one shape most cold tools have (take a symbol,
3
+ // resolve it, render it).
3
4
 
4
5
  import { ToolError } from "../../adapters/config.mjs";
5
6
  import { resolveSymbol } from "../../domain/codegraph.mjs";
6
7
 
7
8
  export const SNIPPET_MAX_LINES = 200;
8
9
 
10
+ /** A handler's answer when it already holds the structured form of what its prose says.
11
+ * `content` is the prose. `data` is that structure, handed to dispatchToolStructured
12
+ * callers as-is so a page renders it instead of parsing the sentence back apart.
13
+ * `text` is what the string-only callers (dispatchTool, the CLI `cli <tool>` route)
14
+ * receive; it defaults to `content`, and a tool sets it only where its flat string
15
+ * already carried the structure in-band. A handler with nothing structured to add
16
+ * keeps returning a bare string, and `data` is absent for it. */
17
+ export function toolResult({ content, data, text = null }) {
18
+ return { content, data, text: text ?? content };
19
+ }
20
+
21
+ /** A handler may return either shape, so both dispatch paths have to tell them apart. */
22
+ export const isToolResult = (value) =>
23
+ !!value && typeof value === "object"
24
+ && typeof value.content === "string" && typeof value.text === "string";
25
+
9
26
  /** A required string argument, trimmed. Missing or blank raises "<key> is required". */
10
27
  export function requiredArg(args, key) {
11
28
  const value = String(args?.[key] || "").trim();
@@ -1,14 +1,19 @@
1
1
  // tmct_ask — a plain-English structural question answered from the graph in one
2
2
  // mechanical, zero-model-call round-trip. See src/domain/ask.mjs.
3
3
 
4
- import { ToolError } from "../../adapters/config.mjs";
5
4
  import { ask } from "../../domain/ask.mjs";
6
- import { requiredArg } from "./kit.mjs";
5
+ import { requiredArg, toolResult } from "./kit.mjs";
6
+
7
+ /** The flat string a dispatchTool caller gets carries the envelope in-band, behind this
8
+ * delimiter, because a string is all that entry can hand back. Exported so the surfaces
9
+ * that still read that string split it on the one constant rather than their own copy. */
10
+ export const ASK_ENVELOPE_DELIM = "\n\n---tmct_ask---\n";
7
11
 
8
12
  export function tmct_ask(args, { graph }) {
9
13
  const { content, tmct_ask: envelope } = ask(graph, requiredArg(args, "query"));
10
- // Every dispatchTool caller (the chat surface, the CLI fallback) expects a plain string —
11
- // append the structured envelope as a delimited, machine-parseable block rather than
12
- // changing that shared contract for one tool.
13
- return `${content}\n\n---tmct_ask---\n${JSON.stringify(envelope, null, 2)}`;
14
+ return toolResult({
15
+ content,
16
+ data: envelope,
17
+ text: `${content}${ASK_ENVELOPE_DELIM}${JSON.stringify(envelope, null, 2)}`,
18
+ });
14
19
  }
@@ -16,6 +16,7 @@
16
16
 
17
17
  import { dirname } from "node:path";
18
18
  import { openConfiguredMemoryBackend } from "../../adapters/memory/core.mjs";
19
+ import { toolResult } from "./kit.mjs";
19
20
 
20
21
  export async function tmct_ingest(args, { config, ingest }) {
21
22
  const text = String(args?.text ?? "");
@@ -31,7 +32,10 @@ export async function tmct_ingest(args, { config, ingest }) {
31
32
  const header = `${result.sentences} sentence(s), ${result.recognized} recognized`
32
33
  + (optimistic ? `, ${result.optimistic.length} optimistic candidate(s)` : "")
33
34
  + `, ${result.skipped} skipped — ${grounded} fact(s) grounded.`;
34
- return grounded ? `${header}\n${result.canonical.join("\n")}` : header;
35
+ return toolResult({
36
+ content: grounded ? `${header}\n${result.canonical.join("\n")}` : header,
37
+ data: { ...result, grounded },
38
+ });
35
39
  } finally {
36
40
  await close();
37
41
  }
@@ -6,11 +6,11 @@
6
6
  import { ToolError } from "../../adapters/config.mjs";
7
7
  import { relatedForTerm } from "../../domain/skos-view.mjs";
8
8
  import { memoryFactRows } from "../memory-fallthrough.mjs";
9
- import { requiredArg } from "./kit.mjs";
9
+ import { requiredArg, toolResult } from "./kit.mjs";
10
10
 
11
- export async function tmct_related(args, { config }) {
11
+ export async function tmct_related(args, { config, memoryBackend = null }) {
12
12
  const term = requiredArg(args, "term");
13
- const hit = relatedForTerm(await memoryFactRows(config), term);
13
+ const hit = relatedForTerm(await memoryFactRows(config, memoryBackend), term);
14
14
  if (!hit) {
15
15
  throw new ToolError(
16
16
  `no synonym or related facts for "${term}" in the memory graph. ` +
@@ -21,7 +21,7 @@ export async function tmct_related(args, { config }) {
21
21
  if (hit.synonyms.length) lines.push(`synonyms (skos:altLabel): ${hit.synonyms.join(", ")}`);
22
22
  if (hit.related.length) lines.push(`related (skos:related): ${hit.related.map((c) => c.prefLabel).join(", ")}`);
23
23
  lines.push("(from the memory graph's mgx:synonym / mgx:relatedTo / mgx:similarTo facts)");
24
- return lines.join("\n");
24
+ return toolResult({ content: lines.join("\n"), data: hit });
25
25
  }
26
26
 
27
27
  // The memory graph is this tool's only source — it answers with or without a
@@ -0,0 +1,147 @@
1
+ // tmct_sprite — the sprite markup for a class, resolved through the memory
2
+ // graph's own rdfs:subClassOf taxonomy, with the expression and size the caller
3
+ // asked for and the chain the resolver walked to find it.
4
+ //
5
+ // The resolution itself is sprite-request.mjs's pure core (the same function the
6
+ // spider-and-fly page splices into its script), so this module only does the
7
+ // three things a tool has to: read the real state, hold the miss wall, and say
8
+ // the answer twice — once as a sentence, once as data.
9
+ //
10
+ // The MISS WALL, since the resolver underneath never refuses (it falls through
11
+ // to the root sprite by design, which is right for a page painting a board and
12
+ // wrong for a question):
13
+ // - an expression outside sprite-expressions.mjs's palette, or a size outside
14
+ // sprite-size.mjs's scale table, is refused before anything is resolved;
15
+ // - a class no term of whose ancestor chain carries a sprite is refused rather
16
+ // than answered with the generic root sprite;
17
+ // - an expression the resolved template does not actually take is refused
18
+ // rather than silently dropped from an otherwise plausible answer.
19
+ //
20
+ // WHICH SENSE OF "LARGE" `size` CARRIES: the taught size PROPERTY, resolved to a
21
+ // numeric render scale (sprite-size.mjs's `sizeScaleFor` — small 0.8, large 1.3,
22
+ // long 1.2, tall 1.25), not the template TIER. Two reasons. It is a fact-shaped
23
+ // question: `mgx:hasProperty large` is a real predicate the memory store can
24
+ // carry and the resolver already consults, so the tool's `memoryFacts`
25
+ // precondition and its resolution chain stay one mechanism, where a tier is the
26
+ // caller's render target rather than anything true of the thing being drawn.
27
+ // And the tier a request could name is a packaging accident: data/sprites-large/
28
+ // is excluded from the npm package, so a tier-selecting `size` would answer
29
+ // differently depending on how tmct was installed, while a scale multiplier
30
+ // answers the same everywhere. The tier in play is still REPORTED in `data.tier`
31
+ // — observed, never asked for.
32
+
33
+ import { ToolError } from "../../adapters/config.mjs";
34
+ import { classAncestorChain, SPRITE_REGISTRY } from "../../domain/sprite-map.mjs";
35
+ import { resolveSpriteAsset } from "../../domain/sprite-templates.mjs";
36
+ import { EXPRESSION_PALETTE } from "../../domain/sprite-expressions.mjs";
37
+ import { sizeScaleFor } from "../../domain/sprite-size.mjs";
38
+ import { resolveSpriteRequest } from "../../domain/sprite-request.mjs";
39
+ import { ICON_TIER_NAME, SPRITE_TIER_NAME } from "../../domain/sprite-facts.mjs";
40
+ import { readSpriteTemplateFiles } from "../../adapters/corpus/sprite-template-files.mjs";
41
+ import { readSpriteLargeTemplateFiles } from "../../adapters/corpus/sprite-large-template-files.mjs";
42
+ import { memoryFactRows } from "../memory-fallthrough.mjs";
43
+ import { requiredArg, toolResult } from "./kit.mjs";
44
+
45
+ /** The property predicate sprite-size.mjs reads a size word off. Used to PROBE
46
+ * its own closed scale table rather than restate it: a word is a size word
47
+ * exactly when it moves the scale, so a word that module gains is a word this
48
+ * tool gains. */
49
+ const SIZE_FACT_PREDICATE = "mgx:hasProperty";
50
+
51
+ /** The expression words a sprite can be asked for — sprite-expressions.mjs's own
52
+ * palette, sorted, never a hand-kept copy. */
53
+ export const spriteExpressionWords = () => Object.keys(EXPRESSION_PALETTE).sort();
54
+
55
+ /** True iff `word` is a size sprite-size.mjs's scale table recognises. */
56
+ export const isSpriteSizeWord = (word) =>
57
+ Boolean(word) && sizeScaleFor([{ predicate: SIZE_FACT_PREDICATE, object: String(word) }]) !== 1;
58
+
59
+ /** The template set to resolve against: the sprite tier when this installation
60
+ * can read it (a git checkout, or the demo site's own build), the icon tier
61
+ * otherwise. Reported alongside the answer so a caller never has to guess which
62
+ * one it got. */
63
+ function templateTier() {
64
+ const large = readSpriteLargeTemplateFiles();
65
+ if (large.length) return { templates: large, tier: SPRITE_TIER_NAME };
66
+ return { templates: readSpriteTemplateFiles(), tier: ICON_TIER_NAME };
67
+ }
68
+
69
+ function chainSentence(chain) {
70
+ return chain.map((step) => step.term).join(" -> ");
71
+ }
72
+
73
+ export async function tmct_sprite(args, { config, memoryBackend = null }) {
74
+ const className = requiredArg(args, "class").toLowerCase();
75
+ const expression = String(args?.expression || "").trim().toLowerCase();
76
+ const size = String(args?.size || "").trim().toLowerCase();
77
+
78
+ if (expression && !Object.hasOwn(EXPRESSION_PALETTE, expression)) {
79
+ throw new ToolError(
80
+ `"${expression}" is not a sprite expression. The palette holds: ${spriteExpressionWords().join(", ")}.`,
81
+ );
82
+ }
83
+ if (size && !isSpriteSizeWord(size)) {
84
+ throw new ToolError(
85
+ `"${size}" is not a size the sprite scale recognises — it reads a taught mgx:hasProperty size word off the individual, and has no entry for this one.`,
86
+ );
87
+ }
88
+
89
+ const { templates, tier } = templateTier();
90
+ const factRows = await memoryFactRows(config, memoryBackend);
91
+ const resolution = resolveSpriteRequest({ class: className, expression, size }, {
92
+ factRows,
93
+ templates,
94
+ spriteRegistry: SPRITE_REGISTRY,
95
+ resolveSpriteAsset,
96
+ classAncestorChain,
97
+ sizeScaleFor,
98
+ expressionPalette: EXPRESSION_PALETTE,
99
+ });
100
+
101
+ if (!resolution.svg || resolution.fellBackToRoot) {
102
+ throw new ToolError(
103
+ `no sprite for "${className}" — neither it nor any rdfs:subClassOf ancestor the memory graph knows ` +
104
+ `(${chainSentence(resolution.chain)}) carries one in the ${tier}. Teach the class its superclass first, or ask for a class the catalog draws.`,
105
+ );
106
+ }
107
+ if (expression && !resolution.expressionApplied) {
108
+ throw new ToolError(
109
+ `the ${tier} draws "${className}" (matched at "${resolution.matched.term}") but that template takes no expression, ` +
110
+ `so "${expression}" would not show. Ask for the plain sprite, or a class whose template carries an emotion parameter.`,
111
+ );
112
+ }
113
+
114
+ const asked = [expression ? `expression ${expression}` : "", size ? `size ${size}` : ""].filter(Boolean);
115
+ const lines = [
116
+ `sprite for "${className}"${asked.length ? ` (${asked.join(", ")})` : ""}, from the ${tier}`,
117
+ resolution.matched.hops === 0
118
+ ? `matched at "${resolution.matched.term}", the class itself, via its own ${resolution.matched.via}`
119
+ : `matched at "${resolution.matched.term}", ${resolution.matched.hops} hop(s) up the ancestor chain, via its ${resolution.matched.via}`,
120
+ `ancestor chain walked: ${chainSentence(resolution.chain)}`,
121
+ ];
122
+ if (resolution.matched.template?.parameters.length) {
123
+ lines.push(`template parameters: ${resolution.matched.template.parameters.join(", ")}`);
124
+ }
125
+ if (size) lines.push(`render scale: ${resolution.scale} (from the taught mgx:hasProperty "${size}")`);
126
+ lines.push(`${resolution.svg.length} characters of SVG markup`);
127
+
128
+ return toolResult({
129
+ content: lines.join("\n"),
130
+ data: {
131
+ class: resolution.class,
132
+ expression: resolution.expression,
133
+ size: resolution.size,
134
+ tier,
135
+ scale: resolution.scale,
136
+ svg: resolution.svg,
137
+ chain: resolution.chain,
138
+ matched: resolution.matched,
139
+ expressionApplied: resolution.expressionApplied,
140
+ },
141
+ });
142
+ }
143
+
144
+ // The sprite catalog and the conversational-memory taxonomy are this tool's only
145
+ // sources — it answers with or without a code-map graph, so the shared code-graph
146
+ // load (which refuses an empty graph) must not gate it.
147
+ tmct_sprite.ownsGraphLoad = true;
@@ -17,9 +17,16 @@ const MEMORY_LIST_CAP = 40;
17
17
  * the dir that CONTAINS .tmct/ (graphFile = <repo>/.tmct/graph.json). The read goes through
18
18
  * the repo's CONFIGURED memory backend — the same store chat's taught facts land in — opened
19
19
  * fresh per call and closed before returning, never the retired flat-file store off a raw
20
- * repo path. */
21
- export async function memoryFactRows(config) {
20
+ * repo path.
21
+ *
22
+ * `memoryBackend` is dispatchTool's seam for a caller that already holds an open store
23
+ * handle (a chat session's own `memoryDir`). When it is supplied the read goes straight to
24
+ * that handle and the caller keeps ownership of closing it. That is the only route to the
25
+ * store for a session whose store was never derived from a config at all — a browser page's
26
+ * in-memory one, where `config` is null and deriving a backend would find nothing. */
27
+ export async function memoryFactRows(config, memoryBackend = null) {
22
28
  try {
29
+ if (memoryBackend) return readFactRows(await loadMemory(memoryBackend));
23
30
  const { dir, close } = await openConfiguredMemoryBackend(dirname(dirname(config.graphFile)));
24
31
  try {
25
32
  return readFactRows(await loadMemory(dir));
@@ -9,6 +9,10 @@
9
9
  // typed service object (the Repository Interface) and hands both to the handler.
10
10
  // Each tool answers one question in ONE compact call so the caller need not
11
11
  // Read/Grep. Errors reach the caller as clean tool errors — message only, never a stack.
12
+ //
13
+ // Two entries, one dispatch: dispatchTool hands back the caller-facing string, and
14
+ // dispatchToolStructured hands back { content, data } for a caller that wants the
15
+ // answer's structure rather than its sentence (a page rendering rows, the router).
12
16
 
13
17
  import { readFile } from "node:fs/promises";
14
18
  import { dirname } from "node:path";
@@ -19,6 +23,7 @@ import { ask } from "../domain/ask.mjs";
19
23
  import { createGraphService } from "../adapters/providers/graph-service.mjs";
20
24
  import { loadGraph } from "./graph-load.mjs";
21
25
  import { HANDLERS } from "./handlers/index.mjs";
26
+ import { isToolResult } from "./handlers/kit.mjs";
22
27
  import { setDefaultNlpAdapter } from "../domain/interpret/nlp-registry.mjs";
23
28
  import { setConstructionBanks } from "../domain/interpret/strategies/constructions.mjs";
24
29
  import { nlpAdapter } from "../adapters/ask-nlp.mjs";
@@ -32,6 +37,7 @@ setConstructionBanks(readConstructionFiles);
32
37
 
33
38
  export { loadGraph } from "./graph-load.mjs";
34
39
  export { buildContextBundle } from "./handlers/tmct-context.mjs";
40
+ export { ASK_ENVELOPE_DELIM } from "./handlers/tmct-ask.mjs";
35
41
 
36
42
  // Tiered tool surface: the hot tools carry full descriptions/schemas in this
37
43
  // catalog; every COLD tool (describe/members/impact/history/…) is still served
@@ -44,7 +50,7 @@ export const TOOLS = HOT_TOOLS.map(({ name, agentDescription, inputSchema }) =>
44
50
  inputSchema,
45
51
  }));
46
52
 
47
- export async function dispatchTool(name, args, { config, source = defaultSource, tel = null, ingest = null, memoryBackend = null } = {}) {
53
+ async function runHandler(name, args, { config, source = defaultSource, tel = null, ingest = null, memoryBackend = null } = {}) {
48
54
  // Reject an unknown tool BEFORE touching the graph — an unknown name never
49
55
  // triggers a load. hasOwn, so an inherited name ("constructor", "toString")
50
56
  // is unknown rather than a callable found on the prototype chain.
@@ -67,3 +73,21 @@ export async function dispatchTool(name, args, { config, source = defaultSource,
67
73
  const svc = createGraphService(graph, { sourceAccess: true, repoRoot, readFile, tel, ask });
68
74
  return handle(args, { graph, svc, config, repoRoot, memoryBackend });
69
75
  }
76
+
77
+ /** The caller-facing string for one tool call. A handler that returns a structured
78
+ * result is flattened to its `text` here, so every existing string caller (the chat
79
+ * surface, the CLI `cli <tool>` route, the HTTP shim) is unaffected by a handler
80
+ * gaining structure. */
81
+ export async function dispatchTool(name, args, ctx = {}) {
82
+ const out = await runHandler(name, args, ctx);
83
+ return isToolResult(out) ? out.text : out;
84
+ }
85
+
86
+ /** The same call, answered as `{ content, data }`: `content` is the prose, `data` the
87
+ * render-ready structure the handler already computed on its way to it. `data` is
88
+ * undefined for a tool whose answer is prose and nothing else, which is most of the
89
+ * cold set — an absent `data` is a real answer, not a failure. */
90
+ export async function dispatchToolStructured(name, args, ctx = {}) {
91
+ const out = await runHandler(name, args, ctx);
92
+ return isToolResult(out) ? { content: out.content, data: out.data } : { content: out, data: undefined };
93
+ }