@polycode-projects/the-mechanical-code-talker 3.3.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +176 -8
  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 +52 -0
  6. package/corpus/worlds/index.json.gz +0 -0
  7. package/corpus/worlds/manifest.json +53 -13
  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-garden.jsonl.gz +0 -0
  11. package/corpus/worlds/shards/mud-hollow.jsonl.gz +0 -0
  12. package/corpus/worlds/shards/mud-warren.jsonl.gz +0 -0
  13. package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
  14. package/corpus/worlds/src/greyvale-museum.jsonl +136 -0
  15. package/corpus/worlds/src/lantern-cottage.jsonl +60 -0
  16. package/corpus/worlds/src/mud-garden.jsonl +34 -1
  17. package/corpus/worlds/src/mud-hollow.jsonl +82 -0
  18. package/corpus/worlds/src/mud-warren.jsonl +124 -0
  19. package/corpus/worlds/src/spider-fly.jsonl +1 -1
  20. package/data/sprites/animal-icon.toml +11 -5
  21. package/data/sprites/book-icon.toml +9 -5
  22. package/data/sprites/cabinet-icon.toml +10 -5
  23. package/data/sprites/cellar-icon.toml +16 -6
  24. package/data/sprites/container-icon.toml +7 -3
  25. package/data/sprites/desk-icon.toml +8 -5
  26. package/data/sprites/dog-icon.toml +8 -5
  27. package/data/sprites/dog-with-colour-icon.toml +13 -12
  28. package/data/sprites/drawing-room-icon.toml +14 -5
  29. package/data/sprites/egg-icon.toml +6 -3
  30. package/data/sprites/fly-icon.toml +10 -5
  31. package/data/sprites/furniture-icon.toml +5 -3
  32. package/data/sprites/garden-icon.toml +10 -3
  33. package/data/sprites/key-icon.toml +3 -1
  34. package/data/sprites/kitchen-icon.toml +15 -6
  35. package/data/sprites/lamp-icon.toml +9 -4
  36. package/data/sprites/letter-icon.toml +7 -4
  37. package/data/sprites/library-icon.toml +14 -3
  38. package/data/sprites/pan-icon.toml +7 -3
  39. package/data/sprites/person-icon.toml +6 -2
  40. package/data/sprites/poodle-icon.toml +1 -1
  41. package/data/sprites/portable-icon.toml +6 -4
  42. package/data/sprites/portrait-icon.toml +7 -4
  43. package/data/sprites/room-icon.toml +11 -2
  44. package/data/sprites/spider-icon.toml +9 -3
  45. package/data/sprites/study-icon.toml +10 -2
  46. package/package.json +5 -4
  47. package/src/adapters/p2p/webrtc-transport.mjs +146 -0
  48. package/src/domain/game-config.mjs +26 -5
  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 +31 -0
  52. package/src/domain/memory/trust.mjs +6 -4
  53. package/src/domain/p2p/facts.mjs +81 -0
  54. package/src/domain/p2p/peer-id.mjs +32 -0
  55. package/src/domain/p2p/provenance-relabel.mjs +26 -0
  56. package/src/domain/p2p/sync-filter.mjs +31 -0
  57. package/src/domain/p2p/wire.mjs +123 -0
  58. package/src/domain/reference-pack.mjs +31 -7
  59. package/src/domain/spider-fly-world.mjs +1 -1
  60. package/src/domain/sprite-map.mjs +10 -2
  61. package/src/domain/sprite-templates.mjs +8 -6
  62. package/src/services/adventure-editor.mjs +10 -2
  63. package/src/services/adventure-viz.mjs +309 -73
  64. package/src/services/adventure.mjs +1034 -330
  65. package/src/services/chat-page-viz.mjs +1097 -16
  66. package/src/services/chat.mjs +99 -18
  67. package/src/services/code-explorer-viz.mjs +52 -14
  68. package/src/services/extract-facts.mjs +4 -7
  69. package/src/services/ingest-viz.mjs +54 -26
  70. package/src/services/ledger-viz.mjs +105 -56
  71. package/src/services/memory-panel-viz.mjs +24 -0
  72. package/src/services/mud-editor.mjs +313 -0
  73. package/src/services/mud-turn.mjs +264 -87
  74. package/src/services/mud-viz.mjs +2606 -384
  75. package/src/services/p2p-room.mjs +559 -0
  76. package/src/services/plan-viz.mjs +119 -61
  77. package/src/services/research-viz.mjs +151 -57
  78. package/src/services/spider-fly-turn.mjs +1 -1
  79. package/src/services/spider-fly-viz.mjs +67 -43
  80. package/src/services/sprite-catalog-viz.mjs +175 -51
  81. package/src/services/viz-theme.mjs +15 -0
  82. package/src/surfaces/web/memory-ask-browser.bundle.js +109 -109
  83. package/src/surfaces/web/mud-browser-entry.mjs +226 -18
  84. package/src/surfaces/web/p2p-browser-entry.mjs +39 -0
@@ -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": "3.3.0",
3
+ "version": "4.0.1",
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",
@@ -0,0 +1,146 @@
1
+ // One WebRTC DataChannel between two browsers, and nothing else. The caller
2
+ // carries the offer and answer SDP strings between the two machines however it
3
+ // likes — a link, a paste, an already-open channel to a third peer — and this
4
+ // module never learns what travels over the channel once it is open: it takes
5
+ // plain JS values in and hands plain JS values out.
6
+ //
7
+ // `connectionState` is a live GETTER PROPERTY, not a method: read
8
+ // `transport.connectionState`, never `transport.connectionState()`. It reports
9
+ // RTCPeerConnection's own state ("new" | "connecting" | "connected" | "failed"
10
+ // | "closed"), so a caller can poll it without registering a handler.
11
+ //
12
+ // `iceServers` defaults to [] and this design keeps it empty: no STUN, no
13
+ // TURN, no third party in the loop at all. Peers that cannot already reach
14
+ // each other directly (same machine, same LAN, or a NAT that allows it) never
15
+ // finish the handshake, which is the stated boundary of staying serverless.
16
+ //
17
+ // Both `createOffer` and `createAnswerFor` resolve only once ICE gathering has
18
+ // completed, so the SDP string they return already carries every candidate.
19
+ // Nothing here trickles, because a pasted blob is a one-shot message rather
20
+ // than a live connection back to the other side.
21
+ //
22
+ // Late handler registration still fires: `onOpen` on an already-open channel
23
+ // and `onClose` on an already-closed one call back immediately, so a caller
24
+ // that registers after the transition never silently misses it. Each fires at
25
+ // most once.
26
+
27
+ const CHANNEL_LABEL = "tmct";
28
+
29
+ export function createTransport({ iceServers = [] } = {}) {
30
+ const PeerConnection = globalThis.RTCPeerConnection;
31
+ if (typeof PeerConnection !== "function") {
32
+ throw new Error("no RTCPeerConnection here: this transport runs in a browser, not in bare node");
33
+ }
34
+
35
+ const connection = new PeerConnection({ iceServers });
36
+ const messageHandlers = [];
37
+ const openHandlers = [];
38
+ const closeHandlers = [];
39
+
40
+ let channel = null;
41
+ let openAnnounced = false;
42
+ let closeAnnounced = false;
43
+ let tornDown = false;
44
+
45
+ function announceOpen() {
46
+ if (openAnnounced) return;
47
+ openAnnounced = true;
48
+ for (const handler of openHandlers) handler();
49
+ }
50
+
51
+ function announceClose() {
52
+ if (closeAnnounced) return;
53
+ closeAnnounced = true;
54
+ for (const handler of closeHandlers) handler();
55
+ }
56
+
57
+ function receive(event) {
58
+ let value;
59
+ try {
60
+ value = JSON.parse(event.data);
61
+ } catch {
62
+ // A peer sending something that isn't JSON must not be able to throw
63
+ // inside our receive path, so the frame is dropped rather than raised.
64
+ return;
65
+ }
66
+ for (const handler of messageHandlers) handler(value);
67
+ }
68
+
69
+ function attachChannel(dataChannel) {
70
+ channel = dataChannel;
71
+ dataChannel.addEventListener("open", announceOpen);
72
+ dataChannel.addEventListener("close", announceClose);
73
+ dataChannel.addEventListener("message", receive);
74
+ if (dataChannel.readyState === "open") announceOpen();
75
+ }
76
+
77
+ connection.addEventListener("datachannel", (event) => attachChannel(event.channel));
78
+ connection.addEventListener("connectionstatechange", () => {
79
+ const state = connection.connectionState;
80
+ if (state === "failed" || state === "closed") announceClose();
81
+ });
82
+
83
+ async function whenIceGatheringCompletes() {
84
+ if (connection.iceGatheringState === "complete") return;
85
+ await new Promise((resolve) => {
86
+ const settle = () => {
87
+ if (connection.iceGatheringState !== "complete") return;
88
+ connection.removeEventListener("icegatheringstatechange", settle);
89
+ resolve();
90
+ };
91
+ connection.addEventListener("icegatheringstatechange", settle);
92
+ });
93
+ }
94
+
95
+ return {
96
+ async createOffer() {
97
+ attachChannel(connection.createDataChannel(CHANNEL_LABEL));
98
+ await connection.setLocalDescription(await connection.createOffer());
99
+ await whenIceGatheringCompletes();
100
+ return connection.localDescription.sdp;
101
+ },
102
+
103
+ async createAnswerFor(offerSdp) {
104
+ await connection.setRemoteDescription({ type: "offer", sdp: offerSdp });
105
+ await connection.setLocalDescription(await connection.createAnswer());
106
+ await whenIceGatheringCompletes();
107
+ return connection.localDescription.sdp;
108
+ },
109
+
110
+ async completeWithAnswer(answerSdp) {
111
+ await connection.setRemoteDescription({ type: "answer", sdp: answerSdp });
112
+ },
113
+
114
+ send(data) {
115
+ const state = channel ? channel.readyState : "missing";
116
+ if (state !== "open") throw new Error(`cannot send over a data channel that is ${state}`);
117
+ channel.send(JSON.stringify(data));
118
+ },
119
+
120
+ onMessage(handler) {
121
+ messageHandlers.push(handler);
122
+ },
123
+
124
+ onOpen(handler) {
125
+ openHandlers.push(handler);
126
+ if (openAnnounced) handler();
127
+ },
128
+
129
+ onClose(handler) {
130
+ closeHandlers.push(handler);
131
+ if (closeAnnounced) handler();
132
+ },
133
+
134
+ close() {
135
+ if (tornDown) return;
136
+ tornDown = true;
137
+ channel?.close();
138
+ connection.close();
139
+ announceClose();
140
+ },
141
+
142
+ get connectionState() {
143
+ return connection.connectionState;
144
+ },
145
+ };
146
+ }
@@ -27,25 +27,32 @@ export const DEFAULT_GAME_CONFIG = Object.freeze({
27
27
  minHatchlingMass: 3,
28
28
  webDurationTurns: 10,
29
29
  }),
30
+ // The drains are what make a mud character mortal: every scripted turn
31
+ // charges one, and a character whose mass reaches zero starves and takes no
32
+ // more turns. They are sized against the demo page's own default run (400
33
+ // shared turns, so roughly 200 each for two animals) — an animal that never
34
+ // eats dies about two thirds of the way through, and one that forages does
35
+ // not. A drain that emptied a starting mass in twenty turns would end every
36
+ // run before it had shown anything.
30
37
  mud: Object.freeze({
31
38
  moleInitialMass: 8,
32
- moleMassDecrementPerTurn: 0.3,
39
+ moleMassDecrementPerTurn: 0.06,
33
40
  moleSpeed: 1,
34
41
  moleDigReach: 1,
35
42
  voleInitialMass: 6,
36
- voleMassDecrementPerTurn: 0.25,
43
+ voleMassDecrementPerTurn: 0.05,
37
44
  voleSpeed: 1,
38
45
  voleDigReach: 1,
39
46
  badgerInitialMass: 20,
40
- badgerMassDecrementPerTurn: 0.5,
47
+ badgerMassDecrementPerTurn: 0.08,
41
48
  badgerSpeed: 2,
42
49
  badgerDigReach: 2,
43
50
  groundhogInitialMass: 12,
44
- groundhogMassDecrementPerTurn: 0.4,
51
+ groundhogMassDecrementPerTurn: 0.06,
45
52
  groundhogSpeed: 2,
46
53
  groundhogDigReach: 2,
47
54
  meerkatInitialMass: 9,
48
- meerkatMassDecrementPerTurn: 0.35,
55
+ meerkatMassDecrementPerTurn: 0.05,
49
56
  meerkatSpeed: 1,
50
57
  meerkatDigReach: 1,
51
58
  }),
@@ -122,6 +129,20 @@ function mergeSection(defaults, raw, keyMap) {
122
129
  return out;
123
130
  }
124
131
 
132
+ /** The species a mud character id names — "mole-1" -> "mole" — which is how
133
+ * every per-species knob above is keyed. Pure. */
134
+ export function mudSpeciesOf(characterId) {
135
+ return String(characterId).replace(/-\d+$/, "");
136
+ }
137
+
138
+ /** What one turn costs `characterId` in mass, from the resolved `mud` section.
139
+ * Zero for a species the config carries no drain for: a knob nobody set is not
140
+ * a reason to invent a number and starve something with it. Pure. */
141
+ export function mudMassDrainPerTurn(mudConfig, characterId) {
142
+ const drain = mudConfig?.[`${mudSpeciesOf(characterId)}MassDecrementPerTurn`];
143
+ return Number.isFinite(drain) ? drain : 0;
144
+ }
145
+
125
146
  /**
126
147
  * Fold a normalized tmct.toml's `games`/`planning` tables (the raw sparse
127
148
  * pass-through src/adapters/toml-config.mjs's normalizeConfig produces —
@@ -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,
@@ -149,6 +167,19 @@ export function loadLexicon(extra, ns = DEFAULT_NS) {
149
167
  return lex;
150
168
  }
151
169
 
170
+ /** `lexicon` with `names` additionally declared as proper names. The noun,
171
+ * verb and adjective maps are SHARED with the base lexicon rather than
172
+ * re-ingested — a caller that re-declares on every turn (a live game world
173
+ * minting ids as it is played) would otherwise rebuild nine thousand core
174
+ * entries to add half a dozen. Proper names outrank every other category, so
175
+ * a name with no dictionary reading of its own ("groundhog-1", "carrot-2")
176
+ * resolves as itself instead of dying as an undeclared word. */
177
+ export function withProperNames(lexicon, names) {
178
+ const properNames = new Map(lexicon.properNames);
179
+ for (const name of names) properNames.set(String(name).toLowerCase(), String(name));
180
+ return { ...lexicon, properNames };
181
+ }
182
+
152
183
  /** Noun lookup with plural folding; returns the entry ({lemma, property?}) or
153
184
  * null. `opts.singularOnly` (an "a"/"an" determiner) prunes the irregular-
154
185
  * plural fold in favor of a standalone-singular entry when both exist for
@@ -40,11 +40,13 @@ function parseChatTagRest(rest) {
40
40
  * same tier the hand-written tier2 corpus already scores at; the :turnN
41
41
  * segment a snapshot write carries records when, not who, and is not
42
42
  * part of the Source identity)
43
- * mud:<character>[:turnN] -> { kind:"corpus", name:"mud:<character>" }
43
+ * mud:<character>[:turnN][:gone] -> { kind:"corpus", name:"mud:<character>" }
44
44
  * (one character's own testimony in a multi-character world — what it told
45
- * someone, what it examined for itself. Same tier as the world, but one
46
- * Source per character; the `mud:` prefix in the name keeps that Source
47
- * apart from a world of the same literal name)
45
+ * someone, what it examined for itself, and with `:gone` what it saw leave
46
+ * the world. Same tier as the world, but one Source per character; the
47
+ * `mud:` prefix in the name keeps that Source apart from a world of the
48
+ * same literal name. Both trailing segments say when and what, not who, so
49
+ * neither is part of the Source identity)
48
50
  * ace:chat:<session>@<ts> -> { kind:"operator", createdAt:<ts>, sessionId:<session> }
49
51
  * teach:chat:<session>@<ts> -> { kind:"teach", createdAt:<ts>, sessionId:<session> }
50
52
  * web:<url> | url:<url> -> { kind:"web", url:<url> }