@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
@@ -1,11 +1,14 @@
1
1
  # letter — Ashcombe Hall's own objective (hidden in the cabinet): an
2
- # envelope with a folded flap (a lighter overlay, the same carved-highlight
3
- # trick as the lid on container.toml) and a small wax seal.
2
+ # envelope with a folded flap (a darker overlay, the same carved-seam trick
3
+ # as the lid on container.toml) and a wax seal. The seal is a fixed red —
4
+ # a wax seal's colour comes from the wax pressed into it, not the paper
5
+ # underneath, the same reasoning as the flame on lamp.toml.
4
6
  classes = ["letter"]
5
7
  svg = """
6
8
  <svg viewBox="0 0 24 24" aria-hidden="true">
7
9
  <rect x="3" y="6" width="18" height="13" rx="1.2" fill="currentColor"/>
8
- <path d="M3 6 L21 6 L21 6.8 L12 13.8 L3 6.8 Z" fill="currentColor" opacity="0.32"/>
9
- <circle cx="12" cy="12.4" r="1.15" fill="currentColor" opacity="0.8"/>
10
+ <path d="M3 6 L21 6 L21 6.8 L12 13.8 L3 6.8 Z" fill="color-mix(in srgb, currentColor 78%, black)"/>
11
+ <path d="M4 7.6 L11 12.9" stroke="color-mix(in srgb, currentColor 25%, white)" stroke-width="0.5" opacity="0.6"/>
12
+ <circle cx="12" cy="12.4" r="1.15" fill="#8a2e2e"/>
10
13
  </svg>
11
14
  """
@@ -1,12 +1,23 @@
1
1
  # library — a room floor plan marked by an open book, so the room-kind icon
2
2
  # reads as a library rather than a plain room: the washed square is room.toml's
3
3
  # own shape, the two-page V is the same open-book cue the large tier uses.
4
+ # One page is shaded a touch darker than the other so the spine reads as a
5
+ # fold rather than a seam drawn on a flat card. The wash is a fixed
6
+ # white/black tint rather than currentColor-mixed (see room-icon.toml's own
7
+ # header for why).
4
8
  classes = ["library"]
5
9
  svg = """
6
10
  <svg viewBox="0 0 24 24" aria-hidden="true">
7
- <rect x="3" y="3" width="18" height="18" rx="1" fill="currentColor" opacity="0.14"/>
11
+ <defs>
12
+ <linearGradient id="library-wash" x1="0" y1="0" x2="0.4" y2="1">
13
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0.1"/>
14
+ <stop offset="100%" stop-color="#000000" stop-opacity="0.14"/>
15
+ </linearGradient>
16
+ </defs>
17
+ <rect x="3" y="3" width="18" height="18" rx="1" fill="url(#library-wash)"/>
8
18
  <rect x="3" y="3" width="18" height="18" rx="1" fill="none" stroke="currentColor" stroke-width="1.6"/>
9
- <path d="M12 9 C10.4 7.9 8.2 7.6 6.4 8.1 V15.6 C8.2 15.1 10.4 15.4 12 16.5 C13.6 15.4 15.8 15.1 17.6 15.6 V8.1 C15.8 7.6 13.6 7.9 12 9 Z" fill="currentColor" opacity="0.75"/>
10
- <path d="M12 9 V16.5" stroke="currentColor" stroke-width="0.5" opacity="0.5"/>
19
+ <path d="M12 9 C10.4 7.9 8.2 7.6 6.4 8.1 V15.6 C8.2 15.1 10.4 15.4 12 16.5 Z" fill="currentColor"/>
20
+ <path d="M12 9 C13.6 7.9 15.8 7.6 17.6 8.1 V15.6 C15.8 15.1 13.6 15.4 12 16.5 Z" fill="color-mix(in srgb, currentColor 78%, black)"/>
21
+ <path d="M12 9 V16.5" stroke="color-mix(in srgb, currentColor 30%, white)" stroke-width="0.4"/>
11
22
  </svg>
12
23
  """
@@ -1,11 +1,15 @@
1
1
  # pan — the kitchen's default portable: a frying pan seen from above with a
2
- # long handle, so a taken pan reads as its own thing rather than the generic
3
- # portable box.
2
+ # long handle. The cooking surface reads darker than the rim (a pan's own
3
+ # interior is seasoned/scorched, its rim stays brighter metal) and the
4
+ # handle carries a rivet, so a taken pan reads as its own thing rather than
5
+ # the generic portable box.
4
6
  classes = ["pan"]
5
7
  svg = """
6
8
  <svg viewBox="0 0 24 24" aria-hidden="true">
7
9
  <circle cx="10" cy="14" r="6" fill="currentColor"/>
8
- <circle cx="10" cy="14" r="3.6" fill="currentColor" opacity="0.35"/>
10
+ <circle cx="10" cy="14" r="4.8" fill="color-mix(in srgb, currentColor 70%, black)"/>
11
+ <path d="M6.6 11 A4.8 4.8 0 0 1 13 10" fill="none" stroke="color-mix(in srgb, currentColor 20%, white)" stroke-width="0.6" stroke-linecap="round" opacity="0.6"/>
9
12
  <rect x="15" y="13" width="7" height="2" rx="1" fill="currentColor"/>
13
+ <circle cx="16.4" cy="14" r="0.5" fill="color-mix(in srgb, currentColor 55%, black)"/>
10
14
  </svg>
11
15
  """
@@ -1,10 +1,14 @@
1
1
  # person — the generic fallback for anyone with no NPC sprite of their own
2
- # (see butler/housekeeper/cook/gardener for the named Ashcombe Hall staff).
2
+ # (see butler/housekeeper/cook/gardener for the named Ashcombe Hall staff):
3
+ # two shoulder notches break the torso silhouette where sleeves would seam,
4
+ # so it reads as a clothed figure rather than a bare blob-with-a-head, while
5
+ # staying deliberately unadorned next to the named staff's own props.
3
6
  classes = ["person"]
4
7
  svg = """
5
8
  <svg viewBox="0 0 24 24" aria-hidden="true">
6
9
  <circle cx="12" cy="6.6" r="3.3" fill="currentColor"/>
7
10
  <path d="M6 21 C6 15.3 8.7 12.8 12 12.8 C15.3 12.8 18 15.3 18 21 Z" fill="currentColor"/>
8
- <path d="M12 12.8 C13.1 12.8 14.1 13 15 13.4" fill="none" stroke="currentColor" stroke-width="0.9" opacity="0.4" stroke-linecap="round"/>
11
+ <path d="M7.6 14.4 L9.4 13" stroke="color-mix(in srgb, currentColor 55%, black)" stroke-width="0.7" opacity="0.6" stroke-linecap="round"/>
12
+ <path d="M16.4 14.4 L14.6 13" stroke="color-mix(in srgb, currentColor 55%, black)" stroke-width="0.7" opacity="0.6" stroke-linecap="round"/>
9
13
  </svg>
10
14
  """
@@ -5,7 +5,7 @@ classes = ["poodle"]
5
5
  svg = """
6
6
  <svg viewBox="0 0 24 24" aria-hidden="true">
7
7
  <ellipse cx="13" cy="15" rx="6.4" ry="4.6" fill="currentColor"/>
8
- <ellipse cx="10.8" cy="13.2" rx="1.8" ry="1.3" fill="currentColor" opacity="0.28"/>
8
+ <ellipse cx="10.8" cy="13.2" rx="1.8" ry="1.3" fill="color-mix(in srgb, currentColor 70%, black)"/>
9
9
  <circle cx="7" cy="8.5" r="4" fill="currentColor"/>
10
10
  <g fill="currentColor">
11
11
  <circle cx="4" cy="6" r="1.6"/>
@@ -1,12 +1,14 @@
1
1
  # portable — the generic fallback for any carryable object with no sprite
2
- # of its own: a drawstring parcel silhouette, replacing the old
3
- # generic-gift-box outline so it reads as "an unspecified small thing"
4
- # rather than a gift.
2
+ # of its own: a drawstring parcel silhouette with a small tag hanging from
3
+ # the neck, so it reads as "an unspecified small thing someone packed"
4
+ # rather than a gift or a sack of grain.
5
5
  classes = ["portable"]
6
6
  svg = """
7
7
  <svg viewBox="0 0 24 24" aria-hidden="true">
8
8
  <path d="M12 6 C7.5 6 5 9.4 5 13.4 C5 17.7 8.1 20 12 20 C15.9 20 19 17.7 19 13.4 C19 9.4 16.5 6 12 6 Z" fill="currentColor"/>
9
9
  <path d="M9.4 6.4 C9.9 4.6 10.8 3.4 12 3.4 C13.2 3.4 14.1 4.6 14.6 6.4" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
10
- <ellipse cx="9.8" cy="11.6" rx="1.6" ry="2.1" fill="currentColor" opacity="0.28"/>
10
+ <ellipse cx="9.8" cy="11.6" rx="1.6" ry="2.1" fill="color-mix(in srgb, currentColor 25%, white)"/>
11
+ <path d="M14.4 6.6 L17.4 8.8 L15.6 10.4 L14 8 Z" fill="color-mix(in srgb, currentColor 65%, black)"/>
12
+ <circle cx="16.1" cy="8.5" r="0.35" fill="color-mix(in srgb, currentColor 20%, white)"/>
11
13
  </svg>
12
14
  """
@@ -1,12 +1,15 @@
1
1
  # portrait — Ashcombe Hall's own drawing-room portrait (a container, hiding
2
2
  # the key): a gilt arched frame around a lighter canvas, with a bust
3
- # silhouette so it reads as a picture of someone, not an empty frame.
3
+ # silhouette so it reads as a picture of someone, not an empty frame. Canvas
4
+ # and figure use fixed cream/sepia tones rather than currentColor opacity —
5
+ # a painting's own palette doesn't belong to the page theme, and a flat
6
+ # opacity wash would shift between a light and a dark backdrop anyway.
4
7
  classes = ["portrait"]
5
8
  svg = """
6
9
  <svg viewBox="0 0 24 24" aria-hidden="true">
7
10
  <path d="M4 21 V7 C4 4 6.8 2 12 2 C17.2 2 20 4 20 7 V21 Z" fill="currentColor"/>
8
- <path d="M6.3 19.4 V8.3 C6.3 6.2 8.4 4.8 12 4.8 C15.6 4.8 17.7 6.2 17.7 8.3 V19.4 Z" fill="currentColor" opacity="0.28"/>
9
- <circle cx="12" cy="10.4" r="2.3" fill="currentColor" opacity="0.7"/>
10
- <path d="M8 18.4 C8 15.2 9.6 13.4 12 13.4 C14.4 13.4 16 15.2 16 18.4 Z" fill="currentColor" opacity="0.7"/>
11
+ <path d="M6.3 19.4 V8.3 C6.3 6.2 8.4 4.8 12 4.8 C15.6 4.8 17.7 6.2 17.7 8.3 V19.4 Z" fill="#f4ecd8"/>
12
+ <circle cx="12" cy="10.4" r="2.3" fill="#8a6a52"/>
13
+ <path d="M8 18.4 C8 15.2 9.6 13.4 12 13.4 C14.4 13.4 16 15.2 16 18.4 Z" fill="#8a6a52"/>
11
14
  </svg>
12
15
  """
@@ -1,10 +1,19 @@
1
1
  # room — a schematic floor plan: a washed interior (a solid tint, not a
2
2
  # hollow box) plus a doorway arch, kept abstract since a room is a space
3
- # to draw, not an object to render literally.
3
+ # to draw, not an object to render literally. The wash is a fixed
4
+ # white/black tint rather than currentColor-mixed: a tint built from
5
+ # currentColor reverses which side looks lighter between a light and a
6
+ # dark theme, a plain white/black one never does.
4
7
  classes = ["room"]
5
8
  svg = """
6
9
  <svg viewBox="0 0 24 24" aria-hidden="true">
7
- <rect x="3" y="3" width="18" height="18" rx="1" fill="currentColor" opacity="0.14"/>
10
+ <defs>
11
+ <linearGradient id="room-wash" x1="0" y1="0" x2="0.4" y2="1">
12
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0.1"/>
13
+ <stop offset="100%" stop-color="#000000" stop-opacity="0.14"/>
14
+ </linearGradient>
15
+ </defs>
16
+ <rect x="3" y="3" width="18" height="18" rx="1" fill="url(#room-wash)"/>
8
17
  <rect x="3" y="3" width="18" height="18" rx="1" fill="none" stroke="currentColor" stroke-width="1.6"/>
9
18
  <path d="M9 21 L9 13 A3 3 0 0 1 15 13 L15 21" fill="none" stroke="currentColor" stroke-width="1.6"/>
10
19
  </svg>
@@ -1,12 +1,18 @@
1
1
  # spider — the spider-and-fly board's own agent. A confident two-lobe body
2
- # (a lighter carved patch on the abdomen for shading) and eight thin
3
- # outward legs.
2
+ # (a darker carved patch on the abdomen for shading), two small fixed-dark
3
+ # eye dots (the same restrained fixed-accent technique the large tier's
4
+ # badger/vole use for an identity marking, not a coat colour) and eight
5
+ # thin outward legs.
4
6
  classes = ["spider"]
5
7
  svg = """
6
8
  <svg viewBox="0 0 24 24" aria-hidden="true">
7
9
  <ellipse cx="12" cy="16" rx="5.2" ry="4.8" fill="currentColor"/>
8
- <ellipse cx="10.3" cy="14.3" rx="1.8" ry="1.4" fill="currentColor" opacity="0.3"/>
10
+ <ellipse cx="10.3" cy="14.3" rx="1.8" ry="1.4" fill="color-mix(in srgb, currentColor 70%, black)"/>
9
11
  <circle cx="12" cy="9.6" r="3.2" fill="currentColor"/>
12
+ <g fill="#1c1c1c">
13
+ <circle cx="10.7" cy="8.7" r="0.5"/>
14
+ <circle cx="13.3" cy="8.7" r="0.5"/>
15
+ </g>
10
16
  <g stroke="currentColor" stroke-width="1.1" fill="none" stroke-linecap="round">
11
17
  <path d="M9.4 8 L2.2 4.4"/>
12
18
  <path d="M9 10.2 L1.4 9.4"/>
@@ -1,11 +1,19 @@
1
1
  # study — a room floor plan marked by a writing desk against one wall, so the
2
2
  # room-kind icon reads as the study you start in rather than a plain room.
3
+ # The wash is a fixed white/black tint rather than currentColor-mixed (see
4
+ # room-icon.toml's own header for why).
3
5
  classes = ["study"]
4
6
  svg = """
5
7
  <svg viewBox="0 0 24 24" aria-hidden="true">
6
- <rect x="3" y="3" width="18" height="18" rx="1" fill="currentColor" opacity="0.14"/>
8
+ <defs>
9
+ <linearGradient id="study-wash" x1="0" y1="0" x2="0.4" y2="1">
10
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0.1"/>
11
+ <stop offset="100%" stop-color="#000000" stop-opacity="0.14"/>
12
+ </linearGradient>
13
+ </defs>
14
+ <rect x="3" y="3" width="18" height="18" rx="1" fill="url(#study-wash)"/>
7
15
  <rect x="3" y="3" width="18" height="18" rx="1" fill="none" stroke="currentColor" stroke-width="1.6"/>
8
- <rect x="7" y="11" width="10" height="2.4" fill="currentColor" opacity="0.7"/>
16
+ <rect x="7" y="11" width="10" height="2.4" fill="currentColor"/>
9
17
  <path d="M8 13.4 V17 M16 13.4 V17" stroke="currentColor" stroke-width="1.2"/>
10
18
  <rect x="9.6" y="7.6" width="4.8" height="3" fill="none" stroke="currentColor" stroke-width="1"/>
11
19
  </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; indexes a repo on request (tmct index) or reads any producer's graph.",
@@ -111,11 +111,12 @@
111
111
  "chat:repo": "node --disable-warning=ExperimentalWarning bin/tmct.mjs chat --repo",
112
112
  "chat:plain": "node --disable-warning=ExperimentalWarning bin/tmct.mjs chat --plain",
113
113
  "chat:narrate": "node --disable-warning=ExperimentalWarning bin/tmct.mjs chat --narrate",
114
- "init": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init",
115
- "init:sqlite": "npm run init",
114
+ "init": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus seon && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus conceptnet && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus aws && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus python && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus java",
115
+ "init:small": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init",
116
+ "init:sqlite": "npm run init:small",
116
117
  "init:persona:human": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --with-persona human",
117
118
  "init:persona:empty": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --with-persona empty",
118
- "init:large": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus seon && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus conceptnet && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus aws && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus python && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus java",
119
+ "init:large": "npm run init",
119
120
  "init:xl": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --persona-size large && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus seon && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus conceptnet && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus aws && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus python && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus java && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus wordnet-xl",
120
121
  "init:xxl": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --persona-size large && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus seon && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus conceptnet && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus aws && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus python && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus java && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus wordnet-full && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus namenet",
121
122
  "memory": "node --disable-warning=ExperimentalWarning bin/tmct.mjs memory",
@@ -172,6 +172,26 @@ export function createInMemoryStore() {
172
172
  return { backend: BACKEND_MEMORY, payload: emptyMemory() };
173
173
  }
174
174
 
175
+ /** Assign `seedPayload` onto `memoryDir`'s own payload — spread over the
176
+ * store's own (possibly empty) payload rather than replacing it outright,
177
+ * so a partial seed (individuals and objectProperties only) still carries
178
+ * the classes/prefixes scaffolding the write path recounts, and a later
179
+ * teach turn works regardless of what the seed carries. A no-op when
180
+ * `seedPayload` is null/undefined — a browser session with nothing to seed
181
+ * keeps its own fresh empty payload untouched. */
182
+ export function applySeedPayload(memoryDir, seedPayload) {
183
+ if (seedPayload) memoryDir.payload = { ...memoryDir.payload, ...seedPayload };
184
+ }
185
+
186
+ /** A structurally independent copy of a memory payload — `structuredClone`
187
+ * where available, falling back to a JSON round-trip for an environment
188
+ * without it. Returns null for a null/undefined `payload`, so a caller can
189
+ * seed a fresh session with "no seed yet" rather than an empty object. */
190
+ export function cloneMemoryPayload(payload) {
191
+ if (!payload) return null;
192
+ try { return structuredClone(payload); } catch { return JSON.parse(JSON.stringify(payload)); }
193
+ }
194
+
175
195
  // ---- Backend C — SQLite: a live node:sqlite connection, per-row
176
196
  // INSERT/REPLACE/DELETE diffed against what's already stored (write cost
177
197
  // proportional to what changed, not total store size). Reads are cached on
@@ -189,6 +189,77 @@ export const RELATIONS = {
189
189
  },
190
190
  };
191
191
 
192
+ // ---- world relations: the predicates a WORLD graph stores about its own
193
+ // individuals, as opposed to RELATIONS above, which is a code vocabulary —
194
+ // a module imports a module, a commit touches a file, and none of its verbs
195
+ // name where a thing is or how it feels. A game board, a burrow and a manor
196
+ // all record those, in the same `mgx:` predicates, so the words for them are
197
+ // curated once here.
198
+ //
199
+ // Each entry is keyed by its stored predicate and carries the LISTING NOUNS a
200
+ // question reaches it by ("the locations of…", "the moods of…") plus `reads`,
201
+ // the third-person phrase one subject's answer is written in. No `verbs` list
202
+ // and no VERB_TO_KIND entry: these answer a listing over a named set, not the
203
+ // "does X <verb> Y" shapes RELATIONS' verbs compile to, and folding them in
204
+ // would put "is in" into the code grammar's own verb table. ----
205
+
206
+ export const WORLD_RELATIONS = Object.freeze({
207
+ placement: Object.freeze({
208
+ predicate: "mgx:currently-in",
209
+ comment: "individual -> place: where the subject is right now.",
210
+ nouns: Object.freeze(["location", "locations", "position", "positions", "place", "places", "whereabouts", "room", "rooms"]),
211
+ reads: "is in",
212
+ }),
213
+ mood: Object.freeze({
214
+ predicate: "mgx:feels",
215
+ comment: "individual -> mood word: how the subject feels right now.",
216
+ nouns: Object.freeze(["mood", "moods", "feeling", "feelings", "emotion", "emotions"]),
217
+ reads: "feels",
218
+ }),
219
+ mass: Object.freeze({
220
+ predicate: "mgx:mass",
221
+ comment: "individual -> number: the subject's current mass.",
222
+ nouns: Object.freeze(["mass", "masses", "weight", "weights"]),
223
+ reads: "has mass",
224
+ }),
225
+ });
226
+
227
+ /** listing noun -> WORLD_RELATIONS key ("locations" -> "placement"). */
228
+ export const WORLD_NOUN_TO_RELATION = Object.freeze(Object.fromEntries(
229
+ Object.entries(WORLD_RELATIONS).flatMap(([token, { nouns }]) => nouns.map((n) => [n, token])),
230
+ ));
231
+
232
+ /** Every stored world predicate, for a caller projecting fact rows into a
233
+ * graph `ask` can traverse. */
234
+ export const WORLD_PREDICATES = Object.freeze(
235
+ Object.values(WORLD_RELATIONS).map((r) => r.predicate),
236
+ );
237
+
238
+ // The regular English 3rd-person-singular suffix rule (the same regular
239
+ // -s/-es/-ies shape src/domain/inflect.mjs's own `pluralOf` applies to a
240
+ // noun) — not imported from there, since inflect.mjs sits downstream of
241
+ // interpret/normalize.mjs, which imports THIS module: importing it back
242
+ // here would be a real circular dependency, not just a style choice.
243
+ function thirdPersonSingular(w) {
244
+ if (/(?:[sxz]|ch|sh)$/i.test(w)) return `${w}es`;
245
+ if (/[^aeiou]y$/i.test(w)) return `${w.slice(0, -1)}ies`;
246
+ return `${w}s`;
247
+ }
248
+
249
+ /** The third-person-singular sentence phrase for a stored relation `kind`
250
+ * ("inherits" -> "inherits from", "cochange" -> "co-changes with"), derived
251
+ * from RELATIONS' own `bare` infinitive rather than a second hand-curated
252
+ * verb table. A symbol-grain kind ("callsSymbol", "touchesSymbol") reads its
253
+ * coarse sibling's phrase; an unrecognized kind falls back to a plain
254
+ * camelCase split. */
255
+ export function phraseForRelation(kind) {
256
+ const key = String(kind || "");
257
+ const entry = RELATIONS[key] || (key.endsWith("Symbol") && RELATIONS[key.slice(0, -6)]) || null;
258
+ if (!entry) return key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
259
+ const [head, ...rest] = entry.bare.split(" ");
260
+ return [thirdPersonSingular(head), ...rest].join(" ");
261
+ }
262
+
192
263
  /** The closed set of reverse `inherits` verb phrasings a strategy checks to
193
264
  * decide whether to swap subject/object. The three "the"-definite forms are
194
265
  * named here but not reachable through VERB_TO_KIND (see above). */
@@ -26,6 +26,7 @@ import {
26
26
  PASSIVE_PARTICIPLE_TO_KIND, GENERIC_AGENT_WORDS, REDUCED_RELATIVE_CLAUSES,
27
27
  AGGREGATE_TRIGGERS, LIST_TRIGGERS, SUPERLATIVE_EXTREMES, EDGE_NOUN_TO_METRIC, METRIC_IMPLIES_ENTITY, ANAPHORA_TRIGGERS,
28
28
  MEMBERSHIP_KINDS, CASCADE_NOISE, CASCADE_SYNONYMS, HELP_TRIGGERS,
29
+ WORLD_RELATIONS, WORLD_NOUN_TO_RELATION, WORLD_PREDICATES,
29
30
  stripTrailingScopeFiller,
30
31
  } from "./ask-vocab.mjs";
31
32
  import { expandContractions, normalizeQuery, applyNegationFrames, applyPhrasingFrames, matchNegationSet, STOPWORDS, splitWords, wordsOf, escapeRegex } from "./interpret/normalize.mjs";
@@ -2135,6 +2136,7 @@ function evalComposite(graph, ast, opts = {}) {
2135
2136
  return { compositeKind: "count", count: evalSet(graph, ast.base, opts).length, entityType: ast.entityType, matches: [] };
2136
2137
  }
2137
2138
  if (ast.node === "list") return { compositeKind: "list", matches: evalSet(graph, ast.base, opts), entityType: ast.entityType, scoped: ast.scoped };
2139
+ if (ast.node === "worldRelation") return evalWorldRelation(graph, ast);
2138
2140
  if (ast.node === "superlative") return evalSuperlative(graph, ast);
2139
2141
  if (ast.node === "temporal") return evalTemporal(graph, ast, opts);
2140
2142
  if (ast.node === "recentCommits") return evalRecentCommits(graph);
@@ -2292,6 +2294,20 @@ function renderComposite(parsed, result, graph) {
2292
2294
  : "";
2293
2295
  return { content: `${compositeList(result.matches)}${hint}.`, miss: false, ambiguous: false, matches: result.matches };
2294
2296
  }
2297
+ // One sentence per subject, in id order, in the world's own words — the
2298
+ // subject's id, the relation's `reads` phrase, and the stored object
2299
+ // verbatim. Nothing is derived: an empty set is the honest miss, never a
2300
+ // sentence about a subject the world has no row for.
2301
+ if (result.compositeKind === "worldRelation") {
2302
+ const asked = listJoin(result.askedClasses);
2303
+ const noun = WORLD_RELATIONS[result.relation].nouns[1];
2304
+ if (!result.pairs.length) {
2305
+ return { content: `no ${noun} on record for ${asked} in this graph.`, miss: true, ambiguous: false, matches: [] };
2306
+ }
2307
+ const reads = WORLD_RELATIONS[result.relation].reads;
2308
+ const sentences = result.pairs.map((p) => `${p.subject.label || p.subject.id} ${reads} ${p.object}`);
2309
+ return { content: `${sentences.join("; ")}.`, miss: false, ambiguous: false, matches: result.matches };
2310
+ }
2295
2311
  // A non-empty inherited result is disclosed out loud ("X has no own <kind>
2296
2312
  // — inherited from <ancestor>: …"), never silently presented as the owner's own.
2297
2313
  if (result.compositeKind === "membership") {
@@ -4403,6 +4419,148 @@ function dynamicClassQuery(graph, query) {
4403
4419
  return listM ? { node: "list", entityType, base, scoped: false } : { node: "count", entityType, base };
4404
4420
  }
4405
4421
 
4422
+ // ---- world-relation listing: "list the locations of flies and spiders", the
4423
+ // plural, multi-class form of a question a world graph can already answer. A
4424
+ // listing over ONE world predicate (ask-vocab.mjs's WORLD_RELATIONS) filtered
4425
+ // to one or more subject CLASSES, resolved dynamically against whatever
4426
+ // classes actually have an individual in this graph — so a world's own
4427
+ // taxonomy needs no entry in the closed code-graph noun table, exactly as the
4428
+ // count/list fallback above resolves its class noun. Fires only once the
4429
+ // normal cascade has already produced an honest miss, and misses honestly
4430
+ // itself when any asked class has no individual here. ----
4431
+
4432
+ // A class list: "flies and spiders", "spiders, flies and eggs". Each part must
4433
+ // be a single bare noun — a phrase with anything else in it is a restrictor
4434
+ // this shape can't honour, so it declines rather than dropping it.
4435
+ function parseWorldClassList(graph, text) {
4436
+ const parts = String(text || "").split(/\s*,\s*|\s+and\s+/i).map((s) => s.trim()).filter(Boolean);
4437
+ if (!parts.length) return null;
4438
+ const asked = [];
4439
+ const classes = [];
4440
+ for (const part of parts) {
4441
+ const word = part.replace(/^(?:all\s+)?(?:the\s+)?/i, "").trim();
4442
+ if (!/^[a-z][a-z'-]*$/i.test(word)) return null;
4443
+ const cls = resolveDynamicClass(graph, word);
4444
+ if (!cls) return null;
4445
+ asked.push(word.toLowerCase());
4446
+ classes.push(cls);
4447
+ }
4448
+ return { asked: [...new Set(asked)], classes: [...new Set(classes)] };
4449
+ }
4450
+
4451
+ const WORLD_LISTING_RE = new RegExp(
4452
+ `^(?:${LIST_TRIGGERS_SORTED.map(escapeRegex).join("|")})\\s+(?:all\\s+)?(?:the\\s+)?([a-z][a-z'-]*)\\s+(?:of|for)\\s+(.+?)[?.!\\s]*$`,
4453
+ "i",
4454
+ );
4455
+ // "where are the flies and spiders" — the same listing said the natural way.
4456
+ // Singular too ("where is the spider"): the class list resolves either way, and
4457
+ // with only the code-graph `where` lane to fall back on the answer would be
4458
+ // "no recorded code location" for a subject whose cell this graph plainly
4459
+ // holds. A phrase that isn't a bare class noun ("where is auth.mjs defined")
4460
+ // fails parseWorldClassList and keeps the definition-site reading.
4461
+ const WORLD_WHERE_RE = /^where(?:'s|\s+is|\s+are)\s+(?:all\s+)?(?:the\s+)?(.+?)[?.!\s]*$/i;
4462
+
4463
+ /** Compile "<list trigger> the <world-relation noun> of <class> and <class>"
4464
+ * (or "where are the <class> and <class>") into a world-relation listing AST,
4465
+ * or null when this isn't that shape. */
4466
+ function worldRelationQuery(graph, query) {
4467
+ const q = String(query || "").trim();
4468
+ const listM = q.match(WORLD_LISTING_RE);
4469
+ const relation = listM ? WORLD_NOUN_TO_RELATION[listM[1].toLowerCase()] : "placement";
4470
+ if (!relation) return null;
4471
+ const subjectText = listM ? listM[2] : q.match(WORLD_WHERE_RE)?.[1];
4472
+ if (!subjectText) return null;
4473
+ const resolved = parseWorldClassList(graph, subjectText);
4474
+ if (!resolved) return null;
4475
+ return {
4476
+ node: "worldRelation",
4477
+ relation,
4478
+ predicate: WORLD_RELATIONS[relation].predicate,
4479
+ classes: resolved.classes,
4480
+ askedClasses: resolved.asked,
4481
+ };
4482
+ }
4483
+
4484
+ const normalizePredicate = (p) => String(p || "").toLowerCase();
4485
+
4486
+ function evalWorldRelation(graph, ast) {
4487
+ const wanted = new Set(ast.classes);
4488
+ const latest = new Map();
4489
+ for (const group of graph?.relations || []) {
4490
+ if (normalizePredicate(group.prop) !== ast.predicate && normalizePredicate(group.predicate) !== ast.predicate) continue;
4491
+ // A world appends a fresh row per turn rather than rewriting one, so the
4492
+ // LAST edge for a subject is its current value — the same last-wins fold
4493
+ // every world's own state reader applies to its rows.
4494
+ for (const edge of group.edges || []) {
4495
+ const subject = graph.byId?.get(edge.subject);
4496
+ if (subject && wanted.has(subject.class)) latest.set(edge.subject, { subject, object: edge.object });
4497
+ }
4498
+ }
4499
+ const pairs = [...latest.values()].sort((a, b) => String(a.subject.id).localeCompare(String(b.subject.id)));
4500
+ return {
4501
+ compositeKind: "worldRelation",
4502
+ relation: ast.relation,
4503
+ askedClasses: ast.askedClasses,
4504
+ pairs,
4505
+ matches: pairs.map((p) => p.subject),
4506
+ };
4507
+ }
4508
+
4509
+ // A world writes each turn's facts against a stamped subject ("spider-1@turn3",
4510
+ // "player@step7") and folds them back onto the base id, newest stamp winning —
4511
+ // spider-fly.mjs's foldSpiderFlyState and domain.mjs's foldWorldState both do
4512
+ // exactly this. An unstamped row is the world's own starting value, stamp 0.
4513
+ const WORLD_SNAPSHOT_RE = /^(.+)@(?:turn|step)(\d+)$/;
4514
+
4515
+ function splitWorldSnapshot(term) {
4516
+ const m = WORLD_SNAPSHOT_RE.exec(String(term ?? ""));
4517
+ return m ? { base: m[1], stamp: Number(m[2]) } : { base: String(term ?? ""), stamp: 0 };
4518
+ }
4519
+
4520
+ /** Project plain world fact rows (`{subject, predicate, object}`, the shape
4521
+ * memory/core.mjs's readFactRows returns) into the `{individuals,
4522
+ * objectProperties}` payload parseEntities consumes, so a page holding a live
4523
+ * world can hand `ask` a graph of it instead of an empty one. Only the
4524
+ * WORLD_PREDICATES rows are carried: a board's own geometry (every cell and
4525
+ * every exit) is thousands of rows no natural-language listing asks for.
4526
+ * Turn-stamped rows are folded onto their base subject, so the graph carries
4527
+ * each subject's CURRENT value and never a stale one from an earlier turn.
4528
+ *
4529
+ * A subject's class comes from its own `rdf:type` row when the world states
4530
+ * one; `classOf(id)` is the fallback for a world that names its individuals
4531
+ * by convention instead (spider-fly's "spider-1" is a spider because of its
4532
+ * id). Returning null from `classOf` drops the subject — that is the seam a
4533
+ * caller filters through, so "which subjects still count as live" stays a
4534
+ * rule of the world module rather than of this projection. Pure. */
4535
+ export function worldRelationGraphPayload(rows, { classOf = () => null } = {}) {
4536
+ const declaredClass = new Map();
4537
+ for (const row of rows || []) {
4538
+ if (row?.predicate === "rdf:type" && row.subject && row.object) {
4539
+ declaredClass.set(splitWorldSnapshot(row.subject).base, String(row.object));
4540
+ }
4541
+ }
4542
+ const wanted = new Set(WORLD_PREDICATES);
4543
+ const individuals = new Map();
4544
+ const newest = new Map();
4545
+ for (const row of rows || []) {
4546
+ if (!row?.subject || !wanted.has(row.predicate)) continue;
4547
+ const { base, stamp } = splitWorldSnapshot(row.subject);
4548
+ const cls = declaredClass.get(base) || classOf(base);
4549
+ if (!cls) continue;
4550
+ const key = `${row.predicate}${base}`;
4551
+ const prior = newest.get(key);
4552
+ if (prior && prior.stamp > stamp) continue;
4553
+ individuals.set(base, { id: base, label: base, class: cls });
4554
+ newest.set(key, { stamp, predicate: row.predicate, subject: base, object: splitWorldSnapshot(row.object).base });
4555
+ }
4556
+ const groups = new Map();
4557
+ for (const edge of newest.values()) {
4558
+ if (!groups.has(edge.predicate)) groups.set(edge.predicate, { prop: edge.predicate, predicate: edge.predicate, examples: [] });
4559
+ groups.get(edge.predicate).examples.push({ subject: edge.subject, object: edge.object });
4560
+ }
4561
+ return { individuals: [...individuals.values()], objectProperties: [...groups.values()] };
4562
+ }
4563
+
4406
4564
  // Matches T5's own bare-object capture (grammar.mjs meta-whatis), reused below to
4407
4565
  // extract the term for the article-insertion fallback rather than duplicating it.
4408
4566
  const BARE_META_WHATIS_RE = /^what\s+(?:is|are)\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
@@ -4447,6 +4605,16 @@ export function ask(graph, query, { contextId = null, nlp = undefined, prev = nu
4447
4605
  if (!dynRendered.miss) { parsed = dyn; result = dynResult; rendered = dynRendered; relaxed = null; }
4448
4606
  }
4449
4607
  }
4608
+ // World-relation listing ("list the locations of flies and spiders"), on the
4609
+ // same terms: only after an honest miss, and only when it answers for real.
4610
+ if (rendered.miss && !rendered.ambiguous) {
4611
+ const world = worldRelationQuery(graph, query);
4612
+ if (world) {
4613
+ const worldResult = traverse(graph, world, { contextId, prev });
4614
+ const worldRendered = render(world, worldResult, graph);
4615
+ if (!worldRendered.miss) { parsed = world; result = worldResult; rendered = worldRendered; relaxed = null; }
4616
+ }
4617
+ }
4450
4618
  // Bare "what is X" (no article) meta fallback, narrow to ask() and never
4451
4619
  // touching the outer `parsed` even on a miss — chat.mjs's own gates key off
4452
4620
  // `!envelope.parsed`, so populating it here would steal turns from lanes
@@ -143,6 +143,17 @@ export function mudMassDrainPerTurn(mudConfig, characterId) {
143
143
  return Number.isFinite(drain) ? drain : 0;
144
144
  }
145
145
 
146
+ /** The mass denominator a spider-and-fly class's HUD bar and sprite
147
+ * expression both scale against — "how full" reads the same on either. `cls`
148
+ * is "spider" or "fly"; any other class (an egg, or a class this world adds
149
+ * later) has no denominator and gets null, never a guessed number. `config`
150
+ * is the page's own `{ maxSpiderMass, maxFlyMass }` pair. Pure. */
151
+ export function massScaleFor(cls, config) {
152
+ if (cls === "spider") return config?.maxSpiderMass ?? null;
153
+ if (cls === "fly") return config?.maxFlyMass ?? null;
154
+ return null;
155
+ }
156
+
146
157
  /**
147
158
  * Fold a normalized tmct.toml's `games`/`planning` tables (the raw sparse
148
159
  * pass-through src/adapters/toml-config.mjs's normalizeConfig produces —