- README.md: 5 plugins / 19 tools (matches /healthz); 'what this proves' now lists consistency engine, multi-world namespace, LLM consumer; 'next steps' section replaced with 'shipped in v2' - docs/CONSISTENCY_DEMO.md: 4 tools, 5 violations, all output verified against live bash examples/test_consistency.sh - docs/MULTI_WORLD_DEMO.md: list_worlds() + entity_context in both worlds + cross-world isolation tests, all output verified live - docs/LLM_CONSUMER_DEMO.md: 5 question types, 9 distinct tools, all output traced to examples/results/*.json - CHANGELOG.md: v1 -> v2 entry, all 9 task refs (T1-T9) - examples/test_e2e.sh: T7 E2E validation script (untracked)
7.1 KiB
Multi-World Namespace — Worked Example
This is a live walkthrough of the world namespace that landed in v2.T6.
Every call below is real tool output against the gateway at localhost:8765
from the v2 build (4f92289 on wt/t6-multi-world).
What the namespace is
The v1 POC stored every node and edge in a single graph. v2 adds a
world_id property on every world-scoped node and edge, and a new
list_worlds() admin tool. The read tools (entity_context,
was_true_at, state_at, ancestors_of, descendants_of,
lineage_of, recall_images, search_images_by_caption,
search_images_semantic, trades_by_buyer, market_price, the
consistency find_* tools) all accept an optional world_id argument
that defaults to "default". Write tools (log_trade, register_image,
embed_images) tag the row with the caller's world_id.
This lets a single Neo4j instance hold multiple parallel worlds with no
node-id collisions. The default seed loads a second world, arda_greyscale,
that mirrors the default world's shape with its own people, factions,
locations, events, and relations.
1. list_worlds() — what's loaded
curl -s -X POST http://localhost:8765/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_worlds","arguments":{}}}'
[
{ "world_id": "arda_greyscale" },
{ "world_id": "default" }
]
Both worlds are alive in the same graph. Note the default ordering is newest-first by seed time.
2. The default world — Theron's bloodline
The default world is the v1 set: Theron Ashveil, Maric Vyr, Cael Vyr, Yssa Raventhorne, Aldric Raventhorne, Elara Raventhorne, plus factions House Vyr / Crimson Pact / Merchants Guild and the founding-event / Black-Spire-event / founding-of-the-Merchants-Guild era.
curl -s -X POST http://localhost:8765/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"entity_context","arguments":{"name":"Theron Ashveil","world_id":"default"}}
}'
{
"found": true,
"name": "Theron Ashveil",
"id": "theron",
"world_id": "default",
"labels": ["Person"],
"properties": {
"world_id": "default",
"tier": "noble",
"culture": "Valdorni",
"born": 10,
"name": "Theron Ashveil",
"id": "theron"
},
"relations": [
{ "rel": "PARENT_OF", "to_id": "maric", "to": "Maric Vyr" },
{ "rel": "MEMBER_OF", "to_id": "house_vyr_bloodline", "to": "House Vyr (bloodline)" }
]
}
Theron Ashveil is the founding ancestor of the House Vyr bloodline.
He exists in the default world and is the earliest known ancestor of
Aldric (see docs/LLM_CONSUMER_DEMO.md Q3 for the full chain).
3. The greyscale world — Mael & Sira Greyscale
arda_greyscale is a parallel world seeded by
seed.py:seed_greyscale_world with its own era (greyscale_age), its
own faction (The Ashen Court), and its own people. The greyscale seed
intentionally uses different node ids — mael_greyscale, sira_greyscale
— so a query in one world cannot accidentally return the other.
curl -s -X POST http://localhost:8765/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"entity_context","arguments":{"name":"Mael Greyscale","world_id":"arda_greyscale"}}
}'
{
"found": true,
"name": "Mael Greyscale",
"id": "mael_greyscale",
"world_id": "arda_greyscale",
"labels": ["Person"],
"properties": {
"world_id": "arda_greyscale",
"tier": "noble",
"culture": "Greyscale",
"born": 220,
"name": "Mael Greyscale",
"id": "mael_greyscale"
},
"relations": [
{ "rel": "MEMBER_OF", "to_id": "ashen_court", "to": "The Ashen Court" },
{ "rel": "SPOUSE_OF", "to_id": "sira_greyscale", "to": "Sira Greyscale" }
]
}
Mael is the greyscale world's analogue of Aldric: a noble, a member of
the Ashen Court, spouse of a Greyscale twin. Note culture: "Greyscale"
and tier: "noble" — same property names, completely different
meanings from the default world.
4. Cross-world isolation — the namespace holds
A query in world X for an entity that exists only in world Y must come back empty. This is the test the namespace was built to pass.
Aldric is default-only — greyscale returns empty
curl -s -X POST http://localhost:8765/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"recall_images","arguments":{"entity_id":"aldric","world_id":"arda_greyscale"}}
}'
{
"entity_id": "aldric",
"world_id": "arda_greyscale",
"count": 0,
"images": []
}
Aldric's images are in the default world's image_manifest table, not
the greyscale one. With world_id="arda_greyscale", the image recall
query finds zero — exactly what the namespace promises.
Trade log — default scope doesn't see greyscale entries (and vice versa)
curl -s -X POST http://localhost:8765/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"market_price","arguments":{"item_id":"pale_ledger","world_id":"default"}}
}'
{
"item_id": "pale_ledger",
"sample_size": 2,
"avg_unit_price": 500.0,
"min_unit_price": 500.0,
"max_unit_price": 500.0,
"most_recent": "2026-06-16T23:04:51.276172+00:00"
}
The same market_price call against arda_greyscale returns zero
trades for pale_ledger (the greyscale world has its own item
namespace, not the default pale_ledger). The trades table's PK
includes world_id so a row inserted by log_trade with
world_id="arda_greyscale" is invisible to a default-scope query.
5. How a tool uses world_id
The MATCH clauses in the world-scoped tools all include
{id: $..., world_id: $world_id} so a row in the wrong world simply
doesn't match. For example, the lineage ancestors query in
plugins/lineage.py:
MATCH path = (a:Person {id: $person, world_id: $world_id})-[:PARENT_OF*1..10]->(ancestor:Person)
WHERE ancestor.world_id = $world_id
RETURN ancestor
Both ends of the path are pinned to the same world_id, so the chain
never crosses a world boundary. The state_at and entity_context
queries follow the same pattern; the image and trade queries hit
Postgres tables that carry world_id in their primary key.
6. The world-resolution rule
Tools that take a world_id argument default it to "default" so v1
callers keep working unchanged. The bash test.sh runner passes
world_id="default" explicitly to verify that the opt-in behaviour
holds. The greyscale seed is loaded by python3 seed.py automatically
(no extra flag), and list_worlds() is the operator's view of what
exists.
Files
seed.py:seed_greyscale_world— thearda_greyscaleseedseed.py:_seed_images_for_world— the per-world image manifest loaderplugins/lineage.py,plugins/world.py,plugins/images.py— every world-scoped read tool filters onworld_idtests/test_multi_world.py— 14 pytest cases for the namespacetest.shsection 12 — thelist_worlds()smoke check