# Contributing to Balanced Breakfast Patterns, conventions, and rules for working on the Balanced Breakfast codebase. ## Project Structure ``` balanced_breakfast/ (workspace root) Cargo.toml # Workspace definition crates/ bb-interface/ # Shared types (leaf crate, no internal deps) bb-core/ # Orchestrator, Rhai plugin runtime, crypto bb-feed/ # Feed aggregation, ordering, scoring bb-db/ # SQLite persistence, repositories src-tauri/ src/ main.rs # Tauri setup state.rs # AppState, scheduler, abort handles commands/ # Tauri commands (thin wrappers) sync_service.rs # SyncKit change tracking frontend/ js/ # JavaScript modules (IIFE + BB.* namespace) css/ # Styles tests/ # Command integration tests plugins/ # Bundled Rhai plugins (rss, mastodon, hackernews, etc.) migrations/sqlite/ # SQLx SQLite migrations ``` ### Crate Dependency Flow ``` bb-interface (leaf, shared types) ↓ bb-core (orchestrator, Rhai runtime) bb-feed (aggregation, ordering) bb-db (SQLite persistence) ↓ src-tauri (Tauri shell) ``` **Strict rule:** `bb-interface` has no internal dependencies. Library crates never depend on `src-tauri`. ## Orchestrator Pattern The `Orchestrator` (`bb-core::orchestrator`) is the central coordinator. It owns the `Database` and a `PluginManager` behind `Arc>`. **Responsibilities:** - Plugin lifecycle (load, init, fetch, shutdown) - Upserting fetched items (with HTML sanitization and URL tracker stripping) - Secret encryption for plugin configs - Circuit breaker enforcement (10 consecutive failures disables auto-fetch) **Lock discipline:** Hold the `PluginManager` lock only during synchronous operations. Release before any `.await`: ```rust // GOOD: Release lock before async DB ops let fetch_result = { let plugins = self.plugins.read().await; plugins.fetch(plugin_id, None) }; // Lock released here, now safe to await self.db.feeds().record_fetch_success(feed_id).await?; ``` ## Plugin System ### Contract: 4 Required Functions Every `.rhai` plugin must implement: | Function | Returns | Purpose | |----------|---------|---------| | `fn id()` | `String` | Unique identifier (e.g., `"rss"`) | | `fn name()` | `String` | Display name (e.g., `"RSS"`) | | `fn config_schema()` | `Map` | `#{ description, fields: [...] }` | | `fn fetch(config, cursor)` | `Map` | `#{ items: [...], has_more: bool }` | Config field types: `Text`, `TextArea`, `Secret`, `Url`, `Number`, `Toggle`, `Select`. ### Sandbox Limits Each plugin runs in an isolated Rhai engine with strict limits (operations, recursion, HTTP timeout/count, response size, SSRF blocking). Full limit table and isolation details in `docs/architecture.md` § Sandboxing. ### Plugin Style All `.rhai` plugins follow the cross-project Rhai style guide. Run `_meta/scripts/lint-rhai.sh` to check formatting. Key points: 4-space indent, `snake_case` functions, `UPPER_CASE` constants, header comment block, contract functions before helpers. ### Host Functions Plugins call host-provided functions: `http_get(url)`, `http_get_json(url)`, `parse_feed(xml)`, `parse_html(html)`, `extract_readability(url)`, `truncate(s, n)`, `to_json(val)`, `from_json(s)`, etc. See `plugins/` for bundled examples. ## Thin Tauri Commands Commands in `src-tauri/src/commands/` are thin wrappers. They extract parameters, call orchestrator or repository methods, and return serialized response types: ```rust #[tauri::command] #[instrument(skip_all)] pub async fn list_items( state: State<'_, Arc>, filter: ItemFilter, ) -> Result, ApiError> { let db = state.orchestrator.database(); let items = db.items().list(filter).await?; Ok(items.into_iter().map(ItemSnapshot::from).collect()) } ``` **Rules:** - Every command: `#[instrument(skip_all)]`, returns `Result` - No business logic in commands — that belongs in library crates - Response types use `#[serde(rename_all = "camelCase")]` for JS consumption ## Error Types All commands return `Result`. The error has typed codes: ```rust pub enum ApiErrorCode { BadRequest, // User-caused errors NotFound, // Missing resource Database, // DB errors Internal, // Unexpected errors Plugin, // Rhai plugin errors } pub struct ApiError { pub code: ApiErrorCode, pub message: String, } ``` `ApiError` implements `From` for `sqlx::Error`, `FeedError`, and `OrchestratorError`. The frontend receives `{code: "BAD_REQUEST", message: "..."}`. ## Circuit Breaker Feeds are auto-disabled after 10 consecutive fetch failures: - `consecutive_failures` counter increments on each failure, resets to 0 on success. - When the counter reaches 10, `circuit_broken` is set to true. - Circuit-broken feeds skip auto-fetch scheduling. - Users can manually reset via `reset_circuit_breaker` command. ## Secret Encryption Plugin configs with `Secret`-type fields are encrypted at rest using AES-256-GCM: - Format: `bb_enc:v1:` - Key stored in OS keychain (via `keyring` crate), with file fallback at `~/.config/balanced-breakfast/encryption.key` - Backward compatible: unencrypted values are read as plaintext, encrypted on next save - `encrypt_config_secrets()` / `decrypt_config_secrets()` operate on JSON values in-place, using the config schema to identify Secret fields ## JavaScript Architecture ### BB.* Namespace All JavaScript lives under the `BB` global namespace (defined in `bb.js`, loaded first): ```javascript window.BB = { api: {}, // Tauri IPC wrappers (thin invoke calls) state: {}, // Centralized reactive state (Proxy-based) ui: {}, // Toast, modal, form builder, confirm dialog utils: {}, // escapeHtml, escapeAttr, sanitizeHtml, debounce sources: {}, // Source list, tag filters, feed CRUD items: {}, // Item list, read/star toggle, pagination detail: {}, // Detail panel, reader view feeds: {}, // Feed management, OPML import/export themes: {}, // Theme loading sync: {}, // Cloud sync settings app: {}, // Bootstrap, keyboard shortcuts }; ``` ### Module Pattern Every JS file is a strict-mode IIFE: ```javascript (function() { 'use strict'; const { escapeHtml, getErrorMessage } = BB.utils; async function load() { /* ... */ } function render(sources) { /* ... */ } BB.sources = { load, render }; })(); ``` ### State Management `BB.state` is a Proxy over a data object with pub/sub: ```javascript BB.state.set('sources', data); // Set + notify BB.state.sources; // Direct read BB.state.subscribe('items', (newVal, oldVal) => {}); // Subscribe ``` ### XSS Prevention - `BB.utils.escapeHtml()` for text content - `BB.utils.escapeAttr()` for HTML attributes - `BB.utils.sanitizeHtml()` for feed body content (strips script, iframe, on* handlers, javascript: URLs) All user-provided content and feed content must be escaped or sanitized before insertion into the DOM. ## Testing ### Rust Integration Tests Integration tests in `src-tauri/tests/` use in-memory SQLite and bypass Tauri `State<>` wrappers: ```rust // Common setup pub async fn setup(suffix: &str) -> Orchestrator { let config = OrchestratorConfig { database_url: "sqlite::memory:".to_string(), plugins_dir: temp_dir.to_string(), fetch_interval_secs: 300, }; let orchestrator = Orchestrator::new(config).await.unwrap(); orchestrator.migrate().await.unwrap(); orchestrator } ``` Tests exercise command-layer code paths without the Tauri runtime. Test helpers in `tests/common/mod.rs` provide setup, fixture creation, and teardown. ### JS Tests Node.js test runner at `src-tauri/frontend/js/tests/run.js` with browser API mocks. Run with `node src-tauri/frontend/js/tests/run.js`. ### Full Pipeline Always verify: plugin fetch → DB upsert → command response → JS render. A bug in any layer can appear as a symptom in another. ## When Adding a New Source Type 1. Create a `.rhai` file in `plugins/` implementing the 4 required functions 2. Test it manually: add a feed with the new busser, trigger a fetch 3. The orchestrator auto-discovers plugins on startup — no Rust changes needed ## When Adding Features 1. **New data type:** Add to `bb-interface` (types), `bb-db` (persistence), then commands 2. **New command:** Add in `src-tauri/src/commands/`, register in `main.rs` 3. **New JS module:** Create IIFE file, attach to `BB.moduleName`, add to `index.html`