# Worldbuilding CMS — Agent Skill Guide This document tells AI agents how to effectively use the worldbuilding model layer. Read this before interacting with a world. ## Quick Start ```ts import { WorldEngine } from "./src/index.js"; // Open a world using default built-in types const engine = new WorldEngine({ worldDir: "world" }); await engine.open(); // Create entities — only name and body are required await engine.create("character", { name: "Kira Solane", body: "A shipwright who discovered she can hear the ocean's memories.", tags: ["protagonist", "gifted"], species: "human", status: "alive", }); // Query, validate, close const chars = await engine.list("character"); const result = await engine.validate(); engine.close(); ``` ## Architecture You Need to Know ``` Agent (you) ↓ reads/writes via WorldEngine ├── DuckDBLayer — SQL queries over flat JSON files (read_json_auto) ├── FileStore — one JSON file per entity in world// ├── Validator — schema + referential integrity checks └── VectorIndex — semantic search (cosine similarity) ``` **Key principle:** Entities are flat JSON files in git. Every entity file is the single source of truth. DuckDB queries them in-place. The search index is disposable. ## Entity Model Every entity has these base fields: | Field | Type | Notes | |---|---|---| | `id` | string | Auto-generated from name via slugify, or pass custom | | `type` | string | Entity type name (e.g. "character", "planet") | | `name` | string | **Required.** Display name | | `body` | string | **Required.** Freeform prose lore — the main content | | `tags` | string[] | Defaults to `[]` | | `relations` | Relation[] | Typed references to other entities. Defaults to `[]` | | `metadata` | object | Arbitrary key-value data. Defaults to `{}` | | `created_at` | ISO 8601 | Auto-set | | `updated_at` | ISO 8601 | Auto-set on create and update | Plus type-specific fields (e.g. `species`, `faction_id` for characters). ### Built-in Types | Type | Directory | Key Fields | |---|---|---| | `character` | characters/ | title, species, status, faction_id, location_id | | `location` | locations/ | region, parent_location_id, climate, population | | `faction` | factions/ | motto, leader_id, headquarters_id, alignment | | `event` | events/ | date_in_world, location_id, participants, outcome | | `item` | items/ | item_type, owner_id, location_id, rarity, properties | | `lore` | lore/ | category, era, scope | ### Custom Types Define in `world.config.json`: ```json { "name": "My Universe", "types": { "planet": { "plural": "planets", "fields": { "system": { "type": "string" }, "classification": { "type": "string", "enum": ["terrestrial", "gas_giant"] }, "population": { "type": "integer", "minimum": 0 }, "parent_star_id": { "type": "string", "reference": true } } } } } ``` Custom types get the same base fields, validation, search indexing, and DuckDB querying as built-ins. Set `"builtInTypes": false` to use only your custom types. Load from config: ```ts const engine = await WorldEngine.fromConfig("./world.config.json", { worldDir: "./world", }); ``` ## CRUD Operations ### Create ```ts // Minimal — just name and body await engine.create("character", { name: "Elara Voss", body: "A wandering mage.", }); // Full — all optional fields await engine.create("character", { name: "Elara Voss", body: "A wandering mage from the Northern Reaches.", id: "custom-id", // optional, auto-slugified from name otherwise tags: ["mage", "wanderer"], relations: [{ target_id: "some-faction", target_type: "faction", relation: "member_of" }], metadata: { alignment: "chaotic good" }, species: "human", status: "alive", faction_id: "some-faction", }); ``` ### Read ```ts const entity = await engine.get("character", "elara-voss"); const allChars = await engine.list("character"); ``` ### Update ```ts await engine.update("character", "elara-voss", { status: "dead", body: "Updated lore text...", }); ``` ### Delete ```ts await engine.delete("character", "elara-voss"); ``` ## Querying ### SQL via DuckDB ```ts // WHERE clause on a type const mages = await engine.queryWhere("character", "faction_id = 'mages-guild'"); // Raw SQL — full DuckDB power const result = await engine.sql(` SELECT c.name, f.name as faction_name FROM read_json_auto('world/characters/*.json', union_by_name=true) c JOIN read_json_auto('world/factions/*.json', union_by_name=true) f ON c.faction_id = f.id `); ``` ### Semantic Search ```ts // Natural language query const results = await engine.semanticSearch("political tensions in the north"); // Filter by type const results = await engine.semanticSearch("magic system", { type: "lore", topK: 5 }); ``` **Important:** Call `await engine.reindex()` after bulk operations to rebuild the search index. Individual creates/updates automatically update it incrementally. ## Validation ```ts const result = await engine.validate(); if (!result.valid) { for (const err of result.errors) { console.log(`[${err.entityType}/${err.entityId}] ${err.message}`); } } ``` Validation checks: 1. **Schema conformance** — every entity matches its type's JSON Schema 2. **Referential integrity** — all `*_id` foreign keys and relation targets point to existing entities Validation runs automatically on every `create()` and `update()` call (schema only). Run `validate()` for a full integrity check across the entire world. ## Relations Relations are explicit typed edges between entities: ```ts await engine.create("character", { name: "Commander Vex", body: "...", faction_id: "iron-compact", // foreign key (validated) relations: [ { target_id: "iron-compact", target_type: "faction", relation: "leads" }, { target_id: "elara-voss", target_type: "character", relation: "rival_of" }, ], }); ``` **Foreign keys** (`faction_id`, `location_id`, etc.) are type-specific fields marked with `reference: true` in the schema. They are validated for existence. **Relations** are a generic edge list on every entity. The `relation` field is freeform — use whatever relationship names make sense for your world. ## File Layout ``` my-world/ world.config.json ← optional: defines custom types world/ characters/ elara-voss.json locations/ ashenvale.json factions/ iron-compact.json planets/ ← custom type directory kepler-442b.json .worldindex/ ← gitignored, regenerable vectors.json ``` ## Working With Git - Each entity is one file → minimal merge conflicts - Branch = alternative world state (e.g. `timeline/war-of-ash`) - Meaningful diffs: changing a faction leader is a one-line change - Run `npm run validate` as a pre-commit hook - Run `npm run reindex` after switching branches ## Common Patterns ### Seeding a World See `examples/verdance/seed.ts` (built-in types) and `examples/the-lattice/seed.ts` (custom types) for complete examples. ### Cross-Entity Queries ```ts // Characters in a location's region const northern = await engine.queryWhere("character", `location_id IN ( SELECT id FROM read_json_auto('world/locations/*.json') WHERE region = 'Northern Reaches' )` ); ``` ### Bulk Operations ```ts // Create many entities, then reindex once for (const char of characters) { await engine.create("character", char); } await engine.reindex(); // rebuild search index once at the end ``` ## What NOT to Do - **Don't edit JSON files directly** — use the engine so validation runs - **Don't commit `.worldindex/`** — it's a derived artifact, regenerate with `npm run reindex` - **Don't rely on the search index for correctness** — it's for discovery; use DuckDB/SQL for precise queries - **Don't create circular foreign keys in a single batch** — create entities first, then update with references (see the Verdance seed script's faction leader pattern)