# Project export: Continuum

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: The longer any knowledge system runs — a story world, an AI agent's memory, a product spec — the harder it gets to keep consistent. Continuum catches the contradictions, with evidence.
- Devpost: https://devpost.com/software/continuum-sx7ife
- GitHub: https://github.com/Prithvi-Maddi/Continuum
- Video: https://www.youtube.com/embed/x11Z38DHsHc?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Prithvi Maddi (5 commits)

## Devpost submission (written by the team)

### Inspiration

Every long-running knowledge system is a giant pile of mutable state. Who's alive, who has the sword, who's at war, who knows the secret. Writers and show runners track it in bibles that rot the moment writing outpaces note-taking. Long-running AI agents have the same problem — they accumulate facts across sessions that drift, contradict, and branch with no one checking. Generic AI makes it worse. It generates more content with no model of your established state. It'll happily write a one-handed knight gripping with both gauntlets and never notice. We wanted to build something that doesn't generate — it checks. A tool that turns accumulated knowledge into queryable, verifiable state, then reads every new claim against it with evidence and a verified fix. Grammarly catches grammar. Continuum catches broken canon. We picked fictional worlds as the proving ground deliberately. They have the most adversarial consistency problem: nonlinear timelines, branching paths, hidden knowledge, character state, world rules, knowledge asymmetry. If the engine works on Westeros, it works on AI agent memory and product specs. We proved it on the hardest case first.

### What it does

Continuum is a live consistency engine for evolving knowledge systems, demoed on a preloaded Game of Thrones world. Load a source of truth — upload notes, paste text, or use the preloaded GoT demo world. Write a new scene. Hit Check Continuity. Every phrase that contradicts established canon lights up inline, with the exact evidence quote it conflicts with, the severity, and a one-click fix that gets verified before it's applied. The demo run: Paste this into the editor on the main timeline: "Jaime tightened the straps on both gauntlets before drawing his sword. He had ridden from Winterfell at first light and reached King's Landing by sunset. Over the capital, three dragons wheeled in formation — a sight every lord had grown up seeing. In the branch where Robb Stark survived the Twins, his mother Catelyn was already dead in the great hall." Four high-severity contradictions surface in under 8 seconds, each with the canon evidence it breaks: "both gauntlets" → Jaime lost his right hand after capture "reached King's Landing by sunset" → Winterfell to King's Landing takes weeks "dragons wheeled … every lord had grown up seeing" → No living dragons existed until Daenerys hatched hers; no one grew up knowing them "Robb survived … Catelyn was already dead" → In the Robb-lives branch, Catelyn also survived Then the smart part: flip the context chip to Flashback — before Jaime's capture and type "Jaime caught the wine cup with both hands." Zero warnings. The lost-hand fact isn't in force yet. The timeline visualization shows "you are here" before the event and greys out the fact. The model never sees the conflicting fact — the filtering happens in TypeScript before any LLM call. Apply a fix: the repair agent patches the text, re-runs the check on the patched span, and only returns verified ✓ if the original contradiction is gone with no new issues introduced.

### How we built it

The architecture bet: separate a platform-agnostic consistency engine (lib/engine/checkScene.ts) from every UI surface. The engine has no Next.js dependency. A browser extension, Google Docs add-on, or agent runtime calls the same /api/check endpoint and gets back ContinuityIssue[]. Build the engine once; bolt surfaces on. The 2-call hot path (not agentic): extractClaims(sceneText) — Claude Haiku extracts structured Claim[] + inferredContext via tool use. Fast, bounded, cheap. filterFacts(claims, position, branch) — pure TypeScript. Every CanonFact has two nullable validity event IDs. A scene's chronological position p (resolved from context chips) filters to only facts in force: start ≤ p < end. The model never sees filtered-out facts. This is the false-positive killer — no temporal reasoning in the prompt. detectContradictions(claims, filteredFacts) — Claude Sonnet with extended thinking returns ContinuityIssue[] via structured tool output, validated through Zod. This is the reasoning step; extended thinking earns its cost here. mapSpans(issues, sceneText) — server maps model-returned verbatim quotes to character offsets via indexOf + whitespace-normalized fuzzy fallback. The model is never asked to produce offsets. Two real agents: The canon-builder agent (lib/agents/canonBuilder.ts) runs a tool-using loop (up to 15 turns) converting raw text into structured entities, facts, events, and branches via add_entity, add_fact, add_event, add_branch, and search_existing_canon. It self-reviews validity windows and branch tags before committing. Working memory lives in Redis across tool turns. The repair verify-loop agent (lib/agents/repairVerify.ts) patches the scene text, re-runs checkScene scoped to the patched span, and confirms the original contradiction is resolved with no new high-severity issues before returning verified: true. The time model: integer event order + two nullable validity event IDs per fact. No interval trees, no temporal logic solver. Main timeline → p = +∞. Flashback before event X → p < order(X). Branch facts filter by branchId with per-claim scoping when prose names an alternate branch inline. TipTap (ProseMirror) handles inline decorations — contradiction spans get colored underlines by severity, clickable to expand issue detail. A custom decoration plugin reads ContinuityIssue[] from Zustand and maps plain-text character offsets to ProseMirror positions. Stack: Next.js 16 App Router · TypeScript · Tailwind v4 · TipTap · Zustand · Anthropic SDK (extended thinking) · Upstash Redis + Vector · Arize (OpenTelemetry) · Zod v4

### Challenges we ran into

Branch scoping across a single detection call. When prose names an alternate branch inline ("in the branch where Robb survived…"), the claim needs to be checked against facts from that branch, not the scene-level branch. Running a separate detection call per claim was too slow. The solution: pass each claim with its applicable fact subset as a labeled block in one prompt — the model sees claims and their branch-filtered facts together, never mixed. Span mapping without trusting the model. Asking Claude to return character offsets is fragile — models hallucinate positions, especially across paragraph boundaries. Instead we ask for the exact verbatim offending substring (highlightedText) and locate it server-side with indexOf, falling back to whitespace-normalized token matching. If a quote still isn't found, the issue degrades to a card without a highlight rather than crashing. TipTap decoration stability. ProseMirror positions shift as the user edits. We had to maintain a paragraph-offset table and remap plain-text character positions to ProseMirror positions on every check rather than caching them. Keeping the editor schema minimal (paragraphs + text, no nested marks) made this tractable. Zod v4 + tool use. Zod v4 broke zod-to-json-schema (the standard way to turn Zod schemas into Anthropic tool input_schema). We hand-wrote JSON Schema objects for all tool schemas, sharing them as both the Anthropic tool definition and the server-side validation layer. Extended thinking + structured output. Extended thinking doesn't compose cleanly with tool use in all model versions. We used the interleaved-thinking-2025-05-14 beta, which allows the model to think, then call a tool, giving us both reasoning quality and structured output in one call.

### Accomplishments we're proud of

The flashback suppression working cleanly. The core architectural bet — filter facts in code before the model sees them — produces correct behavior without any temporal reasoning in the prompt. The model can't hallucinate a false positive on a flashback because the conflicting fact literally isn't there. Seeing it work the first time on the Jaime/wine-cup case was the moment the design proved itself. Four high-signal contradictions on the GoT draft, every time. The combination of per-claim branch scoping, time-filtered facts, few-shot detection prompt, and extended thinking produces consistent results at low temperature. No random false positives; all four expected issues fire reliably. A verified repair loop. "Apply fix" doesn't blindly swap text. The agent patches, re-checks the exact span, and only surfaces verified ✓ when the contradiction is gone and no new issues appeared. That's a non-trivial pipeline to make reliable under time pressure. The generalization claim is structural, not marketing. The engine primitives (Entity, CanonFact, Claim, Branch, ContinuityIssue) contain no fiction-specific logic. The checkScene interface is: (claims, facts, context) → issues. Swap seed data and prompts and the same pipeline checks AI agent memory. That's a real architectural seam, not a slide bullet.

### What we learned

Pre-filter ruthlessly; prompt conservatively. Every fact we cut from the context window before the detection call is a false positive we don't have to explain away. Giving the model only the facts that are actually in force for this scene's position and branch is the single highest-leverage accuracy improvement — higher than prompt engineering alone. Separate the hot path from agents. The first instinct is to make the consistency check agentic — loop until confident. That's the wrong call for a live editor. Two bounded LLM calls with hard timeouts, a pre-warmed cache, and a canned fallback beat one agentic loop that might take 30 seconds or fail mid-stream on stage. Observability from day one. Instrumenting every AI step as a named OpenTelemetry span (extract → infer → detect → repair.propose → repair.verify) let us see exactly where over-flagging came from in Arize traces. The before/after prompt improvement is a story we could only tell because we were tracing from the start. Fiction is a legitimate proving ground for agent memory tooling. The hardest consistency problems — branching history, epistemic asymmetry, validity windows, per-claim context — all appear in serial fiction before they appear in production agent systems. Building on the fiction case first meant the primitives were already stress-tested when we reasoned about the broader use case.

### What's next

AI agent memory as a first-class surface. The engine already handles the right primitives: facts with validity windows, branching state, claim extraction from new output, contradiction detection with evidence. Wiring it to an agent's session store — where each new action is a "scene" checked against accumulated "canon" — is the most direct generalization and the one we're most excited about. Redis vector retrieval at scale. The current demo uses in-memory fact filtering over 8 facts. The Redis vector index and RedisRetriever are designed and partially built — promoting them to the primary retrieval path means the engine scales to thousands of facts without changing the check interface. Browser extension and editor integrations. Because the engine is a clean API (POST /api/check → ContinuityIssue[]), a browser extension that sends selected text is a thin wrapper. Google Docs and Obsidian integrations follow the same pattern — the core doesn't change, only the surface. Arize-driven prompt improvement loop. The tracing infrastructure is in place. The next step is closing the loop: when Arize shows a pattern of over- or under-flagging on a specific issue type, the detection prompt gets a targeted few-shot addition, measured against the golden test suite, and shipped as a new prompt version. Observability as a development workflow, not just a dashboard.

## README (from the GitHub repository)

# Continuum

> **Continuum is a consistency engine for evolving knowledge systems.**

Any system where facts accumulate over time — and new claims must stay consistent with what came before — has a consistency problem. Continuum solves it: load your source of truth, write something new, and every claim that contradicts established facts is flagged inline, with the exact evidence it conflicts with and a verified fix.

We built and demoed on **fictional worlds** because they're the hardest case. Nonlinear timelines, branching paths, hidden knowledge, object state, world rules, character beliefs — if the engine works here, every other domain is easier. The same primitives power **AI agent memory**, product specs, legal case facts, and research claims.

---

## The Problem

Every long-running knowledge system is a giant pile of mutable state. Someone — or something — must hold it all in their head simultaneously:

- **Who is in what state** — alive, dead, injured, exiled, under NDA, decommissioned, deprecated
- **Where things are** — who owns an asset, which version is deployed, what's locked in a vault
- **What relationships hold** — allies, enemies, dependencies, contracts — and *when* each became true
- **What rules govern the world** — physics, API contracts, legal constraints, capability limits
- **What order events happened** — and how claims that reference earlier state should be filtered
- **Which branch of reality applies** — alternate outcomes, divergent versions, A/B states, hypotheticals
- **Who knows what** — information asymmetry between agents, characters, or documents

Existing tools fail the same way across every domain: knowledge accumulates in **passive documents** that require manual upkeep and never check new claims against themselves. They go stale the moment output outpaces note-taking. Generic AI makes it worse — it generates more content with no model of *your* established state.

**Continuum makes accumulated knowledge checkable.** Upload your source of truth, write something new, and see every inconsistency — with the exact evidence, severity, and a fix that preserves what you actually meant.

---

## Why Fiction Is the Proving Ground

We didn't pick fiction arbitrarily. TV show writers' rooms, novelists, game designers, and show runners face the **most adversarial** version of this problem:

- Facts span hundreds of pages and years of real time
- Timelines are nonlinear — flashbacks, flash-forwards, myths, visions, prophecies
- State branches — player choices, alternate outcomes, divergent episodes
- Knowledge is asymmetric — characters believe different things; narrators reveal selectively
- Contradictions are invisible to generic AI — which has no model of your specific world

If Continuum catches broken canon in a Game of Thrones scene, it can catch contradictions in AI agent memory, product specs, and legal filings. The fiction case is harder.

**The direct read-across: AI agent memory.** Long-running agents accumulate facts that drift, contradict, and branch across sessions — exactly the same primitives. An agent's memory is a canon; each new action is a scene; hallucinated or stale state is a continuity error. Continuum is the consistency layer that agent builders need.

---

## The Demo

The demo world is a preloaded **Game of Thrones** canon: 8 facts, a branching timeline, and two alternate-history branches. Paste this into the editor and hit **Check Continuity**:

> *"Jaime tightened the straps on both gauntlets before drawing his sword. He had ridden from Winterfell at first light and reached King's Landing by sunset. Over the capital, three dragons wheeled in formation — a sight every lord had grown up seeing. In the branch where Robb Stark survived the massacre at the Twins, his mother Catelyn was already dead in the great hall."*

**Four high-severity contradictions surface, each with the exact canon evidence:**

| Highlighted Phrase | Issue | Evidence |
|--------------------|-------|----------|
| "both gauntlets" | `character_state` | Jaime lost his right hand after capture |
| "reached King's Landing by sunset" | `travel_time` | Winterfell → King's Landing takes weeks of hard riding |
| "dragons wheeled … every lord had grown up seeing" | `world_rule` | No living dragons existed until Daenerys hatched hers |
| "Robb survived … Catelyn was already dead" | `branch` | In the Robb-lives branch, Catelyn also survived |

**The timeline beat:** flip the context chip to *Flashback — before Jaime's capture* and write `"Jaime caught the wine cup with both hands."` — **zero warnings.** The lost-hand fact isn't in force yet. The timeline visualization shows "you are here" before the event and greys it out. The model never even sees the conflicting fact — the filtering happens in code before any LLM call.

---

## Architecture

### Three-Panel Interface

```
┌──────────────────────────────────────────────────────────────────┐
│  Continuum   [ Demo World: Game of Thrones ]    [Check Continuity]  │
├──────────────┬───────────────────────────────────┬───────────────┤
│  KNOWLEDGE   │  DRAFT / INPUT                     │  ISSUES       │
│  BIBLE       │  Context: [Main timeline ○]         │               │
│              │                                    │  ● HIGH       │
│  Entities    │  Jaime tightened the straps on     │  char_state   │
│  Rules       │  ░both gauntlets░ before drawing   │               │
│  Timeline    │  his sword. He had ridden from     │  ● HIGH       │
│  ──●── here  │  Winterfell at first light and     │  travel_time  │
│  Branches    │  reached ░King's Landing by sunset░ │               │
│              │                                    │  [Apply fix ✓]│
└──────────────┴───────────────────────────────────┴───────────────┘
```

### The Engine Pipeline

Every consistency check runs a **2-call hot path** — not an agentic loop. Speed matters for a live editing experience.

```
1. extractClaims(inputText)             ← Claude Haiku (fast)
   → Claim[] + inferredContext          (pre-fills context chips)

2. filterFacts(claims, position, branch)   ← pure TypeScript
   → only facts in force for this context's time window + branch

3. detectContradictions(claims, facts)     ← Claude Sonnet + extended thinking
   → Issue[] with evidence, spans, suggested fixes

4. mapSpans(issues, inputText)          ← indexOf + fuzzy fallback
   → character offsets for inline decorations
```

The filtering in step 2 is the core accuracy insight: **the model only ever sees facts that are actually in force for this context.** This eliminates false positives from stale or future state without asking the model to reason about time. In agent memory terms: only facts that were established before this action are candidates for contradiction.

### Two Real Agents

**Knowledge-builder agent** (`lib/agents/canonBuilder.ts`): a tool-using loop that converts raw source text into structured entities, facts, events, and branches. Tools: `add_entity`, `add_fact`, `add_event`, `add_branch`, `search_existing_canon`. Self-reviews validity windows and branch tags before committing. Writes embeddings to Redis for retrieval and deduplication.

**Repair verify-loop agent** (`lib/agents/repairVerify.ts`): when you click Apply Fix, it patches the text, re-runs the hot-path check on the patched span, and only returns `verified: true` if the original contradiction is gone with no new issues introduced. Never applies an unverified patch.

### Time & Branch Model

Every fact has two nullable validity event IDs defining its window of truth:

```ts
fact.validityStartEventId  // fact becomes true at this event
fact.validityEndEventId    // fact stops being true at this event (exclusive)
```

A scene or action's chronological position `p` drives filtering:

```ts
function isFactInForce(fact, p, events): boolean {
  const start = fact.validityStartEventId ? orderOf(start, events) : -Infinity;
  const end   = fact.validityEndEventId   ? orderOf(end, e

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 57 recognized source files, 334 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Redis (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (62 of 62)

```
.gitignore
AGENTS.md
app/api/canon/fact/route.ts
app/api/check/route.ts
app/api/ingest/route.ts
app/api/integrations/status/route.ts
app/api/issue/[id]/route.ts
app/api/project/route.ts
app/api/projects/route.ts
app/api/repair/route.ts
app/api/sources/route.ts
app/api/transcribe/route.ts
app/globals.css
app/layout.tsx
app/page.tsx
CLAUDE.md
components/HistoryModal.tsx
components/IngestModal.tsx
components/IntegrationStatus.tsx
components/panels/CanonGraph.tsx
components/panels/IssuePanel.tsx
components/panels/SceneEditor.tsx
components/panels/StoryBible.tsx
components/TopBar.tsx
CONTINUUM_DESIGN.md
eslint.config.mjs
hooks/useCheck.ts
hooks/useCommitScene.ts
hooks/useContinuumStore.ts
hooks/useVoiceDictation.ts
instrumentation-client.ts
instrumentation.ts
lib/agents/canonBuilder.ts
lib/agents/repairVerify.ts
lib/ai/client.ts
lib/ai/models.ts
lib/ai/prompts/canonBuilder.ts
lib/ai/prompts/detectContradictions.ts
lib/ai/prompts/extractClaims.ts
lib/ai/prompts/generateFix.ts
lib/ai/schemas.ts
lib/ai/tasks/detectContradictions.ts
lib/ai/tasks/extractClaims.ts
lib/ai/toolSchema.ts
lib/engine/checkScene.ts
lib/engine/filterFacts.ts
lib/engine/mapSpans.ts
lib/engine/resolveContext.ts
lib/store/memoryStore.ts
lib/store/vectorStore.ts
lib/telemetry/arize.ts
lib/types.ts
lib/utils.ts
next.config.ts
package.json
postcss.config.mjs
README.md
seed/world.ts
sentry.client.config.ts
sentry.edge.config.ts
sentry.server.config.ts
tsconfig.json
```

### Dependencies

- package.json: @anthropic-ai/sdk@^0.105.0, @arizeai/openinference-instrumentation-anthropic@^0.1.13, @deepgram/sdk@^5.4.0, @opentelemetry/auto-instrumentations-node@^0.77.0, @opentelemetry/exporter-trace-otlp-http@^0.219.0, @opentelemetry/instrumentation@^0.219.0, @opentelemetry/sdk-node@^0.219.0, @opentelemetry/sdk-trace-base@^2.8.0, @opentelemetry/sdk-trace-node@^2.8.0, @sentry/nextjs@^10.59.0, @tailwindcss/postcss@^4, @tiptap/extension-highlight@^3.27.1, @tiptap/pm@^3.27.1, @tiptap/react@^3.27.1, @tiptap/starter-kit@^3.27.1, @types/node@^20, @types/react@^19, @types/react-dom@^19, @upstash/redis@^1.38.0, @upstash/vector@^1.2.3, eslint@^9, eslint-config-next@16.2.9, mammoth@^1.12.0, next@16.2.9, openai@^6.44.0, react@19.2.4, react-dom@19.2.4, tailwindcss@^4, typescript@^5, zod@^4.4.3, zod-to-json-schema@^3.25.2, zustand@^5.0.14

### Recent commits (newest first)

- UX changes
- Read.me Updates
- Read.me update, bug fixes, integrations
- Telemetry, vector store
- Continuum MVP
- Initial commit from Create Next App

## Key source files (fetched from GitHub, selected and truncated for size)

### CLAUDE.md

```markdown
@AGENTS.md

```

### AGENTS.md

```markdown
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->

```

### package.json

```
{
  "name": "continuum-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.105.0",
    "@arizeai/openinference-instrumentation-anthropic": "^0.1.13",
    "@deepgram/sdk": "^5.4.0",
    "@opentelemetry/auto-instrumentations-node": "^0.77.0",
    "@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
    "@opentelemetry/instrumentation": "^0.219.0",
    "@opentelemetry/sdk-node": "^0.219.0",
    "@opentelemetry/sdk-trace-base": "^2.8.0",
    "@opentelemetry/sdk-trace-node": "^2.8.0",
    "@sentry/nextjs": "^10.59.0",
    "@tiptap/extension-highlight": "^3.27.1",
    "@tiptap/pm": "^3.27.1",
    "@tiptap/react": "^3.27.1",
    "@tiptap/starter-kit": "^3.27.1",
    "@upstash/redis": "^1.38.0",
    "@upstash/vector": "^1.2.3",
    "mammoth": "^1.12.0",
    "next": "16.2.9",
    "openai": "^6.44.0",
    "react": "19.2.4",
    "react-dom": "19.2.4",
    "zod": "^4.4.3",
    "zod-to-json-schema": "^3.25.2",
    "zustand": "^5.0.14"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.2.9",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### app/layout.tsx

```typescript
import type { Metadata } from 'next';
import './globals.css';

export const metadata: Metadata = {
  title: 'Continuum — Canon consistency engine',
  description: 'Grammarly catches grammar. Continuum catches broken canon.',
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" style={{ height: '100%' }}>
      <body style={{ height: '100%', margin: 0 }}>{children}</body>
    </html>
  );
}

```

### app/page.tsx

```typescript
'use client';
import { useEffect, useRef, useState, useCallback } from 'react';
import { TopBar } from '@/components/TopBar';
import { StoryBible } from '@/components/panels/StoryBible';
import { SceneEditor } from '@/components/panels/SceneEditor';
import { IssuePanel } from '@/components/panels/IssuePanel';
import { useContinuumStore, HARDCODED_ISSUES } from '@/hooks/useContinuumStore';

const DRAG_MIN = 160;
const DRAG_MAX = 600;

export async function loadProject(projectId: string) {
  const res = await fetch(`/api/project?id=${encodeURIComponent(projectId)}`);
  if (!res.ok) throw new Error(`Failed to load project ${projectId}`);
  return res.json();
}

export default function Home() {
  const {
    setProject, setEntities, setFacts, setEvents, setBranches,
    setProjectList, addToProjectList, setIssues, setSceneText,
  } = useContinuumStore();

  const [leftWidth, setLeftWidth] = useState(280);
  const [rightWidth, setRightWidth] = useState(320);
  const dragging = useRef<'left' | 'right' | null>(null);
  const startX = useRef(0);
  const startW = useRef(0);

  // Load project list + default project on mount
  useEffect(() => {
    Promise.all([
      fetch('/api/project').then(r => r.json()),
      loadProject('proj_got_demo').catch(() => null),
    ]).then(([list, world]) => {
      if (Array.isArray(list)) setProjectList(list);
      if (world?.project) {
        setProject(world.project);
        setEntities(world.entities ?? []);
        setFacts(world.facts ?? []);
        setEvents(world.events ?? []);
        setBranches(world.branches ?? []);
        // Restore autosaved draft if available
        try {
          const saved = localStorage.getItem(`continuum:draft:${world.project.id}`);
          if (saved) setSceneText(saved);
        } catch { /* localStorage unavailable */ }
      }
    }).catch(() => {
      import('@/seed/world').then(mod => {
        setProject(mod.PROJECT);
        setEntities(mod.ENTITIES);
        setFacts(mod.FACTS);
        setEvents(mod.EVENTS);
        setBranches(mod.BRANCHES);
        setProjectList([{ id: mod.PROJECT.id, name: mod.PROJECT.name }]);
      });
    });
  }, []);

  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if (e.key === 'F' && (e.metaKey || e.ctrlKey) && e.shiftKey) {
        useContinuumStore.getState().setIssues(HARDCODED_ISSUES);
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, []);

  const startDrag = useCallback((side: 'left' | 'right') => (e: React.MouseEvent) => {
    dragging.current = side;
    startX.current = e.clientX;
    startW.current = side === 'left' ? leftWidth : rightWidth;
    e.preventDefault();
  }, [leftWidth, rightWidth]);

  useEffect(() => {
    const onMove = (e: MouseEvent) => {
      if (!dragging.current) return;
      const delta = e.clientX - startX.current;
      if (dragging.current === 'left') {
        setLeftWidth(Math.max(DRAG_MIN, Math.min(DRAG_MAX, startW.current + delta)));
      } else {
        setRightWidth(Math.max(DRAG_MIN, Math.min(DRAG_MAX, startW.current - delta)));
      }
    };
    const onUp = () => { dragging.current = null; };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    return () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
  }, []);

  const handleStyle: React.CSSProperties = {
    width: 5, cursor: 'col-resize', background: 'transparent', flexShrink: 0, position: 'relative', zIndex: 10,
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
      <TopBar />
      <div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
        <div style={{ width: leftWidth, flexShrink: 0, overflow: 'hidden' }}>
          <StoryBible />
        </div>
        <div style={handleStyle} onMouseDown={startDrag('left')}>
          <div style={{ position: 'absolute', inset: '0 -1px', background: 'var(--border)', transition: 'background 0.15s' }}
            onMouseEnter={e => (e.currentTarget.style.background = 'var(--accent)')}
            onMouseLeave={e => (e.currentTarget.style.background = 'var(--border)')} />
        </div>
        <div style={{ flex: 1, overflow: 'hidden' }}>
          <SceneEditor />
        </div>
        <div style={handleStyle} onMouseDown={startDrag('right')}>
          <div style={{ position: 'absolute', inset: '0 -1px', background: 'var(--border)', transition: 'background 0.15s' }}
            onMouseEnter={e => (e.currentTarget.style.background = 'var(--accent)')}
            onMouseLeave={e => (e.currentTarget.style.background = 'var(--border)')} />
        </div>
        <div style={{ width: rightWidth, flexShrink: 0, overflow: 'hidden' }}>
          <IssuePanel />
        </div>
      </div>
    </div>
  );
}

```

### app/api/sources/route.ts

```typescript
import { NextRequest, NextResponse } from 'next/server';
import { memoryStore } from '@/lib/store/memoryStore';

export async function GET(req: NextRequest) {
  const projectId = req.nextUrl.searchParams.get('projectId') ?? 'proj_got_demo';
  const world = memoryStore.getWorld(projectId);
  // Return sources sorted newest-first, excluding the seed
  const sources = [...world.sources]
    .filter(s => s.kind !== 'seed')
    .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
  return NextResponse.json(sources);
}

```

### app/api/project/route.ts

```typescript
import { NextRequest, NextResponse } from 'next/server';
import { memoryStore } from '@/lib/store/memoryStore';

export async function GET(req: NextRequest) {
  try {
    const id = req.nextUrl.searchParams.get('id');
    if (id) {
      const world = memoryStore.getWorld(id);
      return NextResponse.json(world);
    }
    // No id → return project list
    return NextResponse.json(memoryStore.getProjectList());
  } catch (error) {
    return NextResponse.json({ error: { code: 'store_error', message: String(error) } }, { status: 500 });
  }
}

```

### app/api/projects/route.ts

```typescript
import { NextRequest, NextResponse } from 'next/server';
import { memoryStore } from '@/lib/store/memoryStore';

export async function POST(req: NextRequest) {
  try {
    const { name } = await req.json();
    if (!name?.trim()) return NextResponse.json({ error: 'name required' }, { status: 400 });
    const project = memoryStore.createProject(name.trim());
    return NextResponse.json(project);
  } catch (error) {
    return NextResponse.json({ error: String(error) }, { status: 500 });
  }
}

export async function PATCH(req: NextRequest) {
  try {
    const { id, name } = await req.json();
    if (!id || !name?.trim()) return NextResponse.json({ error: 'id and name required' }, { status: 400 });
    const ok = memoryStore.renameProject(id, name.trim());
    if (!ok) return NextResponse.json({ error: 'project not found' }, { status: 404 });
    return NextResponse.json({ id, name: name.trim() });
  } catch (error) {
    return NextResponse.json({ error: String(error) }, { status: 500 });
  }
}

```

### app/api/transcribe/route.ts

```typescript
import { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
  const apiKey = process.env.DEEPGRAM_API_KEY;
  if (!apiKey) {
    return NextResponse.json({ error: 'Deepgram not configured' }, { status: 503 });
  }

  try {
    const audioBuffer = await req.arrayBuffer();
    const contentType = req.headers.get('content-type') ?? 'audio/webm';

    const dgResponse = await fetch(
      'https://api.deepgram.com/v1/listen?model=nova-3&smart_format=true&punctuate=true',
      {
        method: 'POST',
        headers: {
          Authorization: `Token ${apiKey}`,
          'Content-Type': contentType,
        },
        body: audioBuffer,
      },
    );

    if (!dgResponse.ok) {
      const err = await dgResponse.text();
      console.error('[transcribe] Deepgram error:', err);
      return NextResponse.json({ error: 'Transcription failed' }, { status: 502 });
    }

    const data = await dgResponse.json() as {
      results?: { channels?: Array<{ alternatives?: Array<{ transcript?: string }> }> };
    };
    const transcript = data.results?.channels?.[0]?.alternatives?.[0]?.transcript ?? '';

    return NextResponse.json({ transcript });
  } catch (err) {
    console.error('[transcribe]', err);
    return NextResponse.json({ error: String(err) }, { status: 500 });
  }
}

```

### app/api/repair/route.ts

```typescript
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { memoryStore } from '@/lib/store/memoryStore';
import { runRepairVerify } from '@/lib/agents/repairVerify';
import type { SceneContext, ContinuityIssue, SuggestedFix } from '@/lib/types';

const RequestSchema = z.object({
  projectId: z.string(),
  sceneText: z.string(),
  context: z.object({
    presentation: z.enum(['main', 'flashback', 'flashforward', 'unknown']),
    anchorEventId: z.string().nullable().optional(),
    branchId: z.string().nullable(),
    confirmed: z.boolean(),
  }),
  issue: z.any(),
  fix: z.any().optional(),
});

export async function POST(req: NextRequest) {
  try {
    const body = await req.json();
    const { projectId, sceneText, context, issue, fix } = RequestSchema.parse(body);

    const world = memoryStore.getWorld(projectId);
    const ctx: SceneContext = {
      presentation: context.presentation,
      anchorEventId: context.anchorEventId ?? null,
      branchId: context.branchId,
      confirmed: context.confirmed,
    };

    const result = await runRepairVerify(
      sceneText,
      ctx,
      issue as ContinuityIssue,
      fix as SuggestedFix | null ?? null,
      world,
    );

    return NextResponse.json(result);
  } catch (error) {
    console.error('[/api/repair]', error);
    return NextResponse.json(
      { error: { code: 'repair_error', message: String(error) } },
      { status: 502 },
    );
  }
}

```

[46 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]