JQ as canon keeper — design doc
Status: SHIPPED — Phases 1–3 (as of 2026-07-01; PRs incl. #957–#960). The implementation lives in
canon-mode.ts(read context +canon_healthaudit +CANON_WRITE_TOOLS→ versionedapplyDelta
- cross-story
canon_consistency+ universe audit), wired intochat-service.ts(scope detection → context injection → tool registration →executeCanonTooldispatch, with agpt-4ooverride for canon mode), scoped via migration417_chat_canon_scope.sql, and opened from the season/universe pages via thejq:open-canonevent handled inChatbot.tsx. The rest of this file is the original design (kept for rationale). Known remaining gaps: settings/themes/voice-rules aren't editable via tools yet; editing is season-scope only (universe-level edits are describe-only). (Closed 2026-07-02:canon_healthnow also runs the prose-vs-bible checks — unmentioned characters, first-appearance drift, orphan relationship refs — via the read-layer corescharacter-mention-index.ts+character-relationship-graph.ts;proseCanonFindings+mergeCanonHealthin canon-mode.ts, golden-tested incanon-health.test.ts.)Original design follows. Make JQ (the AI companion) a conversational organizer over story/universe canon — universes, characters, settings, motifs, planted threads/storylines — so a writer manages their world by talking to JQ instead of hand-editing JSON in admin modals. Background: JQ chat =
chat-service.ts; canon =story-bible.ts+universe-canon.ts.
Thesis: JQ is already ~80% of the way there
This is more tools on an existing agent, not new infrastructure:
- JQ is already a function-calling agent.
chat-service.tsruns a real OpenAI tool loop (call →executeToolCall→ appendrole:'tool'result → re-submit), with per-user conversations (chat_conversations/chat_messages, migration 035) and a role-gated tool surface. - JQ already writes canon. It exposes
spawn_universe_batch+spawn_show_in_universe(super-admin), which already createstory_universesandstory_seasons. So JQ already touches the world it would manage. - The canon layer is unusually agent-ready — every mutation is a clean
service function, and the bible already has a safe-edit protocol:
applyDelta()is additive + update-only, versioned, and audit-trailed (snapshots the prior version with a commit message + author intoseason_story_bible_versions). That is exactly what you want an LLM touching canon to use — conversational edits become reviewable, reversible commits, never silent overwrites. There's even a pre-existing propose → review → apply queue (episode_take_bible_deltas) JQ can reuse.
What JQ would manage (the data model already speaks this language)
| Concept | Backing | Level |
|---|---|---|
| Universe | story_universes (+ code registry constants/story-universes.ts) | universe |
| Characters | BibleCharacter (arc · voice_brief · inner_life · relationships · status · verbal_tics) | per-season bible and universe_characters library |
| Settings / locations | BibleSetting (sensory_palette · rules) | per-season bible and universe_places |
| Storylines / threads | planted_threads (status planted→developing→paid_off→dropped, payoff target) + motifs + character arc | per-season and universe canon |
| Themes / voice / tone | themes, voice_rules, tone_rules, do_not_use | both |
"Storylines" aren't a separate table — they're planted_threads (the load-bearing one) plus motifs and character arcs. A universe-wide arc lives in story_universes.canon.planted_threads; a per-novel one in the bible.
Architecture: a JQ "canon mode"
A conversation scoped to a universe or a season, with that canon loaded into context, and canon tools enabled.
- Scope — add nullable
universe_key/season_idtochat_conversations(migration 417). When set, JQ is "in canon mode" for that world. - Context — inject the scoped canon into the system prompt (reuse
buildBiblePromptBlock/ the universe canon serializer) so JQ reasons over the real bible, not a guess. Token-budgeted like the chapter prompt. - Tools — register canon tools (below) gated on ownership of the scoped universe/season, not just super-admin.
- Safe writes — JQ proposes a
BibleDelta, previews it in chat, and on confirm callsapplyDelta()→ a new versioned bible with a JQ-authored commit message. Reuses the existing delta-review UX; nothing is ever silently overwritten.
Candidate tools (thin wrappers over existing services)
Read (Phase 1):
describe_canon(scope)→ current bible / universe canon summarylist_characters/list_settings/list_threads(status?)→ fromgetCurrentBible/listUniverseCharacters/listUniversePlacescanon_health(scope)→ orphaned threads, arc-less characters, prose-vs-bible drift
Write (Phase 2), all via the versioned/audited path:
add_character/update_character(arc, status, voice, relationships) →applyDeltaadd_thread/update_thread(status, payoff, summary) →applyDeltaadd_setting/add_motif/update_theme→applyDeltaorcommitBibleupsert_universe_character/upsert_universe_place→universe-canon.tsapprove_delta/reject_delta→ the existing chapter-delta queue
The three real decisions (recommendations)
- Model. Tools are currently OpenAI-only, pinned
gpt-4o-miniin the LLM shim (Anthropic tool-calling isn't implemented; fallback is disabled when tools are set). gpt-4o-mini is fine for "add a task," likely too weak for nuanced canon reasoning. Rec: usegpt-4ofor canon mode now; teach the shim Anthropic tool-calling later so JQ can use Sonnet (stronger at this). - Permissions. JQ's tools are super-admin-gated and act "for the calling
user." Canon mode needs a per-resource ownership check — the caller may
only edit canon for a universe/season they own or admin. Small addition at
tool-dispatch time in
executeToolCall. - Propose vs. auto-apply. Default to propose + confirm (a previewable
delta), reusing the delta-review flow — never let JQ silently rewrite canon.
As trust grows, allow confident low-risk edits (e.g.
update_thread.status) to auto-commit.
The "organizer" payoff (Phase 3)
The actual ask — JQ keeping canon managed, not just editing it on command:
- Canon health: flag planted threads with no payoff, characters with no arc, settings that appear in prose but aren't in the bible, statuses that contradict the latest chapter.
- Consistency guard: warn when an edit would contradict the universe's mandatory canon, or when two seasons in a universe diverge on a shared character/place.
- Proactive nudges (via the existing JQ routines): "3 threads in <story> are still 'planted' past their payoff chapter — want to resolve or drop them?"
Phasing
- Read-only canon mode — scope a chat to a universe/season, load canon,
add the
list_*/describe_canon/canon_healthread tools. Cheapest, zero write-risk, immediately useful as a canon reference you can interrogate. (Schema: conversation scope columns.) - Conversational edits — the write tools via
applyDelta/upsert, confirm-before-apply, each a versioned commit. (Ownership checks + a stronger model for canon mode.) - Organizer intelligence — canon-health surfacing + cross-story/universe consistency + proactive routine nudges.
Schema / code touch points
- Migration 417:
chat_conversations.universe_key TEXT,season_id UUID(nullable scope). chat-service.ts: a canon-mode branch ofgetSystemPrompt(inject scoped canon), new tool defs +executeToolCallhandlers, ownership guard, model override to gpt-4o for canon mode.- Reuse wholesale:
story-bible.ts(getCurrentBible,applyDelta,commitBible),universe-canon.ts(upsert*/list*),story-bible-deltas.ts(approve/reject),buildBiblePromptBlock. - Frontend: a way to open JQ "about this universe/season" from the season /
universe editor (sets the conversation scope), + render proposed-delta
preview cards inline in the chat (
Chatbot.tsx).
Open questions / risks
- LLM editing canon is high-trust. The versioned/propose-confirm design is
the mitigation; never expose a destructive
commitBiblethat bypasses review to the model without confirmation. - Universe canon is partly code-defined (
constants/story-universes.tsfor Turing Logs / Deep Cut) and partly DB (story_universes+ the canon-library tables). JQ should edit the DB-backed surfaces; code-registry universes are read-only until that registry fully migrates to the DB. - Token budget: a full bible + universe canon can be large; reuse the
existing trim-priority budgeting from
buildBiblePromptBlock. - Scope drift: JQ should confirm universe-level vs season-level before a write ("change this for the whole universe, or just this story?").
Why it's worth it
JQ becomes the connective tissue between the writer and the whole story→script→movie stack — and a well-tended bible directly improves everything downstream that reads it (the Reverse Screenplay Engine, Scene Studio, character-voice casting, audio). It also advances the Odessa north star: the writer shapes their world by conversation, not by editing JSON.