Guide for the Spine Serial domain - web serial fiction authoring. Covers bible entities, structure hierarchy, generation pipeline, review workflow, and serial-specific analysis.
The Serial domain supports authoring web serial fiction with:
packages/serial/
├── types/
│ └── src/
│ ├── index.ts
│ ├── bible.ts # Character, Location, Faction, etc.
│ ├── structure.ts # Book, Arc, Chapter, Scene
│ ├── content.ts # Prose, Analysis scores
│ └── serial.ts # Hook, Cycle, Release types
├── core/
│ └── src/
│ ├── bible/ # Bible entity CRUD
│ ├── structure/ # Structure tree operations
│ ├── generation/ # LLM generation pipeline
│ ├── analysis/ # Tension, pacing, continuity
│ ├── review/ # Version management, locks
│ ├── serial/ # Buffer, hooks, cycles
│ └── handlers/ # Server handler registration
└── mcp/
└── src/
└── tools/ # MCP tool definitions
const CharacterSchema = BaseEntitySchema.extend({
name: z.string().min(1),
role: z.enum(['protagonist', 'antagonist', 'supporting', 'minor']),
aliases: z.array(z.string()).default([]),
description: z.string().default(''),
traits: z.array(z.string()).default([]),
goals: z.array(z.string()).default([]),
backstory: z.string().default(''),
voiceNotes: z.string().default(''), // How they speak
relationships: z.array(RelationshipSchema).default([]),
introducedAt: z.string().optional(), // Structure ID where introduced
status: z.enum(['active', 'inactive', 'deceased']).default('active'),
});
Key fields:
voiceNotes — Critical for generation consistencyintroducedAt — Links to structure for continuityrelationships — Bidirectional tracked separatelyconst LocationSchema = BaseEntitySchema.extend({
name: z.string().min(1),
type: z.enum(['world', 'continent', 'country', 'region', 'city',
'district', 'building', 'room', 'natural', 'virtual', 'other']),
description: z.string().default(''),
sensoryDetails: z.string().default(''), // Sights, sounds, smells
atmosphere: z.string().default(''),
parentId: z.string().uuid().optional(), // Hierarchical locations
significance: z.string().default(''), // Why it matters to story
});
const FactionSchema = BaseEntitySchema.extend({
name: z.string().min(1),
type: z.enum(['government', 'military', 'religious', 'criminal', 'corporate',
'secret-society', 'guild', 'family', 'informal', 'other']),
description: z.string().default(''),
goals: z.array(z.string()).default([]),
values: z.array(z.string()).default([]),
resources: z.array(z.string()).default([]),
relationships: z.array(FactionRelationshipSchema).default([]),
});
const WorldRuleSchema = BaseEntitySchema.extend({
name: z.string().min(1),
category: z.enum(['magic', 'technology', 'social', 'physical', 'economic', 'other']),
description: z.string(),
limitations: z.array(z.string()).default([]),
exceptions: z.array(z.string()).default([]),
implications: z.array(z.string()).default([]),
});
const PlotThreadSchema = BaseEntitySchema.extend({
name: z.string().min(1),
type: z.enum(['main-plot', 'subplot', 'mystery', 'romance',
'conflict', 'character-arc', 'worldbuilding', 'other']),
description: z.string().default(''),
status: z.enum(['planned', 'active', 'dormant', 'resolved', 'abandoned']),
promises: z.array(z.string()).default([]), // Setup/foreshadowing
payoffs: z.array(z.string()).default([]), // Resolutions
involvedCharacters: z.array(z.string()).default([]), // Character IDs
});
const TimelineEventSchema = BaseEntitySchema.extend({
name: z.string().min(1),
date: z.string(), // In-world date (flexible format)
description: z.string().default(''),
type: z.enum(['backstory', 'story', 'future']),
involvedCharacters: z.array(z.string()).default([]),
involvedLocations: z.array(z.string()).default([]),
structureId: z.string().uuid().optional(), // Where it appears in story
});
Project
└── Book
└── Arc
└── Chapter
└── Scene
const StructureTypeSchema = z.enum(['book', 'arc', 'chapter', 'scene']);
const StructureSchema = BaseEntitySchema.extend({
type: StructureTypeSchema,
title: z.string().min(1),
summary: z.string().default(''),
parentId: z.string().uuid().optional(),
order: z.number().int().nonnegative(),
// Chapter-specific
chapterType: z.enum(['action', 'character', 'worldbuilding', 'transition']).optional(),
targetTension: z.number().min(0).max(100).optional(),
targetWordCount: z.number().int().positive().optional(),
// Beats (for chapters)
beats: z.array(BeatSchema).default([]),
// Hook (for chapters)
hook: HookSchema.optional(),
// Notes
notes: z.string().default(''),
});
Beats are the atomic units of a chapter's outline:
const BeatSchema = z.object({
id: z.string().uuid(),
summary: z.string(),
purpose: z.enum(['setup', 'development', 'climax', 'resolution', 'transition']),
tension: z.number().min(0).max(100),
povCharacter: z.string().optional(), // Character ID
order: z.number().int().nonnegative(),
});
Every chapter should end with a hook:
const HookSchema = z.object({
type: z.enum(['revelation', 'decision', 'cliffhanger', 'emotional', 'question']),
description: z.string(),
strength: z.number().min(0).max(100).optional(), // Analysis score
});
Outline → Beats → Draft → Self-Review → Human Review
const GenerationStateSchema = z.object({
structureId: z.string().uuid(),
stage: z.enum(['idle', 'outline', 'beats', 'draft', 'review', 'complete', 'failed']),
startedAt: z.date().optional(),
completedAt: z.date().optional(),
error: z.string().optional(),
attempts: z.number().int().nonnegative().default(0),
});
Generation requires assembling relevant context:
interface GenerationContext {
// Always included
projectSettings: ProjectSettings;
structureContext: StructureContext; // Current chapter + surrounding
// Relevance-scored
characters: Character[]; // POV character + mentioned characters
locations: Location[]; // Scene locations
activeThreads: PlotThread[]; // Active plot threads
worldRules: WorldRule[]; // Relevant rules
// Previous content
previousChapter?: ContentSummary;
previousScene?: ContentSummary;
// Constraints
establishedFacts: string[]; // From bible + published content
tensionTarget: number;
wordCountTarget: number;
}
draft → review → approved → published
↓
rejected → draft (revision)
const ContentStatusSchema = z.enum(['draft', 'review', 'approved', 'published']);
Every content change creates a version:
const ContentVersionSchema = z.object({
id: z.string().uuid(),
contentId: z.string().uuid(),
version: z.number().int().positive(),
body: z.string(),
createdAt: z.date(),
source: z.enum(['generation', 'manual', 'revision']),
analysis: ContentAnalysisSchema.optional(),
});
Protect content from revision cascades:
const LockPointSchema = z.object({
id: z.string().uuid(),
structureId: z.string().uuid(),
reason: z.string(),
createdAt: z.date(),
});
Key rule: Published content is immutable. Lock points prevent changes to approved-but-not-published content.
When content changes, downstream content may need revision:
interface CascadePreview {
affectedStructures: string[]; // Structure IDs that would be invalidated
blockedBy: LockPoint[]; // Lock points preventing cascade
horizon: number; // How far forward to cascade
}
const TensionAnalysisSchema = z.object({
score: z.number().min(0).max(100),
factors: z.array(z.object({
factor: z.string(),
contribution: z.number(),
})),
justification: z.string(),
});
Factors include: conflict level, stakes, pacing, emotional intensity, uncertainty.
const ContinuityCheckSchema = z.object({
passed: z.boolean(),
issues: z.array(z.object({
severity: z.enum(['error', 'warning', 'info']),
description: z.string(),
establishedFact: z.string().optional(),
conflictingContent: z.string().optional(),
structureId: z.string().optional(),
})),
});
const PacingAnalysisSchema = z.object({
score: z.number().min(0).max(100),
actualTension: z.number(),
targetTension: z.number(),
divergence: z.number(), // Absolute difference
suggestions: z.array(z.string()),
});
Track hook types across chapters to ensure variety:
interface HookPattern {
recentHooks: { chapterId: string; type: HookType }[];
distribution: Record<HookType, number>;
suggestions: string[]; // "Consider using a revelation hook"
}
Web serials follow tension cycles:
const TensionCycleSchema = z.object({
length: z.number().int().positive(), // Chapters per cycle
phases: z.array(z.object({
name: z.string(),
targetTension: z.number(),
chapters: z.number(),
})),
});
// Example: 5-chapter cycle
// Rising (2 chapters, 40-60) → Climax (1 chapter, 80+) → Falling (2 chapters, 30-50)
Track how far ahead content is written:
interface ReleaseBuffer {
scheduledReleases: number; // Chapters ready to publish
minimumBuffer: number; // Target minimum
daysUntilDepletion: number; // At current release rate
status: 'healthy' | 'warning' | 'critical';
}
For mysteries and foreshadowing:
const MysterySchema = z.object({
id: z.string().uuid(),
name: z.string(),
layer: z.enum(['surface', 'intermediate', 'deep']),
status: z.enum(['planted', 'developing', 'resolved']),
clues: z.array(z.object({
description: z.string(),
structureId: z.string(),
})),
resolution: z.string().optional(),
resolutionStructureId: z.string().optional(),
});
# Project management
serial project list
serial project create "My Web Serial" --format web-serial
serial project load <id>
# Bible management
serial bible character list
serial bible character create --name "Elena" --role protagonist
serial bible character update <id> --traits "brave,curious"
serial bible location create --name "Dragon Spire" --type building
# Structure
serial structure tree
serial structure create chapter --parent <arc-id> --title "The Awakening"
serial structure beats add <chapter-id> --summary "Elena discovers..." --tension 60
serial structure hook set <chapter-id> --type cliffhanger --description "..."
# Content
serial content get <structure-id>
serial content save <structure-id> --file chapter.md
serial content history <structure-id>
# Generation
serial generate start <chapter-id>
serial generate status
serial generate cancel
# Review
serial review queue
serial review approve <content-id>
serial review reject <content-id> --reason "Continuity issue"
serial review publish <content-id>
serial review lock <structure-id> --reason "Foreshadowing planted"
# Analysis
serial analyze tension <book-id>
serial analyze continuity <chapter-id>
serial analyze pacing <arc-id>
# Serial features
serial release buffer
serial release schedule
serial hooks analyze <book-id>
serial cycles status <book-id>
Tools follow the pattern spine_{resource}_{action}:
Bible tools: spine_bible_character_create, spine_bible_location_list, etc.
Structure tools: spine_structure_tree, spine_structure_create, spine_structure_add_beat
Content tools: spine_content_get, spine_content_save, spine_content_history
Generation tools: spine_generate_start, spine_generate_status
Review tools: spine_review_queue, spine_review_approve, spine_review_lock
Analytics tools: spine_analytics_tension, spine_analytics_characters
Serial tools: spine_serial_buffer, spine_serial_hooks, spine_serial_mysteries
Once content is published, it cannot be changed. This is enforced at the storage layer.
Every fact established in approved content becomes a constraint. The system tracks:
Web serial readers expect compelling endings. The system tracks hook types and warns about repetition.
Running out of content is a crisis. The release buffer is always visible and warnings trigger early.
LLM suggestions require human approval. The system assists but never publishes without explicit author action.37:["$","$L3d",null,{"content":"$3e","frontMatter":{"name":"spine-serial","description":"Guide for the Spine Serial domain - web serial fiction authoring. Covers bible entities, structure hierarchy, generation pipeline, review workflow, and serial-specific analysis."}}]