- 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.3 KiB
Consistency Engine — Worked Example
This is a live end-to-end run of the four consistency tools that landed in v2.T5.
Everything below is real tool output from bash examples/test_consistency.sh
against the current gateway at localhost:8765, taken from the v2 build
(8261c2d on wt/t5-consistency-impl).
What the engine does
The consistency engine has four read-only tools, each backed by pre-materialized
violation nodes in Neo4j. The seed (seed.py:seed_violations) computes the
violations from the same heuristics the tools re-run defensively, so every
violation id is stable, the math is visible in plain Python, and an operator
can re-derive any flagged issue by hand from the seed.
| Tool | Neo4j label | Live count (this run) |
|---|---|---|
find_contradictions |
:Contradiction |
1 |
find_anachronisms |
:Anachronism |
1 |
find_orphans |
:Orphan |
1 |
find_ontology_violations |
:OntologyViolation |
2 |
| Total | 5 |
All four tools support an optional severity argument ("any", "error",
"warn"), and the world-scoped read tools accept world_id="default".
The default world contains the violations; the arda_greyscale world is
clean (its seed doesn't inject any hand-crafted ones).
1. Contradictions — overlapping faction memberships
A :Contradiction is a pair of MEMBER_OF relations on the same person
whose [valid_from, valid_until] windows overlap but whose target factions
differ. It's the classic "sworn to two houses at once" case.
curl -s -X POST http://localhost:8765/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"find_contradictions","arguments":{"world_id":"default"}}
}'
{
"violations": [
{
"id": "c_aldric_double_membership",
"label": "Contradiction",
"severity": "error",
"status": "open",
"details": "Aldric Raventhorne is MEMBER_OF House Vyr (240-) and MEMBER_OF Crimson Pact (260-285); the two memberships overlap.",
"detected_at": "2026-06-16T23:04:51.238226Z"
}
],
"count": 1
}
The math: Aldric's MEMBER_OF House Vyr opens at year 240 with no end date.
His MEMBER_OF Crimson Pact runs 260–285. The two windows overlap from 260
to 285. He can't be a sworn member of both houses at once.
The seed source is seed.py:c_aldric_double_membership — see
Aldric Raventhorne relations block in seed_world_default for the
underlying MEMBER_OF rows.
2. Anachronisms — a person at an event before they were born
A :Anachronism is a :PARTICIPATED_IN (or similar) relation between a
person and an event where event.in_fiction_time is before person.born.
curl -s -X POST http://localhost:8765/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"find_anachronisms","arguments":{"world_id":"default"}}
}'
{
"violations": [
{
"id": "a_vex_at_founding",
"label": "Anachronism",
"severity": "error",
"status": "open",
"details": "Vex the Silent (born 180) is recorded as participating in the Founding of House Vyr (year 85) — 95 years before his birth.",
"detected_at": "2026-06-16T23:04:51.238226Z"
}
],
"count": 1
}
Vex the Silent, born in 180, is tagged as a participant in the
"Founding of House Vyr" event in year 85. The Cypher check joins the
PARTICIPATED_IN edge to the person's born property and the event's
in_fiction_time, extracted as an integer year.
3. Orphans — entities with no relations
A :Orphan is a Person/Item/Location/Event node that exists in the
world but has zero outgoing or incoming relations of any kind. These are
typically world-builder placeholders that haven't been wired into the story
yet.
curl -s -X POST http://localhost:8765/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"find_orphans","arguments":{"world_id":"default"}}
}'
{
"violations": [
{
"id": "o_unfinished_npc",
"label": "Orphan",
"severity": "warn",
"status": "open",
"details": "Person 'Lyssa the Watcher' exists but has no relations — world-builder placeholder, not yet connected.",
"detected_at": "2026-06-16T23:04:51.238226Z"
}
],
"count": 1
}
Lyssa the Watcher is a real Person node in the seed (see
seed.py:Lyssa the Watcher) with no PARENT_OF, MEMBER_OF, SPOUSE_OF,
or any other relation. Note the severity: warn, not error — an
unfinished NPC is a real artifact of worldbuilding, not a story-level
inconsistency.
4. Ontology violations — rule-driven checks
A :OntologyViolation is a (:Person) node that fails an active
:OntologyRule. Rules are themselves Neo4j nodes ((:OntologyRule)) with
a predicate (a short Python expression) and a description. The
consistency plugin runs each rule over the world and materializes a
violation node for every person that fails it.
curl -s -X POST http://localhost:8765/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"find_ontology_violations","arguments":{"world_id":"default"}}
}'
{
"violations": [
{
"id": "ov_maric_no_died",
"label": "OntologyViolation",
"severity": "warn",
"status": "open",
"details": "Person 'Maric Vyr' (born 85) has no death year; rule 'persons_born_before_280_must_die' applies.",
"detected_at": "2026-06-16T23:04:51.238226Z",
"entity_id": "maric",
"rule_id": "persons_born_before_280_must_die"
},
{
"id": "ov_theron_no_died",
"label": "OntologyViolation",
"severity": "warn",
"status": "open",
"details": "Person 'Theron Ashveil' (born 10) has no death year; rule 'persons_born_before_280_must_die' applies.",
"detected_at": "2026-06-16T23:04:51.238226Z",
"entity_id": "theron",
"rule_id": "persons_born_before_280_must_die"
}
],
"count": 2
}
The rule persons_born_before_280_must_die is a world-builder convention:
in the default world's narrative, anyone born before the Age of Iron
(before year 280) must have a recorded death year, because the present
day is well past 280 and a living person from the 1st Age is
unprecedented. Maric (born 85) and Theron (born 10) are intentionally
un-dead in the seed — they are long-lived lineages who are still alive
in the present. The two violations are expected by the world-builder
but flagged so the LLM (or operator) knows the rule is being broken.
How the seed side-stays the violation math
seed.py:seed_violations is the Python source of truth for what the tools
return. Five pre-materialized violation nodes (one Con, one Ana, one
Orph, two OV) get MERGE'd into the default world, and the tool Cypher
queries read them back. If a tool query and the seed drift apart, the
detection surface in seed.py is the one to trust; the queries are a
defensive layer so a missing seed row doesn't silently hide a violation.
Files
plugins/consistency.py— the four toolsseed.py:seed_violations— the 5 hand-crafted violationstests/test_consistency.py— 10 pytest casesexamples/test_consistency.sh— the live E2E runner that produced every block of output above