diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 34e7bef..916b356 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -28,8 +28,6 @@ This document provides context and best practices for GitHub Copilot when workin > Tests must import and exercise the REAL implementation classes, not inline helper functions. > Mock only external dependencies (database, filesystem), never the class under test. -See the [TDD Requirements](#test-driven-development-tdd-requirements) section for detailed guidelines. - --- ## ⚠️ MANDATORY: Fix All Test Failures @@ -124,682 +122,18 @@ See the [TDD Requirements](#test-driven-development-tdd-requirements) section fo ### Separation of Concerns 1. **Engine Classes** (`src/main/engine/`): All business logic lives here - - `PostEngine`: Blog post CRUD, file I/O, markdown handling - - `MediaEngine`: Media import, metadata management - - `SyncEngine`: Remote database synchronization - - `TaskManager`: Async task queue with progress tracking + - No UI code, no IPC code, no direct database or filesystem access + - Methods should be pure functions where possible, with side effects (fs/db/events) abstracted behind interfaces 2. **IPC Layer** (`src/main/ipc/`): Bridge between main and renderer - - Handlers call engine methods, never contain business logic - - Always use `ipcMain.handle` for async operations + - Handles all IPC communication, input validation, and error handling + - Calls engine methods and forwards results/events to renderer + - No business logic or UI code here 3. **UI Components** (`src/renderer/components/`): Presentation only - - Components should be stateless where possible - - Use Zustand store for shared state - - Never call IPC directly from deeply nested components - -### File Storage Pattern - -- **Posts**: Markdown files with YAML frontmatter in `posts/` directory -- **Media**: Binary files with `.meta` JSON sidecar files in `media/` directory -- **Database**: Index/cache only, source of truth is filesystem - -## TypeScript Best Practices - -### Type Safety - -```typescript -// ✅ DO: Use explicit return types for public methods -async function getPost(id: string): Promise { - // ... -} - -// ✅ DO: Use discriminated unions for status types -type SyncStatus = 'pending' | 'syncing' | 'synced' | 'error'; - -// ✅ DO: Use Drizzle's inferred types -import { posts, type Post, type NewPost } from './schema'; - -// ❌ DON'T: Use `any` type -function processData(data: any) { } // Bad - -// ❌ DON'T: Ignore null/undefined possibilities -const post = await getPost(id); -console.log(post.title); // Bad - post might be null -``` - -### Async/Await Patterns - -```typescript -// ✅ DO: Use try-catch with specific error handling -try { - await fileOperation(); -} catch (error) { - if (error instanceof Error && error.message.includes('ENOENT')) { - // Handle file not found - } - throw error; -} - -// ✅ DO: Use Promise.all for concurrent operations -const [posts, media] = await Promise.all([ - postEngine.getAllPosts(), - mediaEngine.getAllMedia() -]); - -// ❌ DON'T: Forget to await async operations -savePost(post); // Bad - fire and forget -``` - -### Error Handling - -```typescript -// ✅ DO: Create typed error classes -class SyncError extends Error { - constructor( - message: string, - public readonly code: 'AUTH_FAILED' | 'NETWORK_ERROR' | 'CONFLICT' - ) { - super(message); - this.name = 'SyncError'; - } -} - -// ✅ DO: Return result objects for operations that can fail -interface OperationResult { - success: boolean; - data?: T; - error?: string; -} -``` - -## Electron Best Practices - -### IPC Communication - -```typescript -// ✅ DO: Use contextBridge for secure IPC exposure -// preload.ts -contextBridge.exposeInMainWorld('electronAPI', { - posts: { - getAll: () => ipcRenderer.invoke('posts:getAll'), - create: (data: CreatePostInput) => ipcRenderer.invoke('posts:create', data), - } -}); - -// ✅ DO: Validate all IPC inputs in main process -ipcMain.handle('posts:create', async (_event, data: unknown) => { - const validated = validateCreatePostInput(data); - return postEngine.createPost(validated); -}); - -// ❌ DON'T: Use nodeIntegration: true -// ❌ DON'T: Expose Node.js APIs directly to renderer -``` - -### Window Management - -```typescript -// ✅ DO: Check window existence before sending messages -if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('event', data); -} - -// ✅ DO: Clean up IPC handlers on window close -mainWindow.on('closed', () => { - ipcMain.removeHandler('posts:getAll'); -}); -``` - -### Process Separation - -```typescript -// Main process: File system, database, native APIs -// Renderer process: UI only, no direct fs/db access - -// ✅ DO: All file operations in main process -// src/main/engine/PostEngine.ts -await fs.writeFile(filePath, content); - -// ❌ DON'T: Import 'fs' in renderer -// src/renderer/components/Editor.tsx -import fs from 'fs'; // Bad! -``` - -## SQLite & Drizzle ORM Best Practices - -### Schema Definition - -```typescript -// ✅ DO: Use proper column types and constraints -export const posts = sqliteTable('posts', { - id: text('id').primaryKey(), - title: text('title').notNull(), - slug: text('slug').notNull().unique(), - createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), - syncStatus: text('sync_status').$type<'pending' | 'synced'>().default('pending'), -}); - -// ✅ DO: Create indexes for frequently queried columns -// CREATE INDEX idx_posts_slug ON posts(slug); -// CREATE INDEX idx_posts_sync_status ON posts(sync_status); -``` - -### Query Patterns - -```typescript -import { eq, and, desc, sql } from 'drizzle-orm'; - -// ✅ DO: Use Drizzle query builder -const drafts = await db - .select() - .from(posts) - .where(eq(posts.status, 'draft')) - .orderBy(desc(posts.updatedAt)); - -// ✅ DO: Use transactions for multi-table updates -await db.transaction(async (tx) => { - await tx.update(posts).set({ status: 'published' }).where(eq(posts.id, id)); - await tx.insert(syncLog).values({ entityId: id, operation: 'update' }); -}); - -// ❌ DON'T: Write raw SQL unless absolutely necessary -// ❌ DON'T: Use string interpolation in queries -const result = db.run(`SELECT * FROM posts WHERE id = '${id}'`); // SQL injection risk! -``` - -### Connection Management - -```typescript -// ✅ DO: Use singleton pattern for database connection -let dbInstance: DatabaseConnection | null = null; - -export function getDatabase(): DatabaseConnection { - if (!dbInstance) { - dbInstance = new DatabaseConnection(); - } - return dbInstance; -} - -// ✅ DO: Close connection gracefully on app quit -app.on('before-quit', async () => { - await getDatabase().close(); -}); -``` - -## Remote Sync Best Practices (Dropbox) - -### Sync Strategy - -```typescript -// ✅ DO: Track sync status per entity -interface SyncableEntity { - syncStatus: 'pending' | 'syncing' | 'synced' | 'conflict'; - syncedAt: number | null; - checksum: string; -} - -// ✅ DO: Use checksums to detect conflicts -const localChecksum = calculateChecksum(localContent); -const remoteChecksum = await fetchRemoteChecksum(id); -if (localChecksum !== remoteChecksum) { - // Handle conflict -} - -// ✅ DO: Implement retry logic with exponential backoff -async function syncWithRetry(maxAttempts = 3): Promise { - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - await sync(); - return; - } catch (error) { - if (attempt === maxAttempts) throw error; - await sleep(Math.pow(2, attempt) * 1000); - } - } -} -``` - -### Offline-First Approach - -```typescript -// ✅ DO: Always write to local first -async function savePost(post: PostData): Promise { - // 1. Save to local file system - await writePostFile(post); - - // 2. Update local database - await updateLocalDatabase(post); - - // 3. Mark for sync (don't wait for network) - await markForSync(post.id); -} - -// ✅ DO: Queue sync operations -const syncQueue: SyncOperation[] = []; - -function queueSync(operation: SyncOperation): void { - syncQueue.push(operation); - processSyncQueue(); // Non-blocking -} - -// ✅ DO: Handle network state changes -window.addEventListener('online', () => { - syncEngine.processPendingChanges(); -}); -``` - -### Conflict Resolution - -```typescript -// ✅ DO: Implement clear conflict resolution strategy -type ConflictResolution = 'local-wins' | 'remote-wins' | 'manual'; - -interface ConflictInfo { - entityId: string; - localVersion: EntityVersion; - remoteVersion: EntityVersion; - resolution?: ConflictResolution; -} - -// ✅ DO: Allow user to resolve conflicts manually when needed -async function resolveConflict( - conflict: ConflictInfo, - resolution: ConflictResolution -): Promise { - switch (resolution) { - case 'local-wins': - await pushLocalToRemote(conflict.entityId); - break; - case 'remote-wins': - await pullRemoteToLocal(conflict.entityId); - break; - case 'manual': - await showConflictResolutionUI(conflict); - break; - } -} -``` - -### Sync Logging - -```typescript -// ✅ DO: Log all sync operations for debugging -await db.insert(syncLog).values({ - id: generateId(), - entityType: 'post', - entityId: postId, - operation: 'push', - status: 'success', - timestamp: Date.now(), -}); - -// ✅ DO: Implement sync status visibility in UI -const pendingCount = await syncEngine.getPendingChangesCount(); -statusBar.setSyncStatus({ pending: pendingCount }); -``` - -## React & State Management - -### Zustand Patterns - -```typescript -// ✅ DO: Keep store slices focused -interface AppState { - // UI state - activeView: 'posts' | 'media' | 'settings'; - sidebarVisible: boolean; - - // Data - posts: PostData[]; - media: MediaData[]; - - // Actions - setActiveView: (view: AppState['activeView']) => void; - setPosts: (posts: PostData[]) => void; -} - -// ✅ DO: Use selectors for derived state -const draftCount = useAppStore((state) => - state.posts.filter(p => p.status === 'draft').length -); - -// ❌ DON'T: Store derived data -interface BadState { - posts: PostData[]; - draftCount: number; // Bad - derived from posts -} -``` - -### Component Patterns - -```typescript -// ✅ DO: Use composition over prop drilling - - - - - - -// ✅ DO: Separate container and presentation components -// Container: handles data fetching/IPC -// Presentation: pure rendering based on props - -// ❌ DON'T: Put IPC calls in deeply nested components -const DeepComponent = () => { - // Bad - hard to test and maintain - const handleClick = async () => { - await window.electronAPI.posts.save(data); - }; -}; -``` - -## File Naming Conventions - -``` -src/ - main/ - database/ - schema.ts # Drizzle schema definitions - connection.ts # Database connection management - engine/ - PostEngine.ts # PascalCase for classes - TaskManager.ts - ipc/ - handlers.ts # camelCase for modules - renderer/ - components/ - Editor/ - Editor.tsx # PascalCase for components - Editor.css # Matching CSS file - index.ts # Barrel exports - store/ - appStore.ts # camelCase for stores - types/ - electron.d.ts # Type declarations -``` - -## Test-Driven Development (TDD) Requirements - -> **⚠️ CRITICAL: This project follows STRICT Test-Driven Development.** -> -> Writing implementation code before tests is NOT acceptable. -> Pull requests without corresponding tests will be rejected. - -**All new features and bug fixes MUST have tests written BEFORE implementation.** - -### The Golden Rule: Test Real Implementations - -```typescript -// ✅ CORRECT: Import and test the REAL class -import { PostEngine } from '../../src/main/engine/PostEngine'; - -describe('PostEngine', () => { - let postEngine: PostEngine; - - beforeEach(() => { - // Mock only external dependencies, NOT the class under test - postEngine = new PostEngine(mockDatabase, mockFileSystem); - }); - - it('should create a post with generated slug from title', async () => { - const result = await postEngine.createPost({ title: 'Hello World' }); - expect(result.slug).toBe('hello-world'); - }); -}); - -// ❌ WRONG: Testing inline helper functions instead of real implementations -describe('PostEngine', () => { - it('should generate slug', () => { - // This tests a local function, not the actual PostEngine class! - const generateSlug = (title: string) => title.toLowerCase().replace(/ /g, '-'); - expect(generateSlug('Hello World')).toBe('hello-world'); - }); -}); -``` - -### TDD Workflow (Red-Green-Refactor) - -```typescript -// 1. RED: Write a failing test first that uses the REAL implementation -import { PostEngine } from '../../src/main/engine/PostEngine'; - -describe('PostEngine.createPost', () => { - it('should create a post with generated slug from title', async () => { - const postEngine = new PostEngine(mockDb, mockFs); - const result = await postEngine.createPost({ title: 'Hello World' }); - expect(result.slug).toBe('hello-world'); - }); -}); - -// 2. GREEN: Write minimal code in PostEngine to pass the test -// 3. REFACTOR: Improve the code while keeping tests green -``` - -### Test File Structure - -``` -tests/ -├── setup.ts # Global test configuration -├── utils/ -│ ├── factories.ts # Mock data factories -│ └── index.ts # Utility exports -└── engine/ - ├── TaskManager.test.ts # TaskManager unit tests - ├── PostEngine.test.ts # PostEngine unit tests - ├── MediaEngine.test.ts # MediaEngine unit tests - └── SyncEngine.test.ts # SyncEngine unit tests -``` - -### Test Naming Conventions - -```typescript -// ✅ DO: Use descriptive test names that explain behavior -it('should generate slug from title with lowercase and hyphens', () => {}); -it('should emit taskCompleted event when task finishes', () => {}); -it('should return null when post not found', () => {}); - -// ❌ DON'T: Use vague test names -it('works', () => {}); -it('test createPost', () => {}); -``` - -### Mock Factory Patterns - -```typescript -// ✅ DO: Use factory functions with overrides -import { createMockPost, createMockMedia } from '../utils/factories'; - -const post = createMockPost({ - title: 'Custom Title', - status: 'published' -}); - -// ✅ DO: Create specialized factories for common scenarios -const draftPost = createMockPost({ status: 'draft' }); -const publishedPost = createMockPublishedPost(); -const imageMedia = createMockMedia({ mimeType: 'image/jpeg' }); -const pdfMedia = createMockPdfMedia(); -``` - -### Testing Async Code - -```typescript -// ✅ DO: Use async/await in tests -it('should save post to filesystem', async () => { - const post = createMockPost(); - await postEngine.createPost(post); - - expect(fs.writeFile).toHaveBeenCalled(); -}); - -// ✅ DO: Test event emissions -it('should emit postCreated event', async () => { - const handler = vi.fn(); - postEngine.on('postCreated', handler); - - await postEngine.createPost({ title: 'Test' }); - - expect(handler).toHaveBeenCalledWith( - expect.objectContaining({ title: 'Test' }) - ); -}); - -// ✅ DO: Test error cases -it('should throw when file write fails', async () => { - vi.mocked(fs.writeFile).mockRejectedValueOnce(new Error('ENOSPC')); - - await expect(postEngine.createPost({ title: 'Test' })) - .rejects.toThrow('ENOSPC'); -}); -``` - -### Mocking Dependencies - -```typescript -// ✅ DO: Mock at module level for consistent behavior -vi.mock('../../src/main/database', () => ({ - getDatabase: vi.fn(() => mockDatabase), -})); - -vi.mock('fs/promises', () => ({ - readFile: vi.fn(), - writeFile: vi.fn(), - unlink: vi.fn(), -})); - -// ✅ DO: Reset mocks between tests -beforeEach(() => { - vi.clearAllMocks(); -}); - -afterEach(() => { - vi.restoreAllMocks(); -}); -``` - -### Test Coverage Requirements - -- **Minimum coverage**: 80% for engine classes -- **Critical paths**: 100% coverage for data mutations -- **Edge cases**: Null handling, empty arrays, error conditions - -```bash -# Run tests with coverage -npm run test:coverage - -# View coverage report -open coverage/index.html -``` - -### When to Write Tests - -| Scenario | Test Requirement | -|----------|-----------------| -| New engine method | Unit tests BEFORE implementation | -| Bug fix | Failing test that reproduces bug FIRST | -| Refactoring | Ensure existing tests pass BEFORE and AFTER | -| IPC handler | Integration test with mocked engine | -| UI component | Behavior tests for user interactions | - -### Test Commands - -```bash -npm run test # Run all tests once -npm run test:watch # Run tests in watch mode -npm run test:coverage # Generate coverage report -npm run test:ui # Open Vitest UI -``` - -### Vitest Configuration - -Tests are configured in `vitest.config.ts` with: -- Global test utilities (describe, it, expect, vi) -- Node environment for main process tests -- Coverage reports via v8 -- Custom setup file for Electron mocks - -## Testing Considerations - -```typescript -// ✅ DO: Design for testability -// - Engine classes should accept dependencies via constructor -// - Use interfaces for external dependencies -// - Keep pure business logic separate from I/O - -class PostEngine { - constructor( - private db: DatabaseConnection, - private fs: FileSystemAdapter, // Mockable - private eventEmitter: EventEmitter - ) {} -} - -// ✅ DO: Use factory functions for test data -function createTestPost(overrides?: Partial): PostData { - return { - id: 'test-id', - title: 'Test Post', - status: 'draft', - ...overrides, - }; -} - -// ✅ DO: Test pure functions in isolation -describe('generateSlug', () => { - it('should convert to lowercase', () => { - expect(generateSlug('Hello World')).toBe('hello-world'); - }); - - it('should replace special characters', () => { - expect(generateSlug('Hello, World!')).toBe('hello-world'); - }); -}); -``` - -## Common Patterns in This Codebase - -### Creating a New Engine Method - -```typescript -// 1. Add method to engine class -async newOperation(input: InputType): Promise { - // Validate input - // Perform operation - // Emit event for UI updates - this.emit('operationCompleted', result); - return result; -} - -// 2. Add IPC handler -ipcMain.handle('entity:operation', async (_event, input) => { - return engine.newOperation(input); -}); - -// 3. Expose in preload -entity: { - operation: (input) => ipcRenderer.invoke('entity:operation', input), -} - -// 4. Update store and UI -``` - -### Adding a New Database Table - -```typescript -// 1. Define in schema.ts with proper types -export const newTable = sqliteTable('new_table', { - id: text('id').primaryKey(), - // ... columns -}); - -// 2. Export types -export type NewTableRow = typeof newTable.$inferSelect; -export type NewTableInsert = typeof newTable.$inferInsert; - -// 3. Add CREATE TABLE to migrations in connection.ts -// 4. Create corresponding engine class -``` + - Components should be stateless where possible + - Use Zustand store for shared state + - Never call IPC directly from deeply nested components ## Security Reminders @@ -808,3 +142,8 @@ export type NewTableInsert = typeof newTable.$inferInsert; - Use `contextIsolation: true` and `sandbox: false` only when necessary - Store Dropbox auth tokens in secure storage, not in code - Sanitize user input before rendering (XSS prevention) + +## Plan Mode + +- Make the plan extremely concise. Sacrifice grammar for the sake of concision. +- At the end of each plan, give me a list of unresolved questions to answer, if any. diff --git a/CLAUDE.md b/CLAUDE.md index 34e7bef..916b356 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,8 +28,6 @@ This document provides context and best practices for GitHub Copilot when workin > Tests must import and exercise the REAL implementation classes, not inline helper functions. > Mock only external dependencies (database, filesystem), never the class under test. -See the [TDD Requirements](#test-driven-development-tdd-requirements) section for detailed guidelines. - --- ## ⚠️ MANDATORY: Fix All Test Failures @@ -124,682 +122,18 @@ See the [TDD Requirements](#test-driven-development-tdd-requirements) section fo ### Separation of Concerns 1. **Engine Classes** (`src/main/engine/`): All business logic lives here - - `PostEngine`: Blog post CRUD, file I/O, markdown handling - - `MediaEngine`: Media import, metadata management - - `SyncEngine`: Remote database synchronization - - `TaskManager`: Async task queue with progress tracking + - No UI code, no IPC code, no direct database or filesystem access + - Methods should be pure functions where possible, with side effects (fs/db/events) abstracted behind interfaces 2. **IPC Layer** (`src/main/ipc/`): Bridge between main and renderer - - Handlers call engine methods, never contain business logic - - Always use `ipcMain.handle` for async operations + - Handles all IPC communication, input validation, and error handling + - Calls engine methods and forwards results/events to renderer + - No business logic or UI code here 3. **UI Components** (`src/renderer/components/`): Presentation only - - Components should be stateless where possible - - Use Zustand store for shared state - - Never call IPC directly from deeply nested components - -### File Storage Pattern - -- **Posts**: Markdown files with YAML frontmatter in `posts/` directory -- **Media**: Binary files with `.meta` JSON sidecar files in `media/` directory -- **Database**: Index/cache only, source of truth is filesystem - -## TypeScript Best Practices - -### Type Safety - -```typescript -// ✅ DO: Use explicit return types for public methods -async function getPost(id: string): Promise { - // ... -} - -// ✅ DO: Use discriminated unions for status types -type SyncStatus = 'pending' | 'syncing' | 'synced' | 'error'; - -// ✅ DO: Use Drizzle's inferred types -import { posts, type Post, type NewPost } from './schema'; - -// ❌ DON'T: Use `any` type -function processData(data: any) { } // Bad - -// ❌ DON'T: Ignore null/undefined possibilities -const post = await getPost(id); -console.log(post.title); // Bad - post might be null -``` - -### Async/Await Patterns - -```typescript -// ✅ DO: Use try-catch with specific error handling -try { - await fileOperation(); -} catch (error) { - if (error instanceof Error && error.message.includes('ENOENT')) { - // Handle file not found - } - throw error; -} - -// ✅ DO: Use Promise.all for concurrent operations -const [posts, media] = await Promise.all([ - postEngine.getAllPosts(), - mediaEngine.getAllMedia() -]); - -// ❌ DON'T: Forget to await async operations -savePost(post); // Bad - fire and forget -``` - -### Error Handling - -```typescript -// ✅ DO: Create typed error classes -class SyncError extends Error { - constructor( - message: string, - public readonly code: 'AUTH_FAILED' | 'NETWORK_ERROR' | 'CONFLICT' - ) { - super(message); - this.name = 'SyncError'; - } -} - -// ✅ DO: Return result objects for operations that can fail -interface OperationResult { - success: boolean; - data?: T; - error?: string; -} -``` - -## Electron Best Practices - -### IPC Communication - -```typescript -// ✅ DO: Use contextBridge for secure IPC exposure -// preload.ts -contextBridge.exposeInMainWorld('electronAPI', { - posts: { - getAll: () => ipcRenderer.invoke('posts:getAll'), - create: (data: CreatePostInput) => ipcRenderer.invoke('posts:create', data), - } -}); - -// ✅ DO: Validate all IPC inputs in main process -ipcMain.handle('posts:create', async (_event, data: unknown) => { - const validated = validateCreatePostInput(data); - return postEngine.createPost(validated); -}); - -// ❌ DON'T: Use nodeIntegration: true -// ❌ DON'T: Expose Node.js APIs directly to renderer -``` - -### Window Management - -```typescript -// ✅ DO: Check window existence before sending messages -if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('event', data); -} - -// ✅ DO: Clean up IPC handlers on window close -mainWindow.on('closed', () => { - ipcMain.removeHandler('posts:getAll'); -}); -``` - -### Process Separation - -```typescript -// Main process: File system, database, native APIs -// Renderer process: UI only, no direct fs/db access - -// ✅ DO: All file operations in main process -// src/main/engine/PostEngine.ts -await fs.writeFile(filePath, content); - -// ❌ DON'T: Import 'fs' in renderer -// src/renderer/components/Editor.tsx -import fs from 'fs'; // Bad! -``` - -## SQLite & Drizzle ORM Best Practices - -### Schema Definition - -```typescript -// ✅ DO: Use proper column types and constraints -export const posts = sqliteTable('posts', { - id: text('id').primaryKey(), - title: text('title').notNull(), - slug: text('slug').notNull().unique(), - createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), - syncStatus: text('sync_status').$type<'pending' | 'synced'>().default('pending'), -}); - -// ✅ DO: Create indexes for frequently queried columns -// CREATE INDEX idx_posts_slug ON posts(slug); -// CREATE INDEX idx_posts_sync_status ON posts(sync_status); -``` - -### Query Patterns - -```typescript -import { eq, and, desc, sql } from 'drizzle-orm'; - -// ✅ DO: Use Drizzle query builder -const drafts = await db - .select() - .from(posts) - .where(eq(posts.status, 'draft')) - .orderBy(desc(posts.updatedAt)); - -// ✅ DO: Use transactions for multi-table updates -await db.transaction(async (tx) => { - await tx.update(posts).set({ status: 'published' }).where(eq(posts.id, id)); - await tx.insert(syncLog).values({ entityId: id, operation: 'update' }); -}); - -// ❌ DON'T: Write raw SQL unless absolutely necessary -// ❌ DON'T: Use string interpolation in queries -const result = db.run(`SELECT * FROM posts WHERE id = '${id}'`); // SQL injection risk! -``` - -### Connection Management - -```typescript -// ✅ DO: Use singleton pattern for database connection -let dbInstance: DatabaseConnection | null = null; - -export function getDatabase(): DatabaseConnection { - if (!dbInstance) { - dbInstance = new DatabaseConnection(); - } - return dbInstance; -} - -// ✅ DO: Close connection gracefully on app quit -app.on('before-quit', async () => { - await getDatabase().close(); -}); -``` - -## Remote Sync Best Practices (Dropbox) - -### Sync Strategy - -```typescript -// ✅ DO: Track sync status per entity -interface SyncableEntity { - syncStatus: 'pending' | 'syncing' | 'synced' | 'conflict'; - syncedAt: number | null; - checksum: string; -} - -// ✅ DO: Use checksums to detect conflicts -const localChecksum = calculateChecksum(localContent); -const remoteChecksum = await fetchRemoteChecksum(id); -if (localChecksum !== remoteChecksum) { - // Handle conflict -} - -// ✅ DO: Implement retry logic with exponential backoff -async function syncWithRetry(maxAttempts = 3): Promise { - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - await sync(); - return; - } catch (error) { - if (attempt === maxAttempts) throw error; - await sleep(Math.pow(2, attempt) * 1000); - } - } -} -``` - -### Offline-First Approach - -```typescript -// ✅ DO: Always write to local first -async function savePost(post: PostData): Promise { - // 1. Save to local file system - await writePostFile(post); - - // 2. Update local database - await updateLocalDatabase(post); - - // 3. Mark for sync (don't wait for network) - await markForSync(post.id); -} - -// ✅ DO: Queue sync operations -const syncQueue: SyncOperation[] = []; - -function queueSync(operation: SyncOperation): void { - syncQueue.push(operation); - processSyncQueue(); // Non-blocking -} - -// ✅ DO: Handle network state changes -window.addEventListener('online', () => { - syncEngine.processPendingChanges(); -}); -``` - -### Conflict Resolution - -```typescript -// ✅ DO: Implement clear conflict resolution strategy -type ConflictResolution = 'local-wins' | 'remote-wins' | 'manual'; - -interface ConflictInfo { - entityId: string; - localVersion: EntityVersion; - remoteVersion: EntityVersion; - resolution?: ConflictResolution; -} - -// ✅ DO: Allow user to resolve conflicts manually when needed -async function resolveConflict( - conflict: ConflictInfo, - resolution: ConflictResolution -): Promise { - switch (resolution) { - case 'local-wins': - await pushLocalToRemote(conflict.entityId); - break; - case 'remote-wins': - await pullRemoteToLocal(conflict.entityId); - break; - case 'manual': - await showConflictResolutionUI(conflict); - break; - } -} -``` - -### Sync Logging - -```typescript -// ✅ DO: Log all sync operations for debugging -await db.insert(syncLog).values({ - id: generateId(), - entityType: 'post', - entityId: postId, - operation: 'push', - status: 'success', - timestamp: Date.now(), -}); - -// ✅ DO: Implement sync status visibility in UI -const pendingCount = await syncEngine.getPendingChangesCount(); -statusBar.setSyncStatus({ pending: pendingCount }); -``` - -## React & State Management - -### Zustand Patterns - -```typescript -// ✅ DO: Keep store slices focused -interface AppState { - // UI state - activeView: 'posts' | 'media' | 'settings'; - sidebarVisible: boolean; - - // Data - posts: PostData[]; - media: MediaData[]; - - // Actions - setActiveView: (view: AppState['activeView']) => void; - setPosts: (posts: PostData[]) => void; -} - -// ✅ DO: Use selectors for derived state -const draftCount = useAppStore((state) => - state.posts.filter(p => p.status === 'draft').length -); - -// ❌ DON'T: Store derived data -interface BadState { - posts: PostData[]; - draftCount: number; // Bad - derived from posts -} -``` - -### Component Patterns - -```typescript -// ✅ DO: Use composition over prop drilling - - - - - - -// ✅ DO: Separate container and presentation components -// Container: handles data fetching/IPC -// Presentation: pure rendering based on props - -// ❌ DON'T: Put IPC calls in deeply nested components -const DeepComponent = () => { - // Bad - hard to test and maintain - const handleClick = async () => { - await window.electronAPI.posts.save(data); - }; -}; -``` - -## File Naming Conventions - -``` -src/ - main/ - database/ - schema.ts # Drizzle schema definitions - connection.ts # Database connection management - engine/ - PostEngine.ts # PascalCase for classes - TaskManager.ts - ipc/ - handlers.ts # camelCase for modules - renderer/ - components/ - Editor/ - Editor.tsx # PascalCase for components - Editor.css # Matching CSS file - index.ts # Barrel exports - store/ - appStore.ts # camelCase for stores - types/ - electron.d.ts # Type declarations -``` - -## Test-Driven Development (TDD) Requirements - -> **⚠️ CRITICAL: This project follows STRICT Test-Driven Development.** -> -> Writing implementation code before tests is NOT acceptable. -> Pull requests without corresponding tests will be rejected. - -**All new features and bug fixes MUST have tests written BEFORE implementation.** - -### The Golden Rule: Test Real Implementations - -```typescript -// ✅ CORRECT: Import and test the REAL class -import { PostEngine } from '../../src/main/engine/PostEngine'; - -describe('PostEngine', () => { - let postEngine: PostEngine; - - beforeEach(() => { - // Mock only external dependencies, NOT the class under test - postEngine = new PostEngine(mockDatabase, mockFileSystem); - }); - - it('should create a post with generated slug from title', async () => { - const result = await postEngine.createPost({ title: 'Hello World' }); - expect(result.slug).toBe('hello-world'); - }); -}); - -// ❌ WRONG: Testing inline helper functions instead of real implementations -describe('PostEngine', () => { - it('should generate slug', () => { - // This tests a local function, not the actual PostEngine class! - const generateSlug = (title: string) => title.toLowerCase().replace(/ /g, '-'); - expect(generateSlug('Hello World')).toBe('hello-world'); - }); -}); -``` - -### TDD Workflow (Red-Green-Refactor) - -```typescript -// 1. RED: Write a failing test first that uses the REAL implementation -import { PostEngine } from '../../src/main/engine/PostEngine'; - -describe('PostEngine.createPost', () => { - it('should create a post with generated slug from title', async () => { - const postEngine = new PostEngine(mockDb, mockFs); - const result = await postEngine.createPost({ title: 'Hello World' }); - expect(result.slug).toBe('hello-world'); - }); -}); - -// 2. GREEN: Write minimal code in PostEngine to pass the test -// 3. REFACTOR: Improve the code while keeping tests green -``` - -### Test File Structure - -``` -tests/ -├── setup.ts # Global test configuration -├── utils/ -│ ├── factories.ts # Mock data factories -│ └── index.ts # Utility exports -└── engine/ - ├── TaskManager.test.ts # TaskManager unit tests - ├── PostEngine.test.ts # PostEngine unit tests - ├── MediaEngine.test.ts # MediaEngine unit tests - └── SyncEngine.test.ts # SyncEngine unit tests -``` - -### Test Naming Conventions - -```typescript -// ✅ DO: Use descriptive test names that explain behavior -it('should generate slug from title with lowercase and hyphens', () => {}); -it('should emit taskCompleted event when task finishes', () => {}); -it('should return null when post not found', () => {}); - -// ❌ DON'T: Use vague test names -it('works', () => {}); -it('test createPost', () => {}); -``` - -### Mock Factory Patterns - -```typescript -// ✅ DO: Use factory functions with overrides -import { createMockPost, createMockMedia } from '../utils/factories'; - -const post = createMockPost({ - title: 'Custom Title', - status: 'published' -}); - -// ✅ DO: Create specialized factories for common scenarios -const draftPost = createMockPost({ status: 'draft' }); -const publishedPost = createMockPublishedPost(); -const imageMedia = createMockMedia({ mimeType: 'image/jpeg' }); -const pdfMedia = createMockPdfMedia(); -``` - -### Testing Async Code - -```typescript -// ✅ DO: Use async/await in tests -it('should save post to filesystem', async () => { - const post = createMockPost(); - await postEngine.createPost(post); - - expect(fs.writeFile).toHaveBeenCalled(); -}); - -// ✅ DO: Test event emissions -it('should emit postCreated event', async () => { - const handler = vi.fn(); - postEngine.on('postCreated', handler); - - await postEngine.createPost({ title: 'Test' }); - - expect(handler).toHaveBeenCalledWith( - expect.objectContaining({ title: 'Test' }) - ); -}); - -// ✅ DO: Test error cases -it('should throw when file write fails', async () => { - vi.mocked(fs.writeFile).mockRejectedValueOnce(new Error('ENOSPC')); - - await expect(postEngine.createPost({ title: 'Test' })) - .rejects.toThrow('ENOSPC'); -}); -``` - -### Mocking Dependencies - -```typescript -// ✅ DO: Mock at module level for consistent behavior -vi.mock('../../src/main/database', () => ({ - getDatabase: vi.fn(() => mockDatabase), -})); - -vi.mock('fs/promises', () => ({ - readFile: vi.fn(), - writeFile: vi.fn(), - unlink: vi.fn(), -})); - -// ✅ DO: Reset mocks between tests -beforeEach(() => { - vi.clearAllMocks(); -}); - -afterEach(() => { - vi.restoreAllMocks(); -}); -``` - -### Test Coverage Requirements - -- **Minimum coverage**: 80% for engine classes -- **Critical paths**: 100% coverage for data mutations -- **Edge cases**: Null handling, empty arrays, error conditions - -```bash -# Run tests with coverage -npm run test:coverage - -# View coverage report -open coverage/index.html -``` - -### When to Write Tests - -| Scenario | Test Requirement | -|----------|-----------------| -| New engine method | Unit tests BEFORE implementation | -| Bug fix | Failing test that reproduces bug FIRST | -| Refactoring | Ensure existing tests pass BEFORE and AFTER | -| IPC handler | Integration test with mocked engine | -| UI component | Behavior tests for user interactions | - -### Test Commands - -```bash -npm run test # Run all tests once -npm run test:watch # Run tests in watch mode -npm run test:coverage # Generate coverage report -npm run test:ui # Open Vitest UI -``` - -### Vitest Configuration - -Tests are configured in `vitest.config.ts` with: -- Global test utilities (describe, it, expect, vi) -- Node environment for main process tests -- Coverage reports via v8 -- Custom setup file for Electron mocks - -## Testing Considerations - -```typescript -// ✅ DO: Design for testability -// - Engine classes should accept dependencies via constructor -// - Use interfaces for external dependencies -// - Keep pure business logic separate from I/O - -class PostEngine { - constructor( - private db: DatabaseConnection, - private fs: FileSystemAdapter, // Mockable - private eventEmitter: EventEmitter - ) {} -} - -// ✅ DO: Use factory functions for test data -function createTestPost(overrides?: Partial): PostData { - return { - id: 'test-id', - title: 'Test Post', - status: 'draft', - ...overrides, - }; -} - -// ✅ DO: Test pure functions in isolation -describe('generateSlug', () => { - it('should convert to lowercase', () => { - expect(generateSlug('Hello World')).toBe('hello-world'); - }); - - it('should replace special characters', () => { - expect(generateSlug('Hello, World!')).toBe('hello-world'); - }); -}); -``` - -## Common Patterns in This Codebase - -### Creating a New Engine Method - -```typescript -// 1. Add method to engine class -async newOperation(input: InputType): Promise { - // Validate input - // Perform operation - // Emit event for UI updates - this.emit('operationCompleted', result); - return result; -} - -// 2. Add IPC handler -ipcMain.handle('entity:operation', async (_event, input) => { - return engine.newOperation(input); -}); - -// 3. Expose in preload -entity: { - operation: (input) => ipcRenderer.invoke('entity:operation', input), -} - -// 4. Update store and UI -``` - -### Adding a New Database Table - -```typescript -// 1. Define in schema.ts with proper types -export const newTable = sqliteTable('new_table', { - id: text('id').primaryKey(), - // ... columns -}); - -// 2. Export types -export type NewTableRow = typeof newTable.$inferSelect; -export type NewTableInsert = typeof newTable.$inferInsert; - -// 3. Add CREATE TABLE to migrations in connection.ts -// 4. Create corresponding engine class -``` + - Components should be stateless where possible + - Use Zustand store for shared state + - Never call IPC directly from deeply nested components ## Security Reminders @@ -808,3 +142,8 @@ export type NewTableInsert = typeof newTable.$inferInsert; - Use `contextIsolation: true` and `sandbox: false` only when necessary - Store Dropbox auth tokens in secure storage, not in code - Sanitize user input before rendering (XSS prevention) + +## Plan Mode + +- Make the plan extremely concise. Sacrifice grammar for the sake of concision. +- At the end of each plan, give me a list of unresolved questions to answer, if any. diff --git a/GEMINI.md b/GEMINI.md index 34e7bef..916b356 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -28,8 +28,6 @@ This document provides context and best practices for GitHub Copilot when workin > Tests must import and exercise the REAL implementation classes, not inline helper functions. > Mock only external dependencies (database, filesystem), never the class under test. -See the [TDD Requirements](#test-driven-development-tdd-requirements) section for detailed guidelines. - --- ## ⚠️ MANDATORY: Fix All Test Failures @@ -124,682 +122,18 @@ See the [TDD Requirements](#test-driven-development-tdd-requirements) section fo ### Separation of Concerns 1. **Engine Classes** (`src/main/engine/`): All business logic lives here - - `PostEngine`: Blog post CRUD, file I/O, markdown handling - - `MediaEngine`: Media import, metadata management - - `SyncEngine`: Remote database synchronization - - `TaskManager`: Async task queue with progress tracking + - No UI code, no IPC code, no direct database or filesystem access + - Methods should be pure functions where possible, with side effects (fs/db/events) abstracted behind interfaces 2. **IPC Layer** (`src/main/ipc/`): Bridge between main and renderer - - Handlers call engine methods, never contain business logic - - Always use `ipcMain.handle` for async operations + - Handles all IPC communication, input validation, and error handling + - Calls engine methods and forwards results/events to renderer + - No business logic or UI code here 3. **UI Components** (`src/renderer/components/`): Presentation only - - Components should be stateless where possible - - Use Zustand store for shared state - - Never call IPC directly from deeply nested components - -### File Storage Pattern - -- **Posts**: Markdown files with YAML frontmatter in `posts/` directory -- **Media**: Binary files with `.meta` JSON sidecar files in `media/` directory -- **Database**: Index/cache only, source of truth is filesystem - -## TypeScript Best Practices - -### Type Safety - -```typescript -// ✅ DO: Use explicit return types for public methods -async function getPost(id: string): Promise { - // ... -} - -// ✅ DO: Use discriminated unions for status types -type SyncStatus = 'pending' | 'syncing' | 'synced' | 'error'; - -// ✅ DO: Use Drizzle's inferred types -import { posts, type Post, type NewPost } from './schema'; - -// ❌ DON'T: Use `any` type -function processData(data: any) { } // Bad - -// ❌ DON'T: Ignore null/undefined possibilities -const post = await getPost(id); -console.log(post.title); // Bad - post might be null -``` - -### Async/Await Patterns - -```typescript -// ✅ DO: Use try-catch with specific error handling -try { - await fileOperation(); -} catch (error) { - if (error instanceof Error && error.message.includes('ENOENT')) { - // Handle file not found - } - throw error; -} - -// ✅ DO: Use Promise.all for concurrent operations -const [posts, media] = await Promise.all([ - postEngine.getAllPosts(), - mediaEngine.getAllMedia() -]); - -// ❌ DON'T: Forget to await async operations -savePost(post); // Bad - fire and forget -``` - -### Error Handling - -```typescript -// ✅ DO: Create typed error classes -class SyncError extends Error { - constructor( - message: string, - public readonly code: 'AUTH_FAILED' | 'NETWORK_ERROR' | 'CONFLICT' - ) { - super(message); - this.name = 'SyncError'; - } -} - -// ✅ DO: Return result objects for operations that can fail -interface OperationResult { - success: boolean; - data?: T; - error?: string; -} -``` - -## Electron Best Practices - -### IPC Communication - -```typescript -// ✅ DO: Use contextBridge for secure IPC exposure -// preload.ts -contextBridge.exposeInMainWorld('electronAPI', { - posts: { - getAll: () => ipcRenderer.invoke('posts:getAll'), - create: (data: CreatePostInput) => ipcRenderer.invoke('posts:create', data), - } -}); - -// ✅ DO: Validate all IPC inputs in main process -ipcMain.handle('posts:create', async (_event, data: unknown) => { - const validated = validateCreatePostInput(data); - return postEngine.createPost(validated); -}); - -// ❌ DON'T: Use nodeIntegration: true -// ❌ DON'T: Expose Node.js APIs directly to renderer -``` - -### Window Management - -```typescript -// ✅ DO: Check window existence before sending messages -if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('event', data); -} - -// ✅ DO: Clean up IPC handlers on window close -mainWindow.on('closed', () => { - ipcMain.removeHandler('posts:getAll'); -}); -``` - -### Process Separation - -```typescript -// Main process: File system, database, native APIs -// Renderer process: UI only, no direct fs/db access - -// ✅ DO: All file operations in main process -// src/main/engine/PostEngine.ts -await fs.writeFile(filePath, content); - -// ❌ DON'T: Import 'fs' in renderer -// src/renderer/components/Editor.tsx -import fs from 'fs'; // Bad! -``` - -## SQLite & Drizzle ORM Best Practices - -### Schema Definition - -```typescript -// ✅ DO: Use proper column types and constraints -export const posts = sqliteTable('posts', { - id: text('id').primaryKey(), - title: text('title').notNull(), - slug: text('slug').notNull().unique(), - createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), - syncStatus: text('sync_status').$type<'pending' | 'synced'>().default('pending'), -}); - -// ✅ DO: Create indexes for frequently queried columns -// CREATE INDEX idx_posts_slug ON posts(slug); -// CREATE INDEX idx_posts_sync_status ON posts(sync_status); -``` - -### Query Patterns - -```typescript -import { eq, and, desc, sql } from 'drizzle-orm'; - -// ✅ DO: Use Drizzle query builder -const drafts = await db - .select() - .from(posts) - .where(eq(posts.status, 'draft')) - .orderBy(desc(posts.updatedAt)); - -// ✅ DO: Use transactions for multi-table updates -await db.transaction(async (tx) => { - await tx.update(posts).set({ status: 'published' }).where(eq(posts.id, id)); - await tx.insert(syncLog).values({ entityId: id, operation: 'update' }); -}); - -// ❌ DON'T: Write raw SQL unless absolutely necessary -// ❌ DON'T: Use string interpolation in queries -const result = db.run(`SELECT * FROM posts WHERE id = '${id}'`); // SQL injection risk! -``` - -### Connection Management - -```typescript -// ✅ DO: Use singleton pattern for database connection -let dbInstance: DatabaseConnection | null = null; - -export function getDatabase(): DatabaseConnection { - if (!dbInstance) { - dbInstance = new DatabaseConnection(); - } - return dbInstance; -} - -// ✅ DO: Close connection gracefully on app quit -app.on('before-quit', async () => { - await getDatabase().close(); -}); -``` - -## Remote Sync Best Practices (Dropbox) - -### Sync Strategy - -```typescript -// ✅ DO: Track sync status per entity -interface SyncableEntity { - syncStatus: 'pending' | 'syncing' | 'synced' | 'conflict'; - syncedAt: number | null; - checksum: string; -} - -// ✅ DO: Use checksums to detect conflicts -const localChecksum = calculateChecksum(localContent); -const remoteChecksum = await fetchRemoteChecksum(id); -if (localChecksum !== remoteChecksum) { - // Handle conflict -} - -// ✅ DO: Implement retry logic with exponential backoff -async function syncWithRetry(maxAttempts = 3): Promise { - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - await sync(); - return; - } catch (error) { - if (attempt === maxAttempts) throw error; - await sleep(Math.pow(2, attempt) * 1000); - } - } -} -``` - -### Offline-First Approach - -```typescript -// ✅ DO: Always write to local first -async function savePost(post: PostData): Promise { - // 1. Save to local file system - await writePostFile(post); - - // 2. Update local database - await updateLocalDatabase(post); - - // 3. Mark for sync (don't wait for network) - await markForSync(post.id); -} - -// ✅ DO: Queue sync operations -const syncQueue: SyncOperation[] = []; - -function queueSync(operation: SyncOperation): void { - syncQueue.push(operation); - processSyncQueue(); // Non-blocking -} - -// ✅ DO: Handle network state changes -window.addEventListener('online', () => { - syncEngine.processPendingChanges(); -}); -``` - -### Conflict Resolution - -```typescript -// ✅ DO: Implement clear conflict resolution strategy -type ConflictResolution = 'local-wins' | 'remote-wins' | 'manual'; - -interface ConflictInfo { - entityId: string; - localVersion: EntityVersion; - remoteVersion: EntityVersion; - resolution?: ConflictResolution; -} - -// ✅ DO: Allow user to resolve conflicts manually when needed -async function resolveConflict( - conflict: ConflictInfo, - resolution: ConflictResolution -): Promise { - switch (resolution) { - case 'local-wins': - await pushLocalToRemote(conflict.entityId); - break; - case 'remote-wins': - await pullRemoteToLocal(conflict.entityId); - break; - case 'manual': - await showConflictResolutionUI(conflict); - break; - } -} -``` - -### Sync Logging - -```typescript -// ✅ DO: Log all sync operations for debugging -await db.insert(syncLog).values({ - id: generateId(), - entityType: 'post', - entityId: postId, - operation: 'push', - status: 'success', - timestamp: Date.now(), -}); - -// ✅ DO: Implement sync status visibility in UI -const pendingCount = await syncEngine.getPendingChangesCount(); -statusBar.setSyncStatus({ pending: pendingCount }); -``` - -## React & State Management - -### Zustand Patterns - -```typescript -// ✅ DO: Keep store slices focused -interface AppState { - // UI state - activeView: 'posts' | 'media' | 'settings'; - sidebarVisible: boolean; - - // Data - posts: PostData[]; - media: MediaData[]; - - // Actions - setActiveView: (view: AppState['activeView']) => void; - setPosts: (posts: PostData[]) => void; -} - -// ✅ DO: Use selectors for derived state -const draftCount = useAppStore((state) => - state.posts.filter(p => p.status === 'draft').length -); - -// ❌ DON'T: Store derived data -interface BadState { - posts: PostData[]; - draftCount: number; // Bad - derived from posts -} -``` - -### Component Patterns - -```typescript -// ✅ DO: Use composition over prop drilling - - - - - - -// ✅ DO: Separate container and presentation components -// Container: handles data fetching/IPC -// Presentation: pure rendering based on props - -// ❌ DON'T: Put IPC calls in deeply nested components -const DeepComponent = () => { - // Bad - hard to test and maintain - const handleClick = async () => { - await window.electronAPI.posts.save(data); - }; -}; -``` - -## File Naming Conventions - -``` -src/ - main/ - database/ - schema.ts # Drizzle schema definitions - connection.ts # Database connection management - engine/ - PostEngine.ts # PascalCase for classes - TaskManager.ts - ipc/ - handlers.ts # camelCase for modules - renderer/ - components/ - Editor/ - Editor.tsx # PascalCase for components - Editor.css # Matching CSS file - index.ts # Barrel exports - store/ - appStore.ts # camelCase for stores - types/ - electron.d.ts # Type declarations -``` - -## Test-Driven Development (TDD) Requirements - -> **⚠️ CRITICAL: This project follows STRICT Test-Driven Development.** -> -> Writing implementation code before tests is NOT acceptable. -> Pull requests without corresponding tests will be rejected. - -**All new features and bug fixes MUST have tests written BEFORE implementation.** - -### The Golden Rule: Test Real Implementations - -```typescript -// ✅ CORRECT: Import and test the REAL class -import { PostEngine } from '../../src/main/engine/PostEngine'; - -describe('PostEngine', () => { - let postEngine: PostEngine; - - beforeEach(() => { - // Mock only external dependencies, NOT the class under test - postEngine = new PostEngine(mockDatabase, mockFileSystem); - }); - - it('should create a post with generated slug from title', async () => { - const result = await postEngine.createPost({ title: 'Hello World' }); - expect(result.slug).toBe('hello-world'); - }); -}); - -// ❌ WRONG: Testing inline helper functions instead of real implementations -describe('PostEngine', () => { - it('should generate slug', () => { - // This tests a local function, not the actual PostEngine class! - const generateSlug = (title: string) => title.toLowerCase().replace(/ /g, '-'); - expect(generateSlug('Hello World')).toBe('hello-world'); - }); -}); -``` - -### TDD Workflow (Red-Green-Refactor) - -```typescript -// 1. RED: Write a failing test first that uses the REAL implementation -import { PostEngine } from '../../src/main/engine/PostEngine'; - -describe('PostEngine.createPost', () => { - it('should create a post with generated slug from title', async () => { - const postEngine = new PostEngine(mockDb, mockFs); - const result = await postEngine.createPost({ title: 'Hello World' }); - expect(result.slug).toBe('hello-world'); - }); -}); - -// 2. GREEN: Write minimal code in PostEngine to pass the test -// 3. REFACTOR: Improve the code while keeping tests green -``` - -### Test File Structure - -``` -tests/ -├── setup.ts # Global test configuration -├── utils/ -│ ├── factories.ts # Mock data factories -│ └── index.ts # Utility exports -└── engine/ - ├── TaskManager.test.ts # TaskManager unit tests - ├── PostEngine.test.ts # PostEngine unit tests - ├── MediaEngine.test.ts # MediaEngine unit tests - └── SyncEngine.test.ts # SyncEngine unit tests -``` - -### Test Naming Conventions - -```typescript -// ✅ DO: Use descriptive test names that explain behavior -it('should generate slug from title with lowercase and hyphens', () => {}); -it('should emit taskCompleted event when task finishes', () => {}); -it('should return null when post not found', () => {}); - -// ❌ DON'T: Use vague test names -it('works', () => {}); -it('test createPost', () => {}); -``` - -### Mock Factory Patterns - -```typescript -// ✅ DO: Use factory functions with overrides -import { createMockPost, createMockMedia } from '../utils/factories'; - -const post = createMockPost({ - title: 'Custom Title', - status: 'published' -}); - -// ✅ DO: Create specialized factories for common scenarios -const draftPost = createMockPost({ status: 'draft' }); -const publishedPost = createMockPublishedPost(); -const imageMedia = createMockMedia({ mimeType: 'image/jpeg' }); -const pdfMedia = createMockPdfMedia(); -``` - -### Testing Async Code - -```typescript -// ✅ DO: Use async/await in tests -it('should save post to filesystem', async () => { - const post = createMockPost(); - await postEngine.createPost(post); - - expect(fs.writeFile).toHaveBeenCalled(); -}); - -// ✅ DO: Test event emissions -it('should emit postCreated event', async () => { - const handler = vi.fn(); - postEngine.on('postCreated', handler); - - await postEngine.createPost({ title: 'Test' }); - - expect(handler).toHaveBeenCalledWith( - expect.objectContaining({ title: 'Test' }) - ); -}); - -// ✅ DO: Test error cases -it('should throw when file write fails', async () => { - vi.mocked(fs.writeFile).mockRejectedValueOnce(new Error('ENOSPC')); - - await expect(postEngine.createPost({ title: 'Test' })) - .rejects.toThrow('ENOSPC'); -}); -``` - -### Mocking Dependencies - -```typescript -// ✅ DO: Mock at module level for consistent behavior -vi.mock('../../src/main/database', () => ({ - getDatabase: vi.fn(() => mockDatabase), -})); - -vi.mock('fs/promises', () => ({ - readFile: vi.fn(), - writeFile: vi.fn(), - unlink: vi.fn(), -})); - -// ✅ DO: Reset mocks between tests -beforeEach(() => { - vi.clearAllMocks(); -}); - -afterEach(() => { - vi.restoreAllMocks(); -}); -``` - -### Test Coverage Requirements - -- **Minimum coverage**: 80% for engine classes -- **Critical paths**: 100% coverage for data mutations -- **Edge cases**: Null handling, empty arrays, error conditions - -```bash -# Run tests with coverage -npm run test:coverage - -# View coverage report -open coverage/index.html -``` - -### When to Write Tests - -| Scenario | Test Requirement | -|----------|-----------------| -| New engine method | Unit tests BEFORE implementation | -| Bug fix | Failing test that reproduces bug FIRST | -| Refactoring | Ensure existing tests pass BEFORE and AFTER | -| IPC handler | Integration test with mocked engine | -| UI component | Behavior tests for user interactions | - -### Test Commands - -```bash -npm run test # Run all tests once -npm run test:watch # Run tests in watch mode -npm run test:coverage # Generate coverage report -npm run test:ui # Open Vitest UI -``` - -### Vitest Configuration - -Tests are configured in `vitest.config.ts` with: -- Global test utilities (describe, it, expect, vi) -- Node environment for main process tests -- Coverage reports via v8 -- Custom setup file for Electron mocks - -## Testing Considerations - -```typescript -// ✅ DO: Design for testability -// - Engine classes should accept dependencies via constructor -// - Use interfaces for external dependencies -// - Keep pure business logic separate from I/O - -class PostEngine { - constructor( - private db: DatabaseConnection, - private fs: FileSystemAdapter, // Mockable - private eventEmitter: EventEmitter - ) {} -} - -// ✅ DO: Use factory functions for test data -function createTestPost(overrides?: Partial): PostData { - return { - id: 'test-id', - title: 'Test Post', - status: 'draft', - ...overrides, - }; -} - -// ✅ DO: Test pure functions in isolation -describe('generateSlug', () => { - it('should convert to lowercase', () => { - expect(generateSlug('Hello World')).toBe('hello-world'); - }); - - it('should replace special characters', () => { - expect(generateSlug('Hello, World!')).toBe('hello-world'); - }); -}); -``` - -## Common Patterns in This Codebase - -### Creating a New Engine Method - -```typescript -// 1. Add method to engine class -async newOperation(input: InputType): Promise { - // Validate input - // Perform operation - // Emit event for UI updates - this.emit('operationCompleted', result); - return result; -} - -// 2. Add IPC handler -ipcMain.handle('entity:operation', async (_event, input) => { - return engine.newOperation(input); -}); - -// 3. Expose in preload -entity: { - operation: (input) => ipcRenderer.invoke('entity:operation', input), -} - -// 4. Update store and UI -``` - -### Adding a New Database Table - -```typescript -// 1. Define in schema.ts with proper types -export const newTable = sqliteTable('new_table', { - id: text('id').primaryKey(), - // ... columns -}); - -// 2. Export types -export type NewTableRow = typeof newTable.$inferSelect; -export type NewTableInsert = typeof newTable.$inferInsert; - -// 3. Add CREATE TABLE to migrations in connection.ts -// 4. Create corresponding engine class -``` + - Components should be stateless where possible + - Use Zustand store for shared state + - Never call IPC directly from deeply nested components ## Security Reminders @@ -808,3 +142,8 @@ export type NewTableInsert = typeof newTable.$inferInsert; - Use `contextIsolation: true` and `sandbox: false` only when necessary - Store Dropbox auth tokens in secure storage, not in code - Sanitize user input before rendering (XSS prevention) + +## Plan Mode + +- Make the plan extremely concise. Sacrifice grammar for the sake of concision. +- At the end of each plan, give me a list of unresolved questions to answer, if any. diff --git a/package-lock.json b/package-lock.json index 060b833..aad949b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -186,6 +186,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -764,6 +765,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz", "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -840,6 +842,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", "license": "MIT", + "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -861,6 +864,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.13.tgz", "integrity": "sha512-QBO8ZsgJLCbI28KdY0/oDy5NQLqOQVZCozBknxc2/7L98V+TVYFHnfaCsnGh1U+alpd2LOkStVwYY7nW2R1xbw==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -956,6 +960,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -996,6 +1001,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1044,9 +1050,9 @@ } }, "node_modules/@electron/asar/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1318,13 +1324,13 @@ } }, "node_modules/@electron/universal/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -1350,7 +1356,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1372,7 +1377,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1389,7 +1393,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -1404,7 +1407,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -2434,9 +2436,9 @@ } }, "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -2497,9 +2499,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3961,6 +3963,7 @@ "resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.17.0.tgz", "integrity": "sha512-TLjSU9Otdpq0SpKHl1tD1Nc9MKhrsZbCFGot3EbCxRa8m1E5R1mMwoOjKMMM31IyF7fr+hPNHLpYfwbMKNusmg==", "license": "MIT", + "peer": true, "dependencies": { "@libsql/core": "^0.17.0", "@libsql/hrana-client": "^0.9.0", @@ -5167,8 +5170,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -5403,6 +5405,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5413,6 +5416,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5520,6 +5524,7 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -5674,13 +5679,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -5899,6 +5904,7 @@ "integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "4.0.18", "fflate": "^0.8.2", @@ -6085,6 +6091,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6113,11 +6120,12 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6148,9 +6156,9 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -6712,6 +6720,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6918,13 +6927,13 @@ "license": "ISC" }, "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -7329,9 +7338,9 @@ } }, "node_modules/conf/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -7430,8 +7439,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-env": { "version": "10.1.0", @@ -7568,7 +7576,8 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/d3-cloud": { "version": "1.2.8", @@ -7828,9 +7837,9 @@ } }, "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -7846,6 +7855,7 @@ "integrity": "sha512-uOOBA3f+kW3o4KpSoMQ6SNpdXU7WtxlJRb9vCZgOvqhTz4b3GjcoWKstdisizNZLsylhTMv8TLHFPFW0Uxsj/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.7.0", "builder-util": "26.4.1", @@ -7947,8 +7957,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dompurify": { "version": "3.3.1", @@ -8421,7 +8430,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -8442,7 +8450,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -8469,16 +8476,6 @@ "dev": true, "license": "MIT" }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -8586,6 +8583,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -8663,6 +8661,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8775,9 +8774,9 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -9101,9 +9100,9 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -9428,9 +9427,9 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -10170,6 +10169,7 @@ "integrity": "sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.7.6", @@ -10505,7 +10505,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -11775,7 +11774,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -11788,6 +11786,7 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "license": "MIT", + "peer": true, "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" @@ -12365,6 +12364,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -12485,7 +12485,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -12503,7 +12502,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -12524,7 +12522,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -12540,7 +12537,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -12695,6 +12691,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "license": "MIT", + "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -12728,6 +12725,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -12761,6 +12759,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.6.tgz", "integrity": "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -12851,6 +12850,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12916,6 +12916,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -12945,8 +12946,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-refresh": { "version": "0.18.0", @@ -13251,7 +13251,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -14019,7 +14018,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -14299,7 +14297,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "devOptional": true, - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.21.0", @@ -14855,6 +14854,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15153,6 +15153,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -15712,6 +15713,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -15789,6 +15791,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz", "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/compiler-sfc": "3.5.28",