feat: deliver P2 v0.2.0 with unified editor, search, and snapshots

Implement schema v2 migration, outline/setting/inspiration CRUD, unified TipTap editing, reference panel with mentions, FTS global search, chapter version history, writing landmarks, word frequency analysis, light theme contrast fixes, and full unit/E2E test coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 14:13:27 +08:00
parent 91c93954df
commit aac51bf183
72 changed files with 5790 additions and 203 deletions
+81
View File
@@ -0,0 +1,81 @@
import { randomUUID } from 'crypto'
import type { Snapshot, SnapshotType } from '../../../shared/types'
import type { SqliteDb } from '../types'
function mapRow(row: Record<string, unknown>): Snapshot {
return {
id: row.id as string,
chapterId: row.chapter_id as string,
type: row.type as SnapshotType,
name: (row.name as string) ?? '',
content: row.content as string,
createdAt: row.created_at as string
}
}
export class SnapshotRepository {
constructor(private db: SqliteDb) {}
create(chapterId: string, type: SnapshotType, content: string, name = ''): Snapshot {
const id = randomUUID()
this.db
.prepare(`INSERT INTO snapshots (id, chapter_id, type, name, content) VALUES (?, ?, ?, ?, ?)`)
.run(id, chapterId, type, name, content)
return this.get(id)!
}
get(id: string): Snapshot | null {
const row = this.db.prepare('SELECT * FROM snapshots WHERE id = ?').get(id) as
| Record<string, unknown>
| undefined
return row ? mapRow(row) : null
}
list(chapterId: string): Snapshot[] {
return (
this.db
.prepare('SELECT * FROM snapshots WHERE chapter_id = ? ORDER BY created_at DESC')
.all(chapterId) as Record<string, unknown>[]
).map(mapRow)
}
delete(id: string): void {
this.db.prepare('DELETE FROM snapshots WHERE id = ?').run(id)
}
pruneAuto(chapterId: string, max: number): void {
const autos = (
this.db
.prepare(
`SELECT id FROM snapshots WHERE chapter_id = ? AND type = 'auto' ORDER BY created_at ASC`
)
.all(chapterId) as { id: string }[]
)
const excess = autos.length - max
if (excess <= 0) return
for (let i = 0; i < excess; i++) {
this.delete(autos[i].id)
}
}
prunePersist(chapterId: string, max: number): void {
const items = (
this.db
.prepare(
`SELECT id FROM snapshots WHERE chapter_id = ? AND type = 'persist' ORDER BY created_at ASC`
)
.all(chapterId) as { id: string }[]
)
const excess = items.length - max
if (excess <= 0) return
for (let i = 0; i < excess; i++) {
this.delete(items[i].id)
}
}
restore(snapshotId: string): string {
const snap = this.get(snapshotId)
if (!snap) throw new Error('Snapshot not found')
return snap.content
}
}