Skip to main content

max / balanced_breakfast

Strip historical narrative from documentation Remove what a doc used to say, when it changed, the incidents that justified a rule, finished migration narration, and counts and versions that rot. State the rules in the present tense instead. Keep every instruction, prohibition and threshold, and keep the measurements that make a rule actionable. Public-facing docs keep their explanatory voice.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-08-31 01:51 UTC
Signed with PGP, not checked
Commit: 2209cacf84578279cc3ce97d92ce7660419bc943
Parent: 1b790a8
13 files changed, +80 insertions, -111 deletions
M CHANGELOG.md +2 -2
@@ -7,10 +7,10 @@
7 7 ## [Unreleased]
8 8
9 9 ### Changed
10 - - The database layer moved from sqlx to rusqlite, the house SQLite standard. The repositories are synchronous now: SQLite has no async API, and the async this layer wore came from sqlx rather than from anything the app wanted. No schema change and no migration of your data -- the existing migration ledger is read and appended to exactly as before, so an upgraded install applies nothing on first launch.
10 + - The database layer moved from sqlx to rusqlite, and the repositories are synchronous. No schema change and no migration of your data: the existing migration ledger is read and appended to as before, so an upgraded install applies nothing on first launch.
11 11
12 12 ### Removed
13 - - Supernote push. The `supernote` build feature, the "Send to Supernote" button, and the `supernote-push` dependency are all gone. The library behind it was decommissioned when the reMarkable displaced the Supernote as the daily e-ink device. Releases built before this carried the feature on; releases after it will not.
13 + - Supernote push. The `supernote` build feature, the "Send to Supernote" button, and the `supernote-push` dependency are all gone.
14 14
15 15 ## [0.3.0] — 2026-03-28
16 16
M CONTRIBUTING.md +3 -3
@@ -17,13 +17,13 @@
17 17 main.rs # Tauri setup
18 18 state.rs # AppState, scheduler, abort handles
19 19 commands/ # Tauri commands (thin wrappers)
20 - sync_service.rs # SyncKit change tracking
20 + sync_service/ # SyncKit change tracking
21 21 frontend/
22 22 js/ # JavaScript modules (IIFE + BB.* namespace)
23 23 css/ # Styles
24 24 tests/ # Command integration tests
25 25 plugins/ # Bundled Rhai plugins (rss, mastodon, hackernews, etc.)
26 - migrations/sqlite/ # SQLx SQLite migrations
26 + migrations/sqlite/ # SQLite migrations
27 27 ```
28 28
29 29 ### Crate Dependency Flow
@@ -136,7 +136,7 @@
136 136 }
137 137 ```
138 138
139 - The repositories in `bb-db` are synchronous: SQLite has no async API, and the async this layer used to wear came from sqlx. Call them directly; do not wrap them in `async fn`.
139 + The repositories in `bb-db` are synchronous: SQLite has no async API. Call them directly; do not wrap them in `async fn`.
140 140
141 141 `ApiError` implements `From` for `bb_db::DbError`, `rusqlite::Error`, `FeedError`, and `OrchestratorError`. The frontend receives `{code: "BAD_REQUEST", message: "..."}`.
142 142
@@ -29,7 +29,7 @@
29 29 - **Plugin lifecycle**: load `.rhai` scripts from the plugins directory, initialize them with config from the DB, and provide fetch/shutdown operations.
30 30 - **Fetch execution**: call a plugin's `fetch()`, strip tracking parameters from item URLs and HTML bodies, upsert results into the DB via the items repository, and record success/failure on the feed.
31 31 - **Circuit breaker**: after 10 consecutive fetch failures (`CIRCUIT_BREAKER_THRESHOLD`), the feed is marked `circuit_broken` and excluded from auto-fetch until manually reset.
32 - - **Secret management**: holds an optional AES-256-GCM key. On startup, encrypts any plaintext Secret fields in existing feed configs (migration from legacy plaintext).
32 + - **Secret management**: holds an optional AES-256-GCM key. On startup, encrypts any plaintext Secret fields in existing feed configs.
33 33 - **Fetch-all**: iterates all loaded plugins and fetches each, collecting total item counts.
34 34
35 35 The orchestrator does not own the fetch scheduler or background tasks. Those are managed by `AppState` in the Tauri layer.
@@ -105,7 +105,7 @@
105 105
106 106 ## Database Layer
107 107
108 - SQLite via rusqlite behind an r2d2 pool (16 connections), with the 13 migrations in `migrations/sqlite/` embedded at build time. The `Database` struct holds the pool and provides typed repository accessors. The repositories are synchronous: SQLite has no async API, and the async this layer used to wear came from sqlx.
108 + SQLite via rusqlite behind an r2d2 pool (16 connections), with the migrations in `migrations/sqlite/` embedded at build time. The `Database` struct holds the pool and provides typed repository accessors. The repositories are synchronous: SQLite has no async API.
109 109
110 110 ### Tables
111 111
@@ -207,10 +207,10 @@
207 207 | Ordering/filtering | `crates/bb-feed/src/ordering.rs` |
208 208 | Database layer | `crates/bb-db/src/` |
209 209 | Repositories | `crates/bb-db/src/repository/` |
210 - | Migrations | `migrations/sqlite/` (001-010) |
210 + | Migrations | `migrations/sqlite/` |
211 211 | Tauri app state | `src-tauri/src/state.rs` |
212 212 | Tauri commands | `src-tauri/src/commands/` |
213 - | Sync service | `src-tauri/src/sync_service.rs` |
213 + | Sync service | `src-tauri/src/sync_service/` |
214 214 | Bundled plugins | `plugins/` |
215 215 | Frontend JS | `src-tauri/frontend/js/` |
216 216 | Frontend CSS | `src-tauri/frontend/css/` |
@@ -1,6 +1,6 @@
1 1 # Balanced Breakfast Database Schema
2 2
3 - SQLite schema reference. 13 migrations. Migrations live in `migrations/sqlite/` as numbered SQL files, embedded at build time and applied on boot by the runner in `crates/bb-db/src/migrate.rs`. The ledger table is still called `_sqlx_migrations`: sqlx wrote it in every install in the field, and adopting it verbatim is what lets an upgrade find nothing pending.
3 + SQLite schema reference. Migrations live in `migrations/sqlite/` as numbered SQL files, embedded at build time and applied on boot by the runner in `crates/bb-db/src/migrate.rs`. The ledger table is named `_sqlx_migrations`; keep that name, or an upgraded install finds every migration pending and re-applies it.
4 4
5 5 ## Domain Map
6 6
@@ -28,10 +28,10 @@
28 28 | `config` | TEXT | JSON config blob, default `{}` |
29 29 | `enabled` | INTEGER | Boolean, default 1 |
30 30 | `last_fetch` | TEXT | ISO datetime, nullable |
31 - | `consecutive_failures` | INTEGER | Health tracking (migration 006), default 0 |
32 - | `last_error` | TEXT | Last failure message (migration 006), nullable |
33 - | `last_success_at` | TEXT | Last successful fetch (migration 006), nullable |
34 - | `circuit_broken` | INTEGER | Auto-disabled after repeated failures (migration 008), default 0 |
31 + | `consecutive_failures` | INTEGER | Health tracking, default 0 |
32 + | `last_error` | TEXT | Last failure message, nullable |
33 + | `last_success_at` | TEXT | Last successful fetch, nullable |
34 + | `circuit_broken` | INTEGER | Auto-disabled after repeated failures, default 0 |
35 35 | `created_at`, `updated_at` | TEXT | ISO datetimes |
36 36
37 37 Indexes: `busser_id`, `enabled`.
@@ -60,7 +60,7 @@
60 60 | `tags` | TEXT | JSON array, default `[]` |
61 61 | `is_read` | INTEGER | Boolean, default 0 |
62 62 | `is_starred` | INTEGER | Boolean, default 0 |
63 - | `actions` | TEXT | JSON array of action definitions (migration 010), default `[]` |
63 + | `actions` | TEXT | JSON array of action definitions, default `[]` |
64 64 | `created_at`, `updated_at` | TEXT | ISO datetimes |
65 65
66 66 Indexes: `feed_id`, `busser_id`, `published_at DESC`, `is_read`, `is_starred`.
@@ -70,7 +70,7 @@
70 70 ## Search
71 71
72 72 ### feed_items_fts
73 - FTS5 virtual table in external content mode. Indexes `title`, `body`, and `bite_text` from `feed_items`. Migration 005.
73 + FTS5 virtual table in external content mode. Indexes `title`, `body`, and `bite_text` from `feed_items`.
74 74
75 75 - External content source: `feed_items` (via `content=` and `content_rowid=`)
76 76 - Kept in sync by three triggers: `feed_items_fts_insert`, `feed_items_fts_update`, `feed_items_fts_delete`
@@ -81,7 +81,7 @@
81 81 ## Organization
82 82
83 83 ### feed_tags
84 - User-assigned flat tags on feeds for organization. Migration 004.
84 + User-assigned flat tags on feeds for organization.
85 85
86 86 | Column | Type | Notes |
87 87 |--------|------|-------|
@@ -91,7 +91,7 @@
91 91 PK: `(feed_id, tag)`. Index: `tag`.
92 92
93 93 ### query_feeds
94 - Saved filter rules that act as virtual feed sources. Migration 009.
94 + Saved filter rules that act as virtual feed sources.
95 95
96 96 | Column | Type | Notes |
97 97 |--------|------|-------|
@@ -105,7 +105,7 @@
105 105 ## Busser State
106 106
107 107 ### busser_state
108 - Key-value state storage for Rhai plugins (pagination cursors, auth tokens, etc.). Migration 003.
108 + Key-value state storage for Rhai plugins (pagination cursors, auth tokens, etc.).
109 109
110 110 | Column | Type | Notes |
111 111 |--------|------|-------|
@@ -122,7 +122,7 @@
122 122 ## Preferences
123 123
124 124 ### user_config
125 - Key-value preferences store. Migration 007.
125 + Key-value preferences store.
126 126
127 127 | Column | Type | Notes |
128 128 |--------|------|-------|
@@ -134,7 +134,7 @@
134 134 ## SyncKit
135 135
136 136 ### sync_state
137 - Sync metadata key-value store. Single-row-per-key design. Migration 007.
137 + Sync metadata key-value store. Single-row-per-key design.
138 138
139 139 | Column | Type | Notes |
140 140 |--------|------|-------|
@@ -144,7 +144,7 @@
144 144 Seeded keys: `device_id`, `pull_cursor`, `auto_sync_enabled`, `sync_interval_minutes`, `applying_remote`, `last_sync_at`, `initial_snapshot_done`.
145 145
146 146 ### sync_changelog
147 - Local change log for push/pull sync. Records changes to synced tables for the next push. Migration 007.
147 + Local change log for push/pull sync. Records changes to synced tables for the next push.
148 148
149 149 | Column | Type | Notes |
150 150 |--------|------|-------|
@@ -178,17 +178,3 @@
178 178 - **Lookup** on feeds (busser_id, enabled), busser_state (busser_id), feed_tags (tag)
179 179 - **Sync** on sync_changelog (pushed) for pending push detection
180 180
181 - ## Schema Evolution
182 -
183 - | Migration | Change |
184 - |-----------|--------|
185 - | 001 | feeds table (sources) |
186 - | 002 | feed_items table (fetched content) |
187 - | 003 | busser_state (plugin key-value storage) |
188 - | 004 | feed_tags (user-assigned tags on feeds) |
189 - | 005 | feed_items_fts (FTS5 full-text search) |
190 - | 006 | Feed health tracking (consecutive_failures, last_error, last_success_at) |
191 - | 007 | SyncKit integration (user_config, sync_state, sync_changelog, sync triggers) |
192 - | 008 | Circuit breaker (circuit_broken column, updated sync triggers) |
193 - | 009 | Query feeds (saved filter rules as virtual sources) |
194 - | 010 | Item actions (actions JSON column on feed_items) |
@@ -1,11 +1,9 @@
1 1 //! Single source of truth for `user_config` keys and their sync posture.
2 2 //!
3 - //! BalancedBreakfast used to sync every `user_config` key unconditionally: the
4 - //! table's triggers enqueued a changelog row for any key, and the initial
5 - //! snapshot swept the whole table. That is the same shape audiofiles was
6 - //! hardened away from (fuzz-2026-07-06 #2, -07-20 #1, -07-21 #3): a config key
7 - //! that names device state must never cross the sync boundary, and "sync
8 - //! everything" cannot hold one back.
3 + //! A config key that names device state must never cross the sync boundary, and
4 + //! a table that syncs every key unconditionally cannot hold one back. So the
5 + //! `user_config` triggers and the initial snapshot both filter on a declared
6 + //! posture rather than sweeping the table.
9 7 //!
10 8 //! [`CONFIG`] is the family's shared [`ConfigSpec`] for BB's `user_config`
11 9 //! table, declaring each known key's [`Posture`]. It drives two filters:
@@ -16,10 +14,10 @@
16 14 //! - the Rust import filter ([`key_excluded_from_sync`]), applied when a remote
17 15 //! pull batch is written back.
18 16 //!
19 - //! Storage stays sqlx (BB's `ConfigRepository`), not `synckit_config::ConfigStore`
20 - //! (rusqlite): this crate takes only the pure-data half of synckit-config, the
21 - //! spec and posture vocabulary, so a sqlx app shares the declaration without the
22 - //! rusqlite store. Posture is fail-closed: an undeclared key is
17 + //! Storage is BB's own `ConfigRepository`, not `synckit_config::ConfigStore`:
18 + //! this crate takes only the pure-data half of synckit-config, the spec and
19 + //! posture vocabulary, so an app keeps its own store and still shares the
20 + //! declaration. Posture is fail-closed: an undeclared key is
23 21 //! [`Local`](Posture::Local) and never syncs.
24 22 //!
25 23 //! <!-- wiki: bb-config -->
@@ -29,16 +27,16 @@
29 27 /// BB's `user_config` posture declaration.
30 28 ///
31 29 /// Only `theme` crosses the sync boundary. `bb-welcomed` is a per-device
32 - /// first-run flag (each device shows the welcome once). `bb-theme` is the legacy
33 - /// theme key read once to migrate the old value into `theme`; it must not sync
34 - /// on its own. Every other key the generic `set_config` command might write is
30 + /// first-run flag (each device shows the welcome once). `bb-theme` is an alias
31 + /// read once to carry its value into `theme`; it must not sync on its own.
32 + /// Every other key the generic `set_config` command might write is
35 33 /// undeclared and therefore `Local` (fail-closed).
36 34 pub const CONFIG: ConfigSpec = ConfigSpec::new(
37 35 "user_config",
38 36 &[
39 37 // Synced: user preferences carried across the user's devices.
40 38 ("theme", Posture::Synced),
41 - // Local: per-device state and the legacy alias, never synced.
39 + // Local: per-device state and the alias, never synced.
42 40 ("bb-welcomed", Posture::Local),
43 41 ("bb-theme", Posture::Local),
44 42 ],
@@ -18,9 +18,8 @@
18 18 impl DbError {
19 19 /// Whether this is rusqlite's "no rows returned" for a single-row query.
20 20 ///
21 - /// Stands in for the `sqlx::Error::RowNotFound` that callers used to match
22 - /// on. Repository methods that can legitimately find nothing return
23 - /// `Option` instead, so this is for the callers that go around them.
21 + /// Repository methods that can legitimately find nothing return `Option`
22 + /// instead, so this is for the callers that go around them.
24 23 #[must_use]
25 24 pub fn is_not_found(&self) -> bool {
26 25 matches!(self, Self::Sqlite(rusqlite::Error::QueryReturnedNoRows))
@@ -1,8 +1,7 @@
1 1 //! BalancedBreakfast Database Layer
2 2 //!
3 3 //! rusqlite behind an `r2d2` pool. The repositories are synchronous: SQLite has
4 - //! no async API, and the async this layer used to wear came from sqlx rather
5 - //! than from anything the app wanted. Async callers on a hot path should reach
4 + //! no async API. Async callers on a hot path should reach
6 5 //! them through `tokio::task::spawn_blocking`; for a local file on a desktop,
7 6 //! calling them inline is fine.
8 7
@@ -32,11 +31,11 @@
32 31
33 32 /// How long a connection waits for a write lock before giving up.
34 33 ///
35 - /// Matches the busy timeout sqlx applied by default. Without it rusqlite fails
36 - /// a contended write immediately instead of waiting for the other writer.
34 + /// Without it rusqlite fails a contended write immediately instead of waiting
35 + /// for the other writer.
37 36 const BUSY_TIMEOUT: Duration = Duration::from_secs(5);
38 37
39 - /// Pool size. Matches the sqlx pool this replaced.
38 + /// Pool size.
40 39 const MAX_CONNECTIONS: u32 = 16;
41 40
42 41 /// Where a pool's connections are opened.
@@ -52,10 +51,9 @@
52 51 /// Opens rusqlite connections for r2d2, applying the per-connection PRAGMAs.
53 52 ///
54 53 /// Every PRAGMA here is per-connection rather than per-database, so it runs on
55 - /// each connection the pool creates. `journal_mode` is the exception -- WAL is
56 - /// persistent in the file header -- but setting it repeatedly is harmless and
57 - /// keeps in-memory databases correct too. Both PRAGMAs match what sqlx applied
58 - /// by default, so an existing database sees no change of behaviour.
54 + /// each connection the pool creates. `journal_mode` is the exception (WAL is
55 + /// persistent in the file header), but setting it repeatedly is harmless and
56 + /// keeps in-memory databases correct too.
59 57 #[derive(Debug, Clone)]
60 58 pub struct SqliteConnectionManager {
61 59 source: Source,