Goal
Validate a flow where characters, relationships, and past events are defined as an ontology graph, then queried to generate new stories. The premise of a story is proposed by the graph rather than written by a human.
Why a graph instead of pasting a setting document into the prompt
- Inference. Relations that were never written down are derived by rules.
A loves B + B loves C yields A rival_of C automatically. Structures the author of the setting did not notice become visible.
- Probes. Structural gaps in the graph are scored and ranked, so the machine proposes the story premise. No human instruction of the form "write a story about X".
- Writeback. A generated story emits new nodes and edges that merge back into the graph. The next round of probes finds new gaps and the world accumulates.
Pipeline
world.json --> reason() --> find_seeds() --> ContextPack --> LLM --> story
inference seed ranking subgraph serialization |
^ |
+---------------- apply_writeback() <--------------+
Scope
Ontology schema
Everything is declared in the ontology block of world.json, so the world can be changed without touching code.
Structural inference:
"child_of": { "inverse": "parent_of" },
"sibling_of": { "symmetric": true },
"located_in": { "transitive": true }
Rule inference:
{
"id": "leverage_by_secret",
"when": [["?a", "knows", "?s"],
["?s", "concerns", "?b"],
["?s", "hidden_from", "?b"]],
"where": { "neq": [["?a", "?b"]] },
"then": ["?a", "has_leverage_over", "?b"],
"props": { "tension": 0.9 }
}
Probes
| Probe |
What it looks for |
secret_leverage |
A secret hidden from the person it concerns. A timed bomb |
love_triangle |
Rivalry derived from affection. Shared form (a to c, b to c) and chain form (a to b, b to c) |
unresolved_tension |
High-tension relations with no event node resolving them |
chekhov_object |
Objects introduced but never paid off |
strangers_shared_past |
Two people in the same event with no explicit relation |
severed_bond |
Broken mentor relations with neither reconciliation nor parting recorded |
dangling_consequence |
Events with no outgoing caused edge. A price not yet paid |
contested_goal |
Multiple characters whose want points at the same target |
Results so far
- 27 nodes and 52 asserted edges produce 52 derived edges at the inference fixed point, and 19 ranked story seeds.
- The demo renders 4 seeds, each expanding into 3 branches. Every branch writes a different set of edges back to the graph, so the next round of probes finds different gaps. Example: in the triangle seed, branch A breaks the
jinu serves gwima edge while branch C keeps it and adds rumi drawn_to gwima instead.
- Derived edges carry provenance (
derived_by), so the demo can distinguish asserted relations from inferred ones.
Known limitations
- Inference is a full join, so it slows down as the node count grows. Rules like
allied_by_faction and shared_past produce O(n^2) edges. On a real graph database, query-time expansion is the right call instead of materialization.
- Probe score weights are hand-tuned. There is no feedback loop telling us which seeds actually turn into good stories.
contested_goal string-matches free-text want fields against node names. Modeling goals as edges (a desires predicate) would remove this.
- Writeback payloads are validated for shape only, not for contradiction against existing facts.
Goal
Validate a flow where characters, relationships, and past events are defined as an ontology graph, then queried to generate new stories. The premise of a story is proposed by the graph rather than written by a human.
Why a graph instead of pasting a setting document into the prompt
A loves B+B loves CyieldsA rival_of Cautomatically. Structures the author of the setting did not notice become visible.Pipeline
Scope
world.jsonschema: nodes, edges, ontology declarations, inference rulesinverse/symmetric/transitive; rule-based: SPARQL-style basic graph patterns declared in JSON)Ontology schema
Everything is declared in the
ontologyblock ofworld.json, so the world can be changed without touching code.Structural inference:
Rule inference:
{ "id": "leverage_by_secret", "when": [["?a", "knows", "?s"], ["?s", "concerns", "?b"], ["?s", "hidden_from", "?b"]], "where": { "neq": [["?a", "?b"]] }, "then": ["?a", "has_leverage_over", "?b"], "props": { "tension": 0.9 } }Probes
secret_leveragelove_triangleunresolved_tensionchekhov_objectstrangers_shared_pastsevered_bonddangling_consequencecausededge. A price not yet paidcontested_goalwantpoints at the same targetResults so far
jinu serves gwimaedge while branch C keeps it and addsrumi drawn_to gwimainstead.derived_by), so the demo can distinguish asserted relations from inferred ones.Known limitations
allied_by_factionandshared_pastproduce O(n^2) edges. On a real graph database, query-time expansion is the right call instead of materialization.contested_goalstring-matches free-textwantfields against node names. Modeling goals as edges (adesirespredicate) would remove this.