Files
lore-engine-poc/docs/MULTI_WORLD_DEMO.md
kanban-dev 99535a8f3a docs(v2): T8 — update README + CHANGELOG + 3 worked-example docs
- 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)
2026-06-17 00:45:30 +00:00

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 — the arda_greyscale seed
  • seed.py:_seed_images_for_world — the per-world image manifest loader
  • plugins/lineage.py, plugins/world.py, plugins/images.py — every world-scoped read tool filters on world_id
  • tests/test_multi_world.py — 14 pytest cases for the namespace
  • test.sh section 12 — the list_worlds() smoke check